def playlistinfo(context, parameter=None):
    """
    *musicpd.org, current playlist section:*

        ``playlistinfo [[SONGPOS] | [START:END]]``

        Displays a list of all songs in the playlist, or if the optional
        argument is given, displays information only for the song
        ``SONGPOS`` or the range of songs ``START:END``.

    *ncmpc and mpc:*

    - uses negative indexes, like ``playlistinfo "-1"``, to request
      the entire playlist
    """
    if parameter is None or parameter == "-1":
        start, end = 0, None
    else:
        tracklist_slice = protocol.RANGE(parameter)
        start, end = tracklist_slice.start, tracklist_slice.stop

    tl_tracks = context.core.tracklist.get_tl_tracks().get()
    if start and start > len(tl_tracks):
        raise exceptions.MpdArgError("Bad song index")
    if end and end > len(tl_tracks):
        end = None
    return translator.tracks_to_mpd_format(tl_tracks, context.session.tagtypes,
                                           start, end)
def addid(context, uri, songpos=None):
    """
    *musicpd.org, current playlist section:*

        ``addid {URI} [POSITION]``

        Adds a song to the playlist (non-recursive) and returns the song id.

        ``URI`` is always a single file or URL. For example::

            addid "foo.mp3"
            Id: 999
            OK

    *Clarifications:*

    - ``addid ""`` should return an error.
    """
    if not uri:
        raise exceptions.MpdNoExistError("No such song")

    length = context.core.tracklist.get_length()
    if songpos is not None and songpos > length.get():
        raise exceptions.MpdArgError("Bad song index")

    tl_tracks = context.core.tracklist.add(uris=[uri],
                                           at_position=songpos).get()

    if not tl_tracks:
        raise exceptions.MpdNoExistError("No such song")
    return ("Id", tl_tracks[0].tlid)
Example #3
0
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, _SEARCH_MAPPING)
    except ValueError:
        return
    results = context.core.library.search(query).get()

    uri = context.lookup_playlist_uri_from_name(playlist_name)
    playlist = uri is not None and context.core.playlists.lookup(uri).get()
    if not playlist:
        playlist = context.core.playlists.create(playlist_name).get()
    tracks = list(playlist.tracks) + _get_tracks(results)
    playlist = playlist.replace(tracks=tracks)
    context.core.playlists.save(playlist)
Example #4
0
def play(context, songpos=None):
    """
    *musicpd.org, playback section:*

        ``play [SONGPOS]``

        Begins playing the playlist at song number ``SONGPOS``.

    The original MPD server resumes from the paused state on ``play``
    without arguments.

    *Clarifications:*

    - ``play "-1"`` when playing is ignored.
    - ``play "-1"`` when paused resumes playback.
    - ``play "-1"`` when stopped with a current track starts playback at the
      current track.
    - ``play "-1"`` when stopped without a current track, e.g. after playlist
      replacement, starts playback at the first track.

    *BitMPC:*

    - issues ``play 6`` without quotes around the argument.
    """
    if songpos is None:
        return context.core.playback.play().get()
    elif songpos == -1:
        return _play_minus_one(context)

    try:
        tl_track = context.core.tracklist.slice(songpos, songpos + 1).get()[0]
        return context.core.playback.play(tl_track).get()
    except IndexError:
        raise exceptions.MpdArgError("Bad song index")
Example #5
0
            def validate(*args, **kwargs):
                if spec.varargs:
                    return func(*args, **kwargs)

                try:
                    ba = inspect.signature(func).bind(*args, **kwargs)
                    ba.apply_defaults()
                    callargs = ba.arguments
                except TypeError:
                    raise exceptions.MpdArgError(
                        f'wrong number of arguments for "{name}"')

                for key, value in callargs.items():
                    default = defaults.get(key, object())
                    if key in validators and value != default:
                        try:
                            callargs[key] = validators[key](value)
                        except ValueError:
                            raise exceptions.MpdArgError("incorrect arguments")

                return func(**callargs)
