diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4aeed34..5806767 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,32 +12,31 @@ env: TIDAL_PRIVATE_CLIENT_SECRET: ${{ secrets.TIDAL_PRIVATE_CLIENT_SECRET }} jobs: build: - name: Continuous integration (Python ${{ matrix.python-version }}) + name: continuous-integration-${{ matrix.os }}-python-${{ matrix.python-version }} strategy: matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-latest] python-version: ["3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} defaults: run: shell: bash -el {0} timeout-minutes: 60 - steps: + steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - uses: FedericoCarboni/setup-ffmpeg@v2 id: setup-ffmpeg - - name: Install required dependencies using pip - run: | - python3 -m pip install -r requirements_minimal.txt - - name: Lint with ruff + - name: pip-install-dependencies + run: python3 -m pip install -r requirements_minimal.txt + - name: ruff-lint run: | python3 -m pip install ruff - ruff --target-version=py39 . + ruff check --target-version=py39 . continue-on-error: true - - name: Test with coverage and pytest + - name: pytest-test run: | - python3 -m pip install coverage pytest - coverage run -m pytest \ No newline at end of file + python3 -m pip install pytest + pytest \ No newline at end of file diff --git a/docs/.doctrees/api.doctree b/docs/.doctrees/api.doctree index 1eb9f07..cc5d927 100644 Binary files a/docs/.doctrees/api.doctree and b/docs/.doctrees/api.doctree differ diff --git a/docs/.doctrees/api/minim.audio.Audio.doctree b/docs/.doctrees/api/minim.audio.Audio.doctree index 6172c8e..a8821cf 100644 Binary files a/docs/.doctrees/api/minim.audio.Audio.doctree and b/docs/.doctrees/api/minim.audio.Audio.doctree differ diff --git a/docs/.doctrees/api/minim.audio.FLACAudio.doctree b/docs/.doctrees/api/minim.audio.FLACAudio.doctree index 282cd2a..a46b40f 100644 Binary files a/docs/.doctrees/api/minim.audio.FLACAudio.doctree and b/docs/.doctrees/api/minim.audio.FLACAudio.doctree differ diff --git a/docs/.doctrees/api/minim.audio.MP3Audio.doctree b/docs/.doctrees/api/minim.audio.MP3Audio.doctree index c1c6293..b23b770 100644 Binary files a/docs/.doctrees/api/minim.audio.MP3Audio.doctree and b/docs/.doctrees/api/minim.audio.MP3Audio.doctree differ diff --git a/docs/.doctrees/api/minim.audio.MP4Audio.doctree b/docs/.doctrees/api/minim.audio.MP4Audio.doctree index f26cf5d..4c46b52 100644 Binary files a/docs/.doctrees/api/minim.audio.MP4Audio.doctree and b/docs/.doctrees/api/minim.audio.MP4Audio.doctree differ diff --git a/docs/.doctrees/api/minim.audio.OGGAudio.doctree b/docs/.doctrees/api/minim.audio.OGGAudio.doctree index ff7c570..b0d39dc 100644 Binary files a/docs/.doctrees/api/minim.audio.OGGAudio.doctree and b/docs/.doctrees/api/minim.audio.OGGAudio.doctree differ diff --git a/docs/.doctrees/api/minim.audio.WAVEAudio.doctree b/docs/.doctrees/api/minim.audio.WAVEAudio.doctree index fd36b09..b01ad26 100644 Binary files a/docs/.doctrees/api/minim.audio.WAVEAudio.doctree and b/docs/.doctrees/api/minim.audio.WAVEAudio.doctree differ diff --git a/docs/.doctrees/api/minim.audio.doctree b/docs/.doctrees/api/minim.audio.doctree index 37992fd..143db08 100644 Binary files a/docs/.doctrees/api/minim.audio.doctree and b/docs/.doctrees/api/minim.audio.doctree differ diff --git a/docs/.doctrees/api/minim.discogs.API.doctree b/docs/.doctrees/api/minim.discogs.API.doctree index be6f3b1..02c7e61 100644 Binary files a/docs/.doctrees/api/minim.discogs.API.doctree and b/docs/.doctrees/api/minim.discogs.API.doctree differ diff --git a/docs/.doctrees/api/minim.discogs.doctree b/docs/.doctrees/api/minim.discogs.doctree index 3ba80cd..fd76e43 100644 Binary files a/docs/.doctrees/api/minim.discogs.doctree and b/docs/.doctrees/api/minim.discogs.doctree differ diff --git a/docs/.doctrees/api/minim.doctree b/docs/.doctrees/api/minim.doctree index 4415aed..1701081 100644 Binary files a/docs/.doctrees/api/minim.doctree and b/docs/.doctrees/api/minim.doctree differ diff --git a/docs/.doctrees/api/minim.itunes.SearchAPI.doctree b/docs/.doctrees/api/minim.itunes.SearchAPI.doctree index 61a2772..cfe58cb 100644 Binary files a/docs/.doctrees/api/minim.itunes.SearchAPI.doctree and b/docs/.doctrees/api/minim.itunes.SearchAPI.doctree differ diff --git a/docs/.doctrees/api/minim.itunes.doctree b/docs/.doctrees/api/minim.itunes.doctree index fc62aef..600f32c 100644 Binary files a/docs/.doctrees/api/minim.itunes.doctree and b/docs/.doctrees/api/minim.itunes.doctree differ diff --git a/docs/.doctrees/api/minim.qobuz.PrivateAPI.doctree b/docs/.doctrees/api/minim.qobuz.PrivateAPI.doctree index 5a8cba1..253cd6d 100644 Binary files a/docs/.doctrees/api/minim.qobuz.PrivateAPI.doctree and b/docs/.doctrees/api/minim.qobuz.PrivateAPI.doctree differ diff --git a/docs/.doctrees/api/minim.qobuz.doctree b/docs/.doctrees/api/minim.qobuz.doctree index e953de7..549572b 100644 Binary files a/docs/.doctrees/api/minim.qobuz.doctree and b/docs/.doctrees/api/minim.qobuz.doctree differ diff --git a/docs/.doctrees/api/minim.spotify.PrivateLyricsService.doctree b/docs/.doctrees/api/minim.spotify.PrivateLyricsService.doctree index 26bfc23..2cb6ddb 100644 Binary files a/docs/.doctrees/api/minim.spotify.PrivateLyricsService.doctree and b/docs/.doctrees/api/minim.spotify.PrivateLyricsService.doctree differ diff --git a/docs/.doctrees/api/minim.spotify.WebAPI.doctree b/docs/.doctrees/api/minim.spotify.WebAPI.doctree index 75663fb..fde02f8 100644 Binary files a/docs/.doctrees/api/minim.spotify.WebAPI.doctree and b/docs/.doctrees/api/minim.spotify.WebAPI.doctree differ diff --git a/docs/.doctrees/api/minim.spotify.doctree b/docs/.doctrees/api/minim.spotify.doctree index 9cbd172..020c990 100644 Binary files a/docs/.doctrees/api/minim.spotify.doctree and b/docs/.doctrees/api/minim.spotify.doctree differ diff --git a/docs/.doctrees/api/minim.tidal.API.doctree b/docs/.doctrees/api/minim.tidal.API.doctree index 2691a84..bf75de1 100644 Binary files a/docs/.doctrees/api/minim.tidal.API.doctree and b/docs/.doctrees/api/minim.tidal.API.doctree differ diff --git a/docs/.doctrees/api/minim.tidal.PrivateAPI.doctree b/docs/.doctrees/api/minim.tidal.PrivateAPI.doctree index 55d8c4d..125a395 100644 Binary files a/docs/.doctrees/api/minim.tidal.PrivateAPI.doctree and b/docs/.doctrees/api/minim.tidal.PrivateAPI.doctree differ diff --git a/docs/.doctrees/api/minim.tidal.doctree b/docs/.doctrees/api/minim.tidal.doctree index 46eaaaa..cc51766 100644 Binary files a/docs/.doctrees/api/minim.tidal.doctree and b/docs/.doctrees/api/minim.tidal.doctree differ diff --git a/docs/.doctrees/api/minim.utility.doctree b/docs/.doctrees/api/minim.utility.doctree index 7deceac..0ad8d0b 100644 Binary files a/docs/.doctrees/api/minim.utility.doctree and b/docs/.doctrees/api/minim.utility.doctree differ diff --git a/docs/.doctrees/api/minim.utility.format_multivalue.doctree b/docs/.doctrees/api/minim.utility.format_multivalue.doctree index 5ba86c1..f33b876 100644 Binary files a/docs/.doctrees/api/minim.utility.format_multivalue.doctree and b/docs/.doctrees/api/minim.utility.format_multivalue.doctree differ diff --git a/docs/.doctrees/api/minim.utility.gestalt_ratio.doctree b/docs/.doctrees/api/minim.utility.gestalt_ratio.doctree index 7c4976a..aa0b47b 100644 Binary files a/docs/.doctrees/api/minim.utility.gestalt_ratio.doctree and b/docs/.doctrees/api/minim.utility.gestalt_ratio.doctree differ diff --git a/docs/.doctrees/api/minim.utility.levenshtein_ratio.doctree b/docs/.doctrees/api/minim.utility.levenshtein_ratio.doctree index 5c5aa32..154a828 100644 Binary files a/docs/.doctrees/api/minim.utility.levenshtein_ratio.doctree and b/docs/.doctrees/api/minim.utility.levenshtein_ratio.doctree differ diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle index 1b4e154..6770b0d 100644 Binary files a/docs/.doctrees/environment.pickle and b/docs/.doctrees/environment.pickle differ diff --git a/docs/.doctrees/index.doctree b/docs/.doctrees/index.doctree index ac1bdb2..adb59d6 100644 Binary files a/docs/.doctrees/index.doctree and b/docs/.doctrees/index.doctree differ diff --git a/docs/.doctrees/notebooks/getting_started.doctree b/docs/.doctrees/notebooks/getting_started.doctree index ecd3525..125ccc9 100644 Binary files a/docs/.doctrees/notebooks/getting_started.doctree and b/docs/.doctrees/notebooks/getting_started.doctree differ diff --git a/docs/.doctrees/notebooks/user_guide/editing_audio_metadata.doctree b/docs/.doctrees/notebooks/user_guide/editing_audio_metadata.doctree index 32341d9..fd2a3c4 100644 Binary files a/docs/.doctrees/notebooks/user_guide/editing_audio_metadata.doctree and b/docs/.doctrees/notebooks/user_guide/editing_audio_metadata.doctree differ diff --git a/docs/.doctrees/notebooks/user_guide/getting_recommendations.doctree b/docs/.doctrees/notebooks/user_guide/getting_recommendations.doctree index fe70e09..4d08344 100644 Binary files a/docs/.doctrees/notebooks/user_guide/getting_recommendations.doctree and b/docs/.doctrees/notebooks/user_guide/getting_recommendations.doctree differ diff --git a/docs/.doctrees/notebooks/user_guide/transferring_music_libraries.doctree b/docs/.doctrees/notebooks/user_guide/transferring_music_libraries.doctree index 07f9bd6..474b986 100644 Binary files a/docs/.doctrees/notebooks/user_guide/transferring_music_libraries.doctree and b/docs/.doctrees/notebooks/user_guide/transferring_music_libraries.doctree differ diff --git a/docs/.doctrees/user_guide.doctree b/docs/.doctrees/user_guide.doctree index 453ddc2..130a117 100644 Binary files a/docs/.doctrees/user_guide.doctree and b/docs/.doctrees/user_guide.doctree differ diff --git a/docs/_modules/index.html b/docs/_modules/index.html index 4451ae4..c33ca2d 100644 --- a/docs/_modules/index.html +++ b/docs/_modules/index.html @@ -4,7 +4,7 @@ - + Overview: module code - Minim 1.0.0 documentation diff --git a/docs/_modules/minim/audio.html b/docs/_modules/minim/audio.html index 07a34c3..3c420b8 100644 --- a/docs/_modules/minim/audio.html +++ b/docs/_modules/minim/audio.html @@ -4,7 +4,7 @@ - + minim.audio - Minim 1.0.0 documentation @@ -259,12 +259,16 @@

Source code for minim.audio

 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, FFMPEG_CODECS
+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
@@ -312,7 +316,7 @@ 

Source code for minim.audio

     }
 
     def __init__(self, filename: str, tags: id3.ID3) -> None:
