Skip to content

Commit

Permalink
WIP: Add support for querying playlists
Browse files Browse the repository at this point in the history
Fixes metabrainz#9

Still to do:

  * Document all new API and check that generated output is correct
  * Add tests for new functions and ensure coverage is OK
  • Loading branch information
ssssam committed Jan 31, 2022
1 parent 417a72f commit 0e8e715
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 1 deletion.
2 changes: 2 additions & 0 deletions pylistenbrainz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@
from pylistenbrainz.client import ListenBrainz
from pylistenbrainz.listen import Listen
from pylistenbrainz.listen import LISTEN_TYPE_IMPORT, LISTEN_TYPE_PLAYING_NOW, LISTEN_TYPE_SINGLE
from pylistenbrainz.playlist import Playlist, PlaylistMetadata, PlaylistTrack
from pylistenbrainz.playlist import PLAYLIST_QUERY_TYPE_CREATED_BY, PLAYLIST_QUERY_TYPE_COLLABORATOR, PLAYLIST_QUERY_TYPE_CREATED_FOR
46 changes: 45 additions & 1 deletion pylistenbrainz/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
from enum import Enum
from pylistenbrainz import errors
from pylistenbrainz.listen import LISTEN_TYPE_IMPORT, LISTEN_TYPE_PLAYING_NOW, LISTEN_TYPE_SINGLE
from pylistenbrainz.playlist import PlaylistMetadata
from pylistenbrainz.playlist import _playlist_metadata_from_response, _playlist_from_response
from pylistenbrainz.playlist import PLAYLIST_QUERY_TYPE_CREATED_BY, PLAYLIST_QUERY_TYPE_COLLABORATOR, PLAYLIST_QUERY_TYPE_CREATED_FOR
from pylistenbrainz.utils import _validate_submit_listens_payload, _convert_api_payload_to_listen
from urllib.parse import urljoin

Expand Down Expand Up @@ -421,4 +424,45 @@ def get_user_listen_count(self, username):
if e.status_code == 204:
return None
else:
raise
raise

def get_user_playlists(self, username, query_type=PLAYLIST_QUERY_TYPE_CREATED_BY, count=25, offset=0):
""" List all playlists for user
By default this returns playlists created by the user. The ``query_type``
parameter can return several things:
* ``PLAYLIST_QUERY_TYPE_CREATED_BY``: playlists the user owns (default)
* ``PLAYLIST_QUERY_TYPE_COLLABORATOR``: any playlist where the user contributed
* ``PLAYLIST_QUERY_TYPE_CREATED_FOR``: playlists created for the user (includes generated recommendations)
This returns the playlist metadata. To retrieve the contents of the
playlist, see: :func:`get_playlist`.
:param username: the username of the user whose playlists are to be fetched.
:type username: str
:param query_type: type of playlists to fetch
:param count: the number of playlists to fetch, defaults to 25, maximum is 100.
:type count: int, optional
:param offset: the number of playlists to skip from the beginning, for pagination, defaults to 0.
:type offset: int, optional
:return: a list of :class:`PlaylistMetadata` instances
:rtype: list
"""
if query_type == PLAYLIST_QUERY_TYPE_COLLABORATOR:
endpoint = '/1/user/{}/playlists/collaborator'.format(username)
elif query_type == PLAYLIST_QUERY_TYPE_CREATED_FOR:
endpoint = '/1/user/{}/playlists/createdfor'.format(username)
else:
endpoint = '/1/user/{}/playlists'.format(username)
data = self._get(endpoint)
playlists = data['playlists']
return [_playlist_metadata_from_response(item) for item in playlists]

def get_playlist(self, mbid):
data = self._get('/1/playlist/{mbid}'.format(mbid=mbid))
return _playlist_from_response(data)
188 changes: 188 additions & 0 deletions pylistenbrainz/playlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# pylistenbrainz - A simple client library for ListenBrainz
# Copyright (C) 2022 Sam Thursfield <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.


import pathlib
import urllib.parse


PLAYLIST_QUERY_TYPE_CREATED_BY = 'created_by'
PLAYLIST_QUERY_TYPE_COLLABORATOR = 'collaborator'
PLAYLIST_QUERY_TYPE_CREATED_FOR = 'created_for'

PLAYLIST_QUERY_TYPES = (
PLAYLIST_QUERY_TYPE_CREATED_BY,
PLAYLIST_QUERY_TYPE_COLLABORATOR,
PLAYLIST_QUERY_TYPE_CREATED_FOR,
)


