m3u: Extract new M3U backend from local

Fixes #1054
This commit is contained in:
Stein Magnus Jodal 2015-03-19 00:02:52 +01:00
parent 8977f71411
commit b2f60bc338
18 changed files with 312 additions and 159 deletions

View File

@ -126,6 +126,11 @@ v1.0.0 (UNRELEASED)
- Sort local playlists by name. (Fixes: :issue:`1026`, PR: :issue:`1028`) - Sort local playlists by name. (Fixes: :issue:`1026`, PR: :issue:`1028`)
- Moved playlist support out to a new extension, :ref:`ext-m3u`.
- *Deprecated:* The config value :confval:`local/playlists_dir` is no longer in
use and can be removed from your config.
**Local library API** **Local library API**
- Implementors of :meth:`mopidy.local.Library.lookup` should now return a list - Implementors of :meth:`mopidy.local.Library.lookup` should now return a list
@ -139,6 +144,12 @@ v1.0.0 (UNRELEASED)
- Add :meth:`mopidy.local.Library.get_images` for looking up images - Add :meth:`mopidy.local.Library.get_images` for looking up images
for local URIs. (Fixes: :issue:`1031`, PR: :issue:`1032` and :issue:`1037`) for local URIs. (Fixes: :issue:`1031`, PR: :issue:`1032` and :issue:`1037`)
**M3U backend**
- Split the M3U playlist handling out of the local backend. See
:ref:`m3u-migration` for how to migrate your local playlists. (Fixes:
:issue:`1054`, PR: :issue:`1066`)
**MPD frontend** **MPD frontend**
- In stored playlist names, replace "/", which are illegal, with "|" instead of - In stored playlist names, replace "/", which are illegal, with "|" instead of

55
docs/ext/m3u.rst Normal file
View File