-        
+
         """
         Create an ID3 tag handler.
         """
@@ -329,11 +333,11 @@ 

Source code for minim.audio

 
         for field, (frame, base, _) in self._FIELDS.items():
             value = self._tags.getall(frame)
-            if value: 
+            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, 
+                    value = utility.format_multivalue(value, False,
                                                       primary=True)
                     if not isinstance(value, self._FIELDS_TYPES[field]):
                         try:
@@ -344,7 +348,7 @@ 

Source code for minim.audio

                 else:
                     if not isinstance(value[0], self._FIELDS_TYPES[field]):
                         try:
-                            value = [self._FIELDS_TYPES[field][0](v) 
+                            value = [self._FIELDS_TYPES[field][0](v)
                                      for v in value]
                         except ValueError:
                             continue
@@ -408,7 +412,7 @@ 

Source code for minim.audio

                         **{base: func(value) if func else value}
                     )
                 )
-        
+
         if "TXXX:comment" in self._tags:
             self._tags.delall("TXXX:comment")
 
@@ -435,7 +439,7 @@ 

Source code for minim.audio

                         else open(self.artwork, "rb") as f:
                     self.artwork = f.read()
             self._tags.add(
-                id3.APIC(data=self.artwork, 
+                id3.APIC(data=self.artwork,
                          mime=IMAGE_FORMATS[self._artwork_format])
             )
 
@@ -485,7 +489,7 @@ 

Source code for minim.audio

     }
 
     def __init__(self, filename: str, tags: id3.ID3) -> None:
-        
+
         """
         Create a Vorbis comment handler.
         """
@@ -504,7 +508,7 @@ 

Source code for minim.audio

             value = self._tags.get(key)
             if value:
                 if list not in self._FIELDS_TYPES[field]:
-                    value = utility.format_multivalue(value, False, 
+                    value = utility.format_multivalue(value, False,
                                                       primary=True)
                     if type(value) not in self._FIELDS_TYPES[field]:
                         try:
@@ -514,7 +518,7 @@ 

Source code for minim.audio

                 else:
                     if type(value[0]) not in self._FIELDS_TYPES[field]:
                         try:
-                            value = [self._FIELDS_TYPES[field][0](v) 
+                            value = [self._FIELDS_TYPES[field][0](v)
                                      for v in value]
                         except ValueError:
                             continue
@@ -526,7 +530,7 @@ 

Source code for minim.audio

 
         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:
@@ -617,7 +621,7 @@ 

Source code for minim.audio

     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
@@ -628,7 +632,7 @@ 

Source code for minim.audio

       container,
     * :class:`OggAudio` for Opus or Vorbis audio stored in an Ogg file,
       and
-    * :class:`WAVEAudio` for audio encoded using linear pulse-code 
+    * :class:`WAVEAudio` for audio encoded using linear pulse-code
       modulation (LPCM) and in the Waveform Audio File Format (WAVE).
 
     .. note::
@@ -638,14 +642,14 @@ 

Source code for minim.audio

        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 
+       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).
@@ -653,16 +657,16 @@ 

Source code for minim.audio

         .. container::
 
            **Valid values**:
-           
+
            The supported metadata fields are
 
            * :code:`"artist"` for the track artist,
-           * :code:`"title"` for the track title, and 
+           * :code:`"title"` for the track title, and
            * :code:`"track_number"` for the track number.
 
            **Examples**:
 
-           * :code:`("(.*) - (.*)", ("artist", "title"))` matches 
+           * :code:`("(.*) - (.*)", ("artist", "title"))` matches
              filenames like "Taylor Swift - Cruel Summer.flac".
            * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
              filenames like "04 - The Man.m4a".
@@ -670,12 +674,12 @@ 

Source code for minim.audio

              filenames like "13 You Need to Calm Down.mp3".
 
     multivalue : `bool`
-        Determines whether multivalue tags are supported. If 
+        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 
+        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
@@ -685,13 +689,13 @@ 

Source code for minim.audio

     ----------
     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.
@@ -707,7 +711,7 @@ 

Source code for minim.audio

 
     codec : `str`
         Audio codec.
-    
+
     comment : `str`
         Comment(s).
 
@@ -734,7 +738,7 @@ 

Source code for minim.audio

 
     isrc : `str`
         International Standard Recording Code (ISRC).
-    
+
     lyrics : `str`
         Lyrics.
 
@@ -777,7 +781,7 @@ 

Source code for minim.audio

     }
 
     def __init__(
-            self, file: Union[str, pathlib.Path], *, 
+            self, file: Union[str, pathlib.Path], *,
             pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
             sep: Union[str, list[str]] = (", ", " & ")) -> None:
 
@@ -808,13 +812,13 @@ 

Source code for minim.audio

             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:
@@ -827,7 +831,7 @@ 

Source code for minim.audio

             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 
+                                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:
@@ -840,12 +844,12 @@ 

