Merge branch 'develop' into feature/taglist-converter

Conflicts:
	docs/changelog.rst
This commit is contained in:
Thomas Adamcik 2014-12-25 22:46:44 +01:00
commit 656f7e976f
21 changed files with 230 additions and 61 deletions

View File

@ -16,4 +16,4 @@ Janez Troha <janez.troha@gmail.com> <dz0ny@ubuntu.si>
Luke Giuliani <luke@giuliani.com.au>
Colin Montgomerie <kiteflyingmonkey@gmail.com>
Ignasi Fosch <natx@y10k.ws> <ifosch@serenity-2.local>
Christopher Schirner <schinken@hackerspace-bamberg.de>
Christopher Schirner <christopher@hackerspace-bamberg.de> <schinken@hackerspace-bamberg.de>

View File

@ -42,7 +42,8 @@
- Sam Willcocks <sam@wlcx.cc>
- Ignasi Fosch <natx@y10k.ws>
- Arjun Naik <arjun@arjunnaik.in>
- Christopher Schirner <schinken@hackerspace-bamberg.de>
- Christopher Schirner <christopher@hackerspace-bamberg.de>
- Dmitry Sandalov <dmitry@sandalov.org>
- Lukas Vogel <lukas@vogelnest.org>
- Thomas Amland <thomas.amland@gmail.com>
- Deni Bertovic <deni@kset.org>

View File

@ -39,8 +39,6 @@ v0.20.0 (UNRELEASED)
- Add symlink support with loop protection to file finder (Fixes: :issue:`858`,
PR: :isusue:`874`)
- Fix scanning of modified files. (PR: :issue:`904`)
**MPD frontend**
- In stored playlist names, replace "/", which are illegal, with "|" instead of
@ -49,15 +47,11 @@ v0.20.0 (UNRELEASED)
- Enable browsing of artist references, in addition to albums and playlists.
(PR: :issue:`884`)
- Re-enable browsing of empty directories. (PR: :issue:`906`)
- Quick workaround for :issue:`881`, which allows for newlines in comments.
(PR: :issue:`882`)
**Audio**
- Deprecated :meth:`mopidy.audio.Audio.emit_end_of_stream`. Pass a
:class:`None` buffer to :meth:`mopidy.audio.Audio.emit_data` end the stream.
:class:`None` buffer to :meth:`mopidy.audio.Audio.emit_data` to end the
stream.
- Internal code cleanup within audio subsystem:
@ -94,16 +88,26 @@ v0.20.0 (UNRELEASED)
make sense for a server such as Mopidy. Currently the only way to find out if
it is in use and will be missed is to go ahead and remove it.
**Stream backend**
- Add basic tests for the stream library provider.
v0.19.5 (UNRELEASED)
v0.19.5 (2014-12-23)
====================
Bug fix release.
Today is Mopidy's five year anniversary. We're celebrating with a bugfix
release and are looking forward to the next five years!
- Config: Support UTF-8 in extension's default config. If an extension with
non-ASCII characters in its default config was installed, and Mopidy didn't
already have a config file, Mopidy would crashed when trying to create the
initial config file based on the default config of all available extensions.
(Fixes: :discuss:`428`)
- Extensions: Fix crash when unpacking data from
:exc:`pkg_resources.VersionConflict` created with a single argument. (Fixes:
:issue:`911`)
- Models: Hide empty collections from :func:`repr()` representations.
@ -117,6 +121,20 @@ Bug fix release.
:attr:`mopidy.models.Track.track_no`, and
:attr:`mopidy.models.Track.last_modified` from ``0`` to :class:`None`.
- Core: When skipping to the next track in consume mode, remove the skipped
track from the tracklist. This is consistent with the original MPD server's
behavior. (Fixes: :issue:`902`)
- Local: Fix scanning of modified files. (PR: :issue:`904`)
- MPD: Re-enable browsing of empty directories. (PR: :issue:`906`)
- MPD: Remove track comments from responses. They are not included by the
original MPD server, and this works around :issue:`881`. (PR: :issue:`882`)
- HTTP: Errors while starting HTTP apps are logged instead of crashing the HTTP
server. (Fixes: :issue:`875`)
v0.19.4 (2014-09-01)
====================

