From bd70eac12492a880284372acb0ad0edbcc7635c8 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Tue, 30 Jun 2015 18:26:28 +0200 Subject: [PATCH] file-browser: initial commit --- mopidy/files/__init__.py | 33 +++++++++ mopidy/files/backend.py | 22 ++++++ mopidy/files/ext.conf | 8 ++ mopidy/files/library.py | 155 +++++++++++++++++++++++++++++++++++++++ mopidy/stream/ext.conf | 1 - setup.py | 1 + 6 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 mopidy/files/__init__.py create mode 100644 mopidy/files/backend.py create mode 100644 mopidy/files/ext.conf create mode 100644 mopidy/files/library.py diff --git a/mopidy/files/__init__.py b/mopidy/files/__init__.py new file mode 100644 index 00000000..cd5e41c1 --- /dev/null +++ b/mopidy/files/__init__.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os + +import mopidy +from mopidy import config, ext + +logger = logging.getLogger(__name__) + + +class Extension(ext.Extension): + + dist_name = 'Mopidy-Files' + ext_name = 'files' + version = mopidy.__version__ + + def get_default_config(self): + conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') + return config.read(conf_file) + + def get_config_schema(self): + schema = super(Extension, self).get_config_schema() + schema['media_dir'] = config.List(optional=True) + schema['show_hidden'] = config.Boolean(optional=True) + schema['follow_symlinks'] = config.Boolean(optional=True) + schema['metadata_timeout'] = config.Integer( + minimum=1000, maximum=1000 * 60 * 60, optional=True) + return schema + + def setup(self, registry): + from .backend import FilesBackend + registry.add('backend', FilesBackend) diff --git a/mopidy/files/backend.py b/mopidy/files/backend.py new file mode 100644 index 00000000..2394881c --- /dev/null +++ b/mopidy/files/backend.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +import pykka + +from mopidy import backend +from mopidy.files import library + + +logger = logging.getLogger(__name__) + + +class FilesBackend(pykka.ThreadingActor, backend.Backend): + uri_schemes = ['file'] + + def __init__(self, config, audio): + super(FilesBackend, self).__init__() + self.library = library.FilesLibraryProvider(backend=self, + config=config) + self.playback = backend.PlaybackProvider(audio=audio, backend=self) + self.playlists = None diff --git a/mopidy/files/ext.conf b/mopidy/files/ext.conf new file mode 100644 index 00000000..8068b528 --- /dev/null +++ b/mopidy/files/ext.conf @@ -0,0 +1,8 @@ +[files] +enabled = true +media_dir = + ~/:Home + /data/music/music_data:Music +show_hidden = false +follow_symlinks = true +metadata_timeout = 1000 \ No newline at end of file diff --git a/mopidy/files/library.py b/mopidy/files/library.py new file mode 100644 index 00000000..cad27638 --- /dev/null +++ b/mopidy/files/library.py @@ -0,0 +1,155 @@ +from __future__ import unicode_literals + +import logging +import operator +import os +import stat +import sys +import urllib2 + + +from mopidy import backend, exceptions, models +from mopidy.audio import scan, utils +from mopidy.internal import path + +logger = logging.getLogger(__name__) + + +class FilesLibraryProvider(backend.LibraryProvider): + """Library for browsing local files.""" + + @property + def root_directory(self): + if not self._media_dirs: + return None + elif len(self._media_dirs) == 1: + localpath = self._media_dirs[0]['path'] + uri = path.path_to_uri(localpath) + else: + uri = u'file:root' + return models.Ref.directory(name='Files', uri=uri) + + def __init__(self, backend, config): + super(FilesLibraryProvider, self).__init__(backend) + self._media_dirs = [] + # import pdb; pdb.set_trace() + for entry in config['files']['media_dir']: + media_dir = {} + media_dict = entry.split(':') + local_path = path.expand_path( + media_dict[0].encode(sys.getfilesystemencoding())) + st = os.stat(local_path) + if not stat.S_ISDIR(st.st_mode): + logger.warn(u'%s is not a directory' % local_path) + continue + media_dir['path'] = local_path + if len(media_dict) == 2: + media_dir['name'] = media_dict[1] + else: + media_dir['name'] = media_dict[0].replace(os.sep, '+') + self._media_dirs.append(media_dir) + logger.debug(self._media_dirs) + self._follow_symlinks = config['files']['follow_symlinks'] + self._show_hidden = config['files']['show_hidden'] + self._scanner = scan.Scanner( + timeout=config['files']['metadata_timeout']) + + def browse(self, uri, encoding=sys.getfilesystemencoding()): + logger.debug(u'browse called with uri %s' % uri) + # import pdb; pdb.set_trace() + result = [] + localpath = path.uri_to_path(uri) + if localpath == 'root': + result = self._show_media_dirs() + else: + if not self._is_in_basedir(localpath): + logger.warn(u'Not in basedir: %s' % localpath) + return [] + for name in os.listdir(localpath): + child = os.path.join(localpath, name) + uri = path.path_to_uri(child) + name = name.decode('ascii', 'ignore') + if self._follow_symlinks: + st = os.stat(child) + else: + st = os.lstat(child) + if not self._show_hidden and name.startswith(b'.'): + continue + elif stat.S_ISDIR(st.st_mode): + result.append(models.Ref.directory(name=name, uri=uri)) + elif stat.S_ISREG(st.st_mode) and self._check_audiofile(uri): + # if self._is_playlist(child): + # result.append(models.Ref.playlist( + # name=name, + # uri='m3u:%s' % child)) + # else: + result.append(models.Ref.track(name=name, uri=uri)) + else: + logger.warn(u'Ignored file: %s' % child.decode(encoding, + 'replace')) + pass + + result.sort(key=operator.attrgetter('name')) + return result + + def lookup(self, uri): + logger.debug(u'looking up uri = %s' % uri) + localpath = path.uri_to_path(uri) + if not self._is_in_basedir(localpath): + logger.warn(u'Not in basedir: %s' % localpath) + return [] + # import pdb; pdb.set_trace() + try: + result = self._scanner.scan(uri) + track = utils.convert_tags_to_track(result.tags).copy( + uri=uri, length=result.duration) + except exceptions.ScannerError as e: + logger.warning(u'Problem looking up %s: %s', uri, e) + track = models.Track(uri=uri) + pass + if not track.name: + filename = os.path.basename(localpath) + name = urllib2.unquote(filename).decode('ascii', 'ignore') + track = track.copy(name=name) + return [track] + + # TODO: get_images that can pull from metadata and/or .folder.png etc? + + def _show_media_dirs(self): + result = [] + for media_dir in self._media_dirs: + dir = models.Ref.directory( + name=media_dir['name'], + uri=path.path_to_uri(media_dir['path'])) + result.append(dir) + return result + + def _check_audiofile(self, uri): + try: + result = self._scanner.scan(uri) + logger.debug(u'got scan result playable: %s for %s' % ( + result.uri, str(result.playable))) + res = result.playable + except exceptions.ScannerError as e: + logger.warning(u'Problem looking up %s: %s', uri, e) + res = False + return res + + def _is_playlist(self, child): + return os.path.splitext(child)[1] == '.m3u' + + def _is_in_basedir(self, localpath): + res = False + basedirs = [mdir['path'] for mdir in self._media_dirs] + for basedir in basedirs: + if basedir == localpath: + res = True + else: + try: + path.check_file_path_is_inside_base_dir(localpath, basedir) + res = True + except: + pass + if not res: + logger.warn(u'%s not inside any basedir' % localpath) + return res diff --git a/mopidy/stream/ext.conf b/mopidy/stream/ext.conf index cedb3085..928ccc63 100644 --- a/mopidy/stream/ext.conf +++ b/mopidy/stream/ext.conf @@ -1,7 +1,6 @@ [stream] enabled = true protocols = - file http https mms diff --git a/setup.py b/setup.py index 9f33236f..ec302548 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ setup( 'mopidy.ext': [ 'http = mopidy.http:Extension', 'local = mopidy.local:Extension', + 'files = mopidy.files:Extension', 'm3u = mopidy.m3u:Extension', 'mpd = mopidy.mpd:Extension', 'softwaremixer = mopidy.softwaremixer:Extension',