Source code for minim.audio

             filename: str = None, preserve: bool = True) -> None:
 
         """
-        Convert the current audio file to another format. 
+        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
@@ -863,22 +867,22 @@ 

Source code for minim.audio

 
                **Valid values**:
 
-               * :code:`"aac"`, :code:`"m4a"`, :code:`"mp4"`, or 
+               * :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 
+               * :code:`"lpcm"`, :code:`"wav"`, or :code:`"wave"` for
                  lossless LPCM audio.
 
         container : `str`, optional
-            New audio file container. If not specified, the best 
+            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
@@ -890,12 +894,12 @@ 

Source code for minim.audio

                  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 
+               * :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 
+            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).
 
@@ -903,24 +907,24 @@ 

Source code for minim.audio

 
                **Defaults**:
 
-               * AAC audio: :code:`"-c:a aac -b:a 256k"` (or 
+               * 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 
+               * 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 
+               * 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 
+            filename of the original audio file, but with the
             appropriate new extension appended, is used.
 
         preserve : `bool`, keyword-only, default: :code:`True`
@@ -941,7 +945,7 @@ 

Source code for minim.audio

             codec = "opus"
         elif codec in "wave":
             codec = "lpcm"
-        
+
         if container:
             container = container.lower()
             if container == "m4a":
@@ -950,8 +954,8 @@ 

Source code for minim.audio

                 container = "wav"
 
             try:
-                acls = next(a for a in Audio.__subclasses__() 
-                            if codec in a._CODECS 
+                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 "
@@ -959,12 +963,12 @@ 

Source code for minim.audio

                 raise RuntimeError(emsg)
         else:
             try:
-                acls = next(a for a in Audio.__subclasses__() 
+                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} "
@@ -1006,7 +1010,7 @@ 

Source code for minim.audio

         obj = acls(filename)
         self.__class__ = obj.__class__
         self.__dict__ = obj.__dict__ | {
-            key: value for (key, value) in self.__dict__.items() 
+            key: value for (key, value) in self.__dict__.items()
             if key in self._FIELDS_TYPES
         }
@@ -1014,7 +1018,7 @@

Source code for minim.audio

 
[docs] def set_metadata_using_itunes( - self, data: dict[str, Any], *, album_data: dict[str, Any] = None, + 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: @@ -1025,15 +1029,15 @@

Source code for minim.audio

         ----------
         data : `dict`
             Information about the track in JSON format obtained using
-            the iTunes Search API via 
+            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, 
+            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`
@@ -1078,11 +1082,18 @@ 

Source code for minim.audio

                 with urllib.request.urlopen(self.artwork) as r:
                     self.artwork = r.read()
                 if self._artwork_format == "tif":
-                    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"
+                    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):
@@ -1091,7 +1102,7 @@ 

Source code for minim.audio

             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:    
+        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"])
@@ -1168,7 +1179,7 @@ 

Source code for minim.audio

                 self.album_artist = data["album"]["artist"]["name"]
 
         credits = _parse_performers(
-            data["performers"], 
+            data["performers"],
             roles=["MainArtist", "FeaturedArtist", "Composers"]
         )
         if self.artist is None or overwrite:
@@ -1185,8 +1196,8 @@ 

Source code for minim.audio

             self.comment = comment
         if self.composer is None or overwrite:
             self.composer = (
-                credits.get("composers") 
-                or (data["composer"]["name"] if hasattr(data, "composer") 
+                credits.get("composers")
+                or (data["composer"]["name"] if hasattr(data, "composer")
                     else None)
             )
         if self.copyright is None or overwrite:
@@ -1197,11 +1208,11 @@ 

Source code for minim.audio

                 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_original",
+                        "release_date_download",
+                        "release_date_stream",
                         "release_date_purchase",
-                        "purchasable_at", 
+                        "purchasable_at",
                         "streamable_at"
                     }
                 )
@@ -1224,7 +1235,7 @@ 

Source code for minim.audio

                     utility.format_multivalue(feat_artist, False)
                 )
             if data["version"]:
-                self.title += (" [{}]" if "(" in self.title 
+                self.title += (" [{}]" if "(" in self.title
                                else " ({})").format(data['version'])
             self.title = self.title.replace("  ", " ")
         if self.track_number is None or overwrite:
@@ -1243,10 +1254,10 @@ 

Source code for minim.audio

 
[docs] def set_metadata_using_spotify( - self, data: dict[str, Any], *, - audio_features: dict[str, Any] = None, + self, data: dict[str, Any], *, + audio_features: dict[str, Any] = None, lyrics: Union[str, dict[str, Any]] = None, overwrite: bool = False - ) -> None: + ) -> None: """ Populate tags using data retrieved from the Spotify Web API @@ -1256,7 +1267,7 @@

Source code for minim.audio

         ----------
         data : `dict`
             Information about the track in JSON format obtained using
-            the Spotify Web API via 
+            the Spotify Web API via
             :meth:`minim.spotify.WebAPI.get_track`.
 
         audio_features : `dict`, keyword-only, optional
@@ -1267,7 +1278,7 @@ 

Source code for minim.audio

 
         lyrics : `str` or `dict`, keyword-only
             Information about the track's formatted or time-synced
-            lyrics obtained using the Spotify Lyrics service via 
+            lyrics obtained using the Spotify Lyrics service via
             :meth:`minim.spotify.PrivateLyricsService.get_lyrics`. If not
             provided, lyrics are unavailable.
 
@@ -1297,7 +1308,7 @@ 

Source code for minim.audio

             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"] 
+                          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"])
@@ -1335,22 +1346,22 @@ 

Source code for minim.audio

             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 
+            :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 
+            **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 
+            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 
+            :meth:`minim.tidal.PrivateAPI.get_track_credits`. If not
             provided, songwriting credits are unavailable.
 
         lyrics : `str` or `dict`, keyword-only, optional
@@ -1373,7 +1384,7 @@ 

Source code for minim.audio

         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"] 