Example #6
0
def _query_from_mpd_search_parameters(parameters, mapping):
    query = {}
    parameters = list(parameters)
    while parameters:
        # TODO: does it matter that this is now case insensitive
        field = mapping.get(parameters.pop(0).lower())
        if not field:
            raise exceptions.MpdArgError("incorrect arguments")
        if not parameters:
            raise ValueError
        value = parameters.pop(0)
        if value.strip():
            query.setdefault(field, []).append(value)
    return query
def delete(context, songrange):
    """
    *musicpd.org, current playlist section:*

        ``delete [{POS} | {START:END}]``

        Deletes a song from the playlist.
    """
    start = songrange.start
    end = songrange.stop
    if end is None:
        end = context.core.tracklist.get_length().get()
    tl_tracks = context.core.tracklist.slice(start, end).get()
    if not tl_tracks:
        raise exceptions.MpdArgError("Bad song index", command="delete")
    for (tlid, _) in tl_tracks:
        context.core.tracklist.remove({"tlid": [tlid]})
Example #8
0
def tagtypes(context, *parameters):
    """
    *mpd.readthedocs.io, connection settings section:*

        ``tagtypes``

        Shows a list of available song metadata.

        ``tagtypes disable {NAME...}``

        Remove one or more tags from the list of tag types the client is interested in.

        ``tagtypes enable {NAME...}``

        Re-enable one or more tags from the list of tag types for this client.

        ``tagtypes clear``

        Clear the list of tag types this client is interested in.

        ``tagtypes all``

        Announce that this client is interested in all tag types.
    """
    parameters = list(parameters)
    if parameters:
        subcommand = parameters.pop(0).lower()
        if subcommand not in ("all", "clear", "disable", "enable"):
            raise exceptions.MpdArgError("Unknown sub command")
        elif subcommand == "all":
            context.session.tagtypes.update(tagtype_list.TAGTYPE_LIST)
        elif subcommand == "clear":
            context.session.tagtypes.clear()
        elif subcommand == "disable":
            _validate_tagtypes(parameters)
            context.session.tagtypes.difference_update(parameters)
        elif subcommand == "enable":
            _validate_tagtypes(parameters)
            context.session.tagtypes.update(parameters)
        return
    return [("tagtype", tagtype) for tagtype in context.session.tagtypes]
Example #9
0
def volume(context, change):
    """
    *musicpd.org, playback section:*

        ``volume {CHANGE}``

        Changes volume by amount ``CHANGE``.

        Note: ``volume`` is deprecated, use ``setvol`` instead.
    """
    if change < -100 or change > 100:
        raise exceptions.MpdArgError("Invalid volume value")

    old_volume = context.core.mixer.get_volume().get()
    if old_volume is None:
        raise exceptions.MpdSystemError("problems setting volume")

    new_volume = min(max(0, old_volume + change), 100)
    success = context.core.mixer.set_volume(new_volume).get()
    if not success:
        raise exceptions.MpdSystemError("problems setting volume")
Example #10
0
def playlistmove(context, name, from_pos, to_pos):
    """
    *musicpd.org, stored playlists section:*

        ``playlistmove {NAME} {SONGID} {SONGPOS}``

        Moves ``SONGID`` in the playlist ``NAME.m3u`` to the position
        ``SONGPOS``.

    *Clarifications:*

    - The second argument is not a ``SONGID`` as used elsewhere in the protocol
      documentation, but just the ``SONGPOS`` to move *from*, i.e.
      ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
    """
    if from_pos == to_pos:
        return

    _check_playlist_name(name)
    playlist = _get_playlist(context, name)
    if from_pos == to_pos:
        return  # Nothing to do

    try:
        # Convert tracks to list and perform move
        tracks = list(playlist.tracks)
        track = tracks.pop(from_pos)
        tracks.insert(to_pos, track)
    except IndexError:
        raise exceptions.MpdArgError("Bad song index")

    # Replace tracks and save playlist
    playlist = playlist.replace(tracks=tracks)
    saved_playlist = context.core.playlists.save(playlist).get()
    if saved_playlist is None:
        raise exceptions.MpdFailedToSavePlaylist(
            urllib.parse.urlparse(playlist.uri).scheme)