@ -0,0 +1,55 @@
.. _ext-m3u:
**********
Mopidy-M3U
**********
Mopidy-M3U is an extension for reading and writing M3U playlists stored
on disk. It is bundled with Mopidy and enabled by default.
This backend handles URIs starting with ``m3u:``.
.. _m3u-migration:
Migrating from Mopidy-Local playlists
=====================================
Mopidy-M3U was split out of the Mopidy-Local extension in Mopidy 1.0. To
migrate your playlists from Mopidy-Local, simply move them from the
:confval:`local/playlists_dir` directory to the :confval:`m3u/playlists_dir`
directory. Assuming you have not changed the default config, run the following
commands to migrate::
mkdir -p ~/.local/share/mopidy/m3u/
mv ~/.local/share/mopidy/local/playlists/* ~/.local/share/mopidy/m3u/
Editing playlists
=================
There is a core playlist API in place for editing playlists. This is supported
by a few Mopidy clients, but not through Mopidy's MPD server yet.
It is possible to edit playlists by editing the M3U files located in the
:confval:`m3u/playlists_dir` directory, usually
:file:`~/.local/share/mopidy/m3u/`, by hand with a text editor. See `Wikipedia
<https://en.wikipedia.org/wiki/M3U>`__ for a short description of the quite
simple M3U playlist format.
Configuration
=============
See :ref:`config` for general help on configuring Mopidy.
.. literalinclude:: ../../mopidy/m3u/ext.conf
:language: ini
.. confval:: m3u/enabled
If the M3U extension should be enabled or not.
.. confval:: m3u/playlists_dir
Path to directory with M3U files.

View File

@ -94,6 +94,7 @@ Extensions
:maxdepth: 2 :maxdepth: 2
ext/local ext/local
ext/m3u
ext/stream ext/stream
ext/http ext/http
ext/mpd ext/mpd

View File

@ -24,7 +24,7 @@ class Extension(ext.Extension):
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.Path()
schema['playlists_dir'] = config.Path() 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(
minimum=1000, maximum=1000 * 60 * 60) minimum=1000, maximum=1000 * 60 * 60)

View File

@ -8,7 +8,6 @@ from mopidy import backend
from mopidy.local import storage from mopidy.local import storage
from mopidy.local.library import LocalLibraryProvider from mopidy.local.library import LocalLibraryProvider
from mopidy.local.playback import LocalPlaybackProvider from mopidy.local.playback import LocalPlaybackProvider
from mopidy.local.playlists import LocalPlaylistsProvider
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,5 +35,4 @@ class LocalBackend(pykka.ThreadingActor, backend.Backend):
logger.warning('Local library %s not found', library_name) logger.warning('Local library %s not found', library_name)
self.playback = LocalPlaybackProvider(audio=audio, backend=self) self.playback = LocalPlaybackProvider(audio=audio, backend=self)
self.playlists = LocalPlaylistsProvider(backend=self)
self.library = LocalLibraryProvider(backend=self, library=library) self.library = LocalLibraryProvider(backend=self, library=library)

View File

@ -3,7 +3,6 @@ enabled = true
library = json library = json
media_dir = $XDG_MUSIC_DIR media_dir = $XDG_MUSIC_DIR
data_dir = $XDG_DATA_DIR/mopidy/local data_dir = $XDG_DATA_DIR/mopidy/local
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
scan_timeout = 1000 scan_timeout = 1000
scan_flush_threshold = 1000 scan_flush_threshold = 1000
scan_follow_symlinks = false scan_follow_symlinks = false

View File

@ -20,11 +20,3 @@ def check_dirs_and_files(config):
logger.warning( logger.warning(
'Could not create local data dir: %s', 'Could not create local data dir: %s',
encoding.locale_decode(error)) encoding.locale_decode(error))
# TODO: replace with data dir?
try:
path.get_or_create_dir(config['local']['playlists_dir'])
except EnvironmentError as error:
logger.warning(
'Could not create local playlists dir: %s',
encoding.locale_decode(error))

View File

@ -2,18 +2,12 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
import os import os
import re
import urllib import urllib
import urlparse
from mopidy import compat from mopidy import compat
from mopidy.models import Track
from mopidy.utils.encoding import locale_decode
from mopidy.utils.path import path_to_uri, uri_to_path from mopidy.utils.path import path_to_uri, uri_to_path
M3U_EXTINF_RE = re.compile(r'#EXTINF:(-1|\d+),(.*)')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,13 +22,6 @@ def local_track_uri_to_path(uri, media_dir):
return os.path.join(media_dir, file_path) return os.path.join(media_dir, file_path)
def local_playlist_uri_to_path(uri, playlists_dir):
if not uri.startswith('local:playlist:'):
raise ValueError('Invalid URI %s' % uri)
file_path = uri_to_path(uri).split(b':', 1)[1]
return os.path.join(playlists_dir, file_path)
def path_to_local_track_uri(relpath): def path_to_local_track_uri(relpath):
"""Convert path relative to media_dir to local track URI.""" """Convert path relative to media_dir to local track URI."""
if isinstance(relpath, compat.text_type): if isinstance(relpath, compat.text_type):
@ -47,89 +34,3 @@ def path_to_local_directory_uri(relpath):
if isinstance(relpath, compat.text_type): if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8') relpath = relpath.encode('utf-8')
return b'local:directory:%s' % urllib.quote(relpath) return b'local:directory:%s' % urllib.quote(relpath)
def path_to_local_playlist_uri(relpath):
"""Convert path relative to playlists_dir to local playlist URI."""
if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8')
return b'local:playlist:%s' % urllib.quote(relpath)
def m3u_extinf_to_track(line):
"""Convert extended M3U directive to track template."""
m = M3U_EXTINF_RE.match(line)
if not m:
logger.warning('Invalid extended M3U directive: %s', line)
return Track()
(runtime, title) = m.groups()
if int(runtime) > 0:
return Track(name=title, length=1000 * int(runtime))
else:
return Track(name=title)
def parse_m3u(file_path, media_dir):
r"""
Convert M3U file list to list of tracks
Example M3U data::
# This is a comment
Alternative\Band - Song.mp3
Classical\Other Band - New Song.mp3
Stuff.mp3
D:\More Music\Foo.mp3
http://www.example.com:8000/Listen.pls
http://www.example.com/~user/Mine.mp3
Example extended M3U data::
#EXTM3U
#EXTINF:123, Sample artist - Sample title
Sample.mp3
#EXTINF:321,Example Artist - Example title
Greatest Hits\Example.ogg
#EXTINF:-1,Radio XMP
http://mp3stream.example.com:8000/
- Relative paths of songs should be with respect to location of M3U.
- Paths are normally platform specific.
- Lines starting with # are ignored, except for extended M3U directives.
- Track.name and Track.length are set from extended M3U directives.
- m3u files are latin-1.
"""
# TODO: uris as bytes
tracks = []
try:
with open(file_path) as m3u:
contents = m3u.readlines()
except IOError as error:
logger.warning('Couldn\'t open m3u: %s', locale_decode(error))
return tracks
if not contents:
return tracks
extended = contents[0].decode('latin1').startswith('#EXTM3U')
track = Track()
for line in contents:
line = line.strip().decode('latin1')
if line.startswith('#'):
if extended and line.startswith('#EXTINF'):
track = m3u_extinf_to_track(line)
continue
if urlparse.urlsplit(line).scheme:
tracks.append(track.copy(uri=line))
elif os.path.normpath(line) == os.path.abspath(line):
path = path_to_uri(line)
tracks.append(track.copy(uri=path))
else:
path = path_to_uri(os.path.join(media_dir, line))
tracks.append(track.copy(uri=path))
track = Track()
return tracks

30
mopidy/m3u/__init__.py Normal file
View File

@ -0,0 +1,30 @@
from __future__ import absolute_import, unicode_literals
import logging
import os
import mopidy
from mopidy import config, ext
logger = logging.getLogger(__name__)
class Extension(ext.Extension):
dist_name = 'Mopidy-M3U'
ext_name = 'm3u'
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
def get_config_schema(self):
schema = super(Extension, self).get_config_schema()
schema['playlists_dir'] = config.Path()
return schema
def setup(self, registry):
from .actor import M3UBackend
registry.add('backend', M3UBackend)

32
mopidy/m3u/actor.py Normal file
View File

@ -0,0 +1,32 @@
from __future__ import absolute_import, unicode_literals
import logging
import pykka
from mopidy import backend
from mopidy.m3u.library import M3ULibraryProvider
from mopidy.m3u.playlists import M3UPlaylistsProvider
from mopidy.utils import encoding, path
logger = logging.getLogger(__name__)
class M3UBackend(pykka.ThreadingActor, backend.Backend):
uri_schemes = ['m3u']
def __init__(self, config, audio):
super(M3UBackend, self).__init__()
self._config = config
try:
path.get_or_create_dir(config['m3u']['playlists_dir'])
except EnvironmentError as error:
logger.warning(
'Could not create M3U playlists dir: %s',
encoding.locale_decode(error))
self.playlists = M3UPlaylistsProvider(backend=self)
self.library = M3ULibraryProvider(backend=self)

3
mopidy/m3u/ext.conf Normal file
View File

@ -0,0 +1,3 @@
[m3u]
enabled = true
playlists_dir = $XDG_DATA_DIR/mopidy/m3u

18
mopidy/m3u/library.py Normal file
View File

@ -0,0 +1,18 @@
from __future__ import absolute_import, unicode_literals
import logging
from mopidy import backend
logger = logging.getLogger(__name__)
class M3ULibraryProvider(backend.LibraryProvider):
"""Library for looking up M3U playlists."""
def __init__(self, backend):
super(M3ULibraryProvider, self).__init__(backend)
def lookup(self, uri):
# TODO Lookup tracks in M3U playlist
return []

View File

@ -8,19 +8,18 @@ import os
import sys import sys
from mopidy import backend from mopidy import backend
from mopidy.m3u import translator
from mopidy.models import Playlist from mopidy.models import Playlist
from .translator import local_playlist_uri_to_path, path_to_local_playlist_uri
from .translator import parse_m3u
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LocalPlaylistsProvider(backend.PlaylistsProvider): class M3UPlaylistsProvider(backend.PlaylistsProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LocalPlaylistsProvider, self).__init__(*args, **kwargs) super(M3UPlaylistsProvider, self).__init__(*args, **kwargs)
self._media_dir = self.backend.config['local']['media_dir']
self._playlists_dir = self.backend.config['local']['playlists_dir'] self._playlists_dir = self.backend._config['m3u']['playlists_dir']
self._playlists = [] self._playlists = []
self.refresh() self.refresh()
@ -49,7 +48,7 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
if not playlist: if not playlist:
logger.warn('Trying to delete unknown playlist %s', uri) logger.warn('Trying to delete unknown playlist %s', uri)
return return
path = local_playlist_uri_to_path(uri, self._playlists_dir) path = translator.playlist_uri_to_path(uri, self._playlists_dir)
if os.path.exists(path): if os.path.exists(path):
os.remove(path) os.remove(path)
else: else:
@ -70,10 +69,10 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
for path in glob.glob(os.path.join(self._playlists_dir, b'*.m3u')): for path in glob.glob(os.path.join(self._playlists_dir, b'*.m3u')):
relpath = os.path.basename(path) relpath = os.path.basename(path)
name = os.path.splitext(relpath)[0].decode(encoding) name = os.path.splitext(relpath)[0].decode(encoding)
uri = path_to_local_playlist_uri(relpath) uri = translator.path_to_playlist_uri(relpath)
tracks = [] tracks = []
for track in parse_m3u(path, self._media_dir): for track in translator.parse_m3u(path):
tracks.append(track) tracks.append(track)
playlist = Playlist(uri=uri, name=name, tracks=tracks) playlist = Playlist(uri=uri, name=name, tracks=tracks)
@ -82,7 +81,7 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
self.playlists = sorted(playlists, key=operator.attrgetter('name')) self.playlists = sorted(playlists, key=operator.attrgetter('name'))
logger.info( logger.info(
'Loaded %d local playlists from %s', 'Loaded %d M3U playlists from %s',
len(playlists), self._playlists_dir) len(playlists), self._playlists_dir)
def save(self, playlist): def save(self, playlist):
@ -99,7 +98,7 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
playlist = self._save_m3u(playlist) playlist = self._save_m3u(playlist)
if index >= 0 and uri != playlist.uri: if index >= 0 and uri != playlist.uri:
path = local_playlist_uri_to_path(uri, self._playlists_dir) path = translator.playlist_uri_to_path(uri, self._playlists_dir)
if os.path.exists(path): if os.path.exists(path):
os.remove(path) os.remove(path)
else: else:
@ -125,11 +124,12 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
def _save_m3u(self, playlist, encoding=sys.getfilesystemencoding()): def _save_m3u(self, playlist, encoding=sys.getfilesystemencoding()):
if playlist.name: if playlist.name:
name = self._sanitize_m3u_name(playlist.name, encoding) name = self._sanitize_m3u_name(playlist.name, encoding)
uri = path_to_local_playlist_uri(name.encode(encoding) + b'.m3u') uri = translator.path_to_playlist_uri(
path = local_playlist_uri_to_path(uri, self._playlists_dir) name.encode(encoding) + b'.m3u')
path = translator.playlist_uri_to_path(uri, self._playlists_dir)
elif playlist.uri: elif playlist.uri:
uri = playlist.uri uri = playlist.uri
path = local_playlist_uri_to_path(uri, self._playlists_dir) path = translator.playlist_uri_to_path(uri, self._playlists_dir)
name, _ = os.path.splitext(os.path.basename(path).decode(encoding)) name, _ = os.path.splitext(os.path.basename(path).decode(encoding))
else: else:
raise ValueError('M3U playlist needs name or URI') raise ValueError('M3U playlist needs name or URI')

110
mopidy/m3u/translator.py Normal file
View File

@ -0,0 +1,110 @@
from __future__ import absolute_import, unicode_literals
import logging
import os
import re
import urllib
import urlparse
from mopidy import compat
from mopidy.models import Track
from mopidy.utils.encoding import locale_decode
from mopidy.utils.path import path_to_uri, uri_to_path
M3U_EXTINF_RE = re.compile(r'#EXTINF:(-1|\d+),(.*)')
logger = logging.getLogger(__name__)
def playlist_uri_to_path(uri, playlists_dir):
if not uri.startswith('m3u:'):
raise ValueError('Invalid URI %s' % uri)
file_path = uri_to_path(uri)
return os.path.join(playlists_dir, file_path)
def path_to_playlist_uri(relpath):
"""Convert path relative to playlists_dir to M3U URI."""
if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8')
return b'm3u:%s' % urllib.quote(relpath)
def m3u_extinf_to_track(line):
"""Convert extended M3U directive to track template."""
m = M3U_EXTINF_RE.match(line)
if not m:
logger.warning('Invalid extended M3U directive: %s', line)
return Track()
(runtime, title) = m.groups()
if int(runtime) > 0:
return Track(name=title, length=1000 * int(runtime))
else:
return Track(name=title)
def parse_m3u(file_path, media_dir=None):
r"""
Convert M3U file list to list of tracks
Example M3U data::
# This is a comment
Alternative\Band - Song.mp3
Classical\Other Band - New Song.mp3
Stuff.mp3
D:\More Music\Foo.mp3
http://www.example.com:8000/Listen.pls
http://www.example.com/~user/Mine.mp3
Example extended M3U data::
#EXTM3U
#EXTINF:123, Sample artist - Sample title
Sample.mp3
#EXTINF:321,Example Artist - Example title
Greatest Hits\Example.ogg
#EXTINF:-1,Radio XMP
http://mp3stream.example.com:8000/
- Relative paths of songs should be with respect to location of M3U.
- Paths are normally platform specific.
- Lines starting with # are ignored, except for extended M3U directives.
- Track.name and Track.length are set from extended M3U directives.
- m3u files are latin-1.
"""
# TODO: uris as bytes
tracks = []
try:
with open(file_path) as m3u:
contents = m3u.readlines()
except IOError as error:
logger.warning('Couldn\'t open m3u: %s', locale_decode(error))
return tracks
if not contents:
return tracks
extended = contents[0].decode('latin1').startswith('#EXTM3U')
track = Track()
for line in contents:
line = line.strip().decode('latin1')
if line.startswith('#'):
if extended and line.startswith('#EXTINF'):
track = m3u_extinf_to_track(line)
continue
if urlparse.urlsplit(line).scheme:
tracks.append(track.copy(uri=line))
elif os.path.normpath(line) == os.path.abspath(line):
path = path_to_uri(line)
tracks.append(track.copy(uri=path))
elif media_dir is not None:
path = path_to_uri(os.path.join(media_dir, line))
tracks.append(track.copy(uri=path))
track = Track()
return tracks

View File

@ -36,6 +36,7 @@ setup(
'mopidy.ext': [ 'mopidy.ext': [
'http = mopidy.http:Extension', 'http = mopidy.http:Extension',
'local = mopidy.local:Extension', 'local = mopidy.local:Extension',
'm3u = mopidy.m3u:Extension',
'mpd = mopidy.mpd:Extension', 'mpd = mopidy.mpd:Extension',
'softwaremixer = mopidy.softwaremixer:Extension', 'softwaremixer = mopidy.softwaremixer:Extension',
'stream = mopidy.stream:Extension', 'stream = mopidy.stream:Extension',

5
tests/m3u/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from __future__ import absolute_import, unicode_literals
def generate_song(i):
return 'dummy:track:song%s' % i

View File

@ -8,30 +8,28 @@ import unittest
import pykka import pykka
from mopidy import core from mopidy import core
from mopidy.local import actor from mopidy.m3u import actor
from mopidy.local.translator import local_playlist_uri_to_path from mopidy.m3u.translator import playlist_uri_to_path
from mopidy.models import Playlist, Track from mopidy.models import Playlist, Track
from tests import dummy_audio, path_to_data_dir from tests import dummy_audio, path_to_data_dir
from tests.local import generate_song from tests.m3u import generate_song
class LocalPlaylistsProviderTest(unittest.TestCase): class M3UPlaylistsProviderTest(unittest.TestCase):
backend_class = actor.LocalBackend backend_class = actor.M3UBackend
config = { config = {
'local': { 'm3u': {
'media_dir': path_to_data_dir(''), 'playlists_dir': path_to_data_dir(''),
'data_dir': path_to_data_dir(''),
'library': 'json',
} }
} }
def setUp(self): # noqa: N802 def setUp(self): # noqa: N802
self.config['local']['playlists_dir'] = tempfile.mkdtemp() self.config['m3u']['playlists_dir'] = tempfile.mkdtemp()
self.playlists_dir = self.config['local']['playlists_dir'] self.playlists_dir = self.config['m3u']['playlists_dir']
self.audio = dummy_audio.create_proxy() self.audio = dummy_audio.create_proxy()
self.backend = actor.LocalBackend.start( self.backend = actor.M3UBackend.start(
config=self.config, audio=self.audio).proxy() config=self.config, audio=self.audio).proxy()
self.core = core.Core(backends=[self.backend]) self.core = core.Core(backends=[self.backend])
@ -42,8 +40,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
shutil.rmtree(self.playlists_dir) shutil.rmtree(self.playlists_dir)
def test_created_playlist_is_persisted(self): def test_created_playlist_is_persisted(self):
uri = 'local:playlist:test.m3u' uri = 'm3u:test.m3u'
path = local_playlist_uri_to_path(uri, self.playlists_dir) path = playlist_uri_to_path(uri, self.playlists_dir)
self.assertFalse(os.path.exists(path)) self.assertFalse(os.path.exists(path))
playlist = self.core.playlists.create('test') playlist = self.core.playlists.create('test')
@ -54,16 +52,16 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
def test_create_sanitizes_playlist_name(self): def test_create_sanitizes_playlist_name(self):
playlist = self.core.playlists.create('../../test FOO baR') playlist = self.core.playlists.create('../../test FOO baR')
self.assertEqual('test FOO baR', playlist.name) self.assertEqual('test FOO baR', playlist.name)
path = local_playlist_uri_to_path(playlist.uri, self.playlists_dir) path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
self.assertEqual(self.playlists_dir, os.path.dirname(path)) self.assertEqual(self.playlists_dir, os.path.dirname(path))
self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.exists(path))
def test_saved_playlist_is_persisted(self): def test_saved_playlist_is_persisted(self):
uri1 = 'local:playlist:test1.m3u' uri1 = 'm3u:test1.m3u'
uri2 = 'local:playlist:test2.m3u' uri2 = 'm3u:test2.m3u'
path1 = local_playlist_uri_to_path(uri1, self.playlists_dir) path1 = playlist_uri_to_path(uri1, self.playlists_dir)
path2 = local_playlist_uri_to_path(uri2, self.playlists_dir) path2 = playlist_uri_to_path(uri2, self.playlists_dir)
playlist = self.core.playlists.create('test1') playlist = self.core.playlists.create('test1')
self.assertEqual('test1', playlist.name) self.assertEqual('test1', playlist.name)
@ -78,8 +76,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
self.assertTrue(os.path.exists(path2)) self.assertTrue(os.path.exists(path2))
def test_deleted_playlist_is_removed(self): def test_deleted_playlist_is_removed(self):
uri = 'local:playlist:test.m3u' uri = 'm3u:test.m3u'
path = local_playlist_uri_to_path(uri, self.playlists_dir) path = playlist_uri_to_path(uri, self.playlists_dir)
self.assertFalse(os.path.exists(path)) self.assertFalse(os.path.exists(path))
@ -95,7 +93,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
track = Track(uri=generate_song(1)) track = Track(uri=generate_song(1))
playlist = self.core.playlists.create('test') playlist = self.core.playlists.create('test')
playlist = self.core.playlists.save(playlist.copy(tracks=[track])) playlist = self.core.playlists.save(playlist.copy(tracks=[track]))
path = local_playlist_uri_to_path(playlist.uri, self.playlists_dir) path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
with open(path) as f: with open(path) as f:
contents = f.read() contents = f.read()
@ -106,7 +104,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
track = Track(uri=generate_song(1), name='Test', length=60000) track = Track(uri=generate_song(1), name='Test', length=60000)
playlist = self.core.playlists.create('test') playlist = self.core.playlists.create('test')
playlist = self.core.playlists.save(playlist.copy(tracks=[track])) playlist = self.core.playlists.save(playlist.copy(tracks=[track]))
path = local_playlist_uri_to_path(playlist.uri, self.playlists_dir) path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
with open(path) as f: with open(path) as f:
contents = f.read().splitlines() contents = f.read().splitlines()
@ -114,7 +112,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
self.assertEqual(contents, ['#EXTM3U', '#EXTINF:60,Test', track.uri]) self.assertEqual(contents, ['#EXTM3U', '#EXTINF:60,Test', track.uri])
def test_playlists_are_loaded_at_startup(self): def test_playlists_are_loaded_at_startup(self):
track = Track(uri='local:track:path2') track = Track(uri='dummy:track:path2')
playlist = self.core.playlists.create('test') playlist = self.core.playlists.create('test')
playlist = playlist.copy(tracks=[track]) playlist = playlist.copy(tracks=[track])
playlist = self.core.playlists.save(playlist) playlist = self.core.playlists.save(playlist)
@ -134,7 +132,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
pass pass
@unittest.SkipTest @unittest.SkipTest
def test_playlist_dir_is_created(self): def test_playlists_dir_is_created(self):
pass pass
def test_create_returns_playlist_with_name_set(self): def test_create_returns_playlist_with_name_set(self):
@ -154,7 +152,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
self.assert_(not self.core.playlists.playlists) self.assert_(not self.core.playlists.playlists)
def test_delete_non_existant_playlist(self): def test_delete_non_existant_playlist(self):
self.core.playlists.delete('local:playlist:unknown') self.core.playlists.delete('m3u:unknown')
def test_delete_playlist_removes_it_from_the_collection(self): def test_delete_playlist_removes_it_from_the_collection(self):
playlist = self.core.playlists.create('test') playlist = self.core.playlists.create('test')
@ -168,7 +166,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
playlist = self.core.playlists.create('test') playlist = self.core.playlists.create('test')
self.assertIn(playlist, self.core.playlists.playlists) self.assertIn(playlist, self.core.playlists.playlists)
path = local_playlist_uri_to_path(playlist.uri, self.playlists_dir) path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.exists(path))
os.remove(path) os.remove(path)
@ -244,12 +242,12 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
def test_save_playlist_with_new_uri(self): def test_save_playlist_with_new_uri(self):
# you *should* not do this # you *should* not do this
uri = 'local:playlist:test.m3u' uri = 'm3u:test.m3u'
playlist = self.core.playlists.save(Playlist(uri=uri)) playlist = self.core.playlists.save(Playlist(uri=uri))
self.assertIn(playlist, self.core.playlists.playlists) self.assertIn(playlist, self.core.playlists.playlists)
self.assertEqual(uri, playlist.uri) self.assertEqual(uri, playlist.uri)
self.assertEqual('test', playlist.name) self.assertEqual('test', playlist.name)
path = local_playlist_uri_to_path(playlist.uri, self.playlists_dir) path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.exists(path))
def test_playlist_with_unknown_track(self): def test_playlist_with_unknown_track(self):
@ -261,8 +259,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
backend = self.backend_class(config=self.config, audio=self.audio) backend = self.backend_class(config=self.config, audio=self.audio)
self.assert_(backend.playlists.playlists) self.assert_(backend.playlists.playlists)
self.assertEqual( self.assertEqual('m3u:test.m3u', backend.playlists.playlists[0].uri)
'local:playlist:test.m3u', backend.playlists.playlists[0].uri)
self.assertEqual( self.assertEqual(
playlist.name, backend.playlists.playlists[0].name) playlist.name, backend.playlists.playlists[0].name)
self.assertEqual( self.assertEqual(
@ -282,12 +279,12 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
check_order(self.core.playlists.playlists, ['a', 'b', 'c']) check_order(self.core.playlists.playlists, ['a', 'b', 'c'])
playlist = self.core.playlists.lookup('local:playlist:a.m3u') playlist = self.core.playlists.lookup('m3u:a.m3u')
playlist = playlist.copy(name='d') playlist = playlist.copy(name='d')
playlist = self.core.playlists.save(playlist) playlist = self.core.playlists.save(playlist)
check_order(self.core.playlists.playlists, ['b', 'c', 'd']) check_order(self.core.playlists.playlists, ['b', 'c', 'd'])
self.core.playlists.delete('local:playlist:c.m3u') self.core.playlists.delete('m3u:c.m3u')
check_order(self.core.playlists.playlists, ['b', 'd']) check_order(self.core.playlists.playlists, ['b', 'd'])

View File

@ -6,7 +6,7 @@ import os
import tempfile import tempfile
import unittest import unittest
from mopidy.local import translator from mopidy.m3u import translator
from mopidy.models import Track from mopidy.models import Track
from mopidy.utils import path from mopidy.utils import path