+                self.composer = sorted({c["name"] for c in composers["items"]
                                         if c["role"] in COMPOSER_TYPES})
             elif isinstance(composers[0], dict):
                 self.composer = sorted({
@@ -1403,19 +1414,19 @@ 

Source code for minim.audio

                 image_urls = sorted(data["album"]["imageCover"],
                                     key=lambda x: x["width"], reverse=True)
                 self.artwork = (
-                    image_urls[-1]["url"] 
+                    image_urls[-1]["url"]
                     if artwork_size < image_urls[-1]["width"]
-                    else next(u["url"] for u in image_urls 
+                    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"] 
+                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 
+                    80 if artwork_size < 80
                     else next(s for s in [1280, 1080, 750, 640, 320, 160, 80]
                               if s <= artwork_size)
                 )
@@ -1436,14 +1447,14 @@ 

Source code for minim.audio

 
             if "barcodeId" in album_data:
                 if self.album_artist is None or overwrite:
-                    self.album_artist = [a["name"] for a in album_data["artists"] 
+                    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"] 
+                        a["name"] for a in album_data["artists"]
                         if a["type"] == "MAIN"
                     ]
@@ -1458,7 +1469,7 @@

Source code for minim.audio

 
     .. seealso::
 
-       For a full list of attributes and their descriptions, see 
+       For a full list of attributes and their descriptions, see
        :class:`Audio`.
 
     Parameters
@@ -1473,16 +1484,16 @@ 

Source code for minim.audio

         .. container::
 
            **Valid values**:
-           
+
            The supported metadata fields are
 
            * :code:`"artist"` for the track artist,
-           * :code:`"title"` for the track title, and 
+           * :code:`"title"` for the track title, and
            * :code:`"track_number"` for the track number.
 
            **Examples**:
 
-           * :code:`("(.*) - (.*)", ("artist", "title"))` matches 
+           * :code:`("(.*) - (.*)", ("artist", "title"))` matches
              filenames like "Taylor Swift - Fearless.flac".
            * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
              filenames like "03 - Love Story.flac".
@@ -1490,12 +1501,12 @@ 

Source code for minim.audio

              filenames like "06 You Belong with Me.flac".
 
     multivalue : `bool`
-        Determines whether multivalue tags are supported. If 
+        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 
+        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
@@ -1506,7 +1517,7 @@ 

Source code for minim.audio

     _EXTENSIONS = ["flac"]
 
     def __init__(
-            self, file: Union[str, pathlib.Path], *, 
+            self, file: Union[str, pathlib.Path], *,
             pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
             sep: Union[str, list[str]] = (", ", " & ")) -> None:
 
@@ -1514,7 +1525,7 @@ 

Source code for minim.audio

         Create a FLAC audio file handler.
         """
 
-        Audio.__init__(self, file, pattern=pattern, multivalue=multivalue, 
+        Audio.__init__(self, file, pattern=pattern, multivalue=multivalue,
                        sep=sep)
         self._handle = flac.FLAC(file)
         if self._handle.tags is None:
@@ -1538,7 +1549,7 @@ 

Source code for minim.audio

 
     .. seealso::
 
-       For a full list of attributes and their descriptions, see 
+       For a full list of attributes and their descriptions, see
        :class:`Audio`.
 
     Parameters
@@ -1553,16 +1564,16 @@ 

Source code for minim.audio

         .. container::
 
            **Valid values**:
-           
+
            The supported metadata fields are
 
            * :code:`"artist"` for the track artist,
-           * :code:`"title"` for the track title, and 
+           * :code:`"title"` for the track title, and
            * :code:`"track_number"` for the track number.
 
            **Examples**:
 
-           * :code:`("(.*) - (.*)", ("artist", "title"))` matches 
+           * :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".
@@ -1570,12 +1581,12 @@ 

Source code for minim.audio

              filenames like "06 22.mp3".
 
     multivalue : `bool`
-        Determines whether multivalue tags are supported. If 
+        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 
+        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
@@ -1586,7 +1597,7 @@ 

Source code for minim.audio

     _EXTENSIONS = ["mp3"]
 
     def __init__(
-            self, file: Union[str, pathlib.Path], *, 
+            self, file: Union[str, pathlib.Path], *,
             pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
             sep: Union[str, list[str]] = (", ", " & ")) -> None:
 
@@ -1617,7 +1628,7 @@ 

Source code for minim.audio

 
     .. seealso::
 
-       For a full list of attributes and their descriptions, see 
+       For a full list of attributes and their descriptions, see
        :class:`Audio`.
 
     Parameters
@@ -1632,16 +1643,16 @@ 

Source code for minim.audio

         .. container::
 
            **Valid values**:
-           
+
            The supported metadata fields are
 
            * :code:`"artist"` for the track artist,
-           * :code:`"title"` for the track title, and 
+           * :code:`"title"` for the track title, and
            * :code:`"track_number"` for the track number.
 
            **Examples**:
 
-           * :code:`("(.*) - (.*)", ("artist", "title"))` matches 
+           * :code:`("(.*) - (.*)", ("artist", "title"))` matches
              filenames like "Taylor Swift - Mine.m4a".
            * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
              filenames like "04 - Speak Now.m4a".
@@ -1649,12 +1660,12 @@ 

Source code for minim.audio

              filenames like "07 The Story of Us.m4a".
 
     multivalue : `bool`
-        Determines whether multivalue tags are supported. If 
+        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 
+        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
@@ -1686,14 +1697,14 @@ 

Source code for minim.audio

     ) | dict.fromkeys(["png", 14], mp4.MP4Cover.FORMAT_PNG)
 
     def __init__(
-            self, file: Union[str, pathlib.Path], *, 
+            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)
@@ -1718,7 +1729,7 @@ 

Source code for minim.audio

             value = self._handle.get(key)
             if value:
                 if list not in self._FIELDS_TYPES[field]:
-                    value = utility.format_multivalue(value, False, 
+                    value = utility.format_multivalue(value, False,
                                                       primary=True)
                     if type(value) not in self._FIELDS_TYPES[field]:
                         try:
@@ -1728,7 +1739,7 @@ 

Source code for minim.audio

                 else:
                     if type(value[0]) not in self._FIELDS_TYPES[field]:
                         try:
-                            value = [self._FIELDS_TYPES[field][0](v) 
+                            value = [self._FIELDS_TYPES[field][0](v)
                                      for v in value]
                         except ValueError:
                             continue
@@ -1745,14 +1756,14 @@ 

Source code for minim.audio

             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"), 
+            self.artwork = utility.format_multivalue(self._handle.get("covr"),
                                                      False, primary=True)
             self._artwork_format = str(
                 self._IMAGE_FORMATS[self.artwork.imageformat]
@@ -1784,12 +1795,12 @@ 

Source code for minim.audio

             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._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._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) \
@@ -1798,7 +1809,7 @@ 

Source code for minim.audio

                     self.artwork = f.read()
             self._handle["covr"] = [
                 mp4.MP4Cover(
-                    self.artwork, 
+                    self.artwork,
                     imageformat=self._IMAGE_FORMATS[self._artwork_format]
                 )
             ]
@@ -1816,7 +1827,7 @@ 

Source code for minim.audio

 
     .. seealso::
 
-       For a full list of attributes and their descriptions, see 
+       For a full list of attributes and their descriptions, see
        :class:`Audio`.
 
     Parameters
@@ -1825,7 +1836,7 @@ 

Source code for minim.audio

         Ogg audio filename or path.
 
     codec : `str`, optional
-        Audio codec. If not specified, it will be determined 
+        Audio codec. If not specified, it will be determined
         automatically.
 
         **Valid values**: :code:`"flac"`, :code:`"opus"`, or
@@ -1838,16 +1849,16 @@ 

Source code for minim.audio

         .. container::
 
            **Valid values**:
-           
+
            The supported metadata fields are
 
            * :code:`"artist"` for the track artist,
-           * :code:`"title"` for the track title, and 
+           * :code:`"title"` for the track title, and
            * :code:`"track_number"` for the track number.
 
            **Examples**:
 
-           * :code:`("(.*) - (.*)", ("artist", "title"))` matches 
+           * :code:`("(.*) - (.*)", ("artist", "title"))` matches
              filenames like "Taylor Swift - Blank Space.ogg".
            * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
              filenames like "03 - Style.ogg".
@@ -1855,12 +1866,12 @@ 

Source code for minim.audio

              filenames like "06 Shake It Off.ogg".
 
     multivalue : `bool`
-        Determines whether multivalue tags are supported. If 
+        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 
+        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
@@ -1868,18 +1879,18 @@ 

Source code for minim.audio

     """
 
     _CODECS = {"flac": {"ffmpeg": "-c:a flac", "mutagen": oggflac.OggFLAC},
-               "opus": {"ffmpeg": "-b:a 256k -c:a libopus -vn", 
+               "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, *, 
+            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, 
+
+        Audio.__init__(self, file, pattern=pattern, multivalue=multivalue,
                        sep=sep)
 
         if codec and codec in self._CODECS:
@@ -1902,7 +1913,7 @@ 

Source code for minim.audio

         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.sample_rate = self._handle.info.sample_rate
             self.bitrate = self.bit_depth * self.channel_count \
                            * self.sample_rate
         elif self.codec == "opus":
@@ -1922,7 +1933,7 @@ 

Source code for minim.audio

 
     .. seealso::
 
-       For a full list of attributes and their descriptions, see 
+       For a full list of attributes and their descriptions, see
        :class:`Audio`.
 
     Parameters
@@ -1937,16 +1948,16 @@ 

Source code for minim.audio

         .. container::
 
            **Valid values**:
-           
+
            The supported metadata fields are
 
            * :code:`"artist"` for the track artist,
-           * :code:`"title"` for the track title, and 
+           * :code:`"title"` for the track title, and
            * :code:`"track_number"` for the track number.
 
            **Examples**:
 
-           * :code:`("(.*) - (.*)", ("artist", "title"))` matches 
+           * :code:`("(.*) - (.*)", ("artist", "title"))` matches
              filenames like "Taylor Swift - Don't Blame Me.wav".
            * :code:`("(\\d*) - (.*)", ("track_number", "title"))` matches
              filenames like "05 - Delicate.wav".
@@ -1954,12 +1965,12 @@ 

Source code for minim.audio

              filenames like "06 Look What You Made Me Do.wav".
 
     multivalue : `bool`
-        Determines whether multivalue tags are supported. If 
+        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 
+        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
@@ -1970,7 +1981,7 @@ 

Source code for minim.audio

     _EXTENSIONS = ["wav"]
 
     def __init__(
-            self, file: Union[str, pathlib.Path], *, 
+            self, file: Union[str, pathlib.Path], *,
             pattern: tuple[str, tuple[str]] = None, multivalue: bool = False,
             sep: Union[str, list[str]] = (", ", " & ")) -> None:
 
diff --git a/docs/_modules/minim/discogs.html b/docs/_modules/minim/discogs.html
index 5203fc6..6b6a678 100644
--- a/docs/_modules/minim/discogs.html
+++ b/docs/_modules/minim/discogs.html
@@ -4,7 +4,7 @@
     
     
 
-    
+    
         minim.discogs - Minim 1.0.0 documentation
       
     
@@ -245,6 +245,8 @@ 

Source code for minim.discogs

 Discogs
 =======
 .. moduleauthor:: Benjamin Ye <GitHub: bbye98>
+
+This module contains a complete implementation of the Discogs API.
 """
 
 from http.server import HTTPServer, BaseHTTPRequestHandler
@@ -262,7 +264,7 @@ 

Source code for minim.discogs

 
 import requests
 
-from . import (FOUND_FLASK, FOUND_PLAYWRIGHT, VERSION, REPOSITORY_URL, 
+from . import (FOUND_FLASK, FOUND_PLAYWRIGHT, VERSION, REPOSITORY_URL,
                DIR_HOME, DIR_TEMP, _config)
 if FOUND_FLASK:
     from flask import Flask, request
@@ -272,13 +274,13 @@ 

Source code for minim.discogs

 __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.
         """
@@ -291,7 +293,7 @@ 

Source code for minim.discogs

         self.send_response(200)
         self.send_header("Content-Type", "text/html")
         self.end_headers()
-        status = "denied" if "denied" in self.server.response else "granted" 
+        status = "denied" if "denied" in self.server.response else "granted"
         self.wfile.write(
             f"Access {status}. You may close this page now.".encode()
         )
@@ -303,7 +305,7 @@ 

Source code for minim.discogs

     """
     Discogs API client.
 
-    The Discogs API lets developers build their own Discogs-powered 
+    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,
@@ -312,32 +314,32 @@ 

Source code for minim.discogs

 
     .. seealso::
 
-       For more information, see the `Discogs API home page 
+       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 
+    (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. 
-    
+    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 
+       /#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 
+       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, 
+    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
@@ -349,9 +351,9 @@ 

Source code for minim.discogs

        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 
+    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.
@@ -360,20 +362,20 @@ 

Source code for minim.discogs

     ----------
     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 
+        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 
+        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 
+        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.
@@ -388,13 +390,13 @@ 

Source code for minim.discogs

 
     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 
+        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**:
@@ -402,22 +404,22 @@ 

Source code for minim.discogs

            * :code:`"http.server"` for the built-in implementation of
              HTTP servers.
            * :code:`"flask"` for the Flask framework.
-           * :code:`"playwright"` for the Playwright framework by 
+           * :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 
+        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 
+        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 
+        Minim configuration file, the authentication process is
         bypassed.
 
     access_token_secret : `str`, keyword-only, optional
@@ -429,7 +431,7 @@ 

Source code for minim.discogs

 
     save : `bool`, keyword-only, default: :code:`True`
         Determines whether newly obtained access tokens and their
-        associated properties are stored to the Minim configuration 
+        associated properties are stored to the Minim configuration
         file.
 
     Attributes
@@ -445,7 +447,7 @@ 

Source code for minim.discogs

 
     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.
     """
@@ -460,11 +462,11 @@ 

Source code for minim.discogs

 
     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, 
+            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.
         """
@@ -472,7 +474,7 @@ 

Source code for minim.discogs

         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) 
+        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")
@@ -484,7 +486,7 @@ 

Source code for minim.discogs

 
         self.set_flow(
             flow, consumer_key=consumer_key, consumer_secret=consumer_secret,
-            browser=browser, web_framework=web_framework, port=port, 
+            browser=browser, web_framework=web_framework, port=port,
             redirect_uri=redirect_uri, save=save
         )
         self.set_access_token(access_token, access_token_secret)
@@ -507,8 +509,8 @@ 

Source code for minim.discogs

         """
 
         if token and (
-                self._flow != "oauth" 
-                or self._flow == "discogs" 
+                self._flow != "oauth"
+                or self._flow == "discogs"
                    and "token" not in self.session.headers["Authorization"]
             ):
             emsg = (f"{self._NAME}.{endpoint}() requires user "
@@ -521,14 +523,14 @@ 

Source code for minim.discogs

     def _get_json(self, url: str, **kwargs) -> dict:
 
         """
-        Send a GET request and return the JSON-encoded content of the 
+        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`.
 
@@ -541,7 +543,7 @@ 

Source code for minim.discogs

         return self._request("get", url, **kwargs).json()
 
     def _request(
-            self, method: str, url: str, *, oauth: dict[str, Any] = None, 
+            self, method: str, url: str, *, oauth: dict[str, Any] = None,
             **kwargs) -> requests.Response:
 
         """
@@ -574,7 +576,7 @@ 

Source code for minim.discogs

             if oauth is None:
                 oauth = {}
             oauth = self._oauth | {
-                "oauth_nonce": secrets.token_hex(32), 
+                "oauth_nonce": secrets.token_hex(32),
                 "oauth_timestamp": f"{time.time():.0f}"
             } | oauth
             kwargs["headers"]["Authorization"] = "OAuth " + ", ".join(
@@ -616,7 +618,7 @@ 

Source code for minim.discogs

                     oauth["oauth_callback"] = self._redirect_uri
                 r = self._request(
                     "get",
-                    self.REQUEST_TOKEN_URL, 
+                    self.REQUEST_TOKEN_URL,
                     headers={
                         "Content-Type": "application/x-www-form-urlencoded"
                     },
@@ -627,13 +629,13 @@ 

Source code for minim.discogs

 
                 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}*", 
+                        page.wait_for_url(f"{self._redirect_uri}*",
                                             wait_until="commit")
                         context.close()
                         browser.close()
@@ -642,7 +644,7 @@ 

Source code for minim.discogs

                         oauth |= dict(
                             urllib.parse.parse_qsl(
                                 urllib.parse.urlparse(
-                                    re.search(fr'{self._redirect_uri}\?(.*?)"', 
+                                    re.search(fr'{self._redirect_uri}\?(.*?)"',
                                               f.read()).group(0)
                                 ).query
                             )
@@ -658,7 +660,7 @@ 

Source code for minim.discogs

                               f"in your web browser:\n\n{auth_url}\n")
 
                     if self._web_framework == "http.server":
-                        httpd = HTTPServer(("", self._port), 
+                        httpd = HTTPServer(("", self._port),
                                            _DiscogsRedirectHandler)
                         httpd.handle_request()
                         oauth |= httpd.response
@@ -677,7 +679,7 @@ 

Source code for minim.discogs

                             return ("Access granted. You may close "
                                     "this page now.")
 
-                        server = Process(target=app.run, 
+                        server = Process(target=app.run,
                                          args=("0.0.0.0", self._port))
                         server.start()
                         while not json_file.is_file():
@@ -721,7 +723,7 @@ 

Source code for minim.discogs

                     }
                     with open(DIR_HOME / "minim.cfg", "w") as f:
                         _config.write(f)
-        
+
             self._oauth |= {
                 "oauth_token": access_token,
                 "oauth_signature": self._consumer_secret
@@ -740,9 +742,9 @@ 

Source code for minim.discogs

             else:
                 self.session.headers["Authorization"] = \
                     f"Discogs token={access_token}"
-                
-        if (self._flow == "oauth" 
-                or self._flow == "discogs" 
+
+        if (self._flow == "oauth"
+                or self._flow == "discogs"
                    and "token" in self.session.headers["Authorization"]):
             identity = self.get_identity()
             self._username = identity["username"]
@@ -763,7 +765,7 @@

Source code for minim.discogs

         ----------
         flow : `str`
             Authorization flow. If :code:`None`, no user authentication
-            will be performed and client credentials will not be 
+            will be performed and client credentials will not be
             attached to requests, even if found or provided.
 
             .. container::
@@ -783,10 +785,10 @@ 

Source code for minim.discogs

 
         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 
+            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`
@@ -806,7 +808,7 @@ 

Source code for minim.discogs

                * :code:`"http.server"` for the built-in implementation
                  of HTTP servers.
                * :code:`"flask"` for the Flask framework.
-               * :code:`"playwright"` for the Playwright framework by 
+               * :code:`"playwright"` for the Playwright framework by
                  Microsoft.
 
         port : `int` or `str`, keyword-only, default: :code:`8888`
@@ -815,9 +817,9 @@ 

Source code for minim.discogs

             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. 
+            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
@@ -842,7 +844,7 @@ 

Source code for minim.discogs

             if redirect_uri:
                 self._redirect_uri = redirect_uri
                 if "localhost" in redirect_uri:
-                    self._port = re.search(r"localhost:(\d+)", 
+                    self._port = re.search(r"localhost:(\d+)",
                                            redirect_uri).group(1)
                 elif web_framework:
                     wmsg = ("The redirect URI is not on localhost, "
@@ -857,8 +859,8 @@ 

Source code for minim.discogs

                 self._port = self._redirect_uri = None
 
             self._web_framework = (
-                web_framework 
-                if web_framework is None 
+                web_framework
+                if web_framework is None
                    or web_framework == "http.server"
                    or globals()[f"FOUND_{web_framework.upper()}"]
                 else None
@@ -896,8 +898,8 @@ 

Source code for minim.discogs

             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"`, 
+            **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"`.
 
@@ -1038,7 +1040,7 @@ 

Source code for minim.discogs

                     "year": <int>
                   }
         """
-        
+
         if curr_abbr and curr_abbr not in (
                 CURRENCIES := {
                     "USD", "GBP", "EUR", "CAD", "AUD", "JPY",
@@ -1054,7 +1056,7 @@ 

Source code for minim.discogs

             params={"curr_abbr": curr_abbr}
         )
- +
[docs] def get_user_release_rating( @@ -1076,7 +1078,7 @@

Source code for minim.discogs

 
         username : `str`, optional
             The username of the user whose rating you are requesting. If
-            not specified, the username of the authenticated user is 
+            not specified, the username of the authenticated user is
             used.
 
             **Example**: :code:`"memory"`.
@@ -1096,14 +1098,14 @@ 

Source code for minim.discogs

                     "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}"
         )
@@ -1112,9 +1114,9 @@

Source code for minim.discogs

 
[docs] def update_user_release_rating( - self, release_id: Union[int, str], rating: int, + 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 @@ -1123,7 +1125,7 @@

Source code for minim.discogs

 
         .. admonition:: User authentication
            :class: warning
-  
+
            Requires user authentication with a personal access token or
            via the OAuth 1.0a flow.
 
@@ -1139,7 +1141,7 @@ 

Source code for minim.discogs

 
         username : `str`, optional
             The username of the user whose rating you are requesting. If
-            not specified, the username of the authenticated user is 
+            not specified, the username of the authenticated user is
             used.
 
             **Example**: :code:`"memory"`.
@@ -1159,7 +1161,7 @@ 

Source code for minim.discogs

                     "release_id": <int>,
                     "rating": <int>
                   }
-        """ 
+        """
 
         self._check_authentication("update_user_release_rating")
 
@@ -1189,7 +1191,7 @@ 

Source code for minim.discogs

 
         .. admonition:: User authentication
             :class: warning
-    
+
             Requires user authentication with a personal access token or
             via the OAuth 1.0a flow.
 
@@ -1202,20 +1204,20 @@ 

Source code for minim.discogs

 
         username : `str`, optional
             The username of the user whose rating you are requesting. If
-            not specified, the username of the authenticated user is 
+            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}"
@@ -1226,7 +1228,7 @@ 

Source code for minim.discogs

 [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
@@ -1258,7 +1260,7 @@ 

Source code for minim.discogs

                     "release_id": <int>
                   }
         """
-        
+
         return self._get_json(f"{self.API_URL}/releases/{release_id}/rating")
@@ -1273,7 +1275,7 @@

Source code for minim.discogs

 
         .. attention::
 
-           This endpoint does not appear to be working correctly. 
+           This endpoint does not appear to be working correctly.
            Currently, the response will be of the form
 
            .. code::
@@ -1304,7 +1306,7 @@ 

Source code for minim.discogs

                     "num_want": <int>
                   }
         """
-        
+
         return self._get_json(f"{self.API_URL}/releases/{release_id}/stats")
@@ -1399,7 +1401,7 @@

Source code for minim.discogs

                     "data_quality": <str>
                   }
         """
-        
+
         return self._get_json(f"{self.API_URL}/masters/{master_id}")
@@ -1408,9 +1410,9 @@

Source code for minim.discogs

     def get_master_release_versions(
             self, master_id: Union[int, str], *, country: str = None,
             format: str = None, label: str = None, released: str = None,
-            page: int = None, per_page: int = None, sort: str = None,
-            sort_order: str = None) -> dict[str, Any]:
-        
+            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
@@ -1444,12 +1446,12 @@ 

Source code for minim.discogs

 
             **Example**: :code:`"1992"`.
 
-        page : `int`, keyword-only, optional
+        page : `int` or `str`, keyword-only, optional
             The page you want to request.
 
             **Example**: :code:`3`.
 
-        per_page : `int`, keyword-only, optional
+        per_page : `int` or `str`, keyword-only, optional
             The number of items per page.
 
             **Example**: :code:`25`.
@@ -1457,8 +1459,8 @@ 

Source code for minim.discogs

         sort : `str`, keyword-only, optional
             Sort items by this field.
 
-            **Valid values**: :code:`"released"`, :code:`"title"`, 
-            :code:`"format"`, :code:`"label"`, :code:`"catno"`, 
+            **Valid values**: :code:`"released"`, :code:`"title"`,
+            :code:`"format"`, :code:`"label"`, :code:`"catno"`,
             and :code:`"country"`.
 
         sort_order : `str`, keyword-only, optional
@@ -1469,7 +1471,7 @@ 

Source code for minim.discogs

         Returns
         -------
         versions : `dict`
-            Discogs database information for all releases that are 
+            Discogs database information for all releases that are
             versions of the specified master.
 
             .. admonition:: Sample
@@ -1515,7 +1517,7 @@ 

Source code for minim.discogs

                     ]
                   }
         """
-        
+
         return self._get_json(
             f"{self.API_URL}/masters/{master_id}/versions",
             params={
@@ -1530,7 +1532,7 @@ 

Source code for minim.discogs

             },
         )
- +
[docs] def get_artist(self, artist_id: Union[int, str]) -> dict[str, Any]: @@ -1585,16 +1587,16 @@

Source code for minim.discogs

                     ]
                   }
         """
-        
+
         return self._get_json(f"{self.API_URL}/artists/{artist_id}")
- +
[docs] def get_artist_releases( - self, artist_id: Union[int, str], *, page: int = None, - per_page: int = None, sort: str = None, sort_order: str = None - ) -> dict[str, Any]: + 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 @@ -1608,10 +1610,10 @@

Source code for minim.discogs

 
             **Example**: :code:`108713`.
 
-        page : `int`, keyword-only, optional
+        page : `int` or `str`, keyword-only, optional
             Page of results to fetch.
 
-        per_page : `int`, keyword-only, optional
+        per_page : `int` or `str`, keyword-only, optional
             Number of results per page.
 
         sort : `str`, keyword-only, optional
@@ -1624,7 +1626,7 @@ 

Source code for minim.discogs

             Sort results in a particular order.
 
             **Valid values**: :code:`"asc"` and :code:`"desc"`.
-        
+
         Returns
         -------
         releases : `dict`
@@ -1662,7 +1664,7 @@ 

Source code for minim.discogs

                     ]
                   }
         """
-        
+
         return self._get_json(
             f"{self.API_URL}/artists/{artist_id}/releases",
             params={
@@ -1677,10 +1679,10 @@ 

Source code for minim.discogs

 
[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, + /#page:database,header:database-label-get>`_: Get a label, company, recording studio, locxation, or other entity involved with artists and releases. @@ -1733,12 +1735,12 @@

Source code for minim.discogs

 
         return self._get_json(f"{self.API_URL}/labels/{label_id}")
- +
[docs] def get_label_releases( - self, label_id: Union[int, str], *, page: int = None, - per_page: int = None) -> dict[str, Any]: + 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 @@ -1752,10 +1754,10 @@

Source code for minim.discogs

 
             **Example**: :code:`1`.
 
-        page : `int`, keyword-only, optional
+        page : `int` or `str`, keyword-only, optional
             Page of results to fetch.
 
-        per_page : `int`, keyword-only, optional
+        per_page : `int` or `str`, keyword-only, optional
             Number of results per page.
 
         Returns
@@ -1795,13 +1797,13 @@ 

Source code for minim.discogs

                     ]
                   }
         """
-        
+
         return self._get_json(
             f"{self.API_URL}/labels/{label_id}/releases",
             params={"page": page, "per_page": per_page}
         )
- +
[docs] def search( @@ -1812,7 +1814,7 @@

Source code for minim.discogs

             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
@@ -1821,8 +1823,8 @@ 

Source code for minim.discogs

         .. admonition:: Authentication
            :class: warning
 
-            Requires authentication with consumer credentials, with a
-            personal access token, or via the OAuth 1.0a flow.
+           Requires authentication with consumer credentials, with a
+           personal access token, or via the OAuth 1.0a flow.
 
         Parameters
         ----------
@@ -1834,7 +1836,7 @@ 

Source code for minim.discogs

         type : `str`, keyword-only, optional
             The type of item to search for.
 
-            **Valid values**: :code:`"release"`, :code:`"master"`, 
+            **Valid values**: :code:`"release"`, :code:`"master"`,
             :code:`"artist"`, and :code:`"label"`.
 
         title : `str`, keyword-only, optional
@@ -1877,7 +1879,7 @@ 

Source code for minim.discogs

             Search styles.
 
             **Example**: :code:`"Grunge"`.
-        
+
         country : `str`, keyword-only, optional
             Search release country.
 
@@ -1964,7 +1966,7 @@ 

Source code for minim.discogs

         """
 
         self._check_authentication("search", False)
-        
+
         return self._get_json(
             f"{self.API_URL}/database/search",
             params={
@@ -1995,9 +1997,9 @@ 

Source code for minim.discogs

 
[docs] def get_inventory( - self, username: str, *, status: str = None, page: str = None, - per_page: str = None, sort: str = None, sort_order: str = None - ) -> dict[str, Any]: + 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 @@ -2008,30 +2010,31 @@

Source code for minim.discogs

            :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 
+           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.
+            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"`, 
+            **Valid values**: :code:`"For Sale"`, :code:`"Draft"`,
             :code:`"Expired"`, :code:`"Sold"`, and :code:`"Deleted"`.
 
-        page : `str`, keyword-only, optional
+        page : `int` or `str`, keyword-only, optional
             The page you want to request.
 
             **Example**: :code:`3`.
 
-        per_page : `str`, keyword-only, optional
+        per_page : `int` or `str`, keyword-only, optional
             The number of items per page.
 
             **Example**: :code:`25`.
@@ -2039,8 +2042,8 @@ 

Source code for minim.discogs

         sort : `str`, keyword-only, optional
             Sort items by this field.
 
-            **Valid values**: :code:`"listed"`, :code:`"price"`, 
-            :code:`"item"`, :code:`"artist"`, :code:`"label"`, 
+            **Valid values**: :code:`"listed"`, :code:`"price"`,
+            :code:`"item"`, :code:`"artist"`, :code:`"label"`,
             :code:`"catno"`, :code:`"audio"`, :code:`"status"`, and
             :code:`"location"`.
 
@@ -2060,86 +2063,95 @@ 

Source code for minim.discogs

                .. 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]: - pass - - def edit_listing( - self, listing_id: Union[int, str], release_id: Union[int, str], - condition: str, price: float, status: str, *, - 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: - - pass - - def delete_listing(self, listing_id: Union[int, str]) -> None: - - pass - - def create_listing( - self, release_id: Union[int, str], condition: str, price: float, - status: str, *, 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]: - - pass - - def get_order(self, order_id: Union[int, str]) -> dict[str, Any]: - - pass - - def edit_order(self, order_id: Union[int, str], status: str) -> None: - - pass - - ### INVENTORY EXPORT ###################################################### - - ### INVENTORY UPLOAD ###################################################### - - ### 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. + `Marketplace > Listing <https://www.discogs.com/developers + /#page:marketplace,header:marketplace-listing-get>`_: View + marketplace listings. - .. admonition:: User authentication - :class: warning + Parameters + ---------- + listing_id : `int` or `str` + The ID of the listing you are fetching. - Requires user authentication with a personal access token or - via the OAuth 1.0a flow. + **Example**: :code:`172723812`. - 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. + curr_abbr : `str`, keyword-only, optional + Currency abbreviation for marketplace listings. Defaults to + the authenticated user's currency. - For more detailed information, make another request for the - user's profile using :meth:`get_profile`. + **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 ------- - identity : `dict` - Basic information about the authenticated user. + listing : `dict` + The marketplace listing. .. admonition:: Sample :class: dropdown @@ -2147,125 +2159,1247 @@

Source code for minim.discogs

                .. 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>,
-                    "username": <str>,
+                    "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>,
-                    "consumer_name": <str>
+                    "audio": <bool>
                   }
         """
 
-        self._check_authentication("get_identity")
+        return self._get_json(
+            f"{self.API_URL}/marketplace/listings/{listing_id}",
+            params={"curr_abbr": curr_abbr}
+        )
- return self._get_json(f"{self.API_URL}/oauth/identity")
- -
-[docs] - def get_profile(self, username: str = None) -> dict[str, Any]: +
+[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]: """ - `User Identity > Profile > Get Profile - <https://www.discogs.com/developers - /#page:user-identity,header:user-identity-profile-get>`_: - Retrieve a user by username. + `Marketplace > New Listing <https://www.discogs.com/developers + /#page:marketplace,header:marketplace-new-listing>`_: Create a + marketplace listing. - 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. + .. admonition:: User authentication + :class: warning - 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. + Requires user authentication with a personal access token or + via the OAuth 1.0a flow. 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"`. + release_id : `int` or `str` + The ID of the release you are posting. - Returns - ------- - profile : `dict` - Detailed information about the user. + **Example**: :code:`1`. - .. admonition:: Sample - :class: dropdown + 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. + """ - .. code:: + 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()
- { - "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_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: - -
-[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. + `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. + :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. + listing_id : `int` or `str` + The ID of the listing you are fetching. - **Example**: :code:`"Nicolas Cage"`. + **Example**: :code:`172723812`. - home_page : `str`, keyword-only, optional + release_id : `int` or `str` + The ID of the release you are posting. + + **Example**: :code:`1`. + + 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": {}, + "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:`1`. + + 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:`1`. + + 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 ###################################################### + + ### INVENTORY UPLOAD ###################################################### + + ### 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. + + 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"`. @@ -2283,8 +3417,8 @@

Source code for minim.discogs

         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"`, 
+            **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"`.
 
@@ -2344,7 +3478,7 @@ 

Source code for minim.discogs

             raise ValueError(emsg)
 
         return self._request(
-            "post", 
+            "post",
             f"{self.API_URL}/users/{self._username}",
             json={
                 "name": name,
@@ -2359,8 +3493,8 @@ 

Source code for minim.discogs

 
[docs] def get_user_submissions( - self, username: str = None, *, page: int = None, - per_page: int = None) -> dict[str, Any]: + 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 @@ -2372,16 +3506,16 @@

Source code for minim.discogs

         Parameters
         ----------
         username : `str`, optional
-            The username of the submissions you are trying to fetch. If 
-            not specified, the username of the authenticated user is 
+            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`, keyword-only, optional
+        page : `int` or `str`, keyword-only, optional
             Page of results to fetch.
 
-        per_page : `int`, keyword-only, optional
+        per_page : `int` or `str`, keyword-only, optional
             Number of results per page.
 
         Returns
@@ -2525,18 +3659,18 @@ 

Source code for minim.discogs

             params={"page": page, "per_page": per_page}
         )
- +
[docs] def get_user_contributions( - self, username: str = None, *, page: int = None, - per_page: int = None, sort: str = None, sort_order: str = None + 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 + :user-identity-user-contributions-get>`_: Retrieve a user's contributions (releases, labels, artists) by username. Parameters @@ -2548,10 +3682,10 @@

Source code for minim.discogs

 
             **Example**: :code:`"shooezgirl"`.
 
-        page : `int`, keyword-only, optional
+        page : `int` or `str`, keyword-only, optional
             Page of results to fetch.
 
-        per_page : `int`, keyword-only, optional
+        per_page : `int` or `str`, keyword-only, optional
             Number of results per page.
 
         sort : `str`, keyword-only, optional
@@ -2565,7 +3699,7 @@ 

Source code for minim.discogs

             Sort items in a particular order.
 
             **Valid values**: :code:`"asc"` and :code:`"desc"`.
-            
+
         Returns
         -------
         contributions : `dict`
@@ -2691,20 +3825,53 @@ 

Source code for minim.discogs

         return self._get_json(
             f"{self.API_URL}/users/{username}/contributions",
             params={
-                "page": page, 
-                "per_page": per_page, 
+                "page": page,
+                "per_page": per_page,
                 "sort": sort,
                 "sort_order": sort_order
             }
         )
- + ### USER COLLECTION ####################################################### + # def get_user_collection_folders( + # self, username: str = None) -> dict[str, Any]: + + # """ + # `User Collection > 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. + + # 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 0 (the + # "All" folder) will be visible (if the requested user's + # collection is public). + + # """ + + # 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" + # ) + + # def create_user_collection_folder(): + # pass + ### USER WANTLIST ######################################################### + + ### USER LISTS ############################################################ +
diff --git a/docs/_modules/minim/itunes.html b/docs/_modules/minim/itunes.html index a0505ad..63d70b8 100644 --- a/docs/_modules/minim/itunes.html +++ b/docs/_modules/minim/itunes.html @@ -4,7 +4,7 @@ - + minim.itunes - Minim 1.0.0 documentation @@ -262,16 +262,16 @@

Source code for minim.itunes

     """
     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, 
+    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 
+       For more information, see the `iTunes Search API
        documentation <https://developer.apple.com/library/archive/
        documentation/AudioVideo/Conceptual/iTuneSearchAPI/index.html>`_.
 
@@ -294,14 +294,14 @@ 

Source code for minim.itunes

     def _get_json(self, url: str, **kwargs) -> dict:
 
         """
-        Send a GET request and return the JSON-encoded content of the 
+        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`.
 
@@ -346,7 +346,7 @@ 

Source code for minim.itunes

 [docs]
     def search(
             self, term: str, *, country: str = None, media: str = None,
-            entity: Union[str, list[str]] = None, attribute: 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]:
@@ -361,9 +361,9 @@ 

Source code for minim.itunes

 
             .. note::
 
-               URL encoding replaces spaces with the plus (:code:`+`) 
-               character, and all characters except letters, numbers, 
-               periods (:code:`.`), dashes (:code:`-`), underscores 
+               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"`.
@@ -371,31 +371,31 @@ 

Source code for minim.itunes

         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. 
-            
+            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"`, 
+               **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`. 
-            
+            specified media type in `media`.
+
             .. seealso::
 
                For a list of available
@@ -410,11 +410,11 @@ 

Source code for minim.itunes

 
             **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`). 
-            
+            to the specified media type (`media`).
+
             .. seealso::
 
                For a list of available
@@ -424,24 +424,24 @@ 

Source code for minim.itunes

                /Searching.html#//apple_ref/doc/uid
                /TP40017632-CH5-SW3>`_.
 
-            **Default**: All attributes associated with the specified 
-            media type. 
+            **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 
+            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. 
+            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
@@ -450,7 +450,7 @@ 

Source code for minim.itunes

             .. container::
 
                **Valid values**:
-            
+
                * :code:`"en_us"` for English.
                * :code:`"ja_jp"` for Japanese.
 
@@ -461,13 +461,13 @@ 

Source code for minim.itunes

             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 
+            A flag indicating whether or not you want to include
             explicit content in your search results.
-            
+
             **Default**: :code:`"Yes"`.
 
         Returns
@@ -529,7 +529,7 @@ 

Source code for minim.itunes

 
         >>> itunes.search("jack johnson")
 
-        To search for all Jack Johnson audio and video content and 
+        To search for all Jack Johnson audio and video content and
         return only the first 25 items,
 
         >>> itunes.search("jack johnson", limit=25)
@@ -537,13 +537,13 @@ 

Source code for minim.itunes

         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 
+
+        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 
+        To search for applications titled “Yelp” and return only the
         results from the United States iTunes Store,
 
         >>> itunes.search("yelp", country="us", entity="software")
@@ -561,7 +561,7 @@ 

Source code for minim.itunes

                 "limit": limit,
                 "lang": lang,
                 "version": version,
-                "explicit": ("No", "Yes")[explicit] 
+                "explicit": ("No", "Yes")[explicit]
                             if isinstance(explicit, bool) else explicit
             }
         )
@@ -583,7 +583,7 @@

Source code for minim.itunes

 
         """
         Search for content based on iTunes IDs, AMG IDs, UPCs/EANs, or
-        ISBNs. ID-based lookups are faster and contain fewer 
+        ISBNs. ID-based lookups are faster and contain fewer
         false-positive results.
 
         Parameters
@@ -610,8 +610,8 @@ 

Source code for minim.itunes

             The 13-digit ISBN(s) to lookup.
 
         entity : `str` or `list`, keyword-only, optional
-            The type(s) of results you want returned. 
-            
+            The type(s) of results you want returned.
+
             .. seealso::
 
                For a list of available entities, see the `iTunes Store
@@ -625,8 +625,8 @@ 

Source code for minim.itunes

 
         limit : `int` or `str`, keyword-only, optional
             The number of search results you want the iTunes Store to
-            return. 
-            
+            return.
+
             **Valid values**: `limit` must be between 1 and 200.
 
             **Default**: :code:`50`.
@@ -686,7 +686,7 @@ 

Source code for minim.itunes

                       }
                     ]
                   }
-                  
+
         Examples
         --------
         Look up Jack Johnson by iTunes artist ID:
@@ -731,8 +731,8 @@ 

Source code for minim.itunes

 
         Look up an album by its AMG Album ID:
 
-        >>> itunes.lookup(amg_album_id=[15175, 15176, 15177, 15178, 
-        ...                             15183, 15184, 15187, 15190, 
+        >>> 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:
@@ -754,28 +754,28 @@ 

Source code for minim.itunes

                 "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": 
+                "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": 
+                "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": 
+                "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 
+                "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))
diff --git a/docs/_modules/minim/qobuz.html b/docs/_modules/minim/qobuz.html
index a0fcab2..4245e01 100644
--- a/docs/_modules/minim/qobuz.html
+++ b/docs/_modules/minim/qobuz.html
@@ -4,7 +4,7 @@
     
     
 
-    
+    
         minim.qobuz - Minim 1.0.0 documentation
       
     
@@ -279,15 +279,15 @@ 

Source code for minim.qobuz

         from calling :meth:`get_track`.
 
     roles : `list` or `set`, keyword-only, optional
-        Role filter. The special :code:`"Composers"` filter will 
+        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"`, 
+        **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
@@ -296,7 +296,7 @@ 

Source code for minim.qobuz

         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(
@@ -304,16 +304,16 @@ 

Source code for minim.qobuz

             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", 
+            p for cr in {"Composer", "ComposerLyricist", "Lyricist",
                          "Writer"}
-            for p, r in people.items() 
+            for p, r in people.items()
             if cr in r
         })
     for role in roles:
@@ -322,7 +322,7 @@ 

Source code for minim.qobuz

                 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
 
 
@@ -333,14 +333,14 @@

Source code for minim.qobuz

     Private Qobuz API client.
 
     The private TIDAL API allows songs, collections (albums, playlists),
-    and performers to be queried, and information about them to be 
+    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 
+    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 
+       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>`_.
@@ -348,32 +348,32 @@ 

Source code for minim.qobuz

     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 
+    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 
+    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 
+    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 
+    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 
+       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 
+    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
@@ -381,7 +381,7 @@ 

Source code for minim.qobuz

     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`.
@@ -392,7 +392,7 @@ 

Source code for minim.qobuz

         .. container::
 
            **Valid values**:
-           
+
            * :code:`"password"` for the password flow.
            * :code:`None` for no authentication.
 
@@ -414,8 +414,8 @@ 

Source code for minim.qobuz

         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 
+        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`
@@ -424,7 +424,7 @@ 

Source code for minim.qobuz

 
     save : `bool`, keyword-only, default: :code:`True`
         Determines whether newly obtained user authentication tokens and
-        their associated properties are stored to the Minim 
+        their associated properties are stored to the Minim
         configuration file.
 
     Attributes
@@ -443,11 +443,11 @@ 

Source code for minim.qobuz

     WEB_URL = "https://play.qobuz.com"
 
     def __init__(
-            self, *, app_id: str = None, app_secret: str = None, 
+            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, 
+            email: str = None, password: str = None, auth_token: str = None,
             overwrite: bool = False, save: bool = True) -> None:
-        
+
         """
         Create a private Qobuz API client.
         """
@@ -463,12 +463,12 @@ 

Source code for minim.qobuz

             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, 
+        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.
 
@@ -486,14 +486,14 @@ 

Source code for minim.qobuz

     def _get_json(self, url: str, **kwargs) -> dict:
 
         """
-        Send a GET request and return the JSON-encoded content of the 
+        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`.
 
@@ -541,7 +541,7 @@ 

Source code for minim.qobuz

 
         if not app_id or not app_secret:
             js = re.search(
-                "/resources/.*/bundle.js", 
+                "/resources/.*/bundle.js",
                 self.session.get(f"{self.WEB_URL}/login").text
             ).group(0)
             bundle = self.session.get(f"{self.WEB_URL}{js}").text
@@ -549,13 +549,13 @@ 

Source code for minim.qobuz

                 '(?:production:{api:{appId:")(.*?)(?:",appSecret)', bundle
             ).group(1)
             app_secret = [
-                base64.b64decode("".join((s, *m.groups()))[:-44]).decode() 
+                base64.b64decode("".join((s, *m.groups()))[:-44]).decode()
                 for s, m in (
                     (s, re.search(f'(?:{c.capitalize()}",info:")(.*?)(?:",extras:")'
-                                  '(.*?)(?:"},{offset)', 
+                                  '(.*?)(?:"},{offset)',
                                   bundle))
                     for s, c in re.findall(r'(?:[a-z].initialSeed\(")(.*?)'
-                                           r'(?:",window.utimezone.)(.*?)\)', 
+                                           r'(?:",window.utimezone.)(.*?)\)',
                                            bundle)) if m
             ][1]
 
@@ -565,7 +565,7 @@ 

Source code for minim.qobuz

 
[docs] def set_auth_token( - self, auth_token: str = None, *, email: str = None, + self, auth_token: str = None, *, email: str = None, password: str = None) -> None: """ @@ -586,7 +586,7 @@

Source code for minim.qobuz

         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:
@@ -599,28 +599,28 @@ 

Source code for minim.qobuz

                             )
                             page = context.new_page()
                             page.goto(f"{self.WEB_URL}/login", timeout=0)
-                            page.wait_for_url(f"{self.WEB_URL}/featured", 
+                            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(
-                                '"X-User-Auth-Token", "value": "(.*?)"',
+                                '(?<=")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 = regex.group(1)
+                        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", 
+                        "post", f"{self.API_URL}/user/login",
                         params={"email": email, "password": password}
                     ).json()
                     auth_token = r["user_auth_token"]
@@ -641,21 +641,21 @@ 

Source code for minim.qobuz

             me = self.get_profile()
             self._user_id = me["id"]
             self._sub = (
-                me["subscription"] is not None 
-                and datetime.datetime.now() 
+                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, + 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. @@ -667,14 +667,14 @@

Source code for minim.qobuz

             .. 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`.
@@ -686,10 +686,10 @@ 

Source code for minim.qobuz

             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 
+            and their associated properties to the Minim configuration
             file.
         """
 
@@ -714,7 +714,7 @@ 

Source code for minim.qobuz

         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)
@@ -895,7 +895,7 @@

Source code for minim.qobuz

         return self._get_json(f"{self.API_URL}/album/get",
                               params={"album_id": album_id})
- +