Example #11
0
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:*

    - use multiple tag-needle pairs to make more specific searches.
    """
    try:
        query = _query_from_mpd_search_parameters(args, _SEARCH_MAPPING)
    except ValueError:
        raise exceptions.MpdArgError("incorrect arguments")
    results = context.core.library.search(query=query, exact=True).get()
    result_tracks = _get_tracks(results)
    total_length = sum(t.length for t in result_tracks if t.length)
    return [
        ("songs", len(result_tracks)),
        ("playtime", int(total_length / 1000)),
    ]
Example #12
0
def playlistdelete(context, name, songpos):
    """
    *musicpd.org, stored playlists section:*

        ``playlistdelete {NAME} {SONGPOS}``

        Deletes ``SONGPOS`` from the playlist ``NAME.m3u``.
    """
    _check_playlist_name(name)
    playlist = _get_playlist(context, name)

    try:
        # Convert tracks to list and remove requested
        tracks = list(playlist.tracks)
        tracks.pop(songpos)
    except IndexError:
        raise exceptions.MpdArgError("Bad song index")

    # Replace tracks and save playlist
    playlist = playlist.replace(tracks=tracks)
    saved_playlist = context.core.playlists.save(playlist).get()
    if saved_playlist is None:
        raise exceptions.MpdFailedToSavePlaylist(
            urllib.parse.urlparse(playlist.uri).scheme)
Example #13
0
def split(line):
    """Splits a line into tokens using same rules as MPD.

    - Lines may not start with whitespace
    - Tokens are split by arbitrary amount of spaces or tabs
    - First token must match `[a-z][a-z0-9_]*`
    - Remaining tokens can be unquoted or quoted tokens.
    - Unquoted tokens consist of all printable characters except double quotes,
      single quotes, spaces and tabs.
    - Quoted tokens are surrounded by a matching pair of double quotes.
    - The closing quote must be followed by space, tab or end of line.
    - Any value is allowed inside a quoted token. Including double quotes,
      assuming it is correctly escaped.
    - Backslash inside a quoted token is used to escape the following
      character.

    For examples see the tests for this function.
    """
    if not line.strip():
        raise exceptions.MpdNoCommand("No command given")
    match = WORD_RE.match(line)
    if not match:
        raise exceptions.MpdUnknownError("Invalid word character")
    whitespace, command, remainder = match.groups()
    if whitespace:
        raise exceptions.MpdUnknownError("Letter expected")

    result = [command]
    while remainder:
        match = PARAM_RE.match(remainder)
        if not match:
            msg = _determine_error_message(remainder)
            raise exceptions.MpdArgError(msg, command=command)
        unquoted, quoted, remainder = match.groups()
        result.append(unquoted or UNESCAPE_RE.sub(r"\g<1>", quoted))
    return result
Example #14
0
def list_(context, *args):
    """
    *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

    *ncmpc:*

    - capitalizes the field argument.
    """
    params = list(args)
    if not params:
        raise exceptions.MpdArgError('too few arguments for "list"')

    field_arg = params.pop(0).lower()
    field = _LIST_MAPPING.get(field_arg)
    if field is None:
        raise exceptions.MpdArgError(f"Unknown tag type: {field_arg}")

    query = None
    if len(params) == 1:
        if field != "album":
            raise exceptions.MpdArgError('should be "Album" for 3 arguments')
        if params[0].strip():
            query = {"artist": params}
    else:
        try:
            query = _query_from_mpd_search_parameters(params, _SEARCH_MAPPING)
        except exceptions.MpdArgError as exc:
            exc.message = "Unknown filter type"  # noqa B306: Our own exception
            raise
        except ValueError:
            return

    name = _LIST_NAME_MAPPING[field]
    result = context.core.library.get_distinct(field, query)
    return [(name, value) for value in result.get()]
Example #15
0
def _validate_tagtypes(parameters):
    param_set = set(parameters)
    if not param_set:
        raise exceptions.MpdArgError("Not enough arguments")
    if not param_set.issubset(tagtype_list.TAGTYPE_LIST):
        raise exceptions.MpdArgError("Unknown tag type")