Merge adamcik/gstreamer

This commit is contained in:
Stein Magnus Jodal 2010-04-27 00:14:32 +02:00
commit b63cc96f16
12 changed files with 316 additions and 14 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,2 @@
# test
song1.mp3

0
tests/data/empty.m3u Normal file
View File

1
tests/data/encoding.m3u Normal file
View File

@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD>.mp3

1
tests/data/one.m3u Normal file
View File

@ -0,0 +1 @@
song1.mp3

66
tests/utils_test.py Normal file
View 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)