parent
8977f71411
commit
b2f60bc338
@ -126,6 +126,11 @@ v1.0.0 (UNRELEASED)
|
||||
|
||||
- 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**
|
||||
|
||||
- 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
|
||||
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**
|
||||
|
||||
- In stored playlist names, replace "/", which are illegal, with "|" instead of
|
||||
|
||||
55
docs/ext/m3u.rst
Normal file
55
docs/ext/m3u.rst
Normal 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.
|
||||
@ -94,6 +94,7 @@ Extensions
|
||||
:maxdepth: 2
|
||||
|
||||
ext/local
|
||||
ext/m3u
|
||||
ext/stream
|
||||
ext/http
|
||||
ext/mpd
|
||||
|
||||
@ -24,7 +24,7 @@ class Extension(ext.Extension):
|
||||
schema['library'] = config.String()
|
||||
schema['media_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['scan_timeout'] = config.Integer(
|
||||
minimum=1000, maximum=1000 * 60 * 60)
|
||||
|
||||
@ -8,7 +8,6 @@ from mopidy import backend
|
||||
from mopidy.local import storage
|
||||
from mopidy.local.library import LocalLibraryProvider
|
||||
from mopidy.local.playback import LocalPlaybackProvider
|
||||
from mopidy.local.playlists import LocalPlaylistsProvider
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -36,5 +35,4 @@ class LocalBackend(pykka.ThreadingActor, backend.Backend):
|
||||
logger.warning('Local library %s not found', library_name)
|
||||
|
||||
self.playback = LocalPlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||
self.library = LocalLibraryProvider(backend=self, library=library)
|
||||
|
||||
@ -3,7 +3,6 @@ enabled = true
|
||||
library = json
|
||||
media_dir = $XDG_MUSIC_DIR
|
||||
data_dir = $XDG_DATA_DIR/mopidy/local
|
||||
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
|
||||
scan_timeout = 1000
|
||||
scan_flush_threshold = 1000
|
||||
scan_follow_symlinks = false
|
||||
|
||||
@ -20,11 +20,3 @@ def check_dirs_and_files(config):
|
||||
logger.warning(
|
||||
'Could not create local data dir: %s',
|
||||
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))
|
||||
|
||||
@ -2,18 +2,12 @@ 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__)
|
||||
|
||||
|
||||
@ -28,13 +22,6 @@ def local_track_uri_to_path(uri, media_dir):
|
||||
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):
|
||||
"""Convert path relative to media_dir to local track URI."""
|
||||
if isinstance(relpath, compat.text_type):
|
||||
@ -47,89 +34,3 @@ def path_to_local_directory_uri(relpath):
|
||||
if isinstance(relpath, compat.text_type):
|
||||
relpath = relpath.encode('utf-8')
|
||||
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
30
mopidy/m3u/__init__.py
Normal 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
32
mopidy/m3u/actor.py
Normal 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
3
mopidy/m3u/ext.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[m3u]
|
||||
enabled = true
|
||||
playlists_dir = $XDG_DATA_DIR/mopidy/m3u
|
||||
18
mopidy/m3u/library.py
Normal file
18
mopidy/m3u/library.py
Normal 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 []
|
||||
@ -8,19 +8,18 @@ import os
|
||||
import sys
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.m3u import translator
|
||||
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__)
|
||||
|
||||
|
||||
class LocalPlaylistsProvider(backend.PlaylistsProvider):
|
||||
class M3UPlaylistsProvider(backend.PlaylistsProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
self._media_dir = self.backend.config['local']['media_dir']
|
||||
self._playlists_dir = self.backend.config['local']['playlists_dir']
|
||||
super(M3UPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
|
||||
self._playlists_dir = self.backend._config['m3u']['playlists_dir']
|
||||
self._playlists = []
|
||||
self.refresh()
|
||||
|
||||
@ -49,7 +48,7 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
|
||||
if not playlist:
|
||||
logger.warn('Trying to delete unknown playlist %s', uri)
|
||||
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):
|
||||
os.remove(path)
|
||||
else:
|
||||
@ -70,10 +69,10 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
|
||||
for path in glob.glob(os.path.join(self._playlists_dir, b'*.m3u')):
|
||||
relpath = os.path.basename(path)
|
||||
name = os.path.splitext(relpath)[0].decode(encoding)
|
||||
uri = path_to_local_playlist_uri(relpath)
|
||||
uri = translator.path_to_playlist_uri(relpath)
|
||||
|
||||
tracks = []
|
||||
for track in parse_m3u(path, self._media_dir):
|
||||
for track in translator.parse_m3u(path):
|
||||
tracks.append(track)
|
||||
|
||||
playlist = Playlist(uri=uri, name=name, tracks=tracks)
|
||||
@ -82,7 +81,7 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
|
||||
self.playlists = sorted(playlists, key=operator.attrgetter('name'))
|
||||
|
||||
logger.info(
|
||||
'Loaded %d local playlists from %s',
|
||||
'Loaded %d M3U playlists from %s',
|
||||
len(playlists), self._playlists_dir)
|
||||
|
||||
def save(self, playlist):
|
||||
@ -99,7 +98,7 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
|
||||
|
||||
playlist = self._save_m3u(playlist)
|
||||
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):
|
||||
os.remove(path)
|
||||
else:
|
||||
@ -125,11 +124,12 @@ class LocalPlaylistsProvider(backend.PlaylistsProvider):
|
||||
def _save_m3u(self, playlist, encoding=sys.getfilesystemencoding()):
|
||||
if playlist.name:
|
||||
name = self._sanitize_m3u_name(playlist.name, encoding)
|
||||
uri = path_to_local_playlist_uri(name.encode(encoding) + b'.m3u')
|
||||
path = local_playlist_uri_to_path(uri, self._playlists_dir)
|
||||
uri = translator.path_to_playlist_uri(
|
||||
name.encode(encoding) + b'.m3u')
|
||||
path = translator.playlist_uri_to_path(uri, self._playlists_dir)
|
||||
elif 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))
|
||||
else:
|
||||
raise ValueError('M3U playlist needs name or URI')
|
||||
110
mopidy/m3u/translator.py
Normal file
110
mopidy/m3u/translator.py
Normal 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
|
||||
1
setup.py
1
setup.py
@ -36,6 +36,7 @@ setup(
|
||||
'mopidy.ext': [
|
||||
'http = mopidy.http:Extension',
|
||||
'local = mopidy.local:Extension',
|
||||
'm3u = mopidy.m3u:Extension',
|
||||
'mpd = mopidy.mpd:Extension',
|
||||
'softwaremixer = mopidy.softwaremixer:Extension',
|
||||
'stream = mopidy.stream:Extension',
|
||||
|
||||
5
tests/m3u/__init__.py
Normal file
5
tests/m3u/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
def generate_song(i):
|
||||
return 'dummy:track:song%s' % i
|
||||
@ -8,30 +8,28 @@ import unittest
|
||||
import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.local import actor
|
||||
from mopidy.local.translator import local_playlist_uri_to_path
|
||||
from mopidy.m3u import actor
|
||||
from mopidy.m3u.translator import playlist_uri_to_path
|
||||
from mopidy.models import Playlist, Track
|
||||
|
||||
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):
|
||||
backend_class = actor.LocalBackend
|
||||
class M3UPlaylistsProviderTest(unittest.TestCase):
|
||||
backend_class = actor.M3UBackend
|
||||
config = {
|
||||
'local': {
|
||||
'media_dir': path_to_data_dir(''),
|
||||
'data_dir': path_to_data_dir(''),
|
||||
'library': 'json',
|
||||
'm3u': {
|
||||
'playlists_dir': path_to_data_dir(''),
|
||||
}
|
||||
}
|
||||
|
||||
def setUp(self): # noqa: N802
|
||||
self.config['local']['playlists_dir'] = tempfile.mkdtemp()
|
||||
self.playlists_dir = self.config['local']['playlists_dir']
|
||||
self.config['m3u']['playlists_dir'] = tempfile.mkdtemp()
|
||||
self.playlists_dir = self.config['m3u']['playlists_dir']
|
||||
|
||||
self.audio = dummy_audio.create_proxy()
|
||||
self.backend = actor.LocalBackend.start(
|
||||
self.backend = actor.M3UBackend.start(
|
||||
config=self.config, audio=self.audio).proxy()
|
||||
self.core = core.Core(backends=[self.backend])
|
||||
|
||||
@ -42,8 +40,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
shutil.rmtree(self.playlists_dir)
|
||||
|
||||
def test_created_playlist_is_persisted(self):
|
||||
uri = 'local:playlist:test.m3u'
|
||||
path = local_playlist_uri_to_path(uri, self.playlists_dir)
|
||||
uri = 'm3u:test.m3u'
|
||||
path = playlist_uri_to_path(uri, self.playlists_dir)
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
playlist = self.core.playlists.create('test')
|
||||
@ -54,16 +52,16 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
def test_create_sanitizes_playlist_name(self):
|
||||
playlist = self.core.playlists.create('../../test FOO baR')
|
||||
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.assertTrue(os.path.exists(path))
|
||||
|
||||
def test_saved_playlist_is_persisted(self):
|
||||
uri1 = 'local:playlist:test1.m3u'
|
||||
uri2 = 'local:playlist:test2.m3u'
|
||||
uri1 = 'm3u:test1.m3u'
|
||||
uri2 = 'm3u:test2.m3u'
|
||||
|
||||
path1 = local_playlist_uri_to_path(uri1, self.playlists_dir)
|
||||
path2 = local_playlist_uri_to_path(uri2, self.playlists_dir)
|
||||
path1 = playlist_uri_to_path(uri1, self.playlists_dir)
|
||||
path2 = playlist_uri_to_path(uri2, self.playlists_dir)
|
||||
|
||||
playlist = self.core.playlists.create('test1')
|
||||
self.assertEqual('test1', playlist.name)
|
||||
@ -78,8 +76,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
self.assertTrue(os.path.exists(path2))
|
||||
|
||||
def test_deleted_playlist_is_removed(self):
|
||||
uri = 'local:playlist:test.m3u'
|
||||
path = local_playlist_uri_to_path(uri, self.playlists_dir)
|
||||
uri = 'm3u:test.m3u'
|
||||
path = playlist_uri_to_path(uri, self.playlists_dir)
|
||||
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
@ -95,7 +93,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
track = Track(uri=generate_song(1))
|
||||
playlist = self.core.playlists.create('test')
|
||||
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:
|
||||
contents = f.read()
|
||||
@ -106,7 +104,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
track = Track(uri=generate_song(1), name='Test', length=60000)
|
||||
playlist = self.core.playlists.create('test')
|
||||
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:
|
||||
contents = f.read().splitlines()
|
||||
@ -114,7 +112,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
self.assertEqual(contents, ['#EXTM3U', '#EXTINF:60,Test', track.uri])
|
||||
|
||||
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 = playlist.copy(tracks=[track])
|
||||
playlist = self.core.playlists.save(playlist)
|
||||
@ -134,7 +132,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
pass
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_playlist_dir_is_created(self):
|
||||
def test_playlists_dir_is_created(self):
|
||||
pass
|
||||
|
||||
def test_create_returns_playlist_with_name_set(self):
|
||||
@ -154,7 +152,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
self.assert_(not self.core.playlists.playlists)
|
||||
|
||||
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):
|
||||
playlist = self.core.playlists.create('test')
|
||||
@ -168,7 +166,7 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
playlist = self.core.playlists.create('test')
|
||||
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))
|
||||
|
||||
os.remove(path)
|
||||
@ -244,12 +242,12 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
|
||||
def test_save_playlist_with_new_uri(self):
|
||||
# you *should* not do this
|
||||
uri = 'local:playlist:test.m3u'
|
||||
uri = 'm3u:test.m3u'
|
||||
playlist = self.core.playlists.save(Playlist(uri=uri))
|
||||
self.assertIn(playlist, self.core.playlists.playlists)
|
||||
self.assertEqual(uri, playlist.uri)
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
self.assert_(backend.playlists.playlists)
|
||||
self.assertEqual(
|
||||
'local:playlist:test.m3u', backend.playlists.playlists[0].uri)
|
||||
self.assertEqual('m3u:test.m3u', backend.playlists.playlists[0].uri)
|
||||
self.assertEqual(
|
||||
playlist.name, backend.playlists.playlists[0].name)
|
||||
self.assertEqual(
|
||||
@ -282,12 +279,12 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
|
||||
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 = self.core.playlists.save(playlist)
|
||||
|
||||
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'])
|
||||
@ -6,7 +6,7 @@ import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from mopidy.local import translator
|
||||
from mopidy.m3u import translator
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils import path
|
||||
|
||||
Loading…
Reference in New Issue
Block a user