View File

@ -18,7 +18,7 @@ DLNA Digital Media Server (DMS) / UPnP AV MediaServer:
A MediaServer provides a library of media and is capable of streaming that
media to a MediaRenderer. If Mopidy was a MediaServer, you could browse and
play Mopidy's music on a TV, smartphone, or tablet supporting UPnP. Mopidy
does not currently support this, but we may in the future. :issue:`52` is
does not currently support this, but we may in the future. :issue:`52` is
the relevant wishlist issue.
DLNA Digital Media Renderer (DMR) / UPnP AV MediaRenderer:
@ -27,13 +27,51 @@ DLNA Digital Media Renderer (DMR) / UPnP AV MediaRenderer:
given media, typically served by a MediaServer. If Mopidy was a
MediaRenderer, you could use e.g. your smartphone or tablet to make Mopidy
play media. Mopidy *does already* have experimental support for being a
MediaRenderer with the help of Rygel, as you can read more about below.
MediaRenderer, as you can read more about below.
Mopidy as an UPnP MediaRenderer
===============================
There are two ways Mopidy can be made available as an UPnP MediaRenderer:
Using Mopidy-MPRIS and Rygel, or using Mopidy-MPD and upmpdcli.
.. _upmpdcli:
upmpdcli
--------
`upmpdcli <http://www.lesbonscomptes.com/upmpdcli/>`_ is recommended, since it
is easier to setup, and offers `OpenHome <http://www.openhome.org> ohMedia`_
compatibility. upmpdcli exposes a UPnP MediaRenderer to the network, while
using the MPD protocol to control Mopidy.
1. Install upmpdcli. On Debian/Ubuntu::
apt-get install upmpdcli
Alternatively, follow the instructions from the upmpdcli website.
2. The default settings of upmpdcli will work with the default settings of
:ref:`ext-mpd`. Edit :file:`/etc/upmpdcli.conf` if you want to use different
ports, hosts, or other settings.
3. Start upmpdcli using the command::
upmpdcli
Or, run it in the background as a service::
sudo service upmpdcli start
4. A UPnP renderer should be available now.
.. _rygel:
How to make Mopidy available as an UPnP MediaRenderer
=====================================================
Rygel
-----
With the help of `the Rygel project <https://live.gnome.org/Rygel>`_ Mopidy can
be made available as an UPnP MediaRenderer. Rygel will interface with the MPRIS
@ -41,15 +79,15 @@ interface provided by the `Mopidy-MPRIS extension
<https://github.com/mopidy/mopidy-mpris>`_, and make Mopidy available as a
MediaRenderer on the local network. Since this depends on the MPRIS frontend,
which again depends on D-Bus being available, this will only work on Linux, and
not OS X. MPRIS/D-Bus is only available to other applications on the same
not OS X. MPRIS/D-Bus is only available to other applications on the same
host, so Rygel must be running on the same machine as Mopidy.
1. Start Mopidy and make sure the MPRIS frontend is working. It is activated
1. Start Mopidy and make sure the MPRIS frontend is working. It is activated
by default when the Mopidy-MPRIS extension is installed, but you may miss
dependencies or be using OS X, in which case it will not work. Check the
console output when Mopidy is started for any errors related to the MPRIS
frontend. If you're unsure it is working, there are instructions for how to
test it on in the `Mopidy-MPRIS readme
test it in the `Mopidy-MPRIS readme
<https://github.com/mopidy/mopidy-mpris>`_.
2. Install Rygel. On Debian/Ubuntu::

View File

@ -157,6 +157,7 @@ extlinks = {
'commit': ('https://github.com/mopidy/mopidy/commit/%s', 'commit '),
'mpris': (
'https://github.com/mopidy/mopidy-mpris/issues/%s', 'mopidy-mpris#'),
'discuss': ('https://discuss.mopidy.com/t/%s', 'discuss.mopidy.com/t/'),
}

