From 32527bfe797ab808e22957de6547118e373a5c5f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 21:16:00 +0200 Subject: [PATCH 01/82] Merge branch 'develop', remote branch 'jodal/develop' into develop From c7c3020453e095725fb02752cefd31343755ddf2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 22:07:16 +0200 Subject: [PATCH 02/82] Add basic file scanner --- mopidy/scanner.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 mopidy/scanner.py diff --git a/mopidy/scanner.py b/mopidy/scanner.py new file mode 100644 index 00000000..385c0a1f --- /dev/null +++ b/mopidy/scanner.py @@ -0,0 +1,28 @@ +import gobject +gobject.threads_init() + +import pygst +pygst.require('0.10') +import gst +import sys + +def main(uri): + pipeline = gst.element_factory_make('playbin2') + + bus = pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message::tag', process_gst_message) + + pipeline.set_property('uri', uri) + pipeline.set_state(gst.STATE_PAUSED) + + gobject.MainLoop().run() + +def process_gst_message(bus, message): + data = message.parse_tag() + tags = dict([(k, data[k]) for k in data.keys()]) + + print tags + +if __name__ == '__main__': + main(sys.argv[1]) From 2bc0c6bee0ad6a736f731a6b1b9bcfa7d22092c3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 21:16:00 +0200 Subject: [PATCH 03/82] Merge branch 'develop', remote branch 'jodal/develop' into develop From 53972f4022e4854f859860455902ffd7b658b5a7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 22:56:28 +0200 Subject: [PATCH 04/82] Rewrite to Scanner class --- mopidy/scanner.py | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 385c0a1f..4b42ed53 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -4,25 +4,46 @@ gobject.threads_init() import pygst pygst.require('0.10') import gst + +from os.path import abspath import sys +import threading -def main(uri): - pipeline = gst.element_factory_make('playbin2') +from mopidy.utils.path import path_to_uri - bus = pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message::tag', process_gst_message) +class Scanner(object): + def __init__(self, files): + self.uris = [path_to_uri(abspath(f)) for f in files] - pipeline.set_property('uri', uri) - pipeline.set_state(gst.STATE_PAUSED) + self.pipe = gst.element_factory_make('playbin2') - gobject.MainLoop().run() + bus = self.pipe.get_bus() + bus.add_signal_watch() + bus.connect('message::tag', self.process_message) -def process_gst_message(bus, message): - data = message.parse_tag() - tags = dict([(k, data[k]) for k in data.keys()]) + self.next_uri() + + gobject.MainLoop().run() + + def process_message(self, bus, message): + data = message.parse_tag() + tags = dict([(k, data[k]) for k in data.keys()]) + + print tags + + self.next_uri() + + def next_uri(self): + if not self.uris: + sys.exit(0) + + self.pipe.set_state(gst.STATE_NULL) + self.pipe.set_property('uri', self.uris.pop()) + self.pipe.set_state(gst.STATE_PAUSED) - print tags if __name__ == '__main__': - main(sys.argv[1]) + if len(sys.argv) == 1: + sys.exit(1) + + Scanner(sys.argv[1:]) From 89fe08a583a5944bdff61195c82f93fc015c2084 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:02:09 +0200 Subject: [PATCH 05/82] Accept callback for actual data handling --- mopidy/scanner.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4b42ed53..921d9272 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -12,8 +12,9 @@ import threading from mopidy.utils.path import path_to_uri class Scanner(object): - def __init__(self, files): + def __init__(self, files, callback): self.uris = [path_to_uri(abspath(f)) for f in files] + self.callback = callback self.pipe = gst.element_factory_make('playbin2') @@ -27,10 +28,7 @@ class Scanner(object): def process_message(self, bus, message): data = message.parse_tag() - tags = dict([(k, data[k]) for k in data.keys()]) - - print tags - + self.callback(dict([(k, data[k]) for k in data.keys()])) self.next_uri() def next_uri(self): @@ -41,9 +39,11 @@ class Scanner(object): self.pipe.set_property('uri', self.uris.pop()) self.pipe.set_state(gst.STATE_PAUSED) +def debug(data): + print data if __name__ == '__main__': if len(sys.argv) == 1: sys.exit(1) - Scanner(sys.argv[1:]) + Scanner(sys.argv[1:], debug) From 21123e3cd9bb2af2004a35c158b15826c0de1d2d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:18:35 +0200 Subject: [PATCH 06/82] Allow scanner to stop nicely when it runs out of files --- mopidy/scanner.py | 16 ++++++---------- tests/data/blank.mp3 | Bin 8208 -> 9360 bytes 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 921d9272..aa562ff1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -15,6 +15,7 @@ class Scanner(object): def __init__(self, files, callback): self.uris = [path_to_uri(abspath(f)) for f in files] self.callback = callback + self.loop = gobject.MainLoop() self.pipe = gst.element_factory_make('playbin2') @@ -24,8 +25,6 @@ class Scanner(object): self.next_uri() - gobject.MainLoop().run() - def process_message(self, bus, message): data = message.parse_tag() self.callback(dict([(k, data[k]) for k in data.keys()])) @@ -33,17 +32,14 @@ class Scanner(object): def next_uri(self): if not self.uris: - sys.exit(0) + return self.stop() self.pipe.set_state(gst.STATE_NULL) self.pipe.set_property('uri', self.uris.pop()) self.pipe.set_state(gst.STATE_PAUSED) -def debug(data): - print data + def start(self): + self.loop.run() -if __name__ == '__main__': - if len(sys.argv) == 1: - sys.exit(1) - - Scanner(sys.argv[1:], debug) + def stop(self): + self.loop.quit() diff --git a/tests/data/blank.mp3 b/tests/data/blank.mp3 index 6aa48cd832849e6d598c48d7f11b9d84f3fc5983..ef159a700449f6a2bf4c03fc206be8f2ff1c7469 100644 GIT binary patch delta 258 zcmbQ>Fu^m*)5VyD0SMU3LOer^fDASU2Ii8?lAP3#09Qkh1dyLtRFYX-65{CN1XYlj zlT?};;u7QxIl*BQi+} Date: Mon, 25 Oct 2010 23:31:40 +0200 Subject: [PATCH 07/82] Add helper for finding files in folder --- mopidy/utils/path.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 0dd163ec..e73258ea 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -26,3 +26,9 @@ def path_to_uri(*paths): if sys.platform == 'win32': return 'file:' + urllib.pathname2url(path) return 'file://' + urllib.pathname2url(path) + +def find_files(folder): + for dirpath, dirnames, filenames in os.walk(folder): + for filename in filenames: + dirpath = os.path.abspath(dirpath) + yield os.path.join(dirpath, filename) From 14ecce6c6b27bbb8b523efdb3ec3b43ece5be7ce Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:31:55 +0200 Subject: [PATCH 08/82] Scanner takes folder instead of files --- mopidy/scanner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index aa562ff1..7bb088c9 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -9,11 +9,11 @@ from os.path import abspath import sys import threading -from mopidy.utils.path import path_to_uri +from mopidy.utils.path import path_to_uri, find_files class Scanner(object): - def __init__(self, files, callback): - self.uris = [path_to_uri(abspath(f)) for f in files] + def __init__(self, folder, callback): + self.uris = [path_to_uri(f) for f in find_files(folder)] self.callback = callback self.loop = gobject.MainLoop() From 10cf68a103daa7333d05ebb495088ff5b64ea71c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:38:26 +0200 Subject: [PATCH 09/82] Add basic detection of errors in scanner --- mopidy/scanner.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 7bb088c9..6b2798ef 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -21,15 +21,19 @@ class Scanner(object): bus = self.pipe.get_bus() bus.add_signal_watch() - bus.connect('message::tag', self.process_message) + bus.connect('message::tag', self.process_tags) + bus.connect('message::error', self.process_error) self.next_uri() - def process_message(self, bus, message): + def process_tags(self, bus, message): data = message.parse_tag() self.callback(dict([(k, data[k]) for k in data.keys()])) self.next_uri() + def process_error(self, bus, message): + print message.parse_error() + def next_uri(self): if not self.uris: return self.stop() From e8a1d1b49da80105dfff84ebfd71320f500a87ed Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:47:12 +0200 Subject: [PATCH 10/82] Add error callback and switch to using uridecodebin --- mopidy/scanner.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 6b2798ef..298d81d8 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -12,12 +12,15 @@ import threading from mopidy.utils.path import path_to_uri, find_files class Scanner(object): - def __init__(self, folder, callback): + def __init__(self, folder, data_callback, error_callback=None): self.uris = [path_to_uri(f) for f in find_files(folder)] - self.callback = callback + self.data_callback = data_callback + self.error_callback = error_callback self.loop = gobject.MainLoop() - self.pipe = gst.element_factory_make('playbin2') + self.uribin = gst.element_factory_make('uridecodebin') + self.pipe = gst.element_factory_make('pipeline') + self.pipe.add(self.uribin) bus = self.pipe.get_bus() bus.add_signal_watch() @@ -28,18 +31,22 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() - self.callback(dict([(k, data[k]) for k in data.keys()])) + self.data_callback(dict([(k, data[k]) for k in data.keys()])) self.next_uri() def process_error(self, bus, message): - print message.parse_error() + if self.error_callback: + uri = self.uribin.get_property('uri') + error = message.parse_error() + self.error_callback(uri, *error) + self.next_uri() def next_uri(self): if not self.uris: return self.stop() self.pipe.set_state(gst.STATE_NULL) - self.pipe.set_property('uri', self.uris.pop()) + self.uribin.set_property('uri', self.uris.pop()) self.pipe.set_state(gst.STATE_PAUSED) def start(self): From 7fdc01b99eba20aee56631d663ea9e059457c284 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:50:01 +0200 Subject: [PATCH 11/82] Move initial next_uri call to start method --- mopidy/scanner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 298d81d8..e8c0c6ba 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -27,8 +27,6 @@ class Scanner(object): bus.connect('message::tag', self.process_tags) bus.connect('message::error', self.process_error) - self.next_uri() - def process_tags(self, bus, message): data = message.parse_tag() self.data_callback(dict([(k, data[k]) for k in data.keys()])) @@ -50,6 +48,7 @@ class Scanner(object): self.pipe.set_state(gst.STATE_PAUSED) def start(self): + self.next_uri() self.loop.run() def stop(self): From efc60a943b2c2e14707c46ab58f5eca2168a34ca Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 00:08:51 +0200 Subject: [PATCH 12/82] Update find files behaviour and add test for it --- mopidy/utils/path.py | 13 ++++++++----- tests/utils/path_test.py | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index e73258ea..9220c53d 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -27,8 +27,11 @@ def path_to_uri(*paths): return 'file:' + urllib.pathname2url(path) return 'file://' + urllib.pathname2url(path) -def find_files(folder): - for dirpath, dirnames, filenames in os.walk(folder): - for filename in filenames: - dirpath = os.path.abspath(dirpath) - yield os.path.join(dirpath, filename) +def find_files(path): + if os.path.isfile(path): + yield os.path.abspath(path) + else: + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + dirpath = os.path.abspath(dirpath) + yield os.path.join(dirpath, filename) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index ae63d5c0..e0359a16 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,9 +6,9 @@ import sys import tempfile import unittest -from mopidy.utils.path import get_or_create_folder, path_to_uri +from mopidy.utils.path import get_or_create_folder, path_to_uri, find_files -from tests import SkipTest +from tests import SkipTest, data_folder class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): @@ -69,3 +69,19 @@ class PathToFileURITest(unittest.TestCase): else: result = path_to_uri(u'/tmp/æøå') self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5') + + +class FindFilesTest(unittest.TestCase): + def find(self, path): + return list(find_files(data_folder(path))) + + def test_basic_folder(self): + self.assert_(self.find('')) + + def test_nonexistant_folder(self): + self.assertEqual(self.find('does-not-exist'), []) + + def test_file(self): + files = self.find('blank.mp3') + self.assertEqual(len(files), 1) + self.assert_(files[0], data_folder('blank.mp3')) From 02bfad2fe48104e23e8ef60ea51e642dbdc8cf2c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 00:13:50 +0200 Subject: [PATCH 13/82] Add basic tests for scanner --- mopidy/scanner.py | 7 ++++--- tests/scanner.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/scanner.py diff --git a/mopidy/scanner.py b/mopidy/scanner.py index e8c0c6ba..914f431e 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -29,14 +29,15 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() - self.data_callback(dict([(k, data[k]) for k in data.keys()])) + uri = self.uribin.get_property('uri') + self.data_callback(uri, dict([(k, data[k]) for k in data.keys()])) self.next_uri() def process_error(self, bus, message): if self.error_callback: uri = self.uribin.get_property('uri') - error = message.parse_error() - self.error_callback(uri, *error) + errors = message.parse_error() + self.error_callback(uri, errors) self.next_uri() def next_uri(self): diff --git a/tests/scanner.py b/tests/scanner.py new file mode 100644 index 00000000..5374e856 --- /dev/null +++ b/tests/scanner.py @@ -0,0 +1,45 @@ +import unittest + +from mopidy.scanner import Scanner + +from tests import data_folder + +class ScannerTest(unittest.TestCase): + def setUp(self): + self.errors = {} + self.data = {} + + def scan(self, path): + scanner = Scanner(data_folder(path), + self.data_callback, self.error_callback) + scanner.start() + + def data_callback(self, uri, data): + uri = uri.lstrip('file://') + uri = uri.lstrip(data_folder('')) + self.data[uri] = data + + def error_callback(self, uri, errors): + uri = uri.lstrip('file://') + uri = uri.lstrip(data_folder('')) + self.errors[uri] = errors + + def test_data_is_set(self): + self.scan('blank.mp3') + self.assert_(self.data) + + def test_errors_is_not_set(self): + self.scan('blank.mp3') + self.assert_(not self.errors) + + def test_artist_is_set(self): + self.scan('blank.mp3') + self.assertEqual(self.data['blank.mp3']['artist'], 'artist') + + def test_album_is_set(self): + self.scan('blank.mp3') + self.assertEqual(self.data['blank.mp3']['album'], 'album') + + def test_track_is_set(self): + self.scan('blank.mp3') + self.assertEqual(self.data['blank.mp3']['title'], 'title') From 23881e1b1d28274d7f0c1c5a63d1659c6944ff19 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 21:33:12 +0200 Subject: [PATCH 14/82] Add basic test structure for scanner testing --- tests/data/scanner/advanced/song1.mp3 | 1 + tests/data/scanner/advanced/song2.mp3 | 1 + tests/data/scanner/advanced/song3.mp3 | 1 + tests/data/scanner/advanced/subdir1/song4.mp3 | 1 + tests/data/scanner/advanced/subdir1/song5.mp3 | 1 + .../scanner/advanced/subdir1/subsubdir/song8.mp3 | 1 + .../scanner/advanced/subdir1/subsubdir/song9.mp3 | 1 + tests/data/scanner/advanced/subdir2/song6.mp3 | 1 + tests/data/scanner/advanced/subdir2/song7.mp3 | 1 + tests/data/scanner/empty/.gitignore | 0 tests/data/scanner/sample.mp3 | Bin 0 -> 9360 bytes tests/data/scanner/simple/song1.mp3 | 1 + 12 files changed, 10 insertions(+) create mode 120000 tests/data/scanner/advanced/song1.mp3 create mode 120000 tests/data/scanner/advanced/song2.mp3 create mode 120000 tests/data/scanner/advanced/song3.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/song4.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/song5.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 create mode 120000 tests/data/scanner/advanced/subdir2/song6.mp3 create mode 120000 tests/data/scanner/advanced/subdir2/song7.mp3 create mode 100644 tests/data/scanner/empty/.gitignore create mode 100644 tests/data/scanner/sample.mp3 create mode 120000 tests/data/scanner/simple/song1.mp3 diff --git a/tests/data/scanner/advanced/song1.mp3 b/tests/data/scanner/advanced/song1.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/advanced/song1.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song2.mp3 b/tests/data/scanner/advanced/song2.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/advanced/song2.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song3.mp3 b/tests/data/scanner/advanced/song3.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/advanced/song3.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/song4.mp3 b/tests/data/scanner/advanced/subdir1/song4.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/song4.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/song5.mp3 b/tests/data/scanner/advanced/subdir1/song5.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/song5.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir2/song6.mp3 b/tests/data/scanner/advanced/subdir2/song6.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir2/song6.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir2/song7.mp3 b/tests/data/scanner/advanced/subdir2/song7.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir2/song7.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/empty/.gitignore b/tests/data/scanner/empty/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/scanner/sample.mp3 b/tests/data/scanner/sample.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 zcmeHtS5#B&)^6xU5Ru+OkrH~9t`ZOk5RewS3P`VlbX2N9Kza|ogc5p(03wKVr6XNY zL_xt15u|*Oe=YYu``qmD@5^&8_B#e+uu9f?<}>?zm(FcjQV6gp0!(#GWq^OEArKNj zU%LnHUUr_2riPl*z&SGD+@C`@QvIK|*m>Cdd;U3h+gRhDx5!9I$(tH$=mCf1z%Qhv zC8fY8{&)SCf&Vh_|0x56Pl&;PL9=l6at03QfIm0{!gB>eOhrX|^(qS+8!v9^zJOXZMo?V=2opX+ztJ-T# zsir5tMJwO7{J0;qoKT}S=+@W$dw0FzXdcy;xI_4)I4Gtd7LePQjDR%Gk9f)%#tb;X zW+on4`>4&ZUQ;X1-RYv{mx!#QBV}?7u$GLD2tV$RNlCQ}DDPo3*B-xWpD>!0n$jOp z&JZcwKX9K*-a4%+U0a(Z zJR=CJw-7Rg-?qDYY6YS(_~hSzLx|sm%{SCOnV9sK_WhX>^dpGy2{y>iH?}v?3(%WxhfLJ&^$lhejPoPmh!ZdDabGZ-e(()v~-uj%{q_y;vs z+M16L+UB~-y}$P=QQx1Rh;KgFtKemt=0gh_mERS87aly~=O%Z<&-dns@Arws-eZuh1nB4?X*d*Dw?|bNgObEWA8dNBX3{Ls~bu zaI`-(p2^>}D{7Q_kyiC_jY=S!TspbaocoipPfrbA!Ug9f!Vd=8v1@(uTuy~ky<|YqqhPH5$jB@(7+sazmH{EEaSo~-;3js1(6Nx^_=YmI zosM;MMoVF;oMEhDv>+ZBXLZOhR1rzT|E$1C>o~#ettXuA$KKAmVEk=XCdw}R0U!Dl zxRwrst-h^;G*aW$Hbum><|&mSs&#BYe!%YvJbRHRVy(NX&1f9?lT|_|U0>HJ4wHv3 zYG;KIiSYe!UcGhWH*fpr4=euI9M&W))j7N7a7bo-I3)7tm$j>#Q6Gr{oKZG&^@k%H4!hA5@1{Kgj~gWh)A zCutmrM~GweXdG&+Wom0c7xw@`I>T%WRXtT9W)>_~>fsn3r_XFQ=JS<3NGNo3_lAH+Jm12vwg@hyf_w6<2Ca#nV962Bce6W?n3 z%&il;LAe-M?9?Zr$~0o~AF5oPIN3WXUaD3OpMO%|<$Hh-dUILaCFgOfv^=bJWXz18 z<0d-`ZS44^aCOb`)>kGkbS1AEHULrUbe_p89`81=R{bbw(yYxXD7U7o=&%z)^dj$U z;>X?nrUv~X&d0OZHyQRmJf_t#;@l(LF4PpnoV1;3P^qrjF8#IVzfSc3VLzi|fLzfw z#$T?f5Z1E-uh)xV(D}+@ViuR7P?%eL@=rE;2;y*h519x#K&mT?~V zQBUBUf^$K+NZ3-JPC^wY#A@(GdpQsymAlF=mPaDBwV|r72W9HoU+H5bggG{!(Kju? zwlUjja+@lZibKZ4wcp);bxEF%-8bay6-wD3Wz?9GVqlhMI%c22M(D1`?xby!cUQW@bEO&f??HXS#({ zCpjxutj=K>*)QU@VG`es2FLcIM_LOQ?NJH6F^6R+$g?E7(!wxQS<7#CoXHSxxs=9fe%<_u zjiRgvJ2&|T8{6!&yyP9mSkspSDbzuBl0uLcPRypt6--?ifiO_V?5yv(U{KL{h7^ce2v?@BRBf*7lV8>LjC6 z96Ar5e7Fc0_LqvI@0Diwce1s*RAk8`w&gvM2aa*?sX-Nu3VW16hV2qAfM6w>^4ONMDBtjr1EhtGJZT8oeGB@y-fZusJA^IPEO8H8wCfIm zaJ0HVB5aaguGr`^bhIWwp1C>u zLHn5z!}UqcdS-c%vt+Oj(6{-_B^U%U>Ev!bI6q!ny?G=zT~e$!DQ$x6MY0F^evtY~ zv)r+KP~TJH=e8;vRNvmTgYY7LQe!V#krrbd|$t*Aj)0Nx`2-&ewJ!)dl zWiZt){-VXyp5l|Wie)UredBR*wcOT?n5?+Q4m1!#h;k$IHb7AVEPT<{@wco#T4(-SM@A&2m&8o(oTrGwb zn*FKz+JIO0x%i=>Qo~6po>`CM-%ae})aItzwSA}%vuJY8tNz$hEqG-PnqN&Vwk=RN zzTy4-u&Cz@irhnLhM+y)X5S9^gGZ{^2Et22l*U-(j)_fa5a}p!ny;Dq#MRemvUK<& znub&2^~=2%1V3e|v%rH{TNW|3J*b(Ki?o$lI z+dQK^siV_W?^0EDj=AhBjR&N`&EhlX$`T7y&vqU5i7fr(EhFn( z=O%YdQY_z^1%U|?l#86B+85B*jMQE$gs3f~_1jsgmwOSzvcBRV)8*O42;yUGb2CZ# z1S8!Jsr4JqACIAPfK`^2oYXJM;;IfhK*GF72(IGV}xsVD2z ziq>U|ezSWL)y^^tVd8B!Phr*XePIrzW`M?V$+bW=tyD1GrYEp^I%mOmfY7y<%=RRlt=@*L|l4lBDWMOTCFhDH%wF)6p2IacM z*#yQ7V;a{JHPi5dMpBXRvJ_lA1A0MOB!1103@=YULW8Z)^>eSUZ%lW-W|Wpsx2?;t+Q+(0nU~P9RFYU&W+Za_S%-re=3?`~!D2b67v+Fkx)v|sMwS`3Rr`9I5_%2y&F>^yJ#{rZUo4uR!o?< zXm2hHR(z)6Ma79efi|wK${UTC+EKT*+2-7IA{Vp`Ro7##rW)Ey$m<&S>didwzWCDZ zIr+HRxYs77>F`njom!$fo81uSpR5F$IFV%Q&yIKeVLX`&)wv?`+|ds^tWsKL`Az9x zP19)UHy_+%?NT`g@km19Af5@@mJy~J{@e?v-YrMh2m!W$hGggd_ugH3Pir~6gnyr- zJd&0Yye5tMB)>BUt{9*ab8Cc^xcpx@X#es)?siN5eRjIMUfJw?E0^CqE*CdmbOCe| zzs_KC(bIHmQC>DSOC2We#;L1N%LBAcM}O(t+oYCPZcl0Ta+7-~Xf z`x zO$Ru7o8O3YH`%OL!xhFvoP%@o@kMp4K-nvl$F#v%KOgs&KCk6_M$a&>2QLjJh=+%b z4Xh7m7?%~%B$q@UZg83}@>CyX&z}#>J1;+iE4k5TjX##J7TDNius366vP^Xwe`%Ve zP-EWlOVn!GUoMPrwvH*BOOn02P>1@5qH6&o>x&+rO?N474QvR+>iX1~;Emlo8Pk*; zEfKkC=ywY)tV-!i*&*H)X+ISLslr)R7ghO!L7s1N&;xNyae`(}d@zB!D73D4plRfR zXz5=Jrb^Un=iR^Eq#e8KI6~8XHfjVv%+WY%YO%tZH0J}+XrQM#T7?h`V?eoRxK03` z4QCjKcVm*X{vw6{9CNTA;5#BVVTAG-nevIWo9FZL^Ol<#;%o8}C!Jwu<lyuu8 z9@>70c8gBpX}|r}%_$9GI>6~K>9m;KPM?r5PW6Gj!M4n>;El9%iEdoZm&-(IyFpo% zEX*WTMkp#{=b(7pSkWF(6RDi;%7+j-tCGe-qq0CeEF8Qcpx!@tJj#$}QPwhuo8glY zu2-h95O(u;{@@Enz^;>b;$f4`P^ z;N5g1CX8_WlW1%~)gnAslJh4mu?X1V05(>a10lM8CV9yfLu~{pc52|u+-aF0BFX7d zFD@XGP4oD8_nNQE2?K~n9GC`rdn99?NtJo50 z^6R`o>U^ao3%zU+GWbSJZH~U@PWg=A$y$)+1C17|7%Tr%F@YlTk#s)3n_HKCN&*!bO-KjYTSqqaHL>rt;(Ldx zLO*Kg>pcEtntr)m)ogv)52uCB!Q+d*13ixa?nJi&@n(f**U;UG5@OQLQXb-25)cm$ zE9hUa39lVOkz&-&C@11&)nohqrkFs|0UgUPAkXTomdc=i@e>>J<~m!ls;Bjx zhY>E`dEHxi+S>bsCO5Qjq3x?WiJMtq9IhrgU=BjmI)iG;P%6)iGn6)-Ej0_K2Ib=5 zva|=DgQVe;H1!{7TYawfnf;FHJvigNtMt6_aIyhgA8{4kcO-O7RMc_s>k)6o)Nu4= zi(-*>v9U=Mk3L4ix276>7XuR@&uskgBYBT)4oubgrJ`&D_I#_3qzYf1Mfy8~DYNLJ z2SS1?pzW3)wdn|{_-S0D@zVg6SHV5v$uQ3A%U46I0OEl`eSmp_G18F#p->v)qlVTU zt@ev_%1?e}eppwiGa9zjoE(A4ZZvk0Q~hAGWb&GggYJfyErL~s!lD!tJhWb|EsPCx z%s9WO@bDgs(8>4ZUX(JDbrBQOG5ga6fTeKzLCEqu{#RZJ-B&k_Q3`D=X*`yi;}K}n z=#dX`rdnPu?=)Jl$!;JXY0iic(7#~Yq7_sXv--Lphcl`V@tJcxo$_+jR1v05Q-eL} zqKG=?SD$ggLi~Irav|@}ynH0D^_dRqy@MRp`DWE0Z|uZh0U{d?-|(;#LVWG4K*HuP zGj7HxiOY{y9X6mlLfqG71(sxf7ck!7kv1)#QoLcWo!o`U+-*O6V~6*s7nfouHsJZA zcQzwn?q@hb+W3;ukl*q9k;g$>lhgVUt>=3J;r@Np4Rn)NiFb6!7?u6&OtZjPOjTIj zB7~?gq%z+1oenw&t*7Gz%EbchcNzxbSa~_Vyz2GHMf2G6MtNO>u36GOA*;B*=6Xfg zS;x|8D)0Q75La3Ca@A#e_&BB5Dw9Zy3QBKV>)^Y})f`nebtd{XICLJ)ZN{hkE;`DI zx{R1ep8wD=#MXW_x>17T3N!0NWdDR&P!Ld-g3Nb+@j`|-jRO5uqF=J4>9#Q}XeI~5 z!_B#b2Y5E5QBn;VxhPcGLxjIq8`@pqdh zDPs{{s#D>?aB;z9J1?d6CH^;ty)|~a_$0$*9{g*}#`h$+V?0j_DYbSmSTY;|9v1V6L-%t;&$*1r{INp`_I~^%5PxbpD`sscT(x&} z8!Pxr7D zw1Ej7FyRG)>XS{}-E>(Mvn4o_Q;n&=MP-RcfqRy$s~Fmhjxkq)qBtd!-39AuZPDUU z(v#P~{+Es%dn1iY_;?2F&%92HJ=hJ1KQmXPlkRQ8wum$(QElHt+y}^tPIz45c8Iq7 z3U(}_?!C2ZML{2d@Wj=&1)2QgiF}GZ;9@v@(P0iysmz2n3%q?)h=D#3YsHEuM;oqe z;jX{qOD_+`E5N@NGS5-+fp{c1?FPU)Jmn*KrtQL)X!-#eJ6#Oi)8M8WIWuD4`rwh6 z0G}QFUBqwcYHA8ChDHMOBXLztiL{ye=Dv?dp$aaNj7*6d9-(6Kcnq+S0MOmY3cTKJ zw@dZjBHv7anl(vA<|TX@g-J@-09~xkR9fT^&+V3(W{T&xjP5|~$!nWWB2pBCC~1Md ziFNQ63!4E@M@@)Hp(fPl6%#PnH+}krnQ1?C3E*04kv8#Ee>IN^3I;lQuoA{UEnpC` z*E(4K1!Z|8-OR$5BnryK#R>nDmyzuv`PFQ)hL|mEQz2aaO2F6|l#7VjC;*)Q7{&wbeZyY>0o~hgXF8>_pP}$bQ#P3~u+Sg#%_(qwCX2K@M5M2rmjN@II@A#%-z=jm zo1pHd2^cRf%o8XH&a)4M9j;!Prx)}Y=3tE~GjLKE&se{#pOeByqBafv(^racEXDuX zmtpYaH&BhRpzgOyUOnK3N-!KRiC&7E*)S=wwUv9DW$CEnb$yr}vrvH69Vw})3G%G5 z4xAg8uDed9j%A!3L5+@&8eo}#ac3UxPpPf2-DiF~xAONps|}%0T@->V`rAI!Mj3@B4DX~ z=lPI=5Rj?K`tkMWxsSfxoF?PB>0GX3S#@npku(zA_v9A!yNSOaug0T^tK4yOo7}$f z;GV1Cg>q-#*N}F@?dh$Uk*aBtkxzmW0_Nq_31x0ay$N8yI$^LOcEH zX}YFuYBVq{8V}@tq-P&%F>2BDC13dtJ<%+cqFp@?rtN82FGZ^HLagzZ@dV(DyTU@w-^3-5{OCv1E8N$kpKVy literal 0 HcmV?d00001 diff --git a/tests/data/scanner/simple/song1.mp3 b/tests/data/scanner/simple/song1.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/simple/song1.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file From 8b963311e1e1449fd34c1a68bbe859fe51cb9fdb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 21:38:46 +0200 Subject: [PATCH 15/82] Update scanner test --- tests/scanner.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/scanner.py b/tests/scanner.py index 5374e856..091e8846 100644 --- a/tests/scanner.py +++ b/tests/scanner.py @@ -14,32 +14,34 @@ class ScannerTest(unittest.TestCase): self.data_callback, self.error_callback) scanner.start() + def check(self, name, key, value): + name = data_folder(name) + self.assertEqual(self.data[name][key], value) + def data_callback(self, uri, data): - uri = uri.lstrip('file://') - uri = uri.lstrip(data_folder('')) + uri = uri[len('file://'):] self.data[uri] = data def error_callback(self, uri, errors): - uri = uri.lstrip('file://') - uri = uri.lstrip(data_folder('')) + uri = uri[len('file://'):] self.errors[uri] = errors def test_data_is_set(self): - self.scan('blank.mp3') + self.scan('scanner/simple') self.assert_(self.data) def test_errors_is_not_set(self): - self.scan('blank.mp3') + self.scan('scanner/simple') self.assert_(not self.errors) def test_artist_is_set(self): - self.scan('blank.mp3') - self.assertEqual(self.data['blank.mp3']['artist'], 'artist') + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'artist', 'name') def test_album_is_set(self): - self.scan('blank.mp3') - self.assertEqual(self.data['blank.mp3']['album'], 'album') + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'album', 'albumname') def test_track_is_set(self): - self.scan('blank.mp3') - self.assertEqual(self.data['blank.mp3']['title'], 'title') + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'title', 'trackname') From 71e2f2c43a485199a3d650c60a8dfb05297aa739 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 21:51:22 +0200 Subject: [PATCH 16/82] Add mpd generated sample tag caches --- tests/data/scanner/advanced_cache | 81 +++++++++++++++++++++++++++++++ tests/data/scanner/empty_cache | 6 +++ tests/data/scanner/simple_cache | 15 ++++++ 3 files changed, 102 insertions(+) create mode 100644 tests/data/scanner/advanced_cache create mode 100644 tests/data/scanner/empty_cache create mode 100644 tests/data/scanner/simple_cache diff --git a/tests/data/scanner/advanced_cache b/tests/data/scanner/advanced_cache new file mode 100644 index 00000000..60f7fca6 --- /dev/null +++ b/tests/data/scanner/advanced_cache @@ -0,0 +1,81 @@ +info_begin +mpd_version: 0.15.4 +fs_charset: UTF-8 +info_end +directory: subdir1 +mtime: 1288121499 +begin: subdir1 +songList begin +key: song4.mp3 +file: subdir1/song4.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song5.mp3 +file: subdir1/song5.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end +end: subdir1 +directory: subdir2 +mtime: 1288121499 +begin: subdir2 +songList begin +key: song6.mp3 +file: subdir2/song6.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song7.mp3 +file: subdir2/song7.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end +end: subdir2 +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song2.mp3 +file: /song2.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song3.mp3 +file: /song3.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end diff --git a/tests/data/scanner/empty_cache b/tests/data/scanner/empty_cache new file mode 100644 index 00000000..3c466a32 --- /dev/null +++ b/tests/data/scanner/empty_cache @@ -0,0 +1,6 @@ +info_begin +mpd_version: 0.15.4 +fs_charset: UTF-8 +info_end +songList begin +songList end diff --git a/tests/data/scanner/simple_cache b/tests/data/scanner/simple_cache new file mode 100644 index 00000000..db11c324 --- /dev/null +++ b/tests/data/scanner/simple_cache @@ -0,0 +1,15 @@ +info_begin +mpd_version: 0.15.4 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end From b76ae84af273413b2022f7bb7908fb33f149c3e6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:13:45 +0200 Subject: [PATCH 17/82] Add basic scanner translator and test --- mopidy/scanner.py | 28 ++++++++++++++++++++++++++++ tests/scanner.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 914f431e..288c8fd1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -6,10 +6,38 @@ pygst.require('0.10') import gst from os.path import abspath +import datetime import sys import threading from mopidy.utils.path import path_to_uri, find_files +from mopidy.models import Track, Artist, Album + +def translator(data): + album = Album( + name=data['album'], + num_tracks=data['track-count'], + ) + + artist = Artist( + name=data['artist'], + ) + + date = datetime.date( + data['date'].year, + data['date'].month, + data['date'].day, + ) + + return Track( + uri=data['uri'], + name=data['title'], + album=album, + artists=[artist], + date=date, + track_no=data['track-number'], + ) + class Scanner(object): def __init__(self, folder, data_callback, error_callback=None): diff --git a/tests/scanner.py b/tests/scanner.py index 091e8846..a30ca9de 100644 --- a/tests/scanner.py +++ b/tests/scanner.py @@ -1,9 +1,45 @@ import unittest +from datetime import date -from mopidy.scanner import Scanner +from mopidy.scanner import Scanner, translator +from mopidy.models import Track, Artist, Album from tests import data_folder +class FakeGstDate(object): + def __init__(self, year, month, day): + self.year = year + self.month = month + self.day = day + +class TranslatorTest(unittest.TestCase): + def test_basic_data(self): + data = { + 'uri': 'uri', + 'album': u'albumname', + 'track-number': 1, + 'artist': u'name', + 'title': u'trackname', + 'track-count': 2, + 'date': FakeGstDate(2006, 1, 1,), + 'container-format': u'ID3 tag', + # length etc? + } + + expected = Track( + uri='uri', + name='trackname', + album=Album(name='albumname', num_tracks=2), + artists=[Artist(name='name')], + date=date(2006, 1, 1), + track_no=1, + ) + + actual = translator(data) + + self.assertEqual(expected, actual) + + class ScannerTest(unittest.TestCase): def setUp(self): self.errors = {} From 9098f3104cf7b3825376feff0569cd7bd5dd3a47 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:14:08 +0200 Subject: [PATCH 18/82] Rename scanner test filename --- tests/{scanner.py => scanner_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{scanner.py => scanner_test.py} (100%) diff --git a/tests/scanner.py b/tests/scanner_test.py similarity index 100% rename from tests/scanner.py rename to tests/scanner_test.py From 7e026174225b8516ee084b82c37bc2466cb124be Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:34:10 +0200 Subject: [PATCH 19/82] Move test data in translator test --- tests/scanner_test.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index a30ca9de..7ba6d56c 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -13,8 +13,8 @@ class FakeGstDate(object): self.day = day class TranslatorTest(unittest.TestCase): - def test_basic_data(self): - data = { + def setUp(self): + self.data = { 'uri': 'uri', 'album': u'albumname', 'track-number': 1, @@ -26,6 +26,7 @@ class TranslatorTest(unittest.TestCase): # length etc? } + def test_basic_data(self): expected = Track( uri='uri', name='trackname', @@ -34,10 +35,7 @@ class TranslatorTest(unittest.TestCase): date=date(2006, 1, 1), track_no=1, ) - - actual = translator(data) - - self.assertEqual(expected, actual) + self.assertEqual(expected, translator(self.data)) class ScannerTest(unittest.TestCase): From 045a5a58c54803541fe77f4f02af42b506fe1a98 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:41:12 +0200 Subject: [PATCH 20/82] Move uri to scanner data --- mopidy/scanner.py | 5 +++-- tests/scanner_test.py | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 288c8fd1..f3792d5a 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -57,8 +57,9 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() - uri = self.uribin.get_property('uri') - self.data_callback(uri, dict([(k, data[k]) for k in data.keys()])) + data = dict([(k, data[k]) for k in data.keys()]) + data['uri'] = self.uribin.get_property('uri') + self.data_callback(data) self.next_uri() def process_error(self, bus, message): diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 7ba6d56c..b3f214a9 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -52,8 +52,8 @@ class ScannerTest(unittest.TestCase): name = data_folder(name) self.assertEqual(self.data[name][key], value) - def data_callback(self, uri, data): - uri = uri[len('file://'):] + def data_callback(self, data): + uri = data['uri'][len('file://'):] self.data[uri] = data def error_callback(self, uri, errors): @@ -68,6 +68,11 @@ class ScannerTest(unittest.TestCase): self.scan('scanner/simple') self.assert_(not self.errors) + def test_uri_is_set(self): + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'uri', 'file://' + + data_folder('scanner/simple/song1.mp3')) + def test_artist_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'artist', 'name') From d6516915e5e8f59b9cb21351469fa41ca8dbcdd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Oct 2010 00:15:08 +0200 Subject: [PATCH 21/82] Setup proper pipeline for handling of files in scanner --- mopidy/scanner.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index f3792d5a..937ee2b1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -46,8 +46,14 @@ class Scanner(object): self.error_callback = error_callback self.loop = gobject.MainLoop() + fakesink = gst.element_factory_make('fakesink') + pad = fakesink.get_pad('sink') + self.uribin = gst.element_factory_make('uridecodebin') + self.uribin.connect('pad-added', self.process_new_pad, pad) + self.pipe = gst.element_factory_make('pipeline') + self.pipe.add(fakesink) self.pipe.add(self.uribin) bus = self.pipe.get_bus() @@ -55,7 +61,13 @@ class Scanner(object): bus.connect('message::tag', self.process_tags) bus.connect('message::error', self.process_error) + def process_new_pad(self, source, pad, target_pad): + pad.link(target_pad) + def process_tags(self, bus, message): + # Block for state change so that duration can be safely determined + self.pipe.get_state() + data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = self.uribin.get_property('uri') From 79887c1988b41584574042f5147a88dae070cc7c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Oct 2010 00:15:26 +0200 Subject: [PATCH 22/82] Add duration to scanner data --- mopidy/scanner.py | 8 ++++++++ tests/scanner_test.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 937ee2b1..79d6c036 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -71,6 +71,7 @@ class Scanner(object): data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = self.uribin.get_property('uri') + data['duration'] = self.get_duration() self.data_callback(data) self.next_uri() @@ -81,6 +82,13 @@ class Scanner(object): self.error_callback(uri, errors) self.next_uri() + def get_duration(self): + try: + return self.pipe.query_duration( + gst.FORMAT_TIME, None)[0] // gst.MSECOND + except gst.QueryError: + return None + def next_uri(self): if not self.uris: return self.stop() diff --git a/tests/scanner_test.py b/tests/scanner_test.py index b3f214a9..d47cc7de 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -73,6 +73,10 @@ class ScannerTest(unittest.TestCase): self.check('scanner/simple/song1.mp3', 'uri', 'file://' + data_folder('scanner/simple/song1.mp3')) + def test_duration_is_set(self): + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'duration', 4680) + def test_artist_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'artist', 'name') From 889b8fdb43eb77a7a62ed0b551170108883024cc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Oct 2010 00:17:03 +0200 Subject: [PATCH 23/82] Add length to scanner translator --- mopidy/scanner.py | 1 + tests/scanner_test.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 79d6c036..ccc3ce9c 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -36,6 +36,7 @@ def translator(data): artists=[artist], date=date, track_no=data['track-number'], + length=data['duration'], ) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index d47cc7de..4b13f47c 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -23,7 +23,7 @@ class TranslatorTest(unittest.TestCase): 'track-count': 2, 'date': FakeGstDate(2006, 1, 1,), 'container-format': u'ID3 tag', - # length etc? + 'duration': 4531, } def test_basic_data(self): @@ -34,6 +34,7 @@ class TranslatorTest(unittest.TestCase): artists=[Artist(name='name')], date=date(2006, 1, 1), track_no=1, + length=4531, ) self.assertEqual(expected, translator(self.data)) From 3178ef46268f664bc09da19246ec9ab30206c41a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:02:51 +0200 Subject: [PATCH 24/82] Move get_state to duration call to fix intermittent errors --- mopidy/scanner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index ccc3ce9c..991a0864 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -66,9 +66,6 @@ class Scanner(object): pad.link(target_pad) def process_tags(self, bus, message): - # Block for state change so that duration can be safely determined - self.pipe.get_state() - data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = self.uribin.get_property('uri') @@ -84,6 +81,7 @@ class Scanner(object): self.next_uri() def get_duration(self): + self.pipe.get_state() try: return self.pipe.query_duration( gst.FORMAT_TIME, None)[0] // gst.MSECOND From c247e1455b9fc0fe42eaf4e2f2dfab2aedcbd6ba Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:03:14 +0200 Subject: [PATCH 25/82] Ensure that state is null on exit --- mopidy/scanner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 991a0864..6537f35a 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -101,4 +101,5 @@ class Scanner(object): self.loop.run() def stop(self): + self.pipe.set_state(gst.STATE_NULL) self.loop.quit() From 2ebaa38ed92e5fc7a9422ea52abb6f324936b2e7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:40:38 +0200 Subject: [PATCH 26/82] Add other media test and set caps to limit to audio for scanner --- mopidy/scanner.py | 2 ++ tests/data/scanner/image/test.png | Bin 0 -> 176 bytes tests/scanner_test.py | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 tests/data/scanner/image/test.png diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 6537f35a..491fc00b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -47,11 +47,13 @@ class Scanner(object): self.error_callback = error_callback self.loop = gobject.MainLoop() + caps = gst.Caps('audio/x-raw-int') fakesink = gst.element_factory_make('fakesink') pad = fakesink.get_pad('sink') self.uribin = gst.element_factory_make('uridecodebin') self.uribin.connect('pad-added', self.process_new_pad, pad) + self.uribin.set_property('caps', caps) self.pipe = gst.element_factory_make('pipeline') self.pipe.add(fakesink) diff --git a/tests/data/scanner/image/test.png b/tests/data/scanner/image/test.png new file mode 100644 index 0000000000000000000000000000000000000000..2aaf9c3ddb4c715b788d32514300b0484d5b7156 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy#^NA%Cx&(BWL^R}oCO|{#S9GG z!XV7ZFl&wkP>{XE)7O>#7MHZBfvD8_ccDNb$r9Iy66gHf+|;}h2Ir#G#FEq$h4Rdj z3gTe~DWM4fc^EG= literal 0 HcmV?d00001 diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 4b13f47c..d6639ce1 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -89,3 +89,7 @@ class ScannerTest(unittest.TestCase): def test_track_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'title', 'trackname') + + def test_other_media_is_ignored(self): + self.scan('scanner/image') + self.assert_(self.errors) From d9d393ac218ffa176d17ab1e6d0e957331b8ecb4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:47:18 +0200 Subject: [PATCH 27/82] Ensure that scanner does not die on non-existant folders --- mopidy/scanner.py | 5 +++-- tests/scanner_test.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 491fc00b..3d5507dc 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -99,8 +99,9 @@ class Scanner(object): self.pipe.set_state(gst.STATE_PAUSED) def start(self): - self.next_uri() - self.loop.run() + if self.uris: + self.next_uri() + self.loop.run() def stop(self): self.pipe.set_state(gst.STATE_NULL) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index d6639ce1..895c73a0 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -90,6 +90,10 @@ class ScannerTest(unittest.TestCase): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'title', 'trackname') + def test_nonexistant_folder_does_not_fail(self): + self.scan('scanner/does-not-exist') + self.assert_(not self.errors) + def test_other_media_is_ignored(self): self.scan('scanner/image') self.assert_(self.errors) From 81da02187f3e5cad6dbcde00e5bde69e8ba08707 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:04:02 +0200 Subject: [PATCH 28/82] Start refactoring translator --- mopidy/scanner.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 3d5507dc..1f990d79 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -14,30 +14,29 @@ from mopidy.utils.path import path_to_uri, find_files from mopidy.models import Track, Artist, Album def translator(data): - album = Album( - name=data['album'], - num_tracks=data['track-count'], - ) + album_kwargs = {} + album_kwargs['name'] = data['album'] + album_kwargs['num_tracks'] = data['track-count'] - artist = Artist( - name=data['artist'], - ) + artist_kwargs = {} + artist_kwargs['name'] =data['artist'] - date = datetime.date( - data['date'].year, - data['date'].month, - data['date'].day, - ) + date = data['date'] + date = datetime.date(date.year, date.month, date.day) - return Track( - uri=data['uri'], - name=data['title'], - album=album, - artists=[artist], - date=date, - track_no=data['track-number'], - length=data['duration'], - ) + track_kwargs = {} + track_kwargs['uri'] = data['uri'] + track_kwargs['name'] = data['title'] + track_kwargs['album'] = Album(**album_kwargs) + track_kwargs['artists'] = [Artist(**artist_kwargs)] + track_kwargs['date'] = date + + if 'track-number' in data: + track_kwargs['track_no'] = data['track-number'] + + track_kwargs['length'] = data['duration'] + + return Track(**track_kwargs) class Scanner(object): From 766c447c71b8da13c6c11c1abab9b89719ffe316 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:04:18 +0200 Subject: [PATCH 29/82] Refactor translator test --- tests/scanner_test.py | 44 +++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 895c73a0..104235a6 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -26,18 +26,42 @@ class TranslatorTest(unittest.TestCase): 'duration': 4531, } - def test_basic_data(self): - expected = Track( - uri='uri', - name='trackname', - album=Album(name='albumname', num_tracks=2), - artists=[Artist(name='name')], - date=date(2006, 1, 1), - track_no=1, - length=4531, - ) + self.album = { + 'name': 'albumname', + 'num_tracks': 2, + } + + self.artist = { + 'name': 'name', + } + + self.track = { + 'uri': 'uri', + 'name': 'trackname', + 'date': date(2006, 1, 1), + 'track_no': 1, + 'length': 4531, + } + + def build_track(self): + if self.album: + self.track['album'] = Album(**self.album) + if self.artist: + self.track['artists'] = [Artist(**self.artist)] + return Track(**self.track) + + def check(self): + expected = self.build_track() + actual = translator(self.data) self.assertEqual(expected, translator(self.data)) + def test_basic_data(self): + self.check() + + def test_missing_track_number(self): + del self.track['track_no'] + del self.data['track-number'] + self.check() class ScannerTest(unittest.TestCase): def setUp(self): From ab97f780608e09f671617c8d628d12e1edeecc15 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:12:20 +0200 Subject: [PATCH 30/82] Test all optional translator values --- mopidy/scanner.py | 33 ++++++++++++++++++++------------- tests/scanner_test.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 1f990d79..2222d3a0 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -15,26 +15,33 @@ from mopidy.models import Track, Artist, Album def translator(data): album_kwargs = {} - album_kwargs['name'] = data['album'] - album_kwargs['num_tracks'] = data['track-count'] - artist_kwargs = {} - artist_kwargs['name'] =data['artist'] - - date = data['date'] - date = datetime.date(date.year, date.month, date.day) - track_kwargs = {} - track_kwargs['uri'] = data['uri'] - track_kwargs['name'] = data['title'] - track_kwargs['album'] = Album(**album_kwargs) - track_kwargs['artists'] = [Artist(**artist_kwargs)] - track_kwargs['date'] = date + + if 'album' in data: + album_kwargs['name'] = data['album'] + + if 'track-count' in data: + album_kwargs['num_tracks'] = data['track-count'] + + if 'artist' in data: + artist_kwargs['name'] =data['artist'] + + if 'date' in data: + date = data['date'] + date = datetime.date(date.year, date.month, date.day) + track_kwargs['date'] = date + + if 'title' in data: + track_kwargs['name'] = data['title'] if 'track-number' in data: track_kwargs['track_no'] = data['track-number'] + track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data['duration'] + track_kwargs['album'] = Album(**album_kwargs) + track_kwargs['artists'] = [Artist(**artist_kwargs)] return Track(**track_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 104235a6..b33ed99a 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -44,10 +44,8 @@ class TranslatorTest(unittest.TestCase): } def build_track(self): - if self.album: - self.track['album'] = Album(**self.album) - if self.artist: - self.track['artists'] = [Artist(**self.artist)] + self.track['album'] = Album(**self.album) + self.track['artists'] = [Artist(**self.artist)] return Track(**self.track) def check(self): @@ -59,8 +57,33 @@ class TranslatorTest(unittest.TestCase): self.check() def test_missing_track_number(self): - del self.track['track_no'] del self.data['track-number'] + del self.track['track_no'] + self.check() + + def test_missing_track_count(self): + del self.data['track-count'] + del self.album['num_tracks'] + self.check() + + def test_missing_track_name(self): + del self.data['title'] + del self.track['name'] + self.check() + + def test_missing_album_name(self): + del self.data['album'] + del self.album['name'] + self.check() + + def test_missing_artist_name(self): + del self.data['artist'] + del self.artist['name'] + self.check() + + def test_missing_date(self): + del self.data['date'] + del self.track['date'] self.check() class ScannerTest(unittest.TestCase): From 9cb9cf3e0cbbbb19398b74a02584aa30cf8206eb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:28:41 +0200 Subject: [PATCH 31/82] Clean start scanner code a tiny bit --- mopidy/scanner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 2222d3a0..ccdab24b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -105,9 +105,10 @@ class Scanner(object): self.pipe.set_state(gst.STATE_PAUSED) def start(self): - if self.uris: - self.next_uri() - self.loop.run() + if not self.uris: + return + self.next_uri() + self.loop.run() def stop(self): self.pipe.set_state(gst.STATE_NULL) From 3e595213ac26c8770b02bf42620bff3221fbde39 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 19:34:33 +0200 Subject: [PATCH 32/82] Ensure that result has exact right number of elements --- tests/frontends/mpd/serializer_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 0e0f8183..6bd61e12 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -14,6 +14,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Album', '') in result) self.assert_(('Track', 0) in result) self.assert_(('Date', '') in result) + self.assertEqual(len(result), 7) def test_mpd_format_for_nonempty_track(self): track = Track( @@ -35,11 +36,12 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', dt.date(1977, 1, 1)) in result) self.assert_(('Pos', 9) in result) self.assert_(('Id', 122) in result) + self.assertEqual(len(result), 9) def test_mpd_format_artists(self): track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) - self.assertEqual(translator.track_artists_to_mpd_format(track), - u'ABBA, Beatles') + translated = translator.track_artists_to_mpd_format(track) + self.assertEqual(translated, u'ABBA, Beatles') class PlaylistMpdFormatTest(unittest.TestCase): From 02df8ca033051d08a724d43dbfea1aec0a6fbd6f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 20:04:53 +0200 Subject: [PATCH 33/82] Start writting tracks_to_tag_cache_format --- mopidy/frontends/mpd/translator.py | 15 +++++++++++++++ tests/frontends/mpd/serializer_test.py | 21 ++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 07a58dd3..44003dfd 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,3 +1,5 @@ +from mopidy.frontends.mpd import protocol + def track_to_mpd_format(track, position=None, cpid=None): """ Format track for output to MPD client. @@ -72,3 +74,16 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): Arguments as for :func:`tracks_to_mpd_format`, except the first one. """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) + +def tracks_to_tag_cache_format(tracks): + result = [ + ('info_begin',), + ('mpd_version', protocol.VERSION), + ('fs_charset', protocol.ENCODING), + ('info_end',) + ] + + result.append(('songList begin',)) + result.append(('songList end',)) + + return result diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6bd61e12..1729df70 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,7 +1,7 @@ import datetime as dt import unittest -from mopidy.frontends.mpd import translator +from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track class TrackMpdFormatTest(unittest.TestCase): @@ -57,3 +57,22 @@ class PlaylistMpdFormatTest(unittest.TestCase): result = translator.playlist_to_mpd_format(playlist, 1, 2) self.assertEqual(len(result), 1) self.assertEqual(dict(result[0])['Track'], 2) + + +class TracksToTagCacheFormatTest(unittest.TestCase): + header_length = 4 + + def check_headers(self, result): + self.assert_(('info_begin',) in result) + self.assert_(('mpd_version', protocol.VERSION) in result) + self.assert_(('fs_charset', protocol.ENCODING) in result) + self.assert_(('info_end',) in result) + + def test_empty_tag_cache(self): + result = translator.tracks_to_tag_cache_format([]) + self.check_headers(result) + + self.assert_(('songList begin',) in result) + self.assert_(('songList end',) in result) + self.assertEqual(len(result), self.header_length+2) + From 44012b30b02acf9b99261383bf72cce90557aa98 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 20:32:50 +0200 Subject: [PATCH 34/82] Add uri_to_mpd_relative_path --- mopidy/frontends/mpd/translator.py | 8 ++++++++ tests/frontends/mpd/serializer_test.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 44003dfd..28d1e08b 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,4 +1,8 @@ +import re + +from mopidy import settings from mopidy.frontends.mpd import protocol +from mopidy.utils.path import path_to_uri def track_to_mpd_format(track, position=None, cpid=None): """ @@ -75,6 +79,10 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) +def uri_to_mpd_relative_path(uri): + path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) + return re.sub('^' + re.escape(path), '', uri) + def tracks_to_tag_cache_format(tracks): result = [ ('info_begin',), diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 1729df70..cd9320b6 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,6 +1,7 @@ import datetime as dt import unittest +from mopidy import settings from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track @@ -59,6 +60,19 @@ class PlaylistMpdFormatTest(unittest.TestCase): self.assertEqual(dict(result[0])['Track'], 2) +class UriToMpdRelativePathTest(unittest.TestCase): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + + def tearDown(self): + settings.runtime.clear() + + def test_file_gets_stripped(self): + uri = 'file:///dir/subdir/music/album/song.mp3' + result = translator.uri_to_mpd_relative_path(uri) + self.assertEqual('/music/album/song.mp3', result) + + class TracksToTagCacheFormatTest(unittest.TestCase): header_length = 4 @@ -75,4 +89,3 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assert_(('songList begin',) in result) self.assert_(('songList end',) in result) self.assertEqual(len(result), self.header_length+2) - From 8240dbbb557e7ef56b51e9d6d04e9b0e35e0c1ab Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 20:36:38 +0200 Subject: [PATCH 35/82] Add docstrings --- mopidy/frontends/mpd/translator.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 28d1e08b..ccd83fde 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -80,10 +80,24 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) def uri_to_mpd_relative_path(uri): + """ + Strip uri and LOCAL_MUSIC_FOLDER part of uri. + + :param uri: the uri + :type uri: string + :rtype: string + """ path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) return re.sub('^' + re.escape(path), '', uri) def tracks_to_tag_cache_format(tracks): + """ + Format list of tracks for output to MPD tag cache + + :param tracks: the tracks + :type tracks: list of :class:`mopidy.models.Track` + :rtype: list of lists of two-tuples + """ result = [ ('info_begin',), ('mpd_version', protocol.VERSION), From 014e29ffa22d010ee572902a371a7c177ff1c078 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:15:32 +0200 Subject: [PATCH 36/82] Fix uri_to_mpd_relative_path so that it handles None --- mopidy/frontends/mpd/translator.py | 2 ++ tests/frontends/mpd/serializer_test.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index ccd83fde..f66c64a7 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -87,6 +87,8 @@ def uri_to_mpd_relative_path(uri): :type uri: string :rtype: string """ + if uri is None: + return '' path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) return re.sub('^' + re.escape(path), '', uri) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index cd9320b6..960e5ebd 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -67,6 +67,11 @@ class UriToMpdRelativePathTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() + def test_none_file_returns_empty_string(self): + uri = 'file:///dir/subdir/music/album/song.mp3' + result = translator.uri_to_mpd_relative_path(None) + self.assertEqual('', result) + def test_file_gets_stripped(self): uri = 'file:///dir/subdir/music/album/song.mp3' result = translator.uri_to_mpd_relative_path(uri) From cf516c5ef04d1a2a709f384d0df3132a72f5f741 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:17:08 +0200 Subject: [PATCH 37/82] Ensure that mpd_format_track handles file: properly --- mopidy/frontends/mpd/translator.py | 2 +- tests/frontends/mpd/serializer_test.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index f66c64a7..49fc064d 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -17,7 +17,7 @@ def track_to_mpd_format(track, position=None, cpid=None): :rtype: list of two-tuples """ result = [ - ('file', track.uri or ''), + ('file', uri_to_mpd_relative_path(track.uri) or ''), ('Time', track.length and (track.length // 1000) or 0), ('Artist', track_artists_to_mpd_format(track)), ('Title', track.name or ''), diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 960e5ebd..af12a0de 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -6,6 +6,12 @@ from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track class TrackMpdFormatTest(unittest.TestCase): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + + def tearDown(self): + settings.runtime.clear() + def test_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) self.assert_(('file', '') in result) @@ -17,6 +23,12 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', '') in result) self.assertEqual(len(result), 7) + def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): + track = Track(uri='file:///dir/subdir/song.mp3') + path = dict(translator.track_to_mpd_format(track))['file'] + correct_path = translator.uri_to_mpd_relative_path(track.uri) + self.assertEqual(path, correct_path) + def test_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', From 151b1c3b4ebed89c195b5352b63f3b05f4f3a49a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:23:15 +0200 Subject: [PATCH 38/82] Add explicit tests for position and cpid in track_to_mpd_format --- tests/frontends/mpd/serializer_test.py | 50 +++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index af12a0de..09db3394 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -23,6 +23,19 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', '') in result) self.assertEqual(len(result), 7) + def test_mpd_format_with_position(self): + result = translator.track_to_mpd_format(Track(), position=1) + self.assert_(('Pos', 1) not in result) + + def test_mpd_format_with_cpid(self): + result = translator.track_to_mpd_format(Track(), cpid=1) + self.assert_(('Id', 1) not in result) + + def test_mpd_format_with_position_and_cpid(self): + result = translator.track_to_mpd_format(Track(), position=1, cpid=2) + self.assert_(('Pos', 1) in result) + self.assert_(('Id', 2) in result) + def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] @@ -91,18 +104,45 @@ class UriToMpdRelativePathTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): - header_length = 4 + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + + def tearDown(self): + settings.runtime.clear() def check_headers(self, result): self.assert_(('info_begin',) in result) self.assert_(('mpd_version', protocol.VERSION) in result) self.assert_(('fs_charset', protocol.ENCODING) in result) self.assert_(('info_end',) in result) + return result[4:] + + def check_song_list(self, result): + self.assertEqual(('songList begin',), result[0]) + self.assertEqual(('songList end',), result[-1]) + return result[1:-1] def test_empty_tag_cache(self): result = translator.tracks_to_tag_cache_format([]) - self.check_headers(result) + result = self.check_headers(result) + result = self.check_song_list(result) + self.assertEqual(len(result), 0) - self.assert_(('songList begin',) in result) - self.assert_(('songList end',) in result) - self.assertEqual(len(result), self.header_length+2) + def test_simple_tag_cache_has_header(self): + track = Track(uri='file:///dir/subdir/song.mp3') + result = translator.tracks_to_tag_cache_format([track]) + result = self.check_headers(result) + result = self.check_song_list(result) + self.assertEqual(len(result), 0) + + def test_simple_tag_cache_has_header(self): + track = Track(uri='file:///dir/subdir/song.mp3') + formated = translator.track_to_mpd_format(track) + formated.insert(0, ('key', 'song.mp3')) + + result = translator.tracks_to_tag_cache_format([track]) + result = self.check_headers(result) + result = self.check_song_list(result) + + for a, b in zip(result, formated): + self.assertEqual(a, b) From 488ac284311e7beb8df66d7c6f147b3784e3aeeb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:23:50 +0200 Subject: [PATCH 39/82] Add key parameter to track_to_mpd_format --- mopidy/frontends/mpd/translator.py | 5 ++++- tests/frontends/mpd/serializer_test.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 49fc064d..87708da5 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,10 +1,11 @@ +import os import re from mopidy import settings from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri -def track_to_mpd_format(track, position=None, cpid=None): +def track_to_mpd_format(track, position=None, cpid=None, key=None): """ Format track for output to MPD client. @@ -32,6 +33,8 @@ def track_to_mpd_format(track, position=None, cpid=None): if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) + if key is not None: + result.insert(0, ('key', key)) return result def track_artists_to_mpd_format(track): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 09db3394..ba4e8f18 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -36,6 +36,10 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Pos', 1) in result) self.assert_(('Id', 2) in result) + def test_mpd_format_with_key(self): + result = translator.track_to_mpd_format(Track(), key='file.mp3') + self.assert_(('key', 'file.mp3') in result) + def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] From b2cb3136b25db39c3d169fd6bff397817bec27c5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:27:40 +0200 Subject: [PATCH 40/82] Rename tests --- tests/frontends/mpd/serializer_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index ba4e8f18..99174ad3 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -12,7 +12,7 @@ class TrackMpdFormatTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() - def test_mpd_format_for_empty_track(self): + def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) self.assert_(('file', '') in result) self.assert_(('Time', 0) in result) @@ -23,30 +23,30 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', '') in result) self.assertEqual(len(result), 7) - def test_mpd_format_with_position(self): + def test_track_to_mpd_format_with_position(self): result = translator.track_to_mpd_format(Track(), position=1) self.assert_(('Pos', 1) not in result) - def test_mpd_format_with_cpid(self): + def test_track_to_mpd_format_with_cpid(self): result = translator.track_to_mpd_format(Track(), cpid=1) self.assert_(('Id', 1) not in result) - def test_mpd_format_with_position_and_cpid(self): + def test_track_to_mpd_format_with_position_and_cpid(self): result = translator.track_to_mpd_format(Track(), position=1, cpid=2) self.assert_(('Pos', 1) in result) self.assert_(('Id', 2) in result) - def test_mpd_format_with_key(self): + def test_track_to_mpd_format_with_key(self): result = translator.track_to_mpd_format(Track(), key='file.mp3') self.assert_(('key', 'file.mp3') in result) - def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): + def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] correct_path = translator.uri_to_mpd_relative_path(track.uri) self.assertEqual(path, correct_path) - def test_mpd_format_for_nonempty_track(self): + def test_track_to_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', artists=[Artist(name=u'an artist')], @@ -68,7 +68,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 122) in result) self.assertEqual(len(result), 9) - def test_mpd_format_artists(self): + def test_track_artists_to_mpd_format(self): track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) translated = translator.track_artists_to_mpd_format(track) self.assertEqual(translated, u'ABBA, Beatles') From a8a447c47fe59d46b596aa54c391473acb165813 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:36:11 +0200 Subject: [PATCH 41/82] Use key=True for track_to_mpd_format generation --- mopidy/frontends/mpd/translator.py | 6 ++++-- tests/frontends/mpd/serializer_test.py | 12 +++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 87708da5..3f7a6adf 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -33,8 +33,8 @@ def track_to_mpd_format(track, position=None, cpid=None, key=None): if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) - if key is not None: - result.insert(0, ('key', key)) + if key and track.uri: + result.insert(0, ('key', os.path.basename(track.uri))) return result def track_artists_to_mpd_format(track): @@ -111,6 +111,8 @@ def tracks_to_tag_cache_format(tracks): ] result.append(('songList begin',)) + for track in tracks: + result.extend(track_to_mpd_format(track, key=True)) result.append(('songList end',)) return result diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 99174ad3..4b4bdabb 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -37,7 +37,8 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 2) in result) def test_track_to_mpd_format_with_key(self): - result = translator.track_to_mpd_format(Track(), key='file.mp3') + track = Track(uri='file:///dir/subdir/file.mp3') + result = translator.track_to_mpd_format(track, key=True) self.assert_(('key', 'file.mp3') in result) def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): @@ -137,16 +138,13 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = translator.tracks_to_tag_cache_format([track]) result = self.check_headers(result) result = self.check_song_list(result) - self.assertEqual(len(result), 0) - def test_simple_tag_cache_has_header(self): + def test_simple_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track) - formated.insert(0, ('key', 'song.mp3')) + formated = translator.track_to_mpd_format(track, key=True) result = translator.tracks_to_tag_cache_format([track]) result = self.check_headers(result) result = self.check_song_list(result) - for a, b in zip(result, formated): - self.assertEqual(a, b) + self.assertEqual(result, formated) From 21eadf3dc734c0fa8a3e742cc2c4728447f8a770 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:56:20 +0200 Subject: [PATCH 42/82] Refactor tag_cache generation tests --- tests/frontends/mpd/serializer_test.py | 55 +++++++++++++++++--------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 4b4bdabb..de0a6814 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -115,36 +115,53 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() - def check_headers(self, result): - self.assert_(('info_begin',) in result) - self.assert_(('mpd_version', protocol.VERSION) in result) - self.assert_(('fs_charset', protocol.ENCODING) in result) - self.assert_(('info_end',) in result) + def consume_headers(self, result): + self.assertEqual(('info_begin',), result[0]) + self.assertEqual(('mpd_version', protocol.VERSION), result[1]) + self.assertEqual(('fs_charset', protocol.ENCODING), result[2]) + self.assertEqual(('info_end',), result[3]) return result[4:] - def check_song_list(self, result): + def consume_song_list(self, result): self.assertEqual(('songList begin',), result[0]) - self.assertEqual(('songList end',), result[-1]) - return result[1:-1] + for i, row in enumerate(result): + if row == ('songList end',): + return result[1:i], result[i+1:] + self.fail("Couldn't find songList end in result") - def test_empty_tag_cache(self): + def test_empty_tag_cache_has_header(self): result = translator.tracks_to_tag_cache_format([]) - result = self.check_headers(result) - result = self.check_song_list(result) + result = self.consume_headers(result) + + def test_empty_tag_cache_has_song_list(self): + result = translator.tracks_to_tag_cache_format([]) + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) - def test_simple_tag_cache_has_header(self): + def test_tag_cache_has_header(self): track = Track(uri='file:///dir/subdir/song.mp3') result = translator.tracks_to_tag_cache_format([track]) - result = self.check_headers(result) - result = self.check_song_list(result) + result = self.consume_headers(result) - def test_simple_tag_cache_has_formated_track(self): + def test_tag_cache_has_song_list(self): + track = Track(uri='file:///dir/subdir/song.mp3') + result = translator.tracks_to_tag_cache_format([track]) + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assert_(song_list) + self.assertEqual(len(result), 0) + + def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = translator.track_to_mpd_format(track, key=True) - result = translator.tracks_to_tag_cache_format([track]) - result = self.check_headers(result) - result = self.check_song_list(result) - self.assertEqual(result, formated) + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(song_list, formated) + self.assertEqual(len(result), 0) From d785b9b14e52d01b5b05564d37423cb49c22a0ef Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:16:33 +0200 Subject: [PATCH 43/82] Added uri_to_path with tests --- mopidy/utils/path.py | 8 ++++++++ tests/utils/path_test.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 9220c53d..73d89d49 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -1,6 +1,7 @@ import logging import os import sys +import re import urllib logger = logging.getLogger('mopidy.utils.path') @@ -27,6 +28,13 @@ def path_to_uri(*paths): return 'file:' + urllib.pathname2url(path) return 'file://' + urllib.pathname2url(path) +def uri_to_path(uri): + if sys.platform == 'win32': + path = urllib.url2pathname(re.sub('^file:', '', uri)) + else: + path = urllib.url2pathname(re.sub('^file://', '', uri)) + return path.encode('latin1').decode('utf-8') # Undo double encoding + def find_files(path): if os.path.isfile(path): yield os.path.abspath(path) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index e0359a16..c60e7833 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,7 +6,8 @@ import sys import tempfile import unittest -from mopidy.utils.path import get_or_create_folder, path_to_uri, find_files +from mopidy.utils.path import (get_or_create_folder, + path_to_uri, uri_to_path, find_files) from tests import SkipTest, data_folder @@ -71,6 +72,32 @@ class PathToFileURITest(unittest.TestCase): self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5') +class UriToPathTest(unittest.TestCase): + def test_simple_uri(self): + if sys.platform == 'win32': + result = uri_to_path('file:///C://WINDOWS/clock.avi') + self.assertEqual(result, u'C:/WINDOWS/clock.avi') + else: + result = uri_to_path('file:///etc/fstab') + self.assertEqual(result, u'/etc/fstab') + + def test_space_in_uri(self): + if sys.platform == 'win32': + result = uri_to_path('file:///C://test%20this') + self.assertEqual(result, u'C:/test this') + else: + result = uri_to_path(u'file:///tmp/test%20this') + self.assertEqual(result, u'/tmp/test this') + + def test_unicode_in_uri(self): + if sys.platform == 'win32': + result = uri_to_path( 'file:///C://%C3%A6%C3%B8%C3%A5') + self.assertEqual(result, u'C:/æøå') + else: + result = uri_to_path(u'file:///tmp/%C3%A6%C3%B8%C3%A5') + self.assertEqual(result, u'/tmp/æøå') + + class FindFilesTest(unittest.TestCase): def find(self, path): return list(find_files(data_folder(path))) From 8d1339ef7f0bd2a38b9aa0e7888c8b81da75c7a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:18:35 +0200 Subject: [PATCH 44/82] Add option to get mtime set in translator --- mopidy/frontends/mpd/translator.py | 7 +++++-- tests/frontends/mpd/serializer_test.py | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 3f7a6adf..41d2af5c 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -3,9 +3,9 @@ import re from mopidy import settings from mopidy.frontends.mpd import protocol -from mopidy.utils.path import path_to_uri +from mopidy.utils.path import path_to_uri, uri_to_path -def track_to_mpd_format(track, position=None, cpid=None, key=None): +def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -35,6 +35,9 @@ def track_to_mpd_format(track, position=None, cpid=None, key=None): result.append(('Id', cpid)) if key and track.uri: result.insert(0, ('key', os.path.basename(track.uri))) + if mtime and track.uri: + mtime = os.stat(uri_to_path(track.uri)).st_mtime + result.append(('mtime', int(mtime))) return result def track_artists_to_mpd_format(track): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index de0a6814..689347fe 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -5,6 +5,8 @@ from mopidy import settings from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track +from tests import data_folder + class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' @@ -41,6 +43,12 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(track, key=True) self.assert_(('key', 'file.mp3') in result) + def test_track_to_mpd_format_with_mtime(self): + uri = translator.path_to_uri(data_folder('blank.mp3')) + result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) + print result + self.assert_(('mtime', 1288125516) in result) + def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] From 059ad2d4c91fd99a4c1fcb9f9d8a391420d45519 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:20:19 +0200 Subject: [PATCH 45/82] Update track_to_mpd_format docstring with new parameters --- mopidy/frontends/mpd/translator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 41d2af5c..98b3f907 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -15,6 +15,10 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) :type position: integer :param cpid: track's CPID (current playlist ID) :type cpid: integer + :param key: if we should set key + :type key: boolean + :param mtime: if we should set mtime + :type mtime: boolean :rtype: list of two-tuples """ result = [ From a48e88104012c4b4ed8d87af5c0e9e92de8fc713 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:49:48 +0200 Subject: [PATCH 46/82] Add split path util --- mopidy/utils/path.py | 10 ++++++++++ tests/utils/path_test.py | 22 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 73d89d49..ef331045 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -35,6 +35,16 @@ def uri_to_path(uri): path = urllib.url2pathname(re.sub('^file://', '', uri)) return path.encode('latin1').decode('utf-8') # Undo double encoding +def split_path(path): + parts = [] + while True: + path, part = os.path.split(path) + if part: + parts.insert(0, part) + if not path or path == '/': + break + return parts + def find_files(path): if os.path.isfile(path): yield os.path.abspath(path) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index c60e7833..2990c6d3 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -7,7 +7,7 @@ import tempfile import unittest from mopidy.utils.path import (get_or_create_folder, - path_to_uri, uri_to_path, find_files) + path_to_uri, uri_to_path, split_path, find_files) from tests import SkipTest, data_folder @@ -98,6 +98,26 @@ class UriToPathTest(unittest.TestCase): self.assertEqual(result, u'/tmp/æøå') +class SplitPathTest(unittest.TestCase): + def test_empty_path(self): + self.assertEqual([], split_path('')) + + def test_single_folder(self): + self.assertEqual(['foo'], split_path('foo')) + + def test_folders(self): + self.assertEqual(['foo', 'bar', 'baz'], split_path('foo/bar/baz')) + + def test_folders(self): + self.assertEqual(['foo', 'bar', 'baz'], split_path('foo/bar/baz')) + + def test_initial_slash_is_ignored(self): + self.assertEqual(['foo', 'bar', 'baz'], split_path('/foo/bar/baz')) + + def test_only_slash(self): + self.assertEqual([], split_path('/')) + + class FindFilesTest(unittest.TestCase): def find(self, path): return list(find_files(data_folder(path))) From 1a6831ab683f75cf9f0fb6d067d5f07871c08f53 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:06:58 +0200 Subject: [PATCH 47/82] Add tracks_to_directory_tree helper --- mopidy/frontends/mpd/translator.py | 14 ++++++- tests/frontends/mpd/serializer_test.py | 56 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 98b3f907..36a6e7ea 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -3,7 +3,7 @@ import re from mopidy import settings from mopidy.frontends.mpd import protocol -from mopidy.utils.path import path_to_uri, uri_to_path +from mopidy.utils.path import path_to_uri, uri_to_path, split_path def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ @@ -123,3 +123,15 @@ def tracks_to_tag_cache_format(tracks): result.append(('songList end',)) return result + +def tracks_to_directory_tree(tracks): + directories = ({}, []) + for track in tracks: + folder = os.path.dirname(uri_to_path(track.uri)) + current = directories + for part in split_path(folder): + if part not in current[0]: + current[0][part] = ({}, []) + current = current[0][part] + current[1].append(track) + return directories diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 689347fe..5fc610b0 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -173,3 +173,59 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(song_list, formated) self.assertEqual(len(result), 0) + + +class TracksToDirectoryTreeTest(unittest.TestCase): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/' + + def tearDown(self): + settings.runtime.clear() + + def test_no_tracks_gives_emtpy_tree(self): + tree = translator.tracks_to_directory_tree([]) + self.assertEqual(tree, ({}, [])) + + def test_top_level_files(self): + tracks = [ + Track(uri='file:///file1.mp3'), + Track(uri='file:///file2.mp3'), + Track(uri='file:///file3.mp3'), + ] + tree = translator.tracks_to_directory_tree(tracks) + self.assertEqual(tree, ({}, tracks)) + + def test_single_file_in_subdir(self): + tracks = [Track(uri='file:///dir/file1.mp3')] + tree = translator.tracks_to_directory_tree(tracks) + expected = ({'dir': ({}, tracks)}, []) + self.assertEqual(tree, expected) + + def test_single_file_in_sub_subdir(self): + tracks = [Track(uri='file:///dir1/dir2/file1.mp3')] + tree = translator.tracks_to_directory_tree(tracks) + expected = ({'dir1': ({'dir2': ({}, tracks)}, [])}, []) + self.assertEqual(tree, expected) + + def test_complex_file_structure(self): + tracks = [ + Track(uri='file:///file1.mp3'), + Track(uri='file:///dir1/file2.mp3'), + Track(uri='file:///dir1/file3.mp3'), + Track(uri='file:///dir2/file4.mp3'), + Track(uri='file:///dir2/sub/file5.mp3'), + ] + tree = translator.tracks_to_directory_tree(tracks) + expected = ( + { + 'dir1': ({}, [tracks[1], tracks[2]]), + 'dir2': ( + { + 'sub': ({}, [tracks[4]]) + }, + [tracks[3]] + ), + }, + [tracks[0]] + ) + self.assertEqual(tree, expected) From 94db967672f2aa22aa7557e609e7ccdcd4990897 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:26:05 +0200 Subject: [PATCH 48/82] Tag cache seems to support directories now --- mopidy/frontends/mpd/translator.py | 15 +++++++++++---- tests/frontends/mpd/serializer_test.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 36a6e7ea..6c93e04e 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -116,18 +116,25 @@ def tracks_to_tag_cache_format(tracks): ('fs_charset', protocol.ENCODING), ('info_end',) ] + _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) + return result + +def _add_to_tag_cache(result, folders, files): + for name, entry in folders.items(): + result.append(('begin', name)) + _add_to_tag_cache(result, *entry) + result.append(('end', name)) result.append(('songList begin',)) - for track in tracks: + for track in files: result.extend(track_to_mpd_format(track, key=True)) result.append(('songList end',)) - return result - def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - folder = os.path.dirname(uri_to_path(track.uri)) + uri = uri_to_mpd_relative_path(track.uri) + folder = os.path.dirname(uri_to_path(uri)) current = directories for part in split_path(folder): if part not in current[0]: diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 5fc610b0..a4abc5a9 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -137,6 +137,14 @@ class TracksToTagCacheFormatTest(unittest.TestCase): return result[1:i], result[i+1:] self.fail("Couldn't find songList end in result") + def consume_directory(self, result): + self.assertEqual('begin', result[0][0]) + directory = result[0][1] + for i, row in enumerate(result): + if row == ('end', directory): + return result[1:i], result[i+1:] + self.fail("Couldn't find end %s in result" % directory) + def test_empty_tag_cache_has_header(self): result = translator.tracks_to_tag_cache_format([]) result = self.consume_headers(result) @@ -174,6 +182,16 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(song_list, formated) self.assertEqual(len(result), 0) + def test_tag_cache_suports_directories(self): + track = Track(uri='file:///dir/subdir/folder/song.mp3') + formated = translator.track_to_mpd_format(track, key=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + directory, result = self.consume_directory(result) + song_list, result = self.consume_song_list(directory) + + self.assertEqual(song_list, formated) class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): From 6b99416830ec881684cd953ad8b6cf95a7d6ccb3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:43:49 +0200 Subject: [PATCH 49/82] Add test for sub sub dirs in tag cache generator --- tests/frontends/mpd/serializer_test.py | 32 +++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index a4abc5a9..6025109e 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -5,7 +5,7 @@ from mopidy import settings from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track -from tests import data_folder +from tests import data_folder, SkipTest class TrackMpdFormatTest(unittest.TestCase): def setUp(self): @@ -188,11 +188,37 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) - directory, result = self.consume_directory(result) - song_list, result = self.consume_song_list(directory) + folder, result = self.consume_directory(result) + song_list, result = self.consume_song_list(result) + self.assertEqual(len(song_list), 0) + self.assertEqual(len(result), 0) + song_list, result = self.consume_song_list(folder) + self.assertEqual(len(result), 0) self.assertEqual(song_list, formated) + def test_tag_cache_suports_sub_directories(self): + track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') + formated = translator.track_to_mpd_format(track, key=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + + folder, result = self.consume_directory(result) + song_list, result = self.consume_song_list(result) + self.assertEqual(len(song_list), 0) + self.assertEqual(len(result), 0) + + folder, result = self.consume_directory(folder) + song_list, result = self.consume_song_list(result) + self.assertEqual(len(result), 0) + self.assertEqual(len(song_list), 0) + + song_list, result = self.consume_song_list(folder) + self.assertEqual(len(result), 0) + self.assertEqual(song_list, formated) + + class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/' From 93eda1c81ed8278b58cc27ade626c21068e83178 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:46:40 +0200 Subject: [PATCH 50/82] Extra test for multiple top level files --- tests/frontends/mpd/serializer_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6025109e..40a790c6 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -218,6 +218,24 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(len(result), 0) self.assertEqual(song_list, formated) + def test_tag_cache_supports_multiple_tracks(self): + tracks = [ + Track(uri='file:///dir/subdir/song1.mp3'), + Track(uri='file:///dir/subdir/song2.mp3'), + ] + + formated = [] + formated.extend(translator.track_to_mpd_format(tracks[0], key=True)) + formated.extend(translator.track_to_mpd_format(tracks[1], key=True)) + + result = translator.tracks_to_tag_cache_format(tracks) + + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(song_list, formated) + self.assertEqual(len(result), 0) + class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): From 7559426c5069f368281ce489a6bf06155d1f93bf Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:56:01 +0200 Subject: [PATCH 51/82] Add test for files in multiple directories --- tests/frontends/mpd/serializer_test.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 40a790c6..6a0dec3a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -236,6 +236,29 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(song_list, formated) self.assertEqual(len(result), 0) + def test_tag_cache_supports_multiple_tracks_in_dirs(self): + tracks = [ + Track(uri='file:///dir/subdir/song1.mp3'), + Track(uri='file:///dir/subdir/folder/song2.mp3'), + ] + + formated = [] + formated.append(translator.track_to_mpd_format(tracks[0], key=True)) + formated.append(translator.track_to_mpd_format(tracks[1], key=True)) + + result = translator.tracks_to_tag_cache_format(tracks) + + result = self.consume_headers(result) + folder, result = self.consume_directory(result) + song_list, song_result = self.consume_song_list(folder) + + self.assertEqual(song_list, formated[1]) + self.assertEqual(len(song_result), 0) + + song_list, result = self.consume_song_list(result) + self.assertEqual(len(result), 0) + self.assertEqual(song_list, formated[0]) + class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): From ca95a510c90669d2e7ccca865f75acb072d4a7a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 00:37:42 +0200 Subject: [PATCH 52/82] Ensure that mtime is included --- mopidy/frontends/mpd/translator.py | 6 ++-- tests/frontends/mpd/serializer_test.py | 41 ++++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 6c93e04e..5d446be8 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -5,6 +5,8 @@ from mopidy import settings from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri, uri_to_path, split_path +stat = os.stat + def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -40,7 +42,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) if key and track.uri: result.insert(0, ('key', os.path.basename(track.uri))) if mtime and track.uri: - mtime = os.stat(uri_to_path(track.uri)).st_mtime + mtime = stat(uri_to_path(track.uri)).st_mtime result.append(('mtime', int(mtime))) return result @@ -127,7 +129,7 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList begin',)) for track in files: - result.extend(track_to_mpd_format(track, key=True)) + result.extend(track_to_mpd_format(track, key=True, mtime=True)) result.append(('songList end',)) def tracks_to_directory_tree(tracks): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6a0dec3a..81ea4cae 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,4 +1,5 @@ import datetime as dt +import os import unittest from mopidy import settings @@ -7,12 +8,21 @@ from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest +def fake_stat(path): + class StatResult(object): + def __getattr__(self, key): + assert key == 'st_mtime', key + return 1234567 + return StatResult() + class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + translator.stat = fake_stat def tearDown(self): settings.runtime.clear() + translator.stat = os.stat def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) @@ -46,8 +56,7 @@ class TrackMpdFormatTest(unittest.TestCase): def test_track_to_mpd_format_with_mtime(self): uri = translator.path_to_uri(data_folder('blank.mp3')) result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) - print result - self.assert_(('mtime', 1288125516) in result) + self.assert_(('mtime', 1234567) in result) def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') @@ -117,11 +126,14 @@ class UriToMpdRelativePathTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): + def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + translator.stat = fake_stat def tearDown(self): settings.runtime.clear() + translator.stat = os.stat def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) @@ -173,7 +185,18 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track, key=True) + formated = translator.track_to_mpd_format(track, key=True, mtime=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(song_list, formated) + self.assertEqual(len(result), 0) + + def test_tag_cache_has_formated_track_with_key_and_mtime(self): + track = Track(uri='file:///dir/subdir/song.mp3') + formated = translator.track_to_mpd_format(track, key=True, mtime=True) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -184,7 +207,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') - formated = translator.track_to_mpd_format(track, key=True) + formated = translator.track_to_mpd_format(track, key=True, mtime=True) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -199,7 +222,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = translator.track_to_mpd_format(track, key=True) + formated = translator.track_to_mpd_format(track, key=True, mtime=True) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -225,8 +248,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.extend(translator.track_to_mpd_format(tracks[0], key=True)) - formated.extend(translator.track_to_mpd_format(tracks[1], key=True)) + formated.extend(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) + formated.extend(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) result = translator.tracks_to_tag_cache_format(tracks) @@ -243,8 +266,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.append(translator.track_to_mpd_format(tracks[0], key=True)) - formated.append(translator.track_to_mpd_format(tracks[1], key=True)) + formated.append(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) + formated.append(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) result = translator.tracks_to_tag_cache_format(tracks) From 96d4633306deb7c7bd67fb6844cba6d22337eaeb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 00:40:01 +0200 Subject: [PATCH 53/82] Ensure that key does not have uri encoded strings --- mopidy/frontends/mpd/translator.py | 2 +- tests/frontends/mpd/serializer_test.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 5d446be8..1e2f061a 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -40,7 +40,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result.append(('Pos', position)) result.append(('Id', cpid)) if key and track.uri: - result.insert(0, ('key', os.path.basename(track.uri))) + result.insert(0, ('key', os.path.basename(uri_to_path(track.uri)))) if mtime and track.uri: mtime = stat(uri_to_path(track.uri)).st_mtime result.append(('mtime', int(mtime))) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 81ea4cae..6a993c2a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -53,6 +53,11 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(track, key=True) self.assert_(('key', 'file.mp3') in result) + def test_track_to_mpd_format_with_key_not_uri_encoded(self): + track = Track(uri='file:///dir/subdir/file%20test.mp3') + result = translator.track_to_mpd_format(track, key=True) + self.assert_(('key', 'file test.mp3') in result) + def test_track_to_mpd_format_with_mtime(self): uri = translator.path_to_uri(data_folder('blank.mp3')) result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) From d2d8e4c0906ad95f4ce62279032bdcea93c6c97f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 00:45:08 +0200 Subject: [PATCH 54/82] Use expanduser for find_files --- mopidy/utils/path.py | 1 + tests/utils/path_test.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index ef331045..84869196 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -46,6 +46,7 @@ def split_path(path): return parts def find_files(path): + path = os.path.expanduser(path) if os.path.isfile(path): yield os.path.abspath(path) else: diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 2990c6d3..269c7a24 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -132,3 +132,6 @@ class FindFilesTest(unittest.TestCase): files = self.find('blank.mp3') self.assertEqual(len(files), 1) self.assert_(files[0], data_folder('blank.mp3')) + + def test_expanduser(self): + raise SkipTest From ea74f539ba4ac6a71c20e7b7591e7f8b9a9e364a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 01:06:26 +0200 Subject: [PATCH 55/82] Fix mistakes in tag cache generation --- mopidy/frontends/mpd/translator.py | 17 +++++++++------ tests/frontends/mpd/serializer_test.py | 30 +++++++++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 1e2f061a..46c3ccbe 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -122,7 +122,10 @@ def tracks_to_tag_cache_format(tracks): return result def _add_to_tag_cache(result, folders, files): - for name, entry in folders.items(): + for path, entry in folders.items(): + name = os.path.split(path)[1] + result.append(('directory', path)) + result.append(('mtime', stat(name).st_mtime)) result.append(('begin', name)) _add_to_tag_cache(result, *entry) result.append(('end', name)) @@ -136,11 +139,13 @@ def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: uri = uri_to_mpd_relative_path(track.uri) - folder = os.path.dirname(uri_to_path(uri)) + path = '' current = directories - for part in split_path(folder): - if part not in current[0]: - current[0][part] = ({}, []) - current = current[0][part] + for part in split_path(os.path.dirname(uri_to_path(uri))): + path = os.path.join(path, part) + print path + if path not in current[0]: + current[0][path] = ({}, []) + current = current[0][path] current[1].append(track) return directories diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6a993c2a..31b03a7a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -8,7 +8,7 @@ from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest -def fake_stat(path): +def fake_mtime(path): class StatResult(object): def __getattr__(self, key): assert key == 'st_mtime', key @@ -18,7 +18,7 @@ def fake_stat(path): class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_stat + translator.stat = fake_mtime def tearDown(self): settings.runtime.clear() @@ -134,7 +134,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_stat + translator.stat = fake_mtime def tearDown(self): settings.runtime.clear() @@ -155,11 +155,13 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.fail("Couldn't find songList end in result") def consume_directory(self, result): - self.assertEqual('begin', result[0][0]) - directory = result[0][1] + self.assertEqual('directory', result[0][0]) + self.assertEqual(('mtime', fake_mtime('').st_mtime), result[1]) + self.assertEqual(('begin', os.path.split(result[0][1])[1]), result[2]) + directory = result[2][1] for i, row in enumerate(result): if row == ('end', directory): - return result[1:i], result[i+1:] + return result[3:i], result[i+1:] self.fail("Couldn't find end %s in result" % directory) def test_empty_tag_cache_has_header(self): @@ -225,6 +227,18 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(len(result), 0) self.assertEqual(song_list, formated) + def test_tag_cache_diretory_header_is_right(self): + track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') + formated = translator.track_to_mpd_format(track, key=True, mtime=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + folder, result = self.consume_directory(result) + + self.assertEqual(('directory', 'folder/sub'), folder[0]) + self.assertEqual(('mtime', fake_mtime('').st_mtime), folder[1]) + self.assertEqual(('begin', 'sub'), folder[2]) + def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') formated = translator.track_to_mpd_format(track, key=True, mtime=True) @@ -317,7 +331,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): def test_single_file_in_sub_subdir(self): tracks = [Track(uri='file:///dir1/dir2/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks) - expected = ({'dir1': ({'dir2': ({}, tracks)}, [])}, []) + expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) def test_complex_file_structure(self): @@ -334,7 +348,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): 'dir1': ({}, [tracks[1], tracks[2]]), 'dir2': ( { - 'sub': ({}, [tracks[4]]) + 'dir2/sub': ({}, [tracks[4]]) }, [tracks[3]] ), From 52ab538fc4d0a12fccdb3d7c223fd53b221f0f4f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:38:20 +0200 Subject: [PATCH 56/82] Minor test cleanup --- tests/scanner_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index b33ed99a..f0f22ecc 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -51,7 +51,7 @@ class TranslatorTest(unittest.TestCase): def check(self): expected = self.build_track() actual = translator(self.data) - self.assertEqual(expected, translator(self.data)) + self.assertEqual(expected, actual) def test_basic_data(self): self.check() From 8a4dc1033b9d648b89fee951be483738c0f7dbfa Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:38:33 +0200 Subject: [PATCH 57/82] Add album-artist support to translator --- mopidy/scanner.py | 7 +++++++ tests/scanner_test.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index ccdab24b..c32115f7 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -14,6 +14,7 @@ from mopidy.utils.path import path_to_uri, find_files from mopidy.models import Track, Artist, Album def translator(data): + albumartist_kwargs = {} album_kwargs = {} artist_kwargs = {} track_kwargs = {} @@ -38,6 +39,12 @@ def translator(data): if 'track-number' in data: track_kwargs['track_no'] = data['track-number'] + if 'album-artist' in data: + albumartist_kwargs['name'] = data['album-artist'] + + if albumartist_kwargs: + album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data['duration'] track_kwargs['album'] = Album(**album_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index f0f22ecc..141f2ceb 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -19,6 +19,7 @@ class TranslatorTest(unittest.TestCase): 'album': u'albumname', 'track-number': 1, 'artist': u'name', + 'album-artist': 'albumartistname', 'title': u'trackname', 'track-count': 2, 'date': FakeGstDate(2006, 1, 1,), @@ -35,6 +36,10 @@ class TranslatorTest(unittest.TestCase): 'name': 'name', } + self.albumartist = { + 'name': 'albumartistname', + } + self.track = { 'uri': 'uri', 'name': 'trackname', @@ -44,6 +49,8 @@ class TranslatorTest(unittest.TestCase): } def build_track(self): + if self.albumartist: + self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) self.track['artists'] = [Artist(**self.artist)] return Track(**self.track) @@ -81,6 +88,11 @@ class TranslatorTest(unittest.TestCase): del self.artist['name'] self.check() + def test_missing_album_artist(self): + del self.data['album-artist'] + del self.albumartist['name'] + self.check() + def test_missing_date(self): del self.data['date'] del self.track['date'] From 6049c7a0944301c824b01862c4e99a80178acf65 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:41:45 +0200 Subject: [PATCH 58/82] Turn track_artists_to_mpd_format into artists_to_mpd_format --- mopidy/frontends/mpd/translator.py | 9 ++++----- tests/frontends/mpd/serializer_test.py | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 46c3ccbe..5f064cc9 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -26,7 +26,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result = [ ('file', uri_to_mpd_relative_path(track.uri) or ''), ('Time', track.length and (track.length // 1000) or 0), - ('Artist', track_artists_to_mpd_format(track)), + ('Artist', artists_to_mpd_format(track.artists)), ('Title', track.name or ''), ('Album', track.album and track.album.name or ''), ('Date', track.date or ''), @@ -46,15 +46,14 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result.append(('mtime', int(mtime))) return result -def track_artists_to_mpd_format(track): +def artists_to_mpd_format(artists): """ Format track artists for output to MPD client. - :param track: the track - :type track: :class:`mopidy.models.Track` + :param artists: the artists + :type track: array of :class:`mopidy.models.Artist` :rtype: string """ - artists = track.artists artists.sort(key=lambda a: a.name) return u', '.join([a.name for a in artists]) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 31b03a7a..deca960f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -91,9 +91,9 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 122) in result) self.assertEqual(len(result), 9) - def test_track_artists_to_mpd_format(self): - track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) - translated = translator.track_artists_to_mpd_format(track) + def test_artists_to_mpd_format(self): + artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] + translated = translator.artists_to_mpd_format(artists) self.assertEqual(translated, u'ABBA, Beatles') From e6cdb8888127057953e842048e28c61eb9d448ac Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:42:34 +0200 Subject: [PATCH 59/82] Remove print statement --- mopidy/frontends/mpd/translator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 5f064cc9..da6c6462 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -142,7 +142,6 @@ def tracks_to_directory_tree(tracks): current = directories for part in split_path(os.path.dirname(uri_to_path(uri))): path = os.path.join(path, part) - print path if path not in current[0]: current[0][path] = ({}, []) current = current[0][path] From 9a99bc46bf220e32adbc7c132bb6802d8ccc91f8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:46:47 +0200 Subject: [PATCH 60/82] Add album artist to track to mpd format --- mopidy/frontends/mpd/translator.py | 3 +++ tests/frontends/mpd/serializer_test.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index da6c6462..7b337b62 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -36,6 +36,9 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) track.track_no, track.album.num_tracks))) else: result.append(('Track', track.track_no)) + if track.album is not None and track.album.artists: + artists = artists_to_mpd_format(track.album.artists) + result.append(('AlbumArtist', artists)) if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index deca960f..5b7e6095 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -74,7 +74,8 @@ class TrackMpdFormatTest(unittest.TestCase): uri=u'a uri', artists=[Artist(name=u'an artist')], name=u'a name', - album=Album(name=u'an album', num_tracks=13), + album=Album(name=u'an album', num_tracks=13, + artists=[Artist(name=u'an other artist')]), track_no=7, date=dt.date(1977, 1, 1), length=137000, @@ -85,11 +86,12 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Artist', 'an artist') in result) self.assert_(('Title', 'a name') in result) self.assert_(('Album', 'an album') in result) + self.assert_(('AlbumArtist', 'an other artist') in result) self.assert_(('Track', '7/13') in result) self.assert_(('Date', dt.date(1977, 1, 1)) in result) self.assert_(('Pos', 9) in result) self.assert_(('Id', 122) in result) - self.assertEqual(len(result), 9) + self.assertEqual(len(result), 10) def test_artists_to_mpd_format(self): artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] From 92333208de04857a98e8735b3aecb5034546a335 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:53:23 +0200 Subject: [PATCH 61/82] Add mopidy.utils.path.mtime helper that is easily faked in tests --- mopidy/utils/path.py | 17 +++++++++++++++++ tests/utils/path_test.py | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 84869196..6bd84ac0 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -54,3 +54,20 @@ def find_files(path): for filename in filenames: dirpath = os.path.abspath(dirpath) yield os.path.join(dirpath, filename) + +class Mtime(object): + def __init__(self): + self.fake = None + + def __call__(self, path): + if self.fake is not None: + return self.fake + return int(os.stat(path).st_mtime) + + def set_fake_time(self, time): + self.fake = time + + def undo_fake(self): + self.fake = None + +mtime = Mtime() diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 269c7a24..065cde5d 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,7 +6,7 @@ import sys import tempfile import unittest -from mopidy.utils.path import (get_or_create_folder, +from mopidy.utils.path import (get_or_create_folder, mtime, path_to_uri, uri_to_path, split_path, find_files) from tests import SkipTest, data_folder @@ -135,3 +135,16 @@ class FindFilesTest(unittest.TestCase): def test_expanduser(self): raise SkipTest + + +class MtimeTest(unittest.TestCase): + def tearDown(self): + mtime.undo_fake() + + def test_mtime_of_current_dir(self): + mtime_dir = int(os.stat('.').st_mtime) + self.assertEqual(mtime_dir, mtime('.')) + + def test_fake_time_is_returned(self): + mtime.set_fake_time(123456) + self.assertEqual(mtime('.'), 123456) From d67bfb9aeeee44d994d24616053fd58a9e6a82fe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:58:41 +0200 Subject: [PATCH 62/82] Use new mtime helper --- mopidy/frontends/mpd/translator.py | 8 +++----- tests/frontends/mpd/serializer_test.py | 20 +++++++------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 7b337b62..514e167f 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -2,11 +2,10 @@ import os import re from mopidy import settings +from mopidy.utils.path import mtime as get_mtime from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri, uri_to_path, split_path -stat = os.stat - def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -45,8 +44,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) if key and track.uri: result.insert(0, ('key', os.path.basename(uri_to_path(track.uri)))) if mtime and track.uri: - mtime = stat(uri_to_path(track.uri)).st_mtime - result.append(('mtime', int(mtime))) + result.append(('mtime', get_mtime(uri_to_path(track.uri)))) return result def artists_to_mpd_format(artists): @@ -127,7 +125,7 @@ def _add_to_tag_cache(result, folders, files): for path, entry in folders.items(): name = os.path.split(path)[1] result.append(('directory', path)) - result.append(('mtime', stat(name).st_mtime)) + result.append(('mtime', get_mtime(name))) result.append(('begin', name)) _add_to_tag_cache(result, *entry) result.append(('end', name)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 5b7e6095..74a4184f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -3,26 +3,20 @@ import os import unittest from mopidy import settings +from mopidy.utils.path import mtime from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest -def fake_mtime(path): - class StatResult(object): - def __getattr__(self, key): - assert key == 'st_mtime', key - return 1234567 - return StatResult() - class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_mtime + mtime.set_fake_time(1234567) def tearDown(self): settings.runtime.clear() - translator.stat = os.stat + mtime.undo_fake() def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) @@ -136,11 +130,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_mtime + mtime.set_fake_time(1234567) def tearDown(self): settings.runtime.clear() - translator.stat = os.stat + mtime.undo_fake() def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) @@ -158,7 +152,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def consume_directory(self, result): self.assertEqual('directory', result[0][0]) - self.assertEqual(('mtime', fake_mtime('').st_mtime), result[1]) + self.assertEqual(('mtime', mtime('.')), result[1]) self.assertEqual(('begin', os.path.split(result[0][1])[1]), result[2]) directory = result[2][1] for i, row in enumerate(result): @@ -238,7 +232,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): folder, result = self.consume_directory(result) self.assertEqual(('directory', 'folder/sub'), folder[0]) - self.assertEqual(('mtime', fake_mtime('').st_mtime), folder[1]) + self.assertEqual(('mtime', mtime('.')), folder[1]) self.assertEqual(('begin', 'sub'), folder[2]) def test_tag_cache_suports_sub_directories(self): From b9976c4cdabefe3bf18954c986bad74579ee7ed0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:26:08 +0200 Subject: [PATCH 63/82] Remove uri_to_mpd_relative_path --- mopidy/frontends/mpd/translator.py | 17 ++-------- tests/frontends/mpd/serializer_test.py | 47 ++++++-------------------- 2 files changed, 13 insertions(+), 51 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 514e167f..69be17ed 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -23,7 +23,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) :rtype: list of two-tuples """ result = [ - ('file', uri_to_mpd_relative_path(track.uri) or ''), + ('file', track.uri or ''), ('Time', track.length and (track.length // 1000) or 0), ('Artist', artists_to_mpd_format(track.artists)), ('Title', track.name or ''), @@ -91,19 +91,6 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) -def uri_to_mpd_relative_path(uri): - """ - Strip uri and LOCAL_MUSIC_FOLDER part of uri. - - :param uri: the uri - :type uri: string - :rtype: string - """ - if uri is None: - return '' - path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) - return re.sub('^' + re.escape(path), '', uri) - def tracks_to_tag_cache_format(tracks): """ Format list of tracks for output to MPD tag cache @@ -138,7 +125,7 @@ def _add_to_tag_cache(result, folders, files): def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - uri = uri_to_mpd_relative_path(track.uri) + uri = track.uri path = '' current = directories for part in split_path(os.path.dirname(uri_to_path(uri))): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 74a4184f..4123e87f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -57,12 +57,6 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) self.assert_(('mtime', 1234567) in result) - def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): - track = Track(uri='file:///dir/subdir/song.mp3') - path = dict(translator.track_to_mpd_format(track))['file'] - correct_path = translator.uri_to_mpd_relative_path(track.uri) - self.assertEqual(path, correct_path) - def test_track_to_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', @@ -108,26 +102,7 @@ class PlaylistMpdFormatTest(unittest.TestCase): self.assertEqual(dict(result[0])['Track'], 2) -class UriToMpdRelativePathTest(unittest.TestCase): - def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - - def tearDown(self): - settings.runtime.clear() - - def test_none_file_returns_empty_string(self): - uri = 'file:///dir/subdir/music/album/song.mp3' - result = translator.uri_to_mpd_relative_path(None) - self.assertEqual('', result) - - def test_file_gets_stripped(self): - uri = 'file:///dir/subdir/music/album/song.mp3' - result = translator.uri_to_mpd_relative_path(uri) - self.assertEqual('/music/album/song.mp3', result) - - class TracksToTagCacheFormatTest(unittest.TestCase): - def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' mtime.set_fake_time(1234567) @@ -300,7 +275,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/' + settings.LOCAL_MUSIC_FOLDER = '/root/' def tearDown(self): settings.runtime.clear() @@ -311,32 +286,32 @@ class TracksToDirectoryTreeTest(unittest.TestCase): def test_top_level_files(self): tracks = [ - Track(uri='file:///file1.mp3'), - Track(uri='file:///file2.mp3'), - Track(uri='file:///file3.mp3'), + Track(uri='file:///root/file1.mp3'), + Track(uri='file:///root/file2.mp3'), + Track(uri='file:///root/file3.mp3'), ] tree = translator.tracks_to_directory_tree(tracks) self.assertEqual(tree, ({}, tracks)) def test_single_file_in_subdir(self): - tracks = [Track(uri='file:///dir/file1.mp3')] + tracks = [Track(uri='file:///root/dir/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks) expected = ({'dir': ({}, tracks)}, []) self.assertEqual(tree, expected) def test_single_file_in_sub_subdir(self): - tracks = [Track(uri='file:///dir1/dir2/file1.mp3')] + tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks) expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) def test_complex_file_structure(self): tracks = [ - Track(uri='file:///file1.mp3'), - Track(uri='file:///dir1/file2.mp3'), - Track(uri='file:///dir1/file3.mp3'), - Track(uri='file:///dir2/file4.mp3'), - Track(uri='file:///dir2/sub/file5.mp3'), + Track(uri='file:///root/file1.mp3'), + Track(uri='file:///root/dir1/file2.mp3'), + Track(uri='file:///root/dir1/file3.mp3'), + Track(uri='file:///root/dir2/file4.mp3'), + Track(uri='file:///root/dir2/sub/file5.mp3'), ] tree = translator.tracks_to_directory_tree(tracks) expected = ( From f0619744ed3198ac796a12c0acc8da874bcb5d48 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:26:53 +0200 Subject: [PATCH 64/82] Fix tracks to direcotory dir handling --- mopidy/frontends/mpd/translator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 69be17ed..e234046b 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -125,10 +125,15 @@ def _add_to_tag_cache(result, folders, files): def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - uri = track.uri path = '' current = directories - for part in split_path(os.path.dirname(uri_to_path(uri))): + + local_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + track_path = uri_to_path(track.uri) + track_path = re.sub('^' + re.escape(local_folder), '', track_path) + track_dir = os.path.dirname(track_path) + + for part in split_path(track_dir): path = os.path.join(path, part) if path not in current[0]: current[0][path] = ({}, []) From 9309b5bd7d69190ad1f4281c82596bf5e04ad885 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:35:31 +0200 Subject: [PATCH 65/82] Handle folders correctly _add_to_tag_cache --- mopidy/frontends/mpd/translator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e234046b..cf39ec1a 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -111,8 +111,10 @@ def tracks_to_tag_cache_format(tracks): def _add_to_tag_cache(result, folders, files): for path, entry in folders.items(): name = os.path.split(path)[1] + music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + mtime = get_mtime(os.path.join(music_folder, path)) result.append(('directory', path)) - result.append(('mtime', get_mtime(name))) + result.append(('mtime', mtime)) result.append(('begin', name)) _add_to_tag_cache(result, *entry) result.append(('end', name)) From 357591e97e8a4113629e9d070267b79d5cf4130b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:53:59 +0200 Subject: [PATCH 66/82] Introduce concept of mpd ordered track info for simpler diffing of tag caches --- mopidy/frontends/mpd/translator.py | 13 ++++++++++++- tests/frontends/mpd/serializer_test.py | 22 +++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index cf39ec1a..b139967c 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -47,6 +47,14 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result.append(('mtime', get_mtime(uri_to_path(track.uri)))) return result +MPD_KEY_ORDER = ''' + key file Time Artist AlbumArtist Title Album Track Date MUSICBRAINZ_ALBUMID + MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime +'''.split() + +def order_mpd_track_info(result): + return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0])) + def artists_to_mpd_format(artists): """ Format track artists for output to MPD client. @@ -105,6 +113,7 @@ def tracks_to_tag_cache_format(tracks): ('fs_charset', protocol.ENCODING), ('info_end',) ] + tracks.sort(key=lambda t: t.uri) _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) return result @@ -121,7 +130,9 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList begin',)) for track in files: - result.extend(track_to_mpd_format(track, key=True, mtime=True)) + track_result = track_to_mpd_format(track, key=True, mtime=True) + track_result = order_mpd_track_info(track_result) + result.extend(track_result) result.append(('songList end',)) def tracks_to_directory_tree(tracks): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 4123e87f..8e8a5d21 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -111,6 +111,10 @@ class TracksToTagCacheFormatTest(unittest.TestCase): settings.runtime.clear() mtime.undo_fake() + def translate(self, track): + result = translator.track_to_mpd_format(track, key=True, mtime=True) + return translator.order_mpd_track_info(result) + def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) self.assertEqual(('mpd_version', protocol.VERSION), result[1]) @@ -163,7 +167,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -174,7 +178,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track_with_key_and_mtime(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -185,7 +189,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -200,7 +204,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -212,7 +216,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -238,8 +242,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.extend(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) - formated.extend(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) + formated.extend(self.translate(tracks[0])) + formated.extend(self.translate(tracks[1])) result = translator.tracks_to_tag_cache_format(tracks) @@ -256,8 +260,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.append(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) - formated.append(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) + formated.append(self.translate(tracks[0])) + formated.append(self.translate(tracks[1])) result = translator.tracks_to_tag_cache_format(tracks) From 52c61634f5e1b2782940eb9b41d4223b15859ac2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:57:33 +0200 Subject: [PATCH 67/82] Add doc that explains why on earth order_mpd_track_info is used --- mopidy/frontends/mpd/translator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index b139967c..96edae49 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -53,6 +53,15 @@ MPD_KEY_ORDER = ''' '''.split() def order_mpd_track_info(result): + """ + Order results from :func:`mopidy.frontends.mpd.translator.track_to_mpd_format` + so that it matches MPD's ordering. Simply a cosmetic fix for easier + diffing of tag_caches. + + :param result: the track info + :type result: list of tuples + :rtype: list of tuples + """ return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0])) def artists_to_mpd_format(artists): From 68a78e1c539f126d33ae36fe74d6317470b80bc2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:59:28 +0200 Subject: [PATCH 68/82] Add simple proof of concept tag_cache generator --- bin/mopidy-scan | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 bin/mopidy-scan diff --git a/bin/mopidy-scan b/bin/mopidy-scan new file mode 100755 index 00000000..73af29fb --- /dev/null +++ b/bin/mopidy-scan @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +if __name__ == '__main__': + import sys + + from mopidy import settings + from mopidy.scanner import Scanner, translator + from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format + + tracks = [] + + def store(data): + track = translator(data) + tracks.append(track) + print >> sys.stderr, 'Added %s' % track.uri + + def debug(uri, error): + print >> sys.stderr, 'Failed %s: %s' % (uri, error) + + print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_FOLDER + + scanner = Scanner(settings.LOCAL_MUSIC_FOLDER, store, debug) + scanner.start() + + print >> sys.stderr, 'Done' + + for a in tracks_to_tag_cache_format(tracks): + if len(a) == 1: + print a[0] + else: + print u': '.join([unicode(b) for b in a]) From b8f67c5bb58f10ffa493bdadfe780516ca2a6b29 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 22:18:53 +0200 Subject: [PATCH 69/82] Update docs with respect to mopidy-scan --- docs/settings.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index afdd39dc..8082b1bb 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -31,6 +31,13 @@ file:: BACKENDS = (u'mopidy.backends.local.LocalBackend',) +Previously this backend relied purely on ``tag_cache`` files from MPD, to +remedy this the command ``mopidy-scan`` has been added. This program will scan +your current ``LOCAL_MUSIC_FOLDER`` and build a MPD compatible ``tag_cache``. +Currently the command outputs the ``tag_cache`` to ``stdout``, this means that +you will need to run ``mopidy-scan > path/to/your/tag_cache`` to actually start +using your new cache. + You may also want to change some of the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of available settings. From b7ebedffddcf551848d3d651c266a176dbcd48cd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 22:19:04 +0200 Subject: [PATCH 70/82] Add mopidy scan to setup.py bin entry --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fabc8353..d0044005 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ setup( package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']}, cmdclass=cmdclasses, data_files=data_files, - scripts=['bin/mopidy'], + scripts=['bin/mopidy', 'bin/mopidy-scan'], url='http://www.mopidy.com/', license='Apache License, Version 2.0', description='MPD server with Spotify support', From a078a7448e31d4e65ca8755fa31e15869c71f1de Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 22:33:36 +0200 Subject: [PATCH 71/82] Match what mpd expects with regards to caps for encoding --- mopidy/frontends/mpd/protocol/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 756aa3c3..6689f627 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -13,7 +13,7 @@ implement our own MPD server which is compatible with the numerous existing import re #: The MPD protocol uses UTF-8 for encoding all data. -ENCODING = u'utf-8' +ENCODING = u'UTF-8' #: The MPD protocol uses ``\n`` as line terminator. LINE_TERMINATOR = u'\n' From dd259d079703e6d8bfba160c16b1b88debeb970e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 23:12:32 +0200 Subject: [PATCH 72/82] Ensure that find_files only returns unicode --- mopidy/utils/path.py | 6 ++++-- tests/utils/path_test.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 6bd84ac0..78377891 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -48,12 +48,14 @@ def split_path(path): def find_files(path): path = os.path.expanduser(path) if os.path.isfile(path): - yield os.path.abspath(path) + filename = os.path.abspath(path) + yield filename.decode('utf-8') else: for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: dirpath = os.path.abspath(dirpath) - yield os.path.join(dirpath, filename) + filename = os.path.join(dirpath, filename) + yield filename.decode('utf-8') class Mtime(object): def __init__(self): diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 065cde5d..758a09ab 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -133,6 +133,12 @@ class FindFilesTest(unittest.TestCase): self.assertEqual(len(files), 1) self.assert_(files[0], data_folder('blank.mp3')) + def test_names_are_unicode(self): + is_unicode = lambda f: isinstance(f, unicode) + for name in self.find(''): + self.assert_(is_unicode(name), + '%s is not unicode object' % repr(name)) + def test_expanduser(self): raise SkipTest From f9e49fc5eb09ee22305c9294ae6cc7b0a8d6807c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 23:17:04 +0200 Subject: [PATCH 73/82] Add mopidy.desktop file to get Mopidy into Gnome menus --- MANIFEST.in | 2 +- data/mopidy.desktop | 10 ++++++++++ docs/changes.rst | 3 ++- setup.py | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 data/mopidy.desktop diff --git a/MANIFEST.in b/MANIFEST.in index 38819adb..33d7dc71 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE pylintrc *.rst *.txt +include LICENSE pylintrc *.rst *.txt data/mopidy.desktop include mopidy/backends/libspotify/spotify_appkey.key recursive-include docs * prune docs/_build diff --git a/data/mopidy.desktop b/data/mopidy.desktop new file mode 100644 index 00000000..f5ca43bb --- /dev/null +++ b/data/mopidy.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Mopidy Music Server +Comment=MPD music server with Spotify support +Icon=audio-x-generic +TryExec=mopidy +Exec=mopidy +Terminal=true +Categories=AudioVideo;Audio;Player;ConsoleOnly diff --git a/docs/changes.rst b/docs/changes.rst index cb34993e..aef0055c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,7 +12,8 @@ No description yet. **Changes** -- None so far. +- Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome + application menus. 0.2.0 (2010-10-24) diff --git a/setup.py b/setup.py index fabc8353..246b41ca 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,8 @@ for dirpath, dirnames, filenames in os.walk(project_dir): data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) +data_files.append(('/usr/local/share/applications', ['data/mopidy.desktop'])) + setup( name='Mopidy', version=get_version(), From 7767dd1ae4567780dd60b15c18b3b3f443987e03 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 23:35:04 +0200 Subject: [PATCH 74/82] Better unicode handling for scan code --- mopidy/frontends/mpd/translator.py | 2 +- mopidy/scanner.py | 2 +- mopidy/utils/path.py | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 96edae49..2b1adf50 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -147,7 +147,7 @@ def _add_to_tag_cache(result, folders, files): def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - path = '' + path = u'' current = directories local_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index c32115f7..436598bd 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -83,7 +83,7 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) - data['uri'] = self.uribin.get_property('uri') + data['uri'] = unicode(self.uribin.get_property('uri')) data['duration'] = self.get_duration() self.data_callback(data) self.next_uri() diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 78377891..b3669e38 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -49,13 +49,17 @@ def find_files(path): path = os.path.expanduser(path) if os.path.isfile(path): filename = os.path.abspath(path) - yield filename.decode('utf-8') + if not isinstance(filename, unicode): + filename = filename.decode('utf-8') + yield filename else: for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: dirpath = os.path.abspath(dirpath) filename = os.path.join(dirpath, filename) - yield filename.decode('utf-8') + if not isinstance(filename, unicode): + filename = filename.decode('utf-8') + yield filename class Mtime(object): def __init__(self): From 39be6d20332394307486f4fe7946c7e400d2a9b8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 00:48:10 +0200 Subject: [PATCH 75/82] Extract GStreamerMessagesThread as a generic GObjectEventThread --- mopidy/core.py | 8 +++++++- mopidy/outputs/gstreamer.py | 21 +++------------------ mopidy/utils/process.py | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 69760094..0be6b96f 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -7,7 +7,7 @@ from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file -from mopidy.utils.process import BaseThread +from mopidy.utils.process import BaseThread, GObjectEventThread from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') @@ -47,6 +47,7 @@ class CoreProcess(BaseThread): def setup(self): self.setup_logging() self.setup_settings() + self.gobject_loop = self.setup_gobject_loop(self.core_queue) self.output = self.setup_output(self.core_queue) self.backend = self.setup_backend(self.core_queue, self.output) self.frontends = self.setup_frontends(self.core_queue, self.backend) @@ -61,6 +62,11 @@ class CoreProcess(BaseThread): get_or_create_file('~/.mopidy/settings.py') settings.validate() + def setup_gobject_loop(self, core_queue): + gobject_loop = GObjectEventThread(core_queue) + gobject_loop.start() + return gobject_loop + def setup_output(self, core_queue): output = get_class(settings.OUTPUT)(core_queue) output.start() diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 52bd302d..3b037f62 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -1,6 +1,3 @@ -import gobject -gobject.threads_init() - import pygst pygst.require('0.10') import gst @@ -28,20 +25,14 @@ class GStreamerOutput(BaseOutput): def __init__(self, *args, **kwargs): super(GStreamerOutput, self).__init__(*args, **kwargs) - # Start a helper thread that can run the gobject.MainLoop - self.messages_thread = GStreamerMessagesThread(self.core_queue) - - # Start a helper thread that can process the output_queue self.output_queue = multiprocessing.Queue() self.player_thread = GStreamerPlayerThread(self.core_queue, self.output_queue) def start(self): - self.messages_thread.start() self.player_thread.start() def destroy(self): - self.messages_thread.destroy() self.player_thread.destroy() def process_message(self, message): @@ -91,21 +82,15 @@ class GStreamerOutput(BaseOutput): return self._send_recv({'command': 'set_volume', 'volume': volume}) -class GStreamerMessagesThread(BaseThread): - def __init__(self, core_queue): - super(GStreamerMessagesThread, self).__init__(core_queue) - self.name = u'GStreamerMessagesThread' - - def run_inside_try(self): - gobject.MainLoop().run() - - class GStreamerPlayerThread(BaseThread): """ A process for all work related to GStreamer. The main loop processes events from both Mopidy and GStreamer. + This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be + running too. This is not enforced in any way by the code. + Make sure this subprocess is started by the MainThread in the top-most parent process, and not some other thread. If not, we can get into the problems described at diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index c34d018c..11dafa8a 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -4,6 +4,9 @@ import multiprocessing.dummy from multiprocessing.reduction import reduce_connection import pickle +import gobject +gobject.threads_init() + from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') @@ -84,3 +87,25 @@ class BaseThread(multiprocessing.dummy.Process): self.core_queue.put({'to': 'core', 'command': 'exit', 'status': status, 'reason': reason}) self.destroy() + + +class GObjectEventThread(BaseThread): + """ + A GObject event loop which is shared by all Mopidy components that uses + libraries that need a GObject event loop, like GStreamer and D-Bus. + + Should be started by Mopidy's core and used by + :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc. + """ + + def __init__(self, core_queue): + super(GObjectEventThread, self).__init__(core_queue) + self.name = u'GObjectEventThread' + self.loop = None + + def run_inside_try(self): + self.loop = gobject.MainLoop().run() + + def destroy(self): + self.loop.quit() + super(GObjectEventThread, self).destroy() From abe77112441bf9cc4eb2abc62b94733c154b63b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 01:48:46 +0200 Subject: [PATCH 76/82] Encode as utf-8 before printing --- bin/mopidy-scan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 73af29fb..8534372c 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -28,4 +28,4 @@ if __name__ == '__main__': if len(a) == 1: print a[0] else: - print u': '.join([unicode(b) for b in a]) + print u': '.join([unicode(b) for b in a]).encode('utf-8') From 4b50e802d25fa0ca01d6dd69b4b66ef8306e8bce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 01:56:44 +0200 Subject: [PATCH 77/82] Expand ~ in LOCAL_TAG_CACHE and LOCAL_MUSIC_FOLDER before use --- mopidy/backends/local/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 04761e17..efcc3bbd 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -143,11 +143,12 @@ class LocalLibraryController(BaseLibraryController): self.refresh() def refresh(self, uri=None): - tracks = parse_mpd_tag_cache(settings.LOCAL_TAG_CACHE, - settings.LOCAL_MUSIC_FOLDER) + tag_cache = os.path.expanduser(settings.LOCAL_TAG_CACHE) + music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) - logger.info('Loading songs in %s from %s', - settings.LOCAL_MUSIC_FOLDER, settings.LOCAL_TAG_CACHE) + tracks = parse_mpd_tag_cache(tag_cache, music_folder) + + logger.info('Loading songs in %s from %s', music_folder, tag_cache) for track in tracks: self._uri_mapping[track.uri] = track From 16d44d5e368807b233f342fea1ebcf4e9f87487a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:00:49 +0200 Subject: [PATCH 78/82] Update changelog and docs wrt. mopidy-scan --- docs/changes.rst | 2 ++ docs/settings.rst | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index aef0055c..c3df7d85 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,6 +14,8 @@ No description yet. - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome application menus. +- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without + any help from the original MPD server. 0.2.0 (2010-10-24) diff --git a/docs/settings.rst b/docs/settings.rst index 8082b1bb..a7638b4e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -31,16 +31,44 @@ file:: BACKENDS = (u'mopidy.backends.local.LocalBackend',) -Previously this backend relied purely on ``tag_cache`` files from MPD, to -remedy this the command ``mopidy-scan`` has been added. This program will scan -your current ``LOCAL_MUSIC_FOLDER`` and build a MPD compatible ``tag_cache``. -Currently the command outputs the ``tag_cache`` to ``stdout``, this means that -you will need to run ``mopidy-scan > path/to/your/tag_cache`` to actually start -using your new cache. - You may also want to change some of the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of available settings. +.. note:: + + Currently, Mopidy supports using Spotify *or* local storage as a music + source. We're working on using both sources simultaneously, and will + hopefully have support for this in the 0.3 release. + + +Generating a tag cache +---------------------- + +Previously the local storage backend relied purely on ``tag_cache`` files +generated by the original MPD server. To remedy this the command +:command:`mopidy-scan` has been created. The program will scan your current +:attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` and build a MPD compatible +``tag_cache``. + +To make a ``tag_cache`` of your local music available for Mopidy: + +#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` points to where your + music is located. Check the current setting by running:: + + mopidy --list-settings + +#. Scan your music library. Currently the command outputs the ``tag_cache`` to + ``stdout``, which means that you will need to redirect the output to a file + yourself:: + + mopidy-scan > tag_cache + +#. Move the ``tag_cache`` file to the location + :attr:`mopidy.settings.LOCAL_TAG_CACHE` is set to, or change the setting to + point to where your ``tag_cache`` file is. + +#. Start Mopidy, find the music library in a client, and play some local music! + Connecting from other machines on the network ============================================= From 1b20c75d77944358ef91f7b8db9d2fbb66d87dfc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:16:06 +0200 Subject: [PATCH 79/82] docs: Split backend docs into multiple files --- docs/api/backends/dummy.rst | 7 +++++ docs/api/{backends.rst => backends/index.rst} | 26 ++++--------------- docs/api/backends/libspotify.rst | 7 +++++ docs/api/backends/local.rst | 7 +++++ docs/api/frontends/index.rst | 4 +++ 5 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 docs/api/backends/dummy.rst rename docs/api/{backends.rst => backends/index.rst} (74%) create mode 100644 docs/api/backends/libspotify.rst create mode 100644 docs/api/backends/local.rst diff --git a/docs/api/backends/dummy.rst b/docs/api/backends/dummy.rst new file mode 100644 index 00000000..03b2e6ce --- /dev/null +++ b/docs/api/backends/dummy.rst @@ -0,0 +1,7 @@ +********************************************************* +:mod:`mopidy.backends.dummy` -- Dummy backend for testing +********************************************************* + +.. automodule:: mopidy.backends.dummy + :synopsis: Dummy backend used for testing + :members: diff --git a/docs/api/backends.rst b/docs/api/backends/index.rst similarity index 74% rename from docs/api/backends.rst rename to docs/api/backends/index.rst index f675541a..100f6f0d 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends/index.rst @@ -82,25 +82,9 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. :undoc-members: -:mod:`mopidy.backends.dummy` -- Dummy backend for testing -========================================================= +Backends +======== -.. automodule:: mopidy.backends.dummy - :synopsis: Dummy backend used for testing - :members: - - -:mod:`mopidy.backends.libspotify` -- Libspotify backend -======================================================= - -.. automodule:: mopidy.backends.libspotify - :synopsis: Spotify backend using the libspotify library - :members: - - -:mod:`mopidy.backends.local` -- Local backend -===================================================== - -.. automodule:: mopidy.backends.local - :synopsis: Backend for playing music files on local storage - :members: +* :mod:`mopidy.backends.dummy` +* :mod:`mopidy.backends.libspotify` +* :mod:`mopidy.backends.local` diff --git a/docs/api/backends/libspotify.rst b/docs/api/backends/libspotify.rst new file mode 100644 index 00000000..e7528757 --- /dev/null +++ b/docs/api/backends/libspotify.rst @@ -0,0 +1,7 @@ +******************************************************* +:mod:`mopidy.backends.libspotify` -- Libspotify backend +******************************************************* + +.. automodule:: mopidy.backends.libspotify + :synopsis: Spotify backend using the libspotify library + :members: diff --git a/docs/api/backends/local.rst b/docs/api/backends/local.rst new file mode 100644 index 00000000..892f5a87 --- /dev/null +++ b/docs/api/backends/local.rst @@ -0,0 +1,7 @@ +********************************************* +:mod:`mopidy.backends.local` -- Local backend +********************************************* + +.. automodule:: mopidy.backends.local + :synopsis: Backend for playing music files on local storage + :members: diff --git a/docs/api/frontends/index.rst b/docs/api/frontends/index.rst index 05595418..2ab1df8b 100644 --- a/docs/api/frontends/index.rst +++ b/docs/api/frontends/index.rst @@ -4,6 +4,10 @@ A frontend is responsible for exposing Mopidy for a type of clients. +.. automodule:: mopidy.frontends + :synopsis: Frontend API + :members: + Frontend API ============ From bc4d671c862225036e2c6d36ba99e2898fbe3076 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:21:18 +0200 Subject: [PATCH 80/82] docs: Improved description of the frontend concept --- docs/api/frontends/index.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/api/frontends/index.rst b/docs/api/frontends/index.rst index 2ab1df8b..b01bac3d 100644 --- a/docs/api/frontends/index.rst +++ b/docs/api/frontends/index.rst @@ -2,7 +2,12 @@ :mod:`mopidy.frontends` *********************** -A frontend is responsible for exposing Mopidy for a type of clients. +A frontend may do whatever it wants to, including creating threads, opening TCP +ports and exposing Mopidy for a type of clients. + +Frontends got one main limitation: they are restricted to passing messages +through the ``core_queue`` for all communication with the rest of Mopidy. Thus, +the frontend API is very small and reveals little of what a frontend may do. .. automodule:: mopidy.frontends :synopsis: Frontend API From f42b4095bd057b31327693bd1362d300c3462bda Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:25:44 +0200 Subject: [PATCH 81/82] docs: Put changes above install guide. Existing user's should read the changes first --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 7a4dc27d..f53373dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,11 +6,11 @@ User documentation .. toctree:: :maxdepth: 3 + changes installation/index settings running clients/index - changes authors licenses From f6f0608dd0e06dd1871116d1771a382967abbfd1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:36:04 +0200 Subject: [PATCH 82/82] docs: How to install develop snapshot using pip --- docs/installation/index.rst | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 9577c383..580ecd6d 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -68,11 +68,11 @@ To install the currently latest release of Mopidy using ``pip``:: sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian sudo brew install pip # On OS X - sudo pip install Mopidy + sudo pip install mopidy To later upgrade to the latest release:: - sudo pip install -U Mopidy + sudo pip install -U mopidy If you for some reason can't use ``pip``, try ``easy_install``. @@ -80,26 +80,38 @@ Next, you need to set a couple of :doc:`settings `, and then you're ready to :doc:`run Mopidy `. -Install development version -=========================== +Install development snapshot +============================ -If you want to follow Mopidy development closer, you may install the -development version of Mopidy:: +If you want to follow Mopidy development closer, you may install a snapshot of +Mopidy's ``develop`` branch:: + + sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo brew install pip # On OS X + sudo pip install mopidy==dev + +Next, you need to set a couple of :doc:`settings `, and then you're +ready to :doc:`run Mopidy `. + + +Run from source code checkout +============================= + +If you may want to contribute to Mopidy, and want access to other branches as +well, you can checkout the Mopidy source from Git and run it directly from the +ckeckout:: sudo aptitude install git-core # On Ubuntu/Debian sudo brew install git # On OS X git clone git://github.com/jodal/mopidy.git cd mopidy/ - sudo python setup.py install + python mopidy # Yes, 'mopidy' is a dir To later update to the very latest version:: cd mopidy/ git pull - sudo python setup.py install For an introduction to ``git``, please visit `git-scm.com -`_. - -Next, you need to set a couple of :doc:`settings `, and then you're -ready to :doc:`run Mopidy `. +`_. Also, please read our :doc:`developer documentation +`.