def _url_to_mbid(url, kind):
parsed = urllib.parse.urlsplit(url)
path = pathlib.Path(parsed.path)
if path.parent.name == kind:
return path.name
else:
raise ValueError("Expected {} URL, got: {}".format(kind, url))

def _mbid_to_url(mbid, kind):
if kind == 'playlist':
base = 'https://listenbrainz.org/'
else:
base = 'https://musicbrainz.org/'
path = pathlib.Path(kind).joinpath(mbid)
return urllib.parse.urljoin(base, str(path))


class PlaylistMetadata():
def __init__(self,
identifier,
annotation=None,
creator=None,
date=None,
title=None,
algorithm_metadata=None,
collaborators=None,
last_modified_at=None,
public=True):
self.identifier = identifier
self.annotation = annotation
self.creator = creator
self.date = date
self.title = title
self.algorithm_metadata = algorithm_metadata
self.collaborators = collaborators or []
self.last_modified_at = last_modified_at
self.public = public

self.url = _mbid_to_url(identifier, 'playlist')

def __repr__(self):
return('<pylistenbrainz.PlaylistMetadata url="{}">'.format(self.url))


def _maybe_add(data, key, value):
if key:
data[key] = value



class PlaylistTrack():
def __init__(self,
identifier,
creator,
title,
added_at=None,
added_by=None,
artist_identifier=None):
self.identifier = identifier
self.creator = creator
self.title = title
self.added_at = added_at
self.added_by = added_by
self.artist_identifier = artist_identifier

self.url = _mbid_to_url(identifier, 'recording')

def __repr__(self):
return('<pylistenbrainz.PlaylistTrack url="{}">'.format(self.url))

def to_jspf(self):
ext = {}
_maybe_add(ext, 'added_at', self.added_at)
_maybe_add(ext, 'added_by', self.added_by)
_maybe_add(ext, 'artist_identifier', self.artist_identifier)

data = {
'identifier': self.identifier,
'extension': {
'https://musicbrainz.org/doc/jspf#track': ext
},
}

_maybe_add(data, 'creator', self.creator)
_maybe_add(data, 'title', self.title)

return data


class Playlist():
def __init__(self, metadata, tracks):
self.metadata = metadata
self.tracks = tracks

self.identifier = metadata.identifier
self.url = metadata.url

def __repr__(self):
return '<pylistenbrainz.Playlist url="{}">'.format(self.url)

def to_jspf(self):
ext = {}
_maybe_add(ext, 'algorithm_metadata', self.metadata.algorithm_metadata)
_maybe_add(ext, 'collaborators', self.metadata.collaborators)
_maybe_add(ext, 'last_modified_at', self.metadata.last_modified_at)
_maybe_add(ext, 'public', self.metadata.public)

data = {
'identifier': self.identifier,
'extension': {
'https://musicbrainz.org/doc/jspf#playlist': ext
},
'tracks': [track.to_jspf() for track in self.tracks],
}
_maybe_add(data, 'annotation', self.metadata.annotation)
_maybe_add(data, 'creator', self.metadata.creator)
_maybe_add(data, 'date', self.metadata.date)
_maybe_add(data, 'title', self.metadata.title)

return data


def _playlist_metadata_from_response(payload):
data = payload['playlist']
ext = data['extension']['https://musicbrainz.org/doc/jspf#playlist']
return PlaylistMetadata(
identifier = _url_to_mbid(data['identifier'], kind='playlist'),
annotation = data['annotation'],
creator = data['creator'],
date = data['date'],
title = data['title'],
algorithm_metadata = ext.get('algorithm_metadata'),
collaborators = ext['collaborators'],
last_modified_at = ext['last_modified_at'],
public = ext['public']
)


def _playlist_from_response(payload):
metadata = _playlist_metadata_from_response(payload)
tracks = []
for track in payload['playlist']['track']:
ext = track['extension']['https://musicbrainz.org/doc/jspf#track']
artist_identifiers = [
_url_to_mbid(artist, kind='artist')
for artist in ext['artist_identifier']
]
tracks.append(PlaylistTrack(
identifier = _url_to_mbid(track['identifier'], kind='recording'),
creator = track['creator'],
title = track['title'],
added_at = ext['added_at'],
added_by = ext['added_by'],
artist_identifier = artist_identifiers,
))
return Playlist(metadata, tracks)

0 comments on commit 0e8e715

Please sign in to comment.