Merge adamcik/gstreamer
This commit is contained in:
commit
b63cc96f16
@ -38,6 +38,9 @@ Stuff we really want to do, but just not right now
|
||||
stuff into the various distros) to make Debian/Ubuntu installation a breeze.
|
||||
- Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies for all our
|
||||
dependencies and Mopidy itself to make OS X installation a breeze.
|
||||
- Run frontend tests against a real MPD server to ensure we are in sync.
|
||||
- Start working with MPD-client maintainers to get rid of weird assumptions
|
||||
like only searching for first two letters etc.
|
||||
|
||||
|
||||
Crazy stuff we had to write down somewhere
|
||||
|
||||
@ -9,6 +9,10 @@ from mopidy.utils import get_class
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
__all__ = ['BaseBackend', 'BasePlaybackController',
|
||||
'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController',
|
||||
'BaseLibraryController']
|
||||
|
||||
class BaseBackend(object):
|
||||
def __init__(self, core_queue=None, mixer=None):
|
||||
self.core_queue = core_queue
|
||||
@ -42,6 +46,11 @@ class BaseBackend(object):
|
||||
uri_handlers = []
|
||||
|
||||
def destroy(self):
|
||||
'''
|
||||
Call destroy on all sub-components in backend so that they can cleanup
|
||||
after themselves.
|
||||
'''
|
||||
|
||||
if self.current_playlist:
|
||||
self.current_playlist.destroy()
|
||||
|
||||
@ -218,6 +227,7 @@ class BaseCurrentPlaylistController(object):
|
||||
self.playlist = self.playlist.with_(tracks=before+shuffled+after)
|
||||
|
||||
def destroy(self):
|
||||
'''Cleanup after component'''
|
||||
pass
|
||||
|
||||
|
||||
@ -274,6 +284,7 @@ class BaseLibraryController(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def destroy(self):
|
||||
'''Cleanup after component'''
|
||||
pass
|
||||
|
||||
|
||||
@ -593,6 +604,7 @@ class BasePlaybackController(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def destroy(self):
|
||||
'''Cleanup after component'''
|
||||
pass
|
||||
|
||||
|
||||
@ -708,4 +720,5 @@ class BaseStoredPlaylistsController(object):
|
||||
return filter(lambda p: query in p.name, self._playlists)
|
||||
|
||||
def destroy(self):
|
||||
'''Cleanup after component'''
|
||||
pass
|
||||
|
||||
@ -8,11 +8,13 @@ pygst.require('0.10')
|
||||
|
||||
import gst
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from mopidy.backends import (BaseBackend,
|
||||
BasePlaybackController,
|
||||
BaseCurrentPlaylistController)
|
||||
from mopidy.backends import *
|
||||
from mopidy.models import Playlist
|
||||
from mopidy import settings
|
||||
|
||||
logger = logging.getLogger(u'backends.gstreamer')
|
||||
|
||||
@ -35,7 +37,9 @@ class GStreamerBackend(BaseBackend):
|
||||
super(GStreamerBackend, self).__init__(*args, **kwargs)
|
||||
|
||||
self.playback = GStreamerPlaybackController(self)
|
||||
self.stored_playlists = GStreamerStoredPlaylistsController(self)
|
||||
self.current_playlist = BaseCurrentPlaylistController(self)
|
||||
self.uri_handlers = [u'file:']
|
||||
|
||||
|
||||
class GStreamerPlaybackController(BasePlaybackController):
|
||||
@ -108,3 +112,43 @@ class GStreamerPlaybackController(BasePlaybackController):
|
||||
|
||||
del bus
|
||||
del bin
|
||||
|
||||
|
||||
class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def __init__(self, *args):
|
||||
super(GStreamerStoredPlaylistsController, self).__init__(*args)
|
||||
# FIXME need test that ensures that folder is created
|
||||
self._folder = os.path.expanduser(settings.PLAYLIST_FOLDER)
|
||||
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name)
|
||||
self.save(playlist)
|
||||
return playlist
|
||||
|
||||
def delete(self, playlist):
|
||||
if playlist not in self._playlists:
|
||||
return
|
||||
|
||||
self._playlists.remove(playlist)
|
||||
file = os.path.join(self._folder, playlist.name + '.m3u')
|
||||
|
||||
if os.path.exists(file):
|
||||
os.remove(file)
|
||||
|
||||
def rename(self, playlist, name):
|
||||
if playlist not in self._playlists:
|
||||
return
|
||||
|
||||
src = os.path.join(self._folder, playlist.name + '.m3u')
|
||||
dst = os.path.join(self._folder, name + '.m3u')
|
||||
|
||||
renamed = playlist.with_(name=name)
|
||||
index = self._playlists.index(playlist)
|
||||
self._playlists[index] = renamed
|
||||
|
||||
shutil.move(src, dst)
|
||||
|
||||
def save(self, playlist):
|
||||
file = os.path.join(self._folder, playlist.name + '.m3u')
|
||||
open(file, 'w').close()
|
||||
self._playlists.append(playlist)
|
||||
|
||||
@ -101,6 +101,9 @@ SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key'
|
||||
#: Path to the libspotify cache. Used by LibspotifyBackend.
|
||||
SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache'
|
||||
|
||||
#: Path to playlist folder with m3u files.
|
||||
PLAYLIST_FOLDER = u'~/.mopidy/playlists'
|
||||
|
||||
# Import user specific settings
|
||||
dotdir = os.path.expanduser(u'~/.mopidy/')
|
||||
settings_file = os.path.join(dotdir, u'settings.py')
|
||||
|
||||
@ -2,6 +2,7 @@ import logging
|
||||
from multiprocessing.reduction import reduce_connection
|
||||
import os
|
||||
import pickle
|
||||
import urllib
|
||||
|
||||
logger = logging.getLogger('mopidy.utils')
|
||||
|
||||
@ -91,3 +92,43 @@ def spotify_uri_to_int(uri, output_bits=31):
|
||||
compressed_id ^= (full_id & (2 ** output_bits - 1))
|
||||
full_id >>= output_bits
|
||||
return int(compressed_id)
|
||||
|
||||
def m3u_to_uris(file_path):
|
||||
"""
|
||||
Convert M3U file list of uris
|
||||
|
||||
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
|
||||
|
||||
- Relative paths of songs should be with respect to location of M3U.
|
||||
- Paths are normaly platform specific.
|
||||
- Lines starting with # should be ignored.
|
||||
- m3u files are latin-1.
|
||||
- This function does not bother with Extended M3U directives.
|
||||
"""
|
||||
|
||||
uris = []
|
||||
folder = os.path.dirname(file_path)
|
||||
|
||||
with open(file_path) as m3u:
|
||||
for line in m3u.readlines():
|
||||
line = line.strip().decode('latin1')
|
||||
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
|
||||
if line.startswith('file:'):
|
||||
uris.append(line)
|
||||
else:
|
||||
file = os.path.join(folder, line)
|
||||
path = urllib.pathname2url(file.encode('utf-8'))
|
||||
uris.append('file:' + path)
|
||||
|
||||
return uris
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Playlist, Track
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
__all__ = ['BaseCurrentPlaylistControllerTest',
|
||||
'BasePlaybackControllerTest',
|
||||
'BaseStoredPlaylistsControllerTest']
|
||||
|
||||
def populate_playlist(func):
|
||||
def wrapper(self):
|
||||
for track in self.tracks:
|
||||
@ -872,3 +880,86 @@ class BasePlaybackControllerTest(object):
|
||||
def test_playing_track_that_isnt_in_playlist(self):
|
||||
test = lambda: self.playback.play(self.tracks[0])
|
||||
self.assertRaises(AssertionError, test)
|
||||
|
||||
|
||||
class BaseStoredPlaylistsControllerTest(object):
|
||||
backend_class = None
|
||||
|
||||
def setUp(self):
|
||||
self.original_folder = settings.PLAYLIST_FOLDER
|
||||
settings.PLAYLIST_FOLDER = tempfile.mkdtemp()
|
||||
self.backend = self.backend_class(mixer=DummyMixer())
|
||||
self.stored = self.backend.stored_playlists
|
||||
|
||||
def tearDown(self):
|
||||
self.backend.destroy()
|
||||
if os.path.exists(settings.PLAYLIST_FOLDER):
|
||||
shutil.rmtree(settings.PLAYLIST_FOLDER)
|
||||
settings.PLAYLIST_FOLDER = self.original_folder
|
||||
|
||||
def test_create(self):
|
||||
playlist = self.stored.create('test')
|
||||
self.assertEqual(playlist.name, 'test')
|
||||
|
||||
def test_create_in_playlists(self):
|
||||
playlist = self.stored.create('test')
|
||||
self.assert_(self.stored.playlists)
|
||||
|
||||
def test_playlists_empty_to_start_with(self):
|
||||
self.assert_(not self.stored.playlists)
|
||||
|
||||
def test_delete_non_existant_playlist(self):
|
||||
self.stored.delete(Playlist())
|
||||
|
||||
def test_delete_playlist(self):
|
||||
playlist = self.stored.create('test')
|
||||
self.stored.delete(playlist)
|
||||
self.assert_(not self.stored.playlists)
|
||||
|
||||
def test_get_without_criteria(self):
|
||||
test = lambda: self.stored.get()
|
||||
self.assertRaises(LookupError, test)
|
||||
|
||||
def test_get_with_wrong_cirteria(self):
|
||||
test = lambda: self.stored.get(name='foo')
|
||||
self.assertRaises(LookupError, test)
|
||||
|
||||
def test_get_with_right_criteria(self):
|
||||
playlist1 = self.stored.create('test')
|
||||
playlist2 = self.stored.get(name='test')
|
||||
self.assertEqual(playlist1, playlist2)
|
||||
|
||||
def test_search_returns_empty_list(self):
|
||||
self.assertEqual([], self.stored.search('test'))
|
||||
|
||||
def test_search_returns_playlist(self):
|
||||
playlist = self.stored.create('test')
|
||||
playlists = self.stored.search('test')
|
||||
self.assert_(playlist in playlists)
|
||||
|
||||
def test_search_returns_mulitple_playlists(self):
|
||||
playlist1 = self.stored.create('test')
|
||||
playlist2 = self.stored.create('test2')
|
||||
playlists = self.stored.search('test')
|
||||
self.assert_(playlist1 in playlists)
|
||||
self.assert_(playlist2 in playlists)
|
||||
|
||||
def test_lookup(self):
|
||||
raise SkipTest
|
||||
|
||||
def test_refresh(self):
|
||||
raise SkipTest
|
||||
|
||||
def test_rename(self):
|
||||
playlist = self.stored.create('test')
|
||||
self.stored.rename(playlist, 'test2')
|
||||
self.stored.get(name='test2')
|
||||
|
||||
def test_rename_unknown_playlist(self):
|
||||
self.stored.rename(Playlist(), 'test2')
|
||||
|
||||
def test_save(self):
|
||||
# FIXME should we handle playlists without names?
|
||||
playlist = Playlist(name='test')
|
||||
self.stored.save(playlist)
|
||||
self.assert_(playlist in self.stored.playlists)
|
||||
|
||||
@ -1,35 +1,38 @@
|
||||
import unittest
|
||||
import os
|
||||
import urllib
|
||||
|
||||
from mopidy.models import Playlist, Track
|
||||
from mopidy.backends.gstreamer import GStreamerBackend
|
||||
from mopidy import settings
|
||||
|
||||
from tests.backends.base import (BasePlaybackControllerTest,
|
||||
BaseCurrentPlaylistControllerTest)
|
||||
from tests.backends.base import *
|
||||
|
||||
folder = os.path.dirname(__file__)
|
||||
folder = os.path.join(folder, '..', 'data')
|
||||
folder = os.path.abspath(folder)
|
||||
song = os.path.join(folder, 'song%s.wav')
|
||||
song = 'file://' + song
|
||||
generate_song = lambda i: 'file:' + urllib.pathname2url(song % i)
|
||||
|
||||
# FIXME can be switched to generic test
|
||||
class GStreamerCurrentPlaylistHandlerTest(BaseCurrentPlaylistControllerTest,
|
||||
unittest.TestCase):
|
||||
tracks = [Track(uri=song % i, id=i, length=4464) for i in range(1, 4)]
|
||||
class GStreamerCurrentPlaylistHandlerTest(BaseCurrentPlaylistControllerTest, unittest.TestCase):
|
||||
tracks = [Track(uri=generate_song(i), id=i, length=4464) for i in range(1, 4)]
|
||||
|
||||
backend_class = GStreamerBackend
|
||||
|
||||
|
||||
class GStreamerPlaybackControllerTest(BasePlaybackControllerTest,
|
||||
unittest.TestCase):
|
||||
tracks = [Track(uri=song % i, id=i, length=4464) for i in range(1, 4)]
|
||||
class GStreamerPlaybackControllerTest(BasePlaybackControllerTest, unittest.TestCase):
|
||||
tracks = [Track(uri=generate_song(i), id=i, length=4464) for i in range(1, 4)]
|
||||
backend_class = GStreamerBackend
|
||||
|
||||
def add_track(self, file):
|
||||
uri = 'file://' + os.path.join(folder, file)
|
||||
uri = 'file:' + urllib.pathname2url(os.path.join(folder, file))
|
||||
track = Track(uri=uri, id=1, length=4464)
|
||||
self.backend.current_playlist.add(track)
|
||||
|
||||
def test_uri_handler(self):
|
||||
self.assert_('file:' in self.backend.uri_handlers)
|
||||
|
||||
def test_play_mp3(self):
|
||||
self.add_track('blank.mp3')
|
||||
self.playback.play()
|
||||
@ -44,3 +47,37 @@ class GStreamerPlaybackControllerTest(BasePlaybackControllerTest,
|
||||
self.add_track('blank.flac')
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
|
||||
|
||||
class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
|
||||
unittest.TestCase):
|
||||
|
||||
backend_class = GStreamerBackend
|
||||
|
||||
def test_created_playlist_is_persisted(self):
|
||||
self.stored.create('test')
|
||||
file = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u')
|
||||
self.assert_(os.path.exists(file))
|
||||
|
||||
def test_saved_playlist_is_persisted(self):
|
||||
self.stored.save(Playlist(name='test2'))
|
||||
file = os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u')
|
||||
self.assert_(os.path.exists(file))
|
||||
|
||||
def test_deleted_playlist_get_removed(self):
|
||||
playlist = self.stored.create('test')
|
||||
self.stored.delete(playlist)
|
||||
file = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u')
|
||||
self.assert_(not os.path.exists(file))
|
||||
|
||||
def test_renamed_playlist_gets_moved(self):
|
||||
playlist = self.stored.create('test')
|
||||
self.stored.rename(playlist, 'test2')
|
||||
file1 = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u')
|
||||
file2 = os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u')
|
||||
self.assert_(not os.path.exists(file1))
|
||||
self.assert_(os.path.exists(file2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
2
tests/data/comment.m3u
Normal file
2
tests/data/comment.m3u
Normal file
@ -0,0 +1,2 @@
|
||||
# test
|
||||
song1.mp3
|
||||
0
tests/data/empty.m3u
Normal file
0
tests/data/empty.m3u
Normal file
1
tests/data/encoding.m3u
Normal file
1
tests/data/encoding.m3u
Normal file
@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD>.mp3
|
||||
1
tests/data/one.m3u
Normal file
1
tests/data/one.m3u
Normal file
@ -0,0 +1 @@
|
||||
song1.mp3
|
||||
66
tests/utils_test.py
Normal file
66
tests/utils_test.py
Normal file
@ -0,0 +1,66 @@
|
||||
#encoding: utf-8
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib
|
||||
|
||||
from mopidy.utils import m3u_to_uris
|
||||
|
||||
def data(name):
|
||||
folder = os.path.dirname(__file__)
|
||||
folder = os.path.join(folder, 'data')
|
||||
folder = os.path.abspath(folder)
|
||||
return os.path.join(folder, name)
|
||||
|
||||
|
||||
song1_path = data('song1.mp3')
|
||||
song2_path = data('song2.mp3')
|
||||
encoded_path = data(u'æøå.mp3')
|
||||
song1_uri = 'file:' + urllib.pathname2url(song1_path)
|
||||
song2_uri = 'file:' + urllib.pathname2url(song2_path)
|
||||
encoded_uri = 'file:' + urllib.pathname2url(encoded_path.encode('utf-8'))
|
||||
|
||||
|
||||
class M3UToUriTest(unittest.TestCase):
|
||||
def test_empty_file(self):
|
||||
uris = m3u_to_uris(data('empty.m3u'))
|
||||
self.assertEqual([], uris)
|
||||
|
||||
def test_basic_file(self):
|
||||
uris = m3u_to_uris(data('one.m3u'))
|
||||
self.assertEqual([song1_uri], uris)
|
||||
|
||||
def test_file_with_comment(self):
|
||||
uris = m3u_to_uris(data('comment.m3u'))
|
||||
self.assertEqual([song1_uri], uris)
|
||||
|
||||
def test_file_with_absolute_files(self):
|
||||
with tempfile.NamedTemporaryFile() as file:
|
||||
file.write(song1_path)
|
||||
file.flush()
|
||||
|
||||
uris = m3u_to_uris(file.name)
|
||||
self.assertEqual([song1_uri], uris)
|
||||
|
||||
def test_file_with_multiple_absolute_files(self):
|
||||
with tempfile.NamedTemporaryFile() as file:
|
||||
file.write(song1_path+'\n')
|
||||
file.write('# comment \n')
|
||||
file.write(song2_path)
|
||||
file.flush()
|
||||
|
||||
uris = m3u_to_uris(file.name)
|
||||
self.assertEqual([song1_uri, song2_uri], uris)
|
||||
|
||||
def test_file_with_uri(self):
|
||||
with tempfile.NamedTemporaryFile() as file:
|
||||
file.write(song1_uri)
|
||||
file.flush()
|
||||
|
||||
uris = m3u_to_uris(file.name)
|
||||
self.assertEqual([song1_uri], uris)
|
||||
|
||||
def test_encoding_is_latin1(self):
|
||||
uris = m3u_to_uris(data('encoding.m3u'))
|
||||
self.assertEqual([encoded_uri], uris)
|
||||
Loading…
Reference in New Issue
Block a user