From bd70eac12492a880284372acb0ad0edbcc7635c8 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Tue, 30 Jun 2015 18:26:28 +0200 Subject: [PATCH 01/17] 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', From 2c587edf7ade49d0bee358e46bc0b91e1a448fc2 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 1 Jul 2015 10:33:51 +0200 Subject: [PATCH 02/17] file-browser: Changed as discussed in PR 1207 --- mopidy/files/ext.conf | 3 +-- mopidy/files/library.py | 19 +++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/mopidy/files/ext.conf b/mopidy/files/ext.conf index 8068b528..59cc362d 100644 --- a/mopidy/files/ext.conf +++ b/mopidy/files/ext.conf @@ -1,8 +1,7 @@ [files] enabled = true media_dir = - ~/:Home - /data/music/music_data:Music + $XDG_MUSIC_DIR 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 index cad27638..0128cd85 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -17,6 +17,8 @@ logger = logging.getLogger(__name__) class FilesLibraryProvider(backend.LibraryProvider): """Library for browsing local files.""" + # TODO: get_images that can pull from metadata and/or .folder.png etc? + # TODO: handle playlists? @property def root_directory(self): @@ -32,7 +34,6 @@ class FilesLibraryProvider(backend.LibraryProvider): 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(':') @@ -48,15 +49,13 @@ class FilesLibraryProvider(backend.LibraryProvider): 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()): + def browse(self, uri): logger.debug(u'browse called with uri %s' % uri) - # import pdb; pdb.set_trace() result = [] localpath = path.uri_to_path(uri) if localpath == 'root': @@ -78,15 +77,10 @@ class FilesLibraryProvider(backend.LibraryProvider): 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')) + logger.warn(u'Ignored file: %s' % child.decode('ascii', + 'ignore')) pass result.sort(key=operator.attrgetter('name')) @@ -98,7 +92,6 @@ class FilesLibraryProvider(backend.LibraryProvider): 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( @@ -113,8 +106,6 @@ class FilesLibraryProvider(backend.LibraryProvider): 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: From 33511d4400e7a5e1e64aec967c79043e66b0eaf5 Mon Sep 17 00:00:00 2001 From: tom roth Date: Fri, 3 Jul 2015 10:44:31 +0200 Subject: [PATCH 03/17] file-browser: Don't rely on configured media dir to be available --- mopidy/files/ext.conf | 3 ++- mopidy/files/library.py | 39 +++++++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/mopidy/files/ext.conf b/mopidy/files/ext.conf index 59cc362d..9bc229ff 100644 --- a/mopidy/files/ext.conf +++ b/mopidy/files/ext.conf @@ -1,7 +1,8 @@ [files] enabled = true media_dir = - $XDG_MUSIC_DIR + $XDG_MUSIC_DIR:Music + ~/:Home 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 index 0128cd85..8bd04272 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -37,18 +37,29 @@ class FilesLibraryProvider(backend.LibraryProvider): 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) + try: + local_path = path.expand_path( + media_dict[0].encode(sys.getfilesystemencoding())) + except: + pass + if not local_path: + logger.warn('Could not expand path %s' % media_dict[0]) 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) + try: + st = os.stat(local_path) + except: + logger.warn('Could not open %s' % local_path) + continue + 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) self._follow_symlinks = config['files']['follow_symlinks'] self._show_hidden = config['files']['show_hidden'] self._scanner = scan.Scanner( @@ -81,7 +92,7 @@ class FilesLibraryProvider(backend.LibraryProvider): else: logger.warn(u'Ignored file: %s' % child.decode('ascii', 'ignore')) - pass + continue result.sort(key=operator.attrgetter('name')) return result @@ -118,11 +129,11 @@ class FilesLibraryProvider(backend.LibraryProvider): 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))) + 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) + logger.warning(u'Problem scanning %s: %s', uri, e) res = False return res From 307a879a901e801a48daf97f071b072b4adcff3e Mon Sep 17 00:00:00 2001 From: tom roth Date: Fri, 3 Jul 2015 13:34:26 +0200 Subject: [PATCH 04/17] file-browser: added some documentation --- docs/ext/backends.rst | 4 ++++ docs/ext/files.rst | 48 +++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 3 files changed, 53 insertions(+) create mode 100644 docs/ext/files.rst diff --git a/docs/ext/backends.rst b/docs/ext/backends.rst index 1ab00005..19f59806 100644 --- a/docs/ext/backends.rst +++ b/docs/ext/backends.rst @@ -65,6 +65,10 @@ https://github.com/tkem/mopidy-dleyna Provides a backend for playing music from Digital Media Servers using the `dLeyna `_ D-Bus interface. +Mopidy-Files +============ + +Bundled with Mopidy. See :ref:`ext-files`. Mopidy-Grooveshark ================== diff --git a/docs/ext/files.rst b/docs/ext/files.rst new file mode 100644 index 00000000..a4508689 --- /dev/null +++ b/docs/ext/files.rst @@ -0,0 +1,48 @@ +.. _ext-files: + +************ +Mopidy-Files +************ + +Mopidy-Files is an extension for playing music from your local music archive. +It is bundled with Mopidy and enabled by default. +It allows you to browse through your local file system. +Only files that are considered playable will be shown + +This backend handles URIs starting with ``file:``. + + +Configuration +============= + +See :ref:`config` for general help on configuring Mopidy. + +.. literalinclude:: ../../mopidy/files/ext.conf + :language: ini + +.. confval:: files/enabled + + If the files extension should be enabled or not. + +.. confval:: files/media_dir + + A list of directories to be browsable. + Each Directory path has to be written in a separate line. + Optionally the path can be followed by : and a name that will be shown for that path. + +.. confval:: files/show_hidden + + Whether to show hidden files and directories that start with a dot. + Default is false. + +.. confval:: files/follow_symlinks + + Whether to follow symbolic links found in :confval:`files/media_dir`. + Directories and files that are outside the configured media_dirs will not be shown. + Default is false + +.. confval:: files/metadata_timeout + + Number of milliseconds before giving up scanning a file and moving on to + the next file. Reducing the value might speed up the directory listing, + but can lead to some tracks not being shown. diff --git a/docs/index.rst b/docs/index.rst index 3a2998d5..8d621d26 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -96,6 +96,7 @@ Extensions :maxdepth: 2 ext/local + ext/files ext/m3u ext/stream ext/http From 1f3a4abab0a1eb3018dfa724425d4a6b858cabda Mon Sep 17 00:00:00 2001 From: tom roth Date: Mon, 6 Jul 2015 11:11:23 +0200 Subject: [PATCH 05/17] file-browser: Various changes as discussed in PR 1207 --- docs/ext/files.rst | 13 ++-- mopidy/files/__init__.py | 2 +- mopidy/files/ext.conf | 8 +-- mopidy/files/library.py | 126 +++++++++++++++++++-------------------- 4 files changed, 72 insertions(+), 77 deletions(-) diff --git a/docs/ext/files.rst b/docs/ext/files.rst index a4508689..c952ba14 100644 --- a/docs/ext/files.rst +++ b/docs/ext/files.rst @@ -7,7 +7,7 @@ Mopidy-Files Mopidy-Files is an extension for playing music from your local music archive. It is bundled with Mopidy and enabled by default. It allows you to browse through your local file system. -Only files that are considered playable will be shown +Only files that are considered playable will be shown. This backend handles URIs starting with ``file:``. @@ -27,10 +27,9 @@ See :ref:`config` for general help on configuring Mopidy. .. confval:: files/media_dir A list of directories to be browsable. - Each Directory path has to be written in a separate line. - Optionally the path can be followed by : and a name that will be shown for that path. + Optionally the path can be followed by | and a name that will be shown for that path. -.. confval:: files/show_hidden +.. confval:: files/show_dotfiles Whether to show hidden files and directories that start with a dot. Default is false. @@ -38,11 +37,11 @@ See :ref:`config` for general help on configuring Mopidy. .. confval:: files/follow_symlinks Whether to follow symbolic links found in :confval:`files/media_dir`. - Directories and files that are outside the configured media_dirs will not be shown. - Default is false + Directories and files that are outside the configured directories will not be shown. + Default is false. .. confval:: files/metadata_timeout Number of milliseconds before giving up scanning a file and moving on to the next file. Reducing the value might speed up the directory listing, - but can lead to some tracks not being shown. + but can lead to some tracks not being shown. Must be larger than 1000. diff --git a/mopidy/files/__init__.py b/mopidy/files/__init__.py index cd5e41c1..1e2e961a 100644 --- a/mopidy/files/__init__.py +++ b/mopidy/files/__init__.py @@ -22,7 +22,7 @@ class Extension(ext.Extension): 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['show_dotfiles'] = config.Boolean(optional=True) schema['follow_symlinks'] = config.Boolean(optional=True) schema['metadata_timeout'] = config.Integer( minimum=1000, maximum=1000 * 60 * 60, optional=True) diff --git a/mopidy/files/ext.conf b/mopidy/files/ext.conf index 9bc229ff..836db665 100644 --- a/mopidy/files/ext.conf +++ b/mopidy/files/ext.conf @@ -1,8 +1,8 @@ [files] enabled = true media_dir = - $XDG_MUSIC_DIR:Music - ~/:Home -show_hidden = false + $XDG_MUSIC_DIR|Music + ~/|Home +show_dotfiles = false follow_symlinks = true -metadata_timeout = 1000 \ No newline at end of file +metadata_timeout = 1000 diff --git a/mopidy/files/library.py b/mopidy/files/library.py index 8bd04272..09d898ae 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -7,7 +7,6 @@ import stat import sys import urllib2 - from mopidy import backend, exceptions, models from mopidy.audio import scan, utils from mopidy.internal import path @@ -33,75 +32,48 @@ class FilesLibraryProvider(backend.LibraryProvider): def __init__(self, backend, config): super(FilesLibraryProvider, self).__init__(backend) - self._media_dirs = [] - for entry in config['files']['media_dir']: - media_dir = {} - media_dict = entry.split(':') - try: - local_path = path.expand_path( - media_dict[0].encode(sys.getfilesystemencoding())) - except: - pass - if not local_path: - logger.warn('Could not expand path %s' % media_dict[0]) - continue - else: - try: - st = os.stat(local_path) - except: - logger.warn('Could not open %s' % local_path) - continue - 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) + self._media_dirs = list(self._get_media_dirs(config)) self._follow_symlinks = config['files']['follow_symlinks'] - self._show_hidden = config['files']['show_hidden'] + self._show_dotfiles = config['files']['show_dotfiles'] self._scanner = scan.Scanner( timeout=config['files']['metadata_timeout']) def browse(self, uri): - logger.debug(u'browse called with uri %s' % uri) + logger.debug('browse called with uri %s', uri) 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): - result.append(models.Ref.track(name=name, uri=uri)) - else: - logger.warn(u'Ignored file: %s' % child.decode('ascii', - 'ignore')) - continue - + return list(self._get_media_dirs_refs()) + 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(sys.getfilesystemencoding(), 'ignore') + if not self._show_dotfiles and name.startswith(b'.'): + continue + if self._follow_symlinks: + st = os.stat(child) + else: + st = os.lstat(child) + if 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): + result.append(models.Ref.track(name=name, uri=uri)) + else: + logger.warn('Ignored file: %s', + child.decode(sys.getfilesystemencoding(), + 'ignore')) + continue result.sort(key=operator.attrgetter('name')) return result def lookup(self, uri): - logger.debug(u'looking up uri = %s' % 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) + logger.warn(u'Not in basedir: %s', localpath) return [] try: result = self._scanner.scan(uri) @@ -110,13 +82,38 @@ class FilesLibraryProvider(backend.LibraryProvider): 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') + name = urllib2.unquote(filename).decode( + sys.getfilesystemencoding(), 'ignore') track = track.copy(name=name) return [track] + def _get_media_dirs(self, config): + for entry in config['files']['media_dir']: + media_dir = {} + media_dir_split = entry.split('|', 1) + local_path = path.expand_path( + media_dir_split[0].encode(sys.getfilesystemencoding())) + if not local_path: + logger.warn('Could not expand path %s', media_dir_split[0]) + continue + elif not os.path.isdir(local_path): + logger.warn('%s is not a directory', local_path) + continue + media_dir['path'] = local_path + if len(media_dir_split) == 2: + media_dir['name'] = media_dir_split[1] + else: + media_dir['name'] = media_dir_split[0].replace(os.sep, '+') + yield media_dir + + def _get_media_dirs_refs(self): + for media_dir in self._media_dirs: + yield models.Ref.directory( + name=media_dir['name'], + uri=path.path_to_uri(media_dir['path'])) + def _show_media_dirs(self): result = [] for media_dir in self._media_dirs: @@ -129,13 +126,12 @@ class FilesLibraryProvider(backend.LibraryProvider): 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 + logger.debug('got scan result playable: %s for %s', + result.uri, str(result.playable)) + return result.playable except exceptions.ScannerError as e: - logger.warning(u'Problem scanning %s: %s', uri, e) - res = False - return res + logger.warning('Problem scanning %s: %s', uri, e) + return False def _is_playlist(self, child): return os.path.splitext(child)[1] == '.m3u' @@ -153,5 +149,5 @@ class FilesLibraryProvider(backend.LibraryProvider): except: pass if not res: - logger.warn(u'%s not inside any basedir' % localpath) + logger.warn('%s not inside any basedir', localpath) return res From 759261d1d0cc36112735be922821b70d89524ef0 Mon Sep 17 00:00:00 2001 From: tom roth Date: Mon, 6 Jul 2015 15:01:32 +0200 Subject: [PATCH 06/17] file-browser: is_local_path_inside_base_dir checks on dirs to --- mopidy/files/library.py | 59 +++++++++++++++-------------------------- mopidy/internal/path.py | 24 ++++++++--------- 2 files changed, 33 insertions(+), 50 deletions(-) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index 09d898ae..bea8c062 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -24,8 +24,8 @@ class FilesLibraryProvider(backend.LibraryProvider): 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) + local_path = self._media_dirs[0]['path'] + uri = path.path_to_uri(local_path) else: uri = u'file:root' return models.Ref.directory(name='Files', uri=uri) @@ -41,14 +41,15 @@ class FilesLibraryProvider(backend.LibraryProvider): def browse(self, uri): logger.debug('browse called with uri %s', uri) result = [] - localpath = path.uri_to_path(uri) - if localpath == 'root': + local_path = path.uri_to_path(uri) + if local_path == 'root': return list(self._get_media_dirs_refs()) - 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) + for name in os.listdir(local_path): + if not self._is_in_basedir(local_path): + logger.warn(u'Not in base_dir: %s', local_path) + continue + child = os.path.join(local_path, name) + logger.debug('child: %s', child) uri = path.path_to_uri(child) name = name.decode(sys.getfilesystemencoding(), 'ignore') if not self._show_dotfiles and name.startswith(b'.'): @@ -59,7 +60,7 @@ class FilesLibraryProvider(backend.LibraryProvider): st = os.lstat(child) if 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): + elif stat.S_ISREG(st.st_mode) and self._is_audiofile(uri): result.append(models.Ref.track(name=name, uri=uri)) else: logger.warn('Ignored file: %s', @@ -71,9 +72,9 @@ class FilesLibraryProvider(backend.LibraryProvider): 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) + local_path = path.uri_to_path(uri) + if not self._is_in_basedir(local_path): + logger.warn(u'Not in base_dir: %s', local_path) return [] try: result = self._scanner.scan(uri) @@ -83,7 +84,7 @@ class FilesLibraryProvider(backend.LibraryProvider): logger.warning(u'Problem looking up %s: %s', uri, e) track = models.Track(uri=uri) if not track.name: - filename = os.path.basename(localpath) + filename = os.path.basename(local_path) name = urllib2.unquote(filename).decode( sys.getfilesystemencoding(), 'ignore') track = track.copy(name=name) @@ -114,16 +115,7 @@ class FilesLibraryProvider(backend.LibraryProvider): name=media_dir['name'], uri=path.path_to_uri(media_dir['path'])) - 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): + def _is_audiofile(self, uri): try: result = self._scanner.scan(uri) logger.debug('got scan result playable: %s for %s', @@ -133,21 +125,12 @@ class FilesLibraryProvider(backend.LibraryProvider): logger.warning('Problem scanning %s: %s', uri, e) return False - def _is_playlist(self, child): - return os.path.splitext(child)[1] == '.m3u' - - def _is_in_basedir(self, localpath): + def _is_in_basedir(self, local_path): res = False - basedirs = [mdir['path'] for mdir in self._media_dirs] - for basedir in basedirs: - if basedir == localpath: + base_dirs = [mdir['path'] for mdir in self._media_dirs] + for base_dir in base_dirs: + if path.is_local_path_inside_base_dir(local_path, base_dir): res = True - else: - try: - path.check_file_path_is_inside_base_dir(localpath, basedir) - res = True - except: - pass if not res: - logger.warn('%s not inside any basedir', localpath) + logger.warn('%s not inside any base_dir', local_path) return res diff --git a/mopidy/internal/path.py b/mopidy/internal/path.py index 3a41d930..9e642755 100644 --- a/mopidy/internal/path.py +++ b/mopidy/internal/path.py @@ -196,23 +196,23 @@ def find_mtimes(root, follow=False): return mtimes, errors -def check_file_path_is_inside_base_dir(file_path, base_path): - assert not file_path.endswith(os.sep), ( - 'File path %s cannot end with a path separator' % file_path) - +def is_local_path_inside_base_dir(local_path, base_path): + if local_path.endswith(os.sep): + raise ValueError('Local path %s cannot end with a path separator' + % local_path) # Expand symlinks real_base_path = os.path.realpath(base_path) - real_file_path = os.path.realpath(file_path) + real_local_path = os.path.realpath(local_path) - # Use dir of file for prefix comparision, so we don't accept - # /tmp/foo.m3u as being inside /tmp/foo, simply because they have a - # common prefix, /tmp/foo, which matches the base path, /tmp/foo. - real_dir_path = os.path.dirname(real_file_path) + if os.path.isfile(local_path): + # Use dir of file for prefix comparision, so we don't accept + # /tmp/foo.m3u as being inside /tmp/foo, simply because they have a + # common prefix, /tmp/foo, which matches the base path, /tmp/foo. + real_local_path = os.path.dirname(real_local_path) # Check if dir of file is the base path or a subdir - common_prefix = os.path.commonprefix([real_base_path, real_dir_path]) - assert common_prefix == real_base_path, ( - 'File path %s must be in %s' % (real_file_path, real_base_path)) + common_prefix = os.path.commonprefix([real_base_path, real_local_path]) + return common_prefix == real_base_path # FIXME replace with mock usage in tests. From 81af757b095398fa9f64271cccfa47822766f46d Mon Sep 17 00:00:00 2001 From: tom roth Date: Mon, 6 Jul 2015 15:23:05 +0200 Subject: [PATCH 07/17] file-browser: let the user decide on minimal scanner timeout --- docs/ext/files.rst | 2 +- mopidy/files/__init__.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/ext/files.rst b/docs/ext/files.rst index c952ba14..4a8e741e 100644 --- a/docs/ext/files.rst +++ b/docs/ext/files.rst @@ -44,4 +44,4 @@ See :ref:`config` for general help on configuring Mopidy. Number of milliseconds before giving up scanning a file and moving on to the next file. Reducing the value might speed up the directory listing, - but can lead to some tracks not being shown. Must be larger than 1000. + but can lead to some tracks not being shown. diff --git a/mopidy/files/__init__.py b/mopidy/files/__init__.py index 1e2e961a..90ebf7f8 100644 --- a/mopidy/files/__init__.py +++ b/mopidy/files/__init__.py @@ -24,8 +24,7 @@ class Extension(ext.Extension): schema['media_dir'] = config.List(optional=True) schema['show_dotfiles'] = config.Boolean(optional=True) schema['follow_symlinks'] = config.Boolean(optional=True) - schema['metadata_timeout'] = config.Integer( - minimum=1000, maximum=1000 * 60 * 60, optional=True) + schema['metadata_timeout'] = config.Integer(optional=True) return schema def setup(self, registry): From b51e2862d1ef885661454a8055e4e11ac2fa6bc8 Mon Sep 17 00:00:00 2001 From: tom roth Date: Mon, 6 Jul 2015 15:26:06 +0200 Subject: [PATCH 08/17] file-browser: let the user decide on minimal scanner timeout --- mopidy/internal/path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/internal/path.py b/mopidy/internal/path.py index 9e642755..7573dfd2 100644 --- a/mopidy/internal/path.py +++ b/mopidy/internal/path.py @@ -199,7 +199,7 @@ def find_mtimes(root, follow=False): def is_local_path_inside_base_dir(local_path, base_path): if local_path.endswith(os.sep): raise ValueError('Local path %s cannot end with a path separator' - % local_path) + % local_path) # Expand symlinks real_base_path = os.path.realpath(base_path) real_local_path = os.path.realpath(local_path) From d8e0099ff45a0ba93750dffd02284ee0e08df4d9 Mon Sep 17 00:00:00 2001 From: tom roth Date: Tue, 7 Jul 2015 08:01:15 +0200 Subject: [PATCH 09/17] file-browser: Changed as discussed in PR 1207 --- docs/ext/files.rst | 4 +-- mopidy/files/__init__.py | 2 +- mopidy/files/ext.conf | 4 +-- mopidy/files/library.py | 56 +++++++++++++++++++++------------------- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/docs/ext/files.rst b/docs/ext/files.rst index 4a8e741e..42120807 100644 --- a/docs/ext/files.rst +++ b/docs/ext/files.rst @@ -24,10 +24,10 @@ See :ref:`config` for general help on configuring Mopidy. If the files extension should be enabled or not. -.. confval:: files/media_dir +.. confval:: files/media_dirs A list of directories to be browsable. - Optionally the path can be followed by | and a name that will be shown for that path. + Optionally the path can be followed by ``|`` and a name that will be shown for that path. .. confval:: files/show_dotfiles diff --git a/mopidy/files/__init__.py b/mopidy/files/__init__.py index 90ebf7f8..d547b256 100644 --- a/mopidy/files/__init__.py +++ b/mopidy/files/__init__.py @@ -21,7 +21,7 @@ class Extension(ext.Extension): def get_config_schema(self): schema = super(Extension, self).get_config_schema() - schema['media_dir'] = config.List(optional=True) + schema['media_dirs'] = config.List(optional=True) schema['show_dotfiles'] = config.Boolean(optional=True) schema['follow_symlinks'] = config.Boolean(optional=True) schema['metadata_timeout'] = config.Integer(optional=True) diff --git a/mopidy/files/ext.conf b/mopidy/files/ext.conf index 836db665..afdd1183 100644 --- a/mopidy/files/ext.conf +++ b/mopidy/files/ext.conf @@ -1,8 +1,8 @@ [files] enabled = true -media_dir = +media_dirs = $XDG_MUSIC_DIR|Music ~/|Home show_dotfiles = false -follow_symlinks = true +follow_symlinks = false metadata_timeout = 1000 diff --git a/mopidy/files/library.py b/mopidy/files/library.py index bea8c062..47c58cda 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import logging import operator import os -import stat import sys import urllib2 @@ -27,7 +26,7 @@ class FilesLibraryProvider(backend.LibraryProvider): local_path = self._media_dirs[0]['path'] uri = path.path_to_uri(local_path) else: - uri = u'file:root' + uri = 'file:root' return models.Ref.directory(name='Files', uri=uri) def __init__(self, backend, config): @@ -39,49 +38,52 @@ class FilesLibraryProvider(backend.LibraryProvider): timeout=config['files']['metadata_timeout']) def browse(self, uri): - logger.debug('browse called with uri %s', uri) + logger.debug('Browsing files at: %s', uri) result = [] local_path = path.uri_to_path(uri) if local_path == 'root': return list(self._get_media_dirs_refs()) - for name in os.listdir(local_path): - if not self._is_in_basedir(local_path): - logger.warn(u'Not in base_dir: %s', local_path) + for dir_entry in os.listdir(local_path): + child_path = os.path.join(local_path, dir_entry) + uri = path.path_to_uri(child_path) + printable_path = child_path.decode(sys.getfilesystemencoding(), + 'ignore') + + if os.path.islink(child_path) and not self._follow_symlinks: + logger.debug('Ignoring symlink: %s', printable_path) continue - child = os.path.join(local_path, name) - logger.debug('child: %s', child) - uri = path.path_to_uri(child) - name = name.decode(sys.getfilesystemencoding(), 'ignore') - if not self._show_dotfiles and name.startswith(b'.'): + + if not self._is_in_basedir(os.path.realpath(child_path)): + logger.debug('Ignoring symlink to outside base dir: %s', + printable_path) continue - if self._follow_symlinks: - st = os.stat(child) - else: - st = os.lstat(child) - if stat.S_ISDIR(st.st_mode): - result.append(models.Ref.directory(name=name, uri=uri)) - elif stat.S_ISREG(st.st_mode) and self._is_audiofile(uri): - result.append(models.Ref.track(name=name, uri=uri)) - else: - logger.warn('Ignored file: %s', - child.decode(sys.getfilesystemencoding(), - 'ignore')) + + if not self._show_dotfiles and dir_entry.startswith(b'.'): continue + + if os.path.isdir(child_path): + result.append(models.Ref.directory(name=dir_entry, uri=uri)) + elif os.path.isfile(child_path): + if self._is_audiofile(uri): + result.append(models.Ref.track(name=dir_entry, uri=uri)) + else: + logger.debug('Ignoring non-audiofile: %s', printable_path) + result.sort(key=operator.attrgetter('name')) return result def lookup(self, uri): - logger.debug(u'looking up uri = %s', uri) + logger.debug('looking up uri = %s', uri) local_path = path.uri_to_path(uri) if not self._is_in_basedir(local_path): - logger.warn(u'Not in base_dir: %s', local_path) + logger.warn('Ignoring URI outside base dir: %s', local_path) return [] 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) + logger.warning('Problem looking up %s: %s', uri, e) track = models.Track(uri=uri) if not track.name: filename = os.path.basename(local_path) @@ -91,7 +93,7 @@ class FilesLibraryProvider(backend.LibraryProvider): return [track] def _get_media_dirs(self, config): - for entry in config['files']['media_dir']: + for entry in config['files']['media_dirs']: media_dir = {} media_dir_split = entry.split('|', 1) local_path = path.expand_path( From a8085cf29a15da2de694388d90df59e60ad01057 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 00:23:15 +0200 Subject: [PATCH 10/17] file-browser: changd local_path to path in internal#path --- mopidy/files/library.py | 2 +- mopidy/internal/path.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index 47c58cda..42465fab 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -131,7 +131,7 @@ class FilesLibraryProvider(backend.LibraryProvider): res = False base_dirs = [mdir['path'] for mdir in self._media_dirs] for base_dir in base_dirs: - if path.is_local_path_inside_base_dir(local_path, base_dir): + if path.is_path_inside_base_dir(local_path, base_dir): res = True if not res: logger.warn('%s not inside any base_dir', local_path) diff --git a/mopidy/internal/path.py b/mopidy/internal/path.py index 7573dfd2..f56520f0 100644 --- a/mopidy/internal/path.py +++ b/mopidy/internal/path.py @@ -196,22 +196,22 @@ def find_mtimes(root, follow=False): return mtimes, errors -def is_local_path_inside_base_dir(local_path, base_path): - if local_path.endswith(os.sep): - raise ValueError('Local path %s cannot end with a path separator' - % local_path) +def is_path_inside_base_dir(path, base_path): + if path.endswith(os.sep): + raise ValueError('Path %s cannot end with a path separator' + % path) # Expand symlinks real_base_path = os.path.realpath(base_path) - real_local_path = os.path.realpath(local_path) + real_path = os.path.realpath(path) - if os.path.isfile(local_path): + if os.path.isfile(path): # Use dir of file for prefix comparision, so we don't accept # /tmp/foo.m3u as being inside /tmp/foo, simply because they have a # common prefix, /tmp/foo, which matches the base path, /tmp/foo. - real_local_path = os.path.dirname(real_local_path) + real_path = os.path.dirname(real_path) # Check if dir of file is the base path or a subdir - common_prefix = os.path.commonprefix([real_base_path, real_local_path]) + common_prefix = os.path.commonprefix([real_base_path, real_path]) return common_prefix == real_base_path From ff14909fab579098f6f7acbb97ef99b0fcb3bc06 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 00:25:48 +0200 Subject: [PATCH 11/17] file-browser: decode Ref.track#name abd Ref.directory#name --- mopidy/files/library.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index 42465fab..92762f03 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -61,6 +61,7 @@ class FilesLibraryProvider(backend.LibraryProvider): if not self._show_dotfiles and dir_entry.startswith(b'.'): continue + dir_entry = dir_entry.decode(sys.getfilesystemencoding(), 'replace') if os.path.isdir(child_path): result.append(models.Ref.directory(name=dir_entry, uri=uri)) elif os.path.isfile(child_path): From 621796d8f81d3f82eda7a81a638c883387ce9cd3 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 00:42:22 +0200 Subject: [PATCH 12/17] file-browser: lint fixes --- mopidy/files/library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index 92762f03..0132542e 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -61,7 +61,8 @@ class FilesLibraryProvider(backend.LibraryProvider): if not self._show_dotfiles and dir_entry.startswith(b'.'): continue - dir_entry = dir_entry.decode(sys.getfilesystemencoding(), 'replace') + dir_entry = dir_entry.decode(sys.getfilesystemencoding(), + 'replace') if os.path.isdir(child_path): result.append(models.Ref.directory(name=dir_entry, uri=uri)) elif os.path.isfile(child_path): From 80887319954f0ad507483c59b24171f7b345678a Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 08:19:01 +0200 Subject: [PATCH 13/17] file-browser: Lower severity for logging scanner fail --- mopidy/files/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index 0132542e..e2a9ad81 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -126,7 +126,7 @@ class FilesLibraryProvider(backend.LibraryProvider): result.uri, str(result.playable)) return result.playable except exceptions.ScannerError as e: - logger.warning('Problem scanning %s: %s', uri, e) + logger.debug('Problem scanning %s: %s', uri, e) return False def _is_in_basedir(self, local_path): From 3b1a16dcce127dfad80604e2aee6db7f6bca3c42 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 08:24:49 +0200 Subject: [PATCH 14/17] file-browser: Changed Message for logging scanner fail --- mopidy/files/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index e2a9ad81..b030b5be 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -126,7 +126,7 @@ class FilesLibraryProvider(backend.LibraryProvider): result.uri, str(result.playable)) return result.playable except exceptions.ScannerError as e: - logger.debug('Problem scanning %s: %s', uri, e) + logger.debug('Could not scan %s: %s', uri, e) return False def _is_in_basedir(self, local_path): From 446a3082002925d7f2c23c78702685faf42a51c8 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 22:10:36 +0200 Subject: [PATCH 15/17] file-browser: Changed as discussed in PR 1207 --- mopidy/files/library.py | 46 ++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/mopidy/files/library.py b/mopidy/files/library.py index b030b5be..2a347f4b 100644 --- a/mopidy/files/library.py +++ b/mopidy/files/library.py @@ -11,7 +11,7 @@ from mopidy.audio import scan, utils from mopidy.internal import path logger = logging.getLogger(__name__) - +FS_ENCODING = sys.getfilesystemencoding() class FilesLibraryProvider(backend.LibraryProvider): """Library for browsing local files.""" @@ -43,11 +43,17 @@ class FilesLibraryProvider(backend.LibraryProvider): local_path = path.uri_to_path(uri) if local_path == 'root': return list(self._get_media_dirs_refs()) + if not self._is_in_basedir(os.path.realpath(local_path)): + logger.warning( + 'Rejected attempt to browse path (%s) outside dirs defined ' + 'in files/media_dirs config.', + local_path.decode(FS_ENCODING, 'replace')) + return [] for dir_entry in os.listdir(local_path): child_path = os.path.join(local_path, dir_entry) uri = path.path_to_uri(child_path) - printable_path = child_path.decode(sys.getfilesystemencoding(), - 'ignore') + printable_path = child_path.decode(FS_ENCODING, + 'replace') if os.path.islink(child_path) and not self._follow_symlinks: logger.debug('Ignoring symlink: %s', printable_path) @@ -61,7 +67,7 @@ class FilesLibraryProvider(backend.LibraryProvider): if not self._show_dotfiles and dir_entry.startswith(b'.'): continue - dir_entry = dir_entry.decode(sys.getfilesystemencoding(), + dir_entry = dir_entry.decode(FS_ENCODING, 'replace') if os.path.isdir(child_path): result.append(models.Ref.directory(name=dir_entry, uri=uri)) @@ -75,22 +81,22 @@ class FilesLibraryProvider(backend.LibraryProvider): return result def lookup(self, uri): - logger.debug('looking up uri = %s', uri) + logger.debug('Looking up file URI: %s', uri) local_path = path.uri_to_path(uri) if not self._is_in_basedir(local_path): - logger.warn('Ignoring URI outside base dir: %s', local_path) + logger.warning('Ignoring URI outside base dir: %s', local_path) return [] 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('Problem looking up %s: %s', uri, e) + logger.warning('Failed looking up %s: %s', uri, e) track = models.Track(uri=uri) if not track.name: filename = os.path.basename(local_path) name = urllib2.unquote(filename).decode( - sys.getfilesystemencoding(), 'ignore') + FS_ENCODING, 'replace') track = track.copy(name=name) return [track] @@ -99,9 +105,10 @@ class FilesLibraryProvider(backend.LibraryProvider): media_dir = {} media_dir_split = entry.split('|', 1) local_path = path.expand_path( - media_dir_split[0].encode(sys.getfilesystemencoding())) + media_dir_split[0].encode(FS_ENCODING)) if not local_path: - logger.warn('Could not expand path %s', media_dir_split[0]) + logger.warn('Failed expanding path (%s) from files/media_dirs config value.', + media_dir_split[0]) continue elif not os.path.isdir(local_path): logger.warn('%s is not a directory', local_path) @@ -110,6 +117,7 @@ class FilesLibraryProvider(backend.LibraryProvider): if len(media_dir_split) == 2: media_dir['name'] = media_dir_split[1] else: + # TODO Mpd client should accept / in dir name media_dir['name'] = media_dir_split[0].replace(os.sep, '+') yield media_dir @@ -122,19 +130,15 @@ class FilesLibraryProvider(backend.LibraryProvider): def _is_audiofile(self, uri): try: result = self._scanner.scan(uri) - logger.debug('got scan result playable: %s for %s', - result.uri, str(result.playable)) + logger.debug( + 'Scan indicates that file %s is %s.', + result.uri, result.playable and 'playable' or 'unplayable') return result.playable except exceptions.ScannerError as e: - logger.debug('Could not scan %s: %s', uri, e) + logger.debug("Failed scanning %s: %s", uri, e) return False def _is_in_basedir(self, local_path): - res = False - base_dirs = [mdir['path'] for mdir in self._media_dirs] - for base_dir in base_dirs: - if path.is_path_inside_base_dir(local_path, base_dir): - res = True - if not res: - logger.warn('%s not inside any base_dir', local_path) - return res + return any( + path.is_path_inside_base_dir(local_path, media_dir['path']) + for media_dir in self._media_dirs) From 07d4f6ddf2255432ae54b6df51ef52b62a0e32f4 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Wed, 8 Jul 2015 23:43:40 +0200 Subject: [PATCH 16/17] Rename Mopidy-Files to Mopidy-File --- docs/ext/backends.rst | 6 +++--- docs/ext/{files.rst => file.rst} | 20 ++++++++++---------- docs/index.rst | 2 +- mopidy/{files => file}/__init__.py | 4 ++-- mopidy/{files => file}/backend.py | 2 +- mopidy/{files => file}/ext.conf | 2 +- mopidy/{files => file}/library.py | 18 ++++++++++-------- setup.py | 2 +- 8 files changed, 29 insertions(+), 27 deletions(-) rename docs/ext/{files.rst => file.rst} (72%) rename mopidy/{files => file}/__init__.py (94%) rename mopidy/{files => file}/backend.py (94%) rename mopidy/{files => file}/ext.conf (94%) rename mopidy/{files => file}/library.py (90%) diff --git a/docs/ext/backends.rst b/docs/ext/backends.rst index 19f59806..5f578e6f 100644 --- a/docs/ext/backends.rst +++ b/docs/ext/backends.rst @@ -65,10 +65,10 @@ https://github.com/tkem/mopidy-dleyna Provides a backend for playing music from Digital Media Servers using the `dLeyna `_ D-Bus interface. -Mopidy-Files -============ +Mopidy-File +=========== -Bundled with Mopidy. See :ref:`ext-files`. +Bundled with Mopidy. See :ref:`ext-file`. Mopidy-Grooveshark ================== diff --git a/docs/ext/files.rst b/docs/ext/file.rst similarity index 72% rename from docs/ext/files.rst rename to docs/ext/file.rst index 42120807..d31f53fd 100644 --- a/docs/ext/files.rst +++ b/docs/ext/file.rst @@ -1,10 +1,10 @@ -.. _ext-files: +.. _ext-file: ************ -Mopidy-Files +Mopidy-File ************ -Mopidy-Files is an extension for playing music from your local music archive. +Mopidy-File is an extension for playing music from your local music archive. It is bundled with Mopidy and enabled by default. It allows you to browse through your local file system. Only files that are considered playable will be shown. @@ -17,30 +17,30 @@ Configuration See :ref:`config` for general help on configuring Mopidy. -.. literalinclude:: ../../mopidy/files/ext.conf +.. literalinclude:: ../../mopidy/file/ext.conf :language: ini -.. confval:: files/enabled +.. confval:: file/enabled - If the files extension should be enabled or not. + If the file extension should be enabled or not. -.. confval:: files/media_dirs +.. confval:: file/media_dirs A list of directories to be browsable. Optionally the path can be followed by ``|`` and a name that will be shown for that path. -.. confval:: files/show_dotfiles +.. confval:: file/show_dotfiles Whether to show hidden files and directories that start with a dot. Default is false. -.. confval:: files/follow_symlinks +.. confval:: file/follow_symlinks Whether to follow symbolic links found in :confval:`files/media_dir`. Directories and files that are outside the configured directories will not be shown. Default is false. -.. confval:: files/metadata_timeout +.. confval:: file/metadata_timeout Number of milliseconds before giving up scanning a file and moving on to the next file. Reducing the value might speed up the directory listing, diff --git a/docs/index.rst b/docs/index.rst index 8d621d26..9085024a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -96,7 +96,7 @@ Extensions :maxdepth: 2 ext/local - ext/files + ext/file ext/m3u ext/stream ext/http diff --git a/mopidy/files/__init__.py b/mopidy/file/__init__.py similarity index 94% rename from mopidy/files/__init__.py rename to mopidy/file/__init__.py index d547b256..089cf6e6 100644 --- a/mopidy/files/__init__.py +++ b/mopidy/file/__init__.py @@ -11,8 +11,8 @@ logger = logging.getLogger(__name__) class Extension(ext.Extension): - dist_name = 'Mopidy-Files' - ext_name = 'files' + dist_name = 'Mopidy-File' + ext_name = 'file' version = mopidy.__version__ def get_default_config(self): diff --git a/mopidy/files/backend.py b/mopidy/file/backend.py similarity index 94% rename from mopidy/files/backend.py rename to mopidy/file/backend.py index 2394881c..74b029e5 100644 --- a/mopidy/files/backend.py +++ b/mopidy/file/backend.py @@ -5,7 +5,7 @@ import logging import pykka from mopidy import backend -from mopidy.files import library +from mopidy.file import library logger = logging.getLogger(__name__) diff --git a/mopidy/files/ext.conf b/mopidy/file/ext.conf similarity index 94% rename from mopidy/files/ext.conf rename to mopidy/file/ext.conf index afdd1183..486619a1 100644 --- a/mopidy/files/ext.conf +++ b/mopidy/file/ext.conf @@ -1,4 +1,4 @@ -[files] +[file] enabled = true media_dirs = $XDG_MUSIC_DIR|Music diff --git a/mopidy/files/library.py b/mopidy/file/library.py similarity index 90% rename from mopidy/files/library.py rename to mopidy/file/library.py index 2a347f4b..d638d0f0 100644 --- a/mopidy/files/library.py +++ b/mopidy/file/library.py @@ -13,6 +13,7 @@ from mopidy.internal import path logger = logging.getLogger(__name__) FS_ENCODING = sys.getfilesystemencoding() + class FilesLibraryProvider(backend.LibraryProvider): """Library for browsing local files.""" # TODO: get_images that can pull from metadata and/or .folder.png etc? @@ -32,10 +33,10 @@ class FilesLibraryProvider(backend.LibraryProvider): def __init__(self, backend, config): super(FilesLibraryProvider, self).__init__(backend) self._media_dirs = list(self._get_media_dirs(config)) - self._follow_symlinks = config['files']['follow_symlinks'] - self._show_dotfiles = config['files']['show_dotfiles'] + self._follow_symlinks = config['file']['follow_symlinks'] + self._show_dotfiles = config['file']['show_dotfiles'] self._scanner = scan.Scanner( - timeout=config['files']['metadata_timeout']) + timeout=config['file']['metadata_timeout']) def browse(self, uri): logger.debug('Browsing files at: %s', uri) @@ -46,7 +47,7 @@ class FilesLibraryProvider(backend.LibraryProvider): if not self._is_in_basedir(os.path.realpath(local_path)): logger.warning( 'Rejected attempt to browse path (%s) outside dirs defined ' - 'in files/media_dirs config.', + 'in file/media_dirs config.', local_path.decode(FS_ENCODING, 'replace')) return [] for dir_entry in os.listdir(local_path): @@ -101,17 +102,18 @@ class FilesLibraryProvider(backend.LibraryProvider): return [track] def _get_media_dirs(self, config): - for entry in config['files']['media_dirs']: + for entry in config['file']['media_dirs']: media_dir = {} media_dir_split = entry.split('|', 1) local_path = path.expand_path( media_dir_split[0].encode(FS_ENCODING)) if not local_path: - logger.warn('Failed expanding path (%s) from files/media_dirs config value.', - media_dir_split[0]) + logger.warning('Failed expanding path (%s) from file/media_dirs' + 'config value.', + media_dir_split[0]) continue elif not os.path.isdir(local_path): - logger.warn('%s is not a directory', local_path) + logger.warning('%s is not a directory', local_path) continue media_dir['path'] = local_path if len(media_dir_split) == 2: diff --git a/setup.py b/setup.py index ec302548..ca121f74 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( 'mopidy.ext': [ 'http = mopidy.http:Extension', 'local = mopidy.local:Extension', - 'files = mopidy.files:Extension', + 'file = mopidy.file:Extension', 'm3u = mopidy.m3u:Extension', 'mpd = mopidy.mpd:Extension', 'softwaremixer = mopidy.softwaremixer:Extension', From 4e0c114ce3b5d9e985b188d014a7ffb10380d952 Mon Sep 17 00:00:00 2001 From: rawdlite Date: Thu, 9 Jul 2015 07:10:09 +0200 Subject: [PATCH 17/17] file-browser: lint fixed --- mopidy/file/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/file/library.py b/mopidy/file/library.py index d638d0f0..f9c4ad97 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -108,8 +108,8 @@ class FilesLibraryProvider(backend.LibraryProvider): local_path = path.expand_path( media_dir_split[0].encode(FS_ENCODING)) if not local_path: - logger.warning('Failed expanding path (%s) from file/media_dirs' - 'config value.', + logger.warning('Failed expanding path (%s) from' + 'file/media_dirs config value.', media_dir_split[0]) continue elif not os.path.isdir(local_path):