Merge branch 'develop' into tidy-up-core
This commit is contained in:
commit
694db515e4
3
.mailmap
3
.mailmap
@ -5,3 +5,6 @@ Kristian Klette <klette@samfundet.no>
|
||||
Johannes Knutsen <johannes@knutseninfo.no> <johannes@iterate.no>
|
||||
Johannes Knutsen <johannes@knutseninfo.no> <johannes@barbarmaclin.(none)>
|
||||
John Bäckstrand <sopues@gmail.com> <sandos@XBMCLive.(none)>
|
||||
Alli Witheford <alzeih@gmail.com>
|
||||
Alexandre Petitjean <alpetitjean@gmail.com>
|
||||
Alexandre Petitjean <alpetitjean@gmail.com> <alexandre.petitjean@lne.fr>
|
||||
|
||||
4
AUTHORS
4
AUTHORS
@ -19,3 +19,7 @@
|
||||
- Nick Steel <kingosticks@gmail.com>
|
||||
- Zan Dobersek <zandobersek@gmail.com>
|
||||
- Thomas Refis <refis.thomas@gmail.com>
|
||||
- Janez Troha <janez.troha@gmail.com>
|
||||
- Tobias Sauerwein <cgtobi@gmail.com>
|
||||
- Alli Witheford <alzeih@gmail.com>
|
||||
- Alexandre Petitjean <alpetitjean@gmail.com>
|
||||
|
||||
@ -25,4 +25,5 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
|
||||
|
||||
.. image:: https://secure.travis-ci.org/mopidy/mopidy.png?branch=develop
|
||||
.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop
|
||||
:target: https://travis-ci.org/mopidy/mopidy
|
||||
|
||||
@ -22,6 +22,53 @@ v0.15.0 (UNRELEASED)
|
||||
- :option:`mopidy --show-config` will now take into consideration any
|
||||
:option:`mopidy --option` arguments appearing later on the command line.
|
||||
|
||||
**Audio**
|
||||
|
||||
- Added support for viusalization. :confval:`audio/visualizer` can now be set
|
||||
to GStreamer visualizers.
|
||||
|
||||
**Local backend**
|
||||
|
||||
- An album's number of discs and a track's disc number are now extracted when
|
||||
scanning your music collection.
|
||||
|
||||
- The scanner now gives up scanning a file after a second, and continues with
|
||||
the next file. This fixes some hangs on non-media files, like logs. (Fixes:
|
||||
:issue:`476`, :issue:`483`)
|
||||
|
||||
- Added support for plugable library updaters. This allows extension writers
|
||||
to start providing their own custom libraries instead of being stuck with
|
||||
just our tag cache as the only option.
|
||||
|
||||
- Converted local backend to use new `local:playlist:path` and
|
||||
`local:track:path` uri scheme. Also moves support of `file://` to streaming
|
||||
backend.
|
||||
|
||||
**Spotify backend**
|
||||
|
||||
- Prepend playlist folder names to the playlist name, so that the playlist
|
||||
hierarchy from your Spotify account is available in Mopidy. (Fixes:
|
||||
:issue:`62`)
|
||||
|
||||
- Fix proxy config values that was broken with the config system change in
|
||||
0.14. (Fixes: :issue:`472`)
|
||||
|
||||
**MPD frontend**
|
||||
|
||||
- Replace newline, carriage return and forward slash in playlist names. (Fixes:
|
||||
:issue:`474`, :issue:`480`)
|
||||
|
||||
|
||||
v0.14.2 (2013-07-01)
|
||||
====================
|
||||
|
||||
This is a maintenance release to make Mopidy 0.14 work with pyspotify 1.11.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- pyspotify >= 1.9, < 2 is now required for Spotify support. In other words,
|
||||
you're free to upgrade to pyspotify 1.11, but it isn't a requirement.
|
||||
|
||||
|
||||
v0.14.1 (2013-04-28)
|
||||
====================
|
||||
|
||||
@ -33,7 +33,10 @@ class Mock(object):
|
||||
if name in ('__file__', '__path__'):
|
||||
return '/dev/null'
|
||||
elif (name[0] == name[0].upper()
|
||||
and not name.startswith('MIXER_TRACK_')):
|
||||
# gst.interfaces.MIXER_TRACK_*
|
||||
and not name.startswith('MIXER_TRACK_')
|
||||
# dbus.String()
|
||||
and not name == 'String'):
|
||||
return type(name, (), {})
|
||||
else:
|
||||
return Mock()
|
||||
@ -98,7 +101,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Mopidy'
|
||||
copyright = '2010-2013, Stein Magnus Jodal and contributors'
|
||||
copyright = '2009-2013, Stein Magnus Jodal and contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
||||
@ -90,6 +90,16 @@ Core configuration values
|
||||
``gst-inspect-0.10`` to see what output properties can be set on the sink.
|
||||
For example: ``gst-inspect-0.10 shout2send``
|
||||
|
||||
.. confval:: audio/visualizer
|
||||
|
||||
Visualizer to use.
|
||||
|
||||
Can be left blank if no visualizer is desired. Otherwise this expects a
|
||||
GStreamer visualizer. Typical values are ``monoscope``, ``goom``,
|
||||
``goom2k1`` or one of the `libvisual`_ visualizers.
|
||||
|
||||
.. _libvisual: http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-plugin-libvisual.html
|
||||
|
||||
.. confval:: logging/console_format
|
||||
|
||||
The log format used for informational logging.
|
||||
@ -137,6 +147,24 @@ Core configuration values
|
||||
.. _the Python logging docs: http://docs.python.org/2/library/logging.config.html
|
||||
|
||||
|
||||
Extension configuration
|
||||
=======================
|
||||
|
||||
Mopidy's extensions have their own config values that you may want to tweak.
|
||||
For the available config values, please refer to the docs for each extension.
|
||||
Most, if not all, can be found at :ref:`ext`.
|
||||
|
||||
Mopidy extensions are enabled by default when they are installed. If you want
|
||||
to disable an extension without uninstalling it, all extensions support the
|
||||
``enabled`` config value even if it isn't explicitly documented by all
|
||||
extensions. If the ``enabled`` config value is set to ``false`` the extension
|
||||
will not be started. For example, to disable the Spotify extension, add the
|
||||
following to your ``mopidy.conf``::
|
||||
|
||||
[spotify]
|
||||
enabled = false
|
||||
|
||||
|
||||
Extension configuration
|
||||
=======================
|
||||
|
||||
|
||||
@ -47,6 +47,11 @@ Configuration values
|
||||
|
||||
Path to tag cache for local media.
|
||||
|
||||
.. confval:: local/scan_timeout
|
||||
|
||||
Number of milliseconds before giving up scanning a file and moving on to
|
||||
the next file.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
@ -48,7 +48,7 @@ About
|
||||
:maxdepth: 1
|
||||
|
||||
authors
|
||||
licenses
|
||||
license
|
||||
changelog
|
||||
versioning
|
||||
|
||||
|
||||
@ -16,12 +16,20 @@ distribution.
|
||||
|
||||
.. _raspi-wheezy:
|
||||
|
||||
How to for Debian 7 (Wheezy)
|
||||
============================
|
||||
How to for Raspbian "wheezy" and Debian "wheezy"
|
||||
================================================
|
||||
|
||||
#. Download the latest wheezy disk image from
|
||||
http://downloads.raspberrypi.org/images/debian/7/. I used the one dated
|
||||
2012-08-08.
|
||||
This guide applies for both:
|
||||
|
||||
- Raspian "wheezy" for armhf (hard-float), and
|
||||
- Debian "wheezy" for armel (soft-float)
|
||||
|
||||
If you don't know which one to select, go for the armhf variant, as it'll give
|
||||
you a lot better performance.
|
||||
|
||||
#. Download the latest "wheezy" disk image from
|
||||
http://www.raspberrypi.org/downloads/. This was last tested with the images
|
||||
from 2013-05-25 for armhf and 2013-05-29 for armel.
|
||||
|
||||
#. Flash the OS image to your SD card. See
|
||||
http://elinux.org/RPi_Easy_SD_Card_Setup for help.
|
||||
@ -82,10 +90,10 @@ card.
|
||||
#. Ensure your system is up to date. On Debian based systems run::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get full-upgrade
|
||||
sudo apt-get dist-upgrade
|
||||
|
||||
#. Ensure you have a new enough firmware. On Debian based systems
|
||||
`rpi-update <http://apt.mopidy.com://github.com/Hexxeh/rpi-update>`_
|
||||
`rpi-update <https://github.com/Hexxeh/rpi-update>`_
|
||||
can be used.
|
||||
|
||||
#. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the
|
||||
|
||||
10
docs/license.rst
Normal file
10
docs/license.rst
Normal file
@ -0,0 +1,10 @@
|
||||
*******
|
||||
License
|
||||
*******
|
||||
|
||||
Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. For a list
|
||||
of contributors, see :doc:`authors`. For details on who have contributed what,
|
||||
please refer to our git repository.
|
||||
|
||||
Mopidy is licensed under the `Apache License, Version 2.0
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>`_.
|
||||
@ -1,34 +0,0 @@
|
||||
********
|
||||
Licenses
|
||||
********
|
||||
|
||||
For a list of contributors, see :doc:`authors`. For details on who have
|
||||
contributed what, please refer to our git repository.
|
||||
|
||||
Source code license
|
||||
===================
|
||||
|
||||
Copyright 2009-2013 Stein Magnus Jodal and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
Documentation license
|
||||
=====================
|
||||
|
||||
Copyright 2010-2013 Stein Magnus Jodal and contributors
|
||||
|
||||
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||
Unported License. To view a copy of this license, visit
|
||||
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
|
||||
Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
|
||||
36
fabfile.py
vendored
36
fabfile.py
vendored
@ -1,21 +1,51 @@
|
||||
from fabric.api import local, settings
|
||||
from fabric.api import execute, local, settings, task
|
||||
|
||||
|
||||
@task
|
||||
def docs():
|
||||
local('make -C docs/ html')
|
||||
|
||||
|
||||
@task
|
||||
def autodocs():
|
||||
auto(docs)
|
||||
|
||||
|
||||
@task
|
||||
def test(path=None):
|
||||
path = path or 'tests/'
|
||||
local('nosetests ' + path)
|
||||
|
||||
|
||||
@task
|
||||
def autotest(path=None):
|
||||
auto(test, path=path)
|
||||
|
||||
|
||||
@task
|
||||
def coverage(path=None):
|
||||
path = path or 'tests/'
|
||||
local(
|
||||
'nosetests --with-coverage --cover-package=mopidy '
|
||||
'--cover-branches --cover-html ' + path)
|
||||
|
||||
|
||||
@task
|
||||
def autocoverage(path=None):
|
||||
auto(coverage, path=path)
|
||||
|
||||
|
||||
def auto(task, *args, **kwargs):
|
||||
while True:
|
||||
local('clear')
|
||||
with settings(warn_only=True):
|
||||
test(path)
|
||||
execute(task, *args, **kwargs)
|
||||
local(
|
||||
'inotifywait -q -e create -e modify -e delete '
|
||||
'--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/')
|
||||
'--exclude ".*\.(pyc|sw.)" -r docs/ mopidy/ tests/')
|
||||
|
||||
|
||||
@task
|
||||
def update_authors():
|
||||
# Keep authors in the order of appearance and use awk to filter out dupes
|
||||
local(
|
||||
|
||||
12
js/README.md
12
js/README.md
@ -51,15 +51,15 @@ Building from source
|
||||
1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're
|
||||
running Ubuntu:
|
||||
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs npm
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs
|
||||
|
||||
2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies:
|
||||
|
||||
cd js/
|
||||
npm install
|
||||
cd js/
|
||||
npm install
|
||||
|
||||
That's it.
|
||||
|
||||
|
||||
@ -23,4 +23,4 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.14.1'
|
||||
__version__ = '0.14.2'
|
||||
|
||||
@ -22,6 +22,22 @@ mixers.register_mixers()
|
||||
|
||||
MB = 1 << 20
|
||||
|
||||
# GST_PLAY_FLAG_VIDEO (1<<0)
|
||||
# GST_PLAY_FLAG_AUDIO (1<<1)
|
||||
# GST_PLAY_FLAG_TEXT (1<<2)
|
||||
# GST_PLAY_FLAG_VIS (1<<3)
|
||||
# GST_PLAY_FLAG_SOFT_VOLUME (1<<4)
|
||||
# GST_PLAY_FLAG_NATIVE_AUDIO (1<<5)
|
||||
# GST_PLAY_FLAG_NATIVE_VIDEO (1<<6)
|
||||
# GST_PLAY_FLAG_DOWNLOAD (1<<7)
|
||||
# GST_PLAY_FLAG_BUFFERING (1<<8)
|
||||
# GST_PLAY_FLAG_DEINTERLACE (1<<9)
|
||||
# GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10)
|
||||
|
||||
# Default flags to use for playbin: AUDIO, SOFT_VOLUME, DOWNLOAD
|
||||
PLAYBIN_FLAGS = (1<<1) | (1<<4) | (1<<7)
|
||||
PLAYBIN_VIS_FLAGS = PLAYBIN_FLAGS | (1<<3)
|
||||
|
||||
|
||||
class Audio(pykka.ThreadingActor):
|
||||
"""
|
||||
@ -55,6 +71,7 @@ class Audio(pykka.ThreadingActor):
|
||||
try:
|
||||
self._setup_playbin()
|
||||
self._setup_output()
|
||||
self._setup_visualizer()
|
||||
self._setup_mixer()
|
||||
self._setup_message_processor()
|
||||
except gobject.GError as ex:
|
||||
@ -78,9 +95,7 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
def _setup_playbin(self):
|
||||
playbin = gst.element_factory_make('playbin2')
|
||||
|
||||
fakesink = gst.element_factory_make('fakesink')
|
||||
playbin.set_property('video-sink', fakesink)
|
||||
playbin.set_property('flags', PLAYBIN_FLAGS)
|
||||
|
||||
self._connect(playbin, 'about-to-finish', self._on_about_to_finish)
|
||||
self._connect(playbin, 'notify::source', self._on_new_source)
|
||||
@ -149,6 +164,19 @@ class Audio(pykka.ThreadingActor):
|
||||
'Failed to create audio output "%s": %s', output_desc, ex)
|
||||
process.exit_process()
|
||||
|
||||
def _setup_visualizer(self):
|
||||
visualizer_element = self._config['audio']['visualizer']
|
||||
if not visualizer_element:
|
||||
return
|
||||
try:
|
||||
visualizer = gst.element_factory_make(visualizer_element)
|
||||
self._playbin.set_property('vis-plugin', visualizer)
|
||||
self._playbin.set_property('flags', PLAYBIN_VIS_FLAGS)
|
||||
logger.info('Audio visualizer set to "%s"', visualizer_element)
|
||||
except gobject.GError as ex:
|
||||
logger.error(
|
||||
'Failed to create audio visualizer "%s": %s', visualizer_element, ex)
|
||||
|
||||
def _setup_mixer(self):
|
||||
mixer_desc = self._config['audio']['mixer']
|
||||
track_desc = self._config['audio']['mixer_track']
|
||||
|
||||
@ -15,11 +15,6 @@ class Backend(object):
|
||||
#: the backend doesn't provide a library.
|
||||
library = None
|
||||
|
||||
#: The library update provider. An instance of
|
||||
#: :class:`~mopidy.backends.base.BaseLibraryUpdateProvider`, or
|
||||
#: :class:`None` if the backend doesn't provide a library.
|
||||
updater = None
|
||||
|
||||
#: The playback provider. An instance of
|
||||
#: :class:`~mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide playback.
|
||||
@ -40,9 +35,6 @@ class Backend(object):
|
||||
def has_library(self):
|
||||
return self.library is not None
|
||||
|
||||
def has_updater(self):
|
||||
return self.updater is not None
|
||||
|
||||
def has_playback(self):
|
||||
return self.playback is not None
|
||||
|
||||
@ -96,15 +88,7 @@ class BaseLibraryProvider(object):
|
||||
|
||||
|
||||
class BaseLibraryUpdateProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
uri_schemes = []
|
||||
|
||||
def load(self):
|
||||
"""Loads the library and returns all tracks in it.
|
||||
@ -172,9 +156,22 @@ class BasePlaybackProvider(object):
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.audio.prepare_change()
|
||||
self.audio.set_uri(track.uri).get()
|
||||
self.change_track(track)
|
||||
return self.audio.start_playback().get()
|
||||
|
||||
def change_track(self, track):
|
||||
"""
|
||||
Swith to provided track.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.audio.set_uri(track.uri).get()
|
||||
return True
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume playback at the same time position playback was paused.
|
||||
|
||||
@ -21,6 +21,7 @@ class Extension(ext.Extension):
|
||||
schema['media_dir'] = config.Path()
|
||||
schema['playlists_dir'] = config.Path()
|
||||
schema['tag_cache_file'] = config.Path()
|
||||
schema['scan_timeout'] = config.Integer(minimum=0)
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
@ -29,3 +30,7 @@ class Extension(ext.Extension):
|
||||
def get_backend_classes(self):
|
||||
from .actor import LocalBackend
|
||||
return [LocalBackend]
|
||||
|
||||
def get_library_updaters(self):
|
||||
from .library import LocalLibraryUpdateProvider
|
||||
return [LocalLibraryUpdateProvider]
|
||||
|
||||
@ -8,8 +8,9 @@ import pykka
|
||||
from mopidy.backends import base
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
from .library import LocalLibraryProvider, LocalLibraryUpdateProvider
|
||||
from .library import LocalLibraryProvider
|
||||
from .playlists import LocalPlaylistsProvider
|
||||
from .playback import LocalPlaybackProvider
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
@ -23,11 +24,10 @@ class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||
self.check_dirs_and_files()
|
||||
|
||||
self.library = LocalLibraryProvider(backend=self)
|
||||
self.updater = LocalLibraryUpdateProvider(backend=self)
|
||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||
self.playback = LocalPlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = ['file']
|
||||
self.uri_schemes = ['local']
|
||||
|
||||
def check_dirs_and_files(self):
|
||||
if not os.path.isdir(self.config['local']['media_dir']):
|
||||
|
||||
@ -3,3 +3,4 @@ enabled = true
|
||||
media_dir = $XDG_MUSIC_DIR
|
||||
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
|
||||
tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache
|
||||
scan_timeout = 1000
|
||||
|
||||
@ -81,7 +81,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
else:
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
return SearchResult(uri='file:search', tracks=result_tracks)
|
||||
# TODO: add local:search:<query>
|
||||
return SearchResult(uri='local:search', tracks=result_tracks)
|
||||
|
||||
def search(self, query=None, uris=None):
|
||||
# TODO Only return results within URI roots given by ``uris``
|
||||
@ -122,7 +123,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
else:
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
return SearchResult(uri='file:search', tracks=result_tracks)
|
||||
# TODO: add local:search:<query>
|
||||
return SearchResult(uri='local:search', tracks=result_tracks)
|
||||
|
||||
def _validate_query(self, query):
|
||||
for (_, values) in query.iteritems():
|
||||
@ -135,11 +137,12 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
|
||||
# TODO: rename and move to tagcache extension.
|
||||
class LocalLibraryUpdateProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalLibraryUpdateProvider, self).__init__(*args, **kwargs)
|
||||
uri_schemes = ['local']
|
||||
|
||||
def __init__(self, config):
|
||||
self._tracks = {}
|
||||
self._media_dir = self.backend.config['local']['media_dir']
|
||||
self._tag_cache_file = self.backend.config['local']['tag_cache_file']
|
||||
self._media_dir = config['local']['media_dir']
|
||||
self._tag_cache_file = config['local']['tag_cache_file']
|
||||
|
||||
def load(self):
|
||||
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
|
||||
@ -156,6 +159,8 @@ class LocalLibraryUpdateProvider(base.BaseLibraryProvider):
|
||||
|
||||
def commit(self):
|
||||
directory, basename = os.path.split(self._tag_cache_file)
|
||||
|
||||
# TODO: cleanup directory/basename.* files.
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix=basename + '.', dir=directory, delete=False)
|
||||
|
||||
|
||||
19
mopidy/backends/local/playback.py
Normal file
19
mopidy/backends/local/playback.py
Normal file
@ -0,0 +1,19 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.utils import path
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalPlaybackProvider(base.BasePlaybackProvider):
|
||||
def change_track(self, track):
|
||||
media_dir = self.backend.config['local']['media_dir']
|
||||
# TODO: check that type is correct.
|
||||
file_path = path.uri_to_path(track.uri).split(':', 1)[1]
|
||||
file_path = os.path.join(media_dir, file_path)
|
||||
track = track.copy(uri=path.path_to_uri(file_path))
|
||||
return super(LocalPlaybackProvider, self).change_track(track)
|
||||
@ -24,7 +24,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
|
||||
def create(self, name):
|
||||
name = formatting.slugify(name)
|
||||
uri = path.path_to_uri(self._get_m3u_path(name))
|
||||
uri = 'local:playlist:%s.m3u' % name
|
||||
playlist = Playlist(uri=uri, name=name)
|
||||
return self.save(playlist)
|
||||
|
||||
@ -37,6 +37,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
self._delete_m3u(playlist.uri)
|
||||
|
||||
def lookup(self, uri):
|
||||
# TODO: store as {uri: playlist}?
|
||||
for playlist in self._playlists:
|
||||
if playlist.uri == uri:
|
||||
return playlist
|
||||
@ -45,8 +46,8 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
playlists = []
|
||||
|
||||
for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')):
|
||||
uri = path.path_to_uri(m3u)
|
||||
name = os.path.splitext(os.path.basename(m3u))[0]
|
||||
uri = 'local:playlist:%s' % name
|
||||
|
||||
tracks = []
|
||||
for track_uri in parse_m3u(m3u, self._media_dir):
|
||||
@ -61,6 +62,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
playlists.append(playlist)
|
||||
|
||||
self.playlists = playlists
|
||||
# TODO: send what scheme we loaded them for?
|
||||
listener.BackendListener.send('playlists_loaded')
|
||||
|
||||
logger.info(
|
||||
@ -86,38 +88,30 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
|
||||
return playlist
|
||||
|
||||
def _get_m3u_path(self, name):
|
||||
name = formatting.slugify(name)
|
||||
file_path = os.path.join(self._playlists_dir, name + '.m3u')
|
||||
def _m3u_uri_to_path(self, uri):
|
||||
# TODO: create uri handling helpers for local uri types.
|
||||
file_path = path.uri_to_path(uri).split(':', 1)[1]
|
||||
file_path = os.path.join(self._playlists_dir, file_path)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
|
||||
return file_path
|
||||
|
||||
def _save_m3u(self, playlist):
|
||||
file_path = path.uri_to_path(playlist.uri)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
|
||||
file_path = self._m3u_uri_to_path(playlist.uri)
|
||||
with open(file_path, 'w') as file_handle:
|
||||
for track in playlist.tracks:
|
||||
if track.uri.startswith('file://'):
|
||||
uri = path.uri_to_path(track.uri)
|
||||
else:
|
||||
uri = track.uri
|
||||
file_handle.write(uri + '\n')
|
||||
file_handle.write(track.uri + '\n')
|
||||
|
||||
def _delete_m3u(self, uri):
|
||||
file_path = path.uri_to_path(uri)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
|
||||
file_path = self._m3u_uri_to_path(uri)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
def _rename_m3u(self, playlist):
|
||||
src_file_path = path.uri_to_path(playlist.uri)
|
||||
path.check_file_path_is_inside_base_dir(
|
||||
src_file_path, self._playlists_dir)
|
||||
dst_name = formatting.slugify(playlist.name)
|
||||
dst_uri = 'local:playlist:%s.m3u' % dst_name
|
||||
|
||||
dst_file_path = self._get_m3u_path(playlist.name)
|
||||
path.check_file_path_is_inside_base_dir(
|
||||
dst_file_path, self._playlists_dir)
|
||||
src_file_path = self._m3u_uri_to_path(playlist.uri)
|
||||
dst_file_path = self._m3u_uri_to_path(dst_uri)
|
||||
|
||||
shutil.move(src_file_path, dst_file_path)
|
||||
|
||||
return playlist.copy(uri=path.path_to_uri(dst_file_path))
|
||||
return playlist.copy(uri=dst_uri)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
from mopidy.models import Track, Artist, Album
|
||||
from mopidy.utils.encoding import locale_decode
|
||||
@ -30,7 +32,6 @@ def parse_m3u(file_path, media_dir):
|
||||
- m3u files are latin-1.
|
||||
- This function does not bother with Extended M3U directives.
|
||||
"""
|
||||
|
||||
# TODO: uris as bytes
|
||||
uris = []
|
||||
try:
|
||||
@ -46,16 +47,19 @@ def parse_m3u(file_path, media_dir):
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
|
||||
# FIXME what about other URI types?
|
||||
if line.startswith('file://'):
|
||||
if urlparse.urlsplit(line).scheme:
|
||||
uris.append(line)
|
||||
elif os.path.normpath(line) == os.path.abspath(line):
|
||||
path = path_to_uri(line)
|
||||
uris.append(path)
|
||||
else:
|
||||
path = path_to_uri(media_dir, line)
|
||||
path = path_to_uri(os.path.join(media_dir, line))
|
||||
uris.append(path)
|
||||
|
||||
return uris
|
||||
|
||||
|
||||
# TODO: remove music_dir from API
|
||||
def parse_mpd_tag_cache(tag_cache, music_dir=''):
|
||||
"""
|
||||
Converts a MPD tag_cache into a lists of tracks, artists and albums.
|
||||
@ -86,17 +90,17 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
|
||||
key, value = line.split(b': ', 1)
|
||||
|
||||
if key == b'key':
|
||||
_convert_mpd_data(current, tracks, music_dir)
|
||||
_convert_mpd_data(current, tracks)
|
||||
current.clear()
|
||||
|
||||
current[key.lower()] = value.decode('utf-8')
|
||||
|
||||
_convert_mpd_data(current, tracks, music_dir)
|
||||
_convert_mpd_data(current, tracks)
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def _convert_mpd_data(data, tracks, music_dir):
|
||||
def _convert_mpd_data(data, tracks):
|
||||
if not data:
|
||||
return
|
||||
|
||||
@ -160,15 +164,8 @@ def _convert_mpd_data(data, tracks, music_dir):
|
||||
path = data['file'][1:]
|
||||
else:
|
||||
path = data['file']
|
||||
path = urllib.unquote(path.encode('utf-8'))
|
||||
|
||||
if isinstance(music_dir, unicode):
|
||||
music_dir = music_dir.encode('utf-8')
|
||||
|
||||
# Make sure we only pass bytestrings to path_to_uri to avoid implicit
|
||||
# decoding of bytestrings to unicode strings
|
||||
track_kwargs['uri'] = path_to_uri(music_dir, path)
|
||||
|
||||
track_kwargs['uri'] = 'local:track:%s' % path
|
||||
track_kwargs['length'] = int(data.get('time', 0)) * 1000
|
||||
|
||||
track = Track(**track_kwargs)
|
||||
|
||||
@ -33,9 +33,17 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
self.cache_location = config['spotify']['cache_dir']
|
||||
self.settings_location = config['spotify']['cache_dir']
|
||||
|
||||
full_proxy = ''
|
||||
if config['proxy']['hostname']:
|
||||
full_proxy = config['proxy']['hostname']
|
||||
if config['proxy']['port']:
|
||||
full_proxy += ':' + str(config['proxy']['port'])
|
||||
if config['proxy']['scheme']:
|
||||
full_proxy = config['proxy']['scheme'] + "://" + full_proxy
|
||||
|
||||
PyspotifySessionManager.__init__(
|
||||
self, config['spotify']['username'], config['spotify']['password'],
|
||||
proxy=config['proxy']['hostname'],
|
||||
proxy=full_proxy,
|
||||
proxy_username=config['proxy']['username'],
|
||||
proxy_password=config['proxy']['password'])
|
||||
|
||||
@ -173,9 +181,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
logger.debug('Still getting data; skipped refresh of playlists')
|
||||
return
|
||||
playlists = []
|
||||
folders = []
|
||||
for spotify_playlist in self.session.playlist_container():
|
||||
if spotify_playlist.type() == 'folder_start':
|
||||
folders.append(spotify_playlist)
|
||||
if spotify_playlist.type() == 'folder_end':
|
||||
folders.pop()
|
||||
playlists.append(translator.to_mopidy_playlist(
|
||||
spotify_playlist,
|
||||
spotify_playlist, folders=folders,
|
||||
bitrate=self.bitrate, username=self.username))
|
||||
playlists.append(translator.to_mopidy_playlist(
|
||||
self.session.starred(),
|
||||
|
||||
@ -67,7 +67,7 @@ def to_mopidy_track(spotify_track, bitrate=None):
|
||||
return track_cache[uri]
|
||||
|
||||
|
||||
def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None):
|
||||
def to_mopidy_playlist(spotify_playlist, folders=None, bitrate=None, username=None):
|
||||
if spotify_playlist is None or spotify_playlist.type() != 'playlist':
|
||||
return
|
||||
try:
|
||||
@ -78,6 +78,9 @@ def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None):
|
||||
if not spotify_playlist.is_loaded():
|
||||
return Playlist(uri=uri, name='[loading...]')
|
||||
name = spotify_playlist.name()
|
||||
if folders:
|
||||
folder_names = '/'.join(folder.name() for folder in folders)
|
||||
name = folder_names + '/' + name
|
||||
tracks = [
|
||||
to_mopidy_track(spotify_track, bitrate=bitrate)
|
||||
for spotify_track in spotify_playlist
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
[stream]
|
||||
enabled = true
|
||||
protocols =
|
||||
file
|
||||
http
|
||||
https
|
||||
mms
|
||||
|
||||
@ -24,9 +24,13 @@ _audio_schema = ConfigSchema('audio')
|
||||
_audio_schema['mixer'] = String()
|
||||
_audio_schema['mixer_track'] = String(optional=True)
|
||||
_audio_schema['output'] = String()
|
||||
_audio_schema['visualizer'] = String(optional=True)
|
||||
|
||||
_proxy_schema = ConfigSchema('proxy')
|
||||
_proxy_schema['scheme'] = String(optional=True,
|
||||
choices=['http', 'https', 'socks4', 'socks5'])
|
||||
_proxy_schema['hostname'] = Hostname(optional=True)
|
||||
_proxy_schema['port'] = Port(optional=True)
|
||||
_proxy_schema['username'] = String(optional=True)
|
||||
_proxy_schema['password'] = Secret(optional=True)
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ def convert(settings):
|
||||
helper('audio/output', 'OUTPUT')
|
||||
|
||||
helper('proxy/hostname', 'SPOTIFY_PROXY_HOST')
|
||||
helper('proxy/port', 'SPOTIFY_PROXY_PORT')
|
||||
helper('proxy/username', 'SPOTIFY_PROXY_USERNAME')
|
||||
helper('proxy/password', 'SPOTIFY_PROXY_PASSWORD')
|
||||
|
||||
|
||||
@ -11,8 +11,11 @@ pykka = info
|
||||
mixer = autoaudiomixer
|
||||
mixer_track =
|
||||
output = autoaudiosink
|
||||
visualizer =
|
||||
|
||||
[proxy]
|
||||
scheme =
|
||||
hostname =
|
||||
port =
|
||||
username =
|
||||
password =
|
||||
|
||||
@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from mopidy.utils import path
|
||||
from mopidy.config import validators
|
||||
@ -72,9 +71,9 @@ class String(ConfigValue):
|
||||
def deserialize(self, value):
|
||||
value = decode(value).strip()
|
||||
validators.validate_required(value, self._required)
|
||||
validators.validate_choice(value, self._choices)
|
||||
if not value:
|
||||
return None
|
||||
validators.validate_choice(value, self._choices)
|
||||
return value
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
@ -112,12 +111,16 @@ class Secret(ConfigValue):
|
||||
class Integer(ConfigValue):
|
||||
"""Integer value."""
|
||||
|
||||
def __init__(self, minimum=None, maximum=None, choices=None):
|
||||
def __init__(self, minimum=None, maximum=None, choices=None, optional=False):
|
||||
self._required = not optional
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._choices = choices
|
||||
|
||||
def deserialize(self, value):
|
||||
validators.validate_required(value, self._required)
|
||||
if not value:
|
||||
return None
|
||||
value = int(value)
|
||||
validators.validate_choice(value, self._choices)
|
||||
validators.validate_minimum(value, self._minimum)
|
||||
@ -223,8 +226,9 @@ class Port(Integer):
|
||||
allocate a port for us.
|
||||
"""
|
||||
# TODO: consider probing if port is free or not?
|
||||
def __init__(self, choices=None):
|
||||
super(Port, self).__init__(minimum=0, maximum=2**16-1, choices=choices)
|
||||
def __init__(self, choices=None, optional=False):
|
||||
super(Port, self).__init__(
|
||||
minimum=0, maximum=2 ** 16 - 1, choices=choices, optional=optional)
|
||||
|
||||
|
||||
class Path(ConfigValue):
|
||||
@ -256,7 +260,7 @@ class Path(ConfigValue):
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode(sys.getfilesystemencoding())
|
||||
raise ValueError('paths should always be bytes')
|
||||
if isinstance(value, ExpandedPath):
|
||||
return value.original
|
||||
return value
|
||||
|
||||
@ -215,6 +215,7 @@ class PlaybackController(object):
|
||||
logger.warning('Track is not playable: %s', tl_track.track.uri)
|
||||
self.core.tracklist.mark("unplayable", tl_track)
|
||||
if on_error_step == 1:
|
||||
# TODO: can cause an endless loop for single track repeat.
|
||||
self.next()
|
||||
elif on_error_step == -1:
|
||||
self.previous()
|
||||
|
||||
@ -79,6 +79,14 @@ class Extension(object):
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_library_updaters(self):
|
||||
"""List of library updater classes
|
||||
|
||||
:returns: list of :class:`~mopidy.backends.base.BaseLibraryUpdateProvider`
|
||||
subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
"""Hook for registering custom GStreamer elements
|
||||
|
||||
|
||||
@ -236,6 +236,8 @@ class MpdContext(object):
|
||||
#: The subsytems that we want to be notified about in idle mode.
|
||||
subscriptions = None
|
||||
|
||||
_invalid_playlist_chars = re.compile(r'[\n\r/]')
|
||||
|
||||
def __init__(self, dispatcher, session=None, config=None, core=None):
|
||||
self.dispatcher = dispatcher
|
||||
self.session = session
|
||||
@ -248,10 +250,11 @@ class MpdContext(object):
|
||||
self.refresh_playlists_mapping()
|
||||
|
||||
def create_unique_name(self, playlist_name):
|
||||
name = playlist_name
|
||||
stripped_name = self._invalid_playlist_chars.sub(' ', playlist_name)
|
||||
name = stripped_name
|
||||
i = 2
|
||||
while name in self._playlist_uri_from_name:
|
||||
name = '%s [%d]' % (playlist_name, i)
|
||||
name = '%s [%d]' % (stripped_name, i)
|
||||
i += 1
|
||||
return name
|
||||
|
||||
@ -266,6 +269,7 @@ class MpdContext(object):
|
||||
for playlist in self.core.playlists.playlists.get():
|
||||
if not playlist.name:
|
||||
continue
|
||||
# TODO: add scheme to name perhaps 'foo (spotify)' etc.
|
||||
name = self.create_unique_name(playlist.name)
|
||||
self._playlist_uri_from_name[name] = playlist.uri
|
||||
self._playlist_name_from_uri[playlist.uri] = name
|
||||
|
||||
@ -39,8 +39,8 @@ def _artist_as_track(artist):
|
||||
artists=[artist])
|
||||
|
||||
|
||||
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||
def count(context, tag, needle):
|
||||
@handle_request(r'^count ' + QUERY_RE)
|
||||
def count(context, mpd_query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -48,6 +48,11 @@ def count(context, tag, needle):
|
||||
|
||||
Counts the number of songs and their total playtime in the db
|
||||
matching ``TAG`` exactly.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- does not add quotes around the tag argument.
|
||||
- use multiple tag-needle pairs to make more specific searches.
|
||||
"""
|
||||
return [('songs', 0), ('playtime', 0)] # TODO
|
||||
|
||||
|
||||
@ -156,14 +156,14 @@ def query_from_mpd_list_format(field, mpd_query):
|
||||
if field == 'album':
|
||||
if not tokens[0]:
|
||||
raise ValueError
|
||||
return {'artist': [tokens[0]]} # See above NOTE
|
||||
return {'artist': [tokens[0]]}
|
||||
else:
|
||||
raise MpdArgError(
|
||||
'should be "Album" for 3 arguments', command='list')
|
||||
elif len(tokens) % 2 == 0:
|
||||
query = {}
|
||||
while tokens:
|
||||
key = str(tokens[0].lower()) # See above NOTE
|
||||
key = tokens[0].lower()
|
||||
value = tokens[1]
|
||||
tokens = tokens[2:]
|
||||
if key not in ('artist', 'album', 'date', 'genre'):
|
||||
|
||||
@ -34,7 +34,7 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
|
||||
self.mpris_object = objects.MprisObject(self.config, self.core)
|
||||
self._send_startup_notification()
|
||||
except Exception as e:
|
||||
logger.error('MPRIS frontend setup failed (%s)', e)
|
||||
logger.warning('MPRIS frontend setup failed (%s)', e)
|
||||
self.stop()
|
||||
|
||||
def on_stop(self):
|
||||
|
||||
@ -27,7 +27,6 @@ pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
from mopidy import config as config_lib, ext
|
||||
from mopidy.audio import dummy as dummy_audio
|
||||
from mopidy.models import Track, Artist, Album
|
||||
from mopidy.utils import log, path, versioning
|
||||
|
||||
@ -45,21 +44,36 @@ def main():
|
||||
log.setup_root_logger()
|
||||
log.setup_console_logging(logging_config, args.verbosity_level)
|
||||
|
||||
extensions = dict((e.ext_name, e) for e in ext.load_extensions())
|
||||
extensions = ext.load_extensions()
|
||||
config, errors = config_lib.load(
|
||||
config_files, extensions.values(), config_overrides)
|
||||
config_files, extensions, config_overrides)
|
||||
log.setup_log_levels(config)
|
||||
|
||||
if not config['local']['media_dir']:
|
||||
logging.warning('Config value local/media_dir is not set.')
|
||||
return
|
||||
|
||||
if not config['local']['scan_timeout']:
|
||||
logging.warning('Config value local/scan_timeout is not set.')
|
||||
return
|
||||
|
||||
# TODO: missing config error checking and other default setup code.
|
||||
|
||||
audio = dummy_audio.DummyAudio()
|
||||
local_backend_classes = extensions['local'].get_backend_classes()
|
||||
local_backend = local_backend_classes[0](config, audio)
|
||||
local_updater = local_backend.updater
|
||||
updaters = {}
|
||||
for e in extensions:
|
||||
for updater_class in e.get_library_updaters():
|
||||
if updater_class and 'local' in updater_class.uri_schemes:
|
||||
updaters[e.ext_name] = updater_class
|
||||
|
||||
if not updaters:
|
||||
logging.error('No usable library updaters found.')
|
||||
return
|
||||
elif len(updaters) > 1:
|
||||
logging.error('More than one library updater found. '
|
||||
'Provided by: %s', ', '.join(updaters.keys()))
|
||||
return
|
||||
|
||||
local_updater = updaters.values()[0](config) # TODO: switch to actor?
|
||||
|
||||
media_dir = config['local']['media_dir']
|
||||
|
||||
@ -97,9 +111,11 @@ def main():
|
||||
logging.warning('Failed %s: %s', uri, error)
|
||||
logging.debug('Debug info for %s: %s', uri, debug)
|
||||
|
||||
scan_timeout = config['local']['scan_timeout']
|
||||
|
||||
logging.info('Scanning new and modified tracks.')
|
||||
# TODO: just pass the library in instead?
|
||||
scanner = Scanner(uris_update, store, debug)
|
||||
scanner = Scanner(uris_update, store, debug, scan_timeout)
|
||||
try:
|
||||
scanner.start()
|
||||
except KeyboardInterrupt:
|
||||
@ -139,6 +155,7 @@ def translator(data):
|
||||
|
||||
_retrieve(gst.TAG_ALBUM, 'name', album_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
|
||||
_retrieve(gst.TAG_ALBUM_VOLUME_COUNT, 'num_discs', album_kwargs)
|
||||
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
|
||||
|
||||
if gst.TAG_DATE in data and data[gst.TAG_DATE]:
|
||||
@ -152,6 +169,7 @@ def translator(data):
|
||||
|
||||
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
|
||||
_retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs)
|
||||
|
||||
# Following keys don't seem to have TAG_* constant.
|
||||
_retrieve('album-artist', 'name', albumartist_kwargs)
|
||||
@ -174,12 +192,14 @@ def translator(data):
|
||||
|
||||
|
||||
class Scanner(object):
|
||||
def __init__(self, uris, data_callback, error_callback=None):
|
||||
def __init__(self, uris, data_callback, error_callback=None, scan_timeout=1000):
|
||||
self.data = {}
|
||||
self.uris = iter(uris)
|
||||
self.data_callback = data_callback
|
||||
self.error_callback = error_callback
|
||||
self.scan_timeout = scan_timeout
|
||||
self.loop = gobject.MainLoop()
|
||||
self.timeout_id = None
|
||||
|
||||
self.fakesink = gst.element_factory_make('fakesink')
|
||||
self.fakesink.set_property('signal-handoffs', True)
|
||||
@ -250,6 +270,14 @@ class Scanner(object):
|
||||
self.error_callback(uri, error, debug)
|
||||
self.next_uri()
|
||||
|
||||
def process_timeout(self):
|
||||
if self.error_callback:
|
||||
uri = self.uribin.get_property('uri')
|
||||
self.error_callback(
|
||||
uri, 'Scan timed out after %d ms' % self.scan_timeout, None)
|
||||
self.next_uri()
|
||||
return False
|
||||
|
||||
def get_duration(self):
|
||||
self.pipe.get_state() # Block until state change is done.
|
||||
try:
|
||||
@ -260,6 +288,9 @@ class Scanner(object):
|
||||
|
||||
def next_uri(self):
|
||||
self.data = {}
|
||||
if self.timeout_id:
|
||||
gobject.source_remove(self.timeout_id)
|
||||
self.timeout_id = None
|
||||
try:
|
||||
uri = next(self.uris)
|
||||
except StopIteration:
|
||||
@ -267,6 +298,7 @@ class Scanner(object):
|
||||
return False
|
||||
self.pipe.set_state(gst.STATE_NULL)
|
||||
self.uribin.set_property('uri', uri)
|
||||
self.timeout_id = gobject.timeout_add(self.scan_timeout, self.process_timeout)
|
||||
self.pipe.set_state(gst.STATE_PLAYING)
|
||||
return True
|
||||
|
||||
|
||||
@ -2,12 +2,11 @@ from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
# pylint: disable = W0402
|
||||
import string
|
||||
# pylint: enable = W0402
|
||||
import sys
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
import glib
|
||||
|
||||
@ -51,7 +50,7 @@ def get_or_create_file(file_path):
|
||||
return file_path
|
||||
|
||||
|
||||
def path_to_uri(*paths):
|
||||
def path_to_uri(path):
|
||||
"""
|
||||
Convert OS specific path to file:// URI.
|
||||
|
||||
@ -61,17 +60,15 @@ def path_to_uri(*paths):
|
||||
|
||||
Returns a file:// URI as an unicode string.
|
||||
"""
|
||||
path = os.path.join(*paths)
|
||||
if isinstance(path, unicode):
|
||||
path = path.encode('utf-8')
|
||||
if sys.platform == 'win32':
|
||||
return 'file:' + urllib.quote(path)
|
||||
return 'file://' + urllib.quote(path)
|
||||
path = urllib.quote(path)
|
||||
return urlparse.urlunsplit((b'file', b'', path, b'', b''))
|
||||
|
||||
|
||||
def uri_to_path(uri):
|
||||
"""
|
||||
Convert the file:// to a OS specific path.
|
||||
Convert an URI to a OS specific path.
|
||||
|
||||
Returns a bytestring, since the file path can contain chars with other
|
||||
encoding than UTF-8.
|
||||
@ -82,10 +79,7 @@ def uri_to_path(uri):
|
||||
"""
|
||||
if isinstance(uri, unicode):
|
||||
uri = uri.encode('utf-8')
|
||||
if sys.platform == 'win32':
|
||||
return urllib.unquote(re.sub(b'^file:', b'', uri))
|
||||
else:
|
||||
return urllib.unquote(re.sub(b'^file://', b'', uri))
|
||||
return urllib.unquote(urlparse.urlsplit(uri).path)
|
||||
|
||||
|
||||
def split_path(path):
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
pyserial
|
||||
# Available as python-serial in Debian/Ubuntu
|
||||
@ -2,4 +2,5 @@ cherrypy >= 3.2.2
|
||||
# Available as python-cherrypy3 in Debian/Ubuntu
|
||||
|
||||
ws4py >= 0.2.3
|
||||
# Available as python-ws4py from apt.mopidy.com
|
||||
# Available as python-ws4py in newer Debian/Ubuntu and from apt.mopidy.com for
|
||||
# older releases of Debian/Ubuntu
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
pyspotify >= 1.9, < 1.11
|
||||
pyspotify >= 1.9, < 2
|
||||
# The libspotify Python wrapper
|
||||
# Available as the python-spotify package from apt.mopidy.com
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
[nosetests]
|
||||
verbosity = 1
|
||||
#with-coverage = 1
|
||||
cover-package = mopidy
|
||||
cover-inclusive = 1
|
||||
cover-html = 1
|
||||
4
setup.py
4
setup.py
@ -28,7 +28,7 @@ setup(
|
||||
'Pykka >= 1.1',
|
||||
],
|
||||
extras_require={
|
||||
'spotify': ['pyspotify >= 1.9, < 1.11'],
|
||||
'spotify': ['pyspotify >= 1.9, < 2'],
|
||||
'scrobbler': ['pylast >= 0.5.7'],
|
||||
'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'],
|
||||
},
|
||||
@ -36,7 +36,6 @@ setup(
|
||||
tests_require=[
|
||||
'nose',
|
||||
'mock >= 1.0',
|
||||
'unittest2',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
@ -61,7 +60,6 @@ setup(
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Multimedia :: Sound/Audio :: Players',
|
||||
],
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def path_to_data_dir(name):
|
||||
if not isinstance(name, bytes):
|
||||
name = name.encode(sys.getfilesystemencoding())
|
||||
name = name.encode('utf-8')
|
||||
path = os.path.dirname(__file__)
|
||||
path = os.path.join(path, b'data')
|
||||
path = os.path.abspath(path)
|
||||
|
||||
@ -21,6 +21,7 @@ class AudioTest(unittest.TestCase):
|
||||
'mixer': 'fakemixer track_max_volume=65536',
|
||||
'mixer_track': None,
|
||||
'output': 'fakesink',
|
||||
'visualizer': None,
|
||||
}
|
||||
}
|
||||
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
||||
@ -70,6 +71,7 @@ class AudioTest(unittest.TestCase):
|
||||
'mixer': 'fakemixer track_max_volume=40',
|
||||
'mixer_track': None,
|
||||
'output': 'fakesink',
|
||||
'visualizer': None,
|
||||
}
|
||||
}
|
||||
self.audio = audio.Audio.start(config=config).proxy()
|
||||
|
||||
@ -7,8 +7,6 @@ import pykka
|
||||
from mopidy import core
|
||||
from mopidy.models import Track, Album, Artist
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
|
||||
class LibraryControllerTest(object):
|
||||
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
|
||||
@ -17,13 +15,10 @@ class LibraryControllerTest(object):
|
||||
Album(name='album2', artists=artists[1:2]),
|
||||
Album()]
|
||||
tracks = [
|
||||
Track(
|
||||
uri='file://' + path_to_data_dir('uri1'), name='track1',
|
||||
artists=artists[:1], album=albums[0], date='2001-02-03',
|
||||
length=4000),
|
||||
Track(
|
||||
uri='file://' + path_to_data_dir('uri2'), name='track2',
|
||||
artists=artists[1:2], album=albums[1], date='2002', length=4000),
|
||||
Track(uri='local:track:path1', name='track1', artists=artists[:1],
|
||||
album=albums[0], date='2001-02-03', length=4000),
|
||||
Track(uri='local:track:path2', name='track2', artists=artists[1:2],
|
||||
album=albums[1], date='2002', length=4000),
|
||||
Track()]
|
||||
config = {}
|
||||
|
||||
@ -66,11 +61,11 @@ class LibraryControllerTest(object):
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
def test_find_exact_uri(self):
|
||||
track_1_uri = 'file://' + path_to_data_dir('uri1')
|
||||
track_1_uri = 'local:track:path1'
|
||||
result = self.library.find_exact(uri=track_1_uri)
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
track_2_uri = 'file://' + path_to_data_dir('uri2')
|
||||
track_2_uri = 'local:track:path2'
|
||||
result = self.library.find_exact(uri=track_2_uri)
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
@ -136,10 +131,10 @@ class LibraryControllerTest(object):
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
def test_search_uri(self):
|
||||
result = self.library.search(uri=['RI1'])
|
||||
result = self.library.search(uri=['TH1'])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(uri=['RI2'])
|
||||
result = self.library.search(uri=['TH2'])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_search_track(self):
|
||||
@ -183,7 +178,7 @@ class LibraryControllerTest(object):
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
result = self.library.search(any=['Bum1'])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
result = self.library.search(any=['RI1'])
|
||||
result = self.library.search(any=['TH1'])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
def test_search_wrong_type(self):
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
|
||||
song = path_to_data_dir('song%s.wav')
|
||||
generate_song = lambda i: path_to_uri(song % i)
|
||||
generate_song = lambda i: 'local:track:song%s.wav' % i
|
||||
|
||||
@ -5,7 +5,6 @@ import unittest
|
||||
from mopidy.backends.local import actor
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
from tests.backends.base.playback import PlaybackControllerTest
|
||||
@ -24,25 +23,25 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
|
||||
tracks = [
|
||||
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
|
||||
|
||||
def add_track(self, path):
|
||||
uri = path_to_uri(path_to_data_dir(path))
|
||||
def add_track(self, uri):
|
||||
track = Track(uri=uri, length=4464)
|
||||
self.tracklist.add([track])
|
||||
|
||||
def test_uri_scheme(self):
|
||||
self.assertIn('file', self.core.uri_schemes)
|
||||
self.assertNotIn('file', self.core.uri_schemes)
|
||||
self.assertIn('local', self.core.uri_schemes)
|
||||
|
||||
def test_play_mp3(self):
|
||||
self.add_track('blank.mp3')
|
||||
self.add_track('local:track:blank.mp3')
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
def test_play_ogg(self):
|
||||
self.add_track('blank.ogg')
|
||||
self.add_track('local:track:blank.ogg')
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
def test_play_flac(self):
|
||||
self.add_track('blank.flac')
|
||||
self.add_track('local:track:blank.flac')
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@ -7,7 +7,7 @@ import unittest
|
||||
|
||||
from mopidy.backends.local import actor
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils.path import path_to_uri
|
||||
from mopidy.utils.path import path_to_uri, uri_to_path
|
||||
|
||||
from tests import path_to_data_dir
|
||||
from tests.backends.base.playlists import (
|
||||
@ -89,21 +89,20 @@ class LocalPlaylistsControllerTest(
|
||||
|
||||
def test_playlist_contents_is_written_to_disk(self):
|
||||
track = Track(uri=generate_song(1))
|
||||
track_path = track.uri[len('file://'):]
|
||||
playlist = self.core.playlists.create('test')
|
||||
playlist_path = playlist.uri[len('file://'):]
|
||||
playlist_path = os.path.join(self.playlists_dir, 'test.m3u')
|
||||
playlist = playlist.copy(tracks=[track])
|
||||
playlist = self.core.playlists.save(playlist)
|
||||
|
||||
with open(playlist_path) as playlist_file:
|
||||
contents = playlist_file.read()
|
||||
|
||||
self.assertEqual(track_path, contents.strip())
|
||||
self.assertEqual(track.uri, contents.strip())
|
||||
|
||||
def test_playlists_are_loaded_at_startup(self):
|
||||
playlist_path = os.path.join(self.playlists_dir, 'test.m3u')
|
||||
|
||||
track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
|
||||
track = Track(uri='local:track:path2')
|
||||
playlist = self.core.playlists.create('test')
|
||||
playlist = playlist.copy(tracks=[track])
|
||||
playlist = self.core.playlists.save(playlist)
|
||||
@ -112,8 +111,7 @@ class LocalPlaylistsControllerTest(
|
||||
|
||||
self.assert_(backend.playlists.playlists)
|
||||
self.assertEqual(
|
||||
path_to_uri(playlist_path),
|
||||
backend.playlists.playlists[0].uri)
|
||||
'local:playlist:test', backend.playlists.playlists[0].uri)
|
||||
self.assertEqual(
|
||||
playlist.name, backend.playlists.playlists[0].name)
|
||||
self.assertEqual(
|
||||
|
||||
@ -98,7 +98,7 @@ expected_tracks = []
|
||||
|
||||
|
||||
def generate_track(path, ident):
|
||||
uri = path_to_uri(path_to_data_dir(path))
|
||||
uri = 'local:track:%s' % path
|
||||
track = Track(
|
||||
uri=uri, name='trackname', artists=expected_artists,
|
||||
album=expected_albums[0], track_no=1, date='2006', length=4000,
|
||||
@ -126,11 +126,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
||||
def test_simple_cache(self):
|
||||
tracks = parse_mpd_tag_cache(
|
||||
path_to_data_dir('simple_tag_cache'), path_to_data_dir(''))
|
||||
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||
track = Track(
|
||||
uri=uri, name='trackname', artists=expected_artists, track_no=1,
|
||||
album=expected_albums[0], date='2006', length=4000,
|
||||
last_modified=1272319626)
|
||||
uri='local:track:song1.mp3', name='trackname',
|
||||
artists=expected_artists, track_no=1, album=expected_albums[0],
|
||||
date='2006', length=4000, last_modified=1272319626)
|
||||
self.assertEqual(set([track]), tracks)
|
||||
|
||||
def test_advanced_cache(self):
|
||||
@ -142,12 +141,11 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
||||
tracks = parse_mpd_tag_cache(
|
||||
path_to_data_dir('utf8_tag_cache'), path_to_data_dir(''))
|
||||
|
||||
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||
artists = [Artist(name='æøå')]
|
||||
album = Album(name='æøå', artists=artists)
|
||||
track = Track(
|
||||
uri=uri, name='æøå', artists=artists, album=album, length=4000,
|
||||
last_modified=1272319626)
|
||||
uri='local:track:song1.mp3', name='æøå', artists=artists,
|
||||
album=album, length=4000, last_modified=1272319626)
|
||||
|
||||
self.assertEqual(track, list(tracks)[0])
|
||||
|
||||
@ -159,8 +157,8 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
||||
def test_cache_with_blank_track_info(self):
|
||||
tracks = parse_mpd_tag_cache(
|
||||
path_to_data_dir('blank_tag_cache'), path_to_data_dir(''))
|
||||
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||
expected = Track(uri=uri, length=4000, last_modified=1272319626)
|
||||
expected = Track(
|
||||
uri='local:track:song1.mp3', length=4000, last_modified=1272319626)
|
||||
self.assertEqual(set([expected]), tracks)
|
||||
|
||||
def test_musicbrainz_tagcache(self):
|
||||
@ -183,10 +181,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
||||
def test_albumartist_tag_cache(self):
|
||||
tracks = parse_mpd_tag_cache(
|
||||
path_to_data_dir('albumartist_tag_cache'), path_to_data_dir(''))
|
||||
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||
artist = Artist(name='albumartistname')
|
||||
album = expected_albums[0].copy(artists=[artist])
|
||||
track = Track(
|
||||
uri=uri, name='trackname', artists=expected_artists, track_no=1,
|
||||
album=album, date='2006', length=4000, last_modified=1272319626)
|
||||
uri='local:track:song1.mp3', name='trackname',
|
||||
artists=expected_artists, track_no=1, album=album, date='2006',
|
||||
length=4000, last_modified=1272319626)
|
||||
self.assertEqual(track, list(tracks)[0])
|
||||
|
||||
@ -5,7 +5,6 @@ from __future__ import unicode_literals
|
||||
import logging
|
||||
import mock
|
||||
import socket
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from mopidy.config import types
|
||||
@ -99,6 +98,11 @@ class StringTest(unittest.TestCase):
|
||||
self.assertIsInstance(result, bytes)
|
||||
self.assertEqual(b'', result)
|
||||
|
||||
def test_deserialize_enforces_choices_optional(self):
|
||||
value = types.String(optional=True, choices=['foo', 'bar', 'baz'])
|
||||
self.assertEqual(None, value.deserialize(b''))
|
||||
self.assertRaises(ValueError, value.deserialize, b'foobar')
|
||||
|
||||
|
||||
class SecretTest(unittest.TestCase):
|
||||
def test_deserialize_passes_through(self):
|
||||
@ -164,6 +168,10 @@ class IntegerTest(unittest.TestCase):
|
||||
self.assertEqual(5, value.deserialize('5'))
|
||||
self.assertRaises(ValueError, value.deserialize, '15')
|
||||
|
||||
def test_deserialize_respects_optional(self):
|
||||
value = types.Integer(optional=True)
|
||||
self.assertEqual(None, value.deserialize(''))
|
||||
|
||||
|
||||
class BooleanTest(unittest.TestCase):
|
||||
def test_deserialize_conversion_success(self):
|
||||
@ -367,7 +375,4 @@ class PathTest(unittest.TestCase):
|
||||
|
||||
def test_serialize_unicode_string(self):
|
||||
value = types.Path()
|
||||
expected = 'æøå'.encode(sys.getfilesystemencoding())
|
||||
result = value.serialize('æøå')
|
||||
self.assertEqual(expected, result)
|
||||
self.assertIsInstance(result, bytes)
|
||||
self.assertRaises(ValueError, value.serialize, 'æøå')
|
||||
|
||||
@ -3,22 +3,22 @@ mpd_version: 0.14.2
|
||||
fs_charset: UTF-8
|
||||
info_end
|
||||
songList begin
|
||||
key: uri1
|
||||
file: /uri1
|
||||
key: key1
|
||||
file: /path1
|
||||
Artist: artist1
|
||||
Title: track1
|
||||
Album: album1
|
||||
Date: 2001-02-03
|
||||
Time: 4
|
||||
key: uri2
|
||||
file: /uri2
|
||||
key: key1
|
||||
file: /path2
|
||||
Artist: artist2
|
||||
Title: track2
|
||||
Album: album2
|
||||
Date: 2002
|
||||
Time: 4
|
||||
key: uri3
|
||||
file: /uri3
|
||||
key: key3
|
||||
file: /path3
|
||||
Artist: artist3
|
||||
Title: track3
|
||||
Album: album3
|
||||
|
||||
BIN
tests/data/scanner/empty.wav
Normal file
BIN
tests/data/scanner/empty.wav
Normal file
Binary file not shown.
BIN
tests/data/scanner/example.log
Normal file
BIN
tests/data/scanner/example.log
Normal file
Binary file not shown.
@ -7,7 +7,19 @@ from tests.frontends.mpd import protocol
|
||||
|
||||
class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
def test_count(self):
|
||||
self.sendRequest('count "tag" "needle"')
|
||||
self.sendRequest('count "artist" "needle"')
|
||||
self.assertInResponse('songs: 0')
|
||||
self.assertInResponse('playtime: 0')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_count_without_quotes(self):
|
||||
self.sendRequest('count artist "needle"')
|
||||
self.assertInResponse('songs: 0')
|
||||
self.assertInResponse('playtime: 0')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_count_with_multiple_pairs(self):
|
||||
self.sendRequest('count "artist" "foo" "album" "bar"')
|
||||
self.assertInResponse('songs: 0')
|
||||
self.assertInResponse('playtime: 0')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
@ -107,6 +107,30 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
|
||||
self.assertNotInResponse('playlist: ')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_listplaylists_replaces_newline_with_space(self):
|
||||
self.backend.playlists.playlists = [
|
||||
Playlist(name='a\n', uri='dummy:')]
|
||||
self.sendRequest('listplaylists')
|
||||
self.assertInResponse('playlist: a ')
|
||||
self.assertNotInResponse('playlist: a\n')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_listplaylists_replaces_carriage_return_with_space(self):
|
||||
self.backend.playlists.playlists = [
|
||||
Playlist(name='a\r', uri='dummy:')]
|
||||
self.sendRequest('listplaylists')
|
||||
self.assertInResponse('playlist: a ')
|
||||
self.assertNotInResponse('playlist: a\r')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_listplaylists_replaces_forward_slash_with_space(self):
|
||||
self.backend.playlists.playlists = [
|
||||
Playlist(name='a/', uri='dummy:')]
|
||||
self.sendRequest('listplaylists')
|
||||
self.assertInResponse('playlist: a ')
|
||||
self.assertNotInResponse('playlist: a/')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_load_appends_to_tracklist(self):
|
||||
self.core.tracklist.add([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(len(self.core.tracklist.tracks.get()), 2)
|
||||
|
||||
@ -26,6 +26,8 @@ class TranslatorTest(unittest.TestCase):
|
||||
'album-artist': 'albumartistname',
|
||||
'title': 'trackname',
|
||||
'track-count': 2,
|
||||
'album-disc-number': 2,
|
||||
'album-disc-count': 3,
|
||||
'date': FakeGstDate(2006, 1, 1,),
|
||||
'container-format': 'ID3 tag',
|
||||
'duration': 4531,
|
||||
@ -39,6 +41,7 @@ class TranslatorTest(unittest.TestCase):
|
||||
self.album = {
|
||||
'name': 'albumname',
|
||||
'num_tracks': 2,
|
||||
'num_discs': 3,
|
||||
'musicbrainz_id': 'mbalbumid',
|
||||
}
|
||||
|
||||
@ -57,6 +60,7 @@ class TranslatorTest(unittest.TestCase):
|
||||
'name': 'trackname',
|
||||
'date': '2006-01-01',
|
||||
'track_no': 1,
|
||||
'disc_no': 2,
|
||||
'length': 4531,
|
||||
'musicbrainz_id': 'mbtrackid',
|
||||
'last_modified': 1234,
|
||||
@ -206,6 +210,14 @@ class ScannerTest(unittest.TestCase):
|
||||
self.scan('scanner/image')
|
||||
self.assert_(self.errors)
|
||||
|
||||
def test_log_file_is_ignored(self):
|
||||
self.scan('scanner/example.log')
|
||||
self.assert_(self.errors)
|
||||
|
||||
def test_empty_wav_file_is_ignored(self):
|
||||
self.scan('scanner/empty.wav')
|
||||
self.assert_(self.errors)
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_song_without_time_is_handeled(self):
|
||||
pass
|
||||
|
||||
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
@ -117,86 +116,42 @@ class GetOrCreateFileTest(unittest.TestCase):
|
||||
|
||||
class PathToFileURITest(unittest.TestCase):
|
||||
def test_simple_path(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.path_to_uri('C:/WINDOWS/clock.avi')
|
||||
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
|
||||
else:
|
||||
result = path.path_to_uri('/etc/fstab')
|
||||
self.assertEqual(result, 'file:///etc/fstab')
|
||||
|
||||
def test_dir_and_path(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.path_to_uri('C:/WINDOWS/', 'clock.avi')
|
||||
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
|
||||
else:
|
||||
result = path.path_to_uri('/etc', 'fstab')
|
||||
self.assertEqual(result, 'file:///etc/fstab')
|
||||
result = path.path_to_uri('/etc/fstab')
|
||||
self.assertEqual(result, 'file:///etc/fstab')
|
||||
|
||||
def test_space_in_path(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.path_to_uri('C:/test this')
|
||||
self.assertEqual(result, 'file:///C://test%20this')
|
||||
else:
|
||||
result = path.path_to_uri('/tmp/test this')
|
||||
self.assertEqual(result, 'file:///tmp/test%20this')
|
||||
result = path.path_to_uri('/tmp/test this')
|
||||
self.assertEqual(result, 'file:///tmp/test%20this')
|
||||
|
||||
def test_unicode_in_path(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.path_to_uri('C:/æøå')
|
||||
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
|
||||
else:
|
||||
result = path.path_to_uri('/tmp/æøå')
|
||||
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
|
||||
result = path.path_to_uri('/tmp/æøå')
|
||||
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
|
||||
|
||||
def test_utf8_in_path(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.path_to_uri('C:/æøå'.encode('utf-8'))
|
||||
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
|
||||
else:
|
||||
result = path.path_to_uri('/tmp/æøå'.encode('utf-8'))
|
||||
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
|
||||
result = path.path_to_uri('/tmp/æøå'.encode('utf-8'))
|
||||
self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
|
||||
|
||||
def test_latin1_in_path(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.path_to_uri('C:/æøå'.encode('latin-1'))
|
||||
self.assertEqual(result, 'file:///C://%E6%F8%E5')
|
||||
else:
|
||||
result = path.path_to_uri('/tmp/æøå'.encode('latin-1'))
|
||||
self.assertEqual(result, 'file:///tmp/%E6%F8%E5')
|
||||
result = path.path_to_uri('/tmp/æøå'.encode('latin-1'))
|
||||
self.assertEqual(result, 'file:///tmp/%E6%F8%E5')
|
||||
|
||||
|
||||
class UriToPathTest(unittest.TestCase):
|
||||
def test_simple_uri(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.uri_to_path('file:///C://WINDOWS/clock.avi')
|
||||
self.assertEqual(result, 'C:/WINDOWS/clock.avi'.encode('utf-8'))
|
||||
else:
|
||||
result = path.uri_to_path('file:///etc/fstab')
|
||||
self.assertEqual(result, '/etc/fstab'.encode('utf-8'))
|
||||
result = path.uri_to_path('file:///etc/fstab')
|
||||
self.assertEqual(result, '/etc/fstab'.encode('utf-8'))
|
||||
|
||||
def test_space_in_uri(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.uri_to_path('file:///C://test%20this')
|
||||
self.assertEqual(result, 'C:/test this'.encode('utf-8'))
|
||||
else:
|
||||
result = path.uri_to_path('file:///tmp/test%20this')
|
||||
self.assertEqual(result, '/tmp/test this'.encode('utf-8'))
|
||||
result = path.uri_to_path('file:///tmp/test%20this')
|
||||
self.assertEqual(result, '/tmp/test this'.encode('utf-8'))
|
||||
|
||||
def test_unicode_in_uri(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.uri_to_path('file:///C://%C3%A6%C3%B8%C3%A5')
|
||||
self.assertEqual(result, 'C:/æøå'.encode('utf-8'))
|
||||
else:
|
||||
result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5')
|
||||
self.assertEqual(result, '/tmp/æøå'.encode('utf-8'))
|
||||
result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5')
|
||||
self.assertEqual(result, '/tmp/æøå'.encode('utf-8'))
|
||||
|
||||
def test_latin1_in_uri(self):
|
||||
if sys.platform == 'win32':
|
||||
result = path.uri_to_path('file:///C://%E6%F8%E5')
|
||||
self.assertEqual(result, 'C:/æøå'.encode('latin-1'))
|
||||
else:
|
||||
result = path.uri_to_path('file:///tmp/%E6%F8%E5')
|
||||
self.assertEqual(result, '/tmp/æøå'.encode('latin-1'))
|
||||
result = path.uri_to_path('file:///tmp/%E6%F8%E5')
|
||||
self.assertEqual(result, '/tmp/æøå'.encode('latin-1'))
|
||||
|
||||
|
||||
class SplitPathTest(unittest.TestCase):
|
||||
|
||||
@ -37,5 +37,6 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertLess(SV('0.11.1'), SV('0.12.0'))
|
||||
self.assertLess(SV('0.12.0'), SV('0.13.0'))
|
||||
self.assertLess(SV('0.13.0'), SV('0.14.0'))
|
||||
self.assertLess(SV('0.14.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.14.2'))
|
||||
self.assertLess(SV('0.14.0'), SV('0.14.1'))
|
||||
self.assertLess(SV('0.14.1'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.14.3'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user