594 lines
18 KiB
Python
594 lines
18 KiB
Python
from __future__ import unicode_literals
|
|
|
|
import functools
|
|
import itertools
|
|
|
|
from mopidy.models import Ref, Track
|
|
from mopidy.mpd import exceptions, protocol, translator
|
|
|
|
|
|
LIST_QUERY = r"""
|
|
("?) # Optional quote around the field type
|
|
(?P<field>( # Field to list in the response
|
|
[Aa]lbum
|
|
| [Aa]lbumartist
|
|
| [Aa]rtist
|
|
| [Cc]omposer
|
|
| [Dd]ate
|
|
| [Gg]enre
|
|
| [Pp]erformer
|
|
))
|
|
\1 # End of optional quote around the field type
|
|
(?: # Non-capturing group for optional search query
|
|
\ # A single space
|
|
(?P<mpd_query>.*)
|
|
)?
|
|
$
|
|
"""
|
|
|
|
_SEARCH_FIELD_MAPPING = {
|
|
'album': 'album',
|
|
'albumartist': 'albumartist',
|
|
'any': 'any',
|
|
'artist': 'artist',
|
|
'comment': 'comment',
|
|
'composer': 'composer',
|
|
'date': 'date',
|
|
'file': 'uri',
|
|
'filename': 'uri',
|
|
'genre': 'genre',
|
|
'performer': 'performer',
|
|
'title': 'track_name',
|
|
'track': 'track_no'}
|
|
|
|
|
|
def _query_from_mpd_search_parameters(parameters):
|
|
query = {}
|
|
parameters = list(parameters)
|
|
while parameters:
|
|
# TODO: does it matter that this is now case insensitive
|
|
field = _SEARCH_FIELD_MAPPING.get(parameters.pop(0).lower())
|
|
if not field:
|
|
raise exceptions.MpdArgError('incorrect arguments')
|
|
if not parameters:
|
|
raise ValueError
|
|
query.setdefault(field, []).append(parameters.pop(0))
|
|
return query
|
|
|
|
|
|
def _get_field(field, search_results):
|
|
return list(itertools.chain(*[getattr(r, field) for r in search_results]))
|
|
|
|
|
|
_get_albums = functools.partial(_get_field, 'albums')
|
|
_get_artists = functools.partial(_get_field, 'artists')
|
|
_get_tracks = functools.partial(_get_field, 'tracks')
|
|
|
|
|
|
def _album_as_track(album):
|
|
return Track(
|
|
uri=album.uri,
|
|
name='Album: ' + album.name,
|
|
artists=album.artists,
|
|
album=album,
|
|
date=album.date)
|
|
|
|
|
|
def _artist_as_track(artist):
|
|
return Track(
|
|
uri=artist.uri,
|
|
name='Artist: ' + artist.name,
|
|
artists=[artist])
|
|
|
|
|
|
@protocol.commands.add('count')
|
|
def count(context, *args):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``count {TAG} {NEEDLE}``
|
|
|
|
Counts the number of songs and their total playtime in the db
|
|
matching ``TAG`` exactly.
|
|
|
|
*GMPC:*
|
|
|
|
- does not add quotes around the tag argument.
|
|
- use multiple tag-needle pairs to make more specific searches.
|
|
"""
|
|
try:
|
|
query = _query_from_mpd_search_parameters(args)
|
|
except ValueError:
|
|
raise exceptions.MpdArgError('incorrect arguments')
|
|
results = context.core.library.find_exact(**query).get()
|
|
result_tracks = _get_tracks(results)
|
|
return [
|
|
('songs', len(result_tracks)),
|
|
('playtime', sum(track.length for track in result_tracks) / 1000),
|
|
]
|
|
|
|
|
|
@protocol.commands.add('find')
|
|
def find(context, *args):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``find {TYPE} {WHAT}``
|
|
|
|
Finds songs in the db that are exactly ``WHAT``. ``TYPE`` can be any
|
|
tag supported by MPD, or one of the two special parameters - ``file``
|
|
to search by full path (relative to database root), and ``any`` to
|
|
match against all available tags. ``WHAT`` is what to find.
|
|
|
|
*GMPC:*
|
|
|
|
- does not add quotes around the field argument.
|
|
- also uses ``find album "[ALBUM]" artist "[ARTIST]"`` to list album
|
|
tracks.
|
|
|
|
*ncmpc:*
|
|
|
|
- does not add quotes around the field argument.
|
|
- capitalizes the type argument.
|
|
|
|
*ncmpcpp:*
|
|
|
|
- also uses the search type "date".
|
|
- uses "file" instead of "filename".
|
|
"""
|
|
try:
|
|
query = _query_from_mpd_search_parameters(args)
|
|
except ValueError:
|
|
return
|
|
|
|
results = context.core.library.find_exact(**query).get()
|
|
result_tracks = []
|
|
if ('artist' not in query and
|
|
'albumartist' not in query and
|
|
'composer' not in query and
|
|
'performer' not in query):
|
|
result_tracks += [_artist_as_track(a) for a in _get_artists(results)]
|
|
if 'album' not in query:
|
|
result_tracks += [_album_as_track(a) for a in _get_albums(results)]
|
|
result_tracks += _get_tracks(results)
|
|
return translator.tracks_to_mpd_format(result_tracks)
|
|
|
|
|
|
@protocol.commands.add('findadd')
|
|
def findadd(context, *args):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``findadd {TYPE} {WHAT}``
|
|
|
|
Finds songs in the db that are exactly ``WHAT`` and adds them to
|
|
current playlist. Parameters have the same meaning as for ``find``.
|
|
"""
|
|
try:
|
|
query = _query_from_mpd_search_parameters(args)
|
|
except ValueError:
|
|
return
|
|
results = context.core.library.find_exact(**query).get()
|
|
context.core.tracklist.add(_get_tracks(results))
|
|
|
|
|
|
@protocol.handle_request(r'list\ ' + LIST_QUERY)
|
|
def list_(context, field, mpd_query=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``list {TYPE} [ARTIST]``
|
|
|
|
Lists all tags of the specified type. ``TYPE`` should be ``album``,
|
|
``artist``, ``albumartist``, ``date``, or ``genre``.
|
|
|
|
``ARTIST`` is an optional parameter when type is ``album``,
|
|
``date``, or ``genre``. This filters the result list by an artist.
|
|
|
|
*Clarifications:*
|
|
|
|
The musicpd.org documentation for ``list`` is far from complete. The
|
|
command also supports the following variant:
|
|
|
|
``list {TYPE} {QUERY}``
|
|
|
|
Where ``QUERY`` applies to all ``TYPE``. ``QUERY`` is one or more pairs
|
|
of a field name and a value. If the ``QUERY`` consists of more than one
|
|
pair, the pairs are AND-ed together to find the result. Examples of
|
|
valid queries and what they should return:
|
|
|
|
``list "artist" "artist" "ABBA"``
|
|
List artists where the artist name is "ABBA". Response::
|
|
|
|
Artist: ABBA
|
|
OK
|
|
|
|
``list "album" "artist" "ABBA"``
|
|
Lists albums where the artist name is "ABBA". Response::
|
|
|
|
Album: More ABBA Gold: More ABBA Hits
|
|
Album: Absolute More Christmas
|
|
Album: Gold: Greatest Hits
|
|
OK
|
|
|
|
``list "artist" "album" "Gold: Greatest Hits"``
|
|
Lists artists where the album name is "Gold: Greatest Hits".
|
|
Response::
|
|
|
|
Artist: ABBA
|
|
OK
|
|
|
|
``list "artist" "artist" "ABBA" "artist" "TLC"``
|
|
Lists artists where the artist name is "ABBA" *and* "TLC". Should
|
|
never match anything. Response::
|
|
|
|
OK
|
|
|
|
``list "date" "artist" "ABBA"``
|
|
Lists dates where artist name is "ABBA". Response::
|
|
|
|
Date:
|
|
Date: 1992
|
|
Date: 1993
|
|
OK
|
|
|
|
``list "date" "artist" "ABBA" "album" "Gold: Greatest Hits"``
|
|
Lists dates where artist name is "ABBA" and album name is "Gold:
|
|
Greatest Hits". Response::
|
|
|
|
Date: 1992
|
|
OK
|
|
|
|
``list "genre" "artist" "The Rolling Stones"``
|
|
Lists genres where artist name is "The Rolling Stones". Response::
|
|
|
|
Genre:
|
|
Genre: Rock
|
|
OK
|
|
|
|
*GMPC:*
|
|
|
|
- does not add quotes around the field argument.
|
|
|
|
*ncmpc:*
|
|
|
|
- does not add quotes around the field argument.
|
|
- capitalizes the field argument.
|
|
"""
|
|
field = field.lower()
|
|
try:
|
|
query = translator.query_from_mpd_list_format(field, mpd_query)
|
|
except ValueError:
|
|
return
|
|
if field == 'artist':
|
|
return _list_artist(context, query)
|
|
if field == 'albumartist':
|
|
return _list_albumartist(context, query)
|
|
elif field == 'album':
|
|
return _list_album(context, query)
|
|
elif field == 'composer':
|
|
return _list_composer(context, query)
|
|
elif field == 'performer':
|
|
return _list_performer(context, query)
|
|
elif field == 'date':
|
|
return _list_date(context, query)
|
|
elif field == 'genre':
|
|
return _list_genre(context, query)
|
|
|
|
|
|
def _list_artist(context, query):
|
|
artists = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
for artist in track.artists:
|
|
if artist.name:
|
|
artists.add(('Artist', artist.name))
|
|
return artists
|
|
|
|
|
|
def _list_albumartist(context, query):
|
|
albumartists = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
if track.album:
|
|
for artist in track.album.artists:
|
|
if artist.name:
|
|
albumartists.add(('AlbumArtist', artist.name))
|
|
return albumartists
|
|
|
|
|
|
def _list_album(context, query):
|
|
albums = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
if track.album and track.album.name:
|
|
albums.add(('Album', track.album.name))
|
|
return albums
|
|
|
|
|
|
def _list_composer(context, query):
|
|
composers = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
for composer in track.composers:
|
|
if composer.name:
|
|
composers.add(('Composer', composer.name))
|
|
return composers
|
|
|
|
|
|
def _list_performer(context, query):
|
|
performers = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
for performer in track.performers:
|
|
if performer.name:
|
|
performers.add(('Performer', performer.name))
|
|
return performers
|
|
|
|
|
|
def _list_date(context, query):
|
|
dates = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
if track.date:
|
|
dates.add(('Date', track.date))
|
|
return dates
|
|
|
|
|
|
def _list_genre(context, query):
|
|
genres = set()
|
|
results = context.core.library.find_exact(**query).get()
|
|
for track in _get_tracks(results):
|
|
if track.genre:
|
|
genres.add(('Genre', track.genre))
|
|
return genres
|
|
|
|
|
|
# TODO: see if we can combine listall, listallinfo and lsinfo to one helper
|
|
@protocol.commands.add('listall')
|
|
def listall(context, uri=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``listall [URI]``
|
|
|
|
Lists all songs and directories in ``URI``.
|
|
"""
|
|
result = []
|
|
root_path = translator.normalize_path(uri)
|
|
try:
|
|
uri = context.directory_path_to_uri(root_path)
|
|
except exceptions.MpdNoExistError as e:
|
|
e.message = 'Not found'
|
|
raise
|
|
browse_futures = [(root_path, context.core.library.browse(uri))]
|
|
|
|
while browse_futures:
|
|
base_path, future = browse_futures.pop()
|
|
for ref in future.get():
|
|
if ref.type == Ref.DIRECTORY:
|
|
path = '/'.join([base_path, ref.name.replace('/', '')])
|
|
result.append(('directory', path))
|
|
browse_futures.append(
|
|
(path, context.core.library.browse(ref.uri)))
|
|
elif ref.type == Ref.TRACK:
|
|
result.append(('file', ref.uri))
|
|
|
|
if not result:
|
|
raise exceptions.MpdNoExistError('Not found')
|
|
|
|
return [('directory', root_path)] + result
|
|
|
|
|
|
@protocol.commands.add('listallinfo')
|
|
def listallinfo(context, uri=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``listallinfo [URI]``
|
|
|
|
Same as ``listall``, except it also returns metadata info in the
|
|
same format as ``lsinfo``.
|
|
"""
|
|
dirs_and_futures = []
|
|
result = []
|
|
root_path = translator.normalize_path(uri)
|
|
try:
|
|
uri = context.directory_path_to_uri(root_path)
|
|
except exceptions.MpdNoExistError as e:
|
|
e.command = 'listallinfo'
|
|
e.message = 'Not found'
|
|
raise
|
|
browse_futures = [(root_path, context.core.library.browse(uri))]
|
|
|
|
while browse_futures:
|
|
base_path, future = browse_futures.pop()
|
|
for ref in future.get():
|
|
if ref.type == Ref.DIRECTORY:
|
|
path = '/'.join([base_path, ref.name.replace('/', '')])
|
|
future = context.core.library.browse(ref.uri)
|
|
browse_futures.append((path, future))
|
|
dirs_and_futures.append(('directory', path))
|
|
elif ref.type == Ref.TRACK:
|
|
# TODO Lookup tracks in batch for better performance
|
|
dirs_and_futures.append(context.core.library.lookup(ref.uri))
|
|
|
|
result = []
|
|
for obj in dirs_and_futures:
|
|
if hasattr(obj, 'get'):
|
|
for track in obj.get():
|
|
result.extend(translator.track_to_mpd_format(track))
|
|
else:
|
|
result.append(obj)
|
|
|
|
if not result:
|
|
raise exceptions.MpdNoExistError('Not found')
|
|
|
|
return [('directory', root_path)] + result
|
|
|
|
|
|
@protocol.commands.add('lsinfo')
|
|
def lsinfo(context, uri=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``lsinfo [URI]``
|
|
|
|
Lists the contents of the directory ``URI``.
|
|
|
|
When listing the root directory, this currently returns the list of
|
|
stored playlists. This behavior is deprecated; use
|
|
``listplaylists`` instead.
|
|
|
|
MPD returns the same result, including both playlists and the files and
|
|
directories located at the root level, for both ``lsinfo``, ``lsinfo
|
|
""``, and ``lsinfo "/"``.
|
|
"""
|
|
result = []
|
|
root_path = translator.normalize_path(uri, relative=True)
|
|
try:
|
|
uri = context.directory_path_to_uri(root_path)
|
|
except exceptions.MpdNoExistError as e:
|
|
e.command = 'lsinfo'
|
|
e.message = 'Not found'
|
|
raise
|
|
|
|
if uri is None:
|
|
result.extend(protocol.stored_playlists.listplaylists(context))
|
|
|
|
for ref in context.core.library.browse(uri).get():
|
|
if ref.type == Ref.DIRECTORY:
|
|
path = '/'.join([root_path, ref.name.replace('/', '')])
|
|
result.append(('directory', path.lstrip('/')))
|
|
elif ref.type == Ref.TRACK:
|
|
# TODO Lookup tracks in batch for better performance
|
|
tracks = context.core.library.lookup(ref.uri).get()
|
|
if tracks:
|
|
result.extend(translator.track_to_mpd_format(tracks[0]))
|
|
return result
|
|
|
|
|
|
@protocol.commands.add('rescan')
|
|
def rescan(context, uri=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``rescan [URI]``
|
|
|
|
Same as ``update``, but also rescans unmodified files.
|
|
"""
|
|
return {'updating_db': 0} # TODO
|
|
|
|
|
|
@protocol.commands.add('search')
|
|
def search(context, *args):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``search {TYPE} {WHAT} [...]``
|
|
|
|
Searches for any song that contains ``WHAT``. Parameters have the same
|
|
meaning as for ``find``, except that search is not case sensitive.
|
|
|
|
*GMPC:*
|
|
|
|
- does not add quotes around the field argument.
|
|
- uses the undocumented field ``any``.
|
|
- searches for multiple words like this::
|
|
|
|
search any "foo" any "bar" any "baz"
|
|
|
|
*ncmpc:*
|
|
|
|
- does not add quotes around the field argument.
|
|
- capitalizes the field argument.
|
|
|
|
*ncmpcpp:*
|
|
|
|
- also uses the search type "date".
|
|
- uses "file" instead of "filename".
|
|
"""
|
|
try:
|
|
query = _query_from_mpd_search_parameters(args)
|
|
except ValueError:
|
|
return
|
|
results = context.core.library.search(**query).get()
|
|
artists = [_artist_as_track(a) for a in _get_artists(results)]
|
|
albums = [_album_as_track(a) for a in _get_albums(results)]
|
|
tracks = _get_tracks(results)
|
|
return translator.tracks_to_mpd_format(artists + albums + tracks)
|
|
|
|
|
|
@protocol.commands.add('searchadd')
|
|
def searchadd(context, *args):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``searchadd {TYPE} {WHAT} [...]``
|
|
|
|
Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds
|
|
them to current playlist.
|
|
|
|
Parameters have the same meaning as for ``find``, except that search is
|
|
not case sensitive.
|
|
"""
|
|
try:
|
|
query = _query_from_mpd_search_parameters(args)
|
|
except ValueError:
|
|
return
|
|
results = context.core.library.search(**query).get()
|
|
context.core.tracklist.add(_get_tracks(results))
|
|
|
|
|
|
@protocol.commands.add('searchaddpl')
|
|
def searchaddpl(context, *args):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``searchaddpl {NAME} {TYPE} {WHAT} [...]``
|
|
|
|
Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds
|
|
them to the playlist named ``NAME``.
|
|
|
|
If a playlist by that name doesn't exist it is created.
|
|
|
|
Parameters have the same meaning as for ``find``, except that search is
|
|
not case sensitive.
|
|
"""
|
|
parameters = list(args)
|
|
if not parameters:
|
|
raise exceptions.MpdArgError('incorrect arguments')
|
|
playlist_name = parameters.pop(0)
|
|
try:
|
|
query = _query_from_mpd_search_parameters(parameters)
|
|
except ValueError:
|
|
return
|
|
results = context.core.library.search(**query).get()
|
|
|
|
playlist = context.lookup_playlist_from_name(playlist_name)
|
|
if not playlist:
|
|
playlist = context.core.playlists.create(playlist_name).get()
|
|
tracks = list(playlist.tracks) + _get_tracks(results)
|
|
playlist = playlist.copy(tracks=tracks)
|
|
context.core.playlists.save(playlist)
|
|
|
|
|
|
@protocol.commands.add('update')
|
|
def update(context, uri=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``update [URI]``
|
|
|
|
Updates the music database: find new files, remove deleted files,
|
|
update modified files.
|
|
|
|
``URI`` is a particular directory or song/file to update. If you do
|
|
not specify it, everything is updated.
|
|
|
|
Prints ``updating_db: JOBID`` where ``JOBID`` is a positive number
|
|
identifying the update job. You can read the current job id in the
|
|
``status`` response.
|
|
"""
|
|
return {'updating_db': 0} # TODO
|