file-browser: Various changes as discussed in PR 1207
This commit is contained in:
parent
307a879a90
commit
1f3a4abab0
@ -7,7 +7,7 @@ Mopidy-Files
|
|||||||
Mopidy-Files is an extension for playing music from your local music archive.
|
Mopidy-Files is an extension for playing music from your local music archive.
|
||||||
It is bundled with Mopidy and enabled by default.
|
It is bundled with Mopidy and enabled by default.
|
||||||
It allows you to browse through your local file system.
|
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:``.
|
This backend handles URIs starting with ``file:``.
|
||||||
|
|
||||||
@ -27,10 +27,9 @@ See :ref:`config` for general help on configuring Mopidy.
|
|||||||
.. confval:: files/media_dir
|
.. confval:: files/media_dir
|
||||||
|
|
||||||
A list of directories to be browsable.
|
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.
|
Whether to show hidden files and directories that start with a dot.
|
||||||
Default is false.
|
Default is false.
|
||||||
@ -38,11 +37,11 @@ See :ref:`config` for general help on configuring Mopidy.
|
|||||||
.. confval:: files/follow_symlinks
|
.. confval:: files/follow_symlinks
|
||||||
|
|
||||||
Whether to follow symbolic links found in :confval:`files/media_dir`.
|
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.
|
Directories and files that are outside the configured directories will not be shown.
|
||||||
Default is false
|
Default is false.
|
||||||
|
|
||||||
.. confval:: files/metadata_timeout
|
.. confval:: files/metadata_timeout
|
||||||
|
|
||||||
Number of milliseconds before giving up scanning a file and moving on to
|
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,
|
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.
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class Extension(ext.Extension):
|
|||||||
def get_config_schema(self):
|
def get_config_schema(self):
|
||||||
schema = super(Extension, self).get_config_schema()
|
schema = super(Extension, self).get_config_schema()
|
||||||
schema['media_dir'] = config.List(optional=True)
|
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['follow_symlinks'] = config.Boolean(optional=True)
|
||||||
schema['metadata_timeout'] = config.Integer(
|
schema['metadata_timeout'] = config.Integer(
|
||||||
minimum=1000, maximum=1000 * 60 * 60, optional=True)
|
minimum=1000, maximum=1000 * 60 * 60, optional=True)
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
[files]
|
[files]
|
||||||
enabled = true
|
enabled = true
|
||||||
media_dir =
|
media_dir =
|
||||||
$XDG_MUSIC_DIR:Music
|
$XDG_MUSIC_DIR|Music
|
||||||
~/:Home
|
~/|Home
|
||||||
show_hidden = false
|
show_dotfiles = false
|
||||||
follow_symlinks = true
|
follow_symlinks = true
|
||||||
metadata_timeout = 1000
|
metadata_timeout = 1000
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import stat
|
|||||||
import sys
|
import sys
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
from mopidy import backend, exceptions, models
|
from mopidy import backend, exceptions, models
|
||||||
from mopidy.audio import scan, utils
|
from mopidy.audio import scan, utils
|
||||||
from mopidy.internal import path
|
from mopidy.internal import path
|
||||||
@ -33,75 +32,48 @@ class FilesLibraryProvider(backend.LibraryProvider):
|
|||||||
|
|
||||||
def __init__(self, backend, config):
|
def __init__(self, backend, config):
|
||||||
super(FilesLibraryProvider, self).__init__(backend)
|
super(FilesLibraryProvider, self).__init__(backend)
|
||||||
self._media_dirs = []
|
self._media_dirs = list(self._get_media_dirs(config))
|
||||||
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._follow_symlinks = config['files']['follow_symlinks']
|
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(
|
self._scanner = scan.Scanner(
|
||||||
timeout=config['files']['metadata_timeout'])
|
timeout=config['files']['metadata_timeout'])
|
||||||
|
|
||||||
def browse(self, uri):
|
def browse(self, uri):
|
||||||
logger.debug(u'browse called with uri %s' % uri)
|
logger.debug('browse called with uri %s', uri)
|
||||||
result = []
|
result = []
|
||||||
localpath = path.uri_to_path(uri)
|
localpath = path.uri_to_path(uri)
|
||||||
if localpath == 'root':
|
if localpath == 'root':
|
||||||
result = self._show_media_dirs()
|
return list(self._get_media_dirs_refs())
|
||||||
else:
|
if not self._is_in_basedir(localpath):
|
||||||
if not self._is_in_basedir(localpath):
|
logger.warn(u'Not in basedir: %s', localpath)
|
||||||
logger.warn(u'Not in basedir: %s' % localpath)
|
return []
|
||||||
return []
|
for name in os.listdir(localpath):
|
||||||
for name in os.listdir(localpath):
|
child = os.path.join(localpath, name)
|
||||||
child = os.path.join(localpath, name)
|
uri = path.path_to_uri(child)
|
||||||
uri = path.path_to_uri(child)
|
name = name.decode(sys.getfilesystemencoding(), 'ignore')
|
||||||
name = name.decode('ascii', 'ignore')
|
if not self._show_dotfiles and name.startswith(b'.'):
|
||||||
if self._follow_symlinks:
|
continue
|
||||||
st = os.stat(child)
|
if self._follow_symlinks:
|
||||||
else:
|
st = os.stat(child)
|
||||||
st = os.lstat(child)
|
else:
|
||||||
if not self._show_hidden and name.startswith(b'.'):
|
st = os.lstat(child)
|
||||||
continue
|
if stat.S_ISDIR(st.st_mode):
|
||||||
elif stat.S_ISDIR(st.st_mode):
|
result.append(models.Ref.directory(name=name, uri=uri))
|
||||||
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._check_audiofile(uri):
|
result.append(models.Ref.track(name=name, uri=uri))
|
||||||
result.append(models.Ref.track(name=name, uri=uri))
|
else:
|
||||||
else:
|
logger.warn('Ignored file: %s',
|
||||||
logger.warn(u'Ignored file: %s' % child.decode('ascii',
|
child.decode(sys.getfilesystemencoding(),
|
||||||
'ignore'))
|
'ignore'))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result.sort(key=operator.attrgetter('name'))
|
result.sort(key=operator.attrgetter('name'))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def lookup(self, uri):
|
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)
|
localpath = path.uri_to_path(uri)
|
||||||
if not self._is_in_basedir(localpath):
|
if not self._is_in_basedir(localpath):
|
||||||
logger.warn(u'Not in basedir: %s' % localpath)
|
logger.warn(u'Not in basedir: %s', localpath)
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
result = self._scanner.scan(uri)
|
result = self._scanner.scan(uri)
|
||||||
@ -110,13 +82,38 @@ class FilesLibraryProvider(backend.LibraryProvider):
|
|||||||
except exceptions.ScannerError as e:
|
except exceptions.ScannerError as e:
|
||||||
logger.warning(u'Problem looking up %s: %s', uri, e)
|
logger.warning(u'Problem looking up %s: %s', uri, e)
|
||||||
track = models.Track(uri=uri)
|
track = models.Track(uri=uri)
|
||||||
pass
|
|
||||||
if not track.name:
|
if not track.name:
|
||||||
filename = os.path.basename(localpath)
|
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)
|
track = track.copy(name=name)
|
||||||
return [track]
|
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):
|
def _show_media_dirs(self):
|
||||||
result = []
|
result = []
|
||||||
for media_dir in self._media_dirs:
|
for media_dir in self._media_dirs:
|
||||||
@ -129,13 +126,12 @@ class FilesLibraryProvider(backend.LibraryProvider):
|
|||||||
def _check_audiofile(self, uri):
|
def _check_audiofile(self, uri):
|
||||||
try:
|
try:
|
||||||
result = self._scanner.scan(uri)
|
result = self._scanner.scan(uri)
|
||||||
logger.debug(u'got scan result playable: %s for %s' %
|
logger.debug('got scan result playable: %s for %s',
|
||||||
(result.uri, str(result.playable)))
|
result.uri, str(result.playable))
|
||||||
res = result.playable
|
return result.playable
|
||||||
except exceptions.ScannerError as e:
|
except exceptions.ScannerError as e:
|
||||||
logger.warning(u'Problem scanning %s: %s', uri, e)
|
logger.warning('Problem scanning %s: %s', uri, e)
|
||||||
res = False
|
return False
|
||||||
return res
|
|
||||||
|
|
||||||
def _is_playlist(self, child):
|
def _is_playlist(self, child):
|
||||||
return os.path.splitext(child)[1] == '.m3u'
|
return os.path.splitext(child)[1] == '.m3u'
|
||||||
@ -153,5 +149,5 @@ class FilesLibraryProvider(backend.LibraryProvider):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
if not res:
|
if not res:
|
||||||
logger.warn(u'%s not inside any basedir' % localpath)
|
logger.warn('%s not inside any basedir', localpath)
|
||||||
return res
|
return res
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user