View File

@ -74,13 +74,13 @@ you a lot better performance.
#. Install Mopidy and its dependencies as described in :ref:`debian-install`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`. Alternatively you may
then you're ready to :doc:`run Mopidy </running>`. Alternatively you may
want to have Mopidy run as a :doc:`system service </debian>`, automatically
starting at boot.
Appendix: Fixing audio quality issues
=====================================
Appendix A: Fixing audio quality issues
=======================================
As of about April 2013 the following steps should resolve any audio
issues for HDMI and analog without the use of an external USB sound
@ -141,3 +141,35 @@ not determined the exact revision that fixed this::
The only remaining known issue is a slight gap in playback at track changes
this is likely due to gapless playback not being implemented and is being
worked on irrespective of Raspberry Pi related work.
Appendix B: Raspbmc not booting
===============================
Due to a dependency version problem where XBMC uses another version of libtag
than what Debian originally ships with, you might have to make some minor
changes for Raspbmc to start properly after installing Mopidy.
If you notice that XBMC is not starting but gets stuck in a loop,
you need to make the following changes::
sudo ln -sf /home/pi/.xbmc-current/xbmc-bin/lib/xbmc/system/libtag.so.1 \
/usr/lib/arm-linux-gnueabihf/libtag.so.1
However, this will not persist the changes. To persist the changes edit
:file:`/etc/ld.so.conf.d/arm-linux-gnueabihf.conf` and add the following at the
top::
/home/pi/.xbmc-current/xbmc-bin/lib/xbmc/system
It's very important to add it at the top of the file as this indicates the
priority of the folder in which to look for shared libraries.
XBMC doesn't play nicely with the system wide installed version of libtag that
got installed together with Mopidy, but rather vendors in its own version.
More info about this issue can be found in `this post
<http://geeks.noeit.com/xbmc-library-dependency-error/>`_.
Please note that if you're running Xbian or another XBMC distribution these
instructions might vary for your system.

View File

@ -30,4 +30,4 @@ except ImportError:
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.19.4'
__version__ = '0.19.5'

View File

