Merge branch 'v1.1.x' into develop
This commit is contained in:
commit
9033bebf1a
@ -256,7 +256,7 @@ chain. The function will be called with the error object as the only argument:
|
|||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
mopidy.playback.getCurrentTrack()
|
mopidy.playback.getCurrentTrack()
|
||||||
.catch(console.error.bind(console));
|
.catch(console.error.bind(console))
|
||||||
.done(printCurrentTrack);
|
.done(printCurrentTrack);
|
||||||
|
|
||||||
You can also register the error handler at the end of the promise chain by
|
You can also register the error handler at the end of the promise chain by
|
||||||
|
|||||||
@ -4,6 +4,65 @@ Changelog
|
|||||||
|
|
||||||
This changelog is used to track all major changes to Mopidy.
|
This changelog is used to track all major changes to Mopidy.
|
||||||
|
|
||||||
|
|
||||||
|
v1.1.1 (UNRELEASED)
|
||||||
|
===================
|
||||||
|
|
||||||
|
Bug fix release.
|
||||||
|
|
||||||
|
- Core: Make :meth:`mopidy.core.LibraryController.refresh` work for all
|
||||||
|
backends with a library provider. Previously, it wrongly worked for all
|
||||||
|
backends with a playlists provider. (Fixes: :issue:`1257`)
|
||||||
|
|
||||||
|
- Core: Respect :confval:`core/cache_dir` and :confval:`core/data_dir` config
|
||||||
|
values added in 1.1.0 when creating the dirs Mopidy need to store data. This
|
||||||
|
should not change the behavior for desktop users running Mopidy. When running
|
||||||
|
Mopidy as a system service installed from a package which sets the core dir
|
||||||
|
configs properly (e.g. Debian and Arch packages), this fix avoids the
|
||||||
|
creation of a couple of directories that should not be used, typically
|
||||||
|
:file:`/var/lib/mopidy/.local` and :file:`/var/lib/mopidy/.cache`. (Fixes:
|
||||||
|
:issue:`1259`, PR: :issue:`1266`)
|
||||||
|
|
||||||
|
- Local: Deprecate :confval:`local/data_dir` and respect
|
||||||
|
:confval:`core/data_dir` instead. This does not change the defaults for
|
||||||
|
desktop users, only system services installed from packages that properly set
|
||||||
|
:confval:`core/data_dir`, like the Debian and Arch packages. (Fixes:
|
||||||
|
:issue:`1259`, PR: :issue:`1266`)
|
||||||
|
|
||||||
|
- M3U: Changed default for the :confval:`m3u/playlists_dir` from
|
||||||
|
``$XDG_DATA_DIR/mopidy/m3u`` to unset, which now means the extension's data
|
||||||
|
dir. This does not change the defaults for desktop users, only system
|
||||||
|
services installed from packages that properly set :confval:`core/data_dir`,
|
||||||
|
like the Debian and Arch pakages. (Fixes: :issue:`1259`, PR: :issue:`1266`)
|
||||||
|
|
||||||
|
- Stream: If "file" is present in the :confval:`stream/protocols` config value
|
||||||
|
and the :ref:`ext-file` extension is enabled, we exited with an error because
|
||||||
|
two extensions claimed the same URI scheme. We now log a warning recommending
|
||||||
|
to remove "file" from the :confval:`stream/protocols` config, and then
|
||||||
|
proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`)
|
||||||
|
|
||||||
|
- Stream: Fix bug in new playlist parser. A non-ASCII char in an urilist
|
||||||
|
comment would cause a crash while parsing due to comparision of a non-ASCII
|
||||||
|
bytestring with a Unicode string. (Fixes: :issue:`1265`)
|
||||||
|
|
||||||
|
- File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real
|
||||||
|
path. This usually happens when running Mopidy as a system service, and thus
|
||||||
|
with a limited set of environment variables. (Fixes: :issue:`1249`, PR:
|
||||||
|
:issue:`1255`)
|
||||||
|
|
||||||
|
- File: When browsing files, we no longer scan the files to check if they're
|
||||||
|
playable. This makes browsing of the file hierarchy instant for HTTP clients,
|
||||||
|
which do no scanning of the files' metadata, and a bit faster for MPD
|
||||||
|
clients, which no longer scan the files twice. (Fixes: :issue:`1260`, PR:
|
||||||
|
:issue:`1261`)
|
||||||
|
|
||||||
|
- Audio: Fix timeout handling in scanner. This regression caused timeouts to
|
||||||
|
expire before it should, causing scans to fail.
|
||||||
|
|
||||||
|
- Audio: Update scanner to emit MIME type instead of an error when missing a
|
||||||
|
plugin.
|
||||||
|
|
||||||
|
|
||||||
v1.1.0 (2015-08-09)
|
v1.1.0 (2015-08-09)
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,13 @@ Core configuration
|
|||||||
|
|
||||||
Path to base directory for storing cached data.
|
Path to base directory for storing cached data.
|
||||||
|
|
||||||
|
Mopidy and extensions will use this path to cache data that can safely be
|
||||||
|
thrown away.
|
||||||
|
|
||||||
|
If your system is running from an SD card, it can help avoid wear and
|
||||||
|
corruption of your SD card by pointing this config to another location. If
|
||||||
|
you have enough RAM, a tmpfs might be a good choice.
|
||||||
|
|
||||||
When running Mopidy as a regular user, this should usually be
|
When running Mopidy as a regular user, this should usually be
|
||||||
``$XDG_CACHE_DIR/mopidy``, i.e. :file:`~/.cache/mopidy`.
|
``$XDG_CACHE_DIR/mopidy``, i.e. :file:`~/.cache/mopidy`.
|
||||||
|
|
||||||
@ -85,6 +92,11 @@ Core configuration
|
|||||||
|
|
||||||
Path to base directory for persistent data files.
|
Path to base directory for persistent data files.
|
||||||
|
|
||||||
|
Mopidy and extensions will use this path to store data that cannot be
|
||||||
|
be thrown away and reproduced without some effort. Examples include
|
||||||
|
Mopidy-Local's index of your media library and Mopidy-M3U's stored
|
||||||
|
playlists.
|
||||||
|
|
||||||
When running Mopidy as a regular user, this should usually be
|
When running Mopidy as a regular user, this should usually be
|
||||||
``$XDG_DATA_DIR/mopidy``, i.e. :file:`~/.local/share/mopidy`.
|
``$XDG_DATA_DIR/mopidy``, i.e. :file:`~/.local/share/mopidy`.
|
||||||
|
|
||||||
|
|||||||
@ -113,12 +113,17 @@ from a regular Mopidy setup you'll want to know about.
|
|||||||
|
|
||||||
sudo service mopidy status
|
sudo service mopidy status
|
||||||
|
|
||||||
- Mopidy installed from a Debian package can use both Mopidy extensions
|
- Mopidy installed from a Debian package can use Mopidy extensions installed
|
||||||
installed both from Debian packages and extensions installed with pip.
|
both from Debian packages and with pip. This has always been the case.
|
||||||
|
|
||||||
The other way around does not work: Mopidy installed with pip can use
|
Mopidy installed with pip can use extensions installed with pip, but
|
||||||
extensions installed with pip, but not extensions installed from a Debian
|
not extensions installed from a Debian package released before August 2015.
|
||||||
package. This is because the Debian packages install extensions into
|
This is because the Debian packages used to install extensions into
|
||||||
:file:`/usr/share/mopidy` which is normally not on your ``PYTHONPATH``.
|
:file:`/usr/share/mopidy` which is normally not on your ``PYTHONPATH``.
|
||||||
Thus, your pip-installed Mopidy will not find the Debian package-installed
|
Thus, your pip-installed Mopidy would not find the Debian package-installed
|
||||||
extensions.
|
extensions.
|
||||||
|
|
||||||
|
In August 2015, all Mopidy extension Debian packages was modified to install
|
||||||
|
into :file:`/usr/lib/python2.7/dist-packages`, like any other Python Debian
|
||||||
|
package. Thus, Mopidy installed with pip can now use extensions installed
|
||||||
|
from Debian.
|
||||||
|
|||||||
@ -47,8 +47,8 @@ active at a time.
|
|||||||
To create a new library provider you must create class that implements the
|
To create a new library provider you must create class that implements the
|
||||||
:class:`mopidy.local.Library` interface and install it in the extension
|
:class:`mopidy.local.Library` interface and install it in the extension
|
||||||
registry under ``local:library``. Any data that the library needs to store on
|
registry under ``local:library``. Any data that the library needs to store on
|
||||||
disc should be stored in :confval:`local/data_dir` using the library name as
|
disc should be stored in the extension's data dir, as returned by
|
||||||
part of the filename or directory to avoid any conflicts.
|
:meth:`~mopidy.ext.Extension.get_data_dir`.
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
|
|||||||
@ -52,4 +52,5 @@ See :ref:`config` for general help on configuring Mopidy.
|
|||||||
|
|
||||||
.. confval:: m3u/playlists_dir
|
.. confval:: m3u/playlists_dir
|
||||||
|
|
||||||
Path to directory with M3U files.
|
Path to directory with M3U files. Unset by default, in which case the
|
||||||
|
extension's data dir is used to store playlists.
|
||||||
|
|||||||
@ -75,15 +75,16 @@ def main():
|
|||||||
|
|
||||||
args = root_cmd.parse(mopidy_args)
|
args = root_cmd.parse(mopidy_args)
|
||||||
|
|
||||||
create_file_structures_and_config(args, extensions_data)
|
|
||||||
check_old_locations()
|
|
||||||
|
|
||||||
config, config_errors = config_lib.load(
|
config, config_errors = config_lib.load(
|
||||||
args.config_files,
|
args.config_files,
|
||||||
[d.config_schema for d in extensions_data],
|
[d.config_schema for d in extensions_data],
|
||||||
[d.config_defaults for d in extensions_data],
|
[d.config_defaults for d in extensions_data],
|
||||||
args.config_overrides)
|
args.config_overrides)
|
||||||
|
|
||||||
|
create_core_dirs(config)
|
||||||
|
create_initial_config_file(args, extensions_data)
|
||||||
|
check_old_locations()
|
||||||
|
|
||||||
verbosity_level = args.base_verbosity_level
|
verbosity_level = args.base_verbosity_level
|
||||||
if args.verbosity_level:
|
if args.verbosity_level:
|
||||||
verbosity_level += args.verbosity_level
|
verbosity_level += args.verbosity_level
|
||||||
@ -166,12 +167,17 @@ def main():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def create_file_structures_and_config(args, extensions_data):
|
def create_core_dirs(config):
|
||||||
path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy')
|
path.get_or_create_dir(config['core']['cache_dir'])
|
||||||
path.get_or_create_dir(b'$XDG_CONFIG_DIR/mopidy')
|
path.get_or_create_dir(config['core']['config_dir'])
|
||||||
|
path.get_or_create_dir(config['core']['data_dir'])
|
||||||
|
|
||||||
|
|
||||||
|
def create_initial_config_file(args, extensions_data):
|
||||||
|
"""Initialize whatever the last config file is with defaults"""
|
||||||
|
|
||||||
# Initialize whatever the last config file is with defaults
|
|
||||||
config_file = args.config_files[-1]
|
config_file = args.config_files[-1]
|
||||||
|
|
||||||
if os.path.exists(path.expand_path(config_file)):
|
if os.path.exists(path.expand_path(config_file)):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,6 @@ from mopidy import exceptions
|
|||||||
from mopidy.audio import utils
|
from mopidy.audio import utils
|
||||||
from mopidy.internal import encoding
|
from mopidy.internal import encoding
|
||||||
|
|
||||||
_missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description
|
|
||||||
|
|
||||||
_Result = collections.namedtuple(
|
_Result = collections.namedtuple(
|
||||||
'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable'))
|
'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable'))
|
||||||
|
|
||||||
@ -134,12 +132,12 @@ def _process(pipeline, timeout_ms):
|
|||||||
clock = pipeline.get_clock()
|
clock = pipeline.get_clock()
|
||||||
bus = pipeline.get_bus()
|
bus = pipeline.get_bus()
|
||||||
timeout = timeout_ms * gst.MSECOND
|
timeout = timeout_ms * gst.MSECOND
|
||||||
tags, mime, have_audio, missing_description = {}, None, False, None
|
tags, mime, have_audio, missing_message = {}, None, False, None
|
||||||
|
|
||||||
types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR
|
types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR
|
||||||
| gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG)
|
| gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG)
|
||||||
|
|
||||||
start = clock.get_time()
|
previous = clock.get_time()
|
||||||
while timeout > 0:
|
while timeout > 0:
|
||||||
message = bus.timed_pop_filtered(timeout, types)
|
message = bus.timed_pop_filtered(timeout, types)
|
||||||
|
|
||||||
@ -147,8 +145,7 @@ def _process(pipeline, timeout_ms):
|
|||||||
break
|
break
|
||||||
elif message.type == gst.MESSAGE_ELEMENT:
|
elif message.type == gst.MESSAGE_ELEMENT:
|
||||||
if gst.pbutils.is_missing_plugin_message(message):
|
if gst.pbutils.is_missing_plugin_message(message):
|
||||||
missing_description = encoding.locale_decode(
|
missing_message = message
|
||||||
_missing_plugin_desc(message))
|
|
||||||
elif message.type == gst.MESSAGE_APPLICATION:
|
elif message.type == gst.MESSAGE_APPLICATION:
|
||||||
if message.structure.get_name() == 'have-type':
|
if message.structure.get_name() == 'have-type':
|
||||||
mime = message.structure['caps'].get_name()
|
mime = message.structure['caps'].get_name()
|
||||||
@ -158,8 +155,10 @@ def _process(pipeline, timeout_ms):
|
|||||||
have_audio = True
|
have_audio = True
|
||||||
elif message.type == gst.MESSAGE_ERROR:
|
elif message.type == gst.MESSAGE_ERROR:
|
||||||
error = encoding.locale_decode(message.parse_error()[0])
|
error = encoding.locale_decode(message.parse_error()[0])
|
||||||
if missing_description:
|
if missing_message and not mime:
|
||||||
error = '%s (%s)' % (missing_description, error)
|
caps = missing_message.structure['detail']
|
||||||
|
mime = caps.get_structure(0).get_name()
|
||||||
|
return tags, mime, have_audio
|
||||||
raise exceptions.ScannerError(error)
|
raise exceptions.ScannerError(error)
|
||||||
elif message.type == gst.MESSAGE_EOS:
|
elif message.type == gst.MESSAGE_EOS:
|
||||||
return tags, mime, have_audio
|
return tags, mime, have_audio
|
||||||
@ -171,7 +170,9 @@ def _process(pipeline, timeout_ms):
|
|||||||
# Note that this will only keep the last tag.
|
# Note that this will only keep the last tag.
|
||||||
tags.update(utils.convert_taglist(taglist))
|
tags.update(utils.convert_taglist(taglist))
|
||||||
|
|
||||||
timeout -= clock.get_time() - start
|
now = clock.get_time()
|
||||||
|
timeout -= now - previous
|
||||||
|
previous = now
|
||||||
|
|
||||||
raise exceptions.ScannerError('Timeout after %dms' % timeout_ms)
|
raise exceptions.ScannerError('Timeout after %dms' % timeout_ms)
|
||||||
|
|
||||||
|
|||||||
@ -161,7 +161,7 @@ class Backends(list):
|
|||||||
|
|
||||||
for scheme in b.uri_schemes.get():
|
for scheme in b.uri_schemes.get():
|
||||||
assert scheme not in backends_by_scheme, (
|
assert scheme not in backends_by_scheme, (
|
||||||
'Cannot add URI scheme %s for %s, '
|
'Cannot add URI scheme "%s" for %s, '
|
||||||
'it is already handled by %s'
|
'it is already handled by %s'
|
||||||
) % (scheme, name(b), name(backends_by_scheme[scheme]))
|
) % (scheme, name(b), name(backends_by_scheme[scheme]))
|
||||||
backends_by_scheme[scheme] = b
|
backends_by_scheme[scheme] = b
|
||||||
|
|||||||
@ -255,7 +255,7 @@ class LibraryController(object):
|
|||||||
backends = {}
|
backends = {}
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme if uri else None
|
uri_scheme = urlparse.urlparse(uri).scheme if uri else None
|
||||||
|
|
||||||
for backend_scheme, backend in self.backends.with_playlists.items():
|
for backend_scheme, backend in self.backends.with_library.items():
|
||||||
backends.setdefault(backend, set()).add(backend_scheme)
|
backends.setdefault(backend, set()).add(backend_scheme)
|
||||||
|
|
||||||
for backend, backend_schemes in backends.items():
|
for backend, backend_schemes in backends.items():
|
||||||
|
|||||||
@ -63,6 +63,8 @@ class Extension(object):
|
|||||||
def get_cache_dir(self, config):
|
def get_cache_dir(self, config):
|
||||||
"""Get or create cache directory for the extension.
|
"""Get or create cache directory for the extension.
|
||||||
|
|
||||||
|
Use this directory to cache data that can safely be thrown away.
|
||||||
|
|
||||||
:param config: the Mopidy config object
|
:param config: the Mopidy config object
|
||||||
:return: string
|
:return: string
|
||||||
"""
|
"""
|
||||||
@ -87,6 +89,8 @@ class Extension(object):
|
|||||||
def get_data_dir(self, config):
|
def get_data_dir(self, config):
|
||||||
"""Get or create data directory for the extension.
|
"""Get or create data directory for the extension.
|
||||||
|
|
||||||
|
Use this directory to store data that should be persistent.
|
||||||
|
|
||||||
:param config: the Mopidy config object
|
:param config: the Mopidy config object
|
||||||
:returns: string
|
:returns: string
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -71,7 +71,7 @@ class FileLibraryProvider(backend.LibraryProvider):
|
|||||||
name = dir_entry.decode(FS_ENCODING, 'replace')
|
name = dir_entry.decode(FS_ENCODING, 'replace')
|
||||||
if os.path.isdir(child_path):
|
if os.path.isdir(child_path):
|
||||||
result.append(models.Ref.directory(name=name, uri=uri))
|
result.append(models.Ref.directory(name=name, uri=uri))
|
||||||
elif os.path.isfile(child_path) and self._is_audio_file(uri):
|
elif os.path.isfile(child_path):
|
||||||
result.append(models.Ref.track(name=name, uri=uri))
|
result.append(models.Ref.track(name=name, uri=uri))
|
||||||
|
|
||||||
result.sort(key=operator.attrgetter('name'))
|
result.sort(key=operator.attrgetter('name'))
|
||||||
@ -108,12 +108,15 @@ class FileLibraryProvider(backend.LibraryProvider):
|
|||||||
media_dir_split[0].encode(FS_ENCODING))
|
media_dir_split[0].encode(FS_ENCODING))
|
||||||
|
|
||||||
if not local_path:
|
if not local_path:
|
||||||
logger.warning('Failed expanding path (%s) from'
|
logger.debug(
|
||||||
'file/media_dirs config value.',
|
'Failed expanding path (%s) from file/media_dirs config '
|
||||||
media_dir_split[0])
|
'value.',
|
||||||
|
media_dir_split[0])
|
||||||
continue
|
continue
|
||||||
elif not os.path.isdir(local_path):
|
elif not os.path.isdir(local_path):
|
||||||
logger.warning('%s is not a directory', local_path)
|
logger.warning(
|
||||||
|
'%s is not a directory. Please create the directory or '
|
||||||
|
'update the file/media_dirs config value.', local_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
media_dir['path'] = local_path
|
media_dir['path'] = local_path
|
||||||
@ -131,18 +134,6 @@ class FileLibraryProvider(backend.LibraryProvider):
|
|||||||
name=media_dir['name'],
|
name=media_dir['name'],
|
||||||
uri=path.path_to_uri(media_dir['path']))
|
uri=path.path_to_uri(media_dir['path']))
|
||||||
|
|
||||||
def _is_audio_file(self, uri):
|
|
||||||
try:
|
|
||||||
result = self._scanner.scan(uri)
|
|
||||||
if result.playable:
|
|
||||||
logger.debug('Playable file: %s', result.uri)
|
|
||||||
else:
|
|
||||||
logger.debug('Unplayable file: %s (not audio)', result.uri)
|
|
||||||
return result.playable
|
|
||||||
except exceptions.ScannerError as e:
|
|
||||||
logger.debug('Unplayable file: %s (%s)', uri, e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _is_in_basedir(self, local_path):
|
def _is_in_basedir(self, local_path):
|
||||||
return any(
|
return any(
|
||||||
path.is_path_inside_base_dir(local_path, media_dir['path'])
|
path.is_path_inside_base_dir(local_path, media_dir['path'])
|
||||||
|
|||||||
@ -122,7 +122,7 @@ def parse_asx(data):
|
|||||||
def parse_urilist(data):
|
def parse_urilist(data):
|
||||||
result = []
|
result = []
|
||||||
for line in data.splitlines():
|
for line in data.splitlines():
|
||||||
if not line.strip() or line.startswith('#'):
|
if not line.strip() or line.startswith(b'#'):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
validation.check_uri(line)
|
validation.check_uri(line)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class Extension(ext.Extension):
|
|||||||
schema = super(Extension, self).get_config_schema()
|
schema = super(Extension, self).get_config_schema()
|
||||||
schema['library'] = config.String()
|
schema['library'] = config.String()
|
||||||
schema['media_dir'] = config.Path()
|
schema['media_dir'] = config.Path()
|
||||||
schema['data_dir'] = config.Path()
|
schema['data_dir'] = config.Deprecated()
|
||||||
schema['playlists_dir'] = config.Deprecated()
|
schema['playlists_dir'] = config.Deprecated()
|
||||||
schema['tag_cache_file'] = config.Deprecated()
|
schema['tag_cache_file'] = config.Deprecated()
|
||||||
schema['scan_timeout'] = config.Integer(
|
schema['scan_timeout'] = config.Integer(
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
enabled = true
|
enabled = true
|
||||||
library = json
|
library = json
|
||||||
media_dir = $XDG_MUSIC_DIR
|
media_dir = $XDG_MUSIC_DIR
|
||||||
data_dir = $XDG_DATA_DIR/mopidy/local
|
|
||||||
scan_timeout = 1000
|
scan_timeout = 1000
|
||||||
scan_flush_threshold = 1000
|
scan_flush_threshold = 1000
|
||||||
scan_follow_symlinks = false
|
scan_follow_symlinks = false
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import tempfile
|
|||||||
import mopidy
|
import mopidy
|
||||||
from mopidy import compat, local, models
|
from mopidy import compat, local, models
|
||||||
from mopidy.internal import encoding, timer
|
from mopidy.internal import encoding, timer
|
||||||
from mopidy.local import search, storage, translator
|
from mopidy.local import Extension, search, storage, translator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ class JsonLibrary(local.Library):
|
|||||||
self._browse_cache = None
|
self._browse_cache = None
|
||||||
self._media_dir = config['local']['media_dir']
|
self._media_dir = config['local']['media_dir']
|
||||||
self._json_file = os.path.join(
|
self._json_file = os.path.join(
|
||||||
config['local']['data_dir'], b'library.json.gz')
|
Extension().get_data_dir(config), b'library.json.gz')
|
||||||
|
|
||||||
storage.check_dirs_and_files(config)
|
storage.check_dirs_and_files(config)
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from mopidy.internal import encoding, path
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -13,10 +11,3 @@ def check_dirs_and_files(config):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
'Local media dir %s does not exist.' %
|
'Local media dir %s does not exist.' %
|
||||||
config['local']['media_dir'])
|
config['local']['media_dir'])
|
||||||
|
|
||||||
try:
|
|
||||||
path.get_or_create_dir(config['local']['data_dir'])
|
|
||||||
except EnvironmentError as error:
|
|
||||||
logger.warning(
|
|
||||||
'Could not create local data dir: %s',
|
|
||||||
encoding.locale_decode(error))
|
|
||||||
|
|||||||
@ -21,7 +21,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['playlists_dir'] = config.Path()
|
schema['playlists_dir'] = config.Path(optional=True)
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def setup(self, registry):
|
def setup(self, registry):
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import logging
|
|||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy import backend
|
from mopidy import backend, m3u
|
||||||
from mopidy.internal import encoding, path
|
from mopidy.internal import encoding, path
|
||||||
from mopidy.m3u.library import M3ULibraryProvider
|
from mopidy.m3u.library import M3ULibraryProvider
|
||||||
from mopidy.m3u.playlists import M3UPlaylistsProvider
|
from mopidy.m3u.playlists import M3UPlaylistsProvider
|
||||||
@ -21,12 +21,16 @@ class M3UBackend(pykka.ThreadingActor, backend.Backend):
|
|||||||
|
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
try:
|
if config['m3u']['playlists_dir'] is not None:
|
||||||
path.get_or_create_dir(config['m3u']['playlists_dir'])
|
self._playlists_dir = config['m3u']['playlists_dir']
|
||||||
except EnvironmentError as error:
|
try:
|
||||||
logger.warning(
|
path.get_or_create_dir(self._playlists_dir)
|
||||||
'Could not create M3U playlists dir: %s',
|
except EnvironmentError as error:
|
||||||
encoding.locale_decode(error))
|
logger.warning(
|
||||||
|
'Could not create M3U playlists dir: %s',
|
||||||
|
encoding.locale_decode(error))
|
||||||
|
else:
|
||||||
|
self._playlists_dir = m3u.Extension().get_data_dir(config)
|
||||||
|
|
||||||
self.playlists = M3UPlaylistsProvider(backend=self)
|
self.playlists = M3UPlaylistsProvider(backend=self)
|
||||||
self.library = M3ULibraryProvider(backend=self)
|
self.library = M3ULibraryProvider(backend=self)
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
[m3u]
|
[m3u]
|
||||||
enabled = true
|
enabled = true
|
||||||
playlists_dir = $XDG_DATA_DIR/mopidy/m3u
|
playlists_dir =
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(M3UPlaylistsProvider, self).__init__(*args, **kwargs)
|
super(M3UPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self._playlists_dir = self.backend._config['m3u']['playlists_dir']
|
self._playlists_dir = self.backend._playlists_dir
|
||||||
self._playlists = {}
|
self._playlists = {}
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
|
|||||||
@ -271,9 +271,9 @@ class MpdContext(object):
|
|||||||
|
|
||||||
If ``lookup`` is true and the ``path`` is to a track, the returned
|
If ``lookup`` is true and the ``path`` is to a track, the returned
|
||||||
``data`` is a future which will contain the results from looking up
|
``data`` is a future which will contain the results from looking up
|
||||||
the URI with :meth:`mopidy.core.LibraryController.lookup` If ``lookup``
|
the URI with :meth:`mopidy.core.LibraryController.lookup`. If
|
||||||
is false and the ``path`` is to a track, the returned ``data`` will be
|
``lookup`` is false and the ``path`` is to a track, the returned
|
||||||
a :class:`mopidy.models.Ref` for the track.
|
``data`` will be a :class:`mopidy.models.Ref` for the track.
|
||||||
|
|
||||||
For all entries that are not tracks, the returned ``data`` will be
|
For all entries that are not tracks, the returned ``data`` will be
|
||||||
:class:`None`.
|
:class:`None`.
|
||||||
|
|||||||
@ -36,6 +36,13 @@ class StreamBackend(pykka.ThreadingActor, backend.Backend):
|
|||||||
self.uri_schemes = audio_lib.supported_uri_schemes(
|
self.uri_schemes = audio_lib.supported_uri_schemes(
|
||||||
config['stream']['protocols'])
|
config['stream']['protocols'])
|
||||||
|
|
||||||
|
if 'file' in self.uri_schemes and config['file']['enabled']:
|
||||||
|
logger.warning(
|
||||||
|
'The stream/protocols config value includes the "file" '
|
||||||
|
'protocol. "file" playback is now handled by Mopidy-File. '
|
||||||
|
'Please remove it from the stream/protocols config.')
|
||||||
|
self.uri_schemes -= {'file'}
|
||||||
|
|
||||||
|
|
||||||
class StreamLibraryProvider(backend.LibraryProvider):
|
class StreamLibraryProvider(backend.LibraryProvider):
|
||||||
|
|
||||||
|
|||||||
@ -40,8 +40,11 @@ class ScannerTest(unittest.TestCase):
|
|||||||
self.assertEqual(self.result[name].tags[key], value)
|
self.assertEqual(self.result[name].tags[key], value)
|
||||||
|
|
||||||
def check_if_missing_plugin(self):
|
def check_if_missing_plugin(self):
|
||||||
if any(['missing a plug-in' in str(e) for e in self.errors.values()]):
|
for path, result in self.result.items():
|
||||||
raise unittest.SkipTest('Missing MP3 support?')
|
if not path.endswith('.mp3'):
|
||||||
|
continue
|
||||||
|
if not result.playable and result.mime == 'audio/mpeg':
|
||||||
|
raise unittest.SkipTest('Missing MP3 support?')
|
||||||
|
|
||||||
def test_tags_is_set(self):
|
def test_tags_is_set(self):
|
||||||
self.scan(self.find('scanner/simple'))
|
self.scan(self.find('scanner/simple'))
|
||||||
@ -109,6 +112,17 @@ class ScannerTest(unittest.TestCase):
|
|||||||
wav = path_to_data_dir('scanner/empty.wav')
|
wav = path_to_data_dir('scanner/empty.wav')
|
||||||
self.assertEqual(self.result[wav].duration, 0)
|
self.assertEqual(self.result[wav].duration, 0)
|
||||||
|
|
||||||
|
def test_uri_list(self):
|
||||||
|
path = path_to_data_dir('scanner/playlist.m3u')
|
||||||
|
self.scan([path])
|
||||||
|
self.assertEqual(self.result[path].mime, 'text/uri-list')
|
||||||
|
|
||||||
|
def test_text_plain(self):
|
||||||
|
# GStreamer decode bin hardcodes bad handling of text plain :/
|
||||||
|
path = path_to_data_dir('scanner/plain.txt')
|
||||||
|
self.scan([path])
|
||||||
|
self.assertIn(path, self.errors)
|
||||||
|
|
||||||
@unittest.SkipTest
|
@unittest.SkipTest
|
||||||
def test_song_without_time_is_handeled(self):
|
def test_song_without_time_is_handeled(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -37,7 +37,8 @@ class CoreActorTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaisesRegexp(
|
self.assertRaisesRegexp(
|
||||||
AssertionError,
|
AssertionError,
|
||||||
'Cannot add URI scheme dummy1 for B2, it is already handled by B1',
|
'Cannot add URI scheme "dummy1" for B2, '
|
||||||
|
'it is already handled by B1',
|
||||||
Core, mixer=None, backends=[self.backend1, self.backend2])
|
Core, mixer=None, backends=[self.backend1, self.backend2])
|
||||||
|
|
||||||
def test_version(self):
|
def test_version(self):
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
|||||||
self.library1.get_images.return_value.get.return_value = {}
|
self.library1.get_images.return_value.get.return_value = {}
|
||||||
self.library1.root_directory.get.return_value = dummy1_root
|
self.library1.root_directory.get.return_value = dummy1_root
|
||||||
self.backend1.library = self.library1
|
self.backend1.library = self.library1
|
||||||
|
self.backend1.has_playlists.return_value.get.return_value = False
|
||||||
|
|
||||||
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
||||||
self.backend2 = mock.Mock()
|
self.backend2 = mock.Mock()
|
||||||
@ -29,13 +30,14 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
|||||||
self.library2.get_images.return_value.get.return_value = {}
|
self.library2.get_images.return_value.get.return_value = {}
|
||||||
self.library2.root_directory.get.return_value = dummy2_root
|
self.library2.root_directory.get.return_value = dummy2_root
|
||||||
self.backend2.library = self.library2
|
self.backend2.library = self.library2
|
||||||
|
self.backend2.has_playlists.return_value.get.return_value = False
|
||||||
|
|
||||||
# A backend without the optional library provider
|
# A backend without the optional library provider
|
||||||
self.backend3 = mock.Mock()
|
self.backend3 = mock.Mock()
|
||||||
self.backend3.uri_schemes.get.return_value = ['dummy3']
|
self.backend3.uri_schemes.get.return_value = ['dummy3']
|
||||||
self.backend3.actor_ref.actor_class.__name__ = 'DummyBackend3'
|
self.backend3.actor_ref.actor_class.__name__ = 'DummyBackend3'
|
||||||
self.backend3.has_library().get.return_value = False
|
self.backend3.has_library.return_value.get.return_value = False
|
||||||
self.backend3.has_library_browse().get.return_value = False
|
self.backend3.has_library_browse.return_value.get.return_value = False
|
||||||
|
|
||||||
self.core = core.Core(mixer=None, backends=[
|
self.core = core.Core(mixer=None, backends=[
|
||||||
self.backend1, self.backend2, self.backend3])
|
self.backend1, self.backend2, self.backend3])
|
||||||
|
|||||||
1
tests/data/scanner/plain.txt
Normal file
1
tests/data/scanner/plain.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Some plain text file with nothing special in it.
|
||||||
1
tests/data/scanner/playlist.m3u
Normal file
1
tests/data/scanner/playlist.m3u
Normal file
@ -0,0 +1 @@
|
|||||||
|
http://example.com/
|
||||||
@ -23,7 +23,7 @@ file:///tmp/baz
|
|||||||
|
|
||||||
URILIST = b"""
|
URILIST = b"""
|
||||||
file:///tmp/foo
|
file:///tmp/foo
|
||||||
# a comment
|
# a comment \xc5\xa7\xc5\x95
|
||||||
file:///tmp/bar
|
file:///tmp/bar
|
||||||
|
|
||||||
file:///tmp/baz
|
file:///tmp/baz
|
||||||
|
|||||||
@ -45,10 +45,11 @@ class BrowseCacheTest(unittest.TestCase):
|
|||||||
class JsonLibraryTest(unittest.TestCase):
|
class JsonLibraryTest(unittest.TestCase):
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
'core': {
|
||||||
|
'data_dir': path_to_data_dir(''),
|
||||||
|
},
|
||||||
'local': {
|
'local': {
|
||||||
'media_dir': path_to_data_dir(''),
|
'media_dir': path_to_data_dir(''),
|
||||||
'data_dir': path_to_data_dir(''),
|
|
||||||
'playlists_dir': b'',
|
|
||||||
'library': 'json',
|
'library': 'json',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,10 +65,11 @@ class LocalLibraryProviderTest(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
'core': {
|
||||||
|
'data_dir': path_to_data_dir(''),
|
||||||
|
},
|
||||||
'local': {
|
'local': {
|
||||||
'media_dir': path_to_data_dir(''),
|
'media_dir': path_to_data_dir(''),
|
||||||
'data_dir': path_to_data_dir(''),
|
|
||||||
'playlists_dir': b'',
|
|
||||||
'library': 'json',
|
'library': 'json',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -105,11 +106,15 @@ class LocalLibraryProviderTest(unittest.TestCase):
|
|||||||
|
|
||||||
tmpdir = tempfile.mkdtemp()
|
tmpdir = tempfile.mkdtemp()
|
||||||
try:
|
try:
|
||||||
tmplib = os.path.join(tmpdir, 'library.json.gz')
|
tmpdir_local = os.path.join(tmpdir, 'local')
|
||||||
shutil.copy(path_to_data_dir('library.json.gz'), tmplib)
|
shutil.copytree(path_to_data_dir('local'), tmpdir_local)
|
||||||
|
|
||||||
config = {'local': self.config['local'].copy()}
|
config = {
|
||||||
config['local']['data_dir'] = tmpdir
|
'core': {
|
||||||
|
'data_dir': tmpdir,
|
||||||
|
},
|
||||||
|
'local': self.config['local'],
|
||||||
|
}
|
||||||
backend = actor.LocalBackend(config=config, audio=None)
|
backend = actor.LocalBackend(config=config, audio=None)
|
||||||
|
|
||||||
# Sanity check that value is in the library
|
# Sanity check that value is in the library
|
||||||
@ -117,6 +122,7 @@ class LocalLibraryProviderTest(unittest.TestCase):
|
|||||||
self.assertEqual(result, self.tracks[0:1])
|
self.assertEqual(result, self.tracks[0:1])
|
||||||
|
|
||||||
# Clear and refresh.
|
# Clear and refresh.
|
||||||
|
tmplib = os.path.join(tmpdir_local, 'library.json.gz')
|
||||||
open(tmplib, 'w').close()
|
open(tmplib, 'w').close()
|
||||||
backend.library.refresh()
|
backend.library.refresh()
|
||||||
|
|
||||||
|
|||||||
@ -23,12 +23,11 @@ from tests.local import generate_song, populate_tracklist
|
|||||||
class LocalPlaybackProviderTest(unittest.TestCase):
|
class LocalPlaybackProviderTest(unittest.TestCase):
|
||||||
config = {
|
config = {
|
||||||
'core': {
|
'core': {
|
||||||
|
'data_dir': path_to_data_dir(''),
|
||||||
'max_tracklist_length': 10000,
|
'max_tracklist_length': 10000,
|
||||||
},
|
},
|
||||||
'local': {
|
'local': {
|
||||||
'media_dir': path_to_data_dir(''),
|
'media_dir': path_to_data_dir(''),
|
||||||
'data_dir': path_to_data_dir(''),
|
|
||||||
'playlists_dir': b'',
|
|
||||||
'library': 'json',
|
'library': 'json',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,11 +18,11 @@ from tests.local import generate_song, populate_tracklist
|
|||||||
class LocalTracklistProviderTest(unittest.TestCase):
|
class LocalTracklistProviderTest(unittest.TestCase):
|
||||||
config = {
|
config = {
|
||||||
'core': {
|
'core': {
|
||||||
|
'data_dir': path_to_data_dir(''),
|
||||||
'max_tracklist_length': 10000
|
'max_tracklist_length': 10000
|
||||||
},
|
},
|
||||||
'local': {
|
'local': {
|
||||||
'media_dir': path_to_data_dir(''),
|
'media_dir': path_to_data_dir(''),
|
||||||
'data_dir': path_to_data_dir(''),
|
|
||||||
'playlists_dir': b'',
|
'playlists_dir': b'',
|
||||||
'library': 'json',
|
'library': 'json',
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user