Source code for tidalapi.playlist

# -*- coding: utf-8 -*-

# Copyright (C) 2019-2022 morguldir
# Copyright (C) 2014 Thomas Amland
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
A module containing things related to TIDAL playlists.
"""

import copy
import dateutil.parser


[docs]class Playlist(object): """ An object containing various data about a playlist and methods to work with them. """ id = None name = None num_tracks = -1 num_videos = -1 creator = None description = None duration = -1 last_updated = None created = None type = None public = False popularity = -1 promoted_artists = None last_item_added_at = None picture = None square_picture = None user_date_added = None _etag = None def __init__(self, session, playlist_id): self.id = playlist_id self.session = session self.requests = session.request self._base_url = 'playlists/%s' if playlist_id: request = self.requests.request('GET', self._base_url % playlist_id) self._etag = request.headers['etag'] self.parse(request.json())
[docs] def parse(self, json_obj): """ Parses a playlist from tidal, replaces the current playlist object. :param json_obj: Json data returned from api.tidal.com containing a playlist :return: Returns a copy of the original :exc:'Playlist': object """ self.id = json_obj['uuid'] self.name = json_obj['title'] self.num_tracks = int(json_obj['numberOfTracks']) self.num_videos = int(json_obj['numberOfVideos']) self.description = json_obj['description'] self.duration = int(json_obj['duration']) # These can be missing on from the /pages endpoints last_updated = json_obj.get('lastUpdated') self.last_updated = dateutil.parser.isoparse(last_updated) if last_updated else None created = json_obj.get('created') self.created = dateutil.parser.isoparse(created) if created else None public = json_obj.get('publicPlaylist') self.public = None if public is None else bool(public) self.popularity = json_obj.get('popularity') self.type = json_obj['type'] self.picture = json_obj['image'] self.square_picture = json_obj['squareImage'] promoted_artists = json_obj['promotedArtists'] self.promoted_artists = self.session.parse_artists(promoted_artists) if promoted_artists else None last_item_added_at = json_obj.get('lastItemAddedAt') self.last_item_added_at = dateutil.parser.isoparse(last_item_added_at) if last_item_added_at else None user_date_added = json_obj.get('dateAdded') self.user_date_added = dateutil.parser.isoparse(user_date_added) if user_date_added else None creator = json_obj.get('creator') if self.type == 'ARTIST' and creator and creator.get('id'): self.creator = self.session.parse_artist(creator) else: self.creator = self.session.parse_user(creator) if creator else None return copy.copy(self)
[docs] def factory(self): if self.creator and self.creator.id == self.session.user.id: return UserPlaylist(self.session, self.id) return self
[docs] def parse_factory(self, json_obj): self.parse(json_obj) return copy.copy(self.factory())
[docs] def tracks(self, limit=None, offset=0): """ Gets the playlists̈́' tracks from TIDAL. :param limit: The amount of items you want returned. :param offset: The index of the first item you want included. :return: A list of :class:`Tracks <.Track>` """ params = {'limit': limit, 'offset': offset} request = self.requests.request('GET', self._base_url % self.id + '/tracks', params=params) self._etag = request.headers['etag'] return self.requests.map_json(json_obj=request.json(), parse=self.session.parse_track)
[docs] def items(self, limit=100, offset=0): """ Fetches up to the first 100 items, including tracks and videos :param limit: The amount of items you want, up to 100. :param offset: The index of the first item you want returned :return: A list of :class:`Tracks<.Track>` and :class:`Videos<.Video>` """ params = {'limit': limit, 'offset': offset} request = self.requests.request('GET', self._base_url % self.id + '/items', params=params) self._etag = request.headers['etag'] return self.requests.map_json(request.json(), parse=self.session.parse_media)
[docs] def image(self, dimensions): """ A URL to a playlist picture :param dimensions: The width and height that want from the image :type dimensions: int :return: A url to the image Original sizes: 160x160, 320x320, 480x480, 640x640, 750x750, 1080x1080 """ if dimensions not in [160, 320, 480, 640, 750, 1080]: raise ValueError("Invalid resolution {0} x {0}".format(dimensions)) return self.session.config.image_url % (self.square_picture.replace('-', '/'), dimensions, dimensions)
[docs] def wide_image(self, width=1080, height=720): """ Create a url to a wider playlist image. :param width: The width of the image :param height: The height of the image :return: Returns a url to the image with the specified resolution Valid sizes: 160x107, 480x320, 750x500, 1080x720 """ if (width, height) not in [(160, 107), (480, 320), (750, 500), (1080, 720)]: raise ValueError("Invalid resolution {} x {}".format(width, height)) return self.session.config.image_url % (self.picture.replace('-', '/'), width, height)
[docs]class UserPlaylist(Playlist): def _reparse(self): request = self.requests.request('GET', self._base_url % self.id) self._etag = request.headers['etag'] self.requests.map_json(request.json(), parse=self.parse)
[docs] def edit(self, title=None, description=None): if not title: title = self.name if not description: description = self.description data = {'title': title, 'description': description} self.requests.request('POST', self._base_url % self.id, data=data)
[docs] def delete(self): self.requests.request('DELETE', self._base_url % self.id)
[docs] def add(self, media_ids): data = { 'onArtifactNotFound': 'SKIP', 'onDupes': 'SKIP', 'trackIds': ','.join(map(str, media_ids)) } params = {'limit': 100} headers = {'If-None-Match': self._etag} self.requests.request('POST', self._base_url % self.id + '/items', params=params, data=data, headers=headers) self._reparse()
[docs] def remove_by_index(self, index): headers = {'If-None-Match': self._etag} self.requests.request('DELETE', (self._base_url + '/items/%i') % (self.id, index), headers=headers)
def _calculate_id(self, media_id): i = 0 while i < self.num_tracks: items = self.items(100, i) for index, item in enumerate(items): if item.id == media_id: # Return the amount of items we have gone through plus the index in the last list. return index + i i += len(items)
[docs] def remove_by_id(self, media_id): index = self._calculate_id(media_id) self.remove_by_index(index)