@ -2,9 +2,11 @@ from __future__ import absolute_import, print_function, unicode_literals
import argparse
import collections
import contextlib
import logging
import os
import sys
import time
import glib
@ -63,6 +65,13 @@ class _HelpAction(argparse.Action):
raise _HelpError()
@contextlib.contextmanager
def _startup_timer(name):
start = time.time()
yield
logger.debug('%s startup took %dms', name, (time.time() - start) * 1000)
class Command(object):
"""Command parser and runner for building trees of commands.
@ -339,8 +348,9 @@ class RootCommand(Command):
backends = []
for backend_class in backend_classes:
try:
backend = backend_class.start(
config=config, audio=audio).proxy()
with _startup_timer(backend_class.__name__):
backend = backend_class.start(
config=config, audio=audio).proxy()
backends.append(backend)
except exceptions.BackendError as exc:
logger.error(
@ -361,7 +371,8 @@ class RootCommand(Command):
for frontend_class in frontend_classes:
try:
frontend_class.start(config=config, core=core)
with _startup_timer(frontend_class.__name__):
frontend_class.start(config=config, core=core)
except exceptions.FrontendError as exc:
logger.error(
'Frontend (%s) initialization error: %s',

View File

@ -98,8 +98,12 @@ def format_initial(extensions):
versions = ['Mopidy %s' % versioning.get_version()]
for extension in sorted(extensions, key=lambda ext: ext.dist_name):
versions.append('%s %s' % (extension.dist_name, extension.version))
description = _INITIAL_HELP.strip() % {'versions': '\n# '.join(versions)}
return description + '\n\n' + _format(config, {}, schemas, False, True)
header = _INITIAL_HELP.strip() % {'versions': '\n# '.join(versions)}
formatted_config = _format(
config=config, comments={}, schemas=schemas,
display=False, disable=True).decode('utf-8')
return header + '\n\n' + formatted_config
def _load(files, defaults, overrides):

View File

@ -185,16 +185,20 @@ class PlaybackController(object):
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
tl_track = self.core.tracklist.next_track(self.current_tl_track)
if tl_track:
original_tl_track = self.current_tl_track
next_tl_track = self.core.tracklist.next_track(original_tl_track)
if next_tl_track:
# TODO: switch to:
# backend.play(track)
# wait for state change?
self.change_track(tl_track)
self.change_track(next_tl_track)
else:
self.stop()
self.current_tl_track = None
self.core.tracklist.mark_played(original_tl_track)
def pause(self):
"""Pause playback."""
backend = self._get_backend()

View File

@ -449,10 +449,10 @@ class TracklistController(object):
def mark_played(self, tl_track):
"""Private method used by :class:`mopidy.core.PlaybackController`."""
if not self.consume:
return False
self.remove(tlid=[tl_track.tlid])
return True
if self.consume and tl_track is not None:
self.remove(tlid=[tl_track.tlid])
return True
return False
def _trigger_tracklist_changed(self):
if self.random:

View File

@ -188,10 +188,13 @@ def validate_extension(extension):
extension.ext_name, ex)
return False
except pkg_resources.VersionConflict as ex:
found, required = ex.args
logger.info(
'Disabled extension %s: %s required, but found %s at %s',
extension.ext_name, required, found, found.location)
if len(ex.args) == 2:
found, required = ex.args
logger.info(
'Disabled extension %s: %s required, but found %s at %s',
extension.ext_name, required, found, found.location)
else:
logger.info('Disabled extension %s: %s', extension.ext_name, ex)
return False
try:

View File

@ -129,11 +129,16 @@ class HttpServer(threading.Thread):
def _get_app_request_handlers(self):
result = []
for app in self.apps:
try:
request_handlers = app['factory'](self.config, self.core)
except Exception:
logger.exception('Loading %s failed.', app['name'])
continue
result.append((
r'/%s' % app['name'],
handlers.AddSlashHandler
))
request_handlers = app['factory'](self.config, self.core)
for handler in request_handlers:
handler = list(handler)
handler[0] = '/%s%s' % (app['name'], handler[0])

View File

@ -196,17 +196,12 @@ class MpdDispatcher(object):
def _format_lines(self, line):
if isinstance(line, dict):
return [self._escape_newlines('%s: %s' % (key, value))
for (key, value)
in line.items()]
return ['%s: %s' % (key, value) for (key, value) in line.items()]
if isinstance(line, tuple):
(key, value) = line
return [self._escape_newlines('%s: %s' % (key, value))]
return ['%s: %s' % (key, value)]
return [line]
def _escape_newlines(self, text):
return text.replace('\n', '\\n')
class MpdContext(object):
"""

View File

@ -81,9 +81,6 @@ def track_to_mpd_format(track, position=None):
if track.disc_no:
result.append(('Disc', track.disc_no))
if track.comment:
result.append(('Comment', track.comment))
if track.musicbrainz_id is not None:
result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id))
return result

View File

@ -46,12 +46,14 @@ def get_or_create_file(file_path, mkdir=True, content=None):
if not isinstance(file_path, bytes):
raise ValueError('Path is not a bytestring.')
file_path = expand_path(file_path)
if isinstance(content, compat.text_type):
content = content.encode('utf-8')
if mkdir:
get_or_create_dir(os.path.dirname(file_path))
if not os.path.isfile(file_path):
logger.info('Creating file %s', file_path)
with open(file_path, 'w') as fh:
if content:
with open(file_path, 'wb') as fh:
if content is not None:
fh.write(content)
return file_path

View File

