diff --git a/actions/minidlna b/actions/minidlna new file mode 100755 index 000000000..544fa4c7f --- /dev/null +++ b/actions/minidlna @@ -0,0 +1,106 @@ +#!/usr/bin/python3 +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Configuration actions for the minidlna server. +""" +import argparse +from tempfile import mkstemp +from shutil import move +from os import fdopen, remove + +import augeas + +from plinth.utils import grep + +config_path = '/etc/minidlna.conf' + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparsers.add_parser('setup', help='Setup SSH server') + + subparsers.add_parser('get-media-dir', help='Get media directory') + + set_media_dir = subparsers.add_parser('set-media-dir', + help='Set custom media directory') + set_media_dir.add_argument('--dir') + + subparsers.required = True + return parser.parse_args() + + +def subcommand_setup(arguments): + """ + Increase inotify watches per folder to allow minidlna to + monitor changes in large media-dirs. + """ + aug = augeas.Augeas( + flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.set('/augeas/load/Sysctl/lens', 'Sysctl.lns') + aug.set('/augeas/load/Sysctl/incl[last() + 1]', '/etc/sysctl.conf') + aug.load() + + aug.set('/files/etc/sysctl.conf/fs.inotify.max_user_watches', '100000') + aug.save() + + +def subcommand_get_media_dir(arguments): + """Retrieve media directory from minidlna.conf""" + line = grep('^media_dir=', config_path) + + print(line[0].split("=")[1]) + + +def subcommand_set_media_dir(arguments): + """Set media directory in minidlna.conf""" + line = grep('^media_dir=', config_path)[0] + + new_line = 'media_dir=%s\n' % arguments.dir + replace_in_config_file(config_path, line, new_line) + + +def replace_in_config_file(file_path, pattern, subst): + """ + Create a temporary minidlna.conf file, + replace the media dir config, + remove original one and move the temporary file. + """ + temp_file, temp_file_path = mkstemp() + with fdopen(temp_file, 'w') as new_file: + with open(file_path) as old_file: + for line in old_file: + new_file.write(line.replace(pattern, subst)) + + remove(file_path) + move(temp_file_path, file_path) + + +def main(): + """Parse arguments and perform all duties.""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) + + +if __name__ == '__main__': + main() diff --git a/plinth/modules/minidlna/__init__.py b/plinth/modules/minidlna/__init__.py new file mode 100644 index 000000000..48ea0bd29 --- /dev/null +++ b/plinth/modules/minidlna/__init__.py @@ -0,0 +1,116 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +FreedomBox app to configure minidlna. +""" +from django.utils.translation import ugettext_lazy as _ + +from plinth import actions +import plinth.app as app_module +from plinth import frontpage, menu +from plinth.modules.apache.components import Webserver +from plinth.modules.firewall.components import Firewall +from plinth.modules.users import register_group +from plinth.action_utils import diagnose_url + +from .manifest import backup, clients # noqa + +version = 1 + +name = 'minidlna' + +icon_name = name + +managed_packages = ['minidlna'] + +short_description = _('Simple Media Server') + +description = [ + _('MiniDLNA is a simple media server software, with the aim of being ' + 'fully compliant with DLNA/UPnP-AV clients. ' + 'The MiniDNLA daemon serves media files ' + '(music, pictures, and video) to clients on a network. ' + 'DNLA/UPnP is zero configuration protocol and is compliant ' + 'with any device passing the DLNA Certification like portable ' + 'media players, Smartphones, Televisions, and gaming systems (' + 'such as PS3 and Xbox 360) or applications such as totem and Kodi.') +] + +clients = clients + +group = ('minidlna', _('Media streaming server')) + +app = None + + +class MiniDLNAApp(app_module.App): + """Freedombox app managing miniDlna""" + app_id = 'minidlna' + + def __init__(self): + """Initialize the app components""" + super().__init__() + menu_item = menu.Menu( + 'menu-minidlna', + name=name, + short_description=short_description, + url_name='minidlna:index', + parent_url_name='apps', + icon=icon_name, + ) + firewall = Firewall('firewall-minidlna', name, ports=['minidlna'], + is_external=False) + webserver = Webserver('webserver-minidlna', 'minidlna-plinth') + shortcut = frontpage.Shortcut( + 'shortcut-minidlna', + name, + short_description=short_description, + description=description, + icon=icon_name, + url='/_minidlna/', + login_required=True, + ) + + self.add(menu_item) + self.add(webserver) + self.add(firewall) + self.add(shortcut) + + +def init(): + global app + app = MiniDLNAApp() + register_group(group) + + setup_helper = globals()['setup_helper'] + if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): + app.set_enabled(True) + + +def setup(helper, old_version=None): + """Install and configure the package""" + helper.install(managed_packages) + helper.call('post', actions.superuser_run, 'minidlna', ['setup']) + helper.call('post', app.enable) + + +def diagnose(): + """Check if the http page listening on 8200 is accessible""" + results = [] + results.append(diagnose_url('http://localhost:8200/')) + + return results diff --git a/plinth/modules/minidlna/data/etc/apache2/conf-available/minidlna-freedombox.conf b/plinth/modules/minidlna/data/etc/apache2/conf-available/minidlna-freedombox.conf new file mode 100644 index 000000000..544e0d862 --- /dev/null +++ b/plinth/modules/minidlna/data/etc/apache2/conf-available/minidlna-freedombox.conf @@ -0,0 +1,3 @@ + + ProxyPass http://localhost:8200/ + diff --git a/plinth/modules/minidlna/data/etc/plinth/modules-enabled/minidlna b/plinth/modules/minidlna/data/etc/plinth/modules-enabled/minidlna new file mode 100644 index 000000000..164cbbd65 --- /dev/null +++ b/plinth/modules/minidlna/data/etc/plinth/modules-enabled/minidlna @@ -0,0 +1 @@ +plinth.modules.minidlna diff --git a/plinth/modules/minidlna/forms.py b/plinth/modules/minidlna/forms.py new file mode 100644 index 000000000..a272a8264 --- /dev/null +++ b/plinth/modules/minidlna/forms.py @@ -0,0 +1,39 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +FreedomBox configuration form for MiniDLNA server. +""" + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from plinth.forms import AppForm + + +class MiniDLNAServerForm(AppForm): + """MiniDLNA server configuration form.""" + media_dir = forms.CharField( + label=_('Media Files Directory'), + help_text=_('Directory that MiniDLNA Server will read for content. All' + ' sub-directories of this will be also scanned for media ' + 'files. ' + 'If you change the default ensure that the new directory ' + 'exists and that is readable from the "minidlna" user. ' + 'Any user media directories ("/home/username/") will ' + 'usually work.'), + required=False, + ) diff --git a/plinth/modules/minidlna/manifest.py b/plinth/modules/minidlna/manifest.py new file mode 100644 index 000000000..24a0643ab --- /dev/null +++ b/plinth/modules/minidlna/manifest.py @@ -0,0 +1,138 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from django.utils.translation import ugettext_lazy as _ + +from plinth.modules.backups.api import validate as validate_backup +from plinth.clients import validate, store_url + +clients = validate([ + { + 'name': _('vlc'), + 'platforms': [ + { + 'type': 'package', + 'os': 'gnu-linux', + 'format': 'deb', + 'name': 'vlc', + }, + { + 'type': 'package', + 'os': 'gnu-linux', + 'format': 'rpm', + 'name': 'vlc', + }, + { + 'type': 'download', + 'os': 'windows', + 'url': 'https://www.videolan.org/vlc/download-windows.html', + }, + { + 'type': 'download', + 'os': 'macos', + 'url': 'https://www.videolan.org/vlc/download-macosx.html', + }, + { + 'type': 'store', + 'os': 'android', + 'store_name': 'google-play', + 'url': store_url('google-play', 'org.videolan.vlc') + }, + { + 'type': 'store', + 'os': 'android', + 'store_name': 'f-droid', + 'url': store_url('f-droid', 'org.videolan.vlc') + }, + ] + }, + { + 'name': _('kodi'), + 'platforms': [ + { + 'type': 'package', + 'os': 'gnu-linux', + 'format': 'deb', + 'name': 'kodi', + }, + { + 'type': 'package', + 'os': 'gnu-linux', + 'format': 'rpm', + 'name': 'kodi', + }, + { + 'type': 'download', + 'os': 'windows', + 'url': 'http://kodi.tv/download/', + }, + { + 'type': 'download', + 'os': 'macos', + 'url': 'http://kodi.tv/download/', + }, + { + 'type': 'store', + 'os': 'android', + 'store_name': 'google-play', + 'url': store_url('google-play', 'org.xbmc.kodi') + }, + { + 'type': 'store', + 'os': 'android', + 'store_name': 'f-droid', + 'url': store_url('f-droid', 'org.xbmc.kodi') + }, + ] + }, + { + 'name': _('yaacc'), + 'platforms': [ + { + 'type': 'store', + 'os': 'android', + 'store_name': 'f-droid', + 'url': store_url('f-droid', 'de.yaacc') + }, + ] + }, + { + 'name': _('totem'), + 'platforms': [ + { + 'type': 'package', + 'os': 'gnu-linux', + 'format': 'deb', + 'name': 'totem', + }, + { + 'type': 'package', + 'os': 'gnu-linux', + 'format': 'rpm', + 'name': 'totem', + }, + ] + }, +]) + +# TODO: get all media directories from config file +# for now hard code default media folder. +backup = validate_backup({ + 'data': { + 'directories': ['/var/lib/minidlna'] + } +}) diff --git a/plinth/modules/minidlna/tests/__init__.py b/plinth/modules/minidlna/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/modules/minidlna/urls.py b/plinth/modules/minidlna/urls.py new file mode 100644 index 000000000..6dd7527f2 --- /dev/null +++ b/plinth/modules/minidlna/urls.py @@ -0,0 +1,27 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +URLs for the minidlna Server module. +""" + +from django.conf.urls import url + +from plinth.modules.minidlna.views import MiniDLNAAppView + +urlpatterns = [ + url(r'^apps/minidlna/$', MiniDLNAAppView.as_view(), name='index'), +] diff --git a/plinth/modules/minidlna/views.py b/plinth/modules/minidlna/views.py new file mode 100644 index 000000000..ee3926f2c --- /dev/null +++ b/plinth/modules/minidlna/views.py @@ -0,0 +1,65 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Views for the minidlna module +""" +import os + +from django.contrib import messages +from django.utils.translation import ugettext_lazy as _ + +from plinth import actions +from plinth.views import AppView +from plinth.modules import minidlna + +from .forms import MiniDLNAServerForm + + +class MiniDLNAAppView(AppView): + app_id = 'minidlna' + name = minidlna.name + description = minidlna.description + form_class = MiniDLNAServerForm + diagnostics_module_name = 'minidlna' + + def get_initial(self): + """Initial form value as found in the minidlna.conf""" + initial = super().get_initial() + initial.update({ + 'media_dir': actions.superuser_run('minidlna', ['get-media-dir']), + }) + + return initial + + def form_valid(self, form): + """Apply changes from the form""" + old_config = form.initial + new_config = form.cleaned_data + + if old_config['media_dir'].strip() != new_config['media_dir']: + if os.path.isdir(new_config['media_dir']) is False: + messages.error(self.request, + _('Specified directory does not exist.')) + else: + actions.superuser_run( + 'minidlna', + ['set-media-dir', '--dir', new_config['media_dir']] + ) + actions.superuser_run('service', ['restart', 'minidlna']) + messages.success(self.request, _('Updated media directory')) + + return super().form_valid(form) diff --git a/static/themes/default/icons/minidlna.svg b/static/themes/default/icons/minidlna.svg new file mode 100644 index 000000000..7ae6aaed4 Binary files /dev/null and b/static/themes/default/icons/minidlna.svg differ