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/bin/mopidy-scan b/bin/mopidy-scan new file mode 100755 index 00000000..8534372c --- /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]).encode('utf-8') 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/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..b01bac3d 100644 --- a/docs/api/frontends/index.rst +++ b/docs/api/frontends/index.rst @@ -2,7 +2,16 @@ :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 + :members: Frontend API diff --git a/docs/changes.rst b/docs/changes.rst index cb34993e..c3df7d85 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,7 +12,10 @@ No description yet. **Changes** -- None so far. +- 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/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 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 +`. diff --git a/docs/settings.rst b/docs/settings.rst index afdd39dc..a7638b4e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -34,6 +34,41 @@ file:: 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 ============================================= diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 4ad8947b..9796414d 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -154,11 +154,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 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/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' diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 07a58dd3..2b1adf50 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,4 +1,12 @@ -def track_to_mpd_format(track, position=None, cpid=None): +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 + +def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -8,12 +16,16 @@ def track_to_mpd_format(track, position=None, cpid=None): :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 = [ ('file', 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 ''), @@ -23,20 +35,43 @@ def track_to_mpd_format(track, position=None, cpid=None): 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)) + if key and track.uri: + result.insert(0, ('key', os.path.basename(uri_to_path(track.uri)))) + if mtime and track.uri: + result.append(('mtime', get_mtime(uri_to_path(track.uri)))) return result -def track_artists_to_mpd_format(track): +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): + """ + 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): """ 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]) @@ -72,3 +107,58 @@ 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): + """ + 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), + ('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 + +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', mtime)) + result.append(('begin', name)) + _add_to_tag_cache(result, *entry) + result.append(('end', name)) + + result.append(('songList begin',)) + for track in files: + 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): + directories = ({}, []) + for track in tracks: + path = u'' + current = directories + + 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] = ({}, []) + current = current[0][path] + current[1].append(track) + return directories 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/scanner.py b/mopidy/scanner.py new file mode 100644 index 00000000..436598bd --- /dev/null +++ b/mopidy/scanner.py @@ -0,0 +1,122 @@ +import gobject +gobject.threads_init() + +import pygst +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): + albumartist_kwargs = {} + album_kwargs = {} + artist_kwargs = {} + track_kwargs = {} + + 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'] + + 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) + track_kwargs['artists'] = [Artist(**artist_kwargs)] + + return Track(**track_kwargs) + + +class Scanner(object): + def __init__(self, folder, data_callback, error_callback=None): + self.uris = [path_to_uri(f) for f in find_files(folder)] + self.data_callback = data_callback + 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) + self.pipe.add(self.uribin) + + bus = self.pipe.get_bus() + bus.add_signal_watch() + 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): + data = message.parse_tag() + data = dict([(k, data[k]) for k in data.keys()]) + data['uri'] = unicode(self.uribin.get_property('uri')) + data['duration'] = self.get_duration() + self.data_callback(data) + self.next_uri() + + def process_error(self, bus, message): + if self.error_callback: + uri = self.uribin.get_property('uri') + errors = message.parse_error() + self.error_callback(uri, errors) + self.next_uri() + + def get_duration(self): + self.pipe.get_state() + 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() + + self.pipe.set_state(gst.STATE_NULL) + self.uribin.set_property('uri', self.uris.pop()) + self.pipe.set_state(gst.STATE_PAUSED) + + def start(self): + if not self.uris: + return + self.next_uri() + self.loop.run() + + def stop(self): + self.pipe.set_state(gst.STATE_NULL) + self.loop.quit() diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 0dd163ec..b3669e38 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') @@ -26,3 +27,53 @@ def path_to_uri(*paths): if sys.platform == 'win32': 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 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): + path = os.path.expanduser(path) + if os.path.isfile(path): + filename = os.path.abspath(path) + 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) + if not isinstance(filename, unicode): + filename = filename.decode('utf-8') + yield 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/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() diff --git a/setup.py b/setup.py index fabc8353..d77be3cd 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(), @@ -78,7 +80,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', diff --git a/tests/data/blank.mp3 b/tests/data/blank.mp3 index 6aa48cd8..ef159a70 100644 Binary files a/tests/data/blank.mp3 and b/tests/data/blank.mp3 differ 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/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/.gitignore b/tests/data/scanner/empty/.gitignore new file mode 100644 index 00000000..e69de29b 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/image/test.png b/tests/data/scanner/image/test.png new file mode 100644 index 00000000..2aaf9c3d Binary files /dev/null and b/tests/data/scanner/image/test.png differ diff --git a/tests/data/scanner/sample.mp3 b/tests/data/scanner/sample.mp3 new file mode 100644 index 00000000..ad5aa37a Binary files /dev/null and b/tests/data/scanner/sample.mp3 differ 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 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 diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 0e0f8183..8e8a5d21 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,11 +1,24 @@ import datetime as dt +import os import unittest -from mopidy.frontends.mpd import translator +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 + class TrackMpdFormatTest(unittest.TestCase): - def test_mpd_format_for_empty_track(self): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + mtime.set_fake_time(1234567) + + def tearDown(self): + settings.runtime.clear() + mtime.undo_fake() + + 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) @@ -14,13 +27,43 @@ 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): + 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_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_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_track_to_mpd_format_with_key(self): + 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_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) + self.assert_(('mtime', 1234567) in result) + + def test_track_to_mpd_format_for_nonempty_track(self): track = Track( 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, @@ -31,15 +74,17 @@ 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), 10) - 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') + 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') class PlaylistMpdFormatTest(unittest.TestCase): @@ -55,3 +100,234 @@ 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): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + mtime.set_fake_time(1234567) + + def tearDown(self): + 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]) + self.assertEqual(('fs_charset', protocol.ENCODING), result[2]) + self.assertEqual(('info_end',), result[3]) + return result[4:] + + def consume_song_list(self, result): + self.assertEqual(('songList begin',), result[0]) + 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 consume_directory(self, result): + self.assertEqual('directory', result[0][0]) + 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): + if row == ('end', directory): + 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): + result = translator.tracks_to_tag_cache_format([]) + 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_tag_cache_has_header(self): + track = Track(uri='file:///dir/subdir/song.mp3') + result = translator.tracks_to_tag_cache_format([track]) + result = self.consume_headers(result) + + 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 = self.translate(track) + 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 = self.translate(track) + 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_suports_directories(self): + track = Track(uri='file:///dir/subdir/folder/song.mp3') + formated = self.translate(track) + 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) + + song_list, result = self.consume_song_list(folder) + 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 = self.translate(track) + 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', 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 = self.translate(track) + 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) + + 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(self.translate(tracks[0])) + formated.extend(self.translate(tracks[1])) + + 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) + + 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(self.translate(tracks[0])) + formated.append(self.translate(tracks[1])) + + 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): + settings.LOCAL_MUSIC_FOLDER = '/root/' + + 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:///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:///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:///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:///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 = ( + { + 'dir1': ({}, [tracks[1], tracks[2]]), + 'dir2': ( + { + 'dir2/sub': ({}, [tracks[4]]) + }, + [tracks[3]] + ), + }, + [tracks[0]] + ) + self.assertEqual(tree, expected) diff --git a/tests/scanner_test.py b/tests/scanner_test.py new file mode 100644 index 00000000..141f2ceb --- /dev/null +++ b/tests/scanner_test.py @@ -0,0 +1,158 @@ +import unittest +from datetime import date + +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 setUp(self): + self.data = { + 'uri': 'uri', + 'album': u'albumname', + 'track-number': 1, + 'artist': u'name', + 'album-artist': 'albumartistname', + 'title': u'trackname', + 'track-count': 2, + 'date': FakeGstDate(2006, 1, 1,), + 'container-format': u'ID3 tag', + 'duration': 4531, + } + + self.album = { + 'name': 'albumname', + 'num_tracks': 2, + } + + self.artist = { + 'name': 'name', + } + + self.albumartist = { + 'name': 'albumartistname', + } + + self.track = { + 'uri': 'uri', + 'name': 'trackname', + 'date': date(2006, 1, 1), + 'track_no': 1, + 'length': 4531, + } + + 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) + + def check(self): + expected = self.build_track() + actual = translator(self.data) + self.assertEqual(expected, actual) + + def test_basic_data(self): + self.check() + + def test_missing_track_number(self): + 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_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'] + self.check() + +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 check(self, name, key, value): + name = data_folder(name) + self.assertEqual(self.data[name][key], value) + + def data_callback(self, data): + uri = data['uri'][len('file://'):] + self.data[uri] = data + + def error_callback(self, uri, errors): + uri = uri[len('file://'):] + self.errors[uri] = errors + + def test_data_is_set(self): + self.scan('scanner/simple') + self.assert_(self.data) + + def test_errors_is_not_set(self): + 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_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') + + def test_album_is_set(self): + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'album', 'albumname') + + def test_track_is_set(self): + 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) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index ae63d5c0..758a09ab 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,9 +6,10 @@ 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, mtime, + path_to_uri, uri_to_path, split_path, find_files) -from tests import SkipTest +from tests import SkipTest, data_folder class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): @@ -69,3 +70,87 @@ class PathToFileURITest(unittest.TestCase): else: result = path_to_uri(u'/tmp/æøå') 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 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))) + + 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')) + + 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 + + +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)