@ -239,6 +239,23 @@ class CorePlaybackTest(unittest.TestCase):
# TODO Test next() more
def test_next_keeps_finished_track_in_tracklist(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.playback.next()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
def test_next_in_consume_mode_removes_finished_track(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.tracklist.consume = True
self.core.playback.next()
self.assertNotIn(tl_track, self.core.tracklist.tl_tracks)
@mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_next_emits_events(self, listener_mock):
@ -265,6 +282,23 @@ class CorePlaybackTest(unittest.TestCase):
# TODO Test previous() more
def test_previous_keeps_finished_track_in_tracklist(self):
tl_track = self.tl_tracks[1]
self.core.playback.play(tl_track)
self.core.playback.previous()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
def test_previous_keeps_finished_track_even_in_consume_mode(self):
tl_track = self.tl_tracks[1]
self.core.playback.play(tl_track)
self.core.tracklist.consume = True
self.core.playback.previous()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
@mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_previous_emits_events(self, listener_mock):
@ -291,6 +325,23 @@ class CorePlaybackTest(unittest.TestCase):
# TODO Test on_end_of_track() more
def test_on_end_of_track_keeps_finished_track_in_tracklist(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.playback.on_end_of_track()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
def test_on_end_of_track_in_consume_mode_removes_finished_track(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.tracklist.consume = True
self.core.playback.on_end_of_track()
self.assertNotIn(tl_track, self.core.tracklist.tl_tracks)
@mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_on_end_of_track_emits_events(self, listener_mock):

View File

@ -347,7 +347,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.tracklist.consume = True
self.playback.play()
self.playback.next()
self.assertIn(self.tracks[0], self.tracklist.tracks)
self.assertNotIn(self.tracks[0], self.tracklist.tracks)
@populate_tracklist
def test_next_with_single_and_repeat(self):

View File

@ -73,10 +73,10 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assertIn(('Track', '7/13'), result)
self.assertIn(('Date', datetime.date(1977, 1, 1)), result)
self.assertIn(('Disc', '1'), result)
self.assertIn(('Comment', 'a comment'), result)
self.assertIn(('Pos', 9), result)
self.assertIn(('Id', 122), result)
self.assertEqual(len(result), 15)
self.assertNotIn(('Comment', 'a comment'), result)
self.assertEqual(len(result), 14)
def test_track_to_mpd_format_musicbrainz_trackid(self):
track = self.track.copy(musicbrainz_id='foo')

View File

@ -50,5 +50,6 @@ class VersionTest(unittest.TestCase):
self.assertLess(SV('0.19.0'), SV('0.19.1'))
self.assertLess(SV('0.19.1'), SV('0.19.2'))
self.assertLess(SV('0.19.2'), SV('0.19.3'))
self.assertLess(SV('0.19.3'), SV(__version__))
self.assertLess(SV(__version__), SV('0.19.5'))
self.assertLess(SV('0.19.3'), SV('0.19.4'))
self.assertLess(SV('0.19.4'), SV(__version__))
self.assertLess(SV(__version__), SV('0.19.6'))

View File

@ -107,12 +107,12 @@ class GetOrCreateFileTest(unittest.TestCase):
with self.assertRaises(IOError):
path.get_or_create_file(conflicting_dir)
def test_create_dir_with_unicode(self):
def test_create_dir_with_unicode_filename_throws_value_error(self):
with self.assertRaises(ValueError):
file_path = compat.text_type(os.path.join(self.parent, b'test'))
path.get_or_create_file(file_path)
def test_create_file_with_none(self):
def test_create_file_with_none_filename_throws_value_error(self):
with self.assertRaises(ValueError):
path.get_or_create_file(None)
@ -121,12 +121,18 @@ class GetOrCreateFileTest(unittest.TestCase):
with self.assertRaises(IOError):
path.get_or_create_file(file_path, mkdir=False)
def test_create_dir_with_default_content(self):
def test_create_dir_with_bytes_content(self):
file_path = os.path.join(self.parent, b'test')
created = path.get_or_create_file(file_path, content=b'foobar')
with open(created) as fh:
self.assertEqual(fh.read(), b'foobar')
def test_create_dir_with_unicode_content(self):
file_path = os.path.join(self.parent, b'test')
created = path.get_or_create_file(file_path, content='foobaræøå')
with open(created) as fh:
self.assertEqual(fh.read(), b'foobaræøå')
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):