-"""
-Audio file objects
-==================
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module provides convenient Python objects to keep track of audio
-file handles and metadata, and convert between different audio formats.
-"""
-
-import base64
-import datetime
-from io import BytesIO
-import logging
-import pathlib
-import re
-import subprocess
-from typing import Any, Union
-import urllib
-import warnings
-
-from mutagen import id3, flac, mp3, mp4, oggflac, oggopus, oggvorbis, wave
-
-from . import utility, FOUND_FFMPEG
-from .qobuz import _parse_performers
-
-if FOUND_FFMPEG:
- from . import FFMPEG_CODECS
-
-try:
- from PIL import Image
- FOUND_PILLOW = True
-except ModuleNotFoundError:
- FOUND_PILLOW = False
-
-__all__ = ["Audio", "FLACAudio", "MP3Audio", "MP4Audio", "OggAudio",
- "WAVEAudio"]
-
-class _ID3:
-
- """
- ID3 metadata container handler for MP3 and WAVE audio files.
-
- .. attention::
-
- This class should *not* be instantiated manually. Instead, use
- :class:`MP3Audio` or :class:`WAVEAudio` to process metadata for
- MP3 and WAVE audio files, respectively.
-
- Parameters
- ----------
- filename : `str`
- Audio filename.
-
- tags : `mutagen.id3.ID3`
- ID3 metadata.
- """
-
- _FIELDS = {
- # field: (ID3 frame, base class, typecasting function)
- "album": ("TALB", "text", None),
- "album_artist": ("TPE2", "text", None),
- "artist": ("TPE1", "text", None),
- "comment": ("COMM", "text", None),
- "compilation": ("TCMP", "text", lambda x: str(int(x))),
- "composer": ("TCOM", "text", None),
- "copyright": ("TCOP", "text", None),
- "date": ("TDRC", "text", None),
- "genre": ("TCON", "text", None),
- "isrc": ("TSRC", "text", None),
- "lyrics": ("USLT", "text", None),
- "tempo": ("TBPM", "text", str),
- "title": ("TIT2", "text", None),
- }
-
- def __init__(self, filename: str, tags: id3.ID3) -> None:
-
- """
- Create an ID3 tag handler.
- """
-
- self._filename = filename
- self._tags = tags
- self._from_file()
-
- def _from_file(self) -> None:
-
- """
- Get metadata from the ID3 tags embedded in the audio file.
- """
-
- for field, (frame, base, _) in self._FIELDS.items():
- value = self._tags.getall(frame)
- if value:
- value = ([sv for v in value for sv in getattr(v, base)]
- if len(value) > 1 else getattr(value[0], base))
- if list not in self._FIELDS_TYPES[field]:
- value = utility.format_multivalue(value, False,
- primary=True)
- if not isinstance(value, self._FIELDS_TYPES[field]):
- try:
- value = self._FIELDS_TYPES[field][0](value)
- except ValueError:
- logging.warning()
- continue
- else:
- if not isinstance(value[0], self._FIELDS_TYPES[field]):
- try:
- value = [self._FIELDS_TYPES[field][0](v)
- for v in value]
- except ValueError:
- continue
- if len(value) == 1:
- value = value[0]
- else:
- value = None
- setattr(self, field, value)
-
- if "TPOS" in self._tags:
- disc_number = getattr(self._tags.get("TPOS"), "text")[0]
- if "/" in disc_number:
- self.disc_number, self.disc_count = (
- int(d) for d in disc_number.split("/")
- )
- else:
- self.disc_number = int(disc_number)
- self.disc_count = None
- else:
- self.disc_number = self.disc_count = None
-
- if "TRCK" in self._tags:
- track_number = getattr(self._tags.get("TRCK"), "text")[0]
- if "/" in track_number:
- self.track_number, self.track_count = (
- int(t) for t in track_number.split("/")
- )
- else:
- self.track_number = int(track_number)
- self.track_count = None
- else:
- self.track_number = self.track_count = None
-
- artwork = self._tags.getall("APIC")
- if artwork:
- self.artwork = artwork[0]
- if self.artwork.type != 3 and len(artwork) > 1:
- for p in artwork:
- if p.type == 3:
- self.artwork = p
- break
- self._artwork_format = self.artwork.mime.split("/")[1]
- self.artwork = self.artwork.data
- else:
- self.artwork = self._artwork_format = None
-
- def write_metadata(self) -> None:
-
- """
- Write metadata to file.
- """
-
- for field, (frame, base, func) in self._FIELDS.items():
- value = getattr(self, field)
- if value:
- value = utility.format_multivalue(
- value, self._multivalue, sep=self._sep
- )
- self._tags.add(
- getattr(id3, frame)(
- **{base: func(value) if func else value}
- )
- )
-
- if "TXXX:comment" in self._tags:
- self._tags.delall("TXXX:comment")
-
- if (disc_number := getattr(self, "disc_number", None)):
- disc = str(disc_number)
- if (disc_count := getattr(self, "disc_count", None)):
- disc += f"/{disc_count}"
- self._tags.add(id3.TPOS(text=disc))
-
- if (track_number := getattr(self, "track_number", None)):
- track = str(track_number)
- if (track_count := getattr(self, "track_count", None)):
- track += f"/{track_count}"
- self._tags.add(id3.TRCK(text=track))
-
- if self.artwork:
- IMAGE_FORMATS = dict.fromkeys(
- ["jpg", "jpeg", "jpe", "jif", "jfif", "jfi"], "image/jpeg"
- ) | {"png": "image/png"}
-
- if isinstance(self.artwork, str):
- with urllib.request.urlopen(self.artwork) \
- if "http" in self.artwork \
- else open(self.artwork, "rb") as f:
- self.artwork = f.read()
- self._tags.add(
- id3.APIC(data=self.artwork,
- mime=IMAGE_FORMATS[self._artwork_format])
- )
-
- self._tags.save()
-
-class _VorbisComment:
-
- """
- Vorbis comment handler for FLAC and Ogg audio files.
-
- .. attention::
-
- This class should *not* be instantiated manually. Instead, use
- :class:`FLACAudio` or :class:`OggAudio` to process metadata for
- FLAC and Ogg audio files, respectively.
-
- Parameters
- ----------
- filename : `str`
- Audio filename.
-
- tags : `mutagen.id3.ID3`
- ID3 metadata.
- """
-
- _FIELDS = {
- # field: (Vorbis comment key, typecasting function)
- "album": ("album", None),
- "album_artist": ("albumartist", None),
- "artist": ("artist", None),
- "comment": ("description", None),
- "composer": ("composer", None),
- "copyright": ("copyright", None),
- "date": ("date", None),
- "genre": ("genre", None),
- "isrc": ("isrc", None),
- "lyrics": ("lyrics", None),
- "tempo": ("bpm", str),
- "title": ("title", None),
- }
- _FIELDS_SPECIAL = {
- "compilation": ("compilation", lambda x: str(int(x))),
- "disc_number": ("discnumber", str),
- "disc_count": ("disctotal", str),
- "track_number": ("tracknumber", str),
- "track_count": ("tracktotal", str)
- }
-
- def __init__(self, filename: str, tags: id3.ID3) -> None:
-
- """
- Create a Vorbis comment handler.
- """
-
- self._filename = filename
- self._tags = tags
- self._from_file()
-
- def _from_file(self) -> None:
-
- """
- Get metadata from the tags embedded in the FLAC audio file.
- """
-
- for field, (key, _) in self._FIELDS.items():
- value = self._tags.get(key)
- if value:
- if list not in self._FIELDS_TYPES[field]:
- value = utility.format_multivalue(value, False,
- primary=True)
- if type(value) not in self._FIELDS_TYPES[field]:
- try:
- value = self._FIELDS_TYPES[field][0](value)
- except ValueError:
- continue
- else:
- if type(value[0]) not in self._FIELDS_TYPES[field]:
- try:
- value = [self._FIELDS_TYPES[field][0](v)
- for v in value]
- except ValueError:
- continue
- if len(value) == 1:
- value = value[0]
- else:
- value = None
- setattr(self, field, value)
-
- self.compilation = bool(int(self._tags.get("compilation")[0])) \
- if "compilation" in self._tags else None
-
- if "discnumber" in self._tags:
- disc_number = self._tags.get("discnumber")[0]
- if "/" in disc_number:
- self.disc_number, self.disc_count = (
- int(d) for d in disc_number.split("/")
- )
- else:
- self.disc_number = int(disc_number)
- self.disc_count = self._tags.get("disctotal")
- if self.disc_count:
- self.disc_count = int(self.disc_count[0])
- else:
- self.disc_number = self.disc_count = None
-
- if "tracknumber" in self._tags:
- track_number = self._tags.get("tracknumber")[0]
- if "/" in track_number:
- self.track_number, self.track_count = (
- int(t) for t in track_number.split("/")
- )
- else:
- self.track_number = int(track_number)
- self.track_count = self._tags.get("tracktotal")
- if self.track_count:
- self.track_count = int(self.track_count[0])
- else:
- self.track_number = self.track_count = None
-
- if hasattr(self._handle, "pictures") and self._handle.pictures:
- self.artwork = self._handle.pictures[0].data
- self._artwork_format = self._handle.pictures[0].mime.split("/")[1]
- elif "metadata_block_picture" in self._tags:
- IMAGE_FILE_SIGS = {
- "jpg": b"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01",
- "png": b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
- }
- self.artwork = base64.b64decode(
- self._tags["metadata_block_picture"][0].encode()
- )
- for img_fmt, file_sig in IMAGE_FILE_SIGS.items():
- if file_sig in self.artwork:
- self.artwork = self.artwork[
- re.search(file_sig, self.artwork).span()[0]:
- ]
- self._artwork_format = img_fmt
- else:
- self.artwork = self._artwork_format = None
-
- def write_metadata(self) -> None:
-
- """
- Write metadata to file.
- """
-
- for field, (key, func) in (self._FIELDS | self._FIELDS_SPECIAL).items():
- value = getattr(self, field)
- if value:
- value = utility.format_multivalue(
- value, self._multivalue, sep=self._sep
- )
- self._tags[key] = func(value) if func else value
-
- if self.artwork:
- artwork = flac.Picture()
- artwork.type = id3.PictureType.COVER_FRONT
- artwork.mime = f"image/{self._artwork_format}"
- if isinstance(self.artwork, str):
- with urllib.request.urlopen(self.artwork) \
- if "http" in self.artwork \
- else open(self.artwork, "rb") as f:
- self.artwork = f.read()
- artwork.data = self.artwork
- try:
- self._handle.clear_pictures()
- self._handle.add_picture(artwork)
- except ValueError:
- self._tags["metadata_block_picture"] = base64.b64encode(
- artwork.write()
- ).decode()
-
- self._handle.save()
-
-
-[docs]
-class Audio:
-
- r"""
- Generic audio file handler.
-
- Subclasses for specific audio containers or formats include
-
- * :class:`FLACAudio` for audio encoded using the Free
- Lossless Audio Codec (FLAC),
- * :class:`MP3Audio` for audio encoded and stored in the MPEG Audio
- Layer III (MP3) format,
- * :class:`MP4Audio` for audio encoded in the Advanced
- Audio Coding (AAC) format, encoded using the Apple Lossless
- Audio Codec (ALAC), or stored in a MPEG-4 Part 14 (MP4, M4A)
- container,
- * :class:`OggAudio` for Opus or Vorbis audio stored in an Ogg file,
- and
- * :class:`WAVEAudio` for audio encoded using linear pulse-code
- modulation (LPCM) and in the Waveform Audio File Format (WAVE).
-
- .. note::
-
- This class can instantiate a specific file handler from the list
- above for an audio file by examining its file extension. However,
- there may be instances when this detection fails, especially when
- the audio codec and format combination is rarely seen. As such,
- it is always best to directly use one of the subclasses above to
- create a file handler for your audio file when its audio codec
- and format are known.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- Audio filename or path.
-
- pattern : `tuple`, keyword-only, optional
- Regular expression search pattern and the corresponding metadata
- field(s).
-
- .. container::
-
- **Valid values**:
-
- The supported metadata fields are
-
- * :code:`"artist"` for the track artist,
- * :code:`"title"` for the track title, and
- * :code:`"track_number"` for the track number.
-
- **Examples**:
-
- * :code:`("(.*) - (.*)", ("artist", "title"))` matches
- filenames like "Taylor Swift - Cruel Summer.flac".
- * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
- filenames like "04 - The Man.m4a".
- * :code:`("(\\d*) (.*)", ("track_number", "title"))` matches
- filenames like "13 You Need to Calm Down.mp3".
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate the first :math:`n - 1` values, and the second
- :code:`str` is used to append the final value.
-
- Attributes
- ----------
- album : `str`
- Album title.
-
- album_artist : `str` or `list`
- Album artist(s).
-
- artist : `str` or `list`
- Artist(s).
-
- artwork : `bytes` or `str`
- Byte-representation of, URL leading to, or filename of file
- containing the cover artwork.
-
- bit_depth : `int`
- Bits per sample.
-
- bitrate : `int`
- Bitrate in bytes per second (B/s).
-
- channel_count : `int`
- Number of audio channels.
-
- codec : `str`
- Audio codec.
-
- comment : `str`
- Comment(s).
-
- compilation : `bool`
- Whether the album is a compilation of songs by various artists.
-
- composer : `str` or `list`
- Composers, lyrics, and/or writers.
-
- copyright : `str`
- Copyright information.
-
- date : `str`
- Release date.
-
- disc_number : `int`
- Disc number.
-
- disc_count : `int`
- Total number of discs.
-
- genre : `str` or `list`
- Genre.
-
- isrc : `str`
- International Standard Recording Code (ISRC).
-
- lyrics : `str`
- Lyrics.
-
- sample_rate : `int`
- Sample rate in Hz.
-
- tempo : `int`
- Tempo in beats per minute (bpm).
-
- title : `str`
- Track title.
-
- track_number : `int`
- Track number.
-
- track_count : `int`
- Total number of tracks.
- """
-
- _FIELDS_TYPES = {
- "_artwork_format": (str,),
- "album": (str,),
- "album_artist": (str, list),
- "artist": (str, list),
- "artwork": (bytes, str),
- "comment": (str,),
- "compilation": (bool,),
- "composer": (str, list),
- "copyright": (str,),
- "date": (str,),
- "disc_number": (int,),
- "disc_count": (int,),
- "genre": (str, list),
- "isrc": (str,),
- "lyrics": (str,),
- "tempo": (int,),
- "title": (str,),
- "track_number": (int,),
- "track_count": (int,)
- }
-
- def __init__(
- self, file: Union[str, pathlib.Path], *,
- pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
- sep: Union[str, list[str]] = (", ", " & ")) -> None:
-
- """
- Instantiate an audio file handler.
- """
-
- self._file = pathlib.Path(file).resolve()
- self._pattern = pattern
- self._multivalue = multivalue
- self._sep = sep
-
- def __new__(cls, *args, **kwargs) -> None:
-
- """
- Create an audio file handler.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- Audio file.
- """
-
- if cls == Audio:
- file = kwargs.get("file")
- if file is None:
- file = args[0]
- file = pathlib.Path(file)
- if not file.is_file():
- raise FileNotFoundError(f"'{file}' not found.")
-
- ext = file.suffix[1:].lower()
- for a in Audio.__subclasses__():
- if ext in a._EXTENSIONS:
- return a(*args, **kwargs)
- raise TypeError(f"'{file}' has an unsupported audio format.")
-
- return super(Audio, cls).__new__(cls)
-
- def _from_filename(self) -> None:
-
- """
- Get track information from the filename.
- """
-
- if self._pattern:
- groups = re.findall(self._pattern[0], self._file.stem)
- if groups:
- missing = tuple(k in {"artist", "title", "track_number"}
- and getattr(self, k) is None
- for k in self._pattern[1])
- for flag, attr, val in zip(missing, self._pattern[1], groups[0]):
- if flag:
- setattr(self, attr, self._FIELDS_TYPES[attr][0](val))
-
-
-[docs]
- def convert(
- self, codec: str, container: str = None, options: str = None, *,
- filename: str = None, preserve: bool = True) -> None:
-
- """
- Convert the current audio file to another format.
-
- .. admonition:: Software dependency
-
- Requires `FFmpeg <https://ffmpeg.org/>`_.
-
- .. note::
-
- The audio file handler is automatically updated to reflect
- the new audio file format. For example, converting a FLAC
- audio file to an ALAC audio file will change the file handler
- from a :class:`FLACAudio` object to an :class:`MP4Audio`
- object.
-
- Parameters
- ----------
- codec : `str`
- New audio codec or coding format.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"aac"`, :code:`"m4a"`, :code:`"mp4"`, or
- :code:`"mp4a"` for lossy AAC audio.
- * :code:`"alac"` for lossless ALAC audio.
- * :code:`"flac"` for lossless FLAC audio.
- * :code:`"mp3"` for lossy MP3 audio.
- * :code:`"ogg"` or :code:`"opus"` for lossy Opus audio
- * :code:`"vorbis"` for lossy Vorbis audio.
- * :code:`"lpcm"`, :code:`"wav"`, or :code:`"wave"` for
- lossless LPCM audio.
-
- container : `str`, optional
- New audio file container. If not specified, the best
- container is determined based on `codec`.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"flac"` for a FLAC audio container, which only
- supports FLAC audio.
- * :code:`"m4a"`, :code:`"mp4"`, or :code:`"mp4a"` for
- a MP4 audio container, which supports AAC and ALAC
- audio.
- * :code:`"mp3"` for a MP3 audio container, which only
- supports MP3 audio.
- * :code:`"ogg"` for an Ogg audio container, which
- supports FLAC, Opus, and Vorbis audio.
- * :code:`"wav"` or :code:`"wave"` for an WAVE audio
- container, which only supports LPCM audio.
-
- options : `str`, optional
- FFmpeg command-line options, excluding the input and output
- files, the :code:`-y` flag (to overwrite files), and the
- :code:`-c:v copy` argument (to preserve cover art for
- containers that support it).
-
- .. container::
-
- **Defaults**:
-
- * AAC audio: :code:`"-c:a aac -b:a 256k"` (or
- :code:`"-c:a libfdk_aac -b:a 256k"` if FFmpeg was
- compiled with :code:`--enable-libfdk-aac`)
- * ALAC audio: :code:`"-c:a alac"`
- * FLAC audio: :code:`"-c:a flac"`
- * MP3 audio: :code:`"-c:a libmp3lame -q:a 0"`
- * Opus audio: :code:`"-c:a libopus -b:a 256k -vn"`
- * Vorbis audio:
- :code:`"-c:a vorbis -strict experimental -vn"` (or
- :code:`"-c:a libvorbis -vn"` if FFmpeg was compiled
- with :code:`--enable-libvorbis`)
- * WAVE audio: :code:`"-c:a pcm_s16le"` or
- :code:`"-c:a pcm_s24le"`, depending on the bit depth of
- the original audio file.
-
- filename : `str`, keyword-only, optional
- Filename of the converted audio file. If not provided, the
- filename of the original audio file, but with the
- appropriate new extension appended, is used.
-
- preserve : `bool`, keyword-only, default: :code:`True`
- Determines whether the original audio file is kept.
- """
-
- if not FOUND_FFMPEG:
- emsg = ("Audio conversion is unavailable because FFmpeg "
- "was not found.")
- raise RuntimeError(emsg)
-
- _codec = (codec.capitalize() if codec in {"opus", "vorbis"}
- else codec.upper())
- codec = codec.lower()
- if codec in {"m4a", "mp4", "mp4a"}:
- codec = "aac"
- elif codec == "ogg":
- codec = "opus"
- elif codec in "wave":
- codec = "lpcm"
-
- if container:
- container = container.lower()
- if container == "m4a":
- container = "mp4"
- elif container == "wave":
- container = "wav"
-
- try:
- acls = next(a for a in Audio.__subclasses__()
- if codec in a._CODECS
- and container in a._EXTENSIONS)
- except StopIteration:
- emsg = (f"{_codec} audio is incompatible with "
- f"the {container.upper()} container.")
- raise RuntimeError(emsg)
- else:
- try:
- acls = next(a for a in Audio.__subclasses__()
- if codec in a._CODECS)
- container = acls._EXTENSIONS[0]
- except StopIteration:
- raise RuntimeError(f"The '{_codec}' codec is not supported.")
-
- if ("mp4" if codec == "aac" else codec) in self.codec \
- and isinstance(self, acls):
- wmsg = (f"'{self._file}' already has {_codec} "
- f"audio in a {container.upper()} container. "
- "Re-encoding may lead to quality degradation from "
- "generation loss.")
- logging.warning(wmsg)
-
- ext = f".{acls._EXTENSIONS[0]}"
- if filename is None:
- filename = self._file.with_suffix(ext)
- else:
- if isinstance(filename, str):
- if "/" not in filename:
- filename = f"{self._file.parent}/{filename}"
- filename = pathlib.Path(filename).resolve()
- if filename.suffix != ext:
- filename = filename.with_suffix(ext)
- filename.parent.mkdir(parents=True, exist_ok=True)
- if self._file == filename:
- filename = filename.with_stem(f"{filename.stem}_")
-
- if options is None:
- if codec == "lpcm":
- options = acls._CODECS[codec]["ffmpeg"].format(
- self.bit_depth if hasattr(self, "bit_depth") else 16
- )
- else:
- options = acls._CODECS[codec]["ffmpeg"]
-
- subprocess.run(
- f'ffmpeg -y -i "{self._file}" {options} -loglevel error '
- f'-stats "{filename}"',
- shell=True
- )
- if not preserve:
- self._file.unlink()
-
- obj = acls(filename)
- self.__class__ = obj.__class__
- self.__dict__ = obj.__dict__ | {
- key: value for (key, value) in self.__dict__.items()
- if key in self._FIELDS_TYPES
- }
-
-
-
-[docs]
- def set_metadata_using_itunes(
- self, data: dict[str, Any], *, album_data: dict[str, Any] = None,
- artwork_size: Union[int, str] = 1400, artwork_format: str = "jpg",
- overwrite: bool = False) -> None:
-
- """
- Populate tags using data retrieved from the iTunes Search API.
-
- Parameters
- ----------
- data : `dict`
- Information about the track in JSON format obtained using
- the iTunes Search API via
- :meth:`minim.itunes.SearchAPI.search` or
- :meth:`minim.itunes.SearchAPI.lookup`.
-
- album_data : `dict`, keyword-only, optional
- Information about the track's album in JSON format obtained
- using the iTunes Search API via
- :meth:`minim.itunes.SearchAPI.search` or
- :meth:`minim.itunes.SearchAPI.lookup`. If not provided,
- album artist and copyright information is unavailable.
-
- artwork_size : `int` or `str`, keyword-only, default: :code:`1400`
- Resized artwork size in pixels. If
- :code:`artwork_size="raw"`, the uncompressed high-resolution
- image is retrieved, regardless of size.
-
- artwork_format : `str`, keyword-only, :code:`{"jpg", "png"}`
- Artwork file format. If :code:`artwork_size="raw"`, the file
- format of the uncompressed high-resolution image takes
- precedence.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether existing metadata should be overwritten.
- """
-
- if self.album is None or overwrite:
- self.album = data["collectionName"]
- if self.artist is None or overwrite:
- self.artist = data["artistName"]
- if self.artwork is None or overwrite:
- self.artwork = data["artworkUrl100"]
- if self.artwork:
- if artwork_size == "raw":
- if "Feature" in self.artwork:
- self.artwork = (
- "https://a5.mzstatic.com/us/r1000/0"
- f"/{re.search(r'Feature.*?(jpg|png|tif)(?=/|$)', self.artwork)[0]}"
- )
- elif "Music" in self.artwork:
- self.artwork = (
- "https://a5.mzstatic.com/"
- f"{re.search(r'Music.*?(jpg|png|tif)(?=/|$)', self.artwork)[0]}"
- )
- self._artwork_format = pathlib.Path(self.artwork).suffix[1:]
- else:
- self.artwork = self.artwork.replace(
- "100x100bb.jpg",
- f"{artwork_size}x{artwork_size}bb.{artwork_format}"
- )
- self._artwork_format = artwork_format
- with urllib.request.urlopen(self.artwork) as r:
- self.artwork = r.read()
- if self._artwork_format == "tif":
- if FOUND_PILLOW:
- with Image.open(BytesIO(self.artwork)) as a:
- with BytesIO() as b:
- a.save(b, format="png")
- self.artwork = b.getvalue()
- self._artwork_format = "png"
- else:
- wmsg = ("The Pillow library is required to process "
- "TIFF images, but was not found. No artwork "
- "will be embedded for the current track.")
- warnings.warn(wmsg)
- self.artwork = self._artwork_format = None
- if self.compilation is None or overwrite:
- self.compilation = self.album_artist == "Various Artists"
- if "releaseDate" in data and (self.date is None or overwrite):
- self.date = data["releaseDate"]
- if self.disc_number is None or overwrite:
- self.disc_number = data["discNumber"]
- if self.disc_count is None or overwrite:
- self.disc_count = data["discCount"]
- if self.genre is None or overwrite:
- self.genre = data["primaryGenreName"]
- if self.title is None or overwrite:
- self.title = max(data["trackName"], data["trackCensoredName"])
- if self.track_number is None or overwrite:
- self.track_number = data["trackNumber"]
- if self.track_count is None or overwrite:
- self.track_count = data["trackCount"]
-
- if album_data:
- if self.album_artist is None or overwrite:
- self.album_artist = album_data["artistName"]
- if self.copyright or overwrite:
- self.copyright = album_data["copyright"]
-
-
-
-[docs]
- def set_metadata_using_qobuz(
- self, data: dict[str, Any], *, artwork_size: str = "large",
- comment: str = None, overwrite: bool = False) -> None:
-
- """
- Populate tags using data retrieved from the Qobuz API.
-
- Parameters
- ----------
- data : `dict`
- Information about the track in JSON format obtained using
- the Qobuz API via :meth:`minim.qobuz.PrivateAPI.get_track`
- or :meth:`minim.qobuz.PrivateAPI.search`.
-
- artwork_size : `str`, keyword-only, default: :code:`"large"`
- Artwork size.
-
- **Valid values**: :code:`"large"`, :code:`"small"`, or
- :code:`"thumbnail"`.
-
- comment : `str`, keyword-only, optional
- Comment or description.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether existing metadata should be overwritten.
- """
-
- if self.album is None or overwrite:
- self.album = data["album"]["title"]
- if (album_artists := data["album"].get("artists")):
- album_feat_artist = [a["name"] for a in album_artists
- if "featured-artist" in a["roles"]]
- if album_feat_artist and "feat." not in self.album:
- self.album += (
- " [feat. {}]" if "(" in self.album else " (feat. {})"
- ).format(
- utility.format_multivalue(album_feat_artist, False)
- )
- if data["album"]["version"]:
- self.album += (
- " [{}]" if "(" in self.album else " ({})"
- ).format(data['album']['version'])
- self.album = self.album.replace(" ", " ")
- if self.album_artist is None or overwrite:
- if (album_artists := data["album"].get("artists")):
- album_artist = [a["name"] for a in album_artists
- if "main-artist" in a["roles"]]
- album_main_artist = data["album"]["artist"]["name"]
- if album_main_artist in album_artist:
- if (i := album_artist.index(album_main_artist)
- if album_main_artist in album_artist else 0) != 0:
- album_artist.insert(0, album_artist.pop(i))
- self.album_artist = album_artist
- else:
- self.album_artist = album_main_artist
- else:
- self.album_artist = data["album"]["artist"]["name"]
-
- credits = _parse_performers(
- data["performers"],
- roles=["MainArtist", "FeaturedArtist", "Composers"]
- )
- if self.artist is None or overwrite:
- self.artist = credits.get("main_artist") or data["performer"]["name"]
- if self.artwork is None or overwrite:
- if artwork_size not in \
- (ARTWORK_SIZES := {"large", "small", "thumbnail"}):
- emsg = (f"Invalid artwork size '{artwork_size}'. "
- f"Valid values: {ARTWORK_SIZES}.")
- raise ValueError(emsg)
- self.artwork = data["album"]["image"][artwork_size]
- self._artwork_format = pathlib.Path(self.artwork).suffix[1:]
- if self.comment is None or overwrite:
- self.comment = comment
- if self.composer is None or overwrite:
- self.composer = (
- credits.get("composers")
- or (data["composer"]["name"] if hasattr(data, "composer")
- else None)
- )
- if self.copyright is None or overwrite:
- self.copyright = data["album"].get("copyright")
- if self.date is None or overwrite:
- self.date = min(
- datetime.datetime.utcfromtimestamp(dt) if isinstance(dt, int)
- else datetime.datetime.strptime(dt, "%Y-%m-%d") if isinstance(dt, str)
- else datetime.datetime.max for dt in (
- data.get(k) for k in {
- "release_date_original",
- "release_date_download",
- "release_date_stream",
- "release_date_purchase",
- "purchasable_at",
- "streamable_at"
- }
- )
- ).strftime('%Y-%m-%dT%H:%M:%SZ')
- if self.disc_number is None or overwrite:
- self.disc_number = data["media_number"]
- if self.disc_count is None or overwrite:
- self.disc_count = data["album"]["media_count"]
- if self.genre is None or overwrite:
- self.genre = data["album"]["genre"]["name"]
- if self.isrc is None or overwrite:
- self.isrc = data["isrc"]
- if self.title is None or overwrite:
- self.title = data["title"]
- if (feat_artist := credits.get("featured_artist")) \
- and "feat." not in self.title:
- self.title += (
- " [feat. {}]" if "(" in self.title else " (feat. {})"
- ).format(
- utility.format_multivalue(feat_artist, False)
- )
- if data["version"]:
- self.title += (" [{}]" if "(" in self.title
- else " ({})").format(data['version'])
- self.title = self.title.replace(" ", " ")
- if self.track_number is None or overwrite:
- self.track_number = data["track_number"]
- if self.track_count is None or overwrite:
- self.track_count = data["album"]["tracks_count"]
-
- if data["album"].get("release_type") == "single" \
- and self.album == self.title:
- self.album += " - Single"
- self.album_artist = self.artist = max(
- self.artist, self.album_artist, key=len
- )
-
-
-
-[docs]
- def set_metadata_using_spotify(
- self, data: dict[str, Any], *,
- audio_features: dict[str, Any] = None,
- lyrics: Union[str, dict[str, Any]] = None, overwrite: bool = False
- ) -> None:
-
- """
- Populate tags using data retrieved from the Spotify Web API
- and Spotify Lyrics service.
-
- Parameters
- ----------
- data : `dict`
- Information about the track in JSON format obtained using
- the Spotify Web API via
- :meth:`minim.spotify.WebAPI.get_track`.
-
- audio_features : `dict`, keyword-only, optional
- Information about the track's audio features obtained using
- the Spotify Web API via
- :meth:`minim.spotify.WebAPI.get_track_audio_features`.
- If not provided, tempo information is unavailable.
-
- lyrics : `str` or `dict`, keyword-only
- Information about the track's formatted or time-synced
- lyrics obtained using the Spotify Lyrics service via
- :meth:`minim.spotify.PrivateLyricsService.get_lyrics`. If not
- provided, lyrics are unavailable.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether existing metadata should be overwritten.
- """
-
- if self.album is None or overwrite:
- self.album = data["album"]["name"]
- if data["album"]["album_type"] == "single":
- self.album += " - Single"
- if self.album_artist is None or overwrite:
- self.album_artist = [a["name"] for a in data["album"]["artists"]]
- if self.artist is None or overwrite:
- self.artist = [a["name"] for a in data["artists"]]
- if self.artwork is None or overwrite:
- with urllib.request.urlopen(data["album"]["images"][0]["url"]) as r:
- self.artwork = r.read()
- self._artwork_format = "jpg"
- if self.compilation is None or overwrite:
- self.compilation = data["album"]["album_type"] == "compilation"
- if self.date is None or overwrite:
- self.date = data["album"]["release_date"]
- if self.disc_number is None or overwrite:
- self.disc_number = data["disc_number"]
- if self.isrc is None or overwrite:
- self.isrc = data["external_ids"]["isrc"]
- if (self.lyrics is None or overwrite) and lyrics:
- self.lyrics = lyrics if isinstance(lyrics, str) \
- else "\n".join(line["words"]
- for line in lyrics["lyrics"]["lines"])
- if (self.tempo is None or overwrite) and audio_features:
- self.tempo = round(audio_features["tempo"])
- if self.title is None or overwrite:
- self.title = data["name"]
- if self.track_number is None or overwrite:
- self.track_number = data["track_number"]
- if self.track_count is None or overwrite:
- self.track_count = data["album"]["total_tracks"]
-
-
-
-[docs]
- def set_metadata_using_tidal(
- self, data: dict[str, Any], *, album_data: dict[str, Any] = None,
- artwork_size: int = 1280,
- composers: Union[str, list[str], dict[str, Any]] = None,
- lyrics: dict[str, Any] = None, comment: str = None,
- overwrite: bool = False) -> None:
-
- """
- Populate tags using data retrieved from the TIDAL API.
-
- Parameters
- ----------
- data : `dict`
- Information about the track in JSON format obtained using
- the TIDAL API via :meth:`minim.tidal.API.get_track`,
- :meth:`minim.tidal.API.search`,
- :meth:`minim.tidal.PrivateAPI.get_track`, or
- :meth:`minim.tidal.PrivateAPI.search`.
-
- album_data : `dict`, keyword-only, optional
- Information about the track's album in JSON format obtained
- using the TIDAL API via :meth:`minim.tidal.API.get_album`,
- :meth:`minim.tidal.API.search`,
- :meth:`minim.tidal.PrivateAPI.get_album`, or
- :meth:`minim.tidal.PrivateAPI.search`. If not provided,
- album artist and disc and track numbering information is
- unavailable.
-
- artwork_size : `int`, keyword-only, default: :code:`1280`
- Maximum artwork size in pixels.
-
- **Valid values**: `artwork_size` should be between
- :code:`80` and :code:`1280`.
-
- composers : `str`, `list`, or `dict`, keyword-only, optional
- Information about the track's composers in a formatted
- `str`, a `list`, or a `dict` obtained using the TIDAL API
- via :meth:`minim.tidal.PrivateAPI.get_track_composers`,
- :meth:`minim.tidal.PrivateAPI.get_track_contributors`, or
- :meth:`minim.tidal.PrivateAPI.get_track_credits`. If not
- provided, songwriting credits are unavailable.
-
- lyrics : `str` or `dict`, keyword-only, optional
- The track's lyrics obtained using the TIDAL API via
- :meth:`minim.tidal.PrivateAPI.get_track_lyrics`.
-
- comment : `str`, keyword-only, optional
- Comment or description.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether existing metadata should be overwritten.
- """
-
- if "resource" in data:
- data = data["resource"]
- if self.album is None or overwrite:
- self.album = data["album"]["title"]
- if (self.comment is None or overwrite) and comment:
- self.comment = comment
- if (self.composer is None or overwrite) and composers:
- COMPOSER_TYPES = {"Composer", "Lyricist", "Writer"}
- if isinstance(composers, dict):
- self.composer = sorted({c["name"] for c in composers["items"]
- if c["role"] in COMPOSER_TYPES})
- elif isinstance(composers[0], dict):
- self.composer = sorted({
- c["name"] for r in composers for c in r["contributors"]
- if r["type"] in COMPOSER_TYPES
- })
- else:
- self.composer = composers
- if self.copyright is None or overwrite:
- self.copyright = data["copyright"]
- if self.disc_number is None or overwrite:
- self.disc_number = data["volumeNumber"]
- if self.isrc is None or overwrite:
- self.isrc = data["isrc"]
- if (self.lyrics is None or overwrite) and lyrics:
- self.lyrics = lyrics if isinstance(lyrics, str) \
- else lyrics["lyrics"]
- if self.title is None or overwrite:
- self.title = data["title"]
- if self.track_number is None or overwrite:
- self.track_number = data["trackNumber"]
-
- if "artifactType" in data:
- if self.artist is None or overwrite:
- self.artist = [a["name"] for a in data["artists"] if a["main"]]
- if self.artwork is None or overwrite:
- image_urls = sorted(data["album"]["imageCover"],
- key=lambda x: x["width"], reverse=True)
- self.artwork = (
- image_urls[-1]["url"]
- if artwork_size < image_urls[-1]["width"]
- else next(u["url"] for u in image_urls
- if u["width"] <= artwork_size)
- )
- self._artwork_format = pathlib.Path(self.artwork).suffix[1:]
- else:
- if self.artist is None or overwrite:
- self.artist = [a["name"] for a in data["artists"]
- if a["type"] == "MAIN"]
- if self.artwork is None or overwrite:
- artwork_size = (
- 80 if artwork_size < 80
- else next(s for s in [1280, 1080, 750, 640, 320, 160, 80]
- if s <= artwork_size)
- )
- self.artwork = ("https://resources.tidal.com/images"
- f"/{data['album']['cover'].replace('-', '/')}"
- f"/{artwork_size}x{artwork_size}.jpg")
- self._artwork_format = "jpg"
- if self.date is None or overwrite:
- self.date = f"{data['streamStartDate'].split('.')[0]}Z"
-
- if album_data:
- if self.copyright is None or overwrite:
- self.copyright = album_data["copyright"]
- if self.disc_count is None or overwrite:
- self.disc_count = album_data["numberOfVolumes"]
- if self.track_count is None or overwrite:
- self.track_count = album_data["numberOfTracks"]
-
- if "barcodeId" in album_data:
- if self.album_artist is None or overwrite:
- self.album_artist = [a["name"] for a in album_data["artists"]
- if a["main"]]
- if self.date is None or overwrite:
- self.date = f"{album_data['releaseDate']}T00:00:00Z"
- else:
- if self.album_artist is None or overwrite:
- self.album_artist = [
- a["name"] for a in album_data["artists"]
- if a["type"] == "MAIN"
- ]
-
-
-
-
-[docs]
-class FLACAudio(Audio, _VorbisComment):
-
- r"""
- FLAC audio file handler.
-
- .. seealso::
-
- For a full list of attributes and their descriptions, see
- :class:`Audio`.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- FLAC audio filename or path.
-
- pattern : `tuple`, keyword-only, optional
- Regular expression search pattern and the corresponding metadata
- field(s).
-
- .. container::
-
- **Valid values**:
-
- The supported metadata fields are
-
- * :code:`"artist"` for the track artist,
- * :code:`"title"` for the track title, and
- * :code:`"track_number"` for the track number.
-
- **Examples**:
-
- * :code:`("(.*) - (.*)", ("artist", "title"))` matches
- filenames like "Taylor Swift - Fearless.flac".
- * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
- filenames like "03 - Love Story.flac".
- * :code:`("(\\d*) (.*)", ("track_number", "title"))` matches
- filenames like "06 You Belong with Me.flac".
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate the first :math:`n - 1` values, and the second
- :code:`str` is used to append the final value.
- """
-
- _CODECS = {"flac": {"ffmpeg": "-c:a flac -c:v copy"}}
- _EXTENSIONS = ["flac"]
-
- def __init__(
- self, file: Union[str, pathlib.Path], *,
- pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
- sep: Union[str, list[str]] = (", ", " & ")) -> None:
-
- """
- Create a FLAC audio file handler.
- """
-
- Audio.__init__(self, file, pattern=pattern, multivalue=multivalue,
- sep=sep)
- self._handle = flac.FLAC(file)
- if self._handle.tags is None:
- self._handle.add_tags()
- _VorbisComment.__init__(self, self._file.name, self._handle.tags)
- self._from_filename()
-
- self.bit_depth = self._handle.info.bits_per_sample
- self.bitrate = self._handle.info.bitrate
- self.channel_count = self._handle.info.channels
- self.codec = "flac"
- self.sample_rate = self._handle.info.sample_rate
-
-
-
-[docs]
-class MP3Audio(Audio, _ID3):
-
- r"""
- MP3 audio file handler.
-
- .. seealso::
-
- For a full list of attributes and their descriptions, see
- :class:`Audio`.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- MP3 audio filename or path.
-
- pattern : `tuple`, keyword-only, optional
- Regular expression search pattern and the corresponding metadata
- field(s).
-
- .. container::
-
- **Valid values**:
-
- The supported metadata fields are
-
- * :code:`"artist"` for the track artist,
- * :code:`"title"` for the track title, and
- * :code:`"track_number"` for the track number.
-
- **Examples**:
-
- * :code:`("(.*) - (.*)", ("artist", "title"))` matches
- filenames like "Taylor Swift - Red.mp3".
- * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
- filenames like "04 - I Knew You Were Trouble.mp3".
- * :code:`("(\\d*) (.*)", ("track_number", "title"))` matches
- filenames like "06 22.mp3".
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate the first :math:`n - 1` values, and the second
- :code:`str` is used to append the final value.
- """
-
- _CODECS = {"mp3": {"ffmpeg": "-c:a libmp3lame -q:a 0 -c:v copy"}}
- _EXTENSIONS = ["mp3"]
-
- def __init__(
- self, file: Union[str, pathlib.Path], *,
- pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
- sep: Union[str, list[str]] = (", ", " & ")) -> None:
-
- """
- Create a MP3 audio file handler.
- """
-
- _handle = mp3.MP3(file)
- _handle.tags.filename = str(file)
- Audio.__init__(self, file, pattern=pattern, multivalue=multivalue,
- sep=sep)
- _ID3.__init__(self, self._file.name, _handle.tags)
- self._from_filename()
-
- self.bit_depth = None
- self.bitrate = _handle.info.bitrate
- self.channel_count = _handle.info.channels
- self.codec = "mp3"
- self.sample_rate = _handle.info.sample_rate
-
-
-
-[docs]
-class MP4Audio(Audio):
-
- r"""
- MP4 audio file handler.
-
- .. seealso::
-
- For a full list of attributes and their descriptions, see
- :class:`Audio`.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- MP4 audio filename or path.
-
- pattern : `tuple`, keyword-only, optional
- Regular expression search pattern and the corresponding metadata
- field(s).
-
- .. container::
-
- **Valid values**:
-
- The supported metadata fields are
-
- * :code:`"artist"` for the track artist,
- * :code:`"title"` for the track title, and
- * :code:`"track_number"` for the track number.
-
- **Examples**:
-
- * :code:`("(.*) - (.*)", ("artist", "title"))` matches
- filenames like "Taylor Swift - Mine.m4a".
- * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
- filenames like "04 - Speak Now.m4a".
- * :code:`("(\\d*) (.*)", ("track_number", "title"))` matches
- filenames like "07 The Story of Us.m4a".
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate the first :math:`n - 1` values, and the second
- :code:`str` is used to append the final value.
- """
-
- _CODECS = {"aac": {"ffmpeg": f"-b:a 256k -c:a {FFMPEG_CODECS['aac']} "
- "-c:v copy"},
- "alac": {"ffmpeg": "-c:a alac -c:v copy"}}
- _EXTENSIONS = ["m4a", "aac", "mp4"]
- _FIELDS = {
- # field: Apple iTunes metadata list key
- "album": "\xa9alb",
- "album_artist": "aART",
- "artist": "\xa9ART",
- "comment": "\xa9cmt",
- "compilation": "cpil",
- "composer": "\xa9wrt",
- "copyright": "cprt",
- "date": "\xa9day",
- "genre": "\xa9gen",
- "lyrics": "\xa9lyr",
- "tempo": "tmpo",
- "title": "\xa9nam",
- }
- _IMAGE_FORMATS = dict.fromkeys(
- ["jpg", "jpeg", "jpe", "jif", "jfif", "jfi", 13],
- mp4.MP4Cover.FORMAT_JPEG
- ) | dict.fromkeys(["png", 14], mp4.MP4Cover.FORMAT_PNG)
-
- def __init__(
- self, file: Union[str, pathlib.Path], *,
- pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
- sep: Union[str, list[str]] = (", ", " & ")) -> None:
-
- """
- Create a MP4 audio file handler.
- """
-
- super().__init__(file, pattern=pattern, multivalue=multivalue, sep=sep)
-
- self._handle = mp4.MP4(file)
- self.bit_depth = self._handle.info.bits_per_sample
- self.bitrate = self._handle.info.bitrate
- self.channel_count = self._handle.info.channels
- self.codec = self._handle.info.codec
- self.sample_rate = self._handle.info.sample_rate
-
- self._multivalue = multivalue
- self._sep = sep
- self._from_file()
- self._from_filename()
-
- def _from_file(self) -> None:
-
- """
- Get metadata from the tags embedded in the MP4 audio file.
- """
-
- for field, key in self._FIELDS.items():
- value = self._handle.get(key)
- if value:
- if list not in self._FIELDS_TYPES[field]:
- value = utility.format_multivalue(value, False,
- primary=True)
- if type(value) not in self._FIELDS_TYPES[field]:
- try:
- value = self._FIELDS_TYPES[field][0](value)
- except ValueError:
- continue
- else:
- if type(value[0]) not in self._FIELDS_TYPES[field]:
- try:
- value = [self._FIELDS_TYPES[field][0](v)
- for v in value]
- except ValueError:
- continue
- if len(value) == 1:
- value = value[0]
- else:
- value = None
- setattr(self, field, value)
-
- self.isrc = (self._handle.get("----:com.apple.iTunes:ISRC")[0].decode()
- if "----:com.apple.iTunes:ISRC" in self._handle else None)
-
- if "disk" in self._handle:
- self.disc_number, self.disc_count = self._handle.get("disk")[0]
- else:
- self.disc_number = self.disc_count = None
-
- if "trkn" in self._handle:
- self.track_number, self.track_count = self._handle.get("trkn")[0]
- else:
- self.track_number = self.track_count = None
-
- if "covr" in self._handle:
- self.artwork = utility.format_multivalue(self._handle.get("covr"),
- False, primary=True)
- self._artwork_format = str(
- self._IMAGE_FORMATS[self.artwork.imageformat]
- ).split(".")[1].lower()
- self.artwork = bytes(self.artwork)
- else:
- self.artwork = self._artwork_format = None
-
-
-[docs]
- def write_metadata(self) -> None:
-
- """
- Write metadata to file.
- """
-
- for field, key in self._FIELDS.items():
- value = getattr(self, field)
- if value:
- value = utility.format_multivalue(
- value, self._multivalue, sep=self._sep
- )
- try:
- self._handle[key] = value
- except ValueError:
- self._handle[key] = [value]
-
- if self.isrc:
- self._handle["----:com.apple.iTunes:ISRC"] = self.isrc.encode()
-
- if self.disc_number or self.disc_count:
- self._handle["disk"] = [(self.disc_number or 0,
- self.disc_count or 0)]
- if self.track_number or self.track_count:
- self._handle["trkn"] = [(self.track_number or 0,
- self.track_count or 0)]
-
- if self.artwork:
- if isinstance(self.artwork, str):
- with urllib.request.urlopen(self.artwork) \
- if "http" in self.artwork \
- else open(self.artwork, "rb") as f:
- self.artwork = f.read()
- self._handle["covr"] = [
- mp4.MP4Cover(
- self.artwork,
- imageformat=self._IMAGE_FORMATS[self._artwork_format]
- )
- ]
-
- self._handle.save()
-
-
-
-
-[docs]
-class OggAudio(Audio, _VorbisComment):
-
- r"""
- Ogg audio file handler.
-
- .. seealso::
-
- For a full list of attributes and their descriptions, see
- :class:`Audio`.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- Ogg audio filename or path.
-
- codec : `str`, optional
- Audio codec. If not specified, it will be determined
- automatically.
-
- **Valid values**: :code:`"flac"`, :code:`"opus"`, or
- :code:`"vorbis"`.
-
- pattern : `tuple`, keyword-only, optional
- Regular expression search pattern and the corresponding metadata
- field(s).
-
- .. container::
-
- **Valid values**:
-
- The supported metadata fields are
-
- * :code:`"artist"` for the track artist,
- * :code:`"title"` for the track title, and
- * :code:`"track_number"` for the track number.
-
- **Examples**:
-
- * :code:`("(.*) - (.*)", ("artist", "title"))` matches
- filenames like "Taylor Swift - Blank Space.ogg".
- * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
- filenames like "03 - Style.ogg".
- * :code:`("(\\d*) (.*)", ("track_number", "title"))` matches
- filenames like "06 Shake It Off.ogg".
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate the first :math:`n - 1` values, and the second
- :code:`str` is used to append the final value.
- """
-
- _CODECS = {"flac": {"ffmpeg": "-c:a flac", "mutagen": oggflac.OggFLAC},
- "opus": {"ffmpeg": "-b:a 256k -c:a libopus -vn",
- "mutagen": oggopus.OggOpus},
- "vorbis": {"ffmpeg": f"-c:a {FFMPEG_CODECS['vorbis']} -vn",
- "mutagen": oggvorbis.OggVorbis}}
- _EXTENSIONS = ["ogg", "oga", "opus"]
-
- def __init__(
- self, file: Union[str, pathlib.Path], codec: str = None, *,
- pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
- sep: Union[str, list[str]] = (", ", " & ")) -> None:
-
- Audio.__init__(self, file, pattern=pattern, multivalue=multivalue,
- sep=sep)
-
- if codec and codec in self._CODECS:
- self.codec = codec
- self._handle = self._CODECS[codec]["mutagen"](file)
- else:
- for codec, options in self._CODECS.items():
- try:
- self._handle = options["mutagen"](file)
- self.codec = codec
- except Exception:
- pass
- else:
- break
- if not hasattr(self, "_handle"):
- raise RuntimeError(f"'{file}' is not a valid Ogg file.")
- _VorbisComment.__init__(self, self._file.name, self._handle.tags)
- self._from_filename()
-
- self.channel_count = self._handle.info.channels
- if self.codec == "flac":
- self.bit_depth = self._handle.info.bits_per_sample
- self.sample_rate = self._handle.info.sample_rate
- self.bitrate = self.bit_depth * self.channel_count \
- * self.sample_rate
- elif self.codec == "opus":
- self.bit_depth = self.bitrate = self.sample_rate = None
- elif self.codec == "vorbis":
- self.bit_depth = None
- self.bitrate = self._handle.info.bitrate
- self.sample_rate = self._handle.info.sample_rate
-
-
-
-[docs]
-class WAVEAudio(Audio, _ID3):
-
- r"""
- WAVE audio file handler.
-
- .. seealso::
-
- For a full list of attributes and their descriptions, see
- :class:`Audio`.
-
- Parameters
- ----------
- file : `str` or `pathlib.Path`
- WAVE audio filename or path.
-
- pattern : `tuple`, keyword-only, optional
- Regular expression search pattern and the corresponding metadata
- field(s).
-
- .. container::
-
- **Valid values**:
-
- The supported metadata fields are
-
- * :code:`"artist"` for the track artist,
- * :code:`"title"` for the track title, and
- * :code:`"track_number"` for the track number.
-
- **Examples**:
-
- * :code:`("(.*) - (.*)", ("artist", "title"))` matches
- filenames like "Taylor Swift - Don't Blame Me.wav".
- * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
- filenames like "05 - Delicate.wav".
- * :code:`("(\\d*) (.*)", ("track_number", "title"))` matches
- filenames like "06 Look What You Made Me Do.wav".
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate the first :math:`n - 1` values, and the second
- :code:`str` is used to append the final value.
- """
-
- _CODECS = {"lpcm": {"ffmpeg": "-c:a pcm_s{0:d}le -c:v copy"}}
- _EXTENSIONS = ["wav"]
-
- def __init__(
- self, file: Union[str, pathlib.Path], *,
- pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
- sep: Union[str, list[str]] = (", ", " & ")) -> None:
-
- """
- Create a WAVE audio file handler.
- """
-
- _handle = wave.WAVE(file)
- if _handle.tags is None:
- _handle.add_tags()
- _handle.tags.filename = str(file)
- Audio.__init__(self, file, pattern=pattern, multivalue=multivalue,
- sep=sep)
- _ID3.__init__(self, self._file.name, _handle.tags)
- self._from_filename()
-
- self.bit_depth = _handle.info.bits_per_sample
- self.bitrate = _handle.info.bitrate
- self.channel_count = _handle.info.channels
- self.codec = "lpcm"
- self.sample_rate = _handle.info.sample_rate
-
-
-"""
-Discogs
-=======
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module contains a complete implementation of the Discogs API.
-"""
-
-from http.server import HTTPServer, BaseHTTPRequestHandler
-import json
-import logging
-from multiprocessing import Process
-import os
-import re
-import secrets
-import time
-from typing import Any, Union
-import urllib
-import warnings
-import webbrowser
-
-import requests
-
-from . import (FOUND_FLASK, FOUND_PLAYWRIGHT, VERSION, REPOSITORY_URL,
- DIR_HOME, DIR_TEMP, _config)
-if FOUND_FLASK:
- from flask import Flask, request
-if FOUND_PLAYWRIGHT:
- from playwright.sync_api import sync_playwright
-
-__all__ = ["API"]
-
-class _DiscogsRedirectHandler(BaseHTTPRequestHandler):
-
- """
- HTTP request handler for the Discogs OAuth 1.0a flow.
- """
-
- def do_GET(self):
-
- """
- Handles an incoming GET request and parses the query string.
- """
-
- self.server.response = dict(
- urllib.parse.parse_qsl(
- urllib.parse.urlparse(f"{self.path}").query
- )
- )
- self.send_response(200)
- self.send_header("Content-Type", "text/html")
- self.end_headers()
- status = "denied" if "denied" in self.server.response else "granted"
- self.wfile.write(
- f"Access {status}. You may close this page now.".encode()
- )
-
-
-[docs]
-class API:
-
- """
- Discogs API client.
-
- The Discogs API lets developers build their own Discogs-powered
- applications for the web, desktop, and mobile devices. It is a
- RESTful interface to Discogs data and enables accessing JSON-
- formatted information about artists, releases, and labels,
- managing user collections and wantlists, creating marketplace
- listings, and more.
-
- .. seealso::
-
- For more information, see the `Discogs API home page
- <https://www.discogs.com/developers>`_.
-
- The Discogs API can be accessed with or without authentication.
- (client credentials, personal access token, or OAuth access token
- and access token secret). However, it is recommended that users at
- least provide client credentials to enjoy higher rate limits and
- access to image URLs. The consumer key and consumer secret can
- either be provided to this class's constructor as keyword arguments
- or be stored as :code:`DISCOGS_CONSUMER_KEY` and
- :code:`DISCOGS_CONSUMER_SECRET` in the operating system's
- environment variables.
-
- .. seealso::
-
- To get client credentials, see the Registration section of the
- `Authentication page <https://www.discogs.com/developers
- /#page:authentication>`_ of the Discogs API website. To take
- advantage of Minim's automatic access token retrieval
- functionality for the OAuth 1.0a flow, the redirect URI should be
- in the form :code:`http://localhost:{port}/callback`, where
- :code:`{port}` is an open port on :code:`localhost`.
-
- To view and make changes to account information and resources, users
- must either provide a personal access token to this class's
- constructor as a keyword argument or undergo the OAuth 1.0a flow,
- which require valid client credentials, using Minim. If an existing
- OAuth access token/secret pair is available, it can be provided to
- this class's constructor as keyword arguments to bypass the access
- token retrieval process.
-
- .. tip::
-
- The authorization flow and access token can be changed or updated
- at any time using :meth:`set_flow` and :meth:`set_access_token`,
- respectively.
-
- Minim also stores and manages access tokens and their properties.
- When the OAuth 1.0a flow is used to acquire an access token/secret
- pair, it is automatically saved to the Minim configuration file to
- be loaded on the next instantiation of this class. This behavior can
- be disabled if there are any security concerns, like if the computer
- being used is a shared device.
-
- Parameters
- ----------
- consumer_key : `str`, keyword-only, optional
- Consumer key. Required for the OAuth 1.0a flow, and can be used
- in the Discogs authorization flow alongside a consumer secret.
- If it is not stored as :code:`DISCOGS_CONSUMER_KEY` in the
- operating system's environment variables or found in the Minim
- configuration file, it can be provided here.
-
- consumer_secret : `str`, keyword-only, optional
- Consumer secret. Required for the OAuth 1.0a flow, and can be
- used in the Discogs authorization flow alongside a consumer key.
- If it is not stored as :code:`DISCOGS_CONSUMER_SECRET` in the
- operating system's environment variables or found in the Minim
- configuration file, it can be provided here.
-
- flow : `str`, keyword-only, optional
- Authorization flow. If :code:`None` and no access token is
- provided, no user authentication will be performed and client
- credentials will not be attached to requests, even if found or
- provided.
-
- .. container::
-
- **Valid values**:
-
- * :code:`None` for no user authentication.
- * :code:`"discogs"` for the Discogs authentication flow.
- * :code:`"oauth"` for the OAuth 1.0a flow.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is automatically opened for the
- OAuth 1.0a flow. If :code:`False`, users will have to manually
- open the authorization URL and provide the full callback URI via
- the terminal.
-
- web_framework : `str`, keyword-only, optional
- Determines which web framework to use for the OAuth 1.0a flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"http.server"` for the built-in implementation of
- HTTP servers.
- * :code:`"flask"` for the Flask framework.
- * :code:`"playwright"` for the Playwright framework by
- Microsoft.
-
- port : `int` or `str`, keyword-only, default: :code:`8888`
- Port on :code:`localhost` to use for the OAuth 1.0a flow with
- the :code:`http.server` and Flask frameworks. Only used if
- `redirect_uri` is not specified.
-
- redirect_uri : `str`, keyword-only, optional
- Redirect URI for the OAuth 1.0a flow. If not on
- :code:`localhost`, the automatic request access token retrieval
- functionality is not available.
-
- access_token : `str`, keyword-only, optional
- Personal or OAuth access token. If provided here or found in the
- Minim configuration file, the authentication process is
- bypassed.
-
- access_token_secret : `str`, keyword-only, optional
- OAuth access token secret accompanying `access_token`.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether to overwrite an existing access token in the
- Minim configuration file.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained access tokens and their
- associated properties are stored to the Minim configuration
- file.
-
- Attributes
- ----------
- API_URL : `str`
- Base URL for the Discogs API.
-
- ACCESS_TOKEN_URL : `str`
- URL for the OAuth 1.0a access token endpoint.
-
- AUTH_URL : `str`
- URL for the OAuth 1.0a authorization endpoint.
-
- REQUEST_TOKEN_URL : `str`
- URL for the OAuth 1.0a request token endpoint.
-
- session : `requests.Session`
- Session used to send requests to the Discogs API.
- """
-
- _FLOWS = {"discogs", "oauth"}
- _NAME = f"{__module__}.{__qualname__}"
-
- API_URL = "https://api.discogs.com"
- ACCESS_TOKEN_URL = f"{API_URL}/oauth/access_token"
- AUTH_URL = "https://www.discogs.com/oauth/authorize"
- REQUEST_TOKEN_URL = f"{API_URL}/oauth/request_token"
-
- def __init__(
- self, *, consumer_key: str = None, consumer_secret: str = None,
- flow: str = None, browser: bool = False, web_framework: str = None,
- port: Union[int, str] = 8888, redirect_uri: str = None,
- access_token: str = None, access_token_secret: str = None,
- overwrite: bool = False, save: bool = True) -> None:
-
- """
- Create a Discogs API client.
- """
-
- self.session = requests.Session()
- self.session.headers["User-Agent"] = f"Minim/{VERSION} +{REPOSITORY_URL}"
-
- if (access_token is None and _config.has_section(self._NAME)
- and not overwrite):
- flow = _config.get(self._NAME, "flow")
- access_token = _config.get(self._NAME, "access_token")
- access_token_secret = _config.get(self._NAME, "access_token_secret")
- consumer_key = _config.get(self._NAME, "consumer_key")
- consumer_secret = _config.get(self._NAME, "consumer_secret")
- elif flow is None and access_token is not None:
- flow = "discogs" if access_token_secret is None else "oauth"
-
- self.set_flow(
- flow, consumer_key=consumer_key, consumer_secret=consumer_secret,
- browser=browser, web_framework=web_framework, port=port,
- redirect_uri=redirect_uri, save=save
- )
- self.set_access_token(access_token, access_token_secret)
-
- def _check_authentication(
- self, endpoint: str, token: bool = True) -> None:
-
- """
- Check if the user is authenticated for the desired endpoint.
-
- Parameters
- ----------
- endpoint : `str`
- Discogs API endpoint.
-
- token : `bool`, default: :code:`True`
- Specifies whether a personal access token or OAuth access
- token is required for the endpoint. If :code:`False`, only
- client credentials are required.
- """
-
- if token and (
- self._flow != "oauth"
- or self._flow == "discogs"
- and "token" not in self.session.headers["Authorization"]
- ):
- emsg = (f"{self._NAME}.{endpoint}() requires user "
- "authentication.")
- raise RuntimeError(emsg)
- elif self._flow is None:
- emsg = f"{self._NAME}.{endpoint}() requires client credentials."
- raise RuntimeError(emsg)
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _request(
- self, method: str, url: str, *, oauth: dict[str, Any] = None,
- **kwargs) -> requests.Response:
-
- """
- Construct and send a request with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- oauth : `dict`, keyword-only, optional
- OAuth-related values to be included in the authorization
- header.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- if "headers" not in kwargs:
- kwargs["headers"] = {}
- if self._flow == "oauth" and "Authorization" not in kwargs["headers"]:
- if oauth is None:
- oauth = {}
- oauth = self._oauth | {
- "oauth_nonce": secrets.token_hex(32),
- "oauth_timestamp": f"{time.time():.0f}"
- } | oauth
- kwargs["headers"]["Authorization"] = "OAuth " + ", ".join(
- f'{k}="{v}"' for k, v in oauth.items()
- )
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code not in range(200, 299):
- j = r.json()
- emsg = f"{r.status_code}: {j['message']}"
- if "detail" in j["message"]:
- emsg += f"\n{json.dumps(j['detail'], indent=2)}"
- raise RuntimeError(emsg)
- return r
-
-
-[docs]
- def set_access_token(
- self, access_token: str = None, access_token_secret: str = None
- ) -> None:
-
- """
- Set the Discogs API personal or OAuth access token (and secret).
-
- Parameters
- ----------
- access_token : `str`, optional
- Personal or OAuth access token.
-
- access_token_secret : `str`, optional
- OAuth access token secret.
- """
-
- if self._flow == "oauth":
- self._oauth = {
- "oauth_consumer_key": self._consumer_key,
- "oauth_signature_method": "PLAINTEXT"
- }
-
- if access_token is None:
- oauth = {"oauth_signature": f"{self._consumer_secret}&"}
- if self._redirect_uri is not None:
- oauth["oauth_callback"] = self._redirect_uri
- r = self._request(
- "get",
- self.REQUEST_TOKEN_URL,
- headers={
- "Content-Type": "application/x-www-form-urlencoded"
- },
- oauth=oauth
- )
- auth_url = f"{self.AUTH_URL}?{r.text}"
- oauth = dict(urllib.parse.parse_qsl(r.text))
-
- if self._web_framework == "playwright":
- har_file = DIR_TEMP / "minim_discogs.har"
-
- with sync_playwright() as playwright:
- browser = playwright.firefox.launch(headless=False)
- context = browser.new_context(record_har_path=har_file)
- page = context.new_page()
- page.goto(auth_url, timeout=0)
- page.wait_for_url(f"{self._redirect_uri}*",
- wait_until="commit")
- context.close()
- browser.close()
-
- with open(har_file, "r") as f:
- oauth |= dict(
- urllib.parse.parse_qsl(
- urllib.parse.urlparse(
- re.search(fr'{self._redirect_uri}\?(.*?)"',
- f.read()).group(0)
- ).query
- )
- )
- har_file.unlink()
-
- else:
- if self._browser:
- webbrowser.open(auth_url)
- else:
- print("To grant Minim access to Discogs data "
- "and features, open the following link "
- f"in your web browser:\n\n{auth_url}\n")
-
- if self._web_framework == "http.server":
- httpd = HTTPServer(("", self._port),
- _DiscogsRedirectHandler)
- httpd.handle_request()
- oauth |= httpd.response
-
- elif self._web_framework == "flask":
- app = Flask(__name__)
- json_file = DIR_TEMP / "minim_discogs.json"
-
- @app.route("/callback", methods=["GET"])
- def _callback() -> str:
- if "error" in request.args:
- return ("Access denied. You may close "
- "this page now.")
- with open(json_file, "w") as f:
- json.dump(request.args, f)
- return ("Access granted. You may close "
- "this page now.")
-
- server = Process(target=app.run,
- args=("0.0.0.0", self._port))
- server.start()
- while not json_file.is_file():
- time.sleep(0.1)
- server.terminate()
-
- with open(json_file, "rb") as f:
- oauth |= json.load(f)
- json_file.unlink()
-
- else:
- oauth["oauth_verifier"] = input(
- "After authorizing Minim to access Discogs "
- "on your behalf, enter the displayed code "
- "below.\n\nCode: "
- )
-
- if "denied" in oauth:
- raise RuntimeError("Authorization failed.")
-
- oauth["oauth_signature"] = (f"{self._consumer_secret}"
- f"&{oauth['oauth_token_secret']}")
- r = self._request(
- "post",
- self.ACCESS_TOKEN_URL,
- headers={
- "Content-Type": "application/x-www-form-urlencoded"
- },
- oauth=oauth
- )
- access_token, access_token_secret = \
- dict(urllib.parse.parse_qsl(r.text)).values()
-
- if self._save:
- _config[self._NAME] = {
- "flow": self._flow,
- "access_token": access_token,
- "access_token_secret": access_token_secret,
- "consumer_key": self._consumer_key,
- "consumer_secret": self._consumer_secret
- }
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- self._oauth |= {
- "oauth_token": access_token,
- "oauth_signature": self._consumer_secret
- + f"&{access_token_secret}"
- }
-
- elif self._flow == "discogs":
- if access_token is None:
- if self._consumer_key is None or self._consumer_secret is None:
- emsg = "Discogs API client credentials not provided."
- raise ValueError(emsg)
- self.session.headers["Authorization"] = (
- f"Discogs key={self._consumer_key}, "
- f"secret={self._consumer_secret}"
- )
- else:
- self.session.headers["Authorization"] = \
- f"Discogs token={access_token}"
-
- if (self._flow == "oauth"
- or self._flow == "discogs"
- and "token" in self.session.headers["Authorization"]):
- identity = self.get_identity()
- self._username = identity["username"]
-
-
-
-[docs]
- def set_flow(
- self, flow: str, *, consumer_key: str = None,
- consumer_secret: str = None, browser: bool = False,
- web_framework: str = None, port: Union[int, str] = 8888,
- redirect_uri: str = None, save: bool = True) -> None:
-
- """
- Set the authorization flow.
-
- Parameters
- ----------
- flow : `str`
- Authorization flow. If :code:`None`, no user authentication
- will be performed and client credentials will not be
- attached to requests, even if found or provided.
-
- .. container::
-
- **Valid values**:
-
- * :code:`None` for no user authentication.
- * :code:`"discogs"` for the Discogs authentication flow.
- * :code:`"oauth"` for the OAuth 1.0a flow.
-
- consumer_key : `str`, keyword-only, optional
- Consumer key. Required for the OAuth 1.0a flow, and can be
- used in the Discogs authorization flow alongside a consumer
- secret. If it is not stored as :code:`DISCOGS_CONSUMER_KEY`
- in the operating system's environment variables or found in
- the Minim configuration file, it can be provided here.
-
- consumer_secret : `str`, keyword-only, optional
- Consumer secret. Required for the OAuth 1.0a flow, and can
- be used in the Discogs authorization flow alongside a
- consumer key. If it is not stored as
- :code:`DISCOGS_CONSUMER_SECRET` in the operating system's
- environment variables or found in the Minim configuration
- file, it can be provided here.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is automatically opened for
- the OAuth 1.0a flow. If :code:`False`, users will have to
- manually open the authorization URL and provide the full
- callback URI via the terminal.
-
- web_framework : `str`, keyword-only, optional
- Determines which web framework to use for the OAuth 1.0a
- flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"http.server"` for the built-in implementation
- of HTTP servers.
- * :code:`"flask"` for the Flask framework.
- * :code:`"playwright"` for the Playwright framework by
- Microsoft.
-
- port : `int` or `str`, keyword-only, default: :code:`8888`
- Port on :code:`localhost` to use for the OAuth 1.0a flow
- with the :code:`http.server` and Flask frameworks. Only used
- if `redirect_uri` is not specified.
-
- redirect_uri : `str`, keyword-only, optional
- Redirect URI for the OAuth 1.0a flow. If not on
- :code:`localhost`, the automatic request access token
- retrieval functionality is not available.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained access tokens and their
- associated properties are stored to the Minim configuration
- file.
- """
-
- if flow and flow not in self._FLOWS:
- emsg = (f"Invalid authorization flow ({flow=}). "
- f"Valid values: {', '.join(self._FLOWS)}.")
- raise ValueError(emsg)
- self._flow = flow
- self._save = save
-
- self._consumer_key = \
- consumer_key or os.environ.get("DISCOGS_CONSUMER_KEY")
- self._consumer_secret = \
- consumer_secret or os.environ.get("DISCOGS_CONSUMER_SECRET")
-
- if flow == "oauth":
- self._browser = browser
- if redirect_uri:
- self._redirect_uri = redirect_uri
- if "localhost" in redirect_uri:
- self._port = re.search(r"localhost:(\d+)",
- redirect_uri).group(1)
- elif web_framework:
- wmsg = ("The redirect URI is not on localhost, "
- "so automatic authorization code "
- "retrieval is not available.")
- logging.warning(wmsg)
- web_framework = None
- elif port:
- self._port = port
- self._redirect_uri = f"http://localhost:{port}/callback"
- else:
- self._port = self._redirect_uri = None
-
- self._web_framework = (
- web_framework
- if web_framework is None
- or web_framework == "http.server"
- or globals()[f"FOUND_{web_framework.upper()}"]
- else None
- )
- if self._web_framework is None and web_framework:
- wmsg = (f"The {web_framework.capitalize()} web "
- "framework was not found, so automatic "
- "authorization code retrieval is not "
- "available.")
- warnings.warn(wmsg)
-
-
- ### DATABASE ##############################################################
-
-
-[docs]
- def get_release(
- self, release_id: Union[int, str], *, curr_abbr: str = None
- ) -> dict[str, Any]:
-
- """
- `Database > Release <https://www.discogs.com
- /developers/#page:database,header:database-release-get>`_:
- Get a release (physical or digital object released by one or
- more artists).
-
- Parameters
- ----------
- release_id : `int` or `str`
- The release ID.
-
- **Example**: :code:`249504`.
-
- curr_abbr : `str`, keyword-only, optional
- Currency abbreviation for marketplace data. Defaults to the
- authenticated user's currency.
-
- **Valid values**: :code:`"USD"`, :code:`"GBP"`,
- :code:`"EUR"`, :code:`"CAD"`, :code:`"AUD"`, :code:`"JPY"`,
- :code:`"CHF"`, :code:`"MXN"`, :code:`"BRL"`, :code:`"NZD"`,
- :code:`"SEK"`, and :code:`"ZAR"`.
-
- Returns
- -------
- release : `dict`
- Discogs database information for a single release.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "title": <str>,
- "id": <int>,
- "artists": [
- {
- "anv": <str>,
- "id": <int>,
- "join": <str>,
- "name": <str>,
- "resource_url": <str>,
- "role": <str>,
- "tracks": <str>
- }
- ],
- "data_quality": <str>,
- "thumb": <str>,
- "community": {
- "contributors": [
- {
- "resource_url": <str>,
- "username": <str>
- }
- ],
- "data_quality": <str>,
- "have": <int>,
- "rating": {
- "average": <float>,
- "count": <int>
- },
- "status": <str>,
- "submitter": {
- "resource_url": <str>,
- "username": <str>
- },
- "want": <int>
- },
- "companies": [
- {
- "catno": <str>,
- "entity_type": <str>,
- "entity_type_name": <str>,
- "id": <int>,
- "name": <str>,
- "resource_url": <str>
- }
- ],
- "country": <str>,
- "date_added": <str>,
- "date_changed": <str>,
- "estimated_weight": <int>,
- "extraartists": [
- {
- "anv": <str>,
- "id": <int>,
- "join": <str>,
- "name": <str>,
- "resource_url": <str>,
- "role": <str>,
- "tracks": <str>
- }
- ],
- "format_quantity": <int>,
- "formats": [
- {
- "descriptions": [<str>],
- "name": <str>,
- "qty": <str>
- }
- ],
- "genres": [<str>],
- "identifiers": [
- {
- "type": <str>,
- "value": <str>
- },
- ],
- "images": [
- {
- "height": <int>,
- "resource_url": <str>,
- "type": <str>,
- "uri": <str>,
- "uri150": <str>,
- "width": <int>
- }
- ],
- "labels": [
- {
- "catno": <str>,
- "entity_type": <str>,
- "id": <int>,
- "name": <str>,
- "resource_url": <str>
- }
- ],
- "lowest_price": <float>,
- "master_id": <int>,
- "master_url": <str>,
- "notes": <str>,
- "num_for_sale": <int>,
- "released": <str>,
- "released_formatted": <str>,
- "resource_url": <str>,
- "series": [],
- "status": <str>,
- "styles": [<str>],
- "tracklist": [
- {
- "duration": <str>,
- "position": <str>,
- "title": <str>,
- "type_": <str>
- }
- ],
- "uri": <str>,
- "videos": [
- {
- "description": <str>,
- "duration": <int>,
- "embed": <bool>,
- "title": <str>,
- "uri": <str>
- },
- ],
- "year": <int>
- }
- """
-
- if curr_abbr and curr_abbr not in (
- CURRENCIES := {
- "USD", "GBP", "EUR", "CAD", "AUD", "JPY",
- "CHF", "MXN", "BRL", "NZD", "SEK", "ZAR"
- }
- ):
- emsg = (f"Invalid currency abbreviation ({curr_abbr=}). "
- f"Valid values: {', '.join(CURRENCIES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/releases/{release_id}",
- params={"curr_abbr": curr_abbr}
- )
-
-
-
-[docs]
- def get_user_release_rating(
- self, release_id: Union[int, str], username: str = None
- ) -> dict[str, Any]:
-
- """
- `Database > Release Rating By User > Get Release Rating By User
- <https://www.discogs.com/developers
- /#page:database,header:database-release-get>`_: Retrieves the
- release's rating for a given user.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The release ID.
-
- **Example**: :code:`249504`.
-
- username : `str`, optional
- The username of the user whose rating you are requesting. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"memory"`.
-
- Returns
- -------
- rating : `dict`
- Rating for the release by the given user.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "username": <str>,
- "release_id": <int>,
- "rating": <int>
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/releases/{release_id}/rating/{username}"
- )
-
-
-
-[docs]
- def update_user_release_rating(
- self, release_id: Union[int, str], rating: int,
- username: str = None) -> dict[str, Any]:
-
- """
- `Database > Release Rating By User > Update Release Rating By
- User <https://www.discogs.com/developers
- /#page:database,header:database-release-rating-by-user-put>`_:
- Updates the release's rating for a given user.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The release ID.
-
- **Example**: :code:`249504`.
-
- rating : `int`
- The new rating for a release between :math:`1` and :math:`5`.
-
- username : `str`, optional
- The username of the user whose rating you are requesting. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"memory"`.
-
- Returns
- -------
- rating : `dict`
- Updated rating for the release by the given user.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "username": <str>,
- "release_id": <int>,
- "rating": <int>
- }
- """
-
- self._check_authentication("update_user_release_rating")
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._request(
- "put",
- f"{self.API_URL}/releases/{release_id}/rating/{username}",
- json={"rating": rating}
- )
-
-
-
-[docs]
- def delete_user_release_rating(
- self, release_id: Union[int, str], username: str = None) -> None:
-
- """
- `Database > Release Rating By User > Delete Release Rating By
- User <https://www.discogs.com/developers
- /#page:database,header:database-release-rating-by-user-delete>`_:
- Deletes the release's rating for a given user.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The release ID.
-
- **Example**: :code:`249504`.
-
- username : `str`, optional
- The username of the user whose rating you are requesting. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"memory"`.
- """
-
- self._check_authentication("delete_user_release_rating")
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._request(
- "delete",
- f"{self.API_URL}/releases/{release_id}/rating/{username}"
- )
-
-
-
-[docs]
- def get_community_release_rating(
- self, release_id: Union[int, str]) -> dict[str, Any]:
-
- """
- `Database > Community Release Rating <https://www.discogs.com
- /developers/#page:database,header
- :database-community-release-rating-get>`_: Retrieves the
- community release rating average and count.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The release ID.
-
- **Example**: :code:`249504`.
-
- Returns
- -------
- rating : `dict`
- Community release rating average and count.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "rating": {
- "count": <int>,
- "average": <float>
- },
- "release_id": <int>
- }
- """
-
- return self._get_json(f"{self.API_URL}/releases/{release_id}/rating")
-
-
-
-[docs]
- def get_release_stats(self, release_id: Union[int, str]) -> dict[str, Any]:
-
- """
- `Database > Release Stats <https://www.discogs.com/developers
- /#page:database,header:database-release-stats-get>`_: Retrieves
- the release's "have" and "want" counts.
-
- .. attention::
-
- This endpoint does not appear to be working correctly.
- Currently, the response will be of the form
-
- .. code::
-
- {
- "is_offense": <bool>
- }
-
- Parameters
- ----------
- release_id : `int` or `str`
- The release ID.
-
- **Example**: :code:`249504`.
-
- Returns
- -------
- stats : `dict`
- Release "have" and "want" counts.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "num_have": <int>,
- "num_want": <int>
- }
- """
-
- return self._get_json(f"{self.API_URL}/releases/{release_id}/stats")
-
-
-
-[docs]
- def get_master_release(self, master_id: Union[int, str]) -> dict[str, Any]:
-
- """
- `Database > Master Release <https://www.discogs.com/developers
- /#page:database,header:database-master-release-get>`_: Get a
- master release.
-
- Parameters
- ----------
- master_id : `int` or `str`
- The master release ID.
-
- **Example**: :code:`1000`.
-
- Returns
- -------
- master_release : `dict`
- Discogs database information for a single master release.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "styles": [<str>],
- "genres": [<str>],
- "videos": [
- {
- "duration": <int>,
- "description": <str>,
- "embed": <bool>,
- "uri": <str>,
- "title": <str>
- }
- ],
- "title": <str>,
- "main_release": <int>,
- "main_release_url": <str>,
- "uri": <str>,
- "artists": [
- {
- "join": <str>,
- "name": <str>,
- "anv": <str>,
- "tracks": <str>,
- "role": <str>,
- "resource_url": <str>,
- "id": <int>
- }
- ],
- "versions_url": <str>,
- "year": <int>,
- "images": [
- {
- "height": <int>,
- "resource_url": <str>,
- "type": <str>,
- "uri": <str>,
- "uri150": <str>,
- "width": <int>
- }
- ],
- "resource_url": <str>,
- "tracklist": [
- {
- "duration": <str>,
- "position": <str>,
- "type_": <str>,
- "extraartists": [
- {
- "join": <str>,
- "name": <str>,
- "anv": <str>,
- "tracks": <str>,
- "role": <str>,
- "resource_url": <str>,
- "id": <int>
- }
- ],
- "title": <str>
- }
- ],
- "id": <int>,
- "num_for_sale": <int>,
- "lowest_price": <float>,
- "data_quality": <str>
- }
- """
-
- return self._get_json(f"{self.API_URL}/masters/{master_id}")
-
-
-
-[docs]
- def get_master_release_versions(
- self, master_id: Union[int, str], *, country: str = None,
- format: str = None, label: str = None, released: str = None,
- page: Union[int, str] = None, per_page: Union[int, str] = None,
- sort: str = None, sort_order: str = None) -> dict[str, Any]:
-
- """
- `Database > Master Release Versions <https://www.discogs.com
- /developers/#page:database,header
- :database-master-release-versions-get>`_: Retrieves a list of
- all releases that are versions of this master.
-
- Parameters
- ----------
- master_id : `int` or `str`
- The master release ID.
-
- **Example**: :code:`1000`.
-
- country : `str`, keyword-only, optional
- The country to filter for.
-
- **Example**: :code:`"Belgium"`.
-
- format : `str`, keyword-only, optional
- The format to filter for.
-
- **Example**: :code:`"Vinyl"`.
-
- label : `str`, keyword-only, optional
- The label to filter for.
-
- **Example**: :code:`"Scorpio Music"`.
-
- released : `str`, keyword-only, optional
- The release year to filter for.
-
- **Example**: :code:`"1992"`.
-
- page : `int` or `str`, keyword-only, optional
- The page you want to request.
-
- **Example**: :code:`3`.
-
- per_page : `int` or `str`, keyword-only, optional
- The number of items per page.
-
- **Example**: :code:`25`.
-
- sort : `str`, keyword-only, optional
- Sort items by this field.
-
- **Valid values**: :code:`"released"`, :code:`"title"`,
- :code:`"format"`, :code:`"label"`, :code:`"catno"`,
- and :code:`"country"`.
-
- sort_order : `str`, keyword-only, optional
- Sort items in a particular order.
-
- **Valid values**: :code:`"asc"` and :code:`"desc"`.
-
- Returns
- -------
- versions : `dict`
- Discogs database information for all releases that are
- versions of the specified master.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "items": <int>,
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- }
- },
- "versions": [
- {
- "status": <str>,
- "stats": {
- "user": {
- "in_collection": <int>,
- "in_wantlist": <int>
- },
- "community": {
- "in_collection": <int>,
- "in_wantlist": <int>
- }
- },
- "thumb": <str>,
- "format": <str>,
- "country": <str>,
- "title": <str>,
- "label": <str>,
- "released": <str>,
- "major_formats": [<str>],
- "catno": <str>,
- "resource_url": <str>,
- "id": <int>
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/masters/{master_id}/versions",
- params={
- "country": country,
- "format": format,
- "label": label,
- "released": released,
- "page": page,
- "per_page": per_page,
- "sort": sort,
- "sort_order": sort_order
- },
- )
-
-
-
-[docs]
- def get_artist(self, artist_id: Union[int, str]) -> dict[str, Any]:
-
- """
- `Database > Artist <https://www.discogs.com/developers
- /#page:database,header:database-artist-get>`_: Get an artist.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- The artist ID.
-
- **Example**: :code:`108713`.
-
- Returns
- -------
- artist : `dict`
- Discogs database information for a single artist.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "namevariations": [<str>],
- "profile": <str>,
- "releases_url": <str>,
- "resource_url": <str>,
- "uri": <str>,
- "urls": [<str>],
- "data_quality": <str>,
- "id": <int>,
- "images": [
- {
- "height": <int>,
- "resource_url": <str>,
- "type": <str>,
- "uri": <str>,
- "uri150": <str>,
- "width": <int>
- }
- ],
- "members": [
- {
- "active": <bool>,
- "id": <int>,
- "name": <str>,
- "resource_url": <str>
- }
- ]
- }
- """
-
- return self._get_json(f"{self.API_URL}/artists/{artist_id}")
-
-
-
-[docs]
- def get_artist_releases(
- self, artist_id: Union[int, str], *, page: Union[int, str] = None,
- per_page: Union[int, str] = None, sort: str = None,
- sort_order: str = None) -> dict[str, Any]:
-
- """
- `Database > Artist Releases <https://www.discogs.com/developers
- /#page:database,header:database-artist-releases-get>`_: Get an
- artist's releases and masters.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- The artist ID.
-
- **Example**: :code:`108713`.
-
- page : `int` or `str`, keyword-only, optional
- Page of results to fetch.
-
- per_page : `int` or `str`, keyword-only, optional
- Number of results per page.
-
- sort : `str`, keyword-only, optional
- Sort results by this field.
-
- **Valid values**: :code:`"year"`, :code:`"title"`, and
- :code:`"format"`.
-
- sort_order : `str`, keyword-only, optional
- Sort results in a particular order.
-
- **Valid values**: :code:`"asc"` and :code:`"desc"`.
-
- Returns
- -------
- releases : `dict`
- Discogs database information for all releases by the
- specified artist.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "items": <int>,
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- }
- },
- "releases": [
- {
- "artist": <str>,
- "id": <int>,
- "main_release": <int>,
- "resource_url": <str>,
- "role": <str>,
- "thumb": <str>,
- "title": <str>,
- "type": <str>,
- "year": <int>
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artists/{artist_id}/releases",
- params={
- "page": page,
- "per_page": per_page,
- "sort": sort,
- "sort_order": sort_order
- }
- )
-
-
-
-[docs]
- def get_label(self, label_id: Union[int, str]) -> dict[str, Any]:
-
- """
- `Database > Label <https://www.discogs.com/developers
- /#page:database,header:database-label-get>`_: Get a label,
- company, recording studio, locxation, or other entity involved
- with artists and releases.
-
- Parameters
- ----------
- label_id : `int` or `str`
- The label ID.
-
- **Example**: :code:`1`.
-
- Returns
- -------
- label : `dict`
- Discogs database information for a single label.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "profile": <str>,
- "releases_url": <str>,
- "name": <str>,
- "contact_info": <str>,
- "uri": <str>,
- "sublabels": [
- {
- "resource_url": <str>,
- "id": <int>,
- "name": <str>
- }
- ],
- "urls": [<str>],
- "images": [
- {
- "height": <int>,
- "resource_url": <str>,
- "type": <str>,
- "uri": <str>,
- "uri150": <str>,
- "width": <int>
- }
- ],
- "resource_url": <str>,
- "id": <int>,
- "data_quality": <str>
- }
- """
-
- return self._get_json(f"{self.API_URL}/labels/{label_id}")
-
-
-
-[docs]
- def get_label_releases(
- self, label_id: Union[int, str], *, page: Union[int, str] = None,
- per_page: Union[int, str] = None) -> dict[str, Any]:
-
- """
- `Database > Label Releases <https://www.discogs.com/developers
- /#page:database,header:database-all-label-releases-get>`_: Get a
- list of releases associated with the label.
-
- Parameters
- ----------
- label_id : `int` or `str`
- The label ID.
-
- **Example**: :code:`1`.
-
- page : `int` or `str`, keyword-only, optional
- Page of results to fetch.
-
- per_page : `int` or `str`, keyword-only, optional
- Number of results per page.
-
- Returns
- -------
- releases : `dict`
- Discogs database information for all releases by the
- specified label.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "items": <int>,
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- }
- },
- "releases": [
- {
- "artist": <str>,
- "catno": <str>,
- "format": <str>,
- "id": <int>,
- "resource_url": <str>,
- "status": <str>,
- "thumb": <str>,
- "title": <str>,
- "year": <int>
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/labels/{label_id}/releases",
- params={"page": page, "per_page": per_page}
- )
-
-
-
-[docs]
- def search(
- self, query: str = None, *, type: str = None, title: str = None,
- release_title: str = None, credit: str = None,
- artist: str = None, anv: str = None, label: str = None,
- genre: str = None, style: str = None, country: str = None,
- year: str = None, format: str = None, catno: str = None,
- barcode: str = None, track: str = None, submitter: str = None,
- contributor: str = None) -> dict[str, Any]:
-
- """
- `Database > Search <https://www.discogs.com/developers
- /#page:database,header:database-search-get>`_: Issue a search
- query to the Discogs database.
-
- .. admonition:: Authentication
- :class: warning
-
- Requires authentication with consumer credentials, with a
- personal access token, or via the OAuth 1.0a flow.
-
- Parameters
- ----------
- query : `str`, optional
- The search query.
-
- **Example**: :code:`"Nirvana"`.
-
- type : `str`, keyword-only, optional
- The type of item to search for.
-
- **Valid values**: :code:`"release"`, :code:`"master"`,
- :code:`"artist"`, and :code:`"label"`.
-
- title : `str`, keyword-only, optional
- Search by combined :code:`"<artist name> - <release title>"`
- title field.
-
- **Example**: :code:`"Nirvana - Nevermind"`.
-
- release_title : `str`, keyword-only, optional
- Search release titles.
-
- **Example**: :code:`"Nevermind"`.
-
- credit : `str`, keyword-only, optional
- Search release credits.
-
- **Example**: :code:`"Kurt"`.
-
- artist : `str`, keyword-only, optional
- Search artist names.
-
- **Example**: :code:`"Nirvana"`.
-
- anv : `str`, keyword-only, optional
- Search artist name variations (ANV).
-
- **Example**: :code:`"Nirvana"`.
-
- label : `str`, keyword-only, optional
- Search labels.
-
- **Example**: :code:`"DGC"`.
-
- genre : `str`, keyword-only, optional
- Search genres.
-
- **Example**: :code:`"Rock"`.
-
- style : `str`, keyword-only, optional
- Search styles.
-
- **Example**: :code:`"Grunge"`.
-
- country : `str`, keyword-only, optional
- Search release country.
-
- **Example**: :code:`"Canada"`.
-
- year : `str`, keyword-only, optional
- Search release year.
-
- **Example**: :code:`"1991"`.
-
- format : `str`, keyword-only, optional
- Search formats.
-
- **Example**: :code:`"Album"`.
-
- catno : `str`, keyword-only, optional
- Search catalog number.
-
- **Example**: :code:`"DGCD-24425"`.
-
- barcode : `str`, keyword-only, optional
- Search barcode.
-
- **Example**: :code:`"720642442524"`.
-
- track : `str`, keyword-only, optional
- Search track.
-
- **Example**: :code:`"Smells Like Teen Spirit"`.
-
- submitter : `str`, keyword-only, optional
- Search submitter username.
-
- **Example**: :code:`"milKt"`.
-
- contributor : `str`, keyword-only, optional
- Search contributor username.
-
- **Example**: :code:`"jerome99"`.
-
- Returns
- -------
- results : `dict`
- Search results.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "items": <int>,
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- }
- },
- "results": [
- {
- "style": [<str>],
- "thumb": <str>,
- "title": <str>,
- "country": <str>,
- "format": [<str>],
- "uri": <str>,
- "community": {
- "want": <int>,
- "have": <int>
- },
- "label": [<str>],
- "catno": <str>,
- "year": <str>,
- "genre": [<str>],
- "resource_url": <str>,
- "type": <str>,
- "id": <int>
- }
- ]
- }
- """
-
- self._check_authentication("search", False)
-
- return self._get_json(
- f"{self.API_URL}/database/search",
- params={
- "q": query,
- "type": type,
- "title": title,
- "release_title": release_title,
- "credit": credit,
- "artist": artist,
- "anv": anv,
- "label": label,
- "genre": genre,
- "style": style,
- "country": country,
- "year": year,
- "format": format,
- "catno": catno,
- "barcode": barcode,
- "track": track,
- "submitter": submitter,
- "contributor": contributor
- }
- )
-
-
- ### MARKETPLACE ###########################################################
-
-
-[docs]
- def get_inventory(
- self, username: str = None, *, status: str = None,
- page: Union[int, str] = None, per_page: Union[int, str] = None,
- sort: str = None, sort_order: str = None) -> dict[str, Any]:
-
- """
- `Marketplace > Inventory <https://www.discogs.com/developers
- /#page:marketplace,header:marketplace-inventory-get>`_:
- Get a seller's inventory.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- If you are authenticated as the inventory owner, additional
- fields will be returned in the response, such as
- :code:`"weight"`, :code:`"format_quantity"`,
- :code:`"external_id"`, :code:`"location"`, and
- :code:`"quantity"`.
-
- Parameters
- ----------
- username : `str`
- The username of the inventory owner. If not specified, the
- username of the authenticated user is used.
-
- **Example**: :code:`"360vinyl"`.
-
- status : `str`, keyword-only, optional
- The status of the listings to return.
-
- **Valid values**: :code:`"For Sale"`, :code:`"Draft"`,
- :code:`"Expired"`, :code:`"Sold"`, and :code:`"Deleted"`.
-
- page : `int` or `str`, keyword-only, optional
- The page you want to request.
-
- **Example**: :code:`3`.
-
- per_page : `int` or `str`, keyword-only, optional
- The number of items per page.
-
- **Example**: :code:`25`.
-
- sort : `str`, keyword-only, optional
- Sort items by this field.
-
- **Valid values**: :code:`"listed"`, :code:`"price"`,
- :code:`"item"`, :code:`"artist"`, :code:`"label"`,
- :code:`"catno"`, :code:`"audio"`, :code:`"status"`, and
- :code:`"location"`.
-
- sort_order : `str`, keyword-only, optional
- Sort items in a particular order.
-
- **Valid values**: :code:`"asc"` and :code:`"desc"`.
-
- Returns
- -------
- inventory : `dict`
- The seller's inventory.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "items": <int>,
- "urls": {}
- },
- "listings": [
- {
- "status": <str>,
- "price": {
- "currency": <str>,
- "value": <float>
- },
- "allow_offers": <bool>,
- "sleeve_condition": <str>,
- "id": <int>,
- "condition": <str>,
- "posted": <str>,
- "ships_from": <str>,
- "uri": <str>,
- "comments": <str>,
- "seller": {
- "username": <str>,
- "resource_url": <str>,
- "id": <int>
- },
- "release": {
- "catalog_number": <str>,
- "resource_url": <str>,
- "year": <int>,
- "id": <int>,
- "description": <str>,
- "artist": <str>,
- "title": <str>,
- "format": <str>,
- "thumbnail": <str>
- },
- "resource_url": <str>,
- "audio": <bool>
- }
- ]
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/inventory",
- params={"status": status, "page": page, "per_page": per_page,
- "sort": sort, "sort_order": sort_order}
- )
-
-
-
-[docs]
- def get_listing(
- self, listing_id: Union[int, str], *, curr_abbr: str = None
- ) -> dict[str, Any]:
-
- """
- `Marketplace > Listing <https://www.discogs.com/developers
- /#page:marketplace,header:marketplace-listing-get>`_: View
- marketplace listings.
-
- Parameters
- ----------
- listing_id : `int` or `str`
- The ID of the listing you are fetching.
-
- **Example**: :code:`172723812`.
-
- curr_abbr : `str`, keyword-only, optional
- Currency abbreviation for marketplace listings. Defaults to
- the authenticated user's currency.
-
- **Valid values**: :code:`"USD"`, :code:`"GBP"`, :code:`"EUR"`,
- :code:`"CAD"`, :code:`"AUD"`, :code:`"JPY"`, :code:`"CHF"`,
- :code:`"MXN"`, :code:`"BRL"`, :code:`"NZD"`, :code:`"SEK"`,
- and :code:`"ZAR"`.
-
- Returns
- -------
- listing : `dict`
- The marketplace listing.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "status": <str>,
- "price": {
- "currency": <str>,
- "value": <int>
- },
- "original_price": {
- "curr_abbr": <str>,
- "curr_id": <int>,
- "formatted": <str>,
- "value": <float>
- },
- "allow_offers": <bool>,
- "sleeve_condition": <str>,
- "id": <int>,
- "condition": <str>,
- "posted": <str>,
- "ships_from": <str>,
- "uri": <str>,
- "comments": <str>,
- "seller": {
- "username": <str>,
- "avatar_url": <str>,
- "resource_url": <str>,
- "url": <str>,
- "id": <int>,
- "shipping": <str>,
- "payment": <str>,
- "stats": {
- "rating": <str>,
- "stars": <float>,
- "total": <int>
- }
- },
- "shipping_price": {
- "currency": <str>,
- "value": <float>
- },
- "original_shipping_price": {
- "curr_abbr": <str>,
- "curr_id": <int>,
- "formatted": <str>,
- "value": <float>
- },
- "release": {
- "catalog_number": <str>,
- "resource_url": <str>,
- "year": <int>,
- "id": <int>,
- "description": <str>,
- "thumbnail": <str>,
- },
- "resource_url": <str>,
- "audio": <bool>
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/marketplace/listings/{listing_id}",
- params={"curr_abbr": curr_abbr}
- )
-
-
-
-[docs]
- def create_listing(
- self, release_id: Union[int, str], condition: str, price: float,
- status: str = "For Sale", *, sleeve_condition: str = None,
- comments: str = None, allow_offers: bool = None,
- external_id: str = None, location: str = None, weight: float = None,
- format_quantity: int = None) -> dict[str, Any]:
-
- """
- `Marketplace > New Listing <https://www.discogs.com/developers
- /#page:marketplace,header:marketplace-new-listing>`_: Create a
- marketplace listing.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The ID of the release you are posting.
-
- **Example**: :code:`249504`.
-
- condition : `str`
- The condition of the release you are posting.
-
- **Valid values**: :code:`"Mint (M)"`,
- :code:`"Near Mint (NM or M-)"`,
- :code:`"Very Good Plus (VG+)"`,
- :code:`"Very Good (VG)"`, :code:`"Good Plus (G+)"`,
- :code:`"Good (G)"`, :code:`"Fair (F)"`, and
- :code:`"Poor (P)"`.
-
- price : `float`
- The price of the item (in the seller's currency).
-
- **Example**: :code:`10.00`.
-
- status : `str`, default: :code:`"For Sale"`
- The status of the listing.
-
- **Valid values**: :code:`"For Sale"` (the listing is ready
- to be shwon on the marketplace) and :code:`"Draft"` (the
- listing is not ready for public display).
-
- sleeve_condition : `str`, optional
- The condition of the sleeve of the item you are posting.
-
- **Valid values**: :code:`"Mint (M)"`,
- :code:`"Near Mint (NM or M-)"`,
- :code:`"Very Good Plus (VG+)"`,
- :code:`"Very Good (VG)"`, :code:`"Good Plus (G+)"`,
- :code:`"Good (G)"`, :code:`"Fair (F)"`, and
- :code:`"Poor (P)"`.
-
- comments : `str`, optional
- Any remarks about the item that will be displated to buyers.
-
- allow_offers : `bool`, optional
- Whether or not to allow buyers to make offers on the item.
-
- **Default**: :code:`False`.
-
- external_id : `str`, optional
- A freeform field that can be used for the seller's own
- reference. Information stored here will not be displayed to
- anyone other than the seller. This field is called “Private
- Comments” on the Discogs website.
-
- location : `str`, optional
- A freeform field that is intended to help identify an item's
- physical storage location. Information stored here will not
- be displayed to anyone other than the seller. This field
- will be visible on the inventory management page and will be
- available in inventory exports via the website.
-
- weight : `float`, optional
- The weight, in grams, of this listing, for the purpose of
- calculating shipping. Set this field to :code:`"auto"` to
- have the weight automatically estimated for you.
-
- format_quantity : `int`, optional
- The number of items this listing counts as, for the purpose
- of calculating shipping. This field is called "Counts As" on
- the Discogs website. Set this field to :code:`"auto"` to
- have the quantity automatically estimated for you.
- """
-
- return self._request(
- "post",
- f"{self.API_URL}/marketplace/listings",
- params={
- "release_id": release_id,
- "condition": condition,
- "price": price,
- "status": status,
- "sleeve_condition": sleeve_condition,
- "comments": comments,
- "allow_offers": allow_offers,
- "external_id": external_id,
- "location": location,
- "weight": weight,
- "format_quantity": format_quantity
- }
- ).json()
-
-
-
-[docs]
- def edit_listing(
- self, listing_id: Union[int, str], release_id: Union[int, str],
- condition: str, price: float, status: str = "For Sale", *,
- sleeve_condition: str = None, comments: str = None,
- allow_offers: bool = None, external_id: str = None,
- location: str = None, weight: float = None,
- format_quantity: int = None) -> None:
-
- """
- `Marketplace > Listing > Edit Listing <https://www.discogs.com
- /developers/#page:marketplace,header:marketplace-listing-post>`_:
- Edit the data associated with a listing.
-
- If the listing's status is not :code:`"For Sale"`,
- :code:`"Draft"`, or :code:`"Expired"`, it cannot be
- modified—only deleted. To re-list a :code:`"Sold"` listing, a
- new listing must be created.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- listing_id : `int` or `str`
- The ID of the listing you are fetching.
-
- **Example**: :code:`172723812`.
-
- release_id : `int` or `str`
- The ID of the release you are posting.
-
- **Example**: :code:`249504`.
-
- condition : `str`
- The condition of the release you are posting.
-
- **Valid values**: :code:`"Mint (M)"`,
- :code:`"Near Mint (NM or M-)"`,
- :code:`"Very Good Plus (VG+)"`,
- :code:`"Very Good (VG)"`, :code:`"Good Plus (G+)"`,
- :code:`"Good (G)"`, :code:`"Fair (F)"`, and
- :code:`"Poor (P)"`.
-
- price : `float`
- The price of the item (in the seller's currency).
-
- **Example**: :code:`10.00`.
-
- status : `str`, default: :code:`"For Sale"`
- The status of the listing.
-
- **Valid values**: :code:`"For Sale"` (the listing is ready
- to be shwon on the marketplace) and :code:`"Draft"` (the
- listing is not ready for public display).
-
- sleeve_condition : `str`, optional
- The condition of the sleeve of the item you are posting.
-
- **Valid values**: :code:`"Mint (M)"`,
- :code:`"Near Mint (NM or M-)"`,
- :code:`"Very Good Plus (VG+)"`,
- :code:`"Very Good (VG)"`, :code:`"Good Plus (G+)"`,
- :code:`"Good (G)"`, :code:`"Fair (F)"`, and
- :code:`"Poor (P)"`.
-
- comments : `str`, optional
- Any remarks about the item that will be displated to buyers.
-
- allow_offers : `bool`, optional
- Whether or not to allow buyers to make offers on the item.
-
- **Default**: :code:`False`.
-
- external_id : `str`, optional
- A freeform field that can be used for the seller's own
- reference. Information stored here will not be displayed to
- anyone other than the seller. This field is called “Private
- Comments” on the Discogs website.
-
- location : `str`, optional
- A freeform field that is intended to help identify an item's
- physical storage location. Information stored here will not
- be displayed to anyone other than the seller. This field
- will be visible on the inventory management page and will be
- available in inventory exports via the website.
-
- weight : `float`, optional
- The weight, in grams, of this listing, for the purpose of
- calculating shipping. Set this field to :code:`"auto"` to
- have the weight automatically estimated for you.
-
- format_quantity : `int`, optional
- The number of items this listing counts as, for the purpose
- of calculating shipping. This field is called "Counts As" on
- the Discogs website. Set this field to :code:`"auto"` to
- have the quantity automatically estimated for you.
- """
-
- self._check_authentication("edit_listing")
-
- self._request(
- "post",
- f"{self.API_URL}/marketplace/listings/{listing_id}",
- json={
- "release_id": release_id,
- "condition": condition,
- "price": price,
- "status": status,
- "sleeve_condition": sleeve_condition,
- "comments": comments,
- "allow_offers": allow_offers,
- "external_id": external_id,
- "location": location,
- "weight": weight,
- "format_quantity": format_quantity
- }
- )
-
-
-
-[docs]
- def delete_listing(self, listing_id: Union[int, str]) -> None:
-
- """
- `Marketplace > Listing > Delete Listing <https://www.discogs.com
- /developers/#page:marketplace,header
- :marketplace-listing-delete>`_: Permanently remove a listing
- from the marketplace.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- listing_id : `int` or `str`
- The ID of the listing you are fetching.
-
- **Example**: :code:`172723812`.
- """
-
- self._check_authentication("delete_listing")
-
- self._request("delete",
- f"{self.API_URL}/marketplace/listings/{listing_id}")
-
-
-
-[docs]
- def get_order(self, order_id: str) -> dict[str, Any]:
-
- """
- `Marketplace > Order > Get Order <https://www.discogs.com/developers
- #page:marketplace,header:marketplace-order-get>`_: View the data
- associated with an order.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- order_id : `str`
- The ID of the order you are fetching.
-
- **Example**: :code:`1-1`.
-
- Returns
- -------
- order : `dict`
- The marketplace order.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <str>,
- "resource_url": <str>,
- "messages_url": <str>,
- "uri": <str>,
- "status": <str>,
- "next_status": [<str>],
- "fee": {
- "currency": <str>,
- "value": <float>
- },
- "created": <str>,
- "items": [
- {
- "release": {
- "id": <int>,
- "description": <str>,
- },
- "price": {
- "currency": <str>,
- "value": <int>
- },
- "media_condition": <str>,
- "sleeve_condition": <str>,
- "id": <int>
- }
- ],
- "shipping": {
- "currency": <str>,
- "method": <str>,
- "value": <int>
- },
- "shipping_address": <str>,
- "additional_instructions": <str>,
- "archived": <bool>,
- "seller": {
- "resource_url": <str>,
- "username": <str>,
- "id": <int>
- },
- "last_activity": <str>,
- "buyer": {
- "resource_url": <str>,
- "username": <str>,
- "id": <int>
- },
- "total": {
- "currency": <str>,
- "value": <int>
- }
- }
- """
-
- self._check_authentication("get_order")
-
- return self._get_json(f"{self.API_URL}/marketplace/orders/{order_id}")
-
-
-
-[docs]
- def edit_order(
- self, order_id: str, status: str, *, shipping: float = None
- ) -> dict[str, Any]:
-
- """
- `Marketplace > Order > Edit Order <https://www.discogs.com/developers
- #page:marketplace,header:marketplace-order-post>`_: Edit the data
- associated with an order.
-
- The response contains a :code:`"next_status"` key—an array of
- valid next statuses for this order.
-
- Changing the order status using this resource will always message
- the buyer with
-
- Seller changed status from [...] to [...]
-
- and does not provide a facility for including a custom message
- along with the change. For more fine-grained control, use the
- :meth:`add_order_message` method, which allows you to
- simultaneously add a message and change the order status. If the
- order status is not :code:`"Cancelled"`,
- :code:`"Payment Received"`, or :code:`"Shipped"`, you can change
- the shipping. Doing so will send an invoice to the buyer and set
- the order status to :code:`"Invoice Sent"`. (For that reason,
- you cannot set the shipping and the order status in the same
- request.)
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- order_id : `str`
- The ID of the order you are fetching.
-
- **Example**: :code:`1-1`.
-
- status : `str`
- The status of the order you are updating. The new status must
- be present in the order's :code:`"next_status"` list.
-
- **Valid values**: :code:`"New Order"`,
- :code:`"Buyer Contacted"`, :code:`"Invoice Sent"`,
- :code:`"Payment Pending"`, :code:`"Payment Received"`,
- :code:`"In Progress"`, :code:`"Shipped"`,
- :code:`"Refund Sent"`, :code:`"Cancelled (Non-Paying Buyer)"`,
- :code:`"Cancelled (Item Unavailable)"`, and
- :code:`"Cancelled (Per Buyer's Request)"`.
-
- shipping : `float`, optional
- The order shipping amount. As a side effect of setting this
- value, the buyer is invoiced and the order status is set to
- :code:`"Invoice Sent"`.
-
- **Example**: :code:`5.00`.
-
- Returns
- -------
- order : `dict`
- The marketplace order.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <str>,
- "resource_url": <str>,
- "messages_url": <str>,
- "uri": <str>,
- "status": <str>,
- "next_status": [<str>],
- "fee": {
- "currency": <str>,
- "value": <float>
- },
- "created": <str>,
- "items": [
- {
- "release": {
- "id": <int>,
- "description": <str>,
- },
- "price": {
- "currency": <str>,
- "value": <int>
- },
- "media_condition": <str>,
- "sleeve_condition": <str>,
- "id": <int>
- }
- ],
- "shipping": {
- "currency": <str>,
- "method": <str>,
- "value": <int>
- },
- "shipping_address": <str>,
- "additional_instructions": <str>,
- "archived": <bool>,
- "seller": {
- "resource_url": <str>,
- "username": <str>,
- "id": <int>
- },
- "last_activity": <str>,
- "buyer": {
- "resource_url": <str>,
- "username": <str>,
- "id": <int>
- },
- "total": {
- "currency": <str>,
- "value": <int>
- }
- }
- """
-
- self._check_authentication("edit_order")
-
- return self._request(
- "post",
- f"{self.API_URL}/marketplace/orders/{order_id}",
- json={"status": status, "shipping": shipping}
- ).json()
-
-
-
-[docs]
- def get_user_orders(
- self, *, status: str = None, created_after: str = None,
- created_before: str = None, archived: bool = None,
- page: Union[int, str] = None, per_page: Union[int, str] = None,
- sort: str = None, sort_order: str = None) -> dict[str, Any]:
-
- """
- `Marketplace > List Orders <https://www.discogs.com/developers
- /#page:marketplace,header:marketplace-list-orders-get>`_:
- Returns a list of the authenticated user's orders.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- status : `str`, keyword-only, optional
- Only show orders with this status.
-
- **Valid values**: :code:`"All"`, :code:`"New Order"`,
- :code:`"Buyer Contacted"`, :code:`"Invoice Sent"`,
- :code:`"Payment Pending"`, :code:`"Payment Received"`,
- :code:`"In Progress"`, :code:`"Shipped"`,
- :code:`"Merged"`, :code:`"Order Changed"`,
- :code:`"Refund Sent"`, :code:`"Cancelled"`,
- :code:`"Cancelled (Non-Paying Buyer)"`,
- :code:`"Cancelled (Item Unavailable)"`,
- :code:`"Cancelled (Per Buyer's Request)"`, and
- :code:`"Cancelled (Refund Received)"`.
-
- created_after : `str`, keyword-only, optional
- Only show orders created after this ISO 8601 timestamp.
-
- **Example**: :code:`"2019-06-24T20:58:58Z"`.
-
- created_before : `str`, keyword-only, optional
- Only show orders created before this ISO 8601 timestamp.
-
- **Example**: :code:`"2019-06-24T20:58:58Z"`.
-
- archived : `bool`, keyword-only, optional
- Only show orders with a specific archived status. If no key
- is provided, both statuses are returned.
-
- page : `int` or `str`, keyword-only, optional
- The page you want to request.
-
- **Example**: :code:`3`.
-
- per_page : `int`, keyword-only, optional
- The number of items per page.
-
- **Example**: :code:`25`.
-
- sort : `str`, keyword-only, optional
- Sort items by this field.
-
- **Valid values**: :code:`"id"`, :code:`"buyer"`,
- :code:`"created"`, :code:`"status"`, and
- :code:`"last_activity"`.
-
- sort_order : `str`, keyword-only, optional
- Sort items in a particular order.
-
- **Valid values**: :code:`"asc"` and :code:`"desc"`.
-
- Returns
- -------
- orders : `dict`
- The authenticated user's orders.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "items": <int>,
- "urls": {}
- },
- "orders": [
- {
- "id": <str>,
- "resource_url": <str>,
- "messages_url": <str>,
- "uri": <str>,
- "status": <str>,
- "next_status": [<str>],
- "fee": {
- "currency": <str>,
- "value": <float>
- },
- "created": <str>,
- "items": [
- {
- "release": {
- "id": <int>,
- "description": <str>,
- },
- "price": {
- "currency": <str>,
- "value": <int>
- },
- "media_condition": <str>,
- "sleeve_condition": <str>,
- "id": <int>
- }
- ],
- "shipping": {
- "currency": <str>,
- "method": <str>,
- "value": <int>
- },
- "shipping_address": <str>,
- "additional_instructions": <str>,
- "archived": <bool>,
- "seller": {
- "resource_url": <str>,
- "username": <str>,
- "id": <int>
- },
- "last_activity": <str>,
- "buyer": {
- "resource_url": <str>,
- "username": <str>,
- "id": <int>
- },
- "total": {
- "currency": <str>,
- "value": <int>
- }
- }
- ]
- }
- """
-
- self._check_authentication("get_user_orders")
-
- return self._get_json(
- f"{self.API_URL}/marketplace/orders",
- params={
- "status": status,
- "created_after": created_after,
- "created_before": created_before,
- "archived": archived,
- "page": page,
- "per_page": per_page,
- "sort": sort,
- "sort_order": sort_order,
- }
- )
-
-
-
-[docs]
- def get_order_messages(
- self, order_id: str, *, page: Union[int, str] = None,
- per_page: Union[int, str] = None) -> dict[str, Any]:
-
- """
- `Marketplace > List Order Messages > List Order Messages
- <https://www.discogs.com/developers/
- #page:marketplace,header:marketplace-list-order-messages-get>`_:
- Returns a list of the order's messages with the most recent
- first.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- order_id : `str`
- The ID of the order you are fetching.
-
- **Example**: :code:`1-1`.
-
- page : `int` or `str`, keyword-only, optional
- The page you want to request.
-
- **Example**: :code:`3`.
-
- per_page : `int` or `str`, keyword-only, optional
- The number of items per page.
-
- **Example**: :code:`25`.
-
- Returns
- -------
- messages : `dict`
- The order's messages.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "per_page": <int>,
- "items": <int>,
- "page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- },
- "pages": <int>
- },
- "messages": [
- {
- "refund": {
- "amount": <int>,
- "order": {
- "resource_url": <str>,
- "id": <str>
- }
- },
- "timestamp": <str>,
- "message": <str>,
- "type": <str>,
- "order": {
- "resource_url": <str>,
- "id": <str>,
- },
- "subject": <str>
- }
- ]
- }
- """
-
- self._check_authentication("get_order_messages")
-
- return self._get_json(
- f"{self.API_URL}/marketplace/orders/{order_id}/messages",
- params={"page": page, "per_page": per_page}
- )
-
-
-
-[docs]
- def add_order_message(
- self, order_id: str, message: str = None, status: str = None
- ) -> dict[str, Any]:
-
- """
- `Marketplace > List Order Messages > Add New Message
- <https://www.discogs.com/developers/
- #page:marketplace,header:marketplace-list-order-messages-post>`_:
- Adds a new message to the order's message log.
-
- When posting a new message, you can simultaneously change the
- order status. IF you do, the message will automatically be
- prepended with:
-
- Seller changed status from [...] to [...]
-
- While `message` and `status` are each optional, one or both
- must be present.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- order_id : `str`
- The ID of the order you are fetching.
-
- **Example**: :code:`1-1`.
-
- message : `str`, optional
- The message you are posting.
-
- **Example**: :code:`"hello world"`
-
- status : `str`, optional
- The status of the order you are updating.
-
- **Valid values**: :code:`"New Order"`,
- :code:`"Buyer Contacted"`, :code:`"Invoice Sent"`,
- :code:`"Payment Pending"`, :code:`"Payment Received"`,
- :code:`"In Progress"`, :code:`"Shipped"`,
- :code:`"Refund Sent"`, :code:`"Cancelled (Non-Paying Buyer)"`,
- :code:`"Cancelled (Item Unavailable)"`, and
- :code:`"Cancelled (Per Buyer's Request)"`.
-
- Returns
- -------
- message : `dict`
- The order's message.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "from": {
- "username": <str>,
- "resource_url": <str>
- },
- "message": <str>,
- "order": {
- "resource_url": <str>,
- "id": <str>
- },
- "timestamp": <str>,
- "subject": <str>
- }
- """
-
- self._check_authentication("add_order_message")
-
- if message is None and status is None:
- emsg = "Either 'message' or 'status' must be provided."
- raise ValueError(emsg)
-
- return self._request(
- "post",
- f"{self.API_URL}/marketplace/orders/{order_id}/messages",
- json={"message": message, "status": status}
- ).json()
-
-
-
-[docs]
- def get_fee(self, price: float, *, currency: str = "USD") -> dict[str, Any]:
-
- """
- `Marketplace > Fee with currency
- <https://www.discogs.com/developers/#page:marketplace,header
- :marketplace-fee-with-currency-get>`_: Calculates the fee for
- selling an item on the marketplace given a particular currency.
-
- Parameters
- ----------
- price : `float`
- The price of the item (in the seller's currency).
-
- **Example**: :code:`10.00`.
-
- currency : `str`, keyword-only, default: :code:`"USD"`
- The currency abbreviation for the fee calculation.
-
- **Valid values**: :code:`"USD"`, :code:`"GBP"`, :code:`"EUR"`,
- :code:`"CAD"`, :code:`"AUD"`, :code:`"JPY"`, :code:`"CHF"`,
- :code:`"MXN"`, :code:`"BRL"`, :code:`"NZD"`, :code:`"SEK"`,
- and :code:`"ZAR"`.
-
- Returns
- -------
- fee : `dict`
- The fee for selling an item on the marketplace.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "value": <float>,
- "currency": <str>,
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/marketplace/fee/{price}/{currency}"
- )
-
-
-
-[docs]
- def get_price_suggestions(
- self, release_id: Union[int, str]) -> dict[str, Any]:
-
- """
- `Marketplace > Price Suggestions <https://www.discogs.com
- /developers/#page:marketplace,header
- :marketplace-price-suggestions>`_: Retrieve price suggestions in
- the user's selling currency for the provided release ID.
-
- If no suggestions are available, an empty object will be
- returned.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The ID of the release you are fetching.
-
- **Example**: :code:`249504`.
-
- Returns
- -------
- prices : `dict`
- The price suggestions for the release.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "Very Good (VG)": {
- "currency": <str>,
- "value": <float>
- },
- "Good Plus (G+)": {
- "currency": <str>,
- "value": <float>
- },
- "Near Mint (NM or M-)": {
- "currency": <str>,
- "value": <float>
- },
- "Good (G)": {
- "currency": <str>,
- "value": <float>
- },
- "Very Good Plus (VG+)": {
- "currency": <str>,
- "value": <float>
- },
- "Mint (M)": {
- "currency": <str>,
- "value": <float>
- },
- "Fair (F)": {
- "currency": <str>,
- "value": <float>
- },
- "Poor (P)": {
- "currency": <str>,
- "value": <float>
- }
- }
- """
-
- self._check_authentication("get_price_suggestions")
-
- return self._get_json(
- f"{self.API_URL}/marketplace/price_suggestions/{release_id}"
- )
-
-
-
-[docs]
- def get_release_marketplace_stats(
- self, release_id: Union[int, str], *, curr_abbr: str = None
- ) -> dict[str, Any]:
-
- """
- `Marketplace > Release Statistics <https://www.discogs.com
- /developers/#page:marketplace,header
- :marketplace-release-statistics-get>`_: Retrieve marketplace
- statistics for the provided release ID.
-
- These statistics reflect the state of the release in the
- marketplace currently, and include the number of items currently
- for sale, lowest listed price of any item for sale, and whether
- the item is blocked for sale in the marketplace.
-
- Releases that have no items or are blocked for sale in the
- marketplace will return a body with null data in the
- :code:`"lowest_price"` and :code:`"num_for_sale"` keys.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- Authentication is optional. Authenticated users will by
- default have the lowest currency expressed in their own buyer
- currency, configurable in buyer settings, in the absence of
- the `curr_abbr` query parameter to specify the currency.
- Unauthenticated users will have the price expressed in US
- Dollars, if no `curr_abbr` is provided.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The ID of the release you are fetching.
-
- **Example**: :code:`249504`.
-
- curr_abbr : `str`, keyword-only, optional
- Currency abbreviation for marketplace data.
-
- **Valid values**: :code:`"USD"`, :code:`"GBP"`,
- :code:`"EUR"`, :code:`"CAD"`, :code:`"AUD"`, :code:`"JPY"`,
- :code:`"CHF"`, :code:`"MXN"`, :code:`"BRL"`, :code:`"NZD"`,
- :code:`"SEK"`, and :code:`"ZAR"`.
-
- Returns
- -------
- stats : `dict`
- The marketplace statistics for the release.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "lowest_price": {
- "currency": <str>,
- "value": <float>
- },
- "num_for_sale": <int>,
- "blocked_from_sale": <bool>
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/marketplace/stats/{release_id}",
- params={"curr_abbr": curr_abbr}
- )
-
-
- ### INVENTORY EXPORT ######################################################
-
-
-[docs]
- def export_inventory(
- self, *, download: bool = True, filename: str = None,
- path: str = None) -> str:
-
- """
- `Inventory Export > Export Your Inventory <https://www.discogs.com
- /developers/#page:inventory-export,header
- :inventory-export-export-your-inventory-post>`_: Request an
- export of your inventory as a CSV.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- download : `bool`, keyword-only, default: :code:`True`
- Specifies whether to download the CSV file. If
- :code:`False`, the export ID is returned.
-
- filename : `str`, optional
- Filename of the exported CSV file. A :code:`.csv` extension
- will be appended if not present. If not specified, the CSV
- file is saved as
- :code:`<username>-inventory-<date>-<number>.csv`.
-
- path : `str`, optional
- Path to save the exported CSV file. If not specified, the
- file is saved in the current working directory.
-
- Returns
- -------
- path_or_id : `str`
- Full path to the exported CSV file (:code:`download=True`)
- or the export ID (:code:`download=False`).
- """
-
- self._check_authentication("export_inventory")
-
- r = self._request("post", f"{self.API_URL}/inventory/export")
- if download:
- return self.download_inventory_export(
- r.headers["Location"].split("/")[-1],
- filename=filename,
- path=path
- )
- return r.headers["Location"]
-
-
-
-[docs]
- def get_inventory_exports(
- self, *, page: int = None, per_page: int = None) -> dict[str, Any]:
-
- """
- `Inventory Export > Get Recent Exports <https://www.discogs.com
- /developers/#page:inventory-export,header
- :inventory-export-get-recent-exports-get>`_: Get all recent
- exports of your inventory.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- page : `int`, keyword-only, optional
- The page you want to request.
-
- **Example**: :code:`3`.
-
- per_page : `int`, keyword-only, optional
- The number of items per page.
-
- **Example**: :code:`25`.
-
- Returns
- -------
- exports : `dict`
- The authenticated user's inventory exports.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "items": [
- {
- "status": <str>,
- "created_ts": <str>,
- "url": <str>,
- "finished_ts": <str>,
- "download_url": <str>,
- "filename": <str>,
- "id": <int>
- }
- ],
- "pagination": {
- "per_page": <int>,
- "items": <int>,
- "page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- },
- "pages": <int>
- }
- }
- """
-
- self._check_authentication("get_inventory_exports")
-
- return self._get_json(f"{self.API_URL}/inventory/export",
- json={"page": page, "per_page": per_page})
-
-
-
-[docs]
- def get_inventory_export(self, export_id: int) -> dict[str, Union[int, str]]:
-
- """
- `Inventory Export > Get An Export <https://www.discogs.com
- /developers/#page:inventory-export,header
- :inventory-export-get-an-export-get>`_: Get details about the
- status of an inventory export.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- export_id : `int`
- ID of the export.
-
- Returns
- -------
- export : `dict`
- Details about the status of the inventory export.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "status": <str>,
- "created_ts": <str>,
- "url": <str>,
- "finished_ts": <str>,
- "download_url": <str>,
- "filename": <str>,
- "id": <int>
- }
- """
-
- self._check_authentication("get_inventory_export")
-
- return self._get_json(f"{self.API_URL}/inventory/export/{export_id}")
-
-
-
-[docs]
- def download_inventory_export(
- self, export_id: int, *, filename: str = None, path: str = None
- ) -> str:
-
- """
- `Inventory Export > Download An Export <https://www.discogs.com
- /developers/#page:inventory-export,header
- :inventory-export-download-an-export-get>`_: Download the
- results of an inventory export.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- export_id : `int`
- ID of the export.
-
- filename : `str`, optional
- Filename of the exported CSV file. A :code:`.csv` extension
- will be appended if not present. If not specified, the CSV
- file is saved as
- :code:`<username>-inventory-<date>-<number>.csv`.
-
- path : `str`, optional
- Path to save the exported CSV file. If not specified, the
- file is saved in the current working directory.
-
- Returns
- -------
- path : `str`
- Full path to the exported CSV file.
- """
-
- self._check_authentication("download_inventory_export")
-
- while True:
- r = self.get_inventory_export(export_id)
- if r["status"] == "success":
- break
- time.sleep(1)
-
- r = self._request(
- "get",
- f"{self.API_URL}/inventory/export/{export_id}/download"
- )
-
- if filename is None:
- filename = r.headers["Content-Disposition"].split("=")[1]
- else:
- if not filename.endswith(".csv"):
- filename += ".csv"
-
- with open(
- path := os.path.join(path or os.getcwd(), filename), "w"
- ) as f:
- f.write(r.text)
-
- return path
-
-
- ### INVENTORY UPLOAD ######################################################
-
- # TODO
-
- ### USER IDENTITY #########################################################
-
-
-[docs]
- def get_identity(self) -> dict[str, Any]:
-
- """
- `User Identity > Identity <https://www.discogs.com/developers
- /#page:user-identity,header:user-identity-identity-get>`_:
- Retrieve basic information about the authenticated user.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- You can use this resource to find out who you're authenticated
- as, and it also doubles as a good sanity check to ensure that
- you're using OAuth correctly.
-
- For more detailed information, make another request for the
- user's profile using :meth:`get_profile`.
-
- Returns
- -------
- identity : `dict`
- Basic information about the authenticated user.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "username": <str>,
- "resource_url": <str>,
- "consumer_name": <str>
- }
- """
-
- self._check_authentication("get_identity")
-
- return self._get_json(f"{self.API_URL}/oauth/identity")
-
-
-
-[docs]
- def get_profile(self, username: str = None) -> dict[str, Any]:
-
- """
- `User Identity > Profile > Get Profile
- <https://www.discogs.com/developers
- /#page:user-identity,header:user-identity-profile-get>`_:
- Retrieve a user by username.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- If authenticated as the requested user, the :code:`"email"`
- key will be visible, and the :code:`"num_lists"` count will
- include the user's private lists.
-
- If authenticated as the requested user or the user's
- collection/wantlist is public, the
- :code:`"num_collection"`/:code:`"num_wantlist"` keys will be
- visible.
-
- Parameters
- ----------
- username : `str`, optional
- The username of whose profile you are requesting. If not
- specified, the username of the authenticated user is used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- profile : `dict`
- Detailed information about the user.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "profile": <str>,
- "wantlist_url": <str>,
- "rank": <int>,
- "num_pending": <int>,
- "id": <int>,
- "num_for_sale": <int>,
- "home_page": <str>,
- "location": <str>,
- "collection_folders_url": <str>,
- "username": <str>,
- "collection_fields_url": <str>,
- "releases_contributed": <int>,
- "registered": <str>,
- "rating_avg": <float>,
- "num_collection": <int>,
- "releases_rated": <int>,
- "num_lists": <int>,
- "name": <str>,
- "num_wantlist": <int>,
- "inventory_url": <str>,
- "avatar_url": <str>,
- "banner_url": <str>,
- "uri": <str>,
- "resource_url": <str>,
- "buyer_rating": <float>,
- "buyer_rating_stars": <int>,
- "buyer_num_ratings": <int>,
- "seller_rating": <float>,
- "seller_rating_stars": <int>,
- "seller_num_ratings": <int>,
- "curr_abbr": <str>,
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
- return self._get_json(f"{self.API_URL}/users/{username}")
-
-
-
-[docs]
- def edit_profile(
- self, *, name: str = None, home_page: str = None,
- location: str = None, profile: str = None,
- curr_abbr: str = None) -> dict[str, Any]:
-
- """
- `User Identity > Profile > Edit Profile
- <https://www.discogs.com/developers
- /#page:user-identity,header:user-identity-profile-post>`_:
- Edit a user's profile data.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- name : `str`, keyword-only, optional
- The real name of the user.
-
- **Example**: :code:`"Nicolas Cage"`.
-
- home_page : `str`, keyword-only, optional
- The user's website.
-
- **Example**: :code:`"www.discogs.com"`.
-
- location : `str`, keyword-only, optional
- The geographical location of the user.
-
- **Example**: :code:`"Portland"`.
-
- profile : `str`, keyword-only, optional
- Biological information about the user.
-
- **Example**: :code:`"I am a Discogs user!"`.
-
- curr_abbr : `str`, keyword-only, optional
- Currency abbreviation for marketplace data.
-
- **Valid values**: :code:`"USD"`, :code:`"GBP"`,
- :code:`"EUR"`, :code:`"CAD"`, :code:`"AUD"`, :code:`"JPY"`,
- :code:`"CHF"`, :code:`"MXN"`, :code:`"BRL"`, :code:`"NZD"`,
- :code:`"SEK"`, and :code:`"ZAR"`.
-
- Returns
- -------
- profile : `dict`
- Updated profile.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <int><int>,
- "username": <str>,
- "name": <str>,
- "email": <str>,
- "resource_url": <str>,
- "inventory_url": <str>,
- "collection_folders_url": <str>,
- "collection_fields_url": <str>,
- "wantlist_url": <str>,
- "uri": <str>,
- "profile": <str>,
- "home_page": <str>,
- "location": <str>,
- "registered": <str>,
- "num_lists": <int>,
- "num_for_sale": <int>,
- "num_collection": <int>,
- "num_wantlist": <int>,
- "num_pending": <int>,
- "releases_contributed": <int>,
- "rank": <int>,
- "releases_rated": <int>,
- "rating_avg": <float>
- }
- """
-
- self._check_authentication("edit_profile")
-
- if name is None and home_page is None and location is None \
- and profile is None and curr_abbr is None:
- wmsg = "No changes were specified or made to the user profile."
- warnings.warn(wmsg)
- return
-
- if curr_abbr and curr_abbr not in (
- CURRENCIES := {
- "USD", "GBP", "EUR", "CAD", "AUD", "JPY",
- "CHF", "MXN", "BRL", "NZD", "SEK", "ZAR"
- }
- ):
- emsg = (f"Invalid currency abbreviation ({curr_abbr=}). "
- f"Valid values: {', '.join(CURRENCIES)}.")
- raise ValueError(emsg)
-
- return self._request(
- "post",
- f"{self.API_URL}/users/{self._username}",
- json={
- "name": name,
- "home_page": home_page,
- "location": location,
- "profile": profile,
- "curr_abbr": curr_abbr
- }
- ).json()
-
-
-
-[docs]
- def get_user_submissions(
- self, username: str = None, *, page: Union[int, str] = None,
- per_page: Union[int, str] = None) -> dict[str, Any]:
-
- """
- `User Identity > User Submissions <https://www.discogs.com
- /developers/#page:user-identity,header
- :user-identity-user-submissions-get>`_: Retrieve a user's
- submissions (edits made to releases, labels, and artists) by
- username.
-
- Parameters
- ----------
- username : `str`, optional
- The username of the submissions you are trying to fetch. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"shooezgirl"`.
-
- page : `int` or `str`, keyword-only, optional
- Page of results to fetch.
-
- per_page : `int` or `str`, keyword-only, optional
- Number of results per page.
-
- Returns
- -------
- submissions : `dict`
- Submissions made by the user.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "items": <int>,
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- }
- },
- "submissions": {
- "artists": [
- {
- "data_quality": <str>,
- "id": <int>,
- "name": <str>,
- "namevariations": [<str>],
- "releases_url": <str>,
- "resource_url": <str>,
- "uri": <str>
- }
- ],
- "labels": [],
- "releases": [
- {
- "artists": [
- {
- "anv": <str>,
- "id": <int>,
- "join": <str>,
- "name": <str>,
- "resource_url": <str>,
- "role": <str>,
- "tracks": <str>
- }
- ],
- "community": {
- "contributors": [
- {
- "resource_url": <str>,
- "username": <str>
- }
- ],
- "data_quality": <str>,
- "have": <int>,
- "rating": {
- "average": <int>,
- "count": <int>
- },
- "status": <str>,
- "submitter": {
- "resource_url": <str>,
- "username": <str>
- },
- "want": <int>
- },
- "companies": [],
- "country": <str>,
- "data_quality": <str>,
- "date_added": <str>,
- "date_changed": <str>,
- "estimated_weight": <int>,
- "format_quantity": <int>,
- "formats": [
- {
- "descriptions": [<str>],
- "name": <str>,
- "qty": <str>
- }
- ],
- "genres": [<str>],
- "id": <int>,
- "images": [
- {
- "height": <int>,
- "resource_url": <str>,
- "type": <str>,
- "uri": <str>,
- "uri150": <str>,
- "width": <int>
- }
- ],
- "labels": [
- {
- "catno": <str>,
- "entity_type": <str>,
- "id": <int>,
- "name": <str>,
- "resource_url": <str>
- }
- ],
- "master_id": <int>,
- "master_url": <str>,
- "notes": <str>,
- "released": <str>,
- "released_formatted": <str>,
- "resource_url": <str>,
- "series": [],
- "status": <str>,
- "styles": [<str>],
- "thumb": <str>,
- "title": <str>,
- "uri": <str>,
- "videos": [
- {
- "description": <str>,
- "duration": <int>,
- "embed": <bool>,
- "title": <str>,
- "uri": <str>
- }
- ],
- "year": <int>
- }
- ]
- }
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/submissions",
- params={"page": page, "per_page": per_page}
- )
-
-
-
-[docs]
- def get_user_contributions(
- self, username: str = None, *, page: Union[int, str] = None,
- per_page: Union[int, str] = None, sort: str = None,
- sort_order: str = None) -> dict[str, Any]:
-
- """
- `User Identity > User Contributions <https://www.discogs.com
- /developers/#page:user-identity,header
- :user-identity-user-contributions-get>`_: Retrieve a user's
- contributions (releases, labels, artists) by username.
-
- Parameters
- ----------
- username : `str`, optional
- The username of the contributions you are trying to fetch.
- If not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"shooezgirl"`.
-
- page : `int` or `str`, keyword-only, optional
- Page of results to fetch.
-
- per_page : `int` or `str`, keyword-only, optional
- Number of results per page.
-
- sort : `str`, keyword-only, optional
- Sort items by this field.
-
- **Valid values**: :code:`"label"`, :code:`"artist"`,
- :code:`"title"`, :code:`"catno"`, :code:`"format"`,
- :code:`"rating"`, :code:`"year"`, and :code:`"added"`.
-
- sort_order : `str`, keyword-only, optional
- Sort items in a particular order.
-
- **Valid values**: :code:`"asc"` and :code:`"desc"`.
-
- Returns
- -------
- contributions : `dict`
- Contributions made by the user.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "items": <int>,
- "page": <int>,
- "pages": <int>,
- "per_page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- }
- },
- "contributions": [
- {
- "artists": [
- {
- "anv": <str>,
- "id": <int>,
- "join": <str>,
- "name": <str>,
- "resource_url": <str>,
- "role": <str>,
- "tracks": <str>
- }
- ],
- "community": {
- "contributors": [
- {
- "resource_url": <str>,
- "username": <str>
- }
- ],
- "data_quality": <str>,
- "have": <int>,
- "rating": {
- "average": <int>,
- "count": <int>
- },
- "status": <str>,
- "submitter": {
- "resource_url": <str>,
- "username": <str>
- },
- "want": <int>
- },
- "companies": [],
- "country": <str>,
- "data_quality": <str>,
- "date_added": <str>,
- "date_changed": <str>,
- "estimated_weight": <int>,
- "format_quantity": <int>,
- "formats": [
- {
- "descriptions": [<str>],
- "name": <str>,
- "qty": <str>
- }
- ],
- "genres": [<str>],
- "id": <int>,
- "images": [
- {
- "height": <int>,
- "resource_url": <str>,
- "type": <str>,
- "uri": <str>,
- "uri150": <str>,
- "width": <int>
- }
- ],
- "labels": [
- {
- "catno": <str>,
- "entity_type": <str>,
- "id": <int>,
- "name": <str>,
- "resource_url": <str>
- }
- ],
- "master_id": <int>,
- "master_url": <str>,
- "notes": <str>,
- "released": <str>,
- "released_formatted": <str>,
- "resource_url": <str>,
- "series": [],
- "status": <str>,
- "styles": [<str>],
- "thumb": <str>,
- "title": <str>,
- "uri": <str>,
- "videos": [
- {
- "description": <str>,
- "duration": <int>,
- "embed": <bool>,
- "title": <str>,
- "uri": <str>
- }
- ],
- "year": <int>
- }
- ]
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/contributions",
- params={
- "page": page,
- "per_page": per_page,
- "sort": sort,
- "sort_order": sort_order
- }
- )
-
-
- ### USER COLLECTION #######################################################
-
-
-[docs]
- def get_collection_folders(
- self, username: str = None) -> list[dict[str, Any]]:
-
- """
- `User Collection > Collection > Get Collection Folders
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-get>`_: Retrieve a list of folders
- in a user's collection.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- If the collection has been made private by its owner,
- authentication as the collection owner is required. If you
- are not authenticated as the collection owner, only folder ID
- :code:`0` (the "All" folder) will be visible (if the
- requested user's collection is public).
-
- Parameters
- ----------
- username : `str`, optional
- The username of the collection you are trying to fetch. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- folders : `list`
- A list of folders in the user's collection.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- [
- {
- "id": <int>,
- "name": <str>,
- "count": <int>,
- "resource_url": <str>
- }
- ]
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/collection/folders"
- )["folders"]
-
-
-
-[docs]
- def create_collection_folder(self, name: str) -> dict[str, Union[int, str]]:
-
- """
- `User Collection > Collection > Create Folder
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-post>`_: Create a new folder in a
- user's collection.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- name : `str`
- The name of the newly-created folder.
-
- **Example**: :code:`"My favorites"`.
-
- Returns
- -------
- folder : `dict`
- Information about the newly-created folder.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "count": <int>,
- "resource_url": <str>
- }
- """
-
- self._check_authentication("create_collection_folder")
-
- return self._request(
- "post",
- f"{self.API_URL}/users/{self._username}/collection/folders",
- json={"name": name}
- ).json()
-
-
-
-[docs]
- def get_collection_folder(
- self, folder_id: int, *, username: str = None
- ) -> dict[str, Union[int, str]]:
-
- """
- `User Collection > Collection Folder > Get Folders
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-folder-get>`_: Retrieve metadata
- about a folder in a user's collection.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- If `folder_id` is not :code:`0`, authentication as the
- collection owner is required.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to request.
-
- **Example**: :code:`3`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to request. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- folder : `dict`
- Metadata about the folder.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "count": <int>,
- "resource_url": <str>
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- if folder_id != 0:
- self._check_authentication("get_collection_folder")
-
- return self._get_json(f"{self.API_URL}/users/{self._username}"
- f"/collection/folders/{folder_id}")
-
-
-
-[docs]
- def rename_collection_folder(
- self, folder_id: int, name: str, *,
- username: str = None) -> dict[str, Union[int, str]]:
-
- """
- `User Collection > Collection Folder > Edit Folder
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-folder-post>`_: Rename a folder.
-
- Folders :code:`0` and :code:`1` cannot be renamed.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to modify.
-
- **Example**: :code:`3`.
-
- name : `str`
- The new name of the folder.
-
- **Example**: :code:`"My favorites"`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to modify. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- folder : `dict`
- Information about the edited folder.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "count": <int>,
- "resource_url": <str>
- }
- """
-
- self._check_authentication("rename_collection_folder")
-
- return self._request(
- "post",
- f"{self.API_URL}/users/{self._username}"
- f"/collection/folders/{folder_id}",
- json={"name": name}
- ).json()
-
-
-
-[docs]
- def delete_collection_folder(
- self, folder_id: int, *, username: str = None) -> None:
-
- """
- `User Collection > Collection Folder > Delete Folder
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-folder-delete>`_: Delete a folder
- from a user's collection.
-
- A folder must be empty before it can be deleted.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to delete.
-
- **Example**: :code:`3`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to delete. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
- """
-
- self._check_authentication("delete_collection_folder")
-
- self._request(
- "delete",
- f"{self.API_URL}/users/{self._username}"
- f"/collection/folders/{folder_id}"
- )
-
-
-
-[docs]
- def get_collection_folders_by_release(
- self, release_id: Union[int, str], *, username: str = None
- ) -> dict[str, Any]:
-
- """
- `User Collection > Collection Items By Release
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-items-by-release-get>`_: View the
- user's collection folders which contain a specified release.
- This will also show information about each release instance.
-
- Parameters
- ----------
- release_id : `int` or `str`
- The ID of the release to request.
-
- **Example**: :code:`7781525`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to view. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"susan.salkeld"`.
-
- Returns
- -------
- releases : `list`
- A list of releases and their folders.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "per_page": <int>,
- "items": <int>,
- "page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- },
- "pages": <int>
- },
- "releases": [
- {
- "instance_id": <int>,
- "rating": <int>,
- "basic_information": {
- "labels": [
- {
- "name": <str>,
- "entity_type": <str>,
- "catno": <str>,
- "resource_url": <str>,
- "id": <int>,
- "entity_type_name": <str>
- }
- ],
- "formats": [
- {
- "descriptions": [<str>],
- "name": <str>,
- "qty": <str>
- }
- ],
- "thumb": <str>,
- "title": <str>,
- "artists": [
- {
- "join": <str>,
- "name": <str>,
- "anv": <str>,
- "tracks": <str>,
- "role": <str>,
- "resource_url": <str>,
- "id": <int>
- }
- ],
- "resource_url": <str>,
- "year": <int>,
- "id": <int>,
- },
- "folder_id": <int>,
- "date_added": <str>,
- "id": <int>
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/users/{self._username}/collection"
- f"/releases/{release_id}"
- )["folders"]
-
-
-
-[docs]
- def get_collection_folder_releases(
- self, folder_id: int, *, username: str = None,
- page: int = None, per_page: int = None, sort: str = None,
- sort_order: str = None) -> dict[str, Any]:
-
- """
- `User Collection > Collection Items By Folder
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-items-by-folder>`_: Returns the items
- in a folder in a user's collection.
-
- Basic information about each release is provided, suitable for
- display in a list. For detailed information, make another call
- to fetch the corresponding release.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- If `folder_id` is not :code:`0` or the collection has been
- made private by its owner, authentication as the collection
- owner is required.
-
- If you are not authenticated as the collection owner, only
- the public notes fields will be visible.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to request.
-
- **Example**: :code:`3`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to request. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- page : `int`, keyword-only, optional
- Page of results to fetch.
-
- per_page : `int`, keyword-only, optional
- Number of results per page.
-
- sort : `str`, keyword-only, optional
- Sort items by this field.
-
- **Valid values**: :code:`"label"`, :code:`"artist"`,
- :code:`"title"`, :code:`"catno"`, :code:`"format"`,
- :code:`"rating"`, :code:`"year"`, and :code:`"added"`.
-
- sort_order : `str`, keyword-only, optional
- Sort items in a particular order.
-
- **Valid values**: :code:`"asc"` and :code:`"desc"`.
-
- Returns
- -------
- items : `dict`
- Items in the folder.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "pagination": {
- "per_page": <int>,
- "items": <int>,
- "page": <int>,
- "urls": {
- "last": <str>,
- "next": <str>
- },
- "pages": <int>
- },
- "releases": [
- {
- "instance_id": <int>,
- "rating": <int>,
- "basic_information": {
- "labels": [
- {
- "name": <str>,
- "entity_type": <str>,
- "catno": <str>,
- "resource_url": <str>,
- "id": <int>,
- "entity_type_name": <str>
- }
- ],
- "formats": [
- {
- "descriptions": [<str>],
- "name": <str>,
- "qty": <str>
- }
- ],
- "thumb": <str>,
- "title": <str>,
- "artists": [
- {
- "join": <str>,
- "name": <str>,
- "anv": <str>,
- "tracks": <str>,
- "role": <str>,
- "resource_url": <str>,
- "id": <int>
- }
- ],
- "resource_url": <str>,
- "year": <int>,
- "id": <int>,
- },
- "folder_id": <int>,
- "date_added": <str>,
- "id": <int>
- }
- ]
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- if folder_id != 0:
- self._check_authentication("get_collection_folder_releases")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/collection"
- f"/folders/{folder_id}/releases",
- params={"page": page, "per_page": per_page, "sort": sort,
- "sort_order": sort_order}
- )
-
-
-
-[docs]
- def add_collection_folder_release(
- self, folder_id: int, release_id: int, *, username: str = None
- ) -> dict[str, Union[int, str]]:
-
- """
- `User Collection > Add To Collection Folder
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-add-to-collection-folder-post>`_: Add a release
- to a folder in a user's collection.
-
- The `folder_id` must be non-zero. You can use :code:`1` for
- "Uncategorized".
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to modify.
-
- **Example**: :code:`3`.
-
- release_id : `int`
- The ID of the release you are adding.
-
- **Example**: :code:`130076`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to modify. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- folder : `dict`
- Information about the folder.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "instance_id": <int>,
- "resource_url": <str>
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._request(
- "post",
- f"{self.API_URL}/users/{username}/collection"
- f"/folders/{folder_id}/releases/{release_id}"
- ).json()
-
-
-
-[docs]
- def edit_collection_folder_release(
- self, folder_id: int, release_id: int, instance_id: int,
- *, username: str = None, new_folder_id: int, rating: int = None
- ) -> None:
-
- """
- `User Collection > Change Rating Of Release
- <https://www.discogs.com/developers#page:user-collection,header
- :user-collection-change-rating-of-release-post>`_: Change the
- rating on a release and/or move the instance to another folder.
-
- This endpoint potentially takes two folder ID parameters:
- `folder_id` (which is the folder you are requesting, and is
- required), and `new_folder_id` (representing the folder you want
- to move the instance to, which is optional).
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to modify.
-
- **Example**: :code:`3`.
-
- release_id : `int`
- The ID of the release you are modifying.
-
- **Example**: :code:`130076`.
-
- instance_id : `int`
- The ID of the instance.
-
- **Example**: :code:`1`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to modify. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- new_folder_id : `int`
- The ID of the folder to move the instance to.
-
- **Example**: :code:`4`.
-
- rating : `int`, keyword-only, optional
- The rating of the instance you are supplying.
-
- **Example**: :code:`5`.
- """
-
- self._request(
- "post",
- f"{self.API_URL}/users/{username}/collection/folders"
- f"/{folder_id}/releases/{release_id}/instances/{instance_id}",
- json={"folder_id": new_folder_id, "rating": rating}
- )
-
-
-
-[docs]
- def delete_collection_folder_release(
- self, folder_id: int, release_id: int, instance_id: int,
- *, username: str = None) -> None:
-
- """
- `User Collection > Delete Instance From Folder
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-delete-instance-from-folder-delete>`_: Remove an
- instance of a release from a user's collection folder.
-
- To move the release to the "Uncategorized" folder instead, use
- the :meth:`edit_collection_folder_release` method.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to modify.
-
- **Example**: :code:`3`.
-
- release_id : `int`
- The ID of the release you are modifying.
-
- **Example**: :code:`130076`.
-
- instance_id : `int`
- The ID of the instance.
-
- **Example**: :code:`1`.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to modify. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
- """
-
- self._request(
- "delete",
- f"{self.API_URL}/users/{username}/collection/folders"
- f"/{folder_id}/releases/{release_id}/instances/{instance_id}"
- )
-
-
-
-[docs]
- def get_collection_fields(
- self, username: str = None) -> list[dict[str, Any]]:
-
- """
- `User Collection > Collection Fields
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-list-custom-fields-get>`_: Retrieve a list of
- user-defined collection notes fields.
-
- These fields are available on every release in the collection.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- If the collection has been made private by its owner,
- authentication as the collection owner is required.
-
- If you are not authenticated as the collection owner, only
- fields with public set to true will be visible.
-
- Parameters
- ----------
- username : `str`, optional
- The username of the collection you are trying to fetch. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- fields : `list`
- A list of user-defined collection fields.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- [
- {
- "id": <int>,
- "name": <str>,
- "options": [<str>],
- "public": <bool>,
- "position": <int>,
- "type": "dropdown"
- },
- {
- "id": <int>,
- "name": <str>,
- "lines": <int>,
- "public": <bool>,
- "position": <int>,
- "type": "textarea"
- }
- ]
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/collection/fields"
- )["fields"]
-
-
-
-[docs]
- def edit_collection_release_field(
- self, folder_id: int, release_id: int, instance_id: int,
- field_id: int, value: str, *, username: str = None) -> None:
-
- """
- `User Collection > Edit Fields Instance
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-edit-fields-instance-post>`_: Change the value
- of a notes field on a particular instance.
-
- .. admonition:: User authentication
- :class: dropdown warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- folder_id : `int`
- The ID of the folder to modify.
-
- **Example**: :code:`3`.
-
- release_id : `int`
- The ID of the release you are modifying.
-
- **Example**: :code:`130076`.
-
- instance_id : `int`
- The ID of the instance.
-
- **Example**: :code:`1`.
-
- field_id : `int`
- The ID of the field you are modifying.
-
- **Example**: :code:`1`.
-
- value : `str`
- The new value of the field. If the field's type is
- :code:`"dropdown"`, `value` must match one of the values in
- the field's list of options.
-
- username : `str`, keyword-only, optional
- The username of the collection you are trying to modify. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
- """
-
- self._check_authentication("edit_collection_fields")
-
- self._request(
- "post",
- f"{self.API_URL}/users/{username}/collection/folders"
- f"/{folder_id}/releases/{release_id}/instances/{instance_id}"
- f"/fields/{field_id}",
- params={"value": value}
- )
-
-
-
-[docs]
- def get_collection_value(self, username: str = None) -> dict[str, Any]:
-
- """
- `User Collection > Collection Value
- <https://www.discogs.com/developers/#page:user-collection,header
- :user-collection-collection-value-get>`_: Returns the minimum,
- median, and maximum value of a user's collection.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication with a personal access token or
- via the OAuth 1.0a flow.
-
- Parameters
- ----------
- username : `str`, optional
- The username of the collection you are trying to fetch. If
- not specified, the username of the authenticated user is
- used.
-
- **Example**: :code:`"rodneyfool"`.
-
- Returns
- -------
- value : `dict`
- The total minimum value of the user's collection.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "maximum": <str>,
- "median": <str>,
- "minimum": <str>
- }
- """
-
- if username is None:
- if hasattr(self, "_username"):
- username = self._username
- else:
- raise ValueError("No username provided.")
-
- return self._get_json(
- f"{self.API_URL}/users/{username}/collection/value"
- )
-
-
-
- ### USER WANTLIST #########################################################
-
- # TODO
-
- ### USER LISTS ############################################################
-
- # TODO
-
-"""
-iTunes
-======
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module contains a complete implementation of all iTunes Search API
-endpoints.
-"""
-
-import requests
-from typing import Any, Union
-
-__all__ = ["SearchAPI"]
-
-
-[docs]
-class SearchAPI:
-
- """
- iTunes Search API client.
-
- The iTunes Search API allows searching for a variety of content,
- including apps, iBooks, movies, podcasts, music, music videos,
- audiobooks, and TV shows within the iTunes Store, App Store,
- iBooks Store and Mac App Store. It also supports ID-based lookup
- requests to create mappings between your content library and the
- digital catalog.
-
- .. seealso::
-
- For more information, see the `iTunes Search API
- documentation <https://developer.apple.com/library/archive/
- documentation/AudioVideo/Conceptual/iTuneSearchAPI/index.html>`_.
-
- Attributes
- ----------
- API_URL : `str`
- Base URL for the iTunes Search API.
- """
-
- API_URL = "https://itunes.apple.com"
-
- def __init__(self) -> None:
-
- """
- Create a iTunes Search API client.
- """
-
- self.session = requests.Session()
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _request(
- self, method: str, url: str, **kwargs
- ) -> requests.Response:
-
- """
- Construct and send a request, but with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code not in range(200, 299):
- raise RuntimeError(f"{r.status_code} {r.json()['errorMessage']}")
- return r
-
-
-[docs]
- def search(
- self, term: str, *, country: str = None, media: str = None,
- entity: Union[str, list[str]] = None, attribute: str = None,
- limit: Union[int, str] = None, lang: str = None,
- version: Union[int, str] = None, explicit: Union[bool, str] = None
- ) -> dict[str, Any]:
-
- """
- Search for content using the iTunes Search API.
-
- Parameters
- ----------
- term : str
- The text string to search for.
-
- .. note::
-
- URL encoding replaces spaces with the plus (:code:`+`)
- character, and all characters except letters, numbers,
- periods (:code:`.`), dashes (:code:`-`), underscores
- (:code:`_`), and asterisks (:code:`*`) are encoded.
-
- **Example**: :code:`"jack+johnson"`.
-
- country : str, keyword-only, optional
- The two-letter country code for the store you want to search.
- The search uses the default store front for the specified
- country.
-
- .. seealso::
-
- For a list of ISO country codes, see the
- `ISO OBP <https://www.iso.org/obp/ui>`_.
-
- **Default**: :code:`"US"`.
-
- media : str, keyword-only, optional
- The media type you want to search for.
-
- .. container::
-
- **Valid values**: :code:`"movie"`, :code:`"podcast"`,
- :code:`"music"`, :code:`"musicVideo"`, :code:`"audioBook"`,
- :code:`"shortFilm"`, :code:`"tvShow"`, :code:`"software"`,
- and :code:`"ebook"`.
-
- **Default**: :code:`"all"`.
-
- entity : `str` or `list`, keyword-only, optional
- The type(s) of results you want returned, relative to the
- specified media type in `media`.
-
- .. seealso::
-
- For a list of available
- entities, see the `iTunes Store API Table 2-1
- <https://developer.apple.com/library/archive
- /documentation/AudioVideo/Conceptual/iTuneSearchAPI
- /Searching.html#//apple_ref/doc/uid
- /TP40017632-CH5-SW2>`_.
-
- **Default**: The track entity associated with the specified
- media type.
-
- **Example**: :code:`"movieArtist"` for a movie media type
- search.
-
- attribute : `str`, keyword-only, optional
- The attribute you want to search for in the stores, relative
- to the specified media type (`media`).
-
- .. seealso::
-
- For a list of available
- attributes, see the `iTunes Store API Table 2-2
- <https://developer.apple.com/library/archive
- /documentation/AudioVideo/Conceptual/iTuneSearchAPI
- /Searching.html#//apple_ref/doc/uid
- /TP40017632-CH5-SW3>`_.
-
- **Default**: All attributes associated with the specified
- media type.
-
- **Example**: If you want to search for an artist by name,
- specify :code:`entity="allArtist"` and
- :code:`attribute="allArtistTerm"`. Then, if you search for
- :code:`term="maroon"`, iTunes returns "Maroon 5" in the
- search results, instead of all artists who have ever
- recorded a song with the word "maroon" in the title.
-
- limit : `int` or `str`, keyword-only, optional
- The number of search results you want the iTunes Store to
- return.
-
- **Valid values**: `limit` must be between 1 and 200.
-
- **Default**: :code:`50`.
-
- lang : `str`, keyword-only, optional
- The language, English or Japanese, you want to use when
- returning search results. Specify the language using the
- five-letter codename.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"en_us"` for English.
- * :code:`"ja_jp"` for Japanese.
-
- **Default**: :code:`"en_us"`.
-
- version : `int` or `str`, keyword-only, optional
- The search result key version you want to receive back from
- your search.
-
- **Valid values**: :code:`1` and :code:`2`.
-
- **Default**: :code:`2`.
-
- explicit : `bool` or `str`, keyword-only, optional
- A flag indicating whether or not you want to include
- explicit content in your search results.
-
- **Default**: :code:`"Yes"`.
-
- Returns
- -------
- results : `dict`
- The search results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "resultCount": <int>,
- "results": [
- {
- "wrapperType": <str>,
- "kind": <str>,
- "artistId": <int>,
- "collectionId": <int>,
- "trackId": <int>,
- "artistName": <str>,
- "collectionName": <str>,
- "trackName": <str>,
- "collectionCensoredName": <str>,
- "trackCensoredName": <str>,
- "collectionArtistId": <int>,
- "collectionArtistName": <str>,
- "artistViewUrl": <str>,
- "collectionViewUrl": <str>,
- "trackViewUrl": <str>,
- "previewUrl": <str>,
- "artworkUrl30": <str>,
- "artworkUrl60": <str>,
- "artworkUrl100": <str>,
- "collectionPrice": <float>,
- "trackPrice": <float>,
- "releaseDate": <str>,
- "collectionExplicitness": <str>,
- "trackExplicitness": <str>,
- "discCount": <int>,
- "discNumber": <int>,
- "trackCount": <int>,
- "trackNumber": <int>,
- "trackTimeMillis": <int>,
- "country": <str>,
- "currency": <str>,
- "primaryGenreName": <str>,
- "isStreamable": <bool>
- }
- ]
- }
-
- Examples
- --------
- To search for all Jack Johnson audio and video content (movies,
- podcasts, music, music videos, audiobooks, short films, and TV
- shows),
-
- >>> itunes.search("jack johnson")
-
- To search for all Jack Johnson audio and video content and
- return only the first 25 items,
-
- >>> itunes.search("jack johnson", limit=25)
-
- To search for only Jack Johnson music videos,
-
- >>> itunes.search("jack johnson", entity="musicVideo")
-
- To search for all Jim Jones audio and video content and return
- only the results from the Canada iTunes Store,
-
- >>> itunes.search("jack johnson", country="ca")
-
- To search for applications titled “Yelp” and return only the
- results from the United States iTunes Store,
-
- >>> itunes.search("yelp", country="us", entity="software")
- """
-
- return self._get_json(
- f"{self.API_URL}/search",
- params={
- "term": term,
- "country": country,
- "media": media,
- "entity": entity if entity is None or isinstance(entity, str)
- else ",".join(entity),
- "attribute": attribute,
- "limit": limit,
- "lang": lang,
- "version": version,
- "explicit": ("No", "Yes")[explicit]
- if isinstance(explicit, bool) else explicit
- }
- )
-
-
-
-[docs]
- def lookup(
- self, id: Union[int, str, list[Union[int, str]]] = None, *,
- amg_artist_id: Union[int, str, list[Union[int, str]]] = None,
- amg_album_id: Union[int, str, list[Union[int, str]]] = None,
- amg_video_id: Union[int, str, list[Union[int, str]]] = None,
- bundle_id: Union[str, list[str]] = None,
- upc: Union[int, str, list[Union[int, str]]] = None,
- isbn: Union[int, str, list[Union[int, str]]] = None,
- entity: Union[str, list[str]] = None,
- limit: Union[int, str] = None, sort: str = None
- ) -> dict[str, Any]:
-
- """
- Search for content based on iTunes IDs, AMG IDs, UPCs/EANs, or
- ISBNs. ID-based lookups are faster and contain fewer
- false-positive results.
-
- Parameters
- ----------
- id : `int`, `str`, or `list`, optional
- The iTunes ID(s) to lookup.
-
- amg_artist_id : `int`, `str`, or `list`, keyword-only, optional
- The AMG artist ID(s) to lookup.
-
- amg_album_id : `int`, `str`, or `list`, keyword-only, optional
- The AMG album ID(s) to lookup.
-
- amg_video_id : `int`, `str`, or `list`, keyword-only, optional
- The AMG video ID(s) to lookup.
-
- bundle_id : `str` or `list`, keyword-only, optional
- The Apple bundle ID(s) to lookup.
-
- upc : `int`, `str`, or `list`, keyword-only, optional
- The UPC(s) to lookup.
-
- isbn : `int`, `str`, or `list`, keyword-only, optional
- The 13-digit ISBN(s) to lookup.
-
- entity : `str` or `list`, keyword-only, optional
- The type(s) of results you want returned.
-
- .. seealso::
-
- For a list of available entities, see the `iTunes Store
- API Table 2-1 <https://developer.apple.com/library
- /archive/documentation/AudioVideo/Conceptual
- /iTuneSearchAPI/Searching.html#//apple_ref/doc/uid
- /TP40017632-CH5-SW2>`_.
-
- **Default**: The track entity associated with the specified
- media type.
-
- limit : `int` or `str`, keyword-only, optional
- The number of search results you want the iTunes Store to
- return.
-
- **Valid values**: `limit` must be between 1 and 200.
-
- **Default**: :code:`50`.
-
- sort : `str`, keyword-only, optional
- The sort applied to the search results.
-
- **Allowed value**: :code:`"recent"`.
-
- Returns
- -------
- results : `dict`
- The lookup results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "resultCount": <int>,
- "results": [
- {
- "wrapperType": <str>,
- "kind": <str>,
- "artistId": <int>,
- "collectionId": <int>,
- "trackId": <int>,
- "artistName": <str>,
- "collectionName": <str>,
- "trackName": <str>,
- "collectionCensoredName": <str>,
- "trackCensoredName": <str>,
- "collectionArtistId": <int>,
- "collectionArtistName": <str>,
- "artistViewUrl": <str>,
- "collectionViewUrl": <str>,
- "trackViewUrl": <str>,
- "previewUrl": <str>,
- "artworkUrl30": <str>,
- "artworkUrl60": <str>,
- "artworkUrl100": <str>,
- "collectionPrice": <float>,
- "trackPrice": <float>,
- "releaseDate": <str>,
- "collectionExplicitness": <str>,
- "trackExplicitness": <str>,
- "discCount": <int>,
- "discNumber": <int>,
- "trackCount": <int>,
- "trackNumber": <int>,
- "trackTimeMillis": <int>,
- "country": <str>,
- "currency": <str>,
- "primaryGenreName": <str>,
- "isStreamable": <bool>
- }
- ]
- }
-
- Examples
- --------
- Look up Jack Johnson by iTunes artist ID:
-
- >>> itunes.lookup(909253)
-
- Look up the Yelp application by iTunes ID:
-
- >>> itunes.lookup(284910350)
-
- Look up Jack Johnson by AMG artist ID:
-
- >>> itunes.lookup(amg_artist_id=468749)
-
- Look up multiple artists by their AMG artist IDs:
-
- >>> itunes.lookup(amg_artist_id=[468749, 5723])
-
- Look up all albums for Jack Johnson:
-
- >>> itunes.lookup(909253, entity="album")
-
- Look up multiple artists by their AMG artist IDs and get each
- artist's top 5 albums:
-
- >>> itunes.lookup(amg_artist_id=[468749, 5723], entity="album",
- ... limit=5)
-
- Look up multiple artists by their AMG artist IDs and get each
- artist's 5 most recent songs:
-
- >>> itunes.lookup(amg_artist_id=[468749, 5723], entity="song",
- ... limit=5, sort="recent")
-
- Look up an album or video by its UPC:
-
- >>> itunes.lookup(upc=720642462928)
-
- Look up an album by its UPC, including the tracks on that album:
-
- >>> itunes.lookup(upc=720642462928, entity="song")
-
- Look up an album by its AMG Album ID:
-
- >>> itunes.lookup(amg_album_id=[15175, 15176, 15177, 15178,
- ... 15183, 15184, 15187, 15190,
- ... 15191, 15195, 15197, 15198])
-
- Look up a Movie by AMG Video ID:
-
- >>> itunes.lookup(amg_video_id=17120)
-
- Look up a book by its 13-digit ISBN:
-
- >>> itunes.lookup(isbn=9780316069359)
-
- Look up the Yelp application by iTunes bundle ID:
-
- >>> itunes.lookup(bundle_id="com.yelp.yelpiphone")
- """
-
- return self._get_json(
- f"{self.API_URL}/lookup",
- params={
- "id": id if id is None or isinstance(id, (int, str))
- else ",".join(id if isinstance(id[0], str)
- else (str(i) for i in id)),
- "amgArtistId":
- amg_artist_id if amg_artist_id is None
- or isinstance(amg_artist_id, (int, str))
- else ",".join(
- amg_artist_id if isinstance(amg_artist_id[0], str)
- else (str(i) for i in amg_artist_id)
- ),
- "amgAlbumId":
- amg_album_id if amg_album_id is None
- or isinstance(amg_album_id, (int, str))
- else ",".join(
- amg_album_id if isinstance(amg_album_id[0], str)
- else (str(i) for i in amg_album_id)
- ),
- "amgVideoId":
- amg_video_id if amg_video_id is None
- or isinstance(amg_video_id, (int, str))
- else ",".join(
- amg_video_id if isinstance(amg_video_id[0], str)
- else (str(i) for i in amg_video_id)
- ),
- "bundleId": bundle_id
- if bundle_id is None or isinstance(bundle_id, str)
- else ",".join(bundle_id),
- "upc": upc if upc is None or isinstance(upc, (int, str))
- else ",".join(upc if isinstance(upc[0], str)
- else (str(u) for u in upc)),
- "isbn": isbn if isbn is None or isinstance(isbn, (int, str))
- else ",".join(isbn if isinstance(isbn[0], str)
- else (str(i) for i in isbn)),
- "entity": entity if entity is None or isinstance(entity, str)
- else ",".join(entity),
- "limit": limit,
- "sort": sort
- }
- )
-
-
-
-"""
-Qobuz
-=====
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module contains a minimum implementation of the private Qobuz API.
-"""
-
-import base64
-import datetime
-import hashlib
-import logging
-import os
-import re
-from typing import Any, Union
-
-import requests
-
-from . import FOUND_PLAYWRIGHT, DIR_HOME, DIR_TEMP, _config
-if FOUND_PLAYWRIGHT:
- from playwright.sync_api import sync_playwright
-
-__all__ = ["PrivateAPI"]
-
-def _parse_performers(
- performers: str, roles: Union[list[str], set[str]] = None
- ) -> dict[str, list]:
-
- """
- Parse a string containing credits for a track.
-
- Parameters
- ----------
- performers : `str`
- An unformatted string containing the track credits obtained
- from calling :meth:`get_track`.
-
- roles : `list` or `set`, keyword-only, optional
- Role filter. The special :code:`"Composers"` filter will
- combine the :code:`"Composer"`, :code:`"ComposerLyricist"`,
- :code:`"Lyricist"`, and :code:`"Writer"` roles.
-
- **Valid values**: :code:`"MainArtist"`,
- :code:`"FeaturedArtist"`, :code:`"Producer"`,
- :code:`"Co-Producer"`, :code:`"Mixer"`,
- :code:`"Composers"` (:code:`"Composer"`,
- :code:`"ComposerLyricist"`, :code:`"Lyricist"`,
- :code:`"Writer"`), :code:`"MusicPublisher"`, etc.
-
- Returns
- -------
- credits : `dict`
- A dictionary containing the track contributors, with their
- roles (in snake case) being the keys.
- """
-
- people = {}
- for p in performers.split(" - "):
- if (regex := re.search(
- r"(^.*[A-Za-z]\.|^.*&.*|[\d\s\w].*?)(?:, )(.*)",
- p.rstrip()
- )):
- people[regex.groups()[0]] = regex.groups()[1].split(", ")
-
- credits = {}
- if roles is None:
- roles = set(c for r in people.values() for c in r)
- elif "Composers" in roles:
- roles.remove("Composers")
- credits["composers"] = sorted({
- p for cr in {"Composer", "ComposerLyricist", "Lyricist",
- "Writer"}
- for p, r in people.items()
- if cr in r
- })
- for role in roles:
- credits[
- "_".join(
- re.findall(r"(?:[A-Z][a-z]+)(?:-[A-Z][a-z]+)?", role)
- ).lower()
- ] = [p for p, r in people.items() if role in r]
-
- return credits
-
-
-[docs]
-class PrivateAPI:
-
- """
- Private Qobuz API client.
-
- The private TIDAL API allows songs, collections (albums, playlists),
- and performers to be queried, and information about them to be
- retrieved. As there is no available official documentation for the
- private Qobuz API, its endpoints have been determined by watching
- HTTP network traffic.
-
- .. attention::
-
- As the private Qobuz API is not designed to be publicly
- accessible, this class can be disabled or removed at any time to
- ensure compliance with the `Qobuz API Terms of Use
- <https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf>`_.
-
- While authentication is not necessary to search for and retrieve
- data from public content, it is required to access personal content
- and stream media (with an active Qobuz subscription). In the latter
- case, requests to the private Qobuz API endpoints must be
- accompanied by a valid user authentication token in the header.
-
- Minim can obtain user authentication tokens via the password grant,
- but it is an inherently unsafe method of authentication since it has
- no mechanisms for multifactor authentication or brute force attack
- detection. As such, it is highly encouraged that you obtain a user
- authentication token yourself through the Qobuz Web Player or the
- Android, iOS, macOS, and Windows applications, and then provide it
- and its accompanying app ID and secret to this class's constructor
- as keyword arguments. The app credentials can also be stored as
- :code:`QOBUZ_PRIVATE_APP_ID` and :code:`QOBUZ_PRIVATE_APP_SECRET`
- in the operating system's environment variables, and they will
- automatically be retrieved.
-
- .. tip::
-
- The app credentials and user authentication token can be changed
- or updated at any time using :meth:`set_flow` and
- :meth:`set_auth_token`, respectively.
-
- Minim also stores and manages user authentication tokens and their
- properties. When the password grant is used to acquire a user
- authentication token, it is automatically saved to the Minim
- configuration file to be loaded on the next instantiation of this
- class. This behavior can be disabled if there are any security
- concerns, like if the computer being used is a shared device.
-
- Parameters
- ----------
- app_id : `str`, keyword-only, optional
- App ID. Required if an user authentication token is provided in
- `auth_token`.
-
- app_secret : `str`, keyword-only, optional
- App secret. Required if an user authentication token is provided
- in `auth_token`.
-
- flow : `str`, keyword-only, optional
- Authorization flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"password"` for the password flow.
- * :code:`None` for no authentication.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is opened with the Qobuz login
- page using the Playwright framework by Microsoft to complete the
- password flow. If :code:`False`, the account email and password
- must be provided in `email` and `password`, respectively.
-
- user_agent : `str`, keyword-only, optional
- User agent information to send in the header of HTTP requests.
-
- email : `str`, keyword-only, optional
- Account email address. Required if an user authentication token
- is not provided in `auth_token` and :code:`browser=False`.
-
- password : `str`, keyword-only, optional
- Account password. Required if an user authentication token is
- not provided in `auth_token` and :code:`browser=False`.
-
- auth_token : `str`, keyword-only, optional
- User authentication token. If provided here or found in the
- Minim configuration file, the authentication process is
- bypassed.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether to overwrite an existing user authentication
- token in the Minim configuration file.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained user authentication tokens and
- their associated properties are stored to the Minim
- configuration file.
-
- Attributes
- ----------
- API_URL : `str`
- URL for the Qobuz API.
-
- WEB_URL : `str`
- URL for the Qobuz Web Player.
- """
-
- _FLOWS = {"password"}
- _NAME = f"{__module__}.{__qualname__}"
-
- API_URL = "https://www.qobuz.com/api.json/0.2"
- WEB_URL = "https://play.qobuz.com"
-
- def __init__(
- self, *, app_id: str = None, app_secret: str = None,
- flow: str = None, browser: bool = False, user_agent: str = None,
- email: str = None, password: str = None, auth_token: str = None,
- overwrite: bool = False, save: bool = True) -> None:
-
- """
- Create a private Qobuz API client.
- """
-
- self.session = requests.Session()
- if user_agent:
- self.session.headers["User-Agent"] = user_agent
-
- if (auth_token is None and _config.has_section(self._NAME)
- and not overwrite):
- flow = _config.get(self._NAME, "flow") or None
- auth_token = _config.get(self._NAME, "auth_token")
- app_id = _config.get(self._NAME, "app_id")
- app_secret = _config.get(self._NAME, "app_secret")
-
- self.set_flow(flow, app_id=app_id, app_secret=app_secret,
- auth_token=auth_token, browser=browser, save=save)
- self.set_auth_token(auth_token, email=email, password=password)
-
- def _check_authentication(self, endpoint: str) -> None:
-
- """
- Check if the user is authenticated for the desired endpoint.
-
- Parameters
- ----------
- endpoint : `str`
- Private Qobuz API endpoint.
- """
-
- if not self._flow:
- emsg = (f"{self._NAME}.{endpoint}() requires user "
- "authentication.")
- raise RuntimeError(emsg)
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _request(self, method: str, url: str, **kwargs) -> requests.Response:
-
- """
- Construct and send a request with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code not in range(200, 299):
- error = r.json()
- raise RuntimeError(f'{error["code"]} {error["message"]}')
- return r
-
- def _set_app_credentials(self, app_id: str, app_secret: str) -> None:
-
- """
- Set the Qobuz app ID and secret.
- """
-
- if not app_id or not app_secret:
- js = re.search(
- "/resources/.*/bundle.js",
- self.session.get(f"{self.WEB_URL}/login").text
- ).group(0)
- bundle = self.session.get(f"{self.WEB_URL}{js}").text
- app_id = re.search(
- '(?:production:{api:{appId:")(.*?)(?:",appSecret)', bundle
- ).group(1)
- app_secret = [
- base64.b64decode("".join((s, *m.groups()))[:-44]).decode()
- for s, m in (
- (s, re.search(f'(?:{c.capitalize()}",info:")(.*?)(?:",extras:")'
- '(.*?)(?:"},{offset)',
- bundle))
- for s, c in re.findall(r'(?:[a-z].initialSeed\(")(.*?)'
- r'(?:",window.utimezone.)(.*?)\)',
- bundle)) if m
- ][1]
-
- self.session.headers["X-App-Id"] = app_id
- self._app_secret = app_secret
-
-
-[docs]
- def set_auth_token(
- self, auth_token: str = None, *, email: str = None,
- password: str = None) -> None:
-
- """
- Set the private Qobuz API user authentication token.
-
- Parameters
- ----------
- auth_token : `str`, optional
- User authentication token.
-
- email : `str`, keyword-only, optional
- Account email address.
-
- password : `str`, keyword-only, optional
- Account password.
- """
-
- if auth_token is None:
- if not self._flow:
- return
-
- if self._flow == "password":
- if email is None or password is None:
- if self._browser:
- har_file = DIR_TEMP / "minim_qobuz_private.har"
-
- with sync_playwright() as playwright:
- browser = playwright.firefox.launch(headless=False)
- context = browser.new_context(
- record_har_path=har_file
- )
- page = context.new_page()
- page.goto(f"{self.WEB_URL}/login", timeout=0)
- page.wait_for_url(f"{self.WEB_URL}/featured",
- wait_until="commit")
- context.close()
- browser.close()
-
- with open(har_file, "r") as f:
- regex = re.search(
- '(?<=")https://www.qobuz.com/api.json/0.2/oauth/callback?(.*)(?=")',
- f.read()
- )
- har_file.unlink()
-
- if regex is None:
- raise RuntimeError("Authentication failed.")
- auth_token = self._request("get", regex.group(0)).json()["token"]
- else:
- emsg = ("No account email or password provided "
- "for the password flow.")
- raise ValueError(emsg)
- else:
- r = self._request(
- "post", f"{self.API_URL}/user/login",
- params={"email": email, "password": password}
- ).json()
- auth_token = r["user_auth_token"]
-
- if self._save:
- _config[self._NAME] = {
- "flow": self._flow,
- "auth_token": auth_token,
- "app_id": self.session.headers["X-App-Id"],
- "app_secret": self._app_secret
- }
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- self.session.headers["X-User-Auth-Token"] = auth_token
-
- if self._flow:
- me = self.get_profile()
- self._user_id = me["id"]
- self._sub = (
- me["subscription"] is not None
- and datetime.datetime.now()
- <= datetime.datetime.strptime(
- me["subscription"]["end_date"], "%Y-%m-%d"
- ) + datetime.timedelta(days=1)
- )
-
-
-
-[docs]
- def set_flow(
- self, flow: str, *, app_id: str = None, app_secret: str = None,
- auth_token: str = None, browser: bool = False, save: bool = True
- ) -> None:
-
- """
- Set the authorization flow.
-
- Parameters
- ----------
- flow : `str`, keyword-only, optional
- Authorization flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"password"` for the password flow.
- * :code:`None` for no authentication.
-
- app_id : `str`, keyword-only, optional
- App ID. Required if an user authentication token is provided
- in `auth_token`.
-
- app_secret : `str`, keyword-only, optional
- App secret. Required if an user authentication token is
- provided in `auth_token`.
-
- auth_token : `str`, keyword-only, optional
- User authentication token.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is opened with the Qobuz login
- page using the Playwright framework by Microsoft to complete the
- password flow.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether to save the newly obtained access tokens
- and their associated properties to the Minim configuration
- file.
- """
-
- if flow and flow not in self._FLOWS:
- emsg = (f"Invalid authorization flow ({flow=}). "
- f"Valid values: {', '.join(self._FLOWS)}.")
- raise ValueError(emsg)
-
- self._flow = flow
- self._save = save
-
- self._browser = browser and FOUND_PLAYWRIGHT
- if self._browser != browser:
- logging.warning(
- "The Playwright web framework was not found, so "
- "user authentication via the Qobuz login page is "
- "unavailable."
- )
-
- app_id = app_id or os.environ.get("QOBUZ_PRIVATE_APP_ID")
- app_secret = app_secret or os.environ.get("QOBUZ_PRIVATE_APP_SECRET")
- if (app_id is None or app_secret is None) and auth_token is not None:
- emsg = ("App credentials are required when an user "
- "authentication token is provided.")
-
- self._set_app_credentials(app_id, app_secret)
-
-
- ### ALBUMS ################################################################
-
-
-[docs]
- def get_album(self, album_id: str) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for a single album.
-
- Parameters
- ----------
- album_id : `str`
- Qobuz album ID.
-
- **Example**: :code:`"0060254735180"`.
-
- Returns
- -------
- album : `dict`
- Qobuz catalog information for a single album.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "maximum_bit_depth": <int>,
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>,
- "back": <str>
- },
- "media_count": <str>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "roles": [<str>]
- }
- ],
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "url": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "popularity": <int>,
- "tracks_count": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "articles": <list>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>,
- "awards": <list>,
- "description": <str>,
- "description_language": <str>,
- "goodies": <list>,
- "area": <str>,
- "catchline": <str>,
- "composer": {
- "id": <int>,
- "name": <str>,
- "slug": <str>,
- "albums_count": <int>,
- "picture": <str>,
- "image": <str>
- },
- "created_at": <int>,
- "genres_list": [<str>],
- "period": <str>,
- "copyright": <str>,
- "is_official": <bool>,
- "maximum_technical_specifications": <str>,
- "product_sales_factors_monthly": <int>,
- "product_sales_factors_weekly": <int>,
- "product_sales_factors_yearly": <int>,
- "product_type": <str>,
- "product_url": <str>,
- "recording_information": <str>,
- "relative_url": <str>,
- "release_tags": <list>,
- "release_type": <str>,
- "slug": <str>,
- "subtitle": <str>,
- "tracks": {
- "offset": <int>,
- "limit": <int>,
- "total": <int>,
- "items": [
- {
- "maximum_bit_depth": <int>,
- "copyright": <str>,
- "performers": <str>,
- "audio_info": {
- "replaygain_track_peak": <float>,
- "replaygain_track_gain": <float>
- },
- "performer": {
- "name": <str>,
- "id": <int>
- },
- "work": <str>,
- "composer": {
- "name": <str>,
- "id": <int>
- },
- "isrc": <str>,
- "title": <str>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "track_number": <int>,
- "maximum_channel_count": <int>,
- "id": <int>,
- "media_number": <int>,
- "maximum_sampling_rate": <int>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>
- }
- ]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/album/get",
- params={"album_id": album_id})
-
-
-
-[docs]
- def get_featured_albums(
- self, type: str = "new-releases", *, limit: int = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for featured albums.
-
- Parameters
- ----------
- type : `str`, default: :code:`"new-releases"`
- Feature type.
-
- **Valid values**: :code:`"best-sellers"`,
- :code:`"editor-picks"`, :code:`"ideal-discography"`,
- :code:`"most-featured"`, :code:`"most-streamed"`,
- :code:`"new-releases"`, :code:`"new-releases-full"`,
- :code:`"press-awards"`, :code:`"recent-releases"`,
- :code:`"qobuzissims"`, :code:`"harmonia-mundi"`,
- :code:`"universal-classic"`, :code:`"universal-jazz"`,
- :code:`"universal-jeunesse"`, and
- :code:`"universal-chanson"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of albums to return.
-
- **Default**: :code:`50`.
-
- offset : `int`, keyword-only, optional
- The index of the first album to return. Use with `limit` to
- get the next page of albums.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- albums : `dict`
- Qobuz catalog information for the albums.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "albums": {
- "total": <int>,
- "limit": <int>,
- "offset": <int>,
- "items": [
- {
- "maximum_bit_depth": <int>,
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>,
- "back": <str>
- },
- "media_count": <str>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "roles": [<str>]
- }
- ],
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "url": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "popularity": <int>,
- "tracks_count": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "articles": <list>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>
- }
- ]
- }
- }
- """
-
- if type not in \
- (ALBUM_FEATURE_TYPES := {
- "best-sellers", "editor-picks", "ideal-discography",
- "most-featured", "most-streamed", "new-releases",
- "new-releases-full", "press-awards", "recent-releases",
- "qobuzissims", "harmonia-mundi", "universal-classic",
- "universal-jazz", "universal-jeunesse", "universal-chanson"
- }):
- emsg = ("Invalid feature type. Valid values: "
- f"types are {', '.join(ALBUM_FEATURE_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/album/getFeatured",
- params={"type": type, "limit": limit, "offset": offset}
- )
-
-
- ### ARTISTS ###############################################################
-
-
-[docs]
- def get_artist(
- self, artist_id: Union[int, str], *,
- extras: Union[str, list[str]] = None,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for a single artist.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- Qobuz artist ID.
-
- extras : `str` or `list`, keyword-only, optional
- Specifies extra information about the artist to return.
-
- **Valid values**: :code:`"albums"`, :code:`"tracks"`,
- :code:`"playlists"`, :code:`"tracks_appears_on"`, and
- :code:`"albums_with_last_release"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of extra items to return. Has no effect
- if :code:`extras=None`.
-
- **Default**: :code:`25`.
-
- offset : `int`, keyword-only, optional
- The index of the first extra item to return. Use with
- `limit` to get the next page of extra items. Has no effect
- if :code:`extras=None`.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- artist : `dict`
- Qobuz catalog information for a single artist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "albums_as_primary_artist_count": <int>,
- "albums_as_primary_composer_count": <int>,
- "albums_count": <int>,
- "slug": <str>,
- "picture": <str>,
- "image": {
- "small": <str>,
- "medium": <str>,
- "large": <str>,
- "extralarge": <str>,
- "mega": <str>
- },
- "similar_artist_ids": [<int>],
- "information": <str>,
- "biography": {
- "summary": <str>,
- "content": <str>,
- "source": <str>,
- "language": <str>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artist/get",
- params={
- "artist_id": artist_id,
- "extra": extras if extras is None or isinstance(extras, str)
- else ",".join(extras),
- "limit": limit,
- "offset": offset
- }
- )
-
-
- ### LABELS ################################################################
-
-
-[docs]
- def get_label(
- self, label_id: Union[int, str], *, albums: bool = False,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for a record label.
-
- Parameters
- ----------
- label_id : `int` or `str`
- Qobuz record label ID.
-
- **Example**: :code:`1153`.
-
- albums : `bool`, keyword-only, default: :code:`False`
- Specifies whether information on the albums released by the
- record label is returned.
-
- limit : `int`, keyword-only, optional
- The maximum number of albums to return. Has no effect if
- :code:`albums=False`.
-
- **Default**: :code:`25`.
-
- offset : `int`, keyword-only, optional
- The index of the first album to return. Use with `limit` to
- get the next page of albums. Has no effect if
- :code:`albums=False`.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- label : `dict`
- Qobuz catalog information for the record label.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "slug": <str>,
- "supplier_id": <int>,
- "albums_count": <int>,
- "image": <str>,
- "description": <str>
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/label/get",
- params={
- "label_id": label_id,
- "extra": "albums" if albums else None,
- "limit": limit,
- "offset": offset
- }
- )
-
-
- ### PLAYLISTS #############################################################
-
-
-[docs]
- def get_playlist(
- self, playlist_id: Union[int, str], *, tracks: bool = True,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for a playlist.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz playlist ID.
-
- **Example**: :code:`15732665`.
-
- tracks : `bool`, keyword-only, default: :code:`True`
- Specifies whether information on the tracks in the playlist
- is returned.
-
- limit : `int`, keyword-only, optional
- The maximum number of tracks to return. Has no effect if
- :code:`tracks=False`.
-
- **Default**: :code:`50`.
-
- offset : `int`, keyword-only, optional
- The index of the first track to return. Use with `limit` to
- get the next page of tracks. Has no effect if
- :code:`tracks=False`.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlist : `dict`
- Qobuz catalog information for the playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "image_rectangle_mini": [<str>],
- "featured_artists": <list>,
- "description": <str>,
- "created_at": <int>,
- "timestamp_position": <int>,
- "images300": [<str>],
- "duration": <int>,
- "updated_at": <int>,
- "genres": [
- {
- "id": <int>,
- "color": <str>,
- "name": <str>,
- "path": [<int>],
- "slug": <str>,
- "percent": <float>
- }
- ],
- "image_rectangle": [<str>],
- "id": <int>,
- "slug": <str>,
- "owner": {
- "id": <int>,
- "name": <str>
- },
- "users_count": <int>,
- "images150": [<str>],
- "images": [<str>],
- "is_collaborative": <bool>,
- "stores": [<str>],
- "tags": [
- {
- "featured_tag_id": <str>,
- "name_json": <str>,
- "slug": <str>,
- "color": <str>,
- "genre_tag": <str>,
- "is_discover": <bool>
- }
- ],
- "tracks_count": <int>,
- "public_at": <int>,
- "name": <str>,
- "is_public": <bool>,
- "is_featured": <bool>,
- "tracks": {
- "offset": <int>,
- "limit": <int>,
- "total": <int>,
- "items": [
- {
- "maximum_bit_depth": <int>,
- "copyright": <str>,
- "performers": <str>,
- "audio_info": {
- "replaygain_track_peak": <float>,
- "replaygain_track_gain": <float>
- },
- "performer": {
- "name": <str>,
- "id": <int>
- },
- "album": {
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>
- },
- "maximum_bit_depth": <int>,
- "media_count": <int>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug": <str>,
- "picture": <str>
- },
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "tracks_count": <int>,
- "popularity": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "displayable": <bool>,
- "streamable": <bool>,
- "streamable_at": <int>,
- "downloadable": <bool>,
- "purchasable_at": <int>,
- "purchasable": <bool>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "hires": <bool>,
- "hires_streamable": <bool>
- },
- "work": <str>,
- "isrc": <str>,
- "title": <str>,
- "version": null,
- "duration": <int>,
- "parental_warning": <bool>,
- "track_number": <int>,
- "maximum_channel_count": <int>,
- "id": <int>,
- "media_number": <int>,
- "maximum_sampling_rate": <int>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>,
- "position": <int>,
- "created_at": <int>,
- "playlist_track_id": <int>
- }
- ]
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/playlist/get",
- params={
- "playlist_id": playlist_id,
- "extra": "tracks" if tracks else None,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_featured_playlists(
- self, type: str = "editor-picks", *, limit: int = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for featured playlists.
-
- Parameters
- ----------
- type : `str`, default: :code:`"editor-picks"`
- Feature type.
-
- **Valid values**: :code:`"editor-picks"` and
- :code:`"last-created"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of playlists to return.
-
- **Default**: :code:`50`.
-
- offset : `int`, keyword-only, optional
- The index of the first playlist to return. Use with `limit`
- to get the next page of playlists.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- Qobuz catalog information for the playlists.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "offset": <int>,
- "limit": <int>,
- "total": <int>,
- "items": [
- {
- "owner": {
- "name": <str>,
- "id": <int>
- },
- "image_rectangle_mini": [<str>],
- "users_count": <int>,
- "images150": [<str>],
- "images": [<str>],
- "featured_artists": <list>,
- "is_collaborative": <bool>,
- "stores": [<str>],
- "description": <str>,
- "created_at": <int>,
- "images300": [<str>],
- "tags": [
- {
- "color": <str>,
- "is_discover": <bool>,
- "featured_tag_id": <str>,
- "name_json": <str>,
- "slug": <str>,
- "genre_tag": <str>
- }
- ],
- "duration": <int>,
- "updated_at": <int>,
- "genres": [
- {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "percent": <float>,
- "slug": <str>
- }
- ],
- "image_rectangle": [<str>],
- "tracks_count": <int>,
- "public_at": <int>,
- "name": <str>,
- "is_public": <bool>,
- "id": <int>,
- "slug": <str>,
- "is_featured": <bool>
- }
- ]
- }
- """
-
- if type not in \
- (PLAYLIST_FEATURE_TYPES := {"editor-picks", "last-created"}):
- emsg = ("Invalid feature type. Valid types: "
- f"{', '.join(PLAYLIST_FEATURE_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/playlist/getFeatured",
- params={"type": type, "limit": limit, "offset": offset}
- )["playlists"]
-
-
-
-[docs]
- def get_user_playlists(
- self, *, limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get the current user's custom and favorite playlists.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of playlists to return.
-
- **Default**: :code:`500`.
-
- offset : `int`, keyword-only, optional
- The index of the first playlist to return. Use with `limit`
- to get the next page of playlists.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- Qobuz catalog information for the current user's custom and
- favorite playlists.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "offset": <int>,
- "limit": <int>,
- "total": <int>,
- "items": [
- {
- "image_rectangle_mini": [<str>],
- "is_published": <bool>,
- "featured_artists": <list>,
- "description": <str>,
- "created_at": <int>,
- "timestamp_position": <int>,
- "images300": [<str>],
- "duration": <int>,
- "updated_at": <int>,
- "published_to": <int>,
- "genres": <list>,
- "image_rectangle": [<str>],
- "id": <int>,
- "slug": <str>,
- "owner": {
- "id": <int>,
- "name": <str>
- },
- "users_count": <int>,
- "images150": [<str>],
- "images": [<str>],
- "is_collaborative": <bool>.
- "stores": [<str>],
- "tracks_count": <int>,
- "public_at": <int>,
- "name": "Welcome to Qobuz",
- "is_public": <bool>,
- "published_from": <int>,
- "is_featured": <bool>,
- "position": <int>
- }
- ]
- }
- """
-
- self._check_authentication("get_user_playlists")
-
- return self._get_json(
- f"{self.API_URL}/playlist/getUserPlaylists",
- params={"limit": limit, "offset": offset}
- )["playlists"]
-
-
-
-[docs]
- def create_playlist(
- self, name: str, *, description: str = None, public: bool = True,
- collaborative: bool = False) -> dict[str, Any]:
-
- """
- Create a user playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- name : `str`
- Qobuz playlist name.
-
- description : `str`, keyword-only, optional
- Brief playlist description.
-
- public : `bool`, keyword-only, default: :code:`True`
- Determines whether the playlist is public (:code:`True`) or
- private (:code:`False`).
-
- collaborative : `bool`, keyword-only, default: :code:`False`
- Determines whether the playlist is collaborative.
-
- Returns
- -------
- playlist : `str`
- Qobuz catalog information for the newly created playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "description": <str>,
- "tracks_count": <int>,
- "users_count": <int>,
- "duration": <int>,
- "public_at": <int>,
- "created_at": <int>,
- "updated_at": <int>,
- "is_public": <bool>,
- "is_collaborative": <bool>,
- "owner": {
- "id": <int>,
- "name": <str>
- }
- }
- """
-
- self._check_authentication("create_playlist")
-
- data = {"name": name, "is_public": str(public).lower(),
- "is_collaborative": str(collaborative).lower()}
- if description:
- data["description"] = description
- return self._request(
- "post", f"{self.API_URL}/playlist/create", data=data
- ).json()
-
-
-
-[docs]
- def update_playlist(
- self, playlist_id: Union[int, str], *, name: str = None,
- description: str = None, public: bool = None,
- collaborative: bool = None) -> dict[str, Any]:
-
- """
- Update the title, description, and/or privacy of a playlist
- owned by the current user.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz user playlist ID.
-
- **Example**: :code:`17737508`.
-
- name : `str`, keyword-only, optional
- Qobuz playlist name.
-
- description : `str`, keyword-only, optional
- Brief playlist description.
-
- public : `bool`, keyword-only, optional
- Determines whether the playlist is public (:code:`True`) or
- private (:code:`False`).
-
- collaborative : `bool`, keyword-only, optional
- Determines whether the playlist is collaborative.
-
- Returns
- -------
- playlist : `str`
- Qobuz catalog information for the updated playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "description": <str>,
- "tracks_count": <int>,
- "users_count": <int>,
- "duration": <int>,
- "public_at": <int>,
- "created_at": <int>,
- "updated_at": <int>,
- "is_public": <bool>,
- "is_collaborative": <bool>,
- "owner": {
- "id": <int>,
- "name": <str>
- }
- }
- """
-
- self._check_authentication("update_playlist")
-
- data = {"playlist_id": playlist_id}
- if name:
- data["name"] = name
- if description:
- data["description"] = description
- if public is not None:
- data["is_public"] = str(public).lower()
- if collaborative is not None:
- data["is_collaborative"] = str(collaborative).lower()
- return self._request("post", f"{self.API_URL}/playlist/update",
- data=data).json()
-
-
-
-[docs]
- def update_playlist_position(
- self, from_playlist_id: Union[int, str],
- to_playlist_id: Union[int, str]) -> None:
-
- """
- Organize a user's playlists.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- from_playlist_id : `int` or `str`
- Qobuz user playlist ID of playlist to move.
-
- **Example**: :code:`17737508`.
-
- to_playlist_id : `int` or `str`
- Qobuz user playlist ID of playlist to swap with that in
- `from_playlist_id`.
-
- **Example**: :code:`17737509`.
- """
-
- self._check_authentication("update_playlist_position")
-
- self._request("post",
- f"{self.API_URL}/playlist/updatePlaylistsPosition",
- data={"playlist_ids": [from_playlist_id, to_playlist_id]})
-
-
-
-[docs]
- def add_playlist_tracks(
- self, playlist_id: Union[int, str],
- track_ids: Union[int, str, list[Union[int, str]]], *,
- duplicate: bool = False) -> dict[str, Any]:
-
- """
- Add tracks to a user playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz user playlist ID.
-
- **Example**: :code:`17737508`.
-
- track_ids : `int`, `str`, or `list`
- Qobuz track ID(s).
-
- **Examples**: :code:`"24393122,24393138"` or
- :code:`[24393122, 24393138]`.
-
- duplicate : `bool`, keyword-only, default: :code:`False`
- Determines whether duplicate tracks should be added to the
- playlist.
-
- Returns
- -------
- playlist : `str`
- Qobuz catalog information for the updated playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "description": <str>,
- "tracks_count": <int>,
- "users_count": <int>,
- "duration": <int>,
- "public_at": <int>,
- "created_at": <int>,
- "updated_at": <int>,
- "is_public": <bool>,
- "is_collaborative": <bool>,
- "owner": {
- "id": <int>,
- "name": <str>
- }
- }
- """
-
- self._check_authentication("add_playlist_tracks")
-
- if isinstance(track_ids, list):
- track_ids = ",".join(str(t) for t in track_ids)
- return self._request(
- "post", f"{self.API_URL}/playlist/addTracks",
- data={
- "playlist_id": playlist_id,
- "track_ids": track_ids,
- "no_duplicate": str(not duplicate).lower()
- }
- ).json()
-
-
-
-[docs]
- def move_playlist_tracks(
- self, playlist_id: Union[int, str],
- playlist_track_ids: Union[int, str, list[Union[int, str]]],
- insert_before: int) -> dict[str, Any]:
-
- """
- Move tracks in a user playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz user playlist ID.
-
- **Example**: :code:`17737508`.
-
- playlist_track_ids : `int`, `str`, or `list`
- Qobuz playlist track ID(s).
-
- .. note::
-
- Playlist track IDs are not the same as track IDs. To get
- playlist track IDs, use :meth:`get_playlist`.
-
- insert_before : `int`
- Position to which to move the tracks specified in
- `track_ids`.
-
- Returns
- -------
- playlist : `str`
- Qobuz catalog information for the updated playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "description": <str>,
- "tracks_count": <int>,
- "users_count": <int>,
- "duration": <int>,
- "public_at": <int>,
- "created_at": <int>,
- "updated_at": <int>,
- "is_public": <bool>,
- "is_collaborative": <bool>,
- "owner": {
- "id": <int>,
- "name": <str>
- }
- }
- """
-
- self._check_authentication("move_playlist_tracks")
-
- if isinstance(playlist_track_ids, list):
- playlist_track_ids = ",".join(str(t) for t in playlist_track_ids)
- return self._request(
- "post", f"{self.API_URL}/playlist/updateTracksPosition",
- data={
- "playlist_id": playlist_id,
- "playlist_track_ids": playlist_track_ids,
- "insert_before": insert_before
- }
- ).json()
-
-
-
-[docs]
- def delete_playlist_tracks(
- self, playlist_id: Union[int, str],
- playlist_track_ids: Union[int, str, list[Union[int, str]]]
- ) -> dict[str, Any]:
-
- """
- Delete tracks from a user playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz user playlist ID.
-
- **Example**: :code:`17737508`.
-
- playlist_track_ids : `int`, `str`, or `list`
- Qobuz playlist track ID(s).
-
- .. note::
-
- Playlist track IDs are not the same as track IDs. To get
- playlist track IDs, use :meth:`get_playlist`.
-
- Returns
- -------
- playlist : `str`
- Qobuz catalog information for the updated playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "description": <str>,
- "tracks_count": <int>,
- "users_count": <int>,
- "duration": <int>,
- "public_at": <int>,
- "created_at": <int>,
- "updated_at": <int>,
- "is_public": <bool>,
- "is_collaborative": <bool>,
- "owner": {
- "id": <int>,
- "name": <str>
- }
- }
- """
-
- self._check_authentication("delete_playlist_tracks")
-
- if isinstance(playlist_track_ids, list):
- playlist_track_ids = ",".join(str(t) for t in playlist_track_ids)
-
- return self._request(
- "post", f"{self.API_URL}/playlist/deleteTracks",
- data={"playlist_id": playlist_id,
- "playlist_track_ids": playlist_track_ids}
- ).json()
-
-
-
-[docs]
- def delete_playlist(self, playlist_id: Union[int, str]) -> None:
-
- """
- Delete a user playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz user playlist ID.
-
- **Example**: :code:`17737508`.
- """
-
- self._check_authentication("delete_playlist")
-
- self._request("post", f"{self.API_URL}/playlist/delete",
- data={"playlist_id": playlist_id})
-
-
-
-[docs]
- def favorite_playlist(self, playlist_id: Union[int, str]) -> None:
-
- """
- Subscribe to a playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz playlist ID.
-
- **Example**: :code:`15732665`.
- """
-
- self._check_authentication("favorite_playlist")
-
- self._request("post", f"{self.API_URL}/playlist/subscribe",
- data={"playlist_id": playlist_id})
-
-
-
-[docs]
- def unfavorite_playlist(self, playlist_id: Union[int, str]) -> None:
-
- """
- Unsubscribe from a playlist.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- playlist_id : `int` or `str`
- Qobuz playlist ID.
-
- **Example**: :code:`15732665`.
- """
-
- self._check_authentication("unfavorite_playlist")
-
- self._request("post", f"{self.API_URL}/playlist/unsubscribe",
- data={"playlist_id": playlist_id})
-
-
- ### SEARCH ################################################################
-
-
-[docs]
- def search(
- self, query: str, type: str = None, *, hi_res: bool = False,
- new_release: bool = False, strict: bool = False, limit: int = 10,
- offset: int = 0) -> dict[str, Any]:
-
- """
- Search Qobuz for media and performers.
-
- Parameters
- ----------
- query : `str`
- Search query.
-
- type : `str`, keyword-only, optional
- Category to search in. If specified, only matching releases
- and tracks will be returned.
-
- **Valid values**: :code:`"MainArtist"`, :code:`"Composer"`,
- :code:`"Performer"`, :code:`"ReleaseName"`, and
- :code:`"Label"`.
-
- hi_res : `bool`, keyword-only, :code:`False`
- High-resolution audio only.
-
- new_release : `bool`, keyword-only, :code:`False`
- New releases only.
-
- strict : `bool`, keyword-only, :code:`False`
- Enable exact word or phrase matching.
-
- limit : `int`, keyword-only, default: :code:`10`
- Maximum number of results to return.
-
- offset : `int`, keyword-only, default: :code:`0`
- Index of the first result to return. Use with `limit` to get
- the next page of search results.
-
- Returns
- -------
- results : `dict`
- Search results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "query": <str>,
- "albums": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "maximum_bit_depth": <int>,
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>,
- "back": <str>,
- },
- "media_count": <int>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "roles": [<str>]
- }
- ],
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "url": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "popularity": <int>,
- "tracks_count": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "articles": <list>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>
- }
- ]
- },
- "tracks": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "maximum_bit_depth": <int>,
- "copyright": <str>,
- "performers": <str>,
- "audio_info": {
- "replaygain_track_peak": <float>,
- "replaygain_track_gain": <float>
- },
- "performer": {
- "name": <str>,
- "id": <int>
- },
- "album": {
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>
- },
- "maximum_bit_depth": <int>,
- "media_count": <int>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug":<str>,
- "picture": <str>
- },
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "tracks_count": <int>,
- "popularity": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "displayable": <bool>,
- "streamable": <bool>,
- "streamable_at": <int>,
- "downloadable": <bool>,
- "purchasable_at": <int>,
- "purchasable": <bool>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "hires": <bool>,
- "hires_streamable": <bool>
- },
- "work": <str>,
- "composer": {
- "name": <str>,
- "id": <int>
- },
- "isrc": <str>,
- "title": <str>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "track_number": <int>,
- "maximum_channel_count": <int>,
- "id": <int>,
- "media_number": <int>,
- "maximum_sampling_rate": <int>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>
- }
- ]
- },
- "artists": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "picture": <str>,
- "image": {
- "small": <str>,
- "medium": <str>,
- "large": <str>,
- "extralarge": <str>,
- "mega": <str>
- },
- "name": <str>,
- "slug": <str>,
- "albums_count": <int>,
- "id": <int>
- }
- ]
- },
- "playlists": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "image_rectangle_mini": [<str>],
- "is_published": <bool>,
- "featured_artists": <list>,
- "description": <str>,
- "created_at": <int>,
- "timestamp_position": <int>,
- "images300": [<str>],
- "duration": <int>,
- "updated_at": <int>,
- "published_to": <int>,
- "genres": <list>,
- "image_rectangle": [<str>],
- "id": <int>,
- "slug": <str>,
- "owner": {
- "id": <int>,
- "name": <str>
- },
- "users_count": <int>,
- "images150": [<str>],
- "images": [<str>],
- "is_collaborative": <bool>,
- "stores": [<str>],
- "tags": [
- {
- "featured_tag_id": <str>,
- "name_json": <str>,
- "slug": <str>,
- "color": <str>,
- "genre_tag": <str>,
- "is_discover": <bool>
- }
- ],
- "tracks_count": <int>,
- "public_at": <int>,
- "name": <str>,
- "is_public": <bool>,
- "published_from": <int>,
- "is_featured": <bool>
- }
- ]
- },
- "focus": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "image": <str>,
- "name_superbloc": <str>,
- "accroche": <str>,
- "id": <str>,
- "title": <str>,
- "genre_ids": [<str>],
- "author": <str>,
- "date": <str>
- }
- ]
- },
- "articles": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "image": <str>,
- "thumbnail": <str>,
- "root_category": <int>,
- "author": <str>,
- "abstract": <str>,
- "source": <str>,
- "title": <str>,
- "type": <str>,
- "url": <str>,
- "image_original": <str>,
- "category_id": <int>,
- "source_image": <str>,
- "id": <int>,
- "published_at": <int>,
- "category": <str>
- }
- ]
- },
- "stories": {
- "limit": <int>,
- "offset": <int>,
- "total": <int>,
- "items": [
- {
- "id": <str>,
- "section_slugs": [<str>],
- "title": <str>,
- "description_short": <str>,
- "authors": [
- {
- "id": <str>,
- "name": <str>,
- "slug": <str>
- }
- ],
- "image": <str>,
- "display_date": <int>
- }
- ]
- }
- }
- """
-
- if type and type not in \
- (SEARCH_TYPES := {"MainArtist", "Composer", "Performer",
- "ReleaseName", "Label"}):
- emsg = ("Invalid search type. Valid values: "
- f"{', '.join(SEARCH_TYPES)}")
- raise ValueError(emsg)
-
- if strict:
- query = f'"{query}"'
- if type:
- query += f" #By{type}"
- if hi_res:
- query += " #HiRes"
- if new_release:
- query += " #NewRelease"
-
- return self._get_json(
- f"{self.API_URL}/catalog/search",
- params={"query": query, "limit": limit, "offset": offset}
- )
-
-
- ### TRACKS ################################################################
-
-
-[docs]
- def get_track(self, track_id: Union[int, str]) -> dict[str, Any]:
-
- """
- Get Qobuz catalog information for a track.
-
- Parameters
- ----------
- track_id : `int` or `str`
- Qobuz track ID.
-
- **Example**: :code:`24393138`.
-
- Returns
- -------
- track : `dict`
- Qobuz catalog information for the track.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "maximum_bit_depth": <int>,
- "copyright": <str>,
- "performers": <str>,
- "audio_info": {
- "replaygain_track_gain": <float>,
- "replaygain_track_peak": <float>
- },
- "performer": {
- "id": <int>,
- "name": <str>
- },
- "album": {
- "maximum_bit_depth": <int>,
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>,
- "back": <str>
- },
- "media_count": <int>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "roles": [<str>]
- }
- ],
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "url": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "popularity": <int>,
- "tracks_count": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "articles": <list>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>,
- "awards": <list>,
- "description": <str>,
- "description_language": <str>,
- "goodies": <list>,
- "area": null,
- "catchline": <str>,
- "composer": {
- "id": <int>,
- "name": <str>,
- "slug": <str>,
- "albums_count": <int>,
- "picture": <str>,
- "image": <str>
- },
- "created_at": <int>,
- "genres_list": [<str>],
- "period": <str>,
- "copyright": <str>,
- "is_official": <bool>,
- "maximum_technical_specifications": <str>,
- "product_sales_factors_monthly": <int>,
- "product_sales_factors_weekly": <int>,
- "product_sales_factors_yearly": <int>,
- "product_type": <str>,
- "product_url": <str>,
- "recording_information": <str>,
- "relative_url": <str>,
- "release_tags": <list>,
- "release_type": <str>,
- "slug": <str>,
- "subtitle": <str>
- },
- "work": <str>,
- "composer": {
- "id": <int>,
- "name": <str>
- },
- "isrc": <str>,
- "title": <str>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "track_number": <int>,
- "maximum_channel_count": <int>,
- "id": <int>,
- "media_number": <int>,
- "maximum_sampling_rate": <int>,
- "articles": <list>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>
- }
- """
-
- return self._get_json(f"{self.API_URL}/track/get",
- params={"track_id": track_id})
-
-
-
-[docs]
- def get_track_performers(
- self, track_id: Union[int, str] = None, *, performers: str = None,
- roles: Union[list[str], set[str]] = None) -> dict[str, list]:
-
- """
- Get credits for a track.
-
- .. note::
-
- This method is provided for convenience and is not a private
- Qobuz API endpoint.
-
- Parameters
- ----------
- track_id : `int` or `str`, optional
- Qobuz track ID. Required if `performers` is not provided.
-
- **Example**: :code:`24393138`.
-
- performers : `str`, keyword-only, optional
- An unformatted string containing the track credits obtained
- from calling :meth:`get_track`.
-
- roles : `list` or `set`, keyword-only, optional
- Role filter. The special :code:`"Composers"` filter will
- combine the :code:`"Composer"`, :code:`"ComposerLyricist"`,
- :code:`"Lyricist"`, and :code:`"Writer"` roles.
-
- **Valid values**: :code:`"MainArtist"`,
- :code:`"FeaturedArtist"`, :code:`"Producer"`,
- :code:`"Co-Producer"`, :code:`"Mixer"`,
- :code:`"Composers"` (:code:`"Composer"`,
- :code:`"ComposerLyricist"`, :code:`"Lyricist"`,
- :code:`"Writer"`), :code:`"MusicPublisher"`, etc.
-
- Returns
- -------
- credits : `dict`
- A dictionary containing the track contributors, with their
- roles (in snake case) being the keys.
- """
-
- if performers is None:
- if track_id is None:
- emsg = ("Either a Qobuz track ID or an unformatted "
- "string containing the track credits must be "
- "provided.")
- raise ValueError(emsg)
- performers = self.get_track(track_id)["performers"]
-
- if performers is None:
- return {}
-
- return _parse_performers(performers, roles=roles)
-
-
-
-[docs]
- def get_track_file_url(
- self, track_id: Union[int, str], format_id: Union[int, str] = 27
- ) -> dict[str, Any]:
-
- """
- Get the file URL for a track.
-
- .. admonition:: Subscription
- :class: warning
-
- Full track playback information and lossless and Hi-Res audio
- is only available with an active Qobuz subscription.
-
- Parameters
- ----------
- track_id : `int` or `str`
- Qobuz track ID.
-
- **Example**: :code:`24393138`.
-
- format_id : `int` or `str`, default: :code:`27`
- Audio format ID that determines the maximum audio quality.
-
- .. container::
-
- **Valid values**:
-
- * :code:`5` for constant bitrate (320 kbps) MP3.
- * :code:`6` for CD-quality (16-bit, 44.1 kHz) FLAC.
- * :code:`7` for up to 24-bit, 96 kHz Hi-Res FLAC.
- * :code:`27` for up to 24-bit, 192 kHz Hi-Res FLAC.
-
- Returns
- -------
- url : `dict`
- A dictionary containing the URL and track information, such
- as the audio format, bit depth, etc.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "track_id": <int>,
- "duration": <int>,
- "url": <str>,
- "format_id": <int>,
- "mime_type": <str>,
- "restrictions": [
- {
- "code": <str>
- }
- ],
- "sampling_rate": <int>,
- "bit_depth": <int>
- }
-
- """
-
- if not self._flow or not self._sub:
- wmsg = ("No user authentication or Qobuz streaming plan "
- "detected. The URL, if available, will lead to a "
- "30-second preview of the track.")
- logging.warning(wmsg)
-
- if int(format_id) not in (FORMAT_IDS := {5, 6, 7, 27}):
- emsg = ("Invalid format ID. Valid values: "
- f"{', '.join(FORMAT_IDS)}.")
- raise ValueError(emsg)
-
- timestamp = datetime.datetime.now().timestamp()
- return self._get_json(
- f"{self.API_URL}/track/getFileUrl",
- params={
- "request_ts": timestamp,
- "request_sig": hashlib.md5(
- (f"trackgetFileUrlformat_id{format_id}"
- f"intentstreamtrack_id{track_id}"
- f"{timestamp}{self._app_secret}").encode()
- ).hexdigest(),
- "track_id": track_id,
- "format_id": format_id,
- "intent": "stream"
- }
- )
-
-
-
-[docs]
- def get_curated_tracks(self) -> list[dict[str, Any]]:
-
- """
- Get weekly curated tracks for the user.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of tracks to return.
-
- **Default**: :code:`50`.
-
- offset : `int`, keyword-only, optional
- The index of the first track to return. Use with `limit`
- to get the next page of tracks.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- tracks : `list`
- Qobuz catalog information for the curated tracks.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "title": <str>,
- "baseline": <str>,
- "description": <str>,
- "type": "weekly",
- "step_pagination": <int>,
- "images": {
- "small": <str>,
- "large": <str>
- },
- "graphics": {
- "background": <str>,
- "foreground": <str>
- },
- "duration": <int>,
- "generated_at": <int>,
- "expires_on": <int>,
- "track_count": <int>,
- "tracks": {
- "offset": <int>,
- "limit": <int>,
- "items": [
- {
- "maximum_bit_depth": <int>,
- "copyright": <str>,
- "performers": <str>,
- "audio_info": {
- "replaygain_track_peak": <float>,
- "replaygain_track_gain": <float>
- },
- "performer": {
- "name": <str>,
- "id": <int>
- },
- "album": {
- "image": {
- "small": <str>,
- "thumbnail": <str>,
- "large": <str>
- },
- "maximum_bit_depth": <int>,
- "media_count": <int>,
- "artist": {
- "image": <str>,
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "slug":<str>,
- "picture": <str>
- },
- "upc": <str>,
- "released_at": <int>,
- "label": {
- "name": <str>,
- "id": <int>,
- "albums_count": <int>,
- "supplier_id": <int>,
- "slug": <str>
- },
- "title": <str>,
- "qobuz_id": <int>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "tracks_count": <int>,
- "popularity": <int>,
- "genre": {
- "path": [<int>],
- "color": <str>,
- "name": <str>,
- "id": <int>,
- "slug": <str>
- },
- "maximum_channel_count": <int>,
- "id": <str>,
- "maximum_sampling_rate": <int>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "displayable": <bool>,
- "streamable": <bool>,
- "streamable_at": <int>,
- "downloadable": <bool>,
- "purchasable_at": <int>,
- "purchasable": <bool>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "hires": <bool>,
- "hires_streamable": <bool>
- },
- "work": <str>,
- "composer": {
- "name": <str>,
- "id": <int>
- },
- "isrc": <str>,
- "title": <str>,
- "version": <str>,
- "duration": <int>,
- "parental_warning": <bool>,
- "track_number": <int>,
- "maximum_channel_count": <int>,
- "id": <int>,
- "media_number": <int>,
- "maximum_sampling_rate": <int>,
- "release_date_original": <str>,
- "release_date_download": <str>,
- "release_date_stream": <str>,
- "release_date_purchase": <str>,
- "purchasable": <bool>,
- "streamable": <bool>,
- "previewable": <bool>,
- "sampleable": <bool>,
- "downloadable": <bool>,
- "displayable": <bool>,
- "purchasable_at": <int>,
- "streamable_at": <int>,
- "hires": <bool>,
- "hires_streamable": <bool>
- }
- ]
- }
- }
- """
-
- self._check_authentication("get_curated_tracks")
-
- return self._get_json(f"{self.API_URL}/dynamic-tracks/get",
- params={"type": "weekly"})
-
-
- ### STREAMS ###############################################################
-
-
-[docs]
- def get_track_stream(
- self, track_id: Union[int, str], *, format_id: Union[int, str] = 27
- ) -> tuple[bytes, str]:
-
- """
- Get the audio stream data for a track.
-
- .. admonition:: Subscription
- :class: warning
-
- Full track playback information and lossless and Hi-Res audio
- is only available with an active Qobuz subscription.
-
- .. note::
-
- This method is provided for convenience and is not a private
- Qobuz API endpoint.
-
- Parameters
- ----------
- track_id : `int` or `str`
- Qobuz track ID.
-
- **Example**: :code:`24393138`.
-
- format_id : `int`, default: :code:`27`
- Audio format ID that determines the maximum audio quality.
-
- .. container::
-
- **Valid values**:
-
- * :code:`5` for constant bitrate (320 kbps) MP3.
- * :code:`6` for CD-quality (16-bit, 44.1 kHz) FLAC.
- * :code:`7` for up to 24-bit, 96 kHz Hi-Res FLAC.
- * :code:`27` for up to 24-bit, 192 kHz Hi-Res FLAC.
-
- Returns
- -------
- stream : `bytes`
- Audio stream data.
-
- mime_type : `str`
- Audio stream MIME type.
- """
-
- file = self.get_track_file_url(track_id, format_id=format_id)
- with self.session.get(file["url"]) as r:
- return r.content, file["mime_type"]
-
-
-
-[docs]
- def get_collection_streams(
- self, id: Union[int, str], type: str, *,
- format_id: Union[int, str] = 27) -> list[tuple[bytes, str]]:
-
- """
- Get audio stream data for all tracks in an album or a playlist.
-
- .. admonition:: Subscription
- :class: warning
-
- Full track playback information and lossless and Hi-Res audio
- is only available with an active Qobuz subscription.
-
- .. note::
-
- This method is provided for convenience and is not a private
- Qobuz API endpoint.
-
- Parameters
- ----------
- id : `int` or `str`
- Qobuz collection ID.
-
- type : `str`
- Collection type.
-
- **Valid values**: :code:`"album"` and :code:`"playlist"`.
-
- format_id : `int`, default: :code:`27`
- Audio format ID that determines the maximum audio quality.
-
- .. container::
-
- **Valid values**:
-
- * :code:`5` for constant bitrate (320 kbps) MP3.
- * :code:`6` for CD-quality (16-bit, 44.1 kHz) FLAC.
- * :code:`7` for up to 24-bit, 96 kHz Hi-Res FLAC.
- * :code:`27` for up to 24-bit, 192 kHz Hi-Res FLAC.
-
- Returns
- -------
- streams : `list`
- Audio stream data.
- """
-
- if type not in (COLLECTION_TYPES := {"album", "playlist"}):
- emsg = ("Invalid collection type. Valid values: "
- f"{', '.join(COLLECTION_TYPES)}.")
- raise ValueError(emsg)
-
- if type == "album":
- data = self.get_album(id)
- elif type == "playlist":
- data = self.get_playlist(id, limit=500)
- return [self.get_track_stream(track["id"], format_id=format_id)
- if track["streamable"] else None
- for track in data["tracks"]["items"]]
-
-
- ### USER ##################################################################
-
-
-[docs]
- def get_profile(self) -> dict[str, Any]:
-
- """
- Get the current user's profile information.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Returns
- -------
- profile : `dict`
- A dictionary containing the current user's profile
- information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "publicId": <str>,
- "email": <str>,
- "login": <str>,
- "firstname": <str>,
- "lastname": <str>,
- "display_name": <str>,
- "country_code": <str>,
- "language_code": <str>,
- "zone": <str>,
- "store": <str>,
- "country": <str>,
- "avatar": <str>,
- "genre": <str>,
- "age": <int>,
- "creation_date": <str>,
- "subscription": {
- "offer": <str>,
- "periodicity": <str>,
- "start_date": <str>,
- "end_date": <str>,
- "is_canceled": <bool>,
- "household_size_max": <int>
- },
- "credential": {
- "id": <int>,
- "label": <str>,
- "description": <str>,
- "parameters": {
- "lossy_streaming": <bool>,
- "lossless_streaming": <bool>,
- "hires_streaming": <bool>,
- "hires_purchases_streaming": <bool>,
- "mobile_streaming": <bool>,
- "offline_streaming": <bool>,
- "hfp_purchase": <bool>,
- "included_format_group_ids": [<int>],
- "color_scheme": {
- "logo": <str>
- },
- "label": <str>,
- "short_label": <str>,
- "source": <str>
- }
- },
- "last_update": {
- "favorite": <int>,
- "favorite_album": <int>,
- "favorite_artist": <int>,
- "favorite_track": <int>,
- "playlist": <int>,
- "purchase": <int>
- },
- "store_features": {
- "download": <bool>,
- "streaming": <bool>,
- "editorial": <bool>,
- "club": <bool>,
- "wallet": <bool>,
- "weeklyq": <bool>,
- "autoplay": <bool>,
- "inapp_purchase_subscripton": <bool>,
- "opt_in": <bool>,
- "music_import": <bool>
- }
- }
- """
-
- self._check_authentication("get_profile")
-
- return self._get_json(f"{self.API_URL}/user/get")
-
-
-
-[docs]
- def get_favorites(
- self, type: str = None, *, limit: int = None,
- offset: int = None) -> dict[str, dict]:
-
- """
- Get the current user's favorite albums, artists, and tracks.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- type : `str`
- Media type to return. If not specified, all of the user's
- favorite items are returned.
-
- .. container::
-
- **Valid values**: :code:`"albums"`, :code:`"artists"`,
- and :code:`"tracks"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of favorited items to return.
-
- **Default**: :code:`50`.
-
- offset : `int`, keyword-only, optional
- The index of the first favorited item to return. Use with
- `limit` to get the next page of favorited items.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- favorites : `dict`
- A dictionary containing Qobuz catalog information for the
- current user's favorite items and the user's ID and email.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "albums": {
- "offset": <int>,
- "limit": <int>,
- "total": <int>,
- "items": <list>
- },
- "user": {
- "id": <int>,
- "login": <str>
- }
- }
- """
-
- self._check_authentication("get_favorites")
-
- if type and type not in (MEDIA_TYPES := {"albums", "artists", "tracks"}):
- emsg = ("Invalid media type. Valid values: "
- f"{', '.join(MEDIA_TYPES)}.")
- raise ValueError(emsg)
-
- timestamp = datetime.datetime.now().timestamp()
- return self._get_json(
- f"{self.API_URL}/favorite/getUserFavorites",
- params={
- "request_ts": timestamp,
- "request_sig": hashlib.md5(
- (f"favoritegetUserFavorites{timestamp}"
- f"{self._app_secret}").encode()
- ).hexdigest(),
- "type": type,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_purchases(
- self, type: str = "albums", *, limit: int = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- Get the current user's purchases.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- Parameters
- ----------
- type : `str`, default: :code:`"albums"`
- Media type.
-
- **Valid values**: :code:`"albums"` and :code:`"tracks"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of albums or tracks to return.
-
- **Default**: :code:`50`.
-
- offset : `int`, keyword-only, optional
- The index of the first album or track to return. Use with
- `limit` to get the next page of albums or tracks.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- purchases : `dict`
- A dictionary containing Qobuz catalog information for the
- current user's purchases.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "offset": <int>,
- "limit": <int>,
- "total": <int>,
- "items": <list>
- }
- """
-
- self._check_authentication("get_purchases")
-
- if type not in (MEDIA_TYPES := {"albums", "tracks"}):
- emsg = ("Invalid media type. Valid values: "
- f"{', '.join(MEDIA_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/purchase/getUserPurchases",
- params={"type": type, "limit": limit, "offset": offset}
- )[type]
-
-
-
-[docs]
- def favorite_items(
- self, *, album_ids: Union[str, list[str]] = None,
- artist_ids: Union[int, str, list[Union[int, str]]] = None,
- track_ids: Union[int, str, list[Union[int, str]]] = None) -> None:
-
- """
- Favorite albums, artists, and/or tracks.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- .. seealso::
-
- For playlists, use :meth:`favorite_playlist`.
-
- Parameters
- ----------
- album_ids : `str` or `list`, keyword-only, optional
- Qobuz album ID(s).
-
- artist_ids : `int`, `str`, or `list`, keyword-only, optional
- Qobuz artist ID(s).
-
- track_ids : `int`, `str`, or `list`, keyword-only, optional
- Qobuz track ID(s).
- """
-
- self._check_authentication("favorite_items")
-
- data = {}
- if album_ids:
- data["album_ids"] = ",".join(str(a) for a in album_ids) \
- if isinstance(album_ids, list) \
- else album_ids
- if artist_ids:
- data["artist_ids"] = ",".join(str(a) for a in artist_ids) \
- if isinstance(artist_ids, list) \
- else artist_ids
- if track_ids:
- data["track_ids"] = ",".join(str(a) for a in track_ids) \
- if isinstance(track_ids, list) \
- else track_ids
- self._request("post", f"{self.API_URL}/favorite/create", data=data)
-
-
-
-[docs]
- def unfavorite_items(
- self, *, album_ids: Union[str, list[str]] = None,
- artist_ids: Union[int, str, list[Union[int, str]]] = None,
- track_ids: Union[int, str, list[Union[int, str]]] = None) -> None:
-
- """
- Unfavorite albums, artists, and/or tracks.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via the password flow.
-
- .. seealso::
-
- For playlists, use :meth:`unfavorite_playlist`.
-
- Parameters
- ----------
- album_ids : `str` or `list`, keyword-only, optional
- Qobuz album ID(s).
-
- artist_ids : `int`, `str`, or `list`, keyword-only, optional
- Qobuz artist ID(s).
-
- track_ids : `int`, `str`, or `list`, keyword-only, optional
- Qobuz track ID(s).
- """
-
- self._check_authentication("unfavorite_items")
-
- data = {}
- if album_ids:
- data["album_ids"] = ",".join(str(a) for a in album_ids) \
- if isinstance(album_ids, list) \
- else album_ids
- if artist_ids:
- data["artist_ids"] = ",".join(str(a) for a in artist_ids) \
- if isinstance(artist_ids, list) \
- else artist_ids
- if track_ids:
- data["track_ids"] = ",".join(str(a) for a in track_ids) \
- if isinstance(track_ids, list) \
- else track_ids
- self._request("post", f"{self.API_URL}/favorite/delete", data=data)
-
-
-
-"""
-Spotify
-=======
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module contains a complete implementation of all Spotify Web API
-endpoints and a minimal implementation to use the private Spotify Lyrics
-service.
-"""
-
-import base64
-import datetime
-import hashlib
-from http.server import HTTPServer, BaseHTTPRequestHandler
-import json
-import logging
-from multiprocessing import Process
-import os
-import re
-import secrets
-import time
-from typing import Any, Union
-import urllib
-import warnings
-import webbrowser
-
-import requests
-
-from . import FOUND_FLASK, FOUND_PLAYWRIGHT, DIR_HOME, DIR_TEMP, _config
-if FOUND_FLASK:
- from flask import Flask, request
-if FOUND_PLAYWRIGHT:
- from playwright.sync_api import sync_playwright
-
-__all__ = ["PrivateLyricsService", "WebAPI"]
-
-class _SpotifyRedirectHandler(BaseHTTPRequestHandler):
-
- """
- HTTP request handler for the Spotify authorization code flow.
- """
-
- def do_GET(self):
-
- """
- Handles an incoming GET request and parses the query string.
- """
-
- self.server.response = dict(
- urllib.parse.parse_qsl(
- urllib.parse.urlparse(f"{self.path}").query
- )
- )
- self.send_response(200)
- self.send_header("Content-Type", "text/html")
- self.end_headers()
- status = "denied" if "error" in self.server.response else "granted"
- self.wfile.write(
- f"Access {status}. You may close this page now.".encode()
- )
-
-
-[docs]
-class PrivateLyricsService:
-
- """
- Spotify Lyrics service client.
-
- The Spotify Lyrics service, which is powered by Musixmatch (or
- PetitLyrics in Japan), provides line- or word-synced lyrics for
- Spotify tracks when available. The Spotify Lyrics interface is not
- publicly documented, so its endpoints have been determined by
- watching HTTP network traffic.
-
- .. attention::
-
- As the Spotify Lyrics service is not designed to be publicly
- accessible, this class can be disabled or removed at any time to
- ensure compliance with the `Spotify Developer Terms of Service
- <https://developer.spotify.com/terms>`_.
-
- Requests to the Spotify Lyrics endpoints must be accompanied by a
- valid access token in the header. An access token can be obtained
- using the Spotify Web Player :code:`sp_dc` cookie, which must either
- be provided to this class's constructor as a keyword argument or be
- stored as :code:`SPOTIFY_SP_DC` in the operating system's
- environment variables.
-
- .. hint::
-
- The :code:`sp_dc` cookie can be extracted from the local storage
- of your web browser after you log into Spotify.
-
- If an existing access token is available, it and its expiry time can
- be provided to this class's constructor as keyword arguments to
- bypass the access token exchange process. It is recommended that all
- other authorization-related keyword arguments be specified so that
- a new access token can be obtained when the existing one expires.
-
- .. tip::
-
- The :code:`sp_dc` cookie and access token can be changed or
- updated at any time using :meth:`set_sp_dc` and
- :meth:`set_access_token`, respectively.
-
- Minim also stores and manages access tokens and their properties.
- When an access token is acquired, it is automatically saved to the
- Minim configuration file to be loaded on the next instantiation of
- this class. This behavior can be disabled if there are any security
- concerns, like if the computer being used is a shared device.
-
- Parameters
- ----------
- sp_dc : `str`, optional
- Spotify Web Player :code:`sp_dc` cookie. If it is not stored
- as :code:`SPOTIFY_SP_DC` in the operating system's environment
- variables or found in the Minim configuration file, it must be
- provided here.
-
- access_token : `str`, keyword-only, optional
- Access token. If provided here or found in the Minim
- configuration file, the authorization process is bypassed. In
- the former case, all other relevant keyword arguments should be
- specified to automatically refresh the access token when it
- expires.
-
- expiry : `datetime.datetime` or `str`, keyword-only, optional
- Expiry time of `access_token` in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated when `access_token` expires.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained access tokens and their
- associated properties are stored to the Minim configuration
- file.
-
- Attributes
- ----------
- LYRICS_URL : `str`
- Base URL for the Spotify Lyrics service.
-
- TOKEN_URL : `str`
- URL for the Spotify Web Player access token endpoint.
-
- session : `requests.Session`
- Session used to send requests to the Spotify Lyrics service.
- """
-
- _NAME = f"{__module__}.{__qualname__}"
-
- LYRICS_URL = "https://spclient.wg.spotify.com/color-lyrics/v2"
- TOKEN_URL = "https://open.spotify.com/get_access_token"
-
- def __init__(
- self, *, sp_dc: str = None, access_token: str = None,
- expiry: Union[datetime.datetime, str] = None, save: bool = True
- ) -> None:
-
- """
- Create a Spotify Lyrics service client.
- """
-
- self.session = requests.Session()
- self.session.headers["App-Platform"] = "WebPlayer"
-
- if access_token is None and _config.has_section(self._NAME):
- sp_dc = _config.get(self._NAME, "sp_dc")
- access_token = _config.get(self._NAME, "access_token")
- expiry = _config.get(self._NAME, "expiry")
-
- self.set_sp_dc(sp_dc, save=save)
- self.set_access_token(access_token=access_token, expiry=expiry)
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _request(
- self, method: str, url: str, retry: bool = True, **kwargs
- ) -> requests.Response:
-
- """
- Construct and send a request with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- retry : `bool`
- Specifies whether to retry the request if the response has
- a non-2xx status code.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- if self._expiry is not None and datetime.datetime.now() > self._expiry:
- self.set_access_token()
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code != 200:
- emsg = f"{r.status_code} {r.reason}"
- if r.status_code == 401 and retry:
- logging.warning(emsg)
- self.set_access_token()
- return self._request(method, url, False, **kwargs)
- else:
- raise RuntimeError(emsg)
- return r
-
-
-[docs]
- def set_sp_dc(self, sp_dc: str = None, *, save: bool = True) -> None:
-
- """
- Set the Spotify Web Player :code:`sp_dc` cookie.
-
- Parameters
- ----------
- sp_dc : `str`, optional
- Spotify Web Player :code:`sp_dc` cookie.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether to save the newly obtained access tokens
- and their associated properties to the Minim configuration
- file.
- """
-
- self._sp_dc = sp_dc or os.environ.get("SPOTIFY_SP_DC")
- self._save = save
-
-
-
-[docs]
- def set_access_token(
- self, access_token: str = None,
- expiry: Union[datetime.datetime, str] = None) -> None:
-
- """
- Set the Spotify Lyrics service access token.
-
- Parameters
- ----------
- access_token : `str`, optional
- Access token. If not provided, an access token is obtained
- from the Spotify Web Player using the :code:`sp_dc` cookie.
-
- expiry : `str` or `datetime.datetime`, keyword-only, optional
- Access token expiry timestamp in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated (if `sp_dc` is found or provided) when the
- `access_token` expires.
- """
-
- if access_token is None:
- if not self._sp_dc:
- raise ValueError("Missing sp_dc cookie.")
-
- r = requests.get(
- self.TOKEN_URL,
- headers={"cookie": f"sp_dc={self._sp_dc}"},
- params={"reason": "transport", "productType": "web_player"}
- ).json()
- if r["isAnonymous"]:
- raise ValueError("Invalid sp_dc cookie.")
- access_token = r["accessToken"]
- expiry = datetime.datetime.fromtimestamp(
- r["accessTokenExpirationTimestampMs"] / 1000
- )
-
- if self._save:
- _config[self._NAME] = {
- "sp_dc": self._sp_dc,
- "access_token": access_token,
- "expiry": expiry.strftime("%Y-%m-%dT%H:%M:%SZ")
- }
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- self.session.headers["Authorization"] = f"Bearer {access_token}"
- self._expiry = (
- datetime.datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ")
- if isinstance(expiry, str) else expiry
- )
-
-
-
-[docs]
- def get_lyrics(self, track_id: str) -> dict[str, Any]:
-
- """
- Get lyrics for a Spotify track.
-
- Parameters
- ----------
- track_id : `str`
- The Spotify ID for the track.
-
- **Example**: :code:`"0VjIjW4GlUZAMYd2vXMi3b"`.
-
- Returns
- -------
- lyrics : `dict`
- Formatted or time-synced lyrics.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "lyrics": {
- "syncType": <str>,
- "lines": [
- {
- "startTimeMs": <str>,
- "words": <str>,
- "syllables": [],
- "endTimeMs": <str>
- }
- ],
- "provider": <str>,
- "providerLyricsId": <str>,
- "providerDisplayName": <str>,
- "syncLyricsUri": <str>,
- "isDenseTypeface": <bool>,
- "alternatives": [],
- "language": <str>,
- "isRtlLanguage": <bool>,
- "fullscreenAction": <str>,
- "showUpsell": <bool>
- },
- "colors": {
- "background": <int>,
- "text": <int>,
- "highlightText": <int>
- },
- "hasVocalRemoval": <bool>
- }
- """
-
- return self._get_json(f"{self.LYRICS_URL}/track/{track_id}",
- params={"format": "json",
- "market": "from_token"})
-
-
-
-
-[docs]
-class WebAPI:
-
- """
- Spotify Web API client.
-
- The Spotify Web API enables the creation of applications that can
- interact with Spotify's streaming service, such as retrieving
- content metadata, getting recommendations, creating and managing
- playlists, or controlling playback.
-
- .. important::
-
- * Spotify content may not be downloaded.
- * Keep visual content in its original form.
- * Ensure content attribution.
-
- .. seealso::
-
- For more information, see the `Spotify Web API Reference
- <https://developer.spotify.com/documentation/web-api>`_.
-
- Requests to the Spotify Web API endpoints must be accompanied by a
- valid access token in the header. An access token can be obtained
- with or without user authentication. While authentication is not
- necessary to search for and retrieve data from public content, it
- is required to access personal content and control playback.
-
- Minim can obtain client-only access tokens via the `client
- credentials <https://developer.spotify.com/documentation/general
- /guides/authorization/client-credentials/>`_ flow and user access
- tokens via the `authorization code <https://developer.spotify.com
- /documentation/web-api/tutorials/code-flow>`_ and `authorization
- code with proof key for code exchange (PKCE)
- <https://developer.spotify.com/documentation/web-api/tutorials/
- code-pkce-flow>`_ flows. These OAuth 2.0 authorization flows
- require valid client credentials (client ID and client secret) to
- either be provided to this class's constructor as keyword arguments
- or be stored as :code:`SPOTIFY_CLIENT_ID` and
- :code:`SPOTIFY_CLIENT_SECRET` in the operating system's environment
- variables.
-
- .. seealso::
-
- To get client credentials, see the `guide on how to create a new
- Spotify application <https://developer.spotify.com/documentation
- /general/guides/authorization/app-settings/>`_. To take advantage
- of Minim's automatic authorization code retrieval functionality
- for the authorization code (with PKCE) flow, the redirect URI
- should be in the form :code:`http://localhost:{port}/callback`,
- where :code:`{port}` is an open port on :code:`localhost`.
-
- Alternatively, a access token can be acquired without client
- credentials through the Spotify Web Player, but this approach is not
- recommended and should only be used as a last resort since it is not
- officially supported and can be deprecated by Spotify at any time.
- The access token is client-only unless a Spotify Web Player
- :code:`sp_dc` cookie is either provided to this class's constructor
- as a keyword argument or be stored as :code:`SPOTIFY_SP_DC` in the
- operating system's environment variables, in which case a user
- access token with all authorization scopes is granted instead.
-
- If an existing access token is available, it and its accompanying
- information (refresh token and expiry time) can be provided to this
- class's constructor as keyword arguments to bypass the access token
- retrieval process. It is recommended that all other
- authorization-related keyword arguments be specified so that a new
- access token can be obtained when the existing one expires.
-
- .. tip::
-
- The authorization flow and access token can be changed or updated
- at any time using :meth:`set_flow` and :meth:`set_access_token`,
- respectively.
-
- Minim also stores and manages access tokens and their properties.
- When any of the authorization flows above are used to acquire an
- access token, it is automatically saved to the Minim configuration
- file to be loaded on the next instantiation of this class. This
- behavior can be disabled if there are any security concerns, like if
- the computer being used is a shared device.
-
- Parameters
- ----------
- client_id : `str`, keyword-only, optional
- Client ID. Required for the authorization code and client
- credentials flows. If it is not stored as
- :code:`SPOTIFY_CLIENT_ID` in the operating system's environment
- variables or found in the Minim configuration file, it must be
- provided here.
-
- client_secret : `str`, keyword-only, optional
- Client secret. Required for the authorization code and client
- credentials flows. If it is not stored as
- :code:`SPOTIFY_CLIENT_SECRET` in the operating system's
- environment variables or found in the Minim configuration file,
- it must be provided here.
-
- flow : `str`, keyword-only, default: :code:`"web_player"`
- Authorization flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"authorization_code"` for the authorization code
- flow.
- * :code:`"pkce"` for the authorization code with proof
- key for code exchange (PKCE) flow.
- * :code:`"client_credentials"` for the client credentials
- flow.
- * :code:`"web_player"` for a Spotify Web Player access
- token.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is automatically opened for the
- authorization code (with PKCE) flow. If :code:`False`, users
- will have to manually open the authorization URL. Not applicable
- when `web_framework="playwright"`.
-
- web_framework : `str`, keyword-only, optional
- Determines which web framework to use for the authorization code
- (with PKCE) flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"http.server"` for the built-in implementation of
- HTTP servers.
- * :code:`"flask"` for the Flask framework.
- * :code:`"playwright"` for the Playwright framework by
- Microsoft.
-
- port : `int` or `str`, keyword-only, default: :code:`8888`
- Port on :code:`localhost` to use for the authorization code
- flow with the :code:`http.server` and Flask frameworks. Only
- used if `redirect_uri` is not specified.
-
- redirect_uri : `str`, keyword-only, optional
- Redirect URI for the authorization code flow. If not on
- :code:`localhost`, the automatic authorization code retrieval
- functionality is not available.
-
- scopes : `str` or `list`, keyword-only, optional
- Authorization scopes to request user access for in the
- authorization code flow.
-
- .. seealso::
-
- See :meth:`get_scopes` for the complete list of scopes.
-
- sp_dc : `str`, keyword-only, optional
- Spotify Web Player :code:`sp_dc` cookie to send with the access
- token request. If provided here, stored as :code:`SPOTIFY_SP_DC`
- in the operating system's environment variables, or found in the
- Minim configuration file, a user access token with all
- authorization scopes is obtained instead of a client-only access
- token.
-
- access_token : `str`, keyword-only, optional
- Access token. If provided here or found in the Minim
- configuration file, the authorization process is bypassed. In
- the former case, all other relevant keyword arguments should be
- specified to automatically refresh the access token when it
- expires.
-
- refresh_token : `str`, keyword-only, optional
- Refresh token accompanying `access_token`. If not provided,
- the user will be reauthenticated using the specified
- authorization flow when `access_token` expires.
-
- expiry : `datetime.datetime` or `str`, keyword-only, optional
- Expiry time of `access_token` in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated using `refresh_token` (if available) or the
- specified authorization flow (if possible) when `access_token`
- expires.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether to overwrite an existing access token in the
- Minim configuration file.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained access tokens and their
- associated properties are stored to the Minim configuration
- file.
-
- Attributes
- ----------
- API_URL : `str`
- Base URL for the Spotify Web API.
-
- AUTH_URL : `str`
- URL for Spotify Web API authorization code requests.
-
- TOKEN_URL : `str`
- URL for Spotify Web API access token requests.
-
- WEB_PLAYER_TOKEN_URL : `str`
- URL for Spotify Web Player access token requests.
-
- session : `requests.Session`
- Session used to send requests to the Spotify Web API.
- """
-
- _FLOWS = {"authorization_code", "pkce", "client_credentials", "web_player"}
- _NAME = f"{__module__}.{__qualname__}"
-
- API_URL = "https://api.spotify.com/v1"
- AUTH_URL = "https://accounts.spotify.com/authorize"
- TOKEN_URL = "https://accounts.spotify.com/api/token"
- WEB_PLAYER_TOKEN_URL = "https://open.spotify.com/get_access_token"
-
-
-[docs]
- @classmethod
- def get_scopes(self, categories: Union[str, list[str]]) -> str:
-
- """
- Get Spotify Web API and Open Access authorization scopes for
- the specified categories.
-
- Parameters
- ----------
- categories : `str` or `list`
- Categories of authorization scopes to get.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"images"` for scopes related to custom images,
- such as :code:`ugc-image-upload`.
- * :code:`"spotify_connect"` for scopes related to Spotify
- Connect, such as
-
- * :code:`user-read-playback-state`,
- * :code:`user-modify-playback-state`, and
- * :code:`user-read-currently-playing`.
-
- * :code:`"playback"` for scopes related to playback
- control, such as :code:`app-remote-control` and
- :code:`streaming`.
- * :code:`"playlists"` for scopes related to playlists,
- such as
-
- * :code:`playlist-read-private`,
- * :code:`playlist-read-collaborative`,
- * :code:`playlist-modify-private`, and
- * :code:`playlist-modify-public`.
-
- * :code:`"follow"` for scopes related to followed artists
- and users, such as :code:`user-follow-modify` and
- :code:`user-follow-read`.
- * :code:`"listening_history"` for scopes related to
- playback history, such as
-
- * :code:`user-read-playback-position`,
- * :code:`user-top-read`, and
- * :code:`user-read-recently-played`.
-
- * :code:`"library"` for scopes related to saved content,
- such as :code:`user-library-modify` and
- :code:`user-library-read`.
- * :code:`"users"` for scopes related to user information,
- such as :code:`user-read-email` and
- :code:`user-read-private`.
- * :code:`"all"` for all scopes above.
- * A substring to match in the possible scopes, such as
-
- * :code:`"read"` for all scopes above that grant read
- access, i.e., scopes with :code:`read` in the name,
- * :code:`"modify"` for all scopes above that grant
- modify access, i.e., scopes with :code:`modify` in
- the name, or
- * :code:`"user"` for all scopes above that grant access
- to all user-related information, i.e., scopes with
- :code:`user` in the name.
-
- .. seealso::
-
- For the endpoints that the scopes allow access to, see the
- `Scopes page of the Spotify Web API Reference
- <https://developer.spotify.com/documentation/web-api
- /concepts/scopes>`_.
- """
-
- SCOPES = {
- "images": ["ugc-image-upload"],
- "spotify_connect": ["user-read-playback-state",
- "user-modify-playback-state",
- "user-read-currently-playing"],
- "playback": ["app-remote-control streaming"],
- "playlists": ["playlist-read-private",
- "playlist-read-collaborative",
- "playlist-modify-private",
- "playlist-modify-public"],
- "follow": ["user-follow-modify", "user-follow-read"],
- "listening_history": ["user-read-playback-position",
- "user-top-read",
- "user-read-recently-played"],
- "library": ["user-library-modify", "user-library-read"],
- "users": ["user-read-email", "user-read-private"]
- }
-
- if isinstance(categories, str):
- if categories in SCOPES.keys():
- return SCOPES[categories]
- if categories == "all":
- return " ".join(s for scopes in SCOPES.values()
- for s in scopes)
- return " ".join(s for scopes in SCOPES.values()
- for s in scopes if categories in s)
-
- return " ".join(s
- for scopes in (self.get_scopes[c] for c in categories)
- for s in scopes)
-
-
- def __init__(
- self, *, client_id: str = None, client_secret: str = None,
- flow: str = "web_player", browser: bool = False,
- web_framework: str = None, port: Union[int, str] = 8888,
- redirect_uri: str = None, scopes: Union[str, list[str]] = "",
- sp_dc: str = None, access_token: str = None,
- refresh_token: str = None,
- expiry: Union[datetime.datetime, str] = None,
- overwrite: bool = False, save: bool = True) -> None:
-
- """
- Create a Spotify Web API client.
- """
-
- self.session = requests.Session()
-
- if (access_token is None and _config.has_section(self._NAME)
- and not overwrite):
- flow = _config.get(self._NAME, "flow")
- access_token = _config.get(self._NAME, "access_token")
- refresh_token = _config.get(self._NAME, "refresh_token",
- fallback=None)
- expiry = _config.get(self._NAME, "expiry", fallback=None)
- client_id = _config.get(self._NAME, "client_id")
- client_secret = _config.get(self._NAME, "client_secret",
- fallback=None)
- redirect_uri = _config.get(self._NAME, "redirect_uri",
- fallback=None)
- scopes = _config.get(self._NAME, "scopes")
- sp_dc = _config.get(self._NAME, "sp_dc", fallback=None)
-
- self.set_flow(
- flow, client_id=client_id, client_secret=client_secret,
- browser=browser, web_framework=web_framework, port=port,
- redirect_uri=redirect_uri, scopes=scopes, sp_dc=sp_dc, save=save
- )
- self.set_access_token(access_token, refresh_token=refresh_token,
- expiry=expiry)
-
- def _check_scope(self, endpoint: str, scope: str) -> None:
-
- """
- Check if the user has granted the appropriate authorization
- scope for the desired endpoint.
-
- Parameters
- ----------
- endpoint : `str`
- Spotify Web API endpoint.
-
- scope : `str`
- Required scope for `endpoint`.
- """
-
- if scope not in self._scopes:
- emsg = (f"{self._NAME}.{endpoint}() requires the '{scope}' "
- "authorization scope.")
- raise RuntimeError(emsg)
-
- def _get_authorization_code(self, code_challenge: str = None) -> str:
-
- """
- Get an authorization code to be exchanged for an access token in
- the authorization code flow.
-
- Parameters
- ----------
- code_challenge : `str`, optional
- Code challenge for the authorization code with PKCE flow.
-
- Returns
- -------
- auth_code : `str`
- Authorization code.
- """
-
- params = {
- "client_id": self._client_id,
- "redirect_uri": self._redirect_uri,
- "response_type": "code",
- "state": secrets.token_urlsafe()
- }
- if self._scopes:
- params["scope"] = self._scopes
- if code_challenge is not None:
- params["code_challenge"] = code_challenge
- params["code_challenge_method"] = "S256"
- auth_url = f"{self.AUTH_URL}?{urllib.parse.urlencode(params)}"
-
- if self._web_framework == "playwright":
- har_file = DIR_TEMP / "minim_spotify.har"
-
- with sync_playwright() as playwright:
- browser = playwright.firefox.launch(headless=False)
- context = browser.new_context(record_har_path=har_file)
- page = context.new_page()
- page.goto(auth_url, timeout=0)
- page.wait_for_url(f"{self._redirect_uri}*",
- wait_until="commit")
- context.close()
- browser.close()
-
- with open(har_file, "r") as f:
- queries = dict(
- urllib.parse.parse_qsl(
- urllib.parse.urlparse(
- re.search(f'{self._redirect_uri}\?(.*?)"',
- f.read()).group(0)
- ).query
- )
- )
- har_file.unlink()
-
- else:
- if self._browser:
- webbrowser.open(auth_url)
- else:
- print("To grant Minim access to Spotify data and "
- "features, open the following link in your web "
- f"browser:\n\n{auth_url}\n")
-
- if self._web_framework == "http.server":
- httpd = HTTPServer(("", self._port), _SpotifyRedirectHandler)
- httpd.handle_request()
- queries = httpd.response
-
- elif self._web_framework == "flask":
- app = Flask(__name__)
- json_file = DIR_TEMP / "minim_spotify.json"
-
- @app.route("/callback", methods=["GET"])
- def _callback() -> str:
- if "error" in request.args:
- return "Access denied. You may close this page now."
- with open(json_file, "w") as f:
- json.dump(request.args, f)
- return "Access granted. You may close this page now."
-
- server = Process(target=app.run, args=("0.0.0.0", self._port))
- server.start()
- while not json_file.is_file():
- time.sleep(0.1)
- server.terminate()
-
- with open(json_file, "rb") as f:
- queries = json.load(f)
- json_file.unlink()
-
- else:
- uri = input("After authorizing Minim to access Spotify on "
- "your behalf, copy and paste the URI beginning "
- f"with '{self._redirect_uri}' below.\n\nURI: ")
- queries = dict(
- urllib.parse.parse_qsl(urllib.parse.urlparse(uri).query)
- )
-
- if "error" in queries:
- raise RuntimeError(f"Authorization failed. Error: {queries['error']}")
- if params["state"] != queries["state"]:
- raise RuntimeError("Authorization failed due to state mismatch.")
- return queries["code"]
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _refresh_access_token(self) -> None:
-
- """
- Refresh the expired excess token.
- """
-
- if self._flow == "web_player" or not self._refresh_token \
- or not self._client_id or not self._client_secret:
- self.set_access_token()
- else:
- client_b64 = base64.urlsafe_b64encode(
- f"{self._client_id}:{self._client_secret}".encode()
- ).decode()
- r = requests.post(
- self.TOKEN_URL,
- data={
- "grant_type": "refresh_token",
- "refresh_token": self._refresh_token
- },
- headers={"Authorization": f"Basic {client_b64}"}
- ).json()
-
- self.session.headers["Authorization"] = f"Bearer {r['access_token']}"
- self._refresh_token = r["refresh_token"]
- self._expiry = (datetime.datetime.now()
- + datetime.timedelta(0, r["expires_in"]))
- self._scopes = r["scope"]
-
- if self._save:
- _config[self._NAME].update({
- "access_token": r["access_token"],
- "refresh_token": self._refresh_token,
- "expiry": self._expiry.strftime("%Y-%m-%dT%H:%M:%SZ"),
- "scopes": self._scopes
- })
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- def _request(
- self, method: str, url: str, retry: bool = True, **kwargs
- ) -> requests.Response:
-
- """
- Construct and send a request with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- retry : `bool`
- Specifies whether to retry the request if the response has
- a non-2xx status code.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- if self._expiry is not None and datetime.datetime.now() > self._expiry:
- self._refresh_access_token()
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code not in range(200, 299):
- error = r.json()["error"]
- emsg = f"{error['status']} {error['message']}"
- if r.status_code == 401 and retry:
- logging.warning(emsg)
- self._refresh_access_token()
- return self._request(method, url, False, **kwargs)
- else:
- raise RuntimeError(emsg)
- return r
-
-
-[docs]
- def set_access_token(
- self, access_token: str = None, *, refresh_token: str = None,
- expiry: Union[str, datetime.datetime] = None) -> None:
-
- """
- Set the Spotify Web API access token.
-
- Parameters
- ----------
- access_token : `str`, optional
- Access token. If not provided, an access token is obtained
- using an OAuth 2.0 authorization flow or from the Spotify
- Web Player.
-
- refresh_token : `str`, keyword-only, optional
- Refresh token accompanying `access_token`.
-
- expiry : `str` or `datetime.datetime`, keyword-only, optional
- Access token expiry timestamp in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated using the refresh token (if available) or
- the default authorization flow (if possible) when
- `access_token` expires.
- """
-
- if access_token is None:
- if self._flow == "web_player":
- headers = ({"cookie": f"sp_dc={self._sp_dc}"} if self._sp_dc
- else {})
- r = requests.get(self.WEB_PLAYER_TOKEN_URL,
- headers=headers).json()
- self._client_id = r["clientId"]
- access_token = r["accessToken"]
- expiry = datetime.datetime.fromtimestamp(
- r["accessTokenExpirationTimestampMs"] / 1000
- )
- if self._sp_dc and r["isAnonymous"]:
- wmsg = ("The sp_dc cookie is invalid, so the "
- "access token granted is client-only.")
- warnings.warn(wmsg)
- else:
- if not self._client_id or not self._client_secret:
- emsg = "Spotify Web API client credentials not provided."
- raise ValueError(emsg)
-
- if self._flow == "client_credentials":
- r = requests.post(
- self.TOKEN_URL,
- data={
- "client_id": self._client_id,
- "client_secret": self._client_secret,
- "grant_type": "client_credentials"
- }
- ).json()
- else:
- client_b64 = base64.urlsafe_b64encode(
- f"{self._client_id}:{self._client_secret}".encode()
- ).decode()
- data = {
- "grant_type": "authorization_code",
- "redirect_uri": self._redirect_uri
- }
- if self._flow == "pkce":
- data["client_id"] = self._client_id
- data["code_verifier"] = secrets.token_urlsafe(96)
- data["code"] = self._get_authorization_code(
- base64.urlsafe_b64encode(
- hashlib.sha256(
- data["code_verifier"].encode()
- ).digest()
- ).decode().replace("=", "")
- )
- else:
- data["code"] = self._get_authorization_code()
- r = requests.post(
- self.TOKEN_URL, data=data,
- headers={"Authorization": f"Basic {client_b64}"}
- ).json()
- refresh_token = r["refresh_token"]
- access_token = r["access_token"]
- expiry = (datetime.datetime.now()
- + datetime.timedelta(0, r["expires_in"]))
-
- if self._save:
- _config[self._NAME] = {
- "flow": self._flow,
- "client_id": self._client_id,
- "access_token": access_token,
- "expiry": expiry.strftime("%Y-%m-%dT%H:%M:%SZ"),
- "scopes": self._scopes
- }
- if refresh_token:
- _config[self._NAME]["refresh_token"] \
- = refresh_token
- for attr in ("client_secret", "redirect_uri", "sp_dc"):
- if hasattr(self, f"_{attr}"):
- _config[self._NAME][attr] \
- = getattr(self, f"_{attr}") or ""
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- self.session.headers["Authorization"] = f"Bearer {access_token}"
- self._refresh_token = refresh_token
- self._expiry = (
- datetime.datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ")
- if isinstance(expiry, str) else expiry
- )
-
- if self._flow in {"authorization_code", "pkce"} \
- or (self._flow == "web_player" and self._sp_dc):
- self._user_id = self.get_profile()["id"]
-
-
-
-[docs]
- def set_flow(
- self, flow: str, *, client_id: str = None,
- client_secret: str = None, browser: bool = False,
- web_framework: str = None, port: Union[int, str] = 8888,
- redirect_uri: str = None, scopes: Union[str, list[str]] = "",
- sp_dc: str = None, save: bool = True) -> None:
-
- """
- Set the authorization flow.
-
- Parameters
- ----------
- flow : `str`
- Authorization flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"authorization_code"` for the authorization code
- flow.
- * :code:`"pkce"` for the authorization code with proof
- key for code exchange (PKCE) flow.
- * :code:`"client_credentials"` for the client credentials
- flow.
- * :code:`"web_player"` for a Spotify Web Player access
- token.
-
- client_id : `str`, keyword-only, optional
- Client ID. Required for all OAuth 2.0 authorization flows.
-
- client_secret : `str`, keyword-only, optional
- Client secret. Required for all OAuth 2.0 authorization
- flows.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is automatically opened for
- the authorization code (with PKCE) flow. If :code:`False`,
- users will have to manually open the authorization URL.
- Not applicable when `web_framework="playwright"`.
-
- web_framework : `str`, keyword-only, optional
- Web framework used to automatically complete the
- authorization code (with PKCE) flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"http.server"` for the built-in implementation of
- HTTP servers.
- * :code:`"flask"` for the Flask framework.
- * :code:`"playwright"` for the Playwright framework.
-
- port : `int` or `str`, keyword-only, default: :code:`8888`
- Port on :code:`localhost` to use for the authorization code
- flow with the :code:`http.server` and Flask frameworks.
-
- redirect_uri : `str`, keyword-only, optional
- Redirect URI for the authorization code flow. If not
- specified, an open port on :code:`localhost` will be used.
-
- scopes : `str` or `list`, keyword-only, optional
- Authorization scopes to request access to in the
- authorization code flow.
-
- sp_dc : `str`, keyword-only, optional
- Spotify Web Player :code:`sp_dc` cookie.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether to save the newly obtained access tokens
- and their associated properties to the Minim configuration
- file.
- """
-
- if flow not in self._FLOWS:
- emsg = (f"Invalid authorization flow ({flow=}). "
- f"Valid values: {', '.join(self._FLOWS)}.")
- raise ValueError(emsg)
- self._flow = flow
- self._save = save
-
- if flow == "web_player":
- self._sp_dc = sp_dc or os.environ.get("SPOTIFY_SP_DC")
- self._scopes = self.get_scopes("all") if self._sp_dc else ""
- else:
- self._client_id = client_id or os.environ.get("SPOTIFY_CLIENT_ID")
- self._client_secret = \
- client_secret or os.environ.get("SPOTIFY_CLIENT_SECRET")
- if flow in {"authorization_code", "pkce"}:
- self._browser = browser
- self._scopes = " ".join(scopes) if isinstance(scopes, list) \
- else scopes
-
- if redirect_uri:
- self._redirect_uri = redirect_uri
- if "localhost" in redirect_uri:
- self._port = re.search("localhost:(\d+)",
- redirect_uri).group(1)
- elif web_framework:
- wmsg = ("The redirect URI is not on localhost, "
- "so automatic authorization code "
- "retrieval is not available.")
- logging.warning(wmsg)
- web_framework = None
- elif port:
- self._port = port
- self._redirect_uri = f"http://localhost:{port}/callback"
- else:
- self._port = self._redirect_uri = None
-
- self._web_framework = (
- web_framework
- if web_framework in {None, "http.server"}
- or globals()[f"FOUND_{web_framework.upper()}"]
- else None
- )
- if self._web_framework is None and web_framework:
- wmsg = (f"The {web_framework.capitalize()} web "
- "framework was not found, so automatic "
- "authorization code retrieval is not "
- "available.")
- warnings.warn(wmsg)
-
- elif flow == "client_credentials":
- self._scopes = ""
-
-
- ### ALBUMS ################################################################
-
-
-[docs]
- def get_album(self, id: str, *, market: str = None) -> dict:
-
- """
- `Albums > Get Album <https://developer.spotify.com/
- documentation/web-api/reference/get-an-album>`_:
- Get Spotify catalog information for a single album.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the album.
-
- **Example**: :code:`"4aawyAB9vmqN3uQ7FjRGTy"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- album : `dict`
- Spotify catalog information for a single album.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "tracks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "type": <str>,
- "uri": <str>
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": <str>,
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/albums/{id}",
- params={"market": market})
-
-
-
-[docs]
- def get_albums(
- self, ids: Union[str, list[str]], *, market: str = None
- ) -> dict[str, Any]:
-
- """
- `Albums > Get Several Albums <https://developer.spotify.com/
- documentation/web-api/reference/
- get-multiple-albums>`_: Get Spotify catalog information for
- albums identified by their Spotify IDs.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the albums.
-
- **Maximum**: 20 IDs.
-
- **Example**: :code:`"382ObEPsp2rxGrnsizN5TX,
- 1A2GTWGtFfWp7KSQTwWOyo, 2noRn2Aes5aoNVsU6iWThc"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- albums : `list`
- A list containing Spotify catalog information for multiple
- albums.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "tracks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "type": <str>,
- "uri": <str>
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": <str>,
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- }
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/albums",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "market": market}
- )["albums"]
-
-
-
-[docs]
- def get_album_tracks(
- self, id: str, *, limit: int = None, market: str = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- `Albums > Get Album Tracks <https://developer.spotify.com/
- documentation/web-api/reference/
- get-an-albums-tracks>`_: Get Spotify catalog information for an
- album's tracks. Optional parameters can be used to limit the
- number of tracks returned.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the album.
-
- **Example**: :code:`"4aawyAB9vmqN3uQ7FjRGTy"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing Spotify catalog information for an
- album's tracks and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "type": <str>,
- "uri": <str>
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": <str>,
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/albums/{id}/tracks",
- params={"limit": limit, "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def get_saved_albums(
- self, *, limit: int = None, market: str = None, offset: int = None
- ) -> dict[str, Any]:
-
- """
- `Albums > Get User's Saved Albums <https://developer.spotify.com/
- documentation/web-api/reference/
- get-users-saved-albums>`_: Get a list of the albums saved in the
- current Spotify user's 'Your Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- albums : `dict`
- A dictionary containing Spotify catalog information for a
- user's saved albums and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "tracks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "type": <str>,
- "uri": <str>
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": <str>,
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- }
- }
- }
- ]
- }
- """
-
- self._check_scope("get_saved_albums", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/albums",
- params={"limit": limit, "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def save_albums(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Albums > Save Albums for Current User
- <https://developer.spotify.com/documentation/web-api/reference/
- save-albums-user>`_: Save one or more albums to the
- current user's 'Your Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the albums.
-
- **Maximum**: 20 (`str`) or 50 (`list`) IDs.
-
- **Example**: :code:`"382ObEPsp2rxGrnsizN5TX,
- 1A2GTWGtFfWp7KSQTwWOyo, 2noRn2Aes5aoNVsU6iWThc"`.
- """
-
- self._check_scope("save_albums", "user-library-modify")
-
- if isinstance(ids, str):
- self._request("put", f"{self.API_URL}/me/albums",
- params={"ids": ids})
- elif isinstance(ids, list):
- self._request("put", f"{self.API_URL}/me/albums",
- json={"ids": ids})
-
-
-
-[docs]
- def remove_saved_albums(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Albums > Remove Users' Saved Albums
- <https://developer.spotify.com/documentation/web-api/reference/
- remove-albums-user>`_: Remove one or more albums
- from the current user's 'Your Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the albums.
-
- **Maximum**: 20 (`str`) or 50 (`list`) IDs.
-
- **Example**: :code:`"382ObEPsp2rxGrnsizN5TX,
- 1A2GTWGtFfWp7KSQTwWOyo, 2noRn2Aes5aoNVsU6iWThc"`.
- """
-
- self._check_scope("remove_saved_albums", "user-library-modify")
-
- if isinstance(ids, str):
- self._request("delete", f"{self.API_URL}/me/albums",
- params={"ids": ids})
- elif isinstance(ids, list):
- self._request("delete", f"{self.API_URL}/me/albums",
- json={"ids": ids})
-
-
-
-[docs]
- def check_saved_albums(self, ids: Union[str, list[str]]) -> list[bool]:
-
- """
- `Albums > Check User's Saved Albums
- <https://developer.spotify.com/documentation/web-api/reference/
- check-users-saved-albums>`_: Check if one or more
- albums is already saved in the current Spotify user's 'Your
- Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the albums.
-
- **Maximum**: 20 IDs.
-
- Returns
- -------
- contains : `list`
- Array of booleans specifying whether the albums are found in
- the user's 'Your Library > Albums'.
-
- **Example**: :code:`[False, True]`.
- """
-
- self._check_scope("check_saved_albums", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/albums/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )
-
-
-
-[docs]
- def get_new_albums(
- self, *, country: str = None, limit: int = None, offset: int = None
- ) -> list[dict[str, Any]]:
-
- """
- `Albums > Get New Releases <https://developer.spotify.com/
- documentation/web-api/reference/
- get-new-releases>`_: Get a list of new album releases featured
- in Spotify (shown, for example, on a Spotify player's "Browse"
- tab).
-
- Parameters
- ----------
- country : `str`, keyword-only, optional
- A country: an ISO 3166-1 alpha-2 country code. Provide this
- parameter if you want the list of returned items to be
- relevant to a particular country. If omitted, the returned
- items will be relevant to all countries.
-
- **Example**: :code:`"SE"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- albums : `list`
- A list containing Spotify catalog information for
- newly-released albums.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/browse/new-releases",
- params={"country": country, "limit": limit, "offset": offset}
- )["albums"]
-
-
- ### ARTISTS ###############################################################
-
-
-[docs]
- def get_artist(self, id: str) -> dict[str, Any]:
-
- """
- `Artists > Get Artist <https://developer.spotify.com/
- documentation/web-api/reference/get-an-artist>`_:
- Get Spotify catalog information for a single artist identified
- by their unique Spotify ID.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the artist.
-
- **Example**: :code:`"0TnOYISbd1XYRBk9myaseg"`.
-
- Returns
- -------
- artist : `dict`
- Spotify catalog information for a single artist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- """
-
- return self._get_json(f"{self.API_URL}/artists/{id}")
-
-
-
-[docs]
- def get_artists(
- self, ids: Union[int, str, list[Union[int, str]]]
- ) -> list[dict[str, Any]]:
-
- """
- `Artists > Get Several Artists <https://developer.spotify.com/
- documentation/web-api/reference/
- get-multiple-artists>`_: Get Spotify catalog information for
- several artists based on their Spotify IDs.
-
- Parameters
- ----------
- ids : `str`
- A (comma-separated) list of the Spotify IDs for the artists.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"2CIMQHirSU0MQqyYHq0eOx,
- 57dN52uHvrHOxijzpIgu3E, 1vCWHaC5f2uS3yhpwWbIA6"`.
-
- Returns
- -------
- artists : `list`
- A list containing Spotify catalog information for multiple
- artists.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/artists",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )["artists"]
-
-
-
-[docs]
- def get_artist_albums(
- self, id: str, *, include_groups: Union[str, list[str]] = None,
- limit: int = None, market: str = None, offset: int = None
- ) -> list[dict[str, Any]]:
-
- """
- `Artist > Get Artist's Albums <https://developer.spotify.com/
- documentation/web-api/reference/
- get-an-artists-albums>`_: Get Spotify catalog information about
- an artist's albums.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the artist.
-
- **Example**: :code:`"0TnOYISbd1XYRBk9myaseg"`.
-
- include_groups : `str` or `list`, keyword-only, optional
- A comma-separated list of keywords that will be used to
- filter the response. If not supplied, all album types will
- be returned.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"album"` for albums.
- * :code:`"single"` for singles or promotional releases.
- * :code:`"appears_on"` for albums that `artist` appears
- on as a featured artist.
- * :code:`"compilation"` for compilations.
-
- **Examples**:
-
- * :code:`"album,single"` for albums and singles where
- `artist` is the main album artist.
- * :code:`"single,appears_on"` for singles and albums that
- `artist` appears on.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- albums : `list`
- A list containing Spotify catalog information for the
- artist's albums.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artists/{id}/albums",
- params={"include_groups": include_groups, "limit": limit,
- "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def get_artist_top_tracks(
- self, id: str, *, market: str = "US") -> list[dict[str, Any]]:
-
- """
- `Artist > Get Artist's Top Tracks
- <https://developer.spotify.com/documentation/web-api/reference/
- get-an-artists-top-tracks>`_: Get Spotify catalog
- information about an artist's top tracks by country.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the artist.
-
- **Example**: :code:`"0TnOYISbd1XYRBk9myaseg"`.
-
- market : `str`, keyword-only, default: :code:`"US"`
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- tracks : `list`
- A list containing Spotify catalog information for the
- artist's top tracks.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>,
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- """
-
- return self._get_json(f"{self.API_URL}/artists/{id}/top-tracks",
- params={"market": market})["tracks"]
-
-
-
-
-
- ### AUDIOBOOKS ############################################################
-
-
-[docs]
- def get_audiobook(self, id: str, *, market: str = None) -> dict[str, Any]:
-
- """
- `Audiobooks > Get an Audiobook
- <https://developer.spotify.com/documentation/web-api/reference/
- get-an-audiobook>`_: Get Spotify catalog information for a
- single audiobook.
-
- .. note::
-
- Audiobooks are only available for the US, UK, Ireland, New
- Zealand, and Australia markets.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the audiobook.
-
- **Example**: :code:`"7iHfbu1YPACw6oZPAFJtqe"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- audiobook : `dict`
- Spotify catalog information for a single audiobook.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "authors": [
- {
- "name": <str>
- }
- ],
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "edition": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "narrators": [
- {
- "name": <str>
- }
- ],
- "publisher": <str>,
- "type": "audiobook",
- "uri": <str>,
- "total_chapters": <int>,
- "chapters": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "audio_preview_url": <str>,
- "available_markets": [<str>],
- "chapter_number": <int>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_playable": <bool>
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- }
- }
- ]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/audiobooks/{id}",
- params={"market": market})
-
-
-
-[docs]
- def get_audiobooks(
- self, ids: Union[int, str, list[Union[int, str]]], *,
- market: str = None) -> list[dict[str, Any]]:
-
- """
- `Audiobooks > Get Several Audiobooks
- <https://developer.spotify.com/documentation/web-api/reference/
- get-multiple-audiobooks>`_: Get Spotify catalog
- information for several audiobooks identified by their Spotify
- IDs.
-
- .. note::
-
- Audiobooks are only available for the US, UK, Ireland, New
- Zealand, and Australia markets.
-
- Parameters
- ----------
- ids : `int`, `str`, or `list`
- A (comma-separated) list of the Spotify IDs for the
- audiobooks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"18yVqkdbdRvS24c0Ilj2ci,
- 1HGw3J3NxZO1TP1BTtVhpZ, 7iHfbu1YPACw6oZPAFJtqe"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- audiobooks : `dict` or `list`
- A list containing Spotify catalog information for multiple
- audiobooks.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "authors": [
- {
- "name": <str>
- }
- ],
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "edition": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "narrators": [
- {
- "name": <str>
- }
- ],
- "publisher": <str>,
- "type": "audiobook",
- "uri": <str>,
- "total_chapters": <int>,
- "chapters": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "audio_preview_url": <str>,
- "available_markets": [<str>],
- "chapter_number": <int>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_playable": <bool>
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- }
- }
- ]
- }
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/audiobooks",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "market": market}
- )["audiobooks"]
-
-
-
-[docs]
- def get_audiobook_chapters(
- self, id: str, *, limit: int = None, market: str = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- `Audiobooks > Get Audiobook Chapters
- <https://developer.spotify.com/documentation/web-api/reference/
- get-audiobook-chapters>`_: Get Spotify catalog
- information about an audiobook's chapters.
-
- .. note::
-
- Audiobooks are only available for the US, UK, Ireland, New
- Zealand, and Australia markets.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the audiobook.
-
- **Example**: :code:`"7iHfbu1YPACw6oZPAFJtqe"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- audiobooks : `dict`
- A dictionary containing Spotify catalog information for an
- audiobook's chapters and the number of results returned.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "audio_preview_url": <str>,
- "available_markets": [<str>],
- "chapter_number": <int>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_playable": <bool>
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- }
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/audiobooks/{id}/chapters",
- params={"limit": limit, "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def get_saved_audiobooks(
- self, *, limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Audiobooks > Get User's Saved Audiobooks
- <https://developer.spotify.com/documentation/web-api/reference/
- get-users-saved-audiobooks>`_: Get a list of the
- albums saved in the current Spotify user's audiobooks library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- audiobooks : `dict`
- A dictionary containing Spotify catalog information for a
- user's saved audiobooks and the number of results
- returned.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "authors": [
- {
- "name": <str>
- }
- ],
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "edition": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "narrators": [
- {
- "name": <str>
- }
- ],
- "publisher": <str>,
- "type": "audiobook",
- "uri": <str>,
- "total_chapters": <int>
- }
- ]
- }
- """
-
- self._check_scope("get_saved_audiobooks", "user-library-read")
-
- return self._get_json(f"{self.API_URL}/me/audiobooks",
- params={"limit": limit, "offset": offset})
-
-
-
-[docs]
- def save_audiobooks(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Audiobooks > Save Audiobooks for Current User
- <https://developer.spotify.com/documentation/web-api/reference/
- save-audiobooks-user>`_: Save one or more
- audiobooks to current Spotify user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the
- audiobooks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"18yVqkdbdRvS24c0Ilj2ci,
- 1HGw3J3NxZO1TP1BTtVhpZ, 7iHfbu1YPACw6oZPAFJtqe"`.
- """
-
- self._check_scope("save_audiobooks", "user-library-modify")
-
- self._request(
- "put", f"{self.API_URL}/me/audiobooks",
- params={"ids": f"{ids if isinstance(ids, str) else ','.join(ids)}"}
- )
-
-
-
-[docs]
- def remove_saved_audiobooks(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Audiobooks > Remove User's Saved Audiobooks
- <https://developer.spotify.com/documentation/web-api/reference/
- remove-audiobooks-user>`_: Delete one or more
- audiobooks from current Spotify user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the
- audiobooks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"18yVqkdbdRvS24c0Ilj2ci,
- 1HGw3J3NxZO1TP1BTtVhpZ, 7iHfbu1YPACw6oZPAFJtqe"`.
- """
-
- self._check_scope("remove_saved_audiobooks", "user-library-modify")
-
- self._request(
- "delete", f"{self.API_URL}/me/audiobooks",
- params={"ids": f"{ids if isinstance(ids, str) else ','.join(ids)}"}
- )
-
-
-
-[docs]
- def check_saved_audiobooks(self, ids: Union[str, list[str]]) -> list[bool]:
-
- """
- `Audiobooks > Check User's Saved Audiobooks
- <https://developer.spotify.com/documentation/web-api/reference/
- check-users-saved-audiobooks>`_: Check if one or
- more audiobooks are already saved in the current Spotify user's
- library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the
- audiobooks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"18yVqkdbdRvS24c0Ilj2ci,
- 1HGw3J3NxZO1TP1BTtVhpZ, 7iHfbu1YPACw6oZPAFJtqe"`.
-
- Returns
- -------
- contains : `list`
- Array of booleans specifying whether the audiobooks are
- found in the user's saved audiobooks.
-
- **Example**: :code:`[False, True]`.
- """
-
- self._check_scope("check_saved_audiobooks", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/audiobooks/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )
-
-
- ### CATEGORIES ############################################################
-
-
-[docs]
- def get_category(
- self, category_id: str, *, country: str = None, locale: str = None
- ) -> dict[str, Any]:
-
- """
- `Categories > Get Single Browse Category
- <https://developer.spotify.com/documentation/web-api/reference/
- get-a-category>`_: Get a single category used to
- tag items in Spotify (on, for example, the Spotify player's
- "Browse" tab).
-
- Parameters
- ----------
- category_id : `str`
- The Spotify category ID for the category.
-
- **Example**: :code:`"dinner"`.
-
- country : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. Provide this parameter
- to ensure that the category exists for a particular country.
-
- **Example**: :code:`"SE"`.
-
- locale : `str`, keyword-only, optional
- The desired language, consisting of an ISO 639-1 language
- code and an ISO 3166-1 alpha-2 country code, joined by an
- underscore. Provide this parameter if you want the category
- strings returned in a particular language.
-
- .. note::
-
- If `locale` is not supplied, or if the specified language
- is not available, the category strings returned will be
- in the Spotify default language (American English).
-
- **Example**: :code:`"es_MX"` for "Spanish (Mexico)".
-
- Returns
- -------
- category : `dict`
- Information for a single browse category.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "icons": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "id": <str>,
- "name": <str>
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/browse/categories/{category_id}",
- params={"country": country, "locale": locale}
- )
-
-
-
-[docs]
- def get_categories(
- self, *, country: str = None, limit: int = None,
- locale: str = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Categories > Get Several Browse Categories
- <https://developer.spotify.com/documentation/web-api/reference/
- get-categories>`_: Get a list of categories used to
- tag items in Spotify (on, for example, the Spotify player's
- "Browse" tab).
-
- Parameters
- ----------
- country : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. Provide this parameter
- to ensure that the category exists for a particular country.
-
- **Example**: :code:`"SE"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- locale : `str`, keyword-only, optional
- The desired language, consisting of an ISO 639-1 language
- code and an ISO 3166-1 alpha-2 country code, joined by an
- underscore. Provide this parameter if you want the category
- strings returned in a particular language.
-
- .. note::
-
- If locale is not supplied, or if the specified language
- is not available, the category strings returned will be
- in the Spotify default language (American English).
-
- **Example**: :code:`"es_MX"` for "Spanish (Mexico)".
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- categories : `dict`
- A dictionary containing nformation for the browse categories
- and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "items": [
- {
- "href": <str>,
- "icons": [
- {
- "height": <int>,
- "url": <str>,
- "width": <int>
- }
- ],
- "id": <str>,
- "name": <str>
- }
- ],
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/browse/categories",
- params={"country": country, "limit": limit, "locale": locale,
- "offset": offset}
- )["categories"]
-
-
- ### CHAPTERS ##############################################################
-
-
-[docs]
- def get_chapter(self, id: str, *, market: str = None) -> dict[str, Any]:
-
- """
- `Chapters > Get a Chapter <https://developer.spotify.com/
- documentation/web-api/reference/get-a-chapter>`_:
- Get Spotify catalog information for a single chapter.
-
- .. note::
- Chapters are only available for the US, UK, Ireland, New
- Zealand, and Australia markets.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the chapter.
-
- **Example**: :code:`"0D5wENdkdwbqlrHoaJ9g29"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- chapter : `dict`
- Spotify catalog information for a single chapter.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "audio_preview_url": <str>,
- "available_markets": [<str>],
- "chapter_number": <int>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_playable": <bool>
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- },
- "audiobook": {
- "authors": [
- {
- "name": <str>
- }
- ],
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "edition": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "narrators": [
- {
- "name": <str>
- }
- ],
- "publisher": <str>,
- "type": "audiobook",
- "uri": <str>,
- "total_chapters": <int>
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/chapters/{id}",
- params={"market": market})
-
-
-
-[docs]
- def get_chapters(
- self, ids: Union[int, str, list[Union[int, str]]], *,
- market: str = None) -> list[dict[str, Any]]:
-
- """
- `Chapters > Get Several Chapters <https://developer.spotify.com/
- documentation/web-api/reference/
- get-several-chapters>`_: Get Spotify catalog information for
- several chapters identified by their Spotify IDs.
-
- .. note::
- Chapters are only available for the US, UK, Ireland, New
- Zealand, and Australia markets.
-
- Parameters
- ----------
- ids : `int`, `str`, or `list`
- A (comma-separated) list of the Spotify IDs for the
- chapters.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"0IsXVP0JmcB2adSE338GkK,
- 3ZXb8FKZGU0EHALYX6uCzU, 0D5wENdkdwbqlrHoaJ9g29"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- chapters : `list`
- A list containing Spotify catalog information for multiple
- chapters.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "audio_preview_url": <str>,
- "available_markets": [<str>],
- "chapter_number": <int>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_playable": <bool>
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- },
- "audiobook": {
- "authors": [
- {
- "name": <str>
- }
- ],
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "edition": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "narrators": [
- {
- "name": <str>
- }
- ],
- "publisher": <str>,
- "type": "audiobook",
- "uri": <str>,
- "total_chapters": <int>
- }
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/chapters",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "market": market}
- )["chapters"]
-
-
- ### EPISODES ##############################################################
-
-
-[docs]
- def get_episode(self, id: str, *, market: str = None) -> dict[str, Any]:
-
- """
- `Episodes > Get Episode <https://developer.spotify.com/
- documentation/web-api/reference/
- get-an-episode>`_: Get Spotify catalog information for a single
- episode identified by its unique Spotify ID.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the episode.
-
- **Example**: :code:`"512ojhOuo1ktJprKbVcKyQ"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- episode : `dict`
- Spotify catalog information for a single episode.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "audio_preview_url": <str>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "is_playable": <bool>
- "language": <str>,
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- },
- "show": {
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/episodes/{id}",
- params={"market": market})
-
-
-
-[docs]
- def get_episodes(
- self, ids: Union[int, str, list[Union[int, str]]], *,
- market: str = None) -> list[dict[str, Any]]:
-
- """
- `Episodes > Get Several Episodes
- <https://developer.spotify.com/documentation/web-api/reference/
- get-multiple-episodes>`_: Get Spotify catalog
- information for several episodes based on their Spotify IDs.
-
- Parameters
- ----------
- ids : `int`, `str`, or `list`
- A (comma-separated) list of the Spotify IDs for the episodes.
-
- **Maximum**: 50 IDs.
-
- **Example**:
- :code:`"77o6BIVlYM3msb4MMIL1jH,0Q86acNRm6V9GYx55SXKwf"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- episodes : `list`
- A list containing Spotify catalog information for multiple
- episodes.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- [
- {
- "audio_preview_url": <str>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "is_playable": <bool>
- "language": <str>,
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- },
- "show": {
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>
- }
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/episodes",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "market": market}
- )["episodes"]
-
-
-
-[docs]
- def get_saved_episodes(
- self, *, limit: int = None, market: str = None, offset: int = None
- ) -> dict[str, Any]:
-
- """
- `Episodes > Get User's Saved Episodes
- <https://developer.spotify.com/documentation/web-api/reference/
- get-users-saved-episodes>`_: Get a list of the
- episodes saved in the current Spotify user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- episodes : `dict`
- A dictionary containing Spotify catalog information for a
- user's saved episodes and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "episode": {
- "audio_preview_url": <str>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "is_playable": <bool>
- "language": <str>,
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- },
- "show": {
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>
- }
- }
- }
- ]
- }
- """
-
- self._check_scope("get_saved_episodes", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/episodes",
- params={"limit": limit, "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def save_episodes(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Episodes > Save Episodes for Current User
- <https://developer.spotify.com/documentation/web-api/reference/
- save-episodes-user>`_: Save one or more episodes to
- the current user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the shows.
-
- **Maximum**: 50 IDs.
-
- **Example**:
- :code:`"77o6BIVlYM3msb4MMIL1jH,0Q86acNRm6V9GYx55SXKwf"`.
- """
-
- self._check_scope("save_episodes", "user-library-modify")
-
- if isinstance(ids, str):
- self._request("put", f"{self.API_URL}/me/episodes",
- params={"ids": ids})
- elif isinstance(ids, list):
- self._request("put", f"{self.API_URL}/me/episodes",
- json={"ids": ids})
-
-
-
-[docs]
- def remove_saved_episodes(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Episodes > Remove User's Saved Episodes
- <https://developer.spotify.com/documentation/web-api/reference/
- remove-episodes-user>`_: Remove one or more
- episodes from the current user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the episodes.
-
- **Maximum**: 50 IDs.
-
- **Example**:
- :code:`"77o6BIVlYM3msb4MMIL1jH,0Q86acNRm6V9GYx55SXKwf"`.
- """
-
- self._check_scope("remove_saved_episodes", "user-library-modify")
-
- if isinstance(ids, str):
- self._request("delete", f"{self.API_URL}/me/episodes",
- params={"ids": ids})
- elif isinstance(ids, list):
- self._request("delete", f"{self.API_URL}/me/episodes",
- json={"ids": ids})
-
-
-
-[docs]
- def check_saved_episodes(self, ids: Union[str, list[str]]) -> list[bool]:
-
- """
- `Episodes > Check User's Saved Episodes
- <https://developer.spotify.com/documentation/web-api/reference/
- check-users-saved-episodes>`_: Check if one or more
- episodes is already saved in the current Spotify user's 'Your
- Episodes' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the
- episodes.
-
- **Maximum**: 50 IDs.
-
- **Example**:
- :code:`"77o6BIVlYM3msb4MMIL1jH,0Q86acNRm6V9GYx55SXKwf"`.
-
- Returns
- -------
- contains : `list`
- Array of booleans specifying whether the episodes are found
- in the user's 'Liked Songs'.
-
- **Example**: :code:`[False, True]`.
- """
-
- self._check_scope("check_saved_episodes", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/episodes/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )
-
-
- ### GENRES ################################################################
-
-
-[docs]
- def get_genre_seeds(self) -> list[str]:
-
- """
- `Genres > Get Available Genre Seeds
- <https://developer.spotify.com/documentation/web-api/reference/
- get-recommendation-genres>`_: Retrieve a list of
- available genres seed parameter values for use in
- :meth:`get_recommendations`.
-
- Returns
- -------
- genres : `list`
- Array of genres.
-
- **Example**: :code:`["acoustic", "afrobeat", ...]`.
- """
-
- return self._get_json(
- f"{self.API_URL}/recommendations/available-genre-seeds"
- )["genres"]
-
-
- ### MARKETS ###############################################################
-
-
-[docs]
- def get_markets(self) -> list[str]:
-
- """
- `Markets > Get Available Markets <https://developer.spotify.com/
- documentation/web-api/reference/
- get-available-markets>`_: Get the list of markets where Spotify
- is available.
-
- Returns
- -------
- markets : `list`
- Array of country codes.
-
- **Example**: :code:`["CA", "BR", "IT"]`.
- """
-
- return self._get_json(f"{self.API_URL}/markets")["markets"]
-
-
- ### PLAYER ################################################################
-
-
-[docs]
- def get_playback_state(
- self, *, market: str = None, additional_types: str = None
- ) -> dict[str, Any]:
-
- """
- `Player > Get Playback State <https://developer.spotify.com/
- documentation/web-api/reference/
- get-information-about-the-users-current-playback>`_: Get
- information about the user's current playback state, including
- track or episode, progress, and active device.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-read-playback-state` scope.
-
- Parameters
- ----------
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- additional_types : `str`, keyword-only, optional
- A comma-separated list of item types that your client
- supports besides the default track type.
-
- .. note::
-
- This parameter was introduced to allow existing clients
- to maintain their current behavior and might be
- deprecated in the future.
-
- **Valid**: :code:`"track"` and :code:`"episode"`.
-
- Returns
- -------
- state : `dict`
- Information about playback state.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "device": {
- "id": <str>,
- "is_active": <bool>,
- "is_private_session": <bool>,
- "is_restricted": <bool>,
- "name": <str>,
- "type": <str>,
- "volume_percent": <int>
- },
- "repeat_state": <str>,
- "shuffle_state": <bool>,
- "context": {
- "type": <str>,
- "href": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "uri": <str>
- },
- "timestamp": <int>,
- "progress_ms": <int>,
- "is_playing": <bool>,
- "item": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- },
- "currently_playing_type": <str>,
- "actions": {
- "interrupting_playback": <bool>,
- "pausing": <bool>,
- "resuming": <bool>,
- "seeking": <bool>,
- "skipping_next": <bool>,
- "skipping_prev": <bool>,
- "toggling_repeat_context": <bool>,
- "toggling_shuffle": <bool>,
- "toggling_repeat_track": <bool>,
- "transferring_playback": <bool>
- }
- }
- """
-
- self._check_scope("get_playback_state", "user-read-playback-state")
-
- return self._get_json(f"{self.API_URL}/me/player",
- params={"market": market,
- "additional_types": additional_types})
-
-
-
-[docs]
- def transfer_playback(
- self, device_ids: Union[str, list[str]], *, play: bool = None
- ) -> None:
-
- """
- `Player > Transfer Playback <https://developer.spotify.com/
- documentation/web-api/reference/transfer-a-users-playback>`_:
- Transfer playback to a new device and determine if it should
- start playing.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- device_ids : `str` or `list`
- The ID of the device on which playback should be started or
- transferred.
-
- .. note::
- Although an array is accepted, only a single device ID is
- currently supported. Supplying more than one will return
- :code:`400 Bad Request`.
-
- **Example**: :code:`["74ASZWbe4lXaubB36ztrGX"]`.
-
- play : `bool`
- If :code:`True`, playback happens on the new device; if
- :code:`False` or not provided, the current playback state is
- kept.
- """
-
- self._check_scope("transfer_playback", "user-modify-playback-state")
-
- json = {"device_ids": [device_ids] if isinstance(device_ids, str)
- else device_ids}
- if play is not None:
- json["play"] = play
- self._request("put", f"{self.API_URL}/me/player", json=json)
-
-
-
-[docs]
- def get_devices(self) -> list[dict[str, Any]]:
-
- """
- `Player > Get Available Devices <https://developer.spotify.com/
- documentation/web-api/reference/
- get-a-users-available-devices>`_: Get information about a user's
- available devices.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-read-playback-state` scope.
-
- Returns
- -------
- devices : `list`
- A list containing information about the available devices.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "devices": [
- {
- "id": <str>,
- "is_active": <bool>,
- "is_private_session": <bool>,
- "is_restricted": <bool>,
- "name": <str>,
- "type": <str>,
- "volume_percent": <int>
- }
- ]
- }
- ]
- """
-
- self._check_scope("get_available_devices", "user-read-playback-state")
-
- self._get_json(f"{self.API_URL}/me/player/devices")
-
-
-
-[docs]
- def get_currently_playing(
- self, *, market: str = None, additional_types: str = None
- ) -> dict[str, Any]:
-
- """
- `Player > Get Currently Playing Track
- <https://developer.spotify.com/documentation/web-api/reference/
- get-the-users-currently-playing-track>`_: Get the object
- currently being played on the user's Spotify account.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-read-currently-playing` scope.
-
- Parameters
- ----------
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- additional_types : `str`, keyword-only, optional
- A comma-separated list of item types that your client
- supports besides the default track type.
-
- .. note::
-
- This parameter was introduced to allow existing clients
- to maintain their current behavior and might be
- deprecated in the future.
-
- **Valid**: :code:`"track"` and :code:`"episode"`.
-
- Returns
- -------
- item : `dict`
- Information about the object currently being played.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "device": {
- "id": <str>,
- "is_active": <bool>,
- "is_private_session": <bool>,
- "is_restricted": <bool>,
- "name": <str>,
- "type": <str>,
- "volume_percent": <int>
- },
- "repeat_state": <str>,
- "shuffle_state": <bool>,
- "context": {
- "type": <str>,
- "href": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "uri": <str>
- },
- "timestamp": <int>,
- "progress_ms": <int>,
- "is_playing": <bool>,
- "item": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- },
- "currently_playing_type": <str>,
- "actions": {
- "interrupting_playback": <bool>,
- "pausing": <bool>,
- "resuming": <bool>,
- "seeking": <bool>,
- "skipping_next": <bool>,
- "skipping_prev": <bool>,
- "toggling_repeat_context": <bool>,
- "toggling_shuffle": <bool>,
- "toggling_repeat_track": <bool>,
- "transferring_playback": <bool>
- }
- }
- """
-
- self._check_scope("get_currently_playing_item",
- "user-read-currently-playing")
-
- self._get_json(f"{self.API_URL}/me/player/currently-playing",
- params={"market": market,
- "additional_types": additional_types})
-
-
-
-[docs]
- def start_playback(
- self, *, device_id: str = None, context_uri: str = None,
- uris: list[str] = None, offset: dict[str, Any],
- position_ms: int = None) -> None:
-
- """
- `Player > Start/Resume Playback <https://developer.spotify.com/
- documentation/web-api/reference/start-a-users-playback>`_: Start
- a new context or resume current playback on the user's active
- device.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
-
- context_uri : `str`, keyword-only, optional
- Spotify URI of the context to play. Only album, artist, and
- playlist contexts are valid.
-
- **Example**: :code:`"spotify:album:1Je1IMUlBXcx1Fz0WE7oPT"`.
-
- uris : `list`, keyword-only, optional
- A JSON array of the Spotify track URIs to play.
-
- **Example**: :code:`["spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
- "spotify:track:1301WleyT98MSxVHPZCA6M"]`.
-
- offset : `dict`, keyword-only, optional
- Indicates from where in the context playback should start.
- Only available when `context_uri` corresponds to an album or
- a playlist.
-
- .. container::
-
- **Valid values**:
-
- * The value corresponding to the :code:`"position"` key
- is zero-based and can't be negative.
- * The value corresponding to the :code:`"uri"` key is a
- string representing the URI of the item to start at.
-
- **Examples**:
-
- * :code:`{"position": 5}` to start playback at the sixth
- item of the collection specified in `context_uri`.
- * :code:`{"uri": <str>}`
- to start playback at the item designated by the URI.
-
- position_ms : `int`, keyword-only, optional
- The position in milliseconds to seek to. Passing in a
- position that is greater than the length of the track will
- cause the player to start playing the next song.
-
- **Valid values**: `position_ms` must be a positive number.
- """
-
- self._check_scope("start_playback", "user-modify-playback-state")
-
- json = {}
- if context_uri is not None:
- json["context_uri"] = context_uri
- if uris is not None:
- json["uris"] = uris
- if offset is not None:
- json["offset"] = offset
- if position_ms is not None:
- json["position_ms"] = position_ms
-
- self._request("put", f"{self.API_URL}/me/player/play",
- params={"device_id": device_id}, json=json)
-
-
-
-[docs]
- def pause_playback(self, *, device_id: str = None) -> None:
-
- """
- `Player > Pause Playback <https://developer.spotify.com/
- documentation/web-api/reference/pause-a-users-playback>`_: Pause
- playback on the user's account.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("pause_playback", "user-modify-playback-state")
-
- self._request("put", f"{self.API_URL}/me/player/pause",
- params={"device_id": device_id})
-
-
-
-[docs]
- def skip_to_next(self, *, device_id: str = None) -> None:
-
- """
- `Player > Skip To Next <https://developer.spotify.com/
- documentation/web-api/reference/
- skip-users-playback-to-next-track>`_: Skips to next track in the
- user's queue.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("skip_to_next", "user-modify-playback-state")
-
- self._request("post", f"{self.API_URL}/me/player/next",
- params={"device_id": device_id})
-
-
-
-[docs]
- def skip_to_previous(self, *, device_id: str = None) -> None:
-
- """
- `Player > Skip To Previous <https://developer.spotify.com/
- documentation/web-api/reference/
- skip-users-playback-to-previous-track>`_: Skips to previous
- track in the user's queue.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("skip_to_previous", "user-modify-playback-state")
-
- self._request("post", f"{self.API_URL}/me/player/previous",
- params={"device_id": device_id})
-
-
-
-[docs]
- def seek_to_position(
- self, position_ms: int, *, device_id: str = None) -> None:
-
- """
- `Player > Seek To Position <https://developer.spotify.com/
- documentation/web-api/reference/
- seek-to-position-in-currently-playing-track>`_: Seeks to the
- given position in the user's currently playing track.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- position_ms : `int`
- The position in milliseconds to seek to. Passing in a
- position that is greater than the length of the track will
- cause the player to start playing the next song.
-
- **Valid values**: `position_ms` must be a positive number.
-
- **Example**: :code:`25000`.
-
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("seek_to_position", "user-modify-playback-state")
-
- self._request("put", f"{self.API_URL}/me/player/seek",
- params={"position_ms": position_ms,
- "device_id": device_id})
-
-
-
-[docs]
- def set_repeat_mode(self, state: str, *, device_id: str = None) -> None:
-
- """
- `Player > Set Repeat Mode <https://developer.spotify.com/
- documentation/web-api/reference/
- set-repeat-mode-on-users-playback>`_: Set the repeat mode for
- the user's playback. Options are repeat-track, repeat-context,
- and off.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- state : `str`
- Repeat mode.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"track"` will repeat the current track.
- * :code:`"context"` will repeat the current context.
- * :code:`"off"` will turn repeat off.
-
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("set_repeat_mode", "user-modify-playback-state")
-
- self._request("put", f"{self.API_URL}/me/player/repeat",
- params={"state": state, "device_id": device_id})
-
-
-
-[docs]
- def set_playback_volume(
- self, volume_percent: int, *, device_id: str = None) -> None:
-
- """
- `Player > Set Playback Volume <https://developer.spotify.com/
- documentation/web-api/reference/
- set-volume-for-users-playback>`_: Set the volume for the user's
- current playback device.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- volume_percent : `int`
- The volume to set.
-
- **Valid values**: `volume_percent` must be a value from 0 to
- 100, inclusive.
-
- **Example**: :code:`50`.
-
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("set_playback_volume", "user-modify-playback-state")
-
- self._request("put", f"{self.API_URL}/me/player/volume",
- params={"volume_percent": volume_percent,
- "device_id": device_id})
-
-
-
-[docs]
- def toggle_playback_shuffle(
- self, state: bool, *, device_id: str = None) -> None:
-
- """
- `Player > Toggle Playback Shuffle
- <https://developer.spotify.com/documentation/web-api/reference/
- toggle-shuffle-for-users-playback>`_: Toggle shuffle on or off
- for user's playback.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- state : `bool`
- Shuffle mode. If :code:`True`, shuffle the user's playback.
-
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("toggle_playback_shuffle",
- "user-modify-playback-state")
-
- self._request("put", f"{self.API_URL}/me/player/shuffle",
- params={"state": state, "device_id": device_id})
-
-
-
-[docs]
- def get_recently_played(
- self, *, limit: int = None, after: int = None, before: int = None
- ) -> dict[str, Any]:
-
- """
- `Player > Get Recently Played Tracks
- <https://developer.spotify.com/documentation/web-api/reference/
- get-recently-played>`_: Get tracks from the current user's
- recently played tracks.
-
- .. note::
-
- Currently doesn't support podcast episodes.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-read-recently-played` scope.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- after : `int`, keyword-only, optional
- A Unix timestamp in milliseconds. Returns all items after
- (but not including) this cursor position. If `after` is
- specified, `before` must not be specified.
-
- **Example**: :code:`1484811043508`.
-
- before : `int`, keyword-only, optional
- A Unix timestamp in milliseconds. Returns all items before
- (but not including) this cursor position. If `before` is
- specified, `after` must not be specified.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing Spotify catalog information for
- the recently played tracks and the number of results
- returned.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "cursors": {
- "after": <str>,
- "before": <str>
- },
- "total": <int>,
- "items": [
- {
- "track": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- },
- "played_at": <str>,
- "context": {
- "type": <str>,
- "href": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "uri": <str>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_recently_played_tracks",
- "user-read-recently-played")
-
- return self._get_json(f"{self.API_URL}/me/player/recently-played",
- params={"limit": limit, "after": after,
- "before": before})
-
-
-
-[docs]
- def get_queue(self) -> dict[str, Any]:
-
- """
- `Player > Get the User's Queue <https://developer.spotify.com/
- documentation/web-api/reference/get-queue>`_: Get the list of
- objects that make up the user's queue.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-read-playback-state` scope.
-
- Returns
- -------
- queue : `dict`
- Information about the user's queue, such as the currently
- playing item and items in the queue.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "currently_playing": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- },
- "queue": [
- {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- }
- """
-
- self._check_scope("get_user_queue", "user-read-playback-state")
-
- return self._get_json(f"{self.API_URL}/me/player/queue")
-
-
-
-[docs]
- def add_to_queue(self, uri: str, *, device_id: str = None) -> None:
-
- """
- `Player > Add Item to Playback Queue
- <https://developer.spotify.com/documentation/web-api/reference/
- add-to-queue>`_: Add an item to the end of the user's current
- playback queue.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-modify-playback-state` scope.
-
- Parameters
- ----------
- uri : `str`
- The URI of the item to add to the queue. Must be a track or
- an episode URL.
-
- device_id : `str`, keyword-only, optional
- The ID of the device this method is targeting. If not
- supplied, the user's currently active device is the target.
-
- **Example**:
- :code:`"0d1841b0976bae2a3a310dd74c0f3df354899bc8"`.
- """
-
- self._check_scope("add_queue_item", "user-modify-playback-state")
-
- self._request("post", f"{self.API_URL}/me/player/queue",
- params={"uri": uri, "device_id": device_id})
-
-
- ### PLAYLISTS #############################################################
-
-
-[docs]
- def get_playlist(
- self, playlist_id: str, *,
- additional_types: Union[str, list[str]] = None,
- fields: str = None, market: str = None) -> dict[str, Any]:
-
- """
- `Playlists > Get Playlist <https://developer.spotify.com/
- documentation/web-api/reference/get-playlist>`_:
- Get a playlist owned by a Spotify user.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- additional_types : `str` or `list`, keyword-only, optional
- A (comma-separated) list of item types besides the default
- track type.
-
- .. note::
-
- This parameter was introduced to allow existing clients
- to maintain their current behavior and might be
- deprecated in the future.
-
- **Valid values**: :code:`"track"` and :code:`"episode"`.
-
- fields : `str` or `list`, keyword-only, optional
- Filters for the query: a (comma-separated) list of the
- fields to return. If omitted, all fields are returned.
- A dot separator can be used to specify non-reoccurring
- fields, while parentheses can be used to specify reoccurring
- fields within objects. Use multiple parentheses to drill
- down into nested objects. Fields can be excluded by
- prefixing them with an exclamation mark.
-
- .. container::
-
- **Examples**:
-
- * :code:`"description,uri"` to get just the playlist's
- description and URI,
- * :code:`"tracks.items(added_at,added_by.id)"` to get just
- the added date and user ID of the adder,
- * :code:`"tracks.items(track(name,href,album(name,href)))"`
- to drill down into the album, and
- * :code:`"tracks.items(track(name,href,album(!name,href)))"`
- to exclude the album name.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- playlist : `dict`
- Spotify catalog information for a single playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "added_by": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>
- },
- "is_local": <bool>,
- "track": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- }
- ]
- },
- "type": <str>,
- "uri": <str>
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/playlists/{playlist_id}",
- params={
- "additional_types": additional_types
- if additional_types is None
- or isinstance(additional_types, str)
- else ",".join(additional_types),
- "fields": fields if fields is None or isinstance(fields, str)
- else ",".join(fields),
- "market": market
- }
- )
-
-
-
-[docs]
- def change_playlist_details(
- self, playlist_id: str, *, name: str = None, public: bool = None,
- collaborative: bool = None, description: str = None) -> None:
-
- """
- `Playlists > Change Playlist Details
- <https://developer.spotify.com/documentation/web-api/reference/
- change-playlist-details>`_: Change a playlist's
- name and public/private state. (The user must, of course, own
- the playlist.)
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-public` or the
- :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- name : `str`, keyword-only, optional
- The new name for the playlist.
-
- **Example**: :code:`"My New Playlist Title"`.
-
- public : `bool`, keyword-only, optional
- If :code:`True`, the playlist will be public. If
- :code:`False`, it will be private.
-
- collaborative : `bool`, keyword-only, optional
- If :code:`True`, the playlist will become collaborative and
- other users will be able to modify the playlist in their
- Spotify client.
-
- .. note::
-
- You can only set :code:`collaborative=True` on non-public
- playlists.
-
- description : `str`, keyword-only, optional
- Value for playlist description as displayed in Spotify
- clients and in the Web API.
- """
-
- self._check_scope("change_playlist_details",
- "playlist-modify-" +
- ("public" if self.get_playlist(playlist_id)["public"]
- else "private"))
-
- json = {}
- if name is not None:
- json["name"] = name
- if public is not None:
- json["public"] = public
- if collaborative is not None:
- json["collaborative"] = collaborative
- if description is not None:
- json["description"] = description
- self._request("put", f"{self.API_URL}/playlists/{playlist_id}",
- json=json)
-
-
-
-[docs]
- def get_playlist_items(
- self, playlist_id: str, *,
- additional_types: Union[str, list[str]] = None,
- fields: str = None, limit: int = None, market: str = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- `Playlists > Get Playlist Items <https://developer.spotify.com/
- documentation/web-api/reference/
- get-playlists-tracks>`_: Get full details of the items of a
- playlist owned by a Spotify user.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- additional_types : `str` or `list`, keyword-only, optional
- A (comma-separated) list of item types besides the default
- track type.
-
- .. note::
-
- This parameter was introduced to allow existing clients
- to maintain their current behavior and might be
- deprecated in the future.
-
- **Valid values**: :code:`"track"` and :code:`"episode"`.
-
- fields : `str` or `list`, keyword-only, optional
- Filters for the query: a (comma-separated) list of the
- fields to return. If omitted, all fields are returned.
- A dot separator can be used to specify non-reoccurring
- fields, while parentheses can be used to specify reoccurring
- fields within objects. Use multiple parentheses to drill
- down into nested objects. Fields can be excluded by
- prefixing them with an exclamation mark.
-
- .. container::
-
- **Examples**:
-
- * :code:`"description,uri"` to get just the playlist's
- description and URI,
- * :code:`"tracks.items(added_at,added_by.id)"` to get just
- the added date and user ID of the adder,
- * :code:`"tracks.items(track(name,href,album(name,href)))"`
- to drill down into the album, and
- * :code:`"tracks.items(track(name,href,album(!name,href)))"`
- to exclude the album name.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing Spotify catalog information for the
- playlist items and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "added_by": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>
- },
- "is_local": <bool>,
- "track": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_playlist_item", "playlist-modify-private")
-
- return self._get_json(
- f"{self.API_URL}/playlists/{playlist_id}/tracks",
- params={
- "additional_types": additional_types
- if additional_types is None
- or isinstance(additional_types, str)
- else ",".join(additional_types),
- "fields": fields if fields is None or isinstance(fields, str)
- else ",".join(fields),
- "limit": limit,
- "market": market,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def add_playlist_items(
- self, playlist_id: str, uris: Union[str, list[str]], *,
- position: int = None) -> str:
-
- """
- `Playlists > Add Items to Playlist
- <https://developer.spotify.com/documentation/web-api/reference/
- add-tracks-to-playlist>`_: Add one or more items to
- a user's playlist.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-public` or the
- :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- uris : `str` or `list`, keyword-only, optional
- A (comma-separated) list of Spotify URIs to add; can be
- track or episode URIs. A maximum of 100 items can be added
- in one request.
-
- .. note::
-
- It is likely that passing a large number of item URIs as
- a query parameter will exceed the maximum length of the
- request URI. When adding a large number of items, it is
- recommended to pass them in the request body (as a
- `list`).
-
- **Example**: :code:`"spotify:track:4iV5W9uYEdYUVa79Axb7Rh,
- spotify:track:1301WleyT98MSxVHPZCA6M,
- spotify:episode:512ojhOuo1ktJprKbVcKyQ"`.
-
- position : `int`, keyword-only, optional
- The position to insert the items, a zero-based index. If
- omitted, the items will be appended to the playlist. Items
- are added in the order they are listed in the query string
- or request body.
-
- .. container::
-
- **Examples**:
-
- * :code:`0` to insert the items in the first position.
- * :code:`2` to insert the items in the third position.
-
- Returns
- -------
- snapshot_id : `str`
- The updated playlist's snapshot ID.
- """
-
- self._check_scope("add_playlist_details",
- "playlist-modify-" +
- ("public" if self.get_playlist(playlist_id)["public"]
- else "private"))
-
- if isinstance(uris, str):
- url = f"{self.API_URL}/playlists/{playlist_id}/tracks?{uris=}"
- if position is not None:
- url += f"{position=}"
- return self._request("post", url).json()["snapshot_id"]
-
- elif isinstance(uris, list):
- json = {"uris": uris}
- if position is not None:
- json["position"] = position
- self._request("post",
- f"{self.API_URL}/playlists/{playlist_id}/tracks",
- json=json).json()["snapshot_id"]
-
-
-
-[docs]
- def update_playlist_items(
- self, playlist_id: str, *, uris: Union[str, list[str]] = None,
- range_start: int = None, insert_before: int = None,
- range_length: int = 1, snapshot_id: str = None) -> str:
-
- """
- `Playlists > Update Playlist Items
- <https://developer.spotify.com/documentation/web-api/reference/
- reorder-or-replace-playlists-tracks>`_: Either reorder or
- replace items in a playlist depending on the request's
- parameters.
-
- To reorder items, include `range_start`, `insert_before`,
- `range_length`, and `snapshot_id` as keyword arguments. To
- replace items, include `uris` as a keyword argument. Replacing
- items in a playlist will overwrite its existing items. This
- operation can be used for replacing or clearing items in a
- playlist.
-
- .. note::
-
- Replace and reorder are mutually exclusive operations which
- share the same endpoint, but have different parameters. These
- operations can't be applied together in a single request.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-public` or the
- :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- uris : `str` or `list`, keyword-only, optional
- A (comma-separated) list of Spotify URIs to add; can be
- track or episode URIs. A maximum of 100 items can be added
- in one request.
-
- .. note::
-
- It is likely that passing a large number of item URIs as
- a query parameter will exceed the maximum length of the
- request URI. When adding a large number of items, it is
- recommended to pass them in the request body (as a
- `list`).
-
- **Example**: :code:`"spotify:track:4iV5W9uYEdYUVa79Axb7Rh,
- spotify:track:1301WleyT98MSxVHPZCA6M,
- spotify:episode:512ojhOuo1ktJprKbVcKyQ"`.
-
- range_start : `int`, keyword-only, optional
- The position of the first item to be reordered.
-
- insert_before : `int`, keyword-only, optional
- The position where the items should be inserted. To reorder
- the items to the end of the playlist, simply set
- `insert_before` to the position after the last item.
-
- .. container::
-
- **Examples**:
-
- * :code:`range_start=0, insert_before=10` to reorder the
- first item to the last position in a playlist with 10
- items, and
- * :code:`range_start=9, insert_before=0` to reorder the
- last item in a playlist with 10 items to the start of
- the playlist.
-
- range_length : `int`, keyword-only, default: :code:`1`
- The amount of items to be reordered. The range of items to
- be reordered begins from the `range_start` position, and
- includes the `range_length` subsequent items.
-
- **Example**: :code:`range_start=9, range_length=2` to move
- the items at indices 9–10 to the start of the playlist.
-
- snapshot_id : `str`, keyword-only, optional
- The playlist's snapshot ID against which you want to make
- the changes.
-
- Returns
- -------
- snapshot_id : `str`
- The updated playlist's snapshot ID.
- """
-
- self._check_scope("update_playlist_details",
- "playlist-modify-" +
- ("public" if self.get_playlist(playlist_id)["public"]
- else "private"))
-
- json = {}
- if snapshot_id is not None:
- json["snapshot_id"] = snapshot_id
-
- if uris is None:
- if range_start is not None:
- json["range_start"] = range_start
- if insert_before is not None:
- json["insert_before"] = insert_before
- if range_length is not None:
- json["range_length"] = range_length
- return self._request(
- "put",
- f"{self.API_URL}/playlists/{playlist_id}/tracks",
- json=json
- ).json()["snapshot_id"]
-
- elif isinstance(uris, str):
- return self._request(
- "put",
- f"{self.API_URL}/playlists/{playlist_id}/tracks?uris={uris}",
- json=json
- ).json()["snapshot_id"]
-
- elif isinstance(uris, list):
- return self._request(
- "put",
- f"{self.API_URL}/playlists/{playlist_id}/tracks",
- json={"uris": uris} | json
- ).json()["snapshot_id"]
-
-
-
-[docs]
- def remove_playlist_items(
- self, playlist_id: str, tracks: list[str], *,
- snapshot_id: str = None) -> str:
-
- """
- `Playlists > Remove Playlist Items
- <https://developer.spotify.com/documentation/web-api/reference/
- remove-tracks-playlist>`_: Remove one or more items from a
- user's playlist.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-public` or the
- :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- tracks : `list`
- A (comma-separated) list containing Spotify URIs of the
- tracks or episodes to remove.
-
- **Maximum**: 100 items can be added in one request.
-
- **Example**: :code:`"spotify:track:4iV5W9uYEdYUVa79Axb7Rh,
- spotify:track:1301WleyT98MSxVHPZCA6M,
- spotify:episode:512ojhOuo1ktJprKbVcKyQ"`.
-
- snapshot_id : `str`, keyword-only, optional
- The playlist's snapshot ID against which you want to make
- the changes. The API will validate that the specified items
- exist and in the specified positions and make the changes,
- even if more recent changes have been made to the playlist.
-
- Returns
- -------
- snapshot_id : `str`
- The updated playlist's snapshot ID.
- """
-
- self._check_scope("remove_playlist_items",
- "playlist-modify-" +
- ("public" if self.get_playlist(playlist_id)["public"]
- else "private"))
-
- json = {"tracks": tracks}
- if snapshot_id is not None:
- json["snapshot_id"] = snapshot_id
- return self._request("delete",
- f"{self.API_URL}/playlists/{playlist_id}/tracks",
- json=json).json()["snapshot_id"]
-
-
-
-[docs]
- def get_personal_playlists(
- self, *, limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Playlist > Get Current User's Playlists
- <https://developer.spotify.com/documentation/web-api/reference/
- get-a-list-of-current-users-playlists>`_: Get a list of the
- playlists owned or followed by the current Spotify user.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-read-private` and the
- :code:`playlist-read-collaborative` scopes.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- A dictionary containing the current user's playlists and the
- number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "total": <int>
- },
- "type": <str>,
- "uri": <str>
- }
- ]
- }
- """
-
- self._check_scope("get_current_user_playlists",
- "playlist-read-private")
- self._check_scope("get_current_user_playlists",
- "playlist-read-collaborative")
-
- return self._get_json(f"{self.API_URL}/me/playlists",
- params={"limit": limit, "offset": offset})
-
-
-
-[docs]
- def get_user_playlists(
- self, user_id: str, *, limit: int = None, offset: int = None
- ) -> dict[str, Any]:
-
- """
- `Playlist > Get User's Playlists
- <https://developer.spotify.com/documentation/web-api/reference/
- get-list-users-playlists>`_: Get a list of the playlists owned
- or followed by a Spotify user.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-read-private` and the
- :code:`playlist-read-collaborative` scopes.
-
- Parameters
- ----------
- user_id : `str`
- The user's Spotify user ID.
-
- **Example**: :code:`"smedjan"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- A dictionary containing the user's playlists and the number
- of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "total": <int>
- },
- "type": <str>,
- "uri": <str>
- }
- ]
- }
- """
-
- self._check_scope("get_user_playlists", "playlist-read-private")
- self._check_scope("get_user_playlists", "playlist-read-collaborative")
-
- return self._get_json(f"{self.API_URL}/users/{user_id}/playlists",
- params={"limit": limit, "offset": offset})
-
-
-
-[docs]
- def create_playlist(
- self, name: str, *, public: bool = True, collaborative: bool = None,
- description: str = None) -> dict[str, Any]:
-
- """
- `Playlists > Create Playlist <https://developer.spotify.com/
- documentation/web-api/reference/create-playlist>`_: Create a
- playlist for a Spotify user. (The playlist will be empty until
- you add tracks.)
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-public` or the
- :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- name : `str`
- The name for the new playlist. This name does not need to be
- unique; a user may have several playlists with the same
- name.
-
- **Example**: :code:`"Your Coolest Playlist"`.
-
- public : `bool`, keyword-only, default: `True`
- If :code:`True`, the playlist will be public; if
- :code:`False`, it will be private.
-
- .. note::
-
- To be able to create private playlists, the user must
- have granted the :code:`playlist-modify-private` scope.
-
- collaborative : `bool`, keyword-only, optional
- If :code:`True`, the playlist will be collaborative.
-
- .. note::
-
- To create a collaborative playlist, you must also set
- `public` to :code:`False`. To create collaborative
- playlists, you must have granted the
- :code:`playlist-modify-private` and
- :code:`playlist-modify-public` scopes.
-
- **Default**: :code:`False`.
-
- description : `str`, keyword-only, optional
- The playlist description, as displayed in Spotify Clients
- and in the Web API.
-
- Returns
- -------
- playlist : `dict`
- Spotify catalog information for the newly created playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "added_by": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>
- },
- "is_local": <bool>,
- "track": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- }
- ]
- },
- "type": <str>,
- "uri": <str>
- }
- """
-
- self._check_scope(
- "create_playlist",
- "playlist-modify-" + ("public" if public else "private")
- )
-
- json = {"name": name, "public": public}
- if collaborative is not None:
- json["collaborative"] = collaborative
- if description is not None:
- json["description"] = description
-
- return self._request("post",
- f"{self.API_URL}/users/{self._user_id}/playlists",
- json=json).json()
-
-
-
-[docs]
- def get_featured_playlists(
- self, *, country: str = None, locale: str = None,
- timestamp: str = None, limit: int = None, offset: int = None
- ) -> dict[str, Any]:
-
- """
- `Playlists > Get Featured Playlists
- <https://developer.spotify.com/documentation/web-api/reference/
- get-featured-playlists>`_: Get a list of Spotify featured
- playlists (shown, for example, on a Spotify player's 'Browse'
- tab).
-
- Parameters
- ----------
- country : `str`, keyword-only, optional
- A country: an ISO 3166-1 alpha-2 country code. Provide this
- parameter if you want the list of returned items to be
- relevant to a particular country. If omitted, the returned
- items will be relevant to all countries.
-
- **Example**: :code:`"SE"`.
-
- locale : `str`, keyword-only, optional
- The desired language, consisting of an ISO 639-1 language
- code and an ISO 3166-1 alpha-2 country code, joined by an
- underscore. Provide this parameter if you want the category
- strings returned in a particular language.
-
- .. note::
-
- If `locale` is not supplied, or if the specified language
- is not available, the category strings returned will be
- in the Spotify default language (American English).
-
- **Example**: :code:`"es_MX"` for "Spanish (Mexico)".
-
- timestamp : `str`, keyword-only, optional
- A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use
- this parameter to specify the user's local time to get
- results tailored for that specific date and time in the day.
- If there were no featured playlists (or there is no data) at
- the specified time, the response will revert to the current
- UTC time. If not provided, the response defaults to the
- current UTC time.
-
- **Example**: :code:`"2014-10-23T09:00:00"` for a user whose
- local time is 9 AM.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- A dictionary containing a message and a list of featured
- playlists.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "message": <str>,
- "playlists": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "total": <int>
- },
- "type": <str>,
- "uri": <str>
- }
- ]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/browse/featured-playlists",
- params={"country": country, "locale": locale,
- "timestamp": timestamp, "limit": limit,
- "offset": offset})
-
-
-
-[docs]
- def get_category_playlists(
- self, category_id: str, *, country: str = None, limit: int = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- `Playlists > Get Category's Playlists
- <https://developer.spotify.com/documentation/web-api/reference/
- get-a-categories-playlists>`_: Get a list of Spotify playlists
- tagged with a particular category.
-
- Parameters
- ----------
- category_id : `str`
- The Spotify category ID for the category.
-
- **Example**: :code:`"dinner"`.
-
- country : `str`, keyword-only, optional
- A country: an ISO 3166-1 alpha-2 country code. Provide this
- parameter to ensure that the category exists for a
- particular country.
-
- **Example**: :code:`"SE"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- A dictionary containing a message and a list of playlists in
- a particular category.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "message": <str>,
- "playlists": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "total": <int>
- },
- "type": <str>,
- "uri": <str>
- }
- ]
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/browse/categories/{category_id}/playlists",
- params={"country": country, "limit": limit, "offset": offset}
- )
-
-
-
-[docs]
- def get_playlist_cover_image(self, playlist_id: str) -> dict[str, Any]:
-
- """
- `Playlists > Get Playlist Cover Image
- <https://developer.spotify.com/documentation/web-api/reference/
- get-playlist-cover>`_: Get the current image associated with a
- specific playlist.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- Returns
- -------
- image : `dict`
- A dictionary containing the URL to and the dimensions of
- the playlist cover image.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- """
-
- return self._get_json(f"{self.API_URL}/playlists/{playlist_id}/images")[0]
-
-
-
-[docs]
- def add_playlist_cover_image(self, playlist_id: str, image: bytes) -> None:
-
- """
- `Playlists > Add Custom Playlist Cover Image
- <https://developer.spotify.com/documentation/web-api/reference/
- upload-custom-playlist-cover>`_: Replace the image used to
- represent a specific playlist.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`ugc-image-upload` and the
- :code:`playlist-modify-public` or
- :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- image : `bytes`
- Base64-encoded JPEG image data. The maximum payload size is
- 256 KB.
- """
-
- self._check_scope("get_categories", "ugc-image-upload")
- self._check_scope("get_categories",
- "playlist-modify-" +
- ("public" if self.get_playlist(playlist_id)["public"]
- else "private"))
-
- self._request("put", f"{self.API_URL}/playlists/{playlist_id}/images",
- data=image, headers={"Content-Type": "image/jpeg"})
-
-
- ### SEARCH ################################################################
-
-
-[docs]
- def search(
- self, q: str, type: Union[str, list[str]], *,
- limit: int = None, market: str = None, offset: int = None
- ) -> dict[str, Any]:
-
- """
- `Search > Search for Item <https://developer.spotify.com/
- documentation/web-api/reference/search>`_: Get
- Spotify catalog information about albums, artists, playlists,
- tracks, shows, episodes or audiobooks that match a keyword
- string.
-
- Parameters
- ----------
- q : `str`
- Your search query.
-
- .. note::
-
- You can narrow down your search using field filters. The
- available filters are :code:`album`, :code:`artist`,
- :code:`track`, :code:`year`, :code:`upc`,
- :code:`tag:hipster`, :code:`tag:new`, :code:`isrc`, and
- :code:`genre`. Each field filter only applies to certain
- result types.
-
- The :code:`artist` and :code:`year` filters can be used
- while searching albums, artists and tracks. You can
- filter on a single :code:`year` or a range (e.g.
- 1955-1960).
-
- The :code:`album` filter can be used while searching
- albums and tracks.
-
- The :code:`genre` filter can be used while searching
- artists and tracks.
-
- The :code:`isrc` and :code:`track` filters can be used
- while searching tracks.
-
- The :code:`upc`, :code:`tag:new` and :code:`tag:hipster`
- filters can only be used while searching albums. The
- :code:`tag:new` filter will return albums released in the
- past two weeks and :code:`tag:hipster` can be used to
- return only albums with the lowest 10% popularity.
-
- **Example**:
- :code:`"remaster track:Doxy artist:Miles Davis"`.
-
- type : `str` or `list`
- A comma-separated list of item types to search across.
- Search results include hits from all the specified item
- types.
-
- **Valid values**: :code:`"album"`, :code:`"artist"`,
- :code:`"audiobook"`, :code:`"episode"`, :code:`"playlist"`,
- :code:`"show"`, and :code:`"track"`.
-
- .. container::
-
- **Example**:
-
- * :code:`"track,artist"` returns both tracks and artists
- matching `query`.
- * :code:`type=album,track` returns both albums and tracks
- matching `query`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- results : `dict`
- The search results.
-
- .. admonition:: Sample
- :class: dropdown
-
- .. code::
-
- {
- "tracks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- },
- "artists": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "albums": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- }
- ]
- },
- "playlists": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "collaborative": <bool>,
- "description": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "owner": {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "type": "user",
- "uri": <str>,
- "display_name": <str>
- },
- "public": <bool>,
- "snapshot_id": <str>,
- "tracks": {
- "href": <str>,
- "total": <int>
- },
- "type": <str>,
- "uri": <str>
- }
- ]
- },
- "shows": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>
- }
- ]
- },
- "episodes": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "audio_preview_url": <str>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "is_playable": <bool>
- "language": <str>,
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- }
- }
- ]
- },
- "audiobooks": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "authors": [
- {
- "name": <str>
- }
- ],
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "edition": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "narrators": [
- {
- "name": <str>
- }
- ],
- "publisher": <str>,
- "type": "audiobook",
- "uri": <str>,
- "total_chapters": <int>
- }
- ]
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/search?q={urllib.parse.quote(q)}",
- params={
- "type": type if isinstance(type, str) else ",".join(type),
- "limit": limit,
- "market": market,
- "offset": offset
- }
- )[f"{type}s"]
-
-
- ### SHOWS #################################################################
-
-
-[docs]
- def get_show(self, id: str, *, market: str = None) -> dict[str, Any]:
-
- """
- `Shows > Get Show <https://developer.spotify.com/documentation/
- web-api/reference/get-a-show>`_: Get Spotify
- catalog information for a single show identified by its unique
- Spotify ID.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the show.
-
- **Example**: :code:`"38bS44xjbVVZ3No3ByF1dJ"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- show : `dict`
- Spotify catalog information for a single show.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>,
- "episodes": {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "audio_preview_url": <str>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "is_playable": <bool>
- "language": <str>,
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- }
- }
- ]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/shows/{id}",
- params={"market": market})
-
-
-
-[docs]
- def get_shows(
- self, ids: Union[str, list[str]], *, market: str = None
- ) -> list[dict[str, Any]]:
-
- """
- `Shows > Get Several Shows <https://developer.spotify.com/
- documentation/web-api/reference/
- get-multiple-shows>`_: Get Spotify catalog information for
- several shows based on their Spotify IDs.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the shows.
-
- **Maximum**: 50 IDs.
-
- **Example**:
- :code:`"5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- shows : `list`
- A list containing Spotify catalog information for multiple
- shows.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "available_markets": [<str>],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [<str>],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/shows",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "market": market}
- )["shows"]
-
-
-
-[docs]
- def get_show_episodes(
- self, id: str, *, limit: int = None, market: str = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- `Shows > Get Show Episodes <https://developer.spotify.com/
- documentation/web-api/reference/
- get-a-shows-episodes>`_: Get Spotify catalog information about
- an show's episodes. Optional parameters can be used to limit the
- number of episodes returned.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the show.
-
- **Example**: :code:`"38bS44xjbVVZ3No3ByF1dJ"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- episodes : `dict`
- A dictionary containing Spotify catalog information for a
- show's episodes and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "audio_preview_url": <str>,
- "description": <str>,
- "html_description": <str>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "is_playable": <bool>
- "language": <str>,
- "languages": [<str>],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "resume_point": {
- "fully_played": <bool>,
- "resume_position_ms": <int>
- },
- "type": "episode",
- "uri": <str>,
- "restrictions": {
- "reason": <str>
- }
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/shows/{id}/episodes",
- params={"limit": limit, "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def get_saved_shows(
- self, *, limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Shows > Get User's Saved Shows <https://developer.spotify.com/
- documentation/web-api/reference/
- get-users-saved-shows>`_: Get a list of shows saved in the
- current Spotify user's library. Optional parameters can be used
- to limit the number of shows returned.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- shows : `dict`
- A dictionary containing Spotify catalog information for a
- user's saved shows and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "show": {
- "available_markets": [
- <str>
- ],
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "description": <str>,
- "html_description": <str>,
- "explicit": <bool>,
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "is_externally_hosted": <bool>,
- "languages": [
- <str>
- ],
- "media_type": <str>,
- "name": <str>,
- "publisher": <str>,
- "type": "show",
- "uri": <str>,
- "total_episodes": <int>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_saved_shows", "user-library-read")
-
- return self._get_json(f"{self.API_URL}/me/shows",
- params={"limit": limit, "offset": offset})
-
-
-
-[docs]
- def save_shows(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Shows > Save Shows for Current User
- <https://developer.spotify.com/documentation/web-api/reference/
- save-shows-user>`_: Save one or more shows to
- current Spotify user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the shows.
- Maximum: 50 IDs.
-
- **Example**:
- :code:`"5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ"`.
- """
-
- self._check_scope("save_shows", "user-library-modify")
-
- self._request(
- "put", f"{self.API_URL}/me/shows",
- params={"ids": f"{ids if isinstance(ids, str) else ','.join(ids)}"}
- )
-
-
-
-[docs]
- def remove_saved_shows(
- self, ids: Union[str, list[str]], *, market: str = None) -> None:
-
- """
- `Shows > Remove User's Saved Shows
- <https://developer.spotify.com/documentation/web-api/reference/
- remove-shows-user>`_: Delete one or more shows from
- current Spotify user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the shows.
- Maximum: 50 IDs.
-
- **Example**:
- :code:`"5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
- """
-
- self._check_scope("remove_saved_shows", "user-library-modify")
-
- self._request("delete", f"{self.API_URL}/me/shows",
- params={"ids": ids if isinstance(ids, str)
- else ",".join(ids),
- "market": market})
-
-
-
-[docs]
- def check_saved_shows(self, ids: Union[str, list[str]]) -> list[bool]:
-
- """
- `Shows > Check User's Saved Shows
- <https://developer.spotify.com/documentation/web-api/reference/
- check-users-saved-shows>`_: Check if one or more
- shows is already saved in the current Spotify user's library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the shows.
-
- **Maximum**: 50 IDs.
-
- **Example**:
- :code:`"5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ"`.
-
- Returns
- -------
- contains : `list`
- Array of booleans specifying whether the shows are found in
- the user's saved shows.
-
- **Example**: :code:`[False, True]`.
- """
-
- self._check_scope("check_saved_shows", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/shows/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )
-
-
- ### TRACKS ################################################################
-
-
-[docs]
- def get_track(self, id: str, *, market: str = None) -> dict[str, Any]:
-
- """
- `Tracks > Get Track <https://developer.spotify.com/
- documentation/web-api/reference/get-track>`_: Get
- Spotify catalog information for a single track identified by its
- unique Spotify ID.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID for the track.
-
- **Example**: :code:`"11dFghVXANMlKmJXsNCbNl"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- track : `dict`
- Spotify catalog information for a single track.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- """
-
- return self._get_json(f"{self.API_URL}/tracks/{id}",
- params={"market": market})
-
-
-
-[docs]
- def get_tracks(
- self, ids: Union[int, str, list[Union[int, str]]], *,
- market: str = None) -> list[dict[str, Any]]:
-
- """
- `Tracks > Get Several Tracks <https://developer.spotify.com/
- documentation/web-api/reference/
- get-several-tracks>`_: Get Spotify catalog information for
- multiple tracks based on their Spotify IDs.
-
- Parameters
- ----------
- ids : `int`, `str`, or `list`
- A (comma-separated) list of the Spotify IDs for the tracks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"7ouMYWpwJ422jRcDASZB7P,
- 4VqPOruhp5EdPBeR92t6lQ, 2takcwOaAZWiXQijPHIx7B"`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- Returns
- -------
- tracks : `dict` or `list`
- A list containing Spotify catalog information for multiple
- tracks.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/tracks",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "market": market}
- )["tracks"]
-
-
-
-[docs]
- def get_saved_tracks(
- self, *, limit: int = None, market: str = None, offset: int = None
- ) -> dict[str, Any]:
-
- """
- `Tracks > Get User's Saved Tracks
- <https://developer.spotify.com/documentation/web-api/reference/
- get-users-saved-tracks>`_: Get a list of the songs
- saved in the current Spotify user's 'Your Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Valid values**: `offset` must be between 0 and 1,000.
-
- **Default**: :code:`0`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing Spotify catalog information for a
- user's saved tracks and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "added_at": <str>,
- "track": {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_saved_tracks", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/tracks",
- params={"limit": limit, "market": market, "offset": offset}
- )
-
-
-
-[docs]
- def save_tracks(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Tracks > Save Track for Current User
- <https://developer.spotify.com/documentation/web-api/reference/
- save-tracks-user>`_: Save one or more tracks to the
- current user's 'Your Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the tracks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"7ouMYWpwJ422jRcDASZB7P,
- 4VqPOruhp5EdPBeR92t6lQ, 2takcwOaAZWiXQijPHIx7B"`.
- """
-
- self._check_scope("save_tracks", "user-library-modify")
-
- if isinstance(ids, str):
- self._request("put", f"{self.API_URL}/me/tracks",
- params={"ids": ids})
- elif isinstance(ids, list):
- self._request("put", f"{self.API_URL}/me/tracks",
- json={"ids": ids})
-
-
-
-[docs]
- def remove_saved_tracks(self, ids: Union[str, list[str]]) -> None:
-
- """
- `Tracks > Remove User's Saved Tracks
- <https://developer.spotify.com/documentation/web-api/reference/
- remove-tracks-user>`_: Remove one or more tracks
- from the current user's 'Your Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the tracks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"7ouMYWpwJ422jRcDASZB7P,
- 4VqPOruhp5EdPBeR92t6lQ, 2takcwOaAZWiXQijPHIx7B"`.
- """
-
- self._check_scope("remove_saved_tracks", "user-library-modify")
-
- if isinstance(ids, str):
- self._request("delete", f"{self.API_URL}/me/tracks",
- params={"ids": ids})
- elif isinstance(ids, list):
- self._request("delete", f"{self.API_URL}/me/tracks",
- json={"ids": ids})
-
-
-
-[docs]
- def check_saved_tracks(self, ids: Union[str, list[str]]) -> list[bool]:
-
- """
- `Tracks > Check User's Saved Tracks
- <https://developer.spotify.com/documentation/web-api/reference/
- check-users-saved-tracks>`_: Check if one or more
- tracks is already saved in the current Spotify user's 'Your
- Music' library.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-library-read` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the tracks.
-
- **Maximum**: 50 IDs.
-
- **Example**: :code:`"7ouMYWpwJ422jRcDASZB7P,
- 4VqPOruhp5EdPBeR92t6lQ, 2takcwOaAZWiXQijPHIx7B"`.
-
- Returns
- -------
- contains : `list`
- Array of booleans specifying whether the tracks are found in
- the user's 'Liked Songs'.
-
- **Example**: :code:`[False, True]`.
- """
-
- self._check_scope("check_saved_tracks", "user-library-read")
-
- return self._get_json(
- f"{self.API_URL}/me/tracks/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )
-
-
-
-[docs]
- def get_track_audio_features(self, id: str) -> dict[str, Any]:
-
- """
- `Tracks > Get Track's Audio Features
- <https://developer.spotify.com/documentation/web-api/reference/
- get-audio-features>`_: Get audio feature information for a
- single track identified by its unique Spotify ID.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the track.
-
- **Example**: :code:`"11dFghVXANMlKmJXsNCbNl"`.
-
- Returns
- -------
- audio_features : `dict`
- The track's audio features.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "acousticness": <float>,
- "analysis_url": <str>,
- "danceability": <float>,
- "duration_ms": <int>,
- "energy": <float>,
- "id": <str>,
- "instrumentalness": <float>,
- "key": <int>,
- "liveness": <float>,
- "loudness": <float>,
- "mode": <int>,
- "speechiness": <float>,
- "tempo": <float>,
- "time_signature": <int>,
- "track_href": <str>,
- "type": "audio_features",
- "uri": <str>,
- "valence": <float>,
- }
- """
-
- return self._get_json(f"{self.API_URL}/audio-features/{id}")
-
-
-
-[docs]
- def get_tracks_audio_features(
- self, ids: Union[str, list[str]]) -> list[dict[str, Any]]:
-
- """
- `Tracks > Get Tracks' Audio Features
- <https://developer.spotify.com/documentation/web-api/reference/
- get-several-audio-features>`_: Get audio features
- for multiple tracks based on their Spotify IDs.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the Spotify IDs for the tracks.
-
- **Maximum**: 100 IDs.
-
- **Example**: :code:`"7ouMYWpwJ422jRcDASZB7P,
- 4VqPOruhp5EdPBeR92t6lQ, 2takcwOaAZWiXQijPHIx7B"`.
-
- Returns
- -------
- audio_features : `dict` or `list`
- A list containing audio features for multiple tracks.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "acousticness": <float>,
- "analysis_url": <str>,
- "danceability": <float>,
- "duration_ms": <int>,
- "energy": <float>,
- "id": <str>,
- "instrumentalness": <float>,
- "key": <int>,
- "liveness": <float>,
- "loudness": <float>,
- "mode": <int>,
- "speechiness": <float>,
- "tempo": <float>,
- "time_signature": <int>,
- "track_href": <str>,
- "type": "audio_features",
- "uri": <str>,
- "valence": <float>,
- }
- ]
- """
-
- return self._get_json(
- f"{self.API_URL}/audio-features",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )["audio_features"]
-
-
-
-[docs]
- def get_track_audio_analysis(self, id: str) -> dict[str, Any]:
-
- """
- `Tracks > Get Track's Audio Analysis
- <https://developer.spotify.com/documentation/web-api/reference/
- get-audio-analysis>`_: Get a low-level audio
- analysis for a track in the Spotify catalog. The audio analysis
- describes the track's structure and musical content, including
- rhythm, pitch, and timbre.
-
- Parameters
- ----------
- id : `str`
- The Spotify ID of the track.
-
- **Example**: :code:`"11dFghVXANMlKmJXsNCbNl"`.
-
- Returns
- -------
- audio_analysis : `dict`
- The track's audio analysis.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "meta": {
- "analyzer_version": <str>,
- "platform": <str>,
- "detailed_status": <str>,
- "status_code": <int>,
- "timestamp": <int>,
- "analysis_time": <float>,
- "input_process": <str>
- },
- "track": {
- "num_samples": <int>,
- "duration": <float>,
- "sample_md5": <str>,
- "offset_seconds": <int>,
- "window_seconds": <int>,
- "analysis_sample_rate": <int>,
- "analysis_channels": <int>,
- "end_of_fade_in": <int>,
- "start_of_fade_out": <float>,
- "loudness": <float>,
- "tempo": <float>,
- "tempo_confidence": <float>,
- "time_signature": <int>,
- "time_signature_confidence": <float>,
- "key": <int>,
- "key_confidence": <float>,
- "mode": <int>,
- "mode_confidence": <float>,
- "codestring": <str>,
- "code_version": <float>,
- "echoprintstring": <str>,
- "echoprint_version": <float>,
- "synchstring": <str>,
- "synch_version": <int>,
- "rhythmstring": <str>,
- "rhythm_version": <int>
- },
- "bars": [
- {
- "start": <float>,
- "duration": <float>,
- "confidence": <float>
- }
- ],
- "beats": [
- {
- "start": <float>,
- "duration": <float>,
- "confidence": <float>
- }
- ],
- "sections": [
- {
- "start": <float>,
- "duration": <float>,
- "confidence": <float>,
- "loudness": <float>,
- "tempo": <float>,
- "tempo_confidence": <float>,
- "key": <int>,
- "key_confidence": <float>,
- "mode": <int>,
- "mode_confidence": <float>,
- "time_signature": <int>,
- "time_signature_confidence": <float>
- }
- ],
- "segments": [
- {
- "start": <float>,
- "duration": <float>,
- "confidence": <float>,
- "loudness_start": <float>,
- "loudness_max": <float>,
- "loudness_max_time": <float>,
- "loudness_end": <int>,
- "pitches": [<float>],
- "timbre": [<float>]
- }
- ],
- "tatums": [
- {
- "start": <float>,
- "duration": <float>,
- "confidence": <float>
- }
- ]
- }
- """
-
- return self._get_json(f"{self.API_URL}/audio-analysis/{id}")
-
-
-
-[docs]
- def get_recommendations(
- self, seed_artists: Union[str, list[str]] = None,
- seed_genres: Union[str, list[str]] = None,
- seed_tracks: Union[str, list[str]] = None, *, limit: int = None,
- market: str = None, **kwargs) -> list[dict[str, Any]]:
-
- """
- `Tracks > Get Recommendations <https://developer.spotify.com/
- documentation/web-api/reference/
- get-recommendations>`_: Recommendations are generated based on
- the available information for a given seed entity and matched
- against similar artists and tracks. If there is sufficient
- information about the provided seeds, a list of tracks will be
- returned together with pool size details.
-
- For artists and tracks that are very new or obscure, there might
- not be enough data to generate a list of tracks.
-
- .. important::
-
- Spotify content may not be used to train machine learning or
- AI models.
-
- Parameters
- ----------
- seed_artists : `str`, optional
- A comma separated list of Spotify IDs for seed artists.
-
- **Maximum**: Up to 5 seed values may be provided in any
- combination of `seed_artists`, `seed_tracks`, and
- `seed_genres`.
-
- **Example**: :code:`"4NHQUGzhtTLFvgF5SZesLK"`.
-
- seed_genres : `str`, optional
- A comma separated list of any genres in the set of available
- genre seeds.
-
- **Maximum**: Up to 5 seed values may be provided in any
- combination of `seed_artists`, `seed_tracks`, and
- `seed_genres`.
-
- **Example**: :code:`"classical,country"`.
-
- seed_tracks : `str`, optional
- A comma separated list of Spotify IDs for a seed track.
-
- **Maximum**: Up to 5 seed values may be provided in any
- combination of `seed_artists`, `seed_tracks`, and
- `seed_genres`.
-
- **Example**: :code:`"0c6xIDDpzE81m2q797ordA"`.
-
- limit : `int`, keyword-only, optional
- The target size of the list of recommended tracks. For seeds
- with unusually small pools or when highly restrictive
- filtering is applied, it may be impossible to generate the
- requested number of recommended tracks. Debugging
- information for such cases is available in the response.
-
- **Minimum**: :code:`1`.
-
- **Maximum**: :code:`100`.
-
- **Default**: :code:`20`.
-
- market : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If a country code is
- specified, only content that is available in that market
- will be returned. If a valid user access token is specified
- in the request header, the country associated with the user
- account will take priority over this parameter.
-
- .. note::
-
- If neither market or user country are provided, the
- content is considered unavailable for the client.
-
- **Example**: :code:`"ES"`.
-
- **kwargs
- Tunable track attributes. For a list of available options,
- see the `Spotify Web API Reference page for this endpoint
- <https://developer.spotify.com/documentation/web-api/
- reference/get-recommendations>`_.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing Spotify catalog information for the
- recommended tracks.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "seeds": [
- {
- "afterFilteringSize": <int>,
- "afterRelinkingSize": <int>,
- "href": <str>,
- "id": <str>,
- "initialPoolSize": <int>,
- "type": <str>
- }
- ],
- "tracks": [
- {
- "album": {
- "album_type": <str>,
- "total_tracks": <int>,
- "available_markets": [<str>],
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "release_date": <str>,
- "release_date_precision": <str>,
- "restrictions": {
- "reason": <str>
- },
- "type": "album",
- "uri": <str>,
- "copyrights": [
- {
- "text": <str>,
- "type": <str>
- }
- ],
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "genres": [<str>],
- "label": <str>,
- "popularity": <int>,
- "album_group": <str>,
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "name": <str>,
- "type": "artist",
- "uri": <str>
- }
- ]
- },
- "artists": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ],
- "available_markets": [<str>],
- "disc_number": <int>,
- "duration_ms": <int>,
- "explicit": <bool>,
- "external_ids": {
- "isrc": <str>,
- "ean": <str>,
- "upc": <str>
- },
- "external_urls": {
- "spotify": <str>
- },
- "href": <str>,
- "id": <str>,
- "is_playable": <bool>
- "linked_from": {
- },
- "restrictions": {
- "reason": <str>
- },
- "name": <str>,
- "popularity": <int>,
- "preview_url": <str>,
- "track_number": <int>,
- "type": "track",
- "uri": <str>,
- "is_local": <bool>
- }
- ]
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/recommendations",
- params={
- "seed_artists": seed_artists if seed_artists is None
- or isinstance(seed_artists, str)
- else ",".join(seed_artists),
- "seed_genres": seed_genres if seed_genres is None
- or isinstance(seed_genres, str)
- else ",".join(seed_genres),
- "seed_tracks": seed_tracks if seed_tracks is None
- or isinstance(seed_tracks, str)
- else ",".join(seed_tracks),
- "limit": limit,
- "market": market,
- **kwargs
- }
- )
-
-
- ### USERS #################################################################
-
-
-[docs]
- def get_profile(self) -> dict[str, Any]:
-
- """
- `Users > Get Current User's Profile
- <https://developer.spotify.com/documentation/web-api/reference/
- get-current-users-profile>`_: Get detailed profile
- information about the current user (including the current user's
- username).
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-read-private` scope.
-
- Returns
- -------
- user : `dict`
- A dictionary containing the current user's information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "country": <str>,
- "display_name": <str>,
- "email": <str>,
- "explicit_content": {
- "filter_enabled": <bool>,
- "filter_locked": <bool>
- },
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "product": <str>,
- "type": <str>,
- "uri": <str>
- }
- """
-
- self._check_scope("get_profile", "user-read-private")
-
- return self._get_json(f"{self.API_URL}/me")
-
-
-
-[docs]
- def get_top_items(
- self, type: str, *, limit: int = None, offset: int = None,
- time_range: str = None) -> dict[str, Any]:
-
- """
- `Users > Get User's Top Items <https://developer.spotify.com/
- documentation/web-api/reference/
- get-users-top-artists-and-tracks>`_: Get the current user's top
- artists or tracks based on calculated affinity.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-top-read` scope.
-
- Parameters
- ----------
- type : `str`
- The type of entity to return.
-
- **Valid values**: :code:`"artists"` and :code:`"tracks"`.
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- offset : `int`, keyword-only, optional
- The index of the first result to return. Use with `limit` to
- get the next page of search results.
-
- **Default**: :code:`0`.
-
- time_range : `str`, keyword-only, optional
- Over what time frame the affinities are computed.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"long_term"` (calculated from several years of
- data and including all new data as it becomes
- available).
- * :code:`"medium_term"` (approximately last 6 months).
- * :code:`"short_term"` (approximately last 4 weeks).
-
- **Default**: :code:`"medium_term"`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing Spotify catalog information for a
- user's top items and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "offset": <int>,
- "previous": <str>,
- "total": <int>,
- "items": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": <str>,
- "uri": <str>
- }
- ]
- }
- """
-
- if type not in (TYPES := {"artists", "tracks"}):
- raise ValueError(f"Invalid entity type ({type=}). "
- f"Valid values: {', '.join(TYPES)}.")
-
- self._check_scope("get_top_items", "user-top-read")
-
- return self._get_json(
- f"{self.API_URL}/me/top/{type}",
- params={"limit": limit, "offset": offset, "time_range": time_range}
- )
-
-
-
-[docs]
- def get_user_profile(self, user_id: str) -> dict[str, Any]:
-
- """
- `Users > Get User's Profile <https://developer.spotify.com/
- documentation/web-api/reference/
- get-users-profile>`_: Get public profile information about a
- Spotify user.
-
- Parameters
- ----------
- user_id : `str`
- The user's Spotify user ID.
-
- **Example**: :code:`"smedjan"`
-
- Returns
- -------
- user : `dict`
- A dictionary containing the user's information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "display_name": <str>,
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "type": "user",
- "uri": <str>
- }
- """
-
- return self._get_json(f"{self.API_URL}/users/{user_id}")
-
-
-
-[docs]
- def follow_playlist(self, playlist_id: str) -> None:
-
- """
- `Users > Follow Playlist <https://developer.spotify.com/
- documentation/web-api/reference/follow-playlist>`_:
- Add the current user as a follower of a playlist.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`
- """
-
- self._check_scope("follow_playlist", "playlist-modify-private")
-
- self._request("put",
- f"{self.API_URL}/playlists/{playlist_id}/followers")
-
-
-
-[docs]
- def unfollow_playlist(self, playlist_id: str) -> None:
-
- """
- `Users > Unfollow Playlist <https://developer.spotify.com/
- documentation/web-api/reference/
- unfollow-playlist>`_: Remove the current user as a follower of a
- playlist.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`playlist-modify-private` scope.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`
- """
-
- self._check_scope("unfollow_playlist", "playlist-modify-private")
-
- self._request("delete",
- f"{self.API_URL}/playlists/{playlist_id}/followers")
-
-
-
-[docs]
- def get_followed_artists(
- self, *, after: str = None, limit: int = None) -> dict[str, Any]:
-
- """
- `Users > Get Followed Artists <https://developer.spotify.com/
- documentation/web-api/reference/get-followed>`_:
- Get the current user's followed artists.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-follow-read` scope.
-
- Parameters
- ----------
- after : `str`, keyword-only, optional
- The last artist ID retrieved from the previous request.
-
- **Example**: :code:`"0I2XqVXqHScXjHhk6AYYRe"`
-
- limit : `int`, keyword-only, optional
- The maximum number of results to return in each item type.
-
- **Valid values**: `limit` must be between 0 and 50.
-
- **Default**: :code:`20`.
-
- Returns
- -------
- artists : `dict`
- A dictionary containing Spotify catalog information for a
- user's followed artists and the number of results returned.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "href": <str>,
- "limit": <int>,
- "next": <str>,
- "cursors": {
- "after": <str>,
- "before": <str>
- },
- "total": <int>,
- "items": [
- {
- "external_urls": {
- "spotify": <str>
- },
- "followers": {
- "href": <str>,
- "total": <int>
- },
- "genres": [<str>],
- "href": <str>,
- "id": <str>,
- "images": [
- {
- "url": <str>,
- "height": <int>,
- "width": <int>
- }
- ],
- "name": <str>,
- "popularity": <int>,
- "type": "artist",
- "uri": <str>
- }
- ]
- }
- """
-
- self._check_scope("get_followed_artists", "user-follow-read")
-
- return self._get_json(
- f"{self.API_URL}/me/following",
- params={"type": "artist", "after": after, "limit": limit}
- )["artists"]
-
-
-
-[docs]
- def follow_people(self, ids: Union[str, list[str]], type: str) -> None:
-
- """
- `Users > Follow Artists or Users <https://developer.spotify.com/
- documentation/web-api/reference/
- follow-artists-users>`_: Add the current user as a follower of
- one or more artists or other Spotify users.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-follow-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the artist or user Spotify IDs.
-
- **Maximum**: Up to 50 IDs can be sent in one request.
-
- **Example**: :code:`"2CIMQHirSU0MQqyYHq0eOx,
- 57dN52uHvrHOxijzpIgu3E, 1vCWHaC5f2uS3yhpwWbIA6"`.
-
- type : `str`
- The ID type.
-
- **Valid values**: :code:`"artist"` and :code:`"user"`.
- """
-
- self._check_scope("follow_people", "user-follow-modify")
-
- if isinstance(ids, str):
- self._request("put", f"{self.API_URL}/me/following",
- params={"ids": ids, "type": type})
- elif isinstance(ids, list):
- self._request("put", f"{self.API_URL}/me/following",
- json={"ids": ids}, params={"type": type})
-
-
-
-[docs]
- def unfollow_people(self, ids: Union[str, list[str]], type: str) -> None:
-
- """
- `Users > Unfollow Artists or Users
- <https://developer.spotify.com/documentation/web-api/reference/
- unfollow-artists-users>`_: Remove the current user
- as a follower of one or more artists or other Spotify users.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-follow-modify` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the artist or user Spotify IDs.
-
- **Maximum**: Up to 50 IDs can be sent in one request.
-
- **Example**: :code:`"2CIMQHirSU0MQqyYHq0eOx,
- 57dN52uHvrHOxijzpIgu3E, 1vCWHaC5f2uS3yhpwWbIA6"`.
-
- type : `str`
- The ID type.
-
- **Valid values**: :code:`"artist"` and :code:`"user"`.
- """
-
- self._check_scope("unfollow_people", "user-follow-modify")
-
- if isinstance(ids, str):
- self._request("delete", f"{self.API_URL}/me/following",
- params={"ids": ids, "type": type})
- elif isinstance(ids, list):
- self._request("delete", f"{self.API_URL}/me/following",
- json={"ids": ids}, params={"type": type})
-
-
-
-[docs]
- def check_followed_people(
- self, ids: Union[str, list[str]], type: str) -> list[bool]:
-
- """
- `Users > Check If User Follows Artists or Users
- <https://developer.spotify.com/documentation/web-api/reference/
- check-current-user-follows>`_: Check to see if the
- current user is following one or more artists or other Spotify
- users.
-
- .. admonition:: Authorization scope
- :class: warning
-
- Requires the :code:`user-follow-read` scope.
-
- Parameters
- ----------
- ids : `str` or `list`
- A (comma-separated) list of the artist or user Spotify IDs.
-
- **Maximum**: Up to 50 IDs can be sent in one request.
-
- **Example**: :code:`"2CIMQHirSU0MQqyYHq0eOx,
- 57dN52uHvrHOxijzpIgu3E, 1vCWHaC5f2uS3yhpwWbIA6"`.
-
- type : `str`
- The ID type.
-
- **Valid values**: :code:`"artist"` and :code:`"user"`.
-
- Returns
- -------
- contains : `list`
- Array of booleans specifying whether the user follows the
- specified artists or Spotify users.
-
- **Example**: :code:`[False, True]`.
- """
-
- self._check_scope("check_followed_people", "user-follow-read")
-
- return self._get_json(
- f"{self.API_URL}/me/following/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids),
- "type": type}
- )
-
-
-
-[docs]
- def check_playlist_followers(
- self, playlist_id: str, ids: Union[str, list[str]]) -> list[bool]:
-
- """
- `Users > Check If Users Follow Playlist
- <https://developer.spotify.com/documentation/web-api/reference/
- check-if-user-follows-playlist>`_: Check to see if
- one or more Spotify users are following a specified playlist.
-
- Parameters
- ----------
- playlist_id : `str`
- The Spotify ID of the playlist.
-
- **Example**: :code:`"3cEYpjA9oz9GiPac4AsH4n"`.
-
- ids : `str` or `list`
- A (comma-separated) list of Spotify user IDs; the IDs of the
- users that you want to check to see if they follow the
- playlist.
-
- **Maximum**: 5 IDs.
-
- **Example**: :code:`"jmperezperez,thelinmichael,wizzler"`.
-
- Returns
- -------
- follows : `list`
- Array of booleans specifying whether the users follow the
- playlist.
-
- **Example**: :code:`[False, True]`.
- """
-
- return self._get_json(
- f"{self.API_URL}/playlists/{playlist_id}/followers/contains",
- params={"ids": ids if isinstance(ids, str) else ",".join(ids)}
- )
-
-
-
-"""
-TIDAL
-=====
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module contains a complete implementation of all public TIDAL API
-endpoints and a minimum implementation of the more robust but private
-TIDAL API.
-"""
-
-import base64
-import datetime
-import hashlib
-import json
-import logging
-import os
-import pathlib
-import re
-import secrets
-import time
-from typing import Any, Union
-import urllib
-import warnings
-import webbrowser
-from xml.dom import minidom
-
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-import requests
-
-from . import FOUND_PLAYWRIGHT, DIR_HOME, DIR_TEMP, _config
-if FOUND_PLAYWRIGHT:
- from playwright.sync_api import sync_playwright
-
-__all__ = ["API", "PrivateAPI"]
-
-
-[docs]
-class API:
-
- """
- TIDAL API client.
-
- The TIDAL API exposes TIDAL functionality and data, making it
- possible to build applications that can search for and retrieve
- metadata from the TIDAL catalog.
-
- .. seealso::
-
- For more information, see the `TIDAL API Reference
- <https://developer.tidal.com/apiref>`_.
-
- Requests to the TIDAL API endpoints must be accompanied by a valid
- access token in the header. Minim can obtain client-only access
- tokens via the client credentials flow, which requires valid client
- credentials (client ID and client secret) to either be provided to
- this class's constructor as keyword arguments or be stored as
- :code:`TIDAL_CLIENT_ID` and :code:`TIDAL_CLIENT_SECRET` in the
- operating system's environment variables.
-
- .. seealso::
-
- To get client credentials, see the `guide on how to register a new
- TIDAL application <https://developer.tidal.com/documentation
- /dashboard/dashboard-client-credentials>`_.
-
- If an existing access token is available, it and its expiry time can
- be provided to this class's constructor as keyword arguments to
- bypass the access token retrieval process. It is recommended that
- all other authorization-related keyword arguments be specified so
- that a new access token can be obtained when the existing one
- expires.
-
- .. tip::
-
- The authorization flow and access token can be changed or updated
- at any time using :meth:`set_auflow` and :meth:`set_access_token`,
- respectively.
-
- Minim also stores and manages access tokens and their properties.
- When an access token is acquired, it is automatically saved to the
- Minim configuration file to be loaded on the next instantiation of
- this class. This behavior can be disabled if there are any security
- concerns, like if the computer being used is a shared device.
-
- Parameters
- ----------
- client_id : `str`, keyword-only, optional
- Client ID. Required for the client credentials flow. If it is
- not stored as :code:`TIDAL_CLIENT_ID` in the operating system's
- environment variables or found in the Minim configuration file,
- it must be provided here.
-
- client_secret : `str`, keyword-only, optional
- Client secret. Required for the client credentials flow. If it
- is not stored as :code:`TIDAL_CLIENT_SECRET` in the operating
- system's environment variables or found in the Minim
- configuration file, it must be provided here.
-
- flow : `str`, keyword-only, optional
- Authorization flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"client_credentials"` for the client credentials
- flow.
-
- access_token : `str`, keyword-only, optional
- Access token. If provided here or found in the Minim
- configuration file, the authorization process is bypassed. In
- the former case, all other relevant keyword arguments should be
- specified to automatically refresh the access token when it
- expires.
-
- expiry : `datetime.datetime` or `str`, keyword-only, optional
- Expiry time of `access_token` in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated using the specified authorization flow (if
- possible) when `access_token` expires.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether to overwrite an existing access token in the
- Minim configuration file.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained access tokens and their
- associated properties are stored to the Minim configuration
- file.
-
- Attributes
- ----------
- session : `requests.Session`
- Session used to send requests to the TIDAL API.
-
- API_URL : `str`
- Base URL for the TIDAL API.
-
- TOKEN_URL : `str`
- URL for the TIDAL API token endpoint.
- """
-
- _FLOWS = {"client_credentials"}
- _NAME = f"{__module__}.{__qualname__}"
- API_URL = "https://openapi.tidal.com"
- TOKEN_URL = "https://auth.tidal.com/v1/oauth2/token"
-
- def __init__(
- self, *, client_id: str = None, client_secret: str = None,
- flow: str = "client_credentials", access_token: str = None,
- expiry: Union[datetime.datetime, str] = None,
- overwrite: bool = False, save: bool = True) -> None:
-
- """
- Create a TIDAL API client.
- """
-
- self.session = requests.Session()
- self.session.headers["Content-Type"] = "application/vnd.tidal.v1+json"
-
- if (access_token is None and _config.has_section(self._NAME)
- and not overwrite):
- flow = _config.get(self._NAME, "flow")
- access_token = _config.get(self._NAME, "access_token")
- expiry = _config.get(self._NAME, "expiry")
- client_id = _config.get(self._NAME, "client_id")
- client_secret = _config.get(self._NAME, "client_secret")
-
- self.set_flow(flow, client_id=client_id, client_secret=client_secret,
- save=save)
- self.set_access_token(access_token, expiry=expiry)
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _request(self, method: str, url: str, **kwargs) -> requests.Response:
-
- """
- Construct and send a request with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- if self._expiry is not None and datetime.datetime.now() > self._expiry:
- self.set_access_token()
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code not in range(200, 299):
- try:
- error = r.json()["errors"][0]
- emsg = f"{r.status_code} {error['code']}: {error['detail']}"
- except requests.exceptions.JSONDecodeError:
- emsg = f"{r.status_code} {r.reason}"
- raise RuntimeError(emsg)
- return r
-
-
-[docs]
- def set_access_token(
- self, access_token: str = None, *,
- expiry: Union[str, datetime.datetime] = None) -> None:
-
- """
- Set the TIDAL API access token.
-
- Parameters
- ----------
- access_token : `str`, optional
- Access token. If not provided, an access token is obtained
- using an OAuth 2.0 authorization flow.
-
- expiry : `str` or `datetime.datetime`, keyword-only, optional
- Access token expiry timestamp in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated using the default authorization flow (if
- possible) when `access_token` expires.
- """
-
- if access_token is None:
- if not self._client_id or not self._client_secret:
- raise ValueError("TIDAL API client credentials not provided.")
-
- if self._flow == "client_credentials":
- client_b64 = base64.urlsafe_b64encode(
- f"{self._client_id}:{self._client_secret}".encode()
- ).decode()
- r = requests.post(
- self.TOKEN_URL,
- data={"grant_type": "client_credentials"},
- headers={"Authorization": f"Basic {client_b64}"}
- ).json()
- access_token = r["access_token"]
- expiry = (datetime.datetime.now()
- + datetime.timedelta(0, r["expires_in"]))
-
- if self._save:
- _config[self._NAME] = {
- "flow": self._flow,
- "client_id": self._client_id,
- "client_secret": self._client_secret,
- "access_token": access_token,
- "expiry": expiry.strftime("%Y-%m-%dT%H:%M:%SZ")
- }
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- self.session.headers["Authorization"] = f"Bearer {access_token}"
- self._expiry = (
- datetime.datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ")
- if isinstance(expiry, str) else expiry
- )
-
-
-
-[docs]
- def set_flow(
- self, flow: str, *, client_id: str = None,
- client_secret: str = None, save: bool = True) -> None:
-
- """
- Set the authorization flow.
-
- Parameters
- ----------
- flow : `str`
- Authorization flow.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"client_credentials"` for the client credentials
- flow.
-
- client_id : `str`, keyword-only, optional
- Client ID. Required for all authorization flows.
-
- client_secret : `str`, keyword-only, optional
- Client secret. Required for all authorization flows.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether to save the newly obtained access tokens
- and their associated properties to the Minim configuration
- file.
- """
-
- if flow not in self._FLOWS:
- emsg = (f"Invalid authorization flow ({flow=}). "
- f"Valid values: {', '.join(self._FLOWS)}.")
- raise ValueError(emsg)
-
- self._flow = flow
- self._save = save
-
- if flow == "client_credentials":
- self._client_id = client_id or os.environ.get("TIDAL_CLIENT_ID")
- self._client_secret = (client_secret
- or os.environ.get("TIDAL_CLIENT_SECRET"))
-
-
- ### ALBUM API #############################################################
-
-
-[docs]
- def get_album(
- self, album_id: Union[int, str], country_code: str
- ) -> dict[str, Any]:
-
- """
- `Album API > Get single album
- <https://developer.tidal.com/apiref?ref=get-album>`_: Retrieve
- album details by TIDAL album ID.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Example**: :code:`251380836`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- album : `dict`
- TIDAL catalog information for a single album.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <str>,
- "barcodeId": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "releaseDate": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "numberOfVolumes": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "type": "ALBUM",
- "copyright": <str>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/albums/{album_id}",
- params={"countryCode": country_code})["resource"]
-
-
-
-[docs]
- def get_albums(
- self, album_ids: Union[int, str, list[Union[int, str]]],
- country_code: str) -> list[dict[str, Any]]:
-
- """
- `Album API > Get multiple albums
- <https://developer.tidal.com/apiref?ref=get-albums-by-ids>`_:
- Retrieve a list of album details by TIDAL album IDs.
-
- Parameters
- ----------
- album_ids : `int`, `str`, or `list`
- TIDAL album ID(s).
-
- **Examples**: :code:`"251380836,275646830"` or
- :code:`[251380836, 275646830]`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- albums : `dict`
- A dictionary containing TIDAL catalog information for
- multiple albums and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>,
- "barcodeId": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "releaseDate": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "numberOfVolumes": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "type": "ALBUM",
- "copyright": <str>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "requested": <int>,
- "success": <int>,
- "failure": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/albums/byIds",
- params={"ids": album_ids, "countryCode": country_code}
- )
-
-
-
-[docs]
- def get_album_items(
- self, album_id: Union[int, str], country_code: str, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Album API > Get album items
- <https://developer.tidal.com/apiref?ref=get-album-items>`_:
- Retrieve a list of album items (tracks and videos) by TIDAL
- album ID.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Examples**: :code:`251380836`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing TIDAL catalog information for
- tracks and videos in the specified album and metadata for
- the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "artifactType": <str>,
- "id": <str>,
- "title": str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "album": {
- "id": <str>,
- "title": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": []
- },
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <str>,
- "copyright": <str>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "total": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/albums/{album_id}/items",
- params={
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_album_by_barcode_id(
- self, barcode_id: Union[int, str], country_code: str
- ) -> dict[str, Any]:
-
- """
- `Album API > Get album by barcode ID
- <https://developer.tidal.com
- /apiref?ref=get-albums-by-barcode-id>`_: Retrieve a list of album
- details by barcode ID.
-
- Parameters
- ----------
- barcode_id : `int` or `str`
- Barcode ID in EAN-13 or UPC-A format.
-
- **Example**: :code:`196589525444`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- album : `dict`
- TIDAL catalog information for a single album.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>,
- "barcodeId": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "releaseDate": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "numberOfVolumes": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "type": "ALBUM",
- "copyright": <str>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "requested": 1,
- "success": 1,
- "failure": 0
- }
- }
-
- """
-
- return self._get_json(
- f"{self.API_URL}/albums/byBarcodeId",
- params={"barcodeId": barcode_id, "countryCode": country_code}
- )
-
-
-
-[docs]
- def get_similar_albums(
- self, album_id: Union[int, str], country_code: str, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Album API > Get similar albums for the given album
- <https://developer.tidal.com/apiref?ref=get-similar-albums>`_:
- Retrieve a list of albums similar to the given album.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Examples**: :code:`251380836`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- album_ids : `dict`
- A dictionary containing TIDAL album IDs for similar albums
- and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>
- }
- }
- ],
- "metadata": {
- "total": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/albums/{album_id}/similar",
- params={
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
- ### ARTIST API ############################################################
-
-
-[docs]
- def get_artist(
- self, artist_id: Union[int, str], country_code: str
- ) -> dict[str, Any]:
-
- """
- `Artist API > Get single artist
- <https://developer.tidal.com/apiref?ref=get-artist>`_: Retrieve
- artist details by TIDAL artist ID.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- artist : `dict`
- TIDAL catalog information for a single artist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- }
- """
-
- return self._get_json(f"{self.API_URL}/artists/{artist_id}",
- params={"countryCode": country_code})["resource"]
-
-
-
-[docs]
- def get_artists(
- self, artist_ids: Union[int, str, list[Union[int, str]]],
- country_code: str) -> dict[str, Any]:
-
- """
- `Artist API > Get multiple artists
- <https://developer.tidal.com/apiref?ref=get-artists-by-ids>`_:
- Retrieve a list of artist details by TIDAL artist IDs.
-
- Parameters
- ----------
- artist_ids : `int`, `str`, or `list`
- TIDAL artist ID(s).
-
- **Examples**: :code:`"1566,7804"` or :code:`[1566, 7804]`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- artists : `dict`
- A dictionary containing TIDAL catalog information for
- multiple artists and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "requested": <int>,
- "success": <int>,
- "failure": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artists",
- params={"ids": artist_ids, "countryCode": country_code}
- )
-
-
-
-[docs]
- def get_artist_albums(
- self, artist_id: Union[int, str], country_code: str, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Artist API > Get albums by artist
- <https://developer.tidal.com/apiref?ref=get-artist-albums>`_:
- Retrieve a list of albums by TIDAL artist ID.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- albums : `dict`
- A dictionary containing TIDAL catalog information for
- albums by the specified artist and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>,
- "barcodeId": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "releaseDate": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "numberOfVolumes": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "type": "ALBUM",
- "copyright": <str>,
- "mediaMetadata": {
- "tags": <str>
- },
- "properties": {
- "content": <str>
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "total": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artists/{artist_id}/albums",
- params={
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_artist_tracks(
- self, artist_id: Union[int, str], country_code: str,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Track API > Get tracks by artist
- <https://developer.tidal.com/apiref?ref=get-tracks-by-artist>`_:
- Retrieve a list of tracks made by the specified artist.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing TIDAL catalog information for tracks
- by the specified artist and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "properties": {
- "content": <str>
- },
- "id": <str>,
- "version": <str>,
- "duration": <int>,
- "album": {
- "id": <str>,
- "title": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "title": <str>,
- "copyright": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "popularity": <float>,
- "isrc": <str>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "tidalUrl": <str>,
- "providerInfo": {
- "providerId": <str>,
- "providerName": <str>
- },
- "artifactType": <str>,
- "mediaMetadata": {
- "tags": <str>
- }
- },
- "id": <str>,
- "status": <int>,
- "message": <str>
- }
- ],
- "metadata": {
- "total": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artists/{artist_id}/tracks",
- params={
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_similar_artists(
- self, artist_id: Union[int, str], country_code: str, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Artist API > Get similar artists for the given artist
- <https://developer.tidal.com/apiref?ref=get-similar-artists>`_:
- Retrieve a list of artists similar to the given artist.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- artist_ids : `dict`
- A dictionary containing TIDAL artist IDs for similar albums
- and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>
- }
- }
- ],
- "metadata": {
- "total": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/artists/{artist_id}/similar",
- params={
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
- ### TRACK API #############################################################
-
-
-[docs]
- def get_track(
- self, track_id: Union[int, str], country_code: str
- ) -> dict[str, Any]:
-
- """
- `Track API > Get single track
- <https://developer.tidal.com/apiref?ref=get-track>`_: Retrieve
- track details by TIDAL track ID.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- track : `dict`
- TIDAL catalog information for a single track.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "artifactType": "track",
- "id": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "album": {
- "id": <str>,
- "title": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <int>,
- "copyright": <int>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/tracks/{track_id}",
- params={"countryCode": country_code})["resource"]
-
-
-
-[docs]
- def get_tracks(
- self, track_ids: Union[int, str, list[Union[int, str]]],
- country_code: str) -> dict[str, Any]:
-
- """
- `Album API > Get multiple tracks
- <https://developer.tidal.com/apiref?ref=get-tracks-by-ids>`_:
- Retrieve a list of track details by TIDAL track IDs.
-
- Parameters
- ----------
- track_ids : `int`, `str`, or `list`
- TIDAL track ID(s).
-
- **Examples**: :code:`"251380837,251380838"` or
- :code:`[251380837, 251380838]`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing TIDAL catalog information for
- multiple tracks and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "artifactType": "track",
- "id": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "album": {
- "id": <str>,
- "title": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <int>,
- "copyright": <int>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "requested": <int>,
- "success": <int>,
- "failure": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/tracks",
- params={"ids": track_ids, "countryCode": country_code}
- )
-
-
-
-[docs]
- def get_tracks_by_isrc(
- self, isrc: str, country_code: str, limit: int = None,
- offset: int = None) -> dict[str, Any]:
-
- """
- `Track API > Get tracks by ISRC
- <https://developer.tidal.com/apiref?ref=get-tracks-by-isrc>`_:
- Retrieve a list of track details by ISRC.
-
- Parameters
- ----------
- isrc : `str`
- Valid ISRC code (usually comprises 12 alphanumeric
- characters).
-
- **Example**: :code:`"USSM12209515"`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing TIDAL catalog information for
- tracks with the specified ISRC and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "artifactType": "track",
- "id": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "album": {
- "id": <str>,
- "title": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <int>,
- "copyright": <int>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "metadata": {
- "requested": <int>,
- "success": <int>,
- "failure": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/tracks/byIsrc",
- params={
- "isrc": isrc,
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_similar_tracks(
- self, track_id: Union[int, str], country_code: str, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- `Track API > Get similar tracks for the given track
- <https://developer.tidal.com/apiref?ref=get-similar-tracks>`_:
- Retrieve a list of tracks similar to the given track.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- track_ids : `dict`
- A dictionary containing TIDAL track IDs for similar albums
- and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "resource": {
- "id": <str>
- }
- }
- ],
- "metadata": {
- "total": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/tracks/{track_id}/similar",
- params={
- "countryCode": country_code,
- "limit": limit,
- "offset": offset
- }
- )
-
-
- ### VIDEO API #############################################################
-
-
-[docs]
- def get_video(
- self, video_id: Union[int, str], country_code: str
- ) -> dict[str, Any]:
-
- """
- `Video API > Get single video
- <https://developer.tidal.com/apiref?ref=get-video>`_: Retrieve
- video details by TIDAL video ID.
-
- Parameters
- ----------
- video_id : `int` or `str`
- TIDAL video ID.
-
- **Example**: :code:`75623239`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- video : `dict`
- TIDAL catalog information for a single video.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "artifactType": "video",
- "id": <str>,
- "title": <str>,
- "image": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "releaseDate": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <str>,
- "properties": {
- "content": [<str>]
- }
- }
- """
-
- return self._get_json(f"{self.API_URL}/videos/{video_id}",
- params={"countryCode": country_code})["resource"]
-
-
-
-[docs]
- def get_videos(
- self, video_ids: Union[int, str, list[Union[int, str]]],
- country_code: str) -> list[dict[str, Any]]:
-
- """
- `Album API > Get multiple videos
- <https://developer.tidal.com/apiref?ref=get-videos-by-ids>`_:
- Retrieve a list of video details by TIDAL video IDs.
-
- Parameters
- ----------
- video_ids : `int`, `str`, or `list`
- TIDAL video ID(s).
-
- **Examples**: :code:`"59727844,75623239"` or
- :code:`[59727844, 75623239]`.
-
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- videos : `dict`
- A dictionary containing TIDAL catalog information for
- multiple videos and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "data": [
- {
- "artifactType": "video",
- "id": <str>,
- "title": <str>,
- "image": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "releaseDate": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <str>,
- "properties": {
- "content": [<str>]
- }
- }
- ],
- "metadata": {
- "requested": <int>,
- "success": <int>,
- "failure": <int>
- }
- }
- """
-
- return self._get_json(
- f"{self.API_URL}/videos",
- params={"ids": video_ids, "countryCode": country_code}
- )
-
-
- ### SEARCH API ############################################################
-
-
-[docs]
- def search(
- self, query: str, country_code: str, *, type: str = None,
- limit: int = None, offset: int = None, popularity: str = None
- ) -> dict[str, list[dict[str, Any]]]:
-
- """
- `Search API > Search for catalog items
- <https://developer.tidal.com/apiref?ref=search>`_: Search for
- albums, artists, tracks, and videos.
-
- Parameters
- ----------
- query : `str`
- Search query.
-
- **Example**: :code:`"Beyoncé"`.
-
- country_code: `str`
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
-
- type : `str`, keyword-only, optional
- Target search type. Searches for all types if not specified.
-
- **Valid values**: :code:`"ALBUMS"`, :code:`"ARTISTS"`,
- :code:`"TRACKS"`, :code:`"VIDEOS"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- popularity : `str`, keyword-only, optional
- Specify which popularity type to apply for query result.
- :code:`"WORLDWIDE"` is used if not specified.
-
- **Valid values**: :code:`"WORLDWIDE"` or :code:`"COUNTRY"`.
-
- Returns
- -------
- results : `dict`
- A dictionary containing TIDAL catalog information for
- albums, artists, tracks, and videos matching the search
- query, and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "albums": [
- {
- "resource": {
- "id": <str>,
- "barcodeId": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "releaseDate": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "numberOfVolumes": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "type": "ALBUM",
- "copyright": <str>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "artists": [
- {
- "resource": {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "tracks": [
- {
- "resource": {
- "artifactType": "track",
- "id": <str>,
- "title": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "album": {
- "id": <str>,
- "title": <str>,
- "imageCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "videoCover": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ]
- },
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <int>,
- "copyright": <int>,
- "mediaMetadata": {
- "tags": [<str>]
- },
- "properties": {
- "content": [<str>]
- }
- },
- "id": <str>,
- "status": 200,
- "message": "success"
- }
- ],
- "videos": [
- {
- "artifactType": "video",
- "id": <str>,
- "title": <str>,
- "image": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "releaseDate": <str>,
- "artists": [
- {
- "id": <str>,
- "name": <str>,
- "picture": [
- {
- "url": <str>,
- "width": <int>,
- "height": <int>
- }
- ],
- "main": <bool>
- }
- ],
- "duration": <int>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "isrc": <str>,
- "properties": {
- "content": [<str>]
- }
- }
- ]
- }
- """
-
- if type and type not in \
- (TYPES := {"ALBUMS", "ARTISTS", "TRACKS", "VIDEOS"}):
- emsg = ("Invalid target search type. Valid values: "
- f"{', '.join(TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/search",
- params={
- "query": query,
- "countryCode": country_code,
- "type": type,
- "limit": limit,
- "offset": offset,
- "popularity": popularity
- }
- )
-
-
-
-
-[docs]
-class PrivateAPI:
-
- """
- Private TIDAL API client.
-
- The private TIDAL API allows media (tracks, videos), collections
- (albums, playlists), and performers to be queried, and information
- about them to be retrieved. As there is no available official
- documentation for the private TIDAL API, its endpoints have been
- determined by watching HTTP network traffic.
-
- .. attention::
-
- As the private TIDAL API is not designed to be publicly
- accessible, this class can be disabled or removed at any time to
- ensure compliance with the `TIDAL Developer Terms of Service
- <https://developer.tidal.com/documentation/guidelines
- /guidelines-developer-terms>`_.
-
- While authentication is not necessary to search for and retrieve
- data from public content, it is required to access personal content
- and stream media (with an active TIDAL subscription). In the latter
- case, requests to the private TIDAL API endpoints must be
- accompanied by a valid user access token in the header.
-
- Minim can obtain user access tokens via the authorization code with
- proof key for code exchange (PKCE) and device code flows. These
- OAuth 2.0 authorization flows require valid client credentials
- (client ID and client secret) to either be provided to this class's
- constructor as keyword arguments or be stored as
- :code:`TIDAL_PRIVATE_CLIENT_ID` and
- :code:`TIDAL_PRIVATE_CLIENT_SECRET` in the operating system's
- environment variables.
-
- .. hint::
-
- Client credentials can be extracted from the software you use to
- access TIDAL, including but not limited to the TIDAL Web Player
- and the Android, iOS, macOS, and Windows applications. Only the
- TIDAL Web Player and desktop application client credentials can
- be used without authorization.
-
- If an existing access token is available, it and its accompanying
- information (refresh token and expiry time) can be provided to this
- class's constructor as keyword arguments to bypass the access token
- retrieval process. It is recommended that all other
- authorization-related keyword arguments be specified so that a new
- access token can be obtained when the existing one expires.
-
- .. tip::
-
- The authorization flow and access token can be changed or updated
- at any time using :meth:`set_flow` and :meth:`set_access_token`,
- respectively.
-
- Minim also stores and manages access tokens and their properties.
- When an access token is acquired, it is automatically saved to the
- Minim configuration file to be loaded on the next instantiation of
- this class. This behavior can be disabled if there are any security
- concerns, like if the computer being used is a shared device.
-
- Parameters
- ----------
- client_id : `str`, keyword-only, optional
- Client ID. If it is not stored as
- :code:`TIDAL_PRIVATE_CLIENT_ID` in the operating system's
- environment variables or found in the Minim configuration file,
- it must be provided here.
-
- client_secret : `str`, keyword-only, optional
- Client secret. Required for the authorization code and device
- code flows. If it is not stored as
- :code:`TIDAL_PRIVATE_CLIENT_SECRET` in the operating system's
- environment variables or found in the Minim configuration file,
- it must be provided here.
-
- flow : `str`, keyword-only, optional
- Authorization flow. If not specified, no user authorization
- will be performed.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"pkce"` for the authorization code with proof key
- for code exchange (PKCE) flow.
- * :code:`"device_code"` for the device code flow.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is automatically opened for the
- authorization code with PKCE or device code flows. If
- :code:`False`, users will have to manually open the
- authorization URL, and for the authorization code flow, provide
- the full callback URI via the terminal. For the authorization
- code with PKCE flow, the Playwright framework by Microsoft is
- used.
-
- scopes : `str` or `list`, keyword-only, default: :code:`"r_usr"`
- Authorization scopes to request user access for in the OAuth 2.0
- flows.
-
- **Valid values**: :code:`"r_usr"`, :code:`"w_usr"`, and
- :code:`"w_sub"` (device code flow only).
-
- user_agent : `str`, keyword-only, optional
- User agent information to send in the header of HTTP requests.
-
- .. note::
-
- If not specified, TIDAL may temporarily block your IP address
- if you are making requests too quickly.
-
- access_token : `str`, keyword-only, optional
- Access token. If provided here or found in the Minim
- configuration file, the authorization process is bypassed. In
- the former case, all other relevant keyword arguments should be
- specified to automatically refresh the access token when it
- expires.
-
- refresh_token : `str`, keyword-only, optional
- Refresh token accompanying `access_token`. If not provided,
- the user will be reauthenticated using the specified
- authorization flow when `access_token` expires.
-
- expiry : `datetime.datetime` or `str`, keyword-only, optional
- Expiry time of `access_token` in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated using `refresh_token` (if available) or the
- specified authorization flow (if possible) when `access_token`
- expires.
-
- overwrite : `bool`, keyword-only, default: :code:`False`
- Determines whether to overwrite an existing access token in the
- Minim configuration file.
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether newly obtained access tokens and their
- associated properties are stored to the Minim configuration
- file.
-
- Attributes
- ----------
- API_URL : `str`
- Base URL for the private TIDAL API.
-
- AUTH_URL : `str`
- URL for device code requests.
-
- LOGIN_URL : `str`
- URL for authorization code requests.
-
- REDIRECT_URL : `str`
- URL for authorization code callbacks.
-
- RESOURCES_URL : `str`
- URL for cover art and image requests.
-
- TOKEN_URL : `str`
- URL for access token requests.
-
- WEB_URL : `str`
- URL for the TIDAL Web Player.
-
- session : `requests.Session`
- Session used to send requests to the private TIDAL API.
- """
-
- _FLOWS = {"pkce", "device_code"}
- _NAME = f"{__module__}.{__qualname__}"
-
- API_URL = "https://api.tidal.com"
- AUTH_URL = "https://auth.tidal.com/v1/oauth2"
- LOGIN_URL = "https://login.tidal.com"
- REDIRECT_URI = "tidal://login/auth"
- RESOURCES_URL = "http://resources.tidal.com"
- WEB_URL = "https://listen.tidal.com"
-
- def __init__(
- self, *, client_id: str = None, client_secret: str = None,
- flow: str = None, browser: bool = False,
- scopes: Union[str, list[str]] = "r_usr", user_agent: str = None,
- access_token: str = None, refresh_token: str = None,
- expiry: datetime.datetime = None, overwrite: bool = False,
- save: bool = True) -> None:
-
- """
- Create a private TIDAL API client.
- """
-
- self.session = requests.Session()
- if user_agent:
- self.session.headers["User-Agent"] = user_agent
-
- if (access_token is None and _config.has_section(self._NAME)
- and not overwrite):
- flow = _config.get(self._NAME, "flow")
- access_token = _config.get(self._NAME, "access_token")
- refresh_token = _config.get(self._NAME, "refresh_token")
- expiry = _config.get(self._NAME, "expiry")
- client_id = _config.get(self._NAME, "client_id")
- client_secret = _config.get(self._NAME, "client_secret")
- scopes = _config.get(self._NAME, "scopes")
-
- self.set_flow(flow, client_id=client_id, client_secret=client_secret,
- browser=browser, scopes=scopes, save=save)
- self.set_access_token(access_token, refresh_token=refresh_token,
- expiry=expiry)
-
- def _check_scope(
- self, endpoint: str, scope: str = None, *,
- flows: Union[str, list[set], set[str]] = None,
- require_authentication: bool = True) -> None:
-
- """
- Check if the user has granted the appropriate authorization
- scope for the desired endpoint.
-
- Parameters
- ----------
- endpoint : `str`
- Private TIDAL API endpoint.
-
- scope : `str`, optional
- Required scope for `endpoint`.
-
- flows : `str`, `list`, or `set`, keyword-only, optional
- Authorization flows for which `scope` is required. If not
- specified, `flows` defaults to all supported authorization
- flows.
-
- require_authentication : `bool`, keyword-only, default: :code:`True`
- Specifies whether the endpoint requires user authentication.
- Some endpoints can be used without authentication but require
- specific scopes when user authentication has been performed.
- """
-
- if flows is None:
- flows = self._FLOWS
-
- if require_authentication:
- if self._flow is None:
- emsg = (f"{self._NAME}.{endpoint}() requires user "
- "authentication.")
- elif self._flow in flows and scope and scope not in self._scopes:
- emsg = (f"{self._NAME}.{endpoint}() requires the '{scope}' "
- "authorization scope.")
- else:
- return
- elif self._flow in flows and scope and scope not in self._scopes:
- emsg = (f"{self._NAME}.{endpoint}() requires the '{scope}' "
- "authorization scope when user authentication has "
- f"been performed via the '{self._flow}' "
- "authorization flow.")
- else:
- return
- raise RuntimeError(emsg)
-
- def _get_authorization_code(self, code_challenge: str) -> str:
-
- """
- Get an authorization code to be exchanged for an access token in
- the authorization code flow.
-
- Parameters
- ----------
- code_challenge : `str`, optional
- Code challenge for the authorization code with PKCE flow.
-
- Returns
- -------
- auth_code : `str`
- Authorization code.
- """
-
- params = {
- "client_id": self._client_id,
- "code_challenge": code_challenge,
- "code_challenge_method": "S256",
- "redirect_uri": self.REDIRECT_URI,
- "response_type": "code",
- }
- if self._scopes:
- params["scope"] = self._scopes
- auth_url = (f"{self.LOGIN_URL}/authorize?"
- f"{urllib.parse.urlencode(params)}")
-
- if self._browser:
- har_file = DIR_TEMP / "minim_tidal_private.har"
-
- with sync_playwright() as playwright:
- browser = playwright.firefox.launch(headless=False)
- context = browser.new_context(
- locale="en-US",
- timezone_id="America/Los_Angeles",
- record_har_path=har_file,
- **playwright.devices["Desktop Firefox HiDPI"]
- )
- page = context.new_page()
- page.goto(auth_url, timeout=0)
- page.wait_for_url(f"{self.REDIRECT_URI}*",
- wait_until="commit")
- context.close()
- browser.close()
-
- with open(har_file, "r") as f:
- queries = dict(
- urllib.parse.parse_qsl(
- urllib.parse.urlparse(
- re.search(fr'{self.REDIRECT_URI}\?(.*?)"',
- f.read()).group(0)
- ).query
- )
- )
- har_file.unlink()
-
- else:
- print("To grant Minim access to TIDAL data and features, "
- "open the following link in your web browser:\n\n"
- f"{auth_url}\n")
- uri = input("After authorizing Minim to access TIDAL on "
- "your behalf, copy and paste the URI beginning "
- f"with '{self.REDIRECT_URI}' below.\n\nURI: ")
- queries = dict(
- urllib.parse.parse_qsl(urllib.parse.urlparse(uri).query)
- )
-
- if "code" not in queries:
- raise RuntimeError("Authorization failed.")
- return queries["code"]
-
- def _get_country_code(self, country_code: str = None) -> str:
-
- """
- Get the ISO 3166-1 alpha-2 country code to use for requests.
-
- Parameters
- ----------
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- country_code : `str`
- ISO 3166-1 alpha-2 country code.
- """
-
- return country_code or getattr(self, "_country_code", None) \
- or self.get_country_code()
-
- def _get_json(self, url: str, **kwargs) -> dict:
-
- """
- Send a GET request and return the JSON-encoded content of the
- response.
-
- Parameters
- ----------
- url : `str`
- URL for the GET request.
-
- **kwargs
- Keyword arguments to pass to :meth:`requests.request`.
-
- Returns
- -------
- resp : `dict`
- JSON-encoded content of the response.
- """
-
- return self._request("get", url, **kwargs).json()
-
- def _refresh_access_token(self) -> None:
-
- """
- Refresh the expired excess token.
- """
-
- if self._flow is None or not self._refresh_token \
- or not self._client_id \
- or (self._flow == "device_code" and not self._client_secret):
- self.set_access_token()
- else:
- r = requests.post(
- f"{self.LOGIN_URL}/oauth2/token",
- data={
- "client_id": self._client_id,
- "client_secret": self._client_secret,
- "grant_type": "refresh_token",
- "refresh_token": self._refresh_token
- },
- ).json()
-
- self.session.headers["Authorization"] = f"Bearer {r['access_token']}"
- self._expiry = (datetime.datetime.now()
- + datetime.timedelta(0, r["expires_in"]))
- self._scopes = r["scope"]
-
- if self._save:
- _config[self._NAME].update({
- "access_token": r["access_token"],
- "expiry": self._expiry.strftime("%Y-%m-%dT%H:%M:%SZ"),
- "scopes": self._scopes
- })
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- def _request(
- self, method: str, url: str, retry: bool = True, **kwargs
- ) -> requests.Response:
-
- """
- Construct and send a request with status code checking.
-
- Parameters
- ----------
- method : `str`
- Method for the request.
-
- url : `str`
- URL for the request.
-
- retry : `bool`
- Specifies whether to retry the request if the response has
- a non-2xx status code.
-
- **kwargs
- Keyword arguments passed to :meth:`requests.request`.
-
- Returns
- -------
- resp : `requests.Response`
- Response to the request.
- """
-
- if self._expiry is not None and datetime.datetime.now() > self._expiry:
- self._refresh_access_token()
-
- r = self.session.request(method, url, **kwargs)
- if r.status_code not in range(200, 299):
- if r.text:
- error = r.json()
- substatus = (error["subStatus"] if "subStatus" in error
- else error["sub_status"] if "sub_status" in error
- else "")
- description = (error["userMessage"] if "userMessage" in error
- else error["description"] if "description" in error
- else error["error_description"] if "error_description" in error
- else "")
- emsg = f"{r.status_code}"
- if substatus:
- emsg += f".{substatus}"
- emsg += f" {description}"
- else:
- emsg = f"{r.status_code} {r.reason}"
- if r.status_code == 401 and substatus == 11003 and retry:
- logging.warning(emsg)
- self._refresh_access_token()
- return self._request(method, url, False, **kwargs)
- else:
- raise RuntimeError(emsg)
- return r
-
-
-[docs]
- def set_access_token(
- self, access_token: str = None, *, refresh_token: str = None,
- expiry: Union[str, datetime.datetime] = None) -> None:
-
- """
- Set the private TIDAL API access token.
-
- Parameters
- ----------
- access_token : `str`, optional
- Access token. If not provided, an access token is obtained
- using an OAuth 2.0 authorization flow or from the Spotify
- Web Player.
-
- refresh_token : `str`, keyword-only, optional
- Refresh token accompanying `access_token`.
-
- expiry : `str` or `datetime.datetime`, keyword-only, optional
- Access token expiry timestamp in the ISO 8601 format
- :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be
- reauthenticated using the refresh token (if available) or
- the default authorization flow (if possible) when
- `access_token` expires.
- """
-
- if access_token is None:
- if self._flow is None:
- self._expiry = datetime.datetime.max
- return
- else:
- if not self._client_id:
- emsg = "Private TIDAL API client ID not provided."
- raise ValueError(emsg)
-
- if self._flow == "pkce":
- data = {
- "client_id": self._client_id,
- "code_verifier": secrets.token_urlsafe(32),
- "grant_type": "authorization_code",
- "redirect_uri": self.REDIRECT_URI,
- "scope": self._scopes
- }
- data["code"] = self._get_authorization_code(
- base64.urlsafe_b64encode(
- hashlib.sha256(
- data["code_verifier"].encode()
- ).digest()
- ).decode().rstrip("=")
- )
- r = requests.post(f"{self.LOGIN_URL}/oauth2/token",
- json=data).json()
- elif self._flow == "device_code":
- if not self._client_id:
- emsg = "Private TIDAL API client secret not provided."
- raise ValueError(emsg)
-
- data = {"client_id": self._client_id}
- if self._scopes:
- data["scope"] = self._scopes
- r = requests.post(f"{self.AUTH_URL}/device_authorization",
- data=data).json()
- if "error" in r:
- emsg = (f"{r['status']}.{r['sub_status']} "
- f"{r['error_description']}")
- raise ValueError(emsg)
- data["device_code"] = r["deviceCode"]
- data["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code"
-
- verification_uri = f"http://{r['verificationUriComplete']}"
- if self._browser:
- webbrowser.open(verification_uri)
- else:
- print("To grant Minim access to TIDAL data and "
- "features, open the following link in "
- f"your web browser:\n\n{verification_uri}\n")
- while True:
- time.sleep(2)
- r = requests.post(
- f"{self.AUTH_URL}/token",
- auth=(self._client_id, self._client_secret),
- data=data
- ).json()
- if "error" not in r:
- break
- elif r["error"] != "authorization_pending":
- raise RuntimeError(f"{r['status']}.{r['sub_status']} "
- f"{r['error_description']}")
- access_token = r["access_token"]
- refresh_token = r["refresh_token"]
- expiry = (datetime.datetime.now()
- + datetime.timedelta(0, r["expires_in"]))
-
- if self._save:
- _config[self._NAME] = {
- "flow": self._flow,
- "client_id": self._client_id,
- "access_token": access_token,
- "refresh_token": refresh_token,
- "expiry": expiry.strftime("%Y-%m-%dT%H:%M:%SZ"),
- "scopes": self._scopes
- }
- if hasattr(self, "_client_secret"):
- _config[self._NAME]["client_secret"] \
- = self._client_secret
- with open(DIR_HOME / "minim.cfg", "w") as f:
- _config.write(f)
-
- if len(access_token) == 16:
- self.session.headers["x-tidal-token"] = access_token
- self._refresh_token = self._expiry = None
- else:
- self.session.headers["Authorization"] = f"Bearer {access_token}"
- self._refresh_token = refresh_token
- self._expiry = (
- datetime.datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ")
- if isinstance(expiry, str) else expiry
- )
-
- if self._flow is not None:
- me = self.get_profile()
- self._country_code = me["countryCode"]
- self._user_id = me["userId"]
-
-
-
-[docs]
- def set_flow(
- self, flow: str, client_id: str, *, client_secret: str = None,
- browser: bool = False, scopes: Union[str, list[str]] = "",
- save: bool = True) -> None:
-
- """
- Set the authorization flow.
-
- Parameters
- ----------
- flow : `str`
- Authorization flow. If not specified, no user authentication
- will be performed.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"pkce"` for the authorization code with proof
- key for code exchange (PKCE) flow.
- * :code:`"client_credentials"` for the client credentials
- flow.
-
- client_id : `str`
- Client ID.
-
- client_secret : `str`, keyword-only, optional
- Client secret. Required for all OAuth 2.0 authorization
- flows.
-
- browser : `bool`, keyword-only, default: :code:`False`
- Determines whether a web browser is automatically opened for
- the authorization code with PKCE or device code flows. If
- :code:`False`, users will have to manually open the
- authorization URL, and for the authorization code flow,
- provide the full callback URI via the terminal. For the
- authorization code with PKCE flow, the Playwright framework
- by Microsoft is used.
-
- scopes : `str` or `list`, keyword-only, optional
- Authorization scopes to request user access for in the OAuth
- 2.0 flows.
-
- **Valid values**: :code:`"r_usr"`, :code:`"w_usr"`, and
- :code:`"w_sub"` (device code flow only).
-
- save : `bool`, keyword-only, default: :code:`True`
- Determines whether to save the newly obtained access tokens
- and their associated properties to the Minim configuration
- file.
- """
-
- if flow and flow not in self._FLOWS:
- emsg = (f"Invalid authorization flow ({flow=}). "
- f"Valid values: {', '.join(self._FLOWS)}.")
- raise ValueError(emsg)
-
- self._flow = flow
- self._save = save
- self._client_id = client_id or os.environ.get("TIDAL_PRIVATE_CLIENT_ID")
-
- if flow:
- if "x-tidal-token" in self.session.headers:
- del self.session.headers["x-tidal-token"]
-
- self._browser = browser
- if flow == "pkce" and browser and not FOUND_PLAYWRIGHT:
- self._browser = False
- wmsg = ("The Playwright web framework was not found, "
- "so automatic authorization code retrieval is "
- "not available.")
- warnings.warn(wmsg)
-
- self._client_secret = (client_secret
- or os.environ.get("TIDAL_PRIVATE_CLIENT_SECRET"))
- self._scopes = " ".join(scopes) if isinstance(scopes, list) \
- else scopes
- else:
- self.session.headers["x-tidal-token"] = self._client_id
- self._scopes = ""
-
-
- ### ALBUMS ################################################################
-
-
-[docs]
- def get_album(
- self, album_id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for an album.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Example**: :code:`251380836`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- album : `dict`
- TIDAL catalog information for an album.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "premiumStreamingOnly": <bool>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "numberOfVolumes": <int>,
- "releaseDate": <str>,
- "copyright": <str>,
- "type": "ALBUM",
- "version": <str>,
- "url": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>,
- "explicit": <bool>,
- "upc": <str>,
- "popularity": <int>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ]
- }
- """
-
- self._check_scope("get_album", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/albums/{album_id}",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_album_items(
- self, album_id: Union[int, str], country_code: str = None, *,
- limit: int = 100, offset: int = None, credits: bool = False
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for items (tracks and videos) in
- an album.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Examples**: :code:`251380836`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- credits : `bool`, keyword-only, default: :code:`False`
- Determines whether credits for each item is returned.
-
- Returns
- -------
- items : `dict`
- A dictionary containing TIDAL catalog information for
- tracks and videos in the specified album and metadata for
- the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "item": {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": >int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- },
- "type": "track"
- }
- ]
- }
- """
-
- self._check_scope("get_album_items", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- url = f"{self.API_URL}/v1/albums/{album_id}/items"
- if credits:
- url += "/credits"
- return self._get_json(
- url,
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_album_credits(
- self, album_id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get credits for an album.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Example**: :code:`251380836`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- credits : `dict`
- A dictionary containing TIDAL catalog information for the
- album contributors.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "type": <str>,
- "contributors": [
- {
- "name": <str>
- }
- ]
- }
- ]
- """
-
- self._check_scope("get_album_credits", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/albums/{album_id}/credits",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_album_review(
- self, album_id: Union[int, str], country_code: str = None
- ) -> dict[str, str]:
-
- """
- Get a review of or a synopsis for an album.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Example**: :code:`251380836`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- review : `dict`
- A dictionary containing a review of or a synopsis for an
- album and its source.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "source": <str>,
- "lastUpdated": <str>,
- "text": <str>,
- "summary": <str>
- }
- """
-
- self._check_scope("get_album_review", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/albums/{album_id}/review",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_similar_albums(
- self, album_id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for albums similar to the
- specified album.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Example**: :code:`251380836`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- album : `dict`
- TIDAL catalog information for an album.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "premiumStreamingOnly": <bool>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "numberOfVolumes": <int>,
- "releaseDate": <str>,
- "copyright": <str>,
- "type": "ALBUM",
- "version": <str>,
- "url": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>,
- "explicit": <bool>,
- "upc": <str>,
- "popularity": <int>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ]
- }
- ],
- "source": <str>
- }
- """
-
- self._check_scope("get_similar_albums", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/albums/{album_id}/similar",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_favorite_albums(
- self, country_code: str = None, *, limit: int = 50,
- offset: int = None, order: str = "DATE",
- order_direction: str = "DESC") -> None:
-
- """
- Get TIDAL catalog information for albums in the current user's
- collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- order : `str`, keyword-only, default: :code:`"DATE"`
- Sorting order.
-
- **Valid values**: :code:`"DATE"` and :code:`"NAME"`.
-
- order_direction : `str`, keyword-only, default: :code:`"DESC"`
- Sorting order direction.
-
- **Valid values**: :code:`"DESC"` and :code:`"ASC"`.
-
- Returns
- -------
- albums : `dict`
- A dictionary containing TIDAL catalog information for albums
- in the current user's collection and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "created": <str>,
- "item": {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "premiumStreamingOnly": <bool>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "numberOfVolumes": <int>,
- "releaseDate": <str>,
- "copyright": <str>,
- "type": "ALBUM",
- "version": <str>,
- "url": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>,
- "explicit": <bool>,
- "upc": <str>,
- "popularity": <int>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ]
- }
- }
- ]
- }
- """
-
- self._check_scope("get_favorite_albums", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/albums",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset,
- "order": order,
- "orderDirection": order_direction,
- }
- )
-
-
-
-[docs]
- def favorite_albums(
- self, album_ids: Union[int, str, list[Union[int, str]]],
- country_code: str = None, *, on_artifact_not_found: str = "FAIL"
- ) -> None:
-
- """
- Add albums to the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- album_ids : `int`, `str`, or `list`
- TIDAL album ID(s).
-
- **Examples**: :code:`"251380836,275646830"` or
- :code:`[251380836, 275646830]`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added does not exist.
-
- **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`.
- """
-
- self._check_scope("favorite_albums", "r_usr", flows={"device_code"})
-
- self._request(
- "post",
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/albums",
- params={"countryCode": self._get_country_code(country_code)},
- data={
- "albumIds": ",".join(map(str, album_ids))
- if isinstance(album_ids, list) else album_ids,
- "onArtifactNotFound": on_artifact_not_found
- }
- )
-
-
-
-[docs]
- def unfavorite_albums(
- self, album_ids: Union[int, str, list[Union[int, str]]]) -> None:
-
- """
- Remove albums from the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- album_ids : `int`, `str`, or `list`
- TIDAL album ID(s).
-
- **Examples**: :code:`"251380836,275646830"` or
- :code:`[251380836, 275646830]`.
- """
-
- self._check_scope("unfavorite_albums", "r_usr", flows={"device_code"})
-
- if isinstance(album_ids, list):
- album_ids = ",".join(map(str, album_ids))
- self._request("delete",
- f"{self.API_URL}/v1/users/{self._user_id}"
- f"/favorites/albums/{album_ids}")
-
-
- ### ARTISTS ###############################################################
-
-
-[docs]
- def get_artist(
- self, artist_id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for an artist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- artist : `dict`
- TIDAL catalog information for an artist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "name": <str>,
- "artistTypes": [<str>],
- "url": <str>,
- "picture": <str>,
- "popularity": <int>,
- "artistRoles": [
- {
- "categoryId": <int>,
- "category": <str>
- }
- ],
- "mixes": {
- "ARTIST_MIX": <str>
- }
- }
- """
-
- self._check_scope("get_artist", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_artist_albums(
- self, artist_id: Union[int, str], country_code: str = None, *,
- filter: str = None, limit: int = 100, offset: int = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for albums by an artist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- filter : `str`, keyword-only, optional
- Subset of albums to retrieve.
-
- **Valid values**: :code:`"EPSANDSINGLES"` and
- :code:`"COMPILATIONS"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- albums : `dict`
- A dictionary containing TIDAL catalog information for
- albums by the specified artist and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "premiumStreamingOnly": <bool>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "numberOfVolumes": <int>,
- "releaseDate": <str>,
- "copyright": <str>,
- "type": "ALBUM",
- "version": <str>,
- "url": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>,
- "explicit": <bool>,
- "upc": <str>,
- "popularity": <int>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ]
- }
- ]
- }
- """
-
- self._check_scope("get_artist_albums", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/albums",
- params={
- "countryCode": self._get_country_code(country_code),
- "filter": filter,
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_artist_top_tracks(
- self, artist_id: Union[int, str], country_code: str = None, *,
- limit: int = 100, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for an artist's top tracks.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing TIDAL catalog information for the
- artist's top tracks and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_artist_top_tracks", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/toptracks",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_artist_videos(
- self, artist_id: Union[int, str], country_code: str = None, *,
- limit: int = 100, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for an artist's videos.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- videos : `dict`
- A dictionary containing TIDAL catalog information for the
- artist's videos and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "volumeNumber": <int>,
- "trackNumber": <int>,
- "releaseDate": <str>,
- "imagePath": <str>,
- "imageId": <str>,
- "vibrantColor": <str>,
- "duration": <int>,
- "quality": <str>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "explicit": <bool>,
- "popularity": <int>,
- "type": "Music Video",
- "adsUrl": <str>,
- "adsPrePaywallOnly": <bool>,
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": <dict>
- }
- ]
- }
- """
-
- self._check_scope("get_artist_videos", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/videos",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_artist_mix_id(
- self, artist_id: Union[int, str], country_code: str = None) -> str:
-
- """
- Get the ID of a curated mix of tracks based on an artist's
- works.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- mix_id : `str`
- TIDAL mix ID.
-
- **Example**: :code:`"000ec0b01da1ddd752ec5dee553d48"`.
- """
-
- self._check_scope("get_artist_mix_id", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/mix",
- params={"countryCode": self._get_country_code(country_code)}
- )["id"]
-
-
-
-[docs]
- def get_artist_radio(
- self, artist_id: Union[int, str], country_code: str = None, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for tracks inspired by an artist's
- works.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- .. note::
-
- This method is functionally identical to first getting the
- artist mix ID using :meth:`get_artist_mix_id` and then
- retrieving TIDAL catalog information for the items in the mix
- using :meth:`get_mix_items`.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Default**: :code:`100`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing TIDAL catalog information for tracks
- inspired by an artist's works and metadata for the returned
- results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_artist_radio", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/radio",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_artist_biography(
- self, artist_id: Union[int, str], country_code: str = None
- ) -> dict[str, str]:
-
- """
- Get an artist's biographical information.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- biography : `dict`
- A dictionary containing an artist's biographical information
- and its source.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "source": <str>,
- "lastUpdated": <str>,
- "text": <str>,
- "summary": <str>
- }
- """
-
- self._check_scope("get_artist_biography", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/bio",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_artist_links(
- self, artist_id: Union[int, str], country_code: str = None, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get links to websites associated with an artist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- links : `dict`
- A dictionary containing the artist's links and metadata for
- the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "url": <str>,
- "siteName": <str>
- }
- ],
- "source": <str>
- }
- """
-
- self._check_scope("get_artist_links", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/links",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_similar_artists(
- self, artist_id: str, country_code: str = None, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for artists similar to a specified
- artist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- artists : `dict`
- A dictionary containing TIDAL catalog information for
- artists similar to the specified artist and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "name": <str>,
- "type": None,
- "artistTypes": [<str>],
- "url": <str>,
- "picture": <str>,
- "popularity": <int>,
- "banner": <str>,
- "artistRoles": <list>,
- "mixes": <dict>,
- "relationType": "SIMILAR_ARTIST"
- }
- ],
- "source": "TIDAL"
- }
- """
-
- self._check_scope("get_similar_artists", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/artists/{artist_id}/similar",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_favorite_artists(
- self, country_code: str = None, *, limit: int = 50,
- offset: int = None, order: str = "DATE",
- order_direction: str = "DESC") -> None:
-
- """
- Get TIDAL catalog information for artists in the current user's
- collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- order : `str`, keyword-only, default: :code:`"DATE"`
- Sorting order.
-
- **Valid values**: :code:`"DATE"` and :code:`"NAME"`.
-
- order_direction : `str`, keyword-only, default: :code:`"DESC"`
- Sorting order direction.
-
- **Valid values**: :code:`"DESC"` and :code:`"ASC"`.
-
- Returns
- -------
- artists : `dict`
- A dictionary containing TIDAL catalog information for
- artists in the current user's collection and metadata for
- the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "created": <str>,
- "item": {
- "id": <int>,
- "name": <str>,
- "artistTypes": [<str>],
- "url": <str>,
- "picture": <str>,
- "popularity": <int>,
- "artistRoles": [
- {
- "categoryId": <int>,
- "category": <str>
- }
- ],
- "mixes": {
- "ARTIST_MIX": <str>
- }
- }
- }
- ]
- }
- """
-
- self._check_scope("get_favorite_artists", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/artists",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset,
- "order": order,
- "orderDirection": order_direction
- }
- )
-
-
-
-[docs]
- def favorite_artists(
- self, artist_ids: Union[int, str, list[Union[int, str]]],
- country_code: str = None, *, on_artifact_not_found: str = "FAIL"
- ) -> None:
-
- """
- Add artists to the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- artist_ids : `int`, `str`, or `list`
- TIDAL artist ID(s).
-
- **Examples**: :code:`"1566,7804"` or :code:`[1566, 7804]`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added does not exist.
-
- **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`.
- """
-
- self._check_scope("favorite_artists", "r_usr", flows={"device_code"})
-
- self._request(
- "post",
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/artists",
- params={"countryCode": self._get_country_code(country_code)},
- data={
- "artistIds": ",".join(map(str, artist_ids))
- if isinstance(artist_ids, list) else artist_ids,
- "onArtifactNotFound": on_artifact_not_found
- }
- )
-
-
-
-[docs]
- def unfavorite_artists(
- self, artist_ids: Union[int, str, list[Union[int, str]]]) -> None:
-
- """
- Remove artists from the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- artist_ids : `int`, `str`, or `list`
- TIDAL artist ID(s).
-
- **Examples**: :code:`"1566,7804"` or :code:`[1566, 7804]`.
- """
-
- self._check_scope("unfavorite_artists", "r_usr", flows={"device_code"})
-
- if isinstance(artist_ids, list):
- artist_ids = ",".join(map(str, artist_ids))
- self._request("delete",
- f"{self.API_URL}/v1/users/{self._user_id}"
- f"/favorites/artists/{artist_ids}")
-
-
-
-[docs]
- def get_blocked_artists(
- self, *, limit: int = 50, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for the current user's blocked
- artists.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- artists : `dict`
- A dictionary containing TIDAL catalog information for the
- the current user's blocked artists and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "item": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "artistTypes": [<str>],
- "url": <str>,
- "picture": <str>,
- "popularity": <int>,
- "banner": <str>,
- "artistRoles": [
- {
- "categoryId": <int>,
- "category": <str>
- }
- ],
- "mixes": {
- "ARTIST_MIX": <str>
- }
- },
- "created": <str>,
- "type": "ARTIST"
- }
- ]
- }
- """
-
- self._check_scope("get_blocked_artists", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/blocks/artists",
- params={"limit": limit, "offset": offset}
- )
-
-
-
-[docs]
- def block_artist(self, artist_id: Union[int, str]) -> None:
-
- """
- Block an artist from appearing in mixes and the radio.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
- """
-
- self._check_scope("block_artist", "r_usr", flows={"device_code"})
-
- self._request(
- "post",
- f"{self.API_URL}/v1/users/{self._user_id}/blocks/artists",
- data={"artistId": artist_id}
- )
-
-
-
-[docs]
- def unblock_artist(self, artist_id: Union[int, str]) -> None:
-
- """
- Unblock an artist from appearing in mixes and the radio.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
- """
-
- self._check_scope("unblock_artist", "r_usr", flows={"device_code"})
-
- self._request("delete",
- f"{self.API_URL}/v1/users/{self._user_id}"
- f"/blocks/artists/{artist_id}")
-
-
- ### COUNTRY ###############################################################
-
-
-[docs]
- def get_country_code(self) -> str:
-
- """
- Get the country code based on the current IP address.
-
- Returns
- -------
- country : `str`, keyword-only, optional
- ISO 3166-1 alpha-2 country code.
-
- **Example**: :code:`"US"`.
- """
-
- return self._get_json(f"{self.API_URL}/v1/country")["countryCode"]
-
-
- ### IMAGES ################################################################
-
-
-[docs]
- def get_image(
- self, uuid: str, type: str = None, animated: bool = False, *,
- width: int = None, height: int = None,
- filename: Union[str, pathlib.Path] = None) -> bytes:
-
- """
- Get (animated) cover art or image for a TIDAL item.
-
- .. note::
-
- This method is provided for convenience and is not a private
- TIDAL API endpoint.
-
- Parameters
- ----------
- uuid : `str`
- Image UUID.
-
- **Example**: :code:`"d3c4372b-a652-40e0-bdb1-fc8d032708f6"`.
-
- type : `str`
- Item type.
-
- **Valid values**: :code:`"artist"`, :code:`"album"`,
- :code:`"playlist"`, :code:`"track"`, :code:`"userProfile"`,
- and :code:`"video"`.
-
- animated : `bool`, default: :code:`False`
- Specifies whether the image is animated.
-
- width : `int`, keyword-only, optional
- Valid image width for the item type. If not specified, the
- default size for the item type is used.
-
- height : `int`, keyword-only, optional
- Valid image height for the item type. If not specified, the
- default size for the item type is used.
-
- filename : `str` or `pathlib.Path`, keyword-only, optional
- Filename with the :code:`.jpg` or :code:`.mp4` extension. If
- specified, the image is saved to a file instead.
-
- Returns
- -------
- image : `bytes`
- Image data. If :code:`save=True`, the stream data is saved
- to an image or video file and its filename is returned
- instead.
- """
-
- IMAGE_SIZES = {
- "artist": (750, 750),
- "album": (1280, 1280),
- "playlist": (1080, 1080),
- "track": (1280, 1280),
- "userProfile": (1080, 1080),
- "video": (640, 360)
- }
-
- if width is None or height is None:
- if type and type in IMAGE_SIZES.keys():
- width, height = IMAGE_SIZES[type.lower()]
- else:
- emsg = ("Either the image dimensions or a valid item "
- "type must be specified.")
- raise ValueError(emsg)
-
- if animated:
- extension = ".mp4"
- media_type = "videos"
- else:
- extension = ".jpg"
- media_type = "images"
-
- with self.session.get(f"{self.RESOURCES_URL}/{media_type}"
- f"/{uuid.replace('-', '/')}"
- f"/{width}x{height}.{extension}") as r:
- image = r.content
-
- if filename:
- if not isinstance(filename, pathlib.Path):
- filename = pathlib.Path(filename)
- if not filename.name.endswith(extension):
- filename += extension
- with open(filename, "wb") as f:
- f.write(image)
- else:
- return image
-
-
- ### MIXES #################################################################
-
-
-[docs]
- def get_mix_items(
- self, mix_id: str, country_code: str = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for items (tracks and videos) in
- a mix.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- mix_id : `str`
- TIDAL mix ID.
-
- **Example**: :code:`"000ec0b01da1ddd752ec5dee553d48"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing TIDAL catalog information for
- tracks and videos in the specified mix and metadata for
- the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "item": {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": >int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- },
- "type": "track"
- }
- ]
- }
- """
-
- self._check_scope("get_mix_items", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/mixes/{mix_id}/items",
- params={"countryCode": self._get_country_code(country_code)})
-
-
-
-[docs]
- def get_favorite_mixes(
- self, *, ids: bool = False, limit: int = 50, cursor: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for or IDs of mixes in the
- current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- ids : `bool`, keyword-only, default: :code:`False`
- Determine whether TIDAL catalog information about the mixes
- (:code:`False`) or the mix IDs (:code:`True`) are
- returned.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- cursor : `str`, keyword-only, optional
- Cursor position of the last item in previous search results.
- Use with `limit` to get the next page of search results.
-
- Returns
- -------
- mixes : `dict`
- A dictionary containing the TIDAL catalog information for or
- IDs of the mixes in the current user's collection and the
- cursor position.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "items": [
- {
- "dateAdded": <str>,
- "title": <str>,
- "id": <str>,
- "mixType": <str>,
- "updated": <str>,
- "subTitleTextInfo": {
- "text": <str>,
- "color": <str>
- },
- "images": {
- "SMALL": {
- "width": <int>,
- "height": <int>,
- "url": <str>
- },
- "MEDIUM": {
- "width": <int>,
- "height": <int>,
- "url": <str>
- },
- "LARGE": {
- "width": <int>,
- "height": <int>,
- "url": <str>
- },
- },
- "detailImages": {
- "SMALL": {
- "width": <int>,
- "height": <int>,
- "url": <str>
- },
- "MEDIUM": {
- "width": <int>,
- "height": <int>,
- "url": <str>
- },
- "LARGE": {
- "width": <int>,
- "height": <int>,
- "url": <str>
- }
- },
- "master": <bool>,
- "subTitle": <str>,
- "titleTextInfo": {
- "text": <str>,
- "color": <str>
- }
- }
- ],
- "cursor": <str>,
- "lastModifiedAt": <str>
- }
- """
-
- self._check_scope("get_favorite_mixes", "r_usr", flows={"device_code"})
-
- url = f"{self.API_URL}/v2/favorites/mixes"
- if ids:
- url += "/ids"
- return self._get_json(url, params={"limit": limit, "cursor": cursor})
-
-
-
-[docs]
- def favorite_mixes(
- self, mix_ids: Union[str, list[str]], *,
- on_artifact_not_found: str = "FAIL") -> None:
-
- """
- Add mixes to the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- mix_ids : `str` or `list`
- TIDAL mix ID(s).
-
- **Examples**: :code:`"000ec0b01da1ddd752ec5dee553d48,\
- 000dd748ceabd5508947c6a5d3880a"` or
- :code:`["000ec0b01da1ddd752ec5dee553d48",
- "000dd748ceabd5508947c6a5d3880a"]`
-
- on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added does not exist.
-
- **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`.
- """
-
- self._check_scope("favorite_mixes", "r_usr", flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/favorites/mixes/add",
- data={
- "mixIds": mix_ids,
- "onArtifactNotFound": on_artifact_not_found
- }
- )
-
-
-
-[docs]
- def unfavorite_mixes(self, mix_ids: Union[str, list[str]]) -> None:
-
- """
- Remove mixes from the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- mix_ids : `str` or `list`
- TIDAL mix ID(s).
-
- **Examples**: :code:`"000ec0b01da1ddd752ec5dee553d48,\
- 000dd748ceabd5508947c6a5d3880a"` or
- :code:`["000ec0b01da1ddd752ec5dee553d48",
- "000dd748ceabd5508947c6a5d3880a"]`
- """
-
- self._check_scope("unfavorite_mixes", "r_usr", flows={"device_code"})
-
- self._request("put", f"{self.API_URL}/v2/favorites/mixes/remove",
- data={"mixIds": mix_ids})
-
-
- ### PAGES #################################################################
-
-
-[docs]
- def get_album_page(
- self, album_id: Union[int, str], country_code: str = None,
- *, device_type: str = "BROWSER") -> dict[str, Any]:
-
- """
- Get the TIDAL page for an album.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- album_id : `int` or `str`
- TIDAL album ID.
-
- **Example**: :code:`251380836`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- device_type : `str`, keyword-only, default: :code:`"BROWSER"`
- Device type.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"BROWSER"` for a web browser.
- * :code:`"DESKTOP"` for the desktop TIDAL application.
- * :code:`"PHONE"` for the mobile TIDAL application.
- * :code:`"TV"` for the smart TV TIDAL application.
-
- Returns
- -------
- page : `dict`
- A dictionary containing the page ID, title, and submodules.
- """
-
- self._check_scope("get_album_page", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- if device_type not in \
- (DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"}):
- emsg = ("Invalid device type. Valid values: "
- f"{', '.join(DEVICE_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/v1/pages/album",
- params={
- "albumId": album_id,
- "countryCode": self._get_country_code(country_code),
- "deviceType": device_type,
- }
- )
-
-
-
-[docs]
- def get_artist_page(
- self, artist_id: Union[int, str], country_code: str = None,
- *, device_type: str = "BROWSER") -> dict[str, Any]:
-
- """
- Get the TIDAL page for an artist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- artist_id : `int` or `str`
- TIDAL artist ID.
-
- **Example**: :code:`1566`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- device_type : `str`, keyword-only, default: :code:`"BROWSER"`
- Device type.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"BROWSER"` for a web browser.
- * :code:`"DESKTOP"` for the desktop TIDAL application.
- * :code:`"PHONE"` for the mobile TIDAL application.
- * :code:`"TV"` for the smart TV TIDAL application.
-
- Returns
- -------
- page : `dict`
- A dictionary containing the page ID, title, and submodules.
- """
-
- self._check_scope("get_artist_page", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- if device_type not in \
- (DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"}):
- emsg = ("Invalid device type. Valid values: "
- f"{', '.join(DEVICE_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/v1/pages/artist",
- params={
- "artistID": artist_id,
- "countryCode": self._get_country_code(country_code),
- "deviceType": device_type
- }
- )
-
-
-
-[docs]
- def get_mix_page(
- self, mix_id: str, country_code: str = None,
- *, device_type: str = "BROWSER") -> dict[str, Any]:
-
- """
- Get the TIDAL page for a mix.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- mix_id : `str`
- TIDAL mix ID.
-
- **Example**: :code:`"000ec0b01da1ddd752ec5dee553d48"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- device_type : `str`, keyword-only, default: :code:`"BROWSER"`
- Device type.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"BROWSER"` for a web browser.
- * :code:`"DESKTOP"` for the desktop TIDAL application.
- * :code:`"PHONE"` for the mobile TIDAL application.
- * :code:`"TV"` for the smart TV TIDAL application.
-
- Returns
- -------
- page : `dict`
- A dictionary containing the page ID, title, and submodules.
- """
-
- self._check_scope("get_mix_page", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- if device_type not in \
- (DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"}):
- emsg = ("Invalid device type. Valid values: "
- f"{', '.join(DEVICE_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/v1/pages/mix",
- params={
- "mixId": mix_id,
- "countryCode": self._get_country_code(country_code),
- "deviceType": device_type,
- }
- )
-
-
-
-[docs]
- def get_video_page(
- self, video_id: Union[int, str], country_code: str = None,
- *, device_type: str = "BROWSER") -> dict[str, Any]:
-
- """
- Get the TIDAL page for a video.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- video_id : `int` or `str`
- TIDAL video ID.
-
- **Example**: :code:`75623239`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- device_type : `str`, keyword-only, default: :code:`"BROWSER"`
- Device type.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"BROWSER"` for a web browser.
- * :code:`"DESKTOP"` for the desktop TIDAL application.
- * :code:`"PHONE"` for the mobile TIDAL application.
- * :code:`"TV"` for the smart TV TIDAL application.
-
- Returns
- -------
- page : `dict`
- A dictionary containing the page ID, title, and submodules.
- """
-
- self._check_scope("get_video_page", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- if device_type not in \
- (DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"}):
- emsg = ("Invalid device type. Valid values: "
- f"{', '.join(DEVICE_TYPES)}.")
- raise ValueError(emsg)
-
- return self._get_json(
- f"{self.API_URL}/v1/pages/videos",
- params={
- "videoId": video_id,
- "countryCode": self._get_country_code(country_code),
- "deviceType": device_type
- }
- )
-
-
- ### PLAYLISTS #############################################################
-
-
-[docs]
- def get_playlist(
- self, playlist_uuid: str, country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for a playlist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- playlist : `dict`
- TIDAL catalog information for a playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "uuid": <str>,
- "title": <str>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "creator": {
- "id": <int>
- },
- "description": <str>,
- "duration": <int>,
- "lastUpdated": <str>,
- "created": <str>,
- "type": <str>,
- "publicPlaylist": <bool>,
- "url": <str>,
- "image": <str>,
- "popularity": <int>,
- "squareImage": <str>,
- "promotedArtists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>,
- }
- ],
- "lastItemAddedAt": <str>
- }
- """
-
- self._check_scope("get_playlist", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/playlists/{playlist_uuid}",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_playlist_etag(
- self, playlist_uuid: str, country_code: str = None) -> str:
-
- """
- Get the entity tag (ETag) for a playlist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- .. note::
-
- This method is provided for convenience and is not a private
- TIDAL API endpoint.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- etag : `str`
- ETag for a playlist.
-
- **Example**: :code:`"1698984074453"`.
- """
-
- self._check_scope("get_playlist_etag", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- r = self._request(
- "get",
- f"{self.API_URL}/v1/playlists/{playlist_uuid}",
- params={"countryCode": self._get_country_code(country_code)}
- )
- return r.headers["ETag"].replace('"', "")
-
-
-
-[docs]
- def get_playlist_items(
- self, playlist_uuid: str, country_code: str = None, *,
- limit: int = 100, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for items (tracks and videos) in
- a playlist.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing TIDAL catalog information for
- tracks and videos in the specified playlist and metadata for
- the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "item": {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": >int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- },
- "type": "track"
- }
- ]
- }
- """
-
- self._check_scope("get_playlist_items", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/playlists/{playlist_uuid}/items",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_playlist_recommendations(
- self, playlist_uuid: str, country_code: str = None, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for recommended tracks based on a
- playlist's items.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing TIDAL catalog information for
- recommended tracks and videos and metadata for the returned
- results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "item": {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": >int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- },
- "type": "track"
- }
- ]
- }
- """
-
- self._check_scope("get_playlist_recommendations", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/playlists/{playlist_uuid}"
- "/recommendations/items",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def favorite_playlists(
- self, playlist_uuids: Union[str, list[str]], *,
- folder_id: str = "root") -> None:
-
- """
- Add playlists to the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuids : `str` or `list`
- TIDAL playlist UUID(s).
-
- **Example**: :code:`["36ea71a8-445e-41a4-82ab-6628c581535d",
- "4261748a-4287-4758-aaab-6d5be3e99e52"]`.
-
- folder_id : `str`, keyword-only, default: :code:`"root"`
- ID of the folder to move the playlist into. To place a
- playlist directly under "My Playlists", use
- :code:`folder_id="root"`.
- """
-
- self._check_scope("favorite_playlists", "r_usr", flow={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/add-favorites",
- params={"uuids": playlist_uuids, "folderId": folder_id}
- )
-
-
-
-[docs]
- def move_playlist(self, playlist_uuid: str, folder_id: str) -> None:
-
- """
- Move a playlist in the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
-
- folder_id : `str`
- ID of the folder to move the playlist into. To place a
- playlist directly under "My Playlists", use
- :code:`folder_id="root"`.
- """
-
- self._check_scope("move_playlist", "r_usr", flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/move",
- params={
- "folderId": folder_id,
- "trns": f"trn:playlist:{playlist_uuid}"
- }
- )
-
-
-
-[docs]
- def unfavorite_playlist(self, playlist_uuid: str) -> None:
-
- """
- Remove a playlist from the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
- """
-
- self._check_scope("unfavorite_playlist", "r_usr",
- flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/remove",
- params={"trns": f"trn:playlist:{playlist_uuid}"}
- )
-
-
-
-[docs]
- def get_user_playlist(self, playlist_uuid: str) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for a user playlist.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL user playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
-
- Returns
- -------
- playlist : `dict`
- TIDAL catalog information for a user playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "playlist": {
- "uuid": <str>,
- "type": "USER",
- "creator": {
- "id": <int>,
- "name": <str>,
- "picture": <str>,
- "type": "USER"
- },
- "contentBehavior": <str>,
- "sharingLevel": <str>,
- "status": <str>,
- "source": <str>,
- "title": <str>,
- "description": <str>,
- "image": <str>,
- "squareImage": <str>,
- "url": <str>,
- "created": <str>,
- "lastUpdated": <str>,
- "lastItemAddedAt": <str>,
- "duration": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "promotedArtists": [],
- "trn": <str>,
- },
- "followInfo": {
- "nrOfFollowers": <int>,
- "tidalResourceName": <str>,
- "followed": <bool>,
- "followType": "PLAYLIST"
- },
- "profile": {
- "userId": <int>,
- "name": <str>,
- "color": [<str>]
- }
- }
- """
-
- self._check_scope("get_user_playlist", "r_usr", flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v2/user-playlists/{playlist_uuid}"
- )
-
-
-
-[docs]
- def get_user_playlists(
- self, user_id: Union[int, str] = None, *, limit: int = 50,
- cursor: str = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for playlists created by a TIDAL
- user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `str`
- TIDAL user ID. If not specified, the ID associated with the
- user account in the current session is used.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- cursor : `str`, keyword-only, optional
- Cursor position of the last item in previous search results.
- Use with `limit` to get the next page of search results.
-
- Returns
- -------
- playlists : `dict`
- A dictionary containing the user's playlists and the cursor
- position.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "items": [
- {
- "playlist": {
- "uuid": <str>,
- "type": "USER",
- "creator": {
- "id": <int>,
- "name": <str>,
- "picture": <str>,
- "type": "USER"
- },
- "contentBehavior": <str>,
- "sharingLevel": <str>,
- "status": <str>,
- "source": <str>,
- "title": <str>,
- "description": <str>,
- "image": <str>,
- "squareImage": <str>,
- "url": <str>,
- "created": <str>,
- "lastUpdated": <str>,
- "lastItemAddedAt": <str>,
- "duration": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "promotedArtists": [],
- "trn": <str>,
- },
- "followInfo": {
- "nrOfFollowers": <int>,
- "tidalResourceName": <str>,
- "followed": <bool>,
- "followType": "PLAYLIST"
- },
- "profile": {
- "userId": <int>,
- "name": <str>,
- "color": [<str>]
- }
- }
- ],
- "cursor": <str>
- }
- """
-
- self._check_scope("get_user_playlists", "r_usr", flows={"device_code"})
-
- if user_id is None:
- user_id = self._user_id
- return self._get_json(
- f"{self.API_URL}/v2/user-playlists/{user_id}/public",
- params={"limit": limit, "cursor": cursor}
- )
-
-
-
-[docs]
- def get_personal_playlists(
- self, country_code: str = None, *, limit: int = 50,
- offset: int = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for playlists created by the
- current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- playlists : `dict`
- TIDAL catalog information for a user playlists created by
- the current user and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "uuid": <str>",
- "title": <str>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "creator": {
- "id": <int>
- },
- "description": <str>,
- "duration": <int>,
- "lastUpdated": <str>,
- "created": <str>,
- "type": "USER",
- "publicPlaylist": <bool>,
- "url": <str>,
- "image": <str>,
- "popularity": <int>,
- "squareImage": <str>,
- "promotedArtists": [],
- "lastItemAddedAt": <str>
- }
- ]
- }
- """
-
- self._check_scope("get_personal_playlists", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/playlists",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def create_playlist(
- self, name: str, *, description: str = None,
- folder_uuid: str = "root", public: bool = None) -> dict[str, Any]:
-
- """
- Create a user playlist.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- name : `str`
- Playlist name.
-
- description : `str`, keyword-only, optional
- Brief playlist description.
-
- folder_uuid : `str`, keyword-only, default: :code:`"root"`
- UUID of the folder the new playlist will be placed in. To
- place a playlist directly under "My Playlists", use
- :code:`folder_id="root"`.
-
- public : `bool`, keyword-only, optional
- Determines whether the playlist is public (:code:`True`) or
- private (:code:`False`).
-
- Returns
- -------
- playlist : `dict`
- TIDAL catalog information for the newly created playlist.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "trn": <str>,
- "itemType": "PLAYLIST",
- "addedAt": <str>,
- "lastModifiedAt": <str>,
- "name": <str>,
- "parent": <str>,
- "data": {
- "uuid": <str>,
- "type": "USER",
- "creator": {
- "id": <int>,
- "name": <str>,
- "picture": <str>,
- "type": "USER"
- },
- "contentBehavior": <str>,
- "sharingLevel": <str>,
- "status": "READY",
- "source": <str>,
- "title": <str>,
- "description": <str>,
- "image": <str>,
- "squareImage": <str>,
- "url": <str>,
- "created": <str>,
- "lastUpdated": <str>,
- "lastItemAddedAt": <str>,
- "duration": <int>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "promotedArtists": <list>,
- "trn": <str>,
- "itemType": "PLAYLIST"
- }
- }
- """
-
- self._check_scope("create_playlist", "r_usr", flows={"device_code"})
-
- return self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/create-playlist",
- params={
- "name": name,
- "description": description,
- "folderId": folder_uuid,
- "isPublic": public
- }
- ).json()
-
-
-
-[docs]
- def update_playlist(
- self, playlist_uuid: str, *, title: str = None,
- description: str = None) -> None:
-
- """
- Update the title or description of a playlist owned by the
- current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
-
- title : `str`, keyword-only, optional
- New playlist title.
-
- description : `str`, keyword-only, optional
- New playlist description.
- """
-
- self._check_scope("update_playlist", "r_usr", flows={"device_code"})
-
- if title is None and description is None:
- wmsg = "No changes were specified or made to the playlist."
- warnings.warn(wmsg)
- return
-
- data = {}
- if title is not None:
- data["title"] = title
- if description is not None:
- data["description"] = description
- self._request("post", f"{self.API_URL}/v1/playlists/{playlist_uuid}",
- data=data)
-
-
-
-[docs]
- def set_playlist_privacy(self, playlist_uuid: str, public: bool) -> None:
-
- """
- Set the privacy of a playlist owned by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
-
- public : `bool`
- Determines whether the playlist is public (:code:`True`) or
- private (:code:`False`).
- """
-
- self._check_scope("set_playlist_privacy", "r_usr",
- flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/playlists/{playlist_uuid}/set-"
- f"{'public' if public else 'private'}"
- )
-
-
-
-[docs]
- def add_playlist_items(
- self, playlist_uuid: str,
- items: Union[int, str, list[Union[int, str]]] = None, *,
- from_playlist_uuid: str = None, on_duplicate: str = "FAIL",
- on_artifact_not_found: str = "FAIL") -> None:
-
- """
- Add items to a playlist owned by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
-
- items : `int`, `str`, or `list`, optional
- Items to add to the playlist. If not specified,
- `from_playlist_uuid` must be provided.
-
- .. note::
-
- If both `items` and `from_playlist_uuid` are specified,
- only the items in `items` will be added to the playlist.
-
- from_playlist_uuid : `str`, keyword-only, optional
- TIDAL playlist from which to copy items.
-
- on_duplicate : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added is already in the
- playlist.
-
- **Valid values**: :code:`"ADD"`, :code:`"SKIP"`, and
- :code:`"FAIL"`.
-
- on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added does not exist.
-
- **Valid values**: :code:`"FAIL"`.
- """
-
- self._check_scope("add_playlist_items", "r_usr", flows={"device_code"})
-
- if items is None and from_playlist_uuid is None:
- wmsg = "No changes were specified or made to the playlist."
- warnings.warn(wmsg)
- return
-
- data = {
- "onArtifactNotFound": on_artifact_not_found,
- "onDuplicate": on_duplicate
- }
- if items:
- data |= {"trackIds": items}
- else:
- data |= {"fromPlaylistUuid": from_playlist_uuid}
- self._request(
- "post",
- f"{self.API_URL}/v1/playlists/{playlist_uuid}/items",
- data=data,
- headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}
- )
-
-
-
-[docs]
- def move_playlist_item(
- self, playlist_uuid: str, from_index: Union[int, str],
- to_index: Union[int, str]) -> None:
-
- """
- Move an item in a playlist owned by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
-
- from_index : `int` or `str`
- Current item index.
-
- to_index : `int` or `str`
- Desired item index.
- """
-
- self._check_scope("move_playlist_item", "r_usr", flows={"device_code"})
-
- self._request(
- "post",
- f"{self.API_URL}/v1/playlists/{playlist_uuid}/items/{from_index}",
- params={"toIndex": to_index},
- headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}
- )
-
-
-
-[docs]
- def delete_playlist_item(
- self, playlist_uuid: str, index: Union[int, str]) -> None:
-
- """
- Delete an item from a playlist owned by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
-
- index : `int` or `str`
- Item index.
- """
-
- self._check_scope("delete_playlist_item", "r_usr",
- flows={"device_code"})
-
- self._request(
- "delete",
- f"{self.API_URL}/v1/playlists/{playlist_uuid}/items/{index}",
- headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}
- )
-
-
-
-[docs]
- def delete_playlist(self, playlist_uuid: str) -> None:
-
- """
- Delete a playlist owned by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- playlist_uuid : `str`
- TIDAL playlist UUID.
-
- **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`.
- """
-
- self._check_scope("delete_playlist", "r_usr", flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/remove",
- params={"trns": f"trn:playlist:{playlist_uuid}"}
- )
-
-
-
-[docs]
- def get_personal_playlist_folders(
- self, folder_uuid: str = None, *, flattened: bool = False,
- include_only: str = None, limit: int = 50, order: str = "DATE",
- order_direction: str = "DESC") -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for a playlist folder (and
- optionally, playlists and other playlist folders in it) created
- by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- folder_uuid : `str`, optional
- UUID of the folder in which to look for playlists and other
- folders. If not specified, all folders and playlists in "My
- Playlists" are returned.
-
- flattened : `bool`, keyword-only, default: :code:`False`
- Determines whether the results are flattened into a list.
-
- include_only : `str`, keyword-only, optional
- Type of playlist-related item to return.
-
- **Valid values**: :code:`"FAVORITE_PLAYLIST"`,
- :code:`"FOLDER"`, and :code:`"PLAYLIST"`.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- order : `str`, keyword-only, default: :code:`"DATE"`
- Sorting order.
-
- **Valid values**: :code:`"DATE"`, :code:`"DATE_UPDATED"`,
- and :code:`"NAME"`.
-
- order_direction : `str`, keyword-only, default: :code:`"DESC"`
- Sorting order direction.
-
- **Valid values**: :code:`"DESC"` and :code:`"ASC"`.
-
- Returns
- -------
- items : `dict`
- A dictionary containing playlist-related items and the total
- number of items available.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "lastModifiedAt": <str>,
- "items": [
- {
- "trn": <str>,
- "itemType": "FOLDER",
- "addedAt": <str>,
- "lastModifiedAt": <str>,
- "name": <str>,
- "parent": <str>,
- "data": {
- "trn": <str>,
- "name": <str>,
- "createdAt": <str>,
- "lastModifiedAt": <str>,
- "totalNumberOfItems": <int>,
- "id": <str>,
- "itemType": "FOLDER"
- }
- }
- ],
- "totalNumberOfItems": <int>,
- "cursor": <str>
- }
- """
-
- self._check_scope("get_personal_playlist_folders", "r_usr",
- flows={"device_code"})
-
- if include_only and include_only not in \
- (ALLOWED_INCLUDES := {"FAVORITE_PLAYLIST", "FOLDER",
- "PLAYLIST"}):
- emsg = ("Invalid include type. Valid values: "
- f"{', '.join(ALLOWED_INCLUDES)}.")
- raise ValueError(emsg)
-
- url = f"{self.API_URL}/v2/my-collection/playlists/folders"
- if flattened:
- url += "/flattened"
- return self._get_json(
- url,
- params={
- "folderId": folder_uuid if folder_uuid else "root",
- "limit": limit,
- "includeOnly": include_only,
- "order": order,
- "orderDirection": order_direction
- }
- )
-
-
-
-[docs]
- def create_playlist_folder(
- self, name: str, *, folder_uuid: str = "root") -> None:
-
- """
- Create a user playlist folder.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- name : `str`
- Playlist folder name.
-
- folder_uuid : `str`, keyword-only, default: :code:`"root"`
- UUID of the folder in which the new playlist folder should
- be created in. To create a folder directly under "My
- Playlists", use :code:`folder_id="root"`.
- """
-
- self._check_scope("create_playlist_folder", "r_usr",
- flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/create-folder",
- params={"name": name, "folderId": folder_uuid}
- )
-
-
-
-[docs]
- def delete_playlist_folder(self, folder_uuid: str) -> None:
-
- """
- Delete a playlist folder owned by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- folder_uuid : `str`
- TIDAL playlist folder UUID.
-
- **Example**: :code:`"92b3c1ea-245a-4e5a-a5a4-c215f7a65b9f"`.
- """
-
- self._check_scope("delete_playlist_folder", "r_usr",
- flows={"device_code"})
-
- self._request(
- "put",
- f"{self.API_URL}/v2/my-collection/playlists/folders/remove",
- params={"trns": f"trn:folder:{folder_uuid}"}
- )
-
-
- ### SEARCH ################################################################
-
-
-[docs]
- def search(
- self, query: str, country_code: str = None, *, type: str = None,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Search for albums, artists, tracks, and videos.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- query : `str`
- Search query.
-
- **Example**: :code:`"Beyoncé"`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- type : `str`, keyword-only, optional
- Target search type. Searches for all types if not specified.
-
- **Valid values**: :code:`"ALBUMS"`, :code:`"ARTISTS"`,
- :code:`"TRACKS"`, :code:`"VIDEOS"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- results : `dict`
- A dictionary containing TIDAL catalog information for
- albums, artists, tracks, and videos matching the search
- query, and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "artists": {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "name": <str>,
- "artistTypes": [<str>],
- "url": <str>,
- "picture": <str>,
- "popularity": <int>,
- "artistRoles": [
- {
- "categoryId": <int>,
- "category": <str>
- }
- ],
- "mixes": {
- "ARTIST_MIX": <str>
- }
- }
- ]
- },
- "albums": {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "premiumStreamingOnly": <bool>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "numberOfVolumes": <int>,
- "releaseDate": <str>,
- "copyright": <str>,
- "type": "ALBUM",
- "version": <str>,
- "url": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>,
- "explicit": <bool>,
- "upc": <str>,
- "popularity": <int>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ]
- }
- ]
- },
- "playlists": {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "uuid": <str>,
- "title": <str>,
- "numberOfTracks": <int>,
- "numberOfVideos": <int>,
- "creator": {
- "id": <int>
- },
- "description": <str>,
- "duration": <int>,
- "lastUpdated": <str>,
- "created": <str>,
- "type": <str>,
- "publicPlaylist": <bool>,
- "url": <str>,
- "image": <str>,
- "popularity": <int>,
- "squareImage": <str>,
- "promotedArtists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>,
- }
- ],
- "lastItemAddedAt": <str>
- }
- ]
- },
- "tracks": {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- }
- ]
- },
- "videos": {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "volumeNumber": <int>,
- "trackNumber": <int>,
- "releaseDate": <str>,
- "imagePath": <str>,
- "imageId": <str>,
- "vibrantColor": <str>,
- "duration": <int>,
- "quality": <str>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "explicit": <bool>,
- "popularity": <int>,
- "type": <str>,
- "adsUrl": <str>,
- "adsPrePaywallOnly": <bool>,
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>,
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>,
- }
- ],
- "album": <dict>
- }
- ]
- },
- "topHit": {
- "value": <dict>,
- "type": <str>
- }
- }
- """
-
- self._check_scope("search", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- url = f"{self.API_URL}/v1/search"
- if type:
- if type not in \
- (TYPES := {"artist", "album", "playlist", "track",
- "userProfile", "video"}):
- emsg = ("Invalid target search type. Valid values: "
- f"{', '.join(TYPES)}.")
- raise ValueError(emsg)
- url += f"/{type}s"
-
- return self._get_json(
- url,
- params={
- "query": query,
- "type": type,
- "limit": limit,
- "offset": offset,
- "countryCode": self._get_country_code(country_code)
- }
- )
-
-
- ### STREAMS ###############################################################
-
-
-[docs]
- def get_collection_streams(
- self, collection_id: Union[int, str], type: str, *,
- audio_quality: str = "HI_RES", video_quality: str = "HIGH",
- max_resolution: int = 2160, playback_mode: str = "STREAM",
- asset_presentation: str = "FULL", streaming_session_id: str = None
- ) -> list[tuple[bytes, str]]:
-
- """
- Get audio and video stream data for items (tracks and videos) in
- an album, mix, or playlist.
-
- .. admonition:: User authentication, authorization scope, and
- subscription
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Full track and video playback information and lossless audio
- is only available with user authentication and an active
- TIDAL subscription.
-
- High-resolution and immersive audio is only available with
- the HiFi Plus plan and when the current client credentials
- are from a supported device.
-
- .. seealso::
-
- For more information on audio quality availability, see
- the `Download TIDAL <https://offer.tidal.com/download>`_,
- `TIDAL Pricing <https://tidal.com/pricing>`_, and
- `Dolby Atmos <https://support.tidal.com/hc/en-us/articles
- /360004255778-Dolby-Atmos>`_ web pages.
-
- .. note::
-
- This method is provided for convenience and is not a private
- TIDAL API endpoint.
-
- Parameters
- ----------
- collection_id : `int` or `str`
- TIDAL collection ID or UUID.
-
- type : `str`
- Collection type.
-
- **Valid values**: :code:`"album"`, :code:`"mix"`, and
- :code:`"playlist"`.
-
- audio_quality : `str`, keyword-only, default: :code:`"HI-RES"`
- Audio quality.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"LOW"` for 64 kbps (22.05 kHz) MP3 without user
- authentication or 96 kbps AAC with user authentication.
- * :code:`"HIGH"` for 320 kbps AAC.
- * :code:`"LOSSLESS"` for 1411 kbps (16-bit, 44.1 kHz) ALAC
- or FLAC.
- * :code:`"HI_RES"` for up to 9216 kbps (24-bit, 96 kHz)
- MQA-encoded FLAC.
-
- video_quality : `str`, keyword-only, default: :code:`"HIGH"`
- Video quality.
-
- **Valid values**: :code:`"AUDIO_ONLY"`, :code:`"LOW"`,
- :code:`"MEDIUM"`, and :code:`"HIGH"`.
-
- max_resolution : `int`, keyword-only, default: :code:`2160`
- Maximum video resolution (number of vertical pixels).
-
- playback_mode : `str`, keyword-only, default: :code:`"STREAM"`
- Playback mode.
-
- **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`.
-
- asset_presentation : `str`, keyword-only, default: :code:`"FULL"`
- Asset presentation.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"FULL"`: Full track or video.
- * :code:`"PREVIEW"`: 30-second preview of the track or
- video.
-
- streaming_session_id : `str`, keyword-only, optional
- Streaming session ID.
-
- Returns
- -------
- streams : `list`
- Audio and video stream data and their MIME types.
- """
-
- if type not in (COLLECTION_TYPES := {"album", "mix", "playlist"}):
- emsg = ("Invalid collection type. Valid values: "
- f"{', '.join(COLLECTION_TYPES)}.")
- raise ValueError(emsg)
-
- if type == "album":
- items = self.get_album_items(collection_id)["items"]
- elif type == "mix":
- items = self.get_mix_items(collection_id)["items"]
- elif type == "playlist":
- items = self.get_playlist_items(collection_id)["items"]
-
- streams = []
- for item in items:
- if item["type"] == "track":
- stream = self.get_track_stream(
- item["item"]["id"],
- audio_quality=audio_quality,
- playback_mode=playback_mode,
- asset_presentation=asset_presentation,
- streaming_session_id=streaming_session_id
- )
- elif item["type"] == "video":
- stream = self.get_video_stream(
- item["item"]["id"],
- video_quality=video_quality,
- max_resolution=max_resolution,
- playback_mode=playback_mode,
- asset_presentation=asset_presentation,
- streaming_session_id=streaming_session_id
- )
- streams.append(stream)
- return streams
-
-
-
-[docs]
- def get_track_stream(
- self, track_id: Union[int, str], *, audio_quality: str = "HI_RES",
- playback_mode: str = "STREAM", asset_presentation: str = "FULL",
- streaming_session_id: str = None) -> Union[bytes, str]:
-
- """
- Get the audio stream data for a track.
-
- .. admonition:: User authentication, authorization scope, and
- subscription
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Full track playback information and lossless audio is only
- available with user authentication and an active TIDAL
- subscription.
-
- High-resolution and immersive audio is only available with
- the HiFi Plus plan and when the current client credentials
- are from a supported device.
-
- .. seealso::
-
- For more information on audio quality availability, see
- the `Download TIDAL <https://offer.tidal.com/download>`_,
- `TIDAL Pricing <https://tidal.com/pricing>`_, and
- `Dolby Atmos <https://support.tidal.com/hc/en-us/articles
- /360004255778-Dolby-Atmos>`_ web pages.
-
- .. note::
-
- This method is provided for convenience and is not a private
- TIDAL API endpoint.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- audio_quality : `str`, keyword-only, default: :code:`"HI-RES"`
- Audio quality.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"LOW"` for 64 kbps (22.05 kHz) MP3 without user
- authentication or 96 kbps AAC with user authentication.
- * :code:`"HIGH"` for 320 kbps AAC.
- * :code:`"LOSSLESS"` for 1411 kbps (16-bit, 44.1 kHz) ALAC
- or FLAC.
- * :code:`"HI_RES"` for up to 9216 kbps (24-bit, 96 kHz)
- MQA-encoded FLAC.
-
- playback_mode : `str`, keyword-only, default: :code:`"STREAM"`
- Playback mode.
-
- **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`.
-
- asset_presentation : `str`, keyword-only, default: :code:`"FULL"`
- Asset presentation.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"FULL"`: Full track.
- * :code:`"PREVIEW"`: 30-second preview of the track.
-
- streaming_session_id : `str`, keyword-only, optional
- Streaming session ID.
-
- Returns
- -------
- stream : `bytes`
- Audio stream data.
-
- codec : `str`
- Audio codec.
- """
-
- manifest = base64.b64decode(
- self.get_track_playback_info(
- track_id,
- audio_quality=audio_quality,
- playback_mode=playback_mode,
- asset_presentation=asset_presentation,
- streaming_session_id=streaming_session_id
- )["manifest"]
- )
-
- if b"urn:mpeg:dash" in manifest:
- manifest = minidom.parseString(manifest)
- codec = (manifest.getElementsByTagName("Representation")[0]
- .getAttribute("codecs"))
- segment = manifest.getElementsByTagName("SegmentTemplate")[0]
- stream = bytearray()
- with self.session.get(
- segment.getAttribute("initialization")
- ) as r:
- stream.extend(r.content)
- for i in range(1, sum(int(tl.getAttribute("r") or 1)
- for tl in
- segment.getElementsByTagName("S")) + 2):
- with self.session.get(
- segment.getAttribute("media").replace(
- "$Number$", str(i)
- )
- ) as r:
- stream.extend(r.content)
- else:
- manifest = json.loads(manifest)
- codec = manifest["codecs"]
- with self.session.get(manifest["urls"][0]) as r:
- stream = r.content
- if manifest["encryptionType"] == "OLD_AES":
- key_id = base64.b64decode(manifest["keyId"])
- key_nonce = Cipher(
- algorithms.AES(b"P\x89SLC&\x98\xb7\xc6\xa3\n?P.\xb4\xc7"
- b"a\xf8\xe5n\x8cth\x13E\xfa?\xbah8\xef\x9e"),
- modes.CBC(key_id[:16])
- ).decryptor().update(key_id[16:])
- stream = Cipher(
- algorithms.AES(key_nonce[:16]),
- modes.CTR(key_nonce[16:32])
- ).decryptor().update(stream)
- elif manifest["encryptionType"] != "NONE":
- raise NotImplementedError("Unsupported encryption type.")
- return stream, codec
-
-
-
-[docs]
- def get_video_stream(
- self, video_id: Union[int, str], *, video_quality: str = "HIGH",
- max_resolution: int = 2160, playback_mode: str = "STREAM",
- asset_presentation: str = "FULL", streaming_session_id: str = None
- ) -> tuple[bytes, str]:
-
- """
- Get the video stream data for a music video.
-
- .. admonition:: User authentication, authorization scope, and
- subscription
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Full video playback information is only available with user
- authentication and an active TIDAL subscription.
-
- .. note::
-
- This method is provided for convenience and is not a private
- TIDAL API endpoint.
-
- Parameters
- ----------
- video_id : `int` or `str`
- TIDAL video ID.
-
- **Example**: :code:`59727844`.
-
- video_quality : `str`, keyword-only, default: :code:`"HIGH"`
- Video quality.
-
- **Valid values**: :code:`"AUDIO_ONLY"`, :code:`"LOW"`,
- :code:`"MEDIUM"`, and :code:`"HIGH"`.
-
- max_resolution : `int`, keyword-only, default: :code:`2160`
- Maximum video resolution (number of vertical pixels).
-
- playback_mode : `str`, keyword-only, default: :code:`"STREAM"`
- Playback mode.
-
- **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`.
-
- asset_presentation : `str`, keyword-only, default: :code:`"FULL"`
- Asset presentation.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"FULL"`: Full video.
- * :code:`"PREVIEW"`: 30-second preview of the video.
-
- streaming_session_id : `str`, keyword-only, optional
- Streaming session ID.
-
- Returns
- -------
- stream : `bytes`
- Video stream data.
-
- codec : `str`
- Video codec.
- """
-
- manifest = base64.b64decode(
- self.get_video_playback_info(
- video_id,
- video_quality=video_quality,
- playback_mode=playback_mode,
- asset_presentation=asset_presentation,
- streaming_session_id=streaming_session_id
- )["manifest"]
- )
-
- codec, playlist = next(
- (c, pl) for c, res, pl in re.findall(
- r'(?<=CODECS=")(.*)",(?:RESOLUTION=)\d+x(\d+)\n(http.*)',
- self.session.get(
- json.loads(manifest)["urls"][0]
- ).content.decode("utf-8")
- )[::-1] if int(res) < max_resolution
- )
-
- stream = bytearray()
- for ts in re.findall(
- "(?<=\n).*(http.*)",
- self.session.get(playlist).content.decode("utf-8")
- ):
- with self.session.get(ts) as r:
- stream.extend(r.content)
- return stream, codec
-
-
- ### TRACKS ################################################################
-
-
-[docs]
- def get_track(
- self, track_id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for a track.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- track : `dict`
- TIDAL catalog information for a track.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- }
- """
-
- self._check_scope("get_track", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/tracks/{track_id}",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_track_contributors(
- self, track_id: Union[int, str], country_code: str = None, *,
- limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get the contributors to a track and their roles.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`100`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- contributors : `dict`
- A dictionary containing a track's contributors and their
- roles, and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "name": <str>,
- "role": <str>
- }
- ]
- }
- """
-
- self._check_scope("get_track_contributors", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/tracks/{track_id}/contributors",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_track_credits(
- self, track_id: Union[int, str], country_code: str = None
- ) -> list[dict[str, Any]]:
-
- """
- Get credits for a track.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- country : `str`, keyword-only, optional
- An ISO 3166-1 alpha-2 country code. If not specified, the
- country associated with the user account will be used.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- credits : `list`
- A list of roles and their associated contributors.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- [
- {
- "type": <str>,
- "contributors": [
- {
- "name": <str>,
- "id": <int>
- }
- ]
- }
- ]
- """
-
- self._check_scope("get_track_credits", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/tracks/{track_id}/credits",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_track_composers(self, track_id: Union[int, str]) -> list[str]:
-
- """
- Get the composers, lyricists, and/or songwriters of a track.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- .. note::
-
- This method is provided for convenience and is not a private
- TIDAL API endpoint.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- Returns
- -------
- composers : `list`
- Composers, lyricists, and/or songwriters of the track.
-
- **Example**: :code:`['Tommy Wright III', 'Beyoncé',
- 'Kelman Duran', 'Terius "The-Dream" G...de-Diamant',
- 'Mike Dean']`
- """
-
- return sorted({c["name"]
- for c in self.get_track_contributors(track_id)["items"]
- if c["role"] in {"Composer", "Lyricist", "Writer"}})
-
-
-
-[docs]
- def get_track_lyrics(
- self, id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get lyrics for a track.
-
- .. admonition:: User authentication and subscription
- :class: warning
-
- Requires user authentication via an OAuth 2.0 authorization
- flow and an active TIDAL subscription.
-
- Parameters
- ----------
- id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- lyrics : `dict`
- A dictionary containing formatted and time-synced lyrics (if
- available) in the `"lyrics"` and `"subtitles"` keys,
- respectively.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "trackId": <int>,
- "lyricsProvider": <str>,
- "providerCommontrackId": <str>,
- "providerLyricsId": <str>,
- "lyrics": <str>,
- "subtitles": <str>,
- "isRightToLeft": <bool>
- }
- """
-
- self._check_scope("get_track_lyrics")
-
- try:
- return self._get_json(
- f"{self.WEB_URL}/v1/tracks/{id}/lyrics",
- params={"countryCode": self._get_country_code(country_code)}
- )
- except RuntimeError:
- logging.warning("Either lyrics are not available for this track "
- "or the current account does not have an active "
- "TIDAL subscription.")
-
-
-
-[docs]
- def get_track_mix_id(
- self, tidal_id: Union[int, str], country_code: str = None) -> str:
-
- """
- Get the curated mix of tracks based on a track.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- tidal_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- mix_id : `str`
- TIDAL mix ID.
-
- **Example**: :code:`"0017159e6a1f34ae3d981792d72ecf"`.
- """
-
- self._check_scope("get_track_mix_id", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/tracks/{tidal_id}/mix",
- params={"countryCode": self._get_country_code(country_code)}
- )["id"]
-
-
-
-[docs]
- def get_track_playback_info(
- self, track_id: Union[int, str], *, audio_quality: str = "HI_RES",
- playback_mode: str = "STREAM", asset_presentation: str = "FULL",
- streaming_session_id: str = None) -> dict[str, Any]:
-
- """
- Get playback information for a track.
-
- .. admonition:: User authentication, authorization scope, and
- subscription
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Full track playback information and lossless audio is only
- available with user authentication and an active TIDAL
- subscription.
-
- High-resolution and immersive audio is only available with
- the HiFi Plus plan and when the current client credentials
- are from a supported device.
-
- .. seealso::
-
- For more information on audio quality availability, see
- the `Download TIDAL <https://offer.tidal.com/download>`_,
- `TIDAL Pricing <https://tidal.com/pricing>`_, and
- `Dolby Atmos <https://support.tidal.com/hc/en-us/articles
- /360004255778-Dolby-Atmos>` web pages.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- audio_quality : `str`, keyword-only, default: :code:`"HI-RES"`
- Audio quality.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"LOW"` for 64 kbps (22.05 kHz) MP3 without user
- authentication or 96 kbps AAC with user authentication.
- * :code:`"HIGH"` for 320 kbps AAC.
- * :code:`"LOSSLESS"` for 1411 kbps (16-bit, 44.1 kHz) ALAC
- or FLAC.
- * :code:`"HI_RES"` for up to 9216 kbps (24-bit, 96 kHz)
- MQA-encoded FLAC.
-
- playback_mode : `str`, keyword-only, default: :code:`"STREAM"`
- Playback mode.
-
- **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`.
-
- asset_presentation : `str`, keyword-only, default: :code:`"FULL"`
- Asset presentation.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"FULL"`: Full track.
- * :code:`"PREVIEW"`: 30-second preview of the track.
-
- streaming_session_id : `str`, keyword-only, optional
- Streaming session ID.
-
- Returns
- -------
- info : `dict`
- Track playback information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "trackId": <int>,
- "assetPresentation": <str>,
- "audioMode": <str>,
- "audioQuality": <str>,
- "manifestMimeType": <str>,
- "manifestHash": <str>,
- "manifest": <str>,
- "albumReplayGain": <float>,
- "albumPeakAmplitude": <float>,
- "trackReplayGain": <float>,
- "trackPeakAmplitude": <float>
- }
- """
-
- self._check_scope("get_track_playback_info", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- if audio_quality not in \
- (AUDIO_QUALITIES := {"LOW", "HIGH", "LOSSLESS", "HI_RES"}):
- emsg = ("Invalid audio quality. Valid values: "
- f"are{', '.join(AUDIO_QUALITIES)}.")
- raise ValueError(emsg)
- if playback_mode not in \
- (PLAYBACK_MODES := {"STREAM", "OFFLINE"}):
- emsg = ("Invalid playback mode. Valid values: "
- f"modes are {', '.join(PLAYBACK_MODES)}.")
- raise ValueError(emsg)
- if asset_presentation not in \
- (ASSET_PRESENTATIONS := {"FULL", "PREVIEW"}):
- emsg = ("Invalid asset presentation. Valid values: "
- "presentations are "
- f"{', '.join(ASSET_PRESENTATIONS)}.")
- raise ValueError(emsg)
-
- url = f"{self.API_URL}/v1/tracks/{track_id}/playbackinfo"
- # if self._flow:
- # url += "postpaywall"
- url += "postpaywall" if self._flow else "prepaywall"
- return self._get_json(
- url,
- params={
- "audioquality": audio_quality,
- "assetpresentation": asset_presentation,
- "playbackmode": playback_mode,
- "streamingsessionid": streaming_session_id
- }
- )
-
-
-
-[docs]
- def get_track_recommendations(
- self, track_id: Union[int, str], country_code: str = None, *,
- limit: int = None, offset = None) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for a track's recommended
- tracks and videos.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- track_id : `int` or `str`
- TIDAL track ID.
-
- **Example**: :code:`251380837`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- recommendations : `dict`
- A dictionary containing TIDAL catalog information for the
- recommended tracks and metadata for the returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- },
- "sources": [
- "SUGGESTED_TRACKS"
- ]
- ]
- }
- """
-
- self._check_scope("get_track_recommendations", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/tracks/{track_id}/recommendations",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset
- }
- )
-
-
-
-[docs]
- def get_favorite_tracks(
- self, country_code: str = None, *, limit: int = 50,
- offset: int = None, order: str = "DATE",
- order_direction: str = "DESC"):
-
- """
- Get TIDAL catalog information for tracks in the current user's
- collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- order : `str`, keyword-only, default: :code:`"DATE"`
- Sorting order.
-
- **Valid values**: :code:`"DATE"` and :code:`"NAME"`.
-
- order_direction : `str`, keyword-only, default: :code:`"DESC"`
- Sorting order direction.
-
- **Valid values**: :code:`"DESC"` and :code:`"ASC"`.
-
- Returns
- -------
- tracks : `dict`
- A dictionary containing TIDAL catalog information for tracks
- in the current user's collection and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "created": <str>,
- "item": {
- "id": <int>,
- "title": <str>,
- "duration": <int>,
- "replayGain": <float>,
- "peak": <float>,
- "allowStreaming": <bool>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "premiumStreamingOnly": <bool>,
- "trackNumber": <int>,
- "volumeNumber": <int>,
- "version": <str>,
- "popularity": <int>,
- "copyright": <str>,
- "url": <str>,
- "isrc": <str>,
- "editable": <bool>,
- "explicit": <bool>,
- "audioQuality": <str>,
- "audioModes": [<str>],
- "mediaMetadata": {
- "tags": [<str>]
- },
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": {
- "id": <int>,
- "title": <str>,
- "cover": <str>,
- "vibrantColor": <str>,
- "videoCover": <str>
- },
- "mixes": {
- "TRACK_MIX": <str>
- }
- }
- }
- ]
- }
- """
-
- self._check_scope("get_favorite_tracks", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/tracks",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset,
- "order": order,
- "orderDirection": order_direction
- }
- )
-
-
-
-[docs]
- def favorite_tracks(
- self, track_ids: Union[int, str, list[Union[int, str]]],
- country_code: str = None, *, on_artifact_not_found: str = "FAIL"
- ) -> None:
-
- """
- Add tracks to the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- track_ids : `int`, `str`, or `list`
- TIDAL track ID(s).
-
- **Examples**: :code:`"251380837,251380838"` or
- :code:`[251380837, 251380838]`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added does not exist.
-
- **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`.
- """
-
- self._check_scope("favorite_tracks", "r_usr", flows={"device_code"})
-
- self._request(
- "post",
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/tracks",
- params={"countryCode": self._get_country_code(country_code)},
- data={
- "trackIds": ",".join(map(str, track_ids))
- if isinstance(track_ids, list) else track_ids,
- "onArtifactNotFound": on_artifact_not_found
- }
- )
-
-
-
-[docs]
- def unfavorite_tracks(
- self, track_ids: Union[int, str, list[Union[int, str]]]) -> None:
-
- """
- Remove tracks from the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- track_ids : `int`, `str`, or `list`
- TIDAL track ID(s).
-
- **Examples**: :code:`"251380837,251380838"` or
- :code:`[251380837, 251380838]`.
- """
-
- self._check_scope("unfavorite_tracks", "r_usr", flows={"device_code"})
-
- if isinstance(track_ids, list):
- track_ids = ",".join(map(str, track_ids))
- self._request("delete",
- f"{self.API_URL}/v1/users/{self._user_id}"
- f"/favorites/tracks/{track_ids}")
-
-
- ### USERS #################################################################
-
-
-[docs]
- def get_profile(self) -> dict[str, Any]:
-
- """
- Get the current user's profile information.
-
- .. admonition:: User authentication
- :class: warning
-
- Requires user authentication via an OAuth 2.0 authorization
- flow.
-
- Returns
- -------
- profile : `dict`
- A dictionary containing the current user's profile
- information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "userId": <int>,
- "email": <str>,
- "countryCode": <str>,
- "fullName": <str>,
- "firstName": <str>,
- "lastName": <str>,
- "nickname": <str>,
- "username": <str>,
- "address": <str>,
- "city": <str>,
- "postalcode": <str>,
- "usState": <str>,
- "phoneNumber": <int>,
- "birthday": <int>,
- "channelId": <int>,
- "parentId": <int>,
- "acceptedEULA": <bool>,
- "created": <int>,
- "updated": <int>,
- "facebookUid": <int>,
- "appleUid": <int>,
- "googleUid": <int>,
- "accountLinkCreated": <bool>,
- "emailVerified": <bool>,
- "newUser": <bool>
- }
- """
-
- self._check_scope("get_profile")
-
- return self._get_json(f"{self.LOGIN_URL}/oauth2/me")
-
-
-
-[docs]
- def get_session(self) -> dict[str, Any]:
-
- """
- Get information about the current private TIDAL API session.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Returns
- -------
- session : `dict`
- Information about the current private TIDAL API session.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "sessionId": <str>,
- "userId": <int>,
- "countryCode": <str>,
- "channelId": <int>,
- "partnerId": <int>,
- "client": {
- "id": <int>,
- "name": <str>,
- "authorizedForOffline": <bool>,
- "authorizedForOfflineDate": <str>
- }
- }
-
- """
-
- self._check_scope("get_session", "r_usr", flows={"device_code"})
-
- return self._get_json(f"{self.API_URL}/v1/sessions")
-
-
-
-[docs]
- def get_favorite_ids(self) -> dict[str, list[str]]:
-
- """
- Get TIDAL IDs or UUIDs of the albums, artists, playlists,
- tracks, and videos in the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Returns
- -------
- ids : `dict`
- A dictionary containing the IDs or UUIDs of the items in the
- current user's collection.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "ARTIST": [<str>],
- "ALBUM": [<str>],
- "VIDEO": [<str>],
- "PLAYLIST": [<str>],
- "TRACK": [<str>]
- }
- """
-
- self._check_scope("get_favorite_ids", "r_usr", flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/ids"
- )
-
-
-
-[docs]
- def get_user_profile(self, user_id: Union[int, str]) -> dict[str, Any]:
-
- """
- Get a TIDAL user's profile information.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `int` or `str`
- TIDAL user ID.
-
- **Example**: :code:`172311284`.
-
- Returns
- -------
- profile : `dict`
- A dictionary containing the user's profile information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "userId": <int>,
- "name": <str>,
- "color": [<str>],
- "picture": <str>,
- "numberOfFollowers": <int>,
- "numberOfFollows": <int>,
- "prompts": [
- {
- "id": <int>,
- "title": <str>,
- "description": <str>,
- "colors": {
- "primary": <str>,
- "secondary": <str>,
- },
- "trn": <str>,
- "data": <str>,
- "updatedTime": <str>,
- "supportedContentType": "TRACK"
- }
- ],
- "profileType": <str>
- }
- """
-
- self._check_scope("get_user_profile", "r_usr", flows={"device_code"})
-
- return self._get_json(f"{self.API_URL}/v2/profiles/{user_id}")
-
-
-
-[docs]
- def get_user_followers(
- self, user_id: Union[int, str] = None, *, limit: int = 500,
- cursor: str = None) -> dict[str, Any]:
-
- """
- Get a TIDAL user's followers.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `str`
- TIDAL user ID. If not specified, the ID associated with the
- user account in the current session is used.
-
- **Example**: :code:`172311284`.
-
- limit : `int`, keyword-only, default: :code:`500`
- Page size.
-
- **Example**: :code:`10`.
-
- cursor : `str`, keyword-only, optional
- Cursor position of the last item in previous search results.
- Use with `limit` to get the next page of search results.
-
- Returns
- -------
- followers : `dict`
- A dictionary containing the user's followers and the cursor
- position.
- """
-
- self._check_scope("get_user_followers", "r_usr", flows={"device_code"})
-
- if user_id is None:
- user_id = self._user_id
- return self._get_json(f"{self.API_URL}/v2/profiles/{user_id}/followers",
- params={"limit": limit, "cursor": cursor})
-
-
-
-[docs]
- def get_user_following(
- self, user_id: Union[int, str] = None, *, include_only: str = None,
- limit: int = 500, cursor: str = None):
-
- """
- Get the people (artists, users, etc.) a TIDAL user follows.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `str`
- TIDAL user ID. If not specified, the ID associated with the
- user account in the current session is used.
-
- **Example**: :code:`172311284`.
-
- include_only : `str`, keyword-only, optional
- Type of people to return.
-
- **Valid values**: :code:`"ARTIST"` and :code:`"USER"`.
-
- limit : `int`, keyword-only, default: :code:`500`
- Page size.
-
- **Example**: :code:`10`.
-
- cursor : `str`, keyword-only, optional
- Cursor position of the last item in previous search results.
- Use with `limit` to get the next page of search results.
-
- Returns
- -------
- following : `dict`
- A dictionary containing the people following the user and
- the cursor position.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "items": [
- {
- "id": <int>,
- "name": <str>,
- "picture": <str>,
- "imFollowing": <bool>,
- "trn": <str>,
- "followType": <str>
- }
- ],
- "cursor": <str>
- }
- """
-
- self._check_scope("get_user_following", "r_usr", flows={"device_code"})
-
- if include_only and include_only not in \
- (ALLOWED_INCLUDES := {"ARTIST", "USER"}):
- emsg = ("Invalid include type. Valid values: "
- f"{', '.join(ALLOWED_INCLUDES)}.")
- raise ValueError(emsg)
-
- if user_id is None:
- user_id = self._user_id
- return self._get_json(
- f"{self.API_URL}/v2/profiles/{user_id}/following",
- params={
- "includeOnly": include_only,
- "limit": limit,
- "cursor": cursor
- }
- )
-
-
-
-[docs]
- def follow_user(self, user_id: Union[int, str]) -> None:
-
- """
- Follow a user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `int` or `str`
- TIDAL user ID.
-
- **Example**: :code:`172311284`.
- """
-
- self._check_scope("follow_user", "r_usr", flows={"device_code"})
-
- self._request("put", f"{self.API_URL}/v2/follow",
- params={"trn": f"trn:user:{user_id}"})
-
-
-
-[docs]
- def unfollow_user(self, user_id: Union[int, str]) -> None:
-
- """
- Unfollow a user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `int` or `str`
- TIDAL user ID.
-
- **Example**: :code:`172311284`.
- """
-
- self._check_scope("unfollow_user", "r_usr", flows={"device_code"})
-
- self._request("delete", f"{self.API_URL}/v2/follow",
- params={"trn": f"trn:user:{user_id}"})
-
-
-
-[docs]
- def get_blocked_users(
- self, *, limit: int = None, offset: int = None) -> dict[str, Any]:
-
- """
- Get users blocked by the current user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- limit : `int`, keyword-only, optional
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- Returns
- -------
- users : `dict`
- A dictionary containing the users blocked by the current
- user and the number of results.
- """
-
- self._check_scope("get_blocked_users", "r_usr", flows={"device_code"})
-
- return self._get_json(f"{self.API_URL}/v2/profiles/blocked-profiles",
- params={"limit": limit, "offset": offset})
-
-
-
-[docs]
- def block_user(self, user_id: Union[int, str]) -> None:
-
- """
- Block a user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `int` or `str`
- TIDAL user ID.
-
- **Example**: :code:`172311284`.
- """
-
- self._check_scope("block_user", "r_usr", flows={"device_code"})
-
- self._request("put", f"{self.API_URL}/v2/profiles/block/{user_id}")
-
-
-
-[docs]
- def unblock_user(self, user_id: Union[int, str]) -> None:
-
- """
- Unblock a user.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- user_id : `int` or `str`
- TIDAL user ID.
-
- **Example**: :code:`172311284`.
- """
-
- self._check_scope("unblock_user", "r_usr", flows={"device_code"})
-
- self._request("delete", f"{self.API_URL}/v2/profiles/block/{user_id}")
-
-
- ### VIDEOS ################################################################
-
-
-[docs]
- def get_video(
- self, video_id: Union[int, str], country_code: str = None
- ) -> dict[str, Any]:
-
- """
- Get TIDAL catalog information for a video.
-
- .. admonition:: Authorization scope
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Parameters
- ----------
- video_id : `int` or `str`
- TIDAL video ID.
-
- **Example**: :code:`59727844`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- Returns
- -------
- video : `dict`
- TIDAL catalog information for a video.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "id": <int>,
- "title": <str>,
- "volumeNumber": <int>,
- "trackNumber": <int>,
- "releaseDate": <str>,
- "imagePath": <str>,
- "imageId": <str>,
- "vibrantColor": <str>,
- "duration": <int>,
- "quality": <str>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "explicit": <bool>,
- "popularity": <int>,
- "type": <str>,
- "adsUrl": <str>,
- "adsPrePaywallOnly": <bool>,
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>,
- },
- "artists": [
- {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>,
- }
- ],
- "album": <dict>
- }
- """
-
- self._check_scope("get_video", "r_usr", flows={"device_code"},
- require_authentication=False)
-
- return self._get_json(
- f"{self.API_URL}/v1/videos/{video_id}",
- params={"countryCode": self._get_country_code(country_code)}
- )
-
-
-
-[docs]
- def get_video_playback_info(
- self, video_id: Union[int, str], *, video_quality: str = "HIGH",
- playback_mode: str = "STREAM", asset_presentation: str = "FULL",
- streaming_session_id: str = None) -> dict[str, Any]:
-
- """
- Get playback information for a video.
-
- .. admonition:: User authentication, authorization scope, and
- subscription
- :class: dropdown warning
-
- Requires the :code:`r_usr` authorization scope if the device
- code flow was used.
-
- Full video playback information is only available with user
- authentication and an active TIDAL subscription.
-
- Parameters
- ----------
- video_id : `int` or `str`
- TIDAL video ID.
-
- **Example**: :code:`59727844`.
-
- video_quality : `str`, keyword-only, default: :code:`"HIGH"`
- Video quality.
-
- **Valid values**: :code:`"AUDIO_ONLY"`, :code:`"LOW"`,
- :code:`"MEDIUM"`, and :code:`"HIGH"`.
-
- playback_mode : `str`, keyword-only, default: :code:`"STREAM"`
- Playback mode.
-
- **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`.
-
- asset_presentation : `str`, keyword-only, default: :code:`"FULL"`
- Asset presentation.
-
- .. container::
-
- **Valid values**:
-
- * :code:`"FULL"`: Full video.
- * :code:`"PREVIEW"`: 30-second preview of the video.
-
- streaming_session_id : `str`, keyword-only, optional
- Streaming session ID.
-
- Returns
- -------
- info : `dict`
- Video playback information.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "videoId": <int>,
- "streamType": <str>,
- "assetPresentation": <str>,
- "videoQuality": <str>,
- "manifestMimeType": <str>,
- "manifestHash": <str>,
- "manifest": <str>
- }
- """
-
- self._check_scope("get_video_playback_info", "r_usr",
- flows={"device_code"}, require_authentication=False)
-
- if video_quality not in \
- (VIDEO_QUALITIES := {"AUDIO_ONLY", "LOW", "MEDIUM", "HIGH"}):
- emsg = ("Invalid video quality. Valid values: "
- f"are{', '.join(VIDEO_QUALITIES)}.")
- raise ValueError(emsg)
- if playback_mode not in (PLAYBACK_MODES := {"STREAM", "OFFLINE"}):
- emsg = ("Invalid playback mode. Valid values: "
- f"modes are {', '.join(PLAYBACK_MODES)}.")
- raise ValueError(emsg)
- if asset_presentation not in \
- (ASSET_PRESENTATIONS := {"FULL", "PREVIEW"}):
- emsg = ("Invalid asset presentation. Valid values: "
- "presentations are "
- f"{', '.join(ASSET_PRESENTATIONS)}.")
- raise ValueError(emsg)
-
- url = f"{self.API_URL}/v1/videos/{video_id}/playbackinfo"
- url += "postpaywall" if self._flow else "prepaywall"
- return self._get_json(
- url,
- params={
- "videoquality": video_quality,
- "assetpresentation": asset_presentation,
- "playbackmode": playback_mode,
- "streamingsessionid": streaming_session_id
- }
- )
-
-
-
-[docs]
- def get_favorite_videos(
- self, country_code: str = None, *, limit: int = 50,
- offset: int = None, order: str = "DATE",
- order_direction: str = "DESC"):
-
- """
- Get TIDAL catalog information for videos in the current user's
- collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- limit : `int`, keyword-only, default: :code:`50`
- Page size.
-
- **Example**: :code:`10`.
-
- offset : `int`, keyword-only, optional
- Pagination offset (in number of items).
-
- **Example**: :code:`0`.
-
- order : `str`, keyword-only, default: :code:`"DATE"`
- Sorting order.
-
- **Valid values**: :code:`"DATE"` and :code:`"NAME"`.
-
- order_direction : `str`, keyword-only, default: :code:`"DESC"`
- Sorting order direction.
-
- **Valid values**: :code:`"DESC"` and :code:`"ASC"`.
-
- Returns
- -------
- videos : `dict`
- A dictionary containing TIDAL catalog information for videos
- in the current user's collection and metadata for the
- returned results.
-
- .. admonition:: Sample response
- :class: dropdown
-
- .. code::
-
- {
- "limit": <int>,
- "offset": <int>,
- "totalNumberOfItems": <int>,
- "items": [
- {
- "created": <str>,
- "item": {
- "id": <int>,
- "title": <str>,
- "volumeNumber": <int>,
- "trackNumber": <int>,
- "releaseDate": <str>,
- "imagePath": <str>,
- "imageId": <str>,
- "vibrantColor": <str>,
- "duration": <int>,
- "quality": <str>,
- "streamReady": <bool>,
- "adSupportedStreamReady": <bool>,
- "djReady": <bool>,
- "stemReady": <bool>,
- "streamStartDate": <str>,
- "allowStreaming": <bool>,
- "explicit": <bool>,
- "popularity": <int>,
- "type": <str>,
- "adsUrl": <str>,
- "adsPrePaywallOnly": <bool>,
- "artist": {
- "id": <int>,
- "name": <str>,
- "type": <str>,
- "picture": <str>
- },
- "artists": [
- {
- "id": <int>,
- "name": "<str>,
- "type": <str>,
- "picture": <str>
- }
- ],
- "album": <dict>
- }
- }
- ]
- }
- """
-
- self._check_scope("get_favorite_videos", "r_usr",
- flows={"device_code"})
-
- return self._get_json(
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/videos",
- params={
- "countryCode": self._get_country_code(country_code),
- "limit": limit,
- "offset": offset,
- "order": order,
- "orderDirection": order_direction
- }
- )
-
-
-
-[docs]
- def favorite_videos(
- self, video_ids: Union[int, str, list[Union[int, str]]],
- country_code: str = None, *, on_artifact_not_found: str = "FAIL"
- ) -> None:
-
- """
- Add videos to the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- video_ids : `int`, `str`, or `list`
- TIDAL video ID(s).
-
- **Examples**: :code:`"59727844,75623239"` or
- :code:`[59727844, 75623239]`.
-
- country_code : `str`, optional
- ISO 3166-1 alpha-2 country code. If not provided, the
- country code associated with the user account in the current
- session or the current IP address will be used instead.
-
- **Example**: :code:`"US"`.
-
- on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"`
- Behavior when the item to be added does not exist.
-
- **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`.
- """
-
- self._check_scope("favorite_videos", "r_usr", flows={"device_code"})
-
- self._request(
- "post",
- f"{self.API_URL}/v1/users/{self._user_id}/favorites/videos",
- params={"countryCode": self._get_country_code(country_code)},
- data={
- "videoIds": ",".join(map(str, video_ids))
- if isinstance(video_ids, list) else video_ids,
- "onArtifactNotFound": on_artifact_not_found
- }
- )
-
-
-
-[docs]
- def unfavorite_videos(
- self, video_ids: Union[int, str, list[Union[int, str]]]) -> None:
-
- """
- Remove videos from the current user's collection.
-
- .. admonition:: User authentication and authorization scope
- :class: warning
-
- Requires user authentication and the :code:`r_usr`
- authorization scope if the device code flow was used.
-
- Parameters
- ----------
- video_ids : `int`, `str`, or `list`
- TIDAL video ID(s).
-
- **Examples**: :code:`"59727844,75623239"` or
- :code:`[59727844, 75623239]`.
- """
-
- self._check_scope("unfavorite_videos", "r_usr", flows={"device_code"})
-
- if isinstance(video_ids, list):
- video_ids = ",".join(map(str, video_ids))
- self._request("delete",
- f"{self.API_URL}/v1/users/{self._user_id}"
- f"/favorites/videos/{video_ids}")
-
-
-
-"""
-Utility functions
-=================
-.. moduleauthor:: Benjamin Ye <GitHub: bbye98>
-
-This module contains a collection of utility functions.
-"""
-
-from difflib import SequenceMatcher
-from typing import Any, Union
-
-try:
- import Levenshtein
- FOUND_LEVENSHTEIN = True
-except ModuleNotFoundError:
- FOUND_LEVENSHTEIN = False
-try:
- import numpy as np
- FOUND_NUMPY = True
-except ModuleNotFoundError:
- FOUND_NUMPY = False
-
-__all__ = ["format_multivalue", "gestalt_ratio", "levenshtein_ratio"]
-
-
-[docs]
-def format_multivalue(
- value: Any, multivalue: bool, *, primary: bool = False,
- sep: Union[str, tuple[str]] = (", ", " & ")) -> Union[str, list[Any]]:
-
- """
- Format a field value based on whether multivalue for that field is
- supported.
-
- Parameters
- ----------
- value : `Any`
- Field value to format.
-
- multivalue : `bool`
- Determines whether multivalue tags are supported. If
- :code:`False`, the items in `value` are concatenated using the
- separator(s) specified in `sep`.
-
- primary : `bool`, keyword-only, default: :code:`False`
- Specifies whether the first item in `value` should be used when
- `value` is a `list` and :code:`multivalue=False`.
-
- sep : `str` or `tuple`, keyword-only, default: :code:`(", ", " & ")`
- Separator(s) to use to concatenate multivalue tags. If a
- :code:`str` is provided, it is used to concatenate all values.
- If a :code:`tuple` is provided, the first :code:`str` is used to
- concatenate all values except the last, and the second
- :code:`str` is used to append the final value.
-
- Returns
- -------
- value : `Any`
- Formatted field value.
- """
-
- if isinstance(value, list):
- if not multivalue:
- if len(value) == 1 or primary:
- return value[0]
- elif isinstance(value[0], str):
- if isinstance(sep, str):
- return sep.join(value)
- return f"{sep[0].join(value[:-1])}{sep[1]}{value[-1]}"
- elif multivalue:
- return [value]
- return value
-
-
-
-[docs]
-def gestalt_ratio(
- reference: str, strings: Union[str, list[str]]
- ) -> Union[float, list[float], "np.ndarray[float]"]:
-
- """
- Compute the Gestalt or Ratcliff–Obershelp ratios, a measure of
- similarity, for strings with respect to a reference string.
-
- Parameters
- ----------
- reference : `str`
- Reference string.
-
- strings : `str` or `list`
- Strings to compare with `reference`.
-
- Returns
- -------
- ratios : `float`, `list`, or `numpy.ndarray`
- Gestalt or Ratcliff–Obershelp ratios. If `strings` is a `str`, a
- `float` is returned. If `strings` is a `list`, a `numpy.ndarray`
- is returned if NumPy is installed; otherwise, a `list` is
- returned.
- """
-
- if isinstance(strings, str):
- return SequenceMatcher(None, reference, strings).ratio()
- gen = (SequenceMatcher(None, reference, s).ratio() for s in strings)
- if FOUND_NUMPY:
- return np.fromiter(gen, dtype=float, count=len(strings))
- return list(gen)
-
-
-
-[docs]
-def levenshtein_ratio(
- reference: str, strings: Union[str, list[str]]
- ) -> Union[float, list[float], "np.ndarray[float]"]:
-
- """
- Compute the Levenshtein ratios, a measure of similarity, for
- strings with respect to a reference string.
-
- Parameters
- ----------
- reference : `str`
- Reference string.
-
- strings : `str` or `list`
- Strings to compare with `reference`.
-
- Returns
- -------
- ratios : `float`, `list`, or `numpy.ndarray`
- Levenshtein ratios. If `strings` is a `str`, a `float` is
- returned. If `strings` is a `list`, a `numpy.ndarray` is
- returned if NumPy is installed; otherwise, a `list` is returned.
- """
-
- if not FOUND_LEVENSHTEIN:
- emsg = ("The Levenshtein module was not found, so "
- "minim.utility.levenshtein_ratio() is unavailable.")
- raise ImportError(emsg)
-
- if isinstance(strings, str):
- return Levenshtein.ratio(reference, strings)
- gen = (Levenshtein.ratio(reference, s) for s in strings)
- if FOUND_NUMPY:
- return np.fromiter(gen, dtype=float, count=len(strings))
- return list(gen)
-
-Short
- */ - .o-tooltip--left { - position: relative; - } - - .o-tooltip--left:after { - opacity: 0; - visibility: hidden; - position: absolute; - content: attr(data-tooltip); - padding: .2em; - font-size: .8em; - left: -.2em; - background: grey; - color: white; - white-space: nowrap; - z-index: 2; - border-radius: 2px; - transform: translateX(-102%) translateY(0); - transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); -} - -.o-tooltip--left:hover:after { - display: block; - opacity: 1; - visibility: visible; - transform: translateX(-100%) translateY(0); - transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); - transition-delay: .5s; -} - -/* By default the copy button shouldn't show up when printing a page */ -@media print { - button.copybtn { - display: none; - } -} diff --git a/docs/_static/copybutton.js b/docs/_static/copybutton.js deleted file mode 100644 index 2ea7ff3..0000000 --- a/docs/_static/copybutton.js +++ /dev/null @@ -1,248 +0,0 @@ -// Localization support -const messages = { - 'en': { - 'copy': 'Copy', - 'copy_to_clipboard': 'Copy to clipboard', - 'copy_success': 'Copied!', - 'copy_failure': 'Failed to copy', - }, - 'es' : { - 'copy': 'Copiar', - 'copy_to_clipboard': 'Copiar al portapapeles', - 'copy_success': '¡Copiado!', - 'copy_failure': 'Error al copiar', - }, - 'de' : { - 'copy': 'Kopieren', - 'copy_to_clipboard': 'In die Zwischenablage kopieren', - 'copy_success': 'Kopiert!', - 'copy_failure': 'Fehler beim Kopieren', - }, - 'fr' : { - 'copy': 'Copier', - 'copy_to_clipboard': 'Copier dans le presse-papier', - 'copy_success': 'Copié !', - 'copy_failure': 'Échec de la copie', - }, - 'ru': { - 'copy': 'Скопировать', - 'copy_to_clipboard': 'Скопировать в буфер', - 'copy_success': 'Скопировано!', - 'copy_failure': 'Не удалось скопировать', - }, - 'zh-CN': { - 'copy': '复制', - 'copy_to_clipboard': '复制到剪贴板', - 'copy_success': '复制成功!', - 'copy_failure': '复制失败', - }, - 'it' : { - 'copy': 'Copiare', - 'copy_to_clipboard': 'Copiato negli appunti', - 'copy_success': 'Copiato!', - 'copy_failure': 'Errore durante la copia', - } -} - -let locale = 'en' -if( document.documentElement.lang !== undefined - && messages[document.documentElement.lang] !== undefined ) { - locale = document.documentElement.lang -} - -let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; -if (doc_url_root == '#') { - doc_url_root = ''; -} - -/** - * SVG files for our copy buttons - */ -let iconCheck = `' + - '' + - _("Hide Search Matches") + - "
" - ) - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms") - }, - - initEscapeListener: () => { - // only install a listener if it is really needed - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; - if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { - SphinxHighlight.hideSearchWords(); - event.preventDefault(); - } - }); - }, -}; - -_ready(() => { - /* Do not call highlightSearchWords() when we are on the search page. - * It will highlight words from the *previous* search query. - */ - if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); - SphinxHighlight.initEscapeListener(); -}); diff --git a/docs/_static/styles/furo-extensions.css b/docs/_static/styles/furo-extensions.css deleted file mode 100644 index bc447f2..0000000 --- a/docs/_static/styles/furo-extensions.css +++ /dev/null @@ -1,2 +0,0 @@ -#furo-sidebar-ad-placement{padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)}#furo-sidebar-ad-placement .ethical-sidebar{background:var(--color-background-secondary);border:none;box-shadow:none}#furo-sidebar-ad-placement .ethical-sidebar:hover{background:var(--color-background-hover)}#furo-sidebar-ad-placement .ethical-sidebar a{color:var(--color-foreground-primary)}#furo-sidebar-ad-placement .ethical-callout a{color:var(--color-foreground-secondary)!important}#furo-readthedocs-versions{background:transparent;display:block;position:static;width:100%}#furo-readthedocs-versions .rst-versions{background:#1a1c1e}#furo-readthedocs-versions .rst-current-version{background:var(--color-sidebar-item-background);cursor:unset}#furo-readthedocs-versions .rst-current-version:hover{background:var(--color-sidebar-item-background)}#furo-readthedocs-versions .rst-current-version .fa-book{color:var(--color-foreground-primary)}#furo-readthedocs-versions>.rst-other-versions{padding:0}#furo-readthedocs-versions>.rst-other-versions small{opacity:1}#furo-readthedocs-versions .injected .rst-versions{position:unset}#furo-readthedocs-versions:focus-within,#furo-readthedocs-versions:hover{box-shadow:0 0 0 1px var(--color-sidebar-background-border)}#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:hover .rst-current-version{background:#1a1c1e;font-size:inherit;height:auto;line-height:inherit;padding:12px;text-align:right}#furo-readthedocs-versions:focus-within .rst-current-version .fa-book,#furo-readthedocs-versions:hover .rst-current-version .fa-book{color:#fff;float:left}#furo-readthedocs-versions:focus-within .fa-caret-down,#furo-readthedocs-versions:hover .fa-caret-down{display:none}#furo-readthedocs-versions:focus-within .injected,#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:focus-within .rst-other-versions,#furo-readthedocs-versions:hover .injected,#furo-readthedocs-versions:hover .rst-current-version,#furo-readthedocs-versions:hover .rst-other-versions{display:block}#furo-readthedocs-versions:focus-within>.rst-current-version,#furo-readthedocs-versions:hover>.rst-current-version{display:none}.highlight:hover button.copybtn{color:var(--color-code-foreground)}.highlight button.copybtn{align-items:center;background-color:var(--color-code-background);border:none;color:var(--color-background-item);cursor:pointer;height:1.25em;opacity:1;right:.5rem;top:.625rem;transition:color .3s,opacity .3s;width:1.25em}.highlight button.copybtn:hover{background-color:var(--color-code-background);color:var(--color-brand-content)}.highlight button.copybtn:after{background-color:transparent;color:var(--color-code-foreground);display:none}.highlight button.copybtn.success{color:#22863a;transition:color 0ms}.highlight button.copybtn.success:after{display:block}.highlight button.copybtn svg{padding:0}body{--sd-color-primary:var(--color-brand-primary);--sd-color-primary-highlight:var(--color-brand-content);--sd-color-primary-text:var(--color-background-primary);--sd-color-shadow:rgba(0,0,0,.05);--sd-color-card-border:var(--color-card-border);--sd-color-card-border-hover:var(--color-brand-content);--sd-color-card-background:var(--color-card-background);--sd-color-card-text:var(--color-foreground-primary);--sd-color-card-header:var(--color-card-marginals-background);--sd-color-card-footer:var(--color-card-marginals-background);--sd-color-tabs-label-active:var(--color-brand-content);--sd-color-tabs-label-hover:var(--color-foreground-muted);--sd-color-tabs-label-inactive:var(--color-foreground-muted);--sd-color-tabs-underline-active:var(--color-brand-content);--sd-color-tabs-underline-hover:var(--color-foreground-border);--sd-color-tabs-underline-inactive:var(--color-background-border);--sd-color-tabs-overline:var(--color-background-border);--sd-color-tabs-underline:var(--color-background-border)}.sd-tab-content{box-shadow:0 -2px var(--sd-color-tabs-overline),0 1px var(--sd-color-tabs-underline)}.sd-card{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)}.sd-shadow-sm{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-md{box-shadow:0 .3rem .75rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-lg{box-shadow:0 .6rem 1.5rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-card-hover:hover{transform:none}.sd-cards-carousel{gap:.25rem;padding:.25rem}body{--tabs--label-text:var(--color-foreground-muted);--tabs--label-text--hover:var(--color-foreground-muted);--tabs--label-text--active:var(--color-brand-content);--tabs--label-text--active--hover:var(--color-brand-content);--tabs--label-background:transparent;--tabs--label-background--hover:transparent;--tabs--label-background--active:transparent;--tabs--label-background--active--hover:transparent;--tabs--padding-x:0.25em;--tabs--margin-x:1em;--tabs--border:var(--color-background-border);--tabs--label-border:transparent;--tabs--label-border--hover:var(--color-foreground-muted);--tabs--label-border--active:var(--color-brand-content);--tabs--label-border--active--hover:var(--color-brand-content)}[role=main] .container{max-width:none;padding-left:0;padding-right:0}.shadow.docutils{border:none;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)!important}.sphinx-bs .card{background-color:var(--color-background-secondary);color:var(--color-foreground)} -/*# sourceMappingURL=furo-extensions.css.map*/ \ No newline at end of file diff --git a/docs/_static/styles/furo-extensions.css.map b/docs/_static/styles/furo-extensions.css.map deleted file mode 100644 index 9ba5637..0000000 --- a/docs/_static/styles/furo-extensions.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"styles/furo-extensions.css","mappings":"AAGA,2BACE,oFACA,4CAKE,6CAHA,YACA,eAEA,CACA,kDACE,yCAEF,8CACE,sCAEJ,8CACE,kDAEJ,2BAGE,uBACA,cAHA,gBACA,UAEA,CAGA,yCACE,mBAEF,gDAEE,gDADA,YACA,CACA,sDACE,gDACF,yDACE,sCAEJ,+CACE,UACA,qDACE,UAGF,mDACE,eAEJ,yEAEE,4DAEA,mHASE,mBAPA,kBAEA,YADA,oBAGA,aADA,gBAIA,CAEA,qIAEE,WADA,UACA,CAEJ,uGACE,aAEF,iUAGE,cAEF,mHACE,aC1EJ,gCACE,mCAEF,0BAKE,mBAUA,8CACA,YAFA,mCAKA,eAZA,cALA,UASA,YADA,YAYA,iCAdA,YAcA,CAEA,gCAEE,8CADA,gCACA,CAEF,gCAGE,6BADA,mCADA,YAEA,CAEF,kCAEE,cADA,oBACA,CACA,wCACE,cAEJ,8BACE,UC5CN,KAEE,6CAA8C,CAC9C,uDAAwD,CACxD,uDAAwD,CAGxD,iCAAsC,CAGtC,+CAAgD,CAChD,uDAAwD,CACxD,uDAAwD,CACxD,oDAAqD,CACrD,6DAA8D,CAC9D,6DAA8D,CAG9D,uDAAwD,CACxD,yDAA0D,CAC1D,4DAA6D,CAC7D,2DAA4D,CAC5D,8DAA+D,CAC/D,iEAAkE,CAClE,uDAAwD,CACxD,wDAAyD,CAG3D,gBACE,qFAGF,SACE,6EAEF,cACE,uFAEF,cACE,uFAEF,cACE,uFAGF,qBACE,eAEF,mBACE,WACA,eChDF,KACE,gDAAiD,CACjD,uDAAwD,CACxD,qDAAsD,CACtD,4DAA6D,CAC7D,oCAAqC,CACrC,2CAA4C,CAC5C,4CAA6C,CAC7C,mDAAoD,CACpD,wBAAyB,CACzB,oBAAqB,CACrB,6CAA8C,CAC9C,gCAAiC,CACjC,yDAA0D,CAC1D,uDAAwD,CACxD,8DAA+D,CCbjE,uBACE,eACA,eACA,gBAGF,iBACE,YACA,+EAGF,iBACE,mDACA","sources":["webpack:///./src/furo/assets/styles/extensions/_readthedocs.sass","webpack:///./src/furo/assets/styles/extensions/_copybutton.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-design.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-inline-tabs.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-panels.sass"],"sourcesContent":["// This file contains the styles used for tweaking how ReadTheDoc's embedded\n// contents would show up inside the theme.\n\n#furo-sidebar-ad-placement\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n .ethical-sidebar\n // Remove the border and box-shadow.\n border: none\n box-shadow: none\n // Manage the background colors.\n background: var(--color-background-secondary)\n &:hover\n background: var(--color-background-hover)\n // Ensure the text is legible.\n a\n color: var(--color-foreground-primary)\n\n .ethical-callout a\n color: var(--color-foreground-secondary) !important\n\n#furo-readthedocs-versions\n position: static\n width: 100%\n background: transparent\n display: block\n\n // Make the background color fit with the theme's aesthetic.\n .rst-versions\n background: rgb(26, 28, 30)\n\n .rst-current-version\n cursor: unset\n background: var(--color-sidebar-item-background)\n &:hover\n background: var(--color-sidebar-item-background)\n .fa-book\n color: var(--color-foreground-primary)\n\n > .rst-other-versions\n padding: 0\n small\n opacity: 1\n\n .injected\n .rst-versions\n position: unset\n\n &:hover,\n &:focus-within\n box-shadow: 0 0 0 1px var(--color-sidebar-background-border)\n\n .rst-current-version\n // Undo the tweaks done in RTD's CSS\n font-size: inherit\n line-height: inherit\n height: auto\n text-align: right\n padding: 12px\n\n // Match the rest of the body\n background: #1a1c1e\n\n .fa-book\n float: left\n color: white\n\n .fa-caret-down\n display: none\n\n .rst-current-version,\n .rst-other-versions,\n .injected\n display: block\n\n > .rst-current-version\n display: none\n",".highlight\n &:hover button.copybtn\n color: var(--color-code-foreground)\n\n button.copybtn\n // Make it visible\n opacity: 1\n\n // Align things correctly\n align-items: center\n\n height: 1.25em\n width: 1.25em\n\n top: 0.625rem // $code-spacing-vertical\n right: 0.5rem\n\n // Make it look better\n color: var(--color-background-item)\n background-color: var(--color-code-background)\n border: none\n\n // Change to cursor to make it obvious that you can click on it\n cursor: pointer\n\n // Transition smoothly, for aesthetics\n transition: color 300ms, opacity 300ms\n\n &:hover\n color: var(--color-brand-content)\n background-color: var(--color-code-background)\n\n &::after\n display: none\n color: var(--color-code-foreground)\n background-color: transparent\n\n &.success\n transition: color 0ms\n color: #22863a\n &::after\n display: block\n\n svg\n padding: 0\n","body\n // Colors\n --sd-color-primary: var(--color-brand-primary)\n --sd-color-primary-highlight: var(--color-brand-content)\n --sd-color-primary-text: var(--color-background-primary)\n\n // Shadows\n --sd-color-shadow: rgba(0, 0, 0, 0.05)\n\n // Cards\n --sd-color-card-border: var(--color-card-border)\n --sd-color-card-border-hover: var(--color-brand-content)\n --sd-color-card-background: var(--color-card-background)\n --sd-color-card-text: var(--color-foreground-primary)\n --sd-color-card-header: var(--color-card-marginals-background)\n --sd-color-card-footer: var(--color-card-marginals-background)\n\n // Tabs\n --sd-color-tabs-label-active: var(--color-brand-content)\n --sd-color-tabs-label-hover: var(--color-foreground-muted)\n --sd-color-tabs-label-inactive: var(--color-foreground-muted)\n --sd-color-tabs-underline-active: var(--color-brand-content)\n --sd-color-tabs-underline-hover: var(--color-foreground-border)\n --sd-color-tabs-underline-inactive: var(--color-background-border)\n --sd-color-tabs-overline: var(--color-background-border)\n --sd-color-tabs-underline: var(--color-background-border)\n\n// Tabs\n.sd-tab-content\n box-shadow: 0 -2px var(--sd-color-tabs-overline), 0 1px var(--sd-color-tabs-underline)\n\n// Shadows\n.sd-card // Have a shadow by default\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n.sd-shadow-sm\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-md\n box-shadow: 0 0.3rem 0.75rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-lg\n box-shadow: 0 0.6rem 1.5rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Cards\n.sd-card-hover:hover // Don't change scale on hover\n transform: none\n\n.sd-cards-carousel // Have a bit of gap in the carousel by default\n gap: 0.25rem\n padding: 0.25rem\n","// This file contains styles to tweak sphinx-inline-tabs to work well with Furo.\n\nbody\n --tabs--label-text: var(--color-foreground-muted)\n --tabs--label-text--hover: var(--color-foreground-muted)\n --tabs--label-text--active: var(--color-brand-content)\n --tabs--label-text--active--hover: var(--color-brand-content)\n --tabs--label-background: transparent\n --tabs--label-background--hover: transparent\n --tabs--label-background--active: transparent\n --tabs--label-background--active--hover: transparent\n --tabs--padding-x: 0.25em\n --tabs--margin-x: 1em\n --tabs--border: var(--color-background-border)\n --tabs--label-border: transparent\n --tabs--label-border--hover: var(--color-foreground-muted)\n --tabs--label-border--active: var(--color-brand-content)\n --tabs--label-border--active--hover: var(--color-brand-content)\n","// This file contains styles to tweak sphinx-panels to work well with Furo.\n\n// sphinx-panels includes Bootstrap 4, which uses .container which can conflict\n// with docutils' `.. container::` directive.\n[role=\"main\"] .container\n max-width: initial\n padding-left: initial\n padding-right: initial\n\n// Make the panels look nicer!\n.shadow.docutils\n border: none\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Make panel colors respond to dark mode\n.sphinx-bs .card\n background-color: var(--color-background-secondary)\n color: var(--color-foreground)\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/docs/_static/styles/furo.css b/docs/_static/styles/furo.css deleted file mode 100644 index 3d29a21..0000000 --- a/docs/_static/styles/furo.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}@media print{.content-icon-container,.headerlink,.mobile-header,.related-pages{display:none!important}.highlight{border:.1pt solid var(--color-foreground-border)}a,blockquote,dl,ol,pre,table,ul{page-break-inside:avoid}caption,figure,h1,h2,h3,h4,h5,h6,img{page-break-after:avoid;page-break-inside:avoid}dl,ol,ul{page-break-before:avoid}}.visually-hidden{clip:rect(0,0,0,0)!important;border:0!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:1px!important}:-moz-focusring{outline:auto}body{--font-stack:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;--font-stack--monospace:"SFMono-Regular",Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace;--font-size--normal:100%;--font-size--small:87.5%;--font-size--small--2:81.25%;--font-size--small--3:75%;--font-size--small--4:62.5%;--sidebar-caption-font-size:var(--font-size--small--2);--sidebar-item-font-size:var(--font-size--small);--sidebar-search-input-font-size:var(--font-size--small);--toc-font-size:var(--font-size--small--3);--toc-font-size--mobile:var(--font-size--normal);--toc-title-font-size:var(--font-size--small--4);--admonition-font-size:0.8125rem;--admonition-title-font-size:0.8125rem;--code-font-size:var(--font-size--small--2);--api-font-size:var(--font-size--small);--header-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*4);--header-padding:0.5rem;--sidebar-tree-space-above:1.5rem;--sidebar-caption-space-above:1rem;--sidebar-item-line-height:1rem;--sidebar-item-spacing-vertical:0.5rem;--sidebar-item-spacing-horizontal:1rem;--sidebar-item-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*2);--sidebar-expander-width:var(--sidebar-item-height);--sidebar-search-space-above:0.5rem;--sidebar-search-input-spacing-vertical:0.5rem;--sidebar-search-input-spacing-horizontal:0.5rem;--sidebar-search-input-height:1rem;--sidebar-search-icon-size:var(--sidebar-search-input-height);--toc-title-padding:0.25rem 0;--toc-spacing-vertical:1.5rem;--toc-spacing-horizontal:1.5rem;--toc-item-spacing-vertical:0.4rem;--toc-item-spacing-horizontal:1rem;--icon-search:url('data:image/svg+xml;charset=utf-8,