file-browser: initial commit
This commit is contained in:
parent
3b1b0dd2e9
commit
bd70eac124
33
mopidy/files/__init__.py
Normal file
33
mopidy/files/__init__.py
Normal file
@ -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)
|
||||||
22
mopidy/files/backend.py
Normal file
22
mopidy/files/backend.py
Normal file
@ -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
|
||||||
8
mopidy/files/ext.conf
Normal file
8
mopidy/files/ext.conf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[files]
|
||||||
|
enabled = true
|
||||||
|
media_dir =
|
||||||
|
~/:Home
|
||||||
|
/data/music/music_data:Music
|
||||||
|
show_hidden = false
|
||||||
|
follow_symlinks = true
|
||||||
|
metadata_timeout = 1000
|
||||||
155
mopidy/files/library.py
Normal file
155
mopidy/files/library.py
Normal file
@ -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
|
||||||
@ -1,7 +1,6 @@
|
|||||||
[stream]
|
[stream]
|
||||||
enabled = true
|
enabled = true
|
||||||
protocols =
|
protocols =
|
||||||
file
|
|
||||||
http
|
http
|
||||||
https
|
https
|
||||||
mms
|
mms
|
||||||
|
|||||||
1
setup.py
1
setup.py
@ -36,6 +36,7 @@ setup(
|
|||||||
'mopidy.ext': [
|
'mopidy.ext': [
|
||||||
'http = mopidy.http:Extension',
|
'http = mopidy.http:Extension',
|
||||||
'local = mopidy.local:Extension',
|
'local = mopidy.local:Extension',
|
||||||
|
'files = mopidy.files:Extension',
|
||||||
'm3u = mopidy.m3u:Extension',
|
'm3u = mopidy.m3u:Extension',
|
||||||
'mpd = mopidy.mpd:Extension',
|
'mpd = mopidy.mpd:Extension',
|
||||||
'softwaremixer = mopidy.softwaremixer:Extension',
|
'softwaremixer = mopidy.softwaremixer:Extension',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user