Files
metadata-agregator/docs/research/minim/analysis/API.md
T
Alexander a1f6701bac feat: initial implementation of metadata aggregator
- gRPC service with MusicBrainz provider
- PostgreSQL schema with migrations
- Service layer with database-first caching
- Repository pattern for data access
- YAML configuration support
- Research documentation for 17 music metadata projects
2026-04-28 16:28:53 +02:00

1180 lines
26 KiB
Markdown

# minim: API Reference
## API Type
minim is a **Python library**, not an HTTP API. This document describes the public Python API surface for each module.
**Import Pattern:**
```python
from minim import spotify, tidal, qobuz, discogs, itunes
from minim.audio import Audio
```
## audio Module
### Audio Class
**Purpose:** Read, write, and convert audio file metadata.
**Constructor:**
```python
Audio(filepath: str)
```
**Parameters:**
- `filepath`: Path to audio file (FLAC, MP3, MP4/M4A, Ogg Vorbis, WAVE)
**Returns:** Subclass instance (`FLAC`, `MP3`, `MP4`, `OggVorbis`, `WAVE`) based on file format.
**Raises:**
- `FileNotFoundError`: File does not exist
- `ValueError`: Unsupported audio format
**Example:**
```python
audio = Audio("track.flac") # Returns FLAC instance
print(audio.title, audio.artist, audio.album)
```
### Attributes
All `Audio` instances expose these attributes:
| Attribute | Type | Description |
|-----------|------|-------------|
| `filepath` | `str` | Path to audio file |
| `title` | `str` | Track title |
| `artist` | `str` | Primary artist(s), comma-separated |
| `album` | `str` | Album title |
| `album_artist` | `str` | Album artist (for compilations) |
| `date` | `str` | Release date (YYYY-MM-DD or YYYY) |
| `genre` | `str` | Genre classification |
| `track_number` | `int` | Track position in album |
| `disc_number` | `int` | Disc number (for multi-disc albums) |
| `isrc` | `str` | International Standard Recording Code |
| `upc` | `str` | Universal Product Code (album barcode) |
| `artwork` | `bytes` | Album cover image (JPEG or PNG) |
| `lyrics` | `str` | Song lyrics |
| `comment` | `str` | Freeform comment field |
| `copyright` | `str` | Copyright notice |
| `duration` | `float` | Track duration in seconds |
| `bitrate` | `int` | Bitrate in bits per second |
| `sample_rate` | `int` | Sample rate in Hz |
| `channels` | `int` | Number of audio channels (1=mono, 2=stereo) |
### Methods
#### read_metadata()
```python
audio.read_metadata() -> None
```
Read metadata from file and populate attributes. Called automatically by constructor.
#### write_metadata()
```python
audio.write_metadata() -> None
```
Write current attribute values to file tags.
**Example:**
```python
audio = Audio("track.flac")
audio.title = "New Title"
audio.artist = "New Artist"
audio.write_metadata()
```
#### convert()
```python
audio.convert(output_path: str, format: str, **ffmpeg_options) -> Audio
```
Convert audio file to different format using FFmpeg.
**Parameters:**
- `output_path`: Destination file path
- `format`: Target format (`"flac"`, `"mp3"`, `"m4a"`, `"ogg"`, `"wav"`)
- `**ffmpeg_options`: Additional FFmpeg arguments (e.g., `bitrate="320k"`, `sample_rate=48000`)
**Returns:** New `Audio` instance for converted file with metadata copied.
**Requires:** FFmpeg installed and in PATH.
**Example:**
```python
audio = Audio("track.flac")
mp3_audio = audio.convert("track.mp3", "mp3", bitrate="320k")
```
#### set_metadata_using_itunes()
```python
audio.set_metadata_using_itunes(data: dict) -> None
```
Map iTunes Search API track object to audio metadata.
**Parameters:**
- `data`: Track object from `itunes.SearchAPI.lookup()` or `search()`
**Example:**
```python
itunes_api = itunes.SearchAPI()
results = itunes_api.search("Radiohead Creep", media="music", entity="song")
track = results["results"][0]
audio = Audio("track.mp3")
audio.set_metadata_using_itunes(track)
audio.write_metadata()
```
#### set_metadata_using_qobuz()
```python
audio.set_metadata_using_qobuz(data: dict) -> None
```
Map Qobuz API track object to audio metadata.
**Parameters:**
- `data`: Track object from `qobuz.PrivateAPI.get_track()`
#### set_metadata_using_spotify()
```python
audio.set_metadata_using_spotify(data: dict) -> None
```
Map Spotify Web API track object to audio metadata.
**Parameters:**
- `data`: Track object from `spotify.WebAPI.get_track()` or `search()`
**Example:**
```python
spotify_api = spotify.WebAPI(client_id="...", client_secret="...")
spotify_api.set_flow("client_credentials")
spotify_api.set_access_token()
track = spotify_api.get_track("3n3Ppam7vgaVa1iaRUc9Lp") # Radiohead - Creep
audio = Audio("track.flac")
audio.set_metadata_using_spotify(track)
audio.write_metadata()
```
#### set_metadata_using_tidal()
```python
audio.set_metadata_using_tidal(data: dict) -> None
```
Map TIDAL API track object to audio metadata.
**Parameters:**
- `data`: Track object from `tidal.API.get_track()` or `tidal.PrivateAPI.get_track()`
## discogs Module
### API Class
**Purpose:** Interact with Discogs database, marketplace, collection, and wantlist.
**Constructor:**
```python
discogs.API(
consumer_key: str = None,
consumer_secret: str = None,
access_token: str = None,
access_token_secret: str = None,
personal_access_token: str = None
)
```
**Parameters:**
- `consumer_key`: OAuth consumer key (from Discogs app settings)
- `consumer_secret`: OAuth consumer secret
- `access_token`: OAuth access token (obtained via `set_access_token()`)
- `access_token_secret`: OAuth access token secret
- `personal_access_token`: Alternative to OAuth (from Discogs settings)
**Authentication:** OAuth 1.0a or personal access token. OAuth required for write operations (collection, wantlist).
### Methods
#### search()
```python
api.search(
query: str = None,
type: str = None,
title: str = None,
artist: str = None,
label: str = None,
genre: str = None,
year: str = None,
format: str = None,
country: str = None,
per_page: int = 50,
page: int = 1
) -> dict
```
Search Discogs database.
**Parameters:**
- `query`: General search query
- `type`: Result type (`"release"`, `"master"`, `"artist"`, `"label"`)
- `title`: Filter by release title
- `artist`: Filter by artist name
- `label`: Filter by label name
- `genre`: Filter by genre
- `year`: Filter by release year
- `format`: Filter by format (e.g., `"Vinyl"`, `"CD"`)
- `country`: Filter by release country
- `per_page`: Results per page (max 100)
- `page`: Page number
**Returns:** Dict with `results` array and pagination info.
**Example:**
```python
api = discogs.API(personal_access_token="...")
results = api.search(artist="Radiohead", type="release", format="Vinyl")
for release in results["results"]:
print(release["title"], release["year"])
```
#### get_artist()
```python
api.get_artist(artist_id: int) -> dict
```
Get artist details.
**Parameters:**
- `artist_id`: Discogs artist ID
**Returns:** Artist object with name, profile, images, members, URLs.
#### get_release()
```python
api.get_release(release_id: int) -> dict
```
Get release details.
**Parameters:**
- `release_id`: Discogs release ID
**Returns:** Release object with tracklist, artists, labels, formats, identifiers (barcode, catalog number).
#### get_master_release()
```python
api.get_master_release(master_id: int) -> dict
```
Get master release details (canonical version of a release with multiple pressings).
**Parameters:**
- `master_id`: Discogs master release ID
**Returns:** Master release object with main release info and versions list.
#### get_label()
```python
api.get_label(label_id: int) -> dict
```
Get label details.
**Parameters:**
- `label_id`: Discogs label ID
**Returns:** Label object with name, profile, images, sublabels, releases.
#### get_collection_folders()
```python
api.get_collection_folders(username: str) -> dict
```
Get user's collection folders.
**Parameters:**
- `username`: Discogs username
**Returns:** Array of folder objects with ID, name, count.
#### get_collection_items()
```python
api.get_collection_items(
username: str,
folder_id: int = 0,
per_page: int = 50,
page: int = 1
) -> dict
```
Get items in collection folder.
**Parameters:**
- `username`: Discogs username
- `folder_id`: Folder ID (0 = "All" folder)
- `per_page`: Results per page
- `page`: Page number
**Returns:** Array of collection items with release info, notes, rating.
#### add_to_collection()
```python
api.add_to_collection(
username: str,
folder_id: int,
release_id: int
) -> dict
```
Add release to collection.
**Requires:** OAuth authentication.
#### remove_from_collection()
```python
api.remove_from_collection(
username: str,
folder_id: int,
release_id: int,
instance_id: int
) -> None
```
Remove release from collection.
**Requires:** OAuth authentication.
#### get_wantlist()
```python
api.get_wantlist(
username: str,
per_page: int = 50,
page: int = 1
) -> dict
```
Get user's wantlist.
#### add_to_wantlist()
```python
api.add_to_wantlist(username: str, release_id: int) -> dict
```
Add release to wantlist.
**Requires:** OAuth authentication.
#### remove_from_wantlist()
```python
api.remove_from_wantlist(username: str, release_id: int) -> None
```
Remove release from wantlist.
**Requires:** OAuth authentication.
## itunes Module
### SearchAPI Class
**Purpose:** Search and lookup items in iTunes catalog.
**Constructor:**
```python
itunes.SearchAPI()
```
**Authentication:** None required (public API).
### Methods
#### search()
```python
api.search(
term: str,
country: str = "US",
media: str = "all",
entity: str = None,
attribute: str = None,
limit: int = 50,
lang: str = "en_us",
explicit: str = "Yes"
) -> dict
```
Search iTunes catalog.
**Parameters:**
- `term`: Search query
- `country`: Two-letter country code (e.g., `"US"`, `"GB"`, `"JP"`)
- `media`: Media type (`"all"`, `"music"`, `"movie"`, `"podcast"`, `"audiobook"`, etc.)
- `entity`: Entity type (`"song"`, `"album"`, `"musicArtist"`, `"movie"`, etc.)
- `attribute`: Search attribute (`"artistTerm"`, `"albumTerm"`, `"songTerm"`, etc.)
- `limit`: Max results (max 200)
- `lang`: Language code
- `explicit`: Include explicit content (`"Yes"` or `"No"`)
**Returns:** Dict with `resultCount` and `results` array.
**Example:**
```python
api = itunes.SearchAPI()
results = api.search("Radiohead", media="music", entity="musicArtist", limit=10)
for artist in results["results"]:
print(artist["artistName"], artist["artistId"])
```
#### lookup()
```python
api.lookup(
id: int = None,
amg_artist_id: int = None,
amg_album_id: int = None,
upc: str = None,
isbn: str = None,
entity: str = None,
limit: int = 50
) -> dict
```
Lookup item by ID or identifier.
**Parameters:**
- `id`: iTunes ID (artist, album, track, etc.)
- `amg_artist_id`: All Music Guide artist ID
- `amg_album_id`: All Music Guide album ID
- `upc`: Universal Product Code (album barcode)
- `isbn`: ISBN (for books)
- `entity`: Entity type to return
- `limit`: Max results
**Returns:** Dict with `resultCount` and `results` array.
**Example:**
```python
# Lookup by iTunes ID
results = api.lookup(id=1419227, entity="album")
# Lookup by UPC
results = api.lookup(upc="724384260910")
```
## qobuz Module
### PrivateAPI Class
**Purpose:** Interact with Qobuz catalog and streaming service.
**Note:** Uses undocumented private API endpoints. May break without notice.
**Constructor:**
```python
qobuz.PrivateAPI(
app_id: str = None,
app_secret: str = None,
email: str = None,
password: str = None,
access_token: str = None
)
```
**Parameters:**
- `app_id`: Qobuz app ID (auto-extracted from web player if not provided)
- `app_secret`: Qobuz app secret (auto-extracted)
- `email`: User email for password grant
- `password`: User password
- `access_token`: Existing access token
**Authentication:** Password grant OAuth. Requires Qobuz subscription for streaming URLs.
### Methods
#### search()
```python
api.search(
query: str,
type: str = "tracks",
limit: int = 50,
offset: int = 0
) -> dict
```
Search Qobuz catalog.
**Parameters:**
- `query`: Search query
- `type`: Result type (`"tracks"`, `"albums"`, `"artists"`, `"playlists"`)
- `limit`: Max results
- `offset`: Pagination offset
**Returns:** Dict with type-specific results array.
#### get_artist()
```python
api.get_artist(artist_id: int) -> dict
```
Get artist details.
#### get_album()
```python
api.get_album(album_id: str) -> dict
```
Get album details with tracklist.
**Parameters:**
- `album_id`: Qobuz album ID (string, not int)
**Returns:** Album object with tracks, artists, label, release date, UPC.
#### get_track()
```python
api.get_track(track_id: int) -> dict
```
Get track details.
#### get_track_file_url()
```python
api.get_track_file_url(
track_id: int,
quality: int = 27
) -> dict
```
Get streaming URL for track.
**Parameters:**
- `track_id`: Qobuz track ID
- `quality`: Audio quality (5=MP3 320kbps, 6=FLAC 16/44.1, 7=FLAC 24/96, 27=FLAC Hi-Res)
**Returns:** Dict with `url`, `format_id`, `mime_type`, `sampling_rate`, `bit_depth`.
**Requires:** Active Qobuz subscription. Quality availability depends on subscription tier.
**Example:**
```python
api = qobuz.PrivateAPI(email="user@example.com", password="...")
api.set_access_token()
track = api.get_track(12345678)
file_url = api.get_track_file_url(track["id"], quality=27)
# Download track
import requests
audio_data = requests.get(file_url["url"]).content
with open("track.flac", "wb") as f:
f.write(audio_data)
```
#### get_user_playlists()
```python
api.get_user_playlists(limit: int = 50, offset: int = 0) -> dict
```
Get user's playlists.
#### get_playlist()
```python
api.get_playlist(playlist_id: str) -> dict
```
Get playlist details with tracks.
#### create_playlist()
```python
api.create_playlist(
name: str,
description: str = "",
is_public: bool = False
) -> dict
```
Create new playlist.
#### add_playlist_tracks()
```python
api.add_playlist_tracks(
playlist_id: str,
track_ids: list[int]
) -> dict
```
Add tracks to playlist.
#### get_favorites()
```python
api.get_favorites(
type: str = "tracks",
limit: int = 50,
offset: int = 0
) -> dict
```
Get user's favorites.
**Parameters:**
- `type`: Favorite type (`"tracks"`, `"albums"`, `"artists"`)
#### add_favorite()
```python
api.add_favorite(item_id: int, type: str = "tracks") -> dict
```
Add item to favorites.
#### remove_favorite()
```python
api.remove_favorite(item_id: int, type: str = "tracks") -> dict
```
Remove item from favorites.
## spotify Module
### WebAPI Class
**Purpose:** Interact with Spotify Web API.
**Constructor:**
```python
spotify.WebAPI(
client_id: str = None,
client_secret: str = None,
redirect_uri: str = "http://localhost:8888",
access_token: str = None,
refresh_token: str = None
)
```
**Parameters:**
- `client_id`: Spotify app client ID
- `client_secret`: Spotify app client secret
- `redirect_uri`: OAuth redirect URI (must match app settings)
- `access_token`: Existing access token
- `refresh_token`: Existing refresh token
**Authentication:** OAuth 2.0 with four flow types (set via `set_flow()`).
### Authentication Methods
#### set_flow()
```python
api.set_flow(
flow_type: str = "authorization_code",
scopes: list[str] = None
)
```
Configure OAuth flow.
**Parameters:**
- `flow_type`: Flow type (`"authorization_code"`, `"pkce"`, `"client_credentials"`, `"web_player"`)
- `scopes`: Permission scopes (see Spotify documentation for available scopes)
**Common Scopes:**
- `user-read-private`: Read user profile
- `user-read-email`: Read user email
- `user-library-read`: Read saved tracks/albums
- `user-library-modify`: Modify saved tracks/albums
- `playlist-read-private`: Read private playlists
- `playlist-modify-public`: Modify public playlists
- `playlist-modify-private`: Modify private playlists
- `user-read-playback-state`: Read playback state
- `user-modify-playback-state`: Control playback
#### set_access_token()
```python
api.set_access_token(method: str = "http.server")
```
Obtain access token via OAuth flow.
**Parameters:**
- `method`: Callback method (`"http.server"`, `"flask"`, `"playwright"`)
**Example:**
```python
api = spotify.WebAPI(client_id="...", client_secret="...")
api.set_flow("authorization_code", scopes=["user-library-read", "playlist-read-private"])
api.set_access_token() # Opens browser for user login
```
### Catalog Methods
#### get_album()
```python
api.get_album(album_id: str, market: str = None) -> dict
```
Get album details.
**Parameters:**
- `album_id`: Spotify album ID
- `market`: ISO 3166-1 alpha-2 country code (affects track availability)
#### get_artist()
```python
api.get_artist(artist_id: str) -> dict
```
Get artist details.
#### get_track()
```python
api.get_track(track_id: str, market: str = None) -> dict
```
Get track details.
#### get_playlist()
```python
api.get_playlist(
playlist_id: str,
market: str = None,
fields: str = None
) -> dict
```
Get playlist details.
**Parameters:**
- `playlist_id`: Spotify playlist ID
- `market`: Country code
- `fields`: Comma-separated list of fields to return (filters response)
#### search()
```python
api.search(
query: str,
types: list[str] = ["track"],
market: str = None,
limit: int = 20,
offset: int = 0
) -> dict
```
Search Spotify catalog.
**Parameters:**
- `query`: Search query (supports field filters like `artist:Radiohead track:Creep`)
- `types`: Result types (`["track"]`, `["album"]`, `["artist"]`, `["playlist"]`, or combinations)
- `market`: Country code
- `limit`: Max results per type (max 50)
- `offset`: Pagination offset
**Returns:** Dict with keys for each type (`tracks`, `albums`, `artists`, `playlists`), each containing `items` array and pagination info.
**Example:**
```python
results = api.search("Radiohead Creep", types=["track", "album"], limit=10)
for track in results["tracks"]["items"]:
print(track["name"], track["artists"][0]["name"])
```
### Library Methods
#### get_saved_tracks()
```python
api.get_saved_tracks(limit: int = 20, offset: int = 0, market: str = None) -> dict
```
Get user's saved tracks.
**Requires:** `user-library-read` scope.
#### save_tracks()
```python
api.save_tracks(track_ids: list[str]) -> None
```
Save tracks to user's library.
**Requires:** `user-library-modify` scope.
#### remove_saved_tracks()
```python
api.remove_saved_tracks(track_ids: list[str]) -> None
```
Remove tracks from user's library.
**Requires:** `user-library-modify` scope.
#### check_saved_tracks()
```python
api.check_saved_tracks(track_ids: list[str]) -> list[bool]
```
Check if tracks are in user's library.
**Returns:** Array of booleans corresponding to input track IDs.
### Playlist Methods
#### get_user_playlists()
```python
api.get_user_playlists(limit: int = 20, offset: int = 0) -> dict
```
Get current user's playlists.
**Requires:** `playlist-read-private` scope.
#### create_playlist()
```python
api.create_playlist(
user_id: str,
name: str,
description: str = "",
public: bool = True
) -> dict
```
Create playlist.
**Requires:** `playlist-modify-public` or `playlist-modify-private` scope.
#### add_playlist_items()
```python
api.add_playlist_items(
playlist_id: str,
uris: list[str],
position: int = None
) -> dict
```
Add tracks to playlist.
**Parameters:**
- `playlist_id`: Spotify playlist ID
- `uris`: Track URIs (format: `spotify:track:{id}`)
- `position`: Insert position (0-based, None = append)
#### remove_playlist_items()
```python
api.remove_playlist_items(
playlist_id: str,
uris: list[str]
) -> dict
```
Remove tracks from playlist.
### Playback Methods
#### get_playback_state()
```python
api.get_playback_state(market: str = None) -> dict
```
Get current playback state.
**Requires:** `user-read-playback-state` scope.
**Returns:** Dict with device, track, progress, shuffle/repeat state, or `None` if nothing playing.
#### start_playback()
```python
api.start_playback(
device_id: str = None,
context_uri: str = None,
uris: list[str] = None,
offset: dict = None,
position_ms: int = None
) -> None
```
Start or resume playback.
**Requires:** `user-modify-playback-state` scope.
#### pause_playback()
```python
api.pause_playback(device_id: str = None) -> None
```
Pause playback.
#### skip_to_next()
```python
api.skip_to_next(device_id: str = None) -> None
```
Skip to next track.
#### skip_to_previous()
```python
api.skip_to_previous(device_id: str = None) -> None
```
Skip to previous track.
### Audio Features
#### get_audio_features()
```python
api.get_audio_features(track_id: str) -> dict
```
Get audio features for track.
**Returns:** Dict with:
- `danceability`: 0.0 to 1.0
- `energy`: 0.0 to 1.0
- `key`: Pitch class (0=C, 1=C#, etc.)
- `loudness`: dB
- `mode`: Major (1) or minor (0)
- `speechiness`: 0.0 to 1.0
- `acousticness`: 0.0 to 1.0
- `instrumentalness`: 0.0 to 1.0
- `liveness`: 0.0 to 1.0
- `valence`: 0.0 to 1.0 (positivity)
- `tempo`: BPM
- `duration_ms`: Track length
- `time_signature`: 3 to 7
### PrivateLyricsService Class
**Purpose:** Get lyrics via Spotify's Musixmatch integration.
**Note:** Undocumented private API. May break without notice.
**Constructor:**
```python
spotify.PrivateLyricsService(sp_dc: str = None)
```
**Parameters:**
- `sp_dc`: Spotify `sp_dc` cookie value (from browser)
#### get_lyrics()
```python
service.get_lyrics(track_id: str) -> dict
```
Get lyrics for track.
**Returns:** Dict with `lyrics` object containing `lines` array. Each line has `startTimeMs`, `words`, `syllables`.
**Example:**
```python
service = spotify.PrivateLyricsService(sp_dc="...")
lyrics = service.get_lyrics("3n3Ppam7vgaVa1iaRUc9Lp")
for line in lyrics["lyrics"]["lines"]:
print(f"[{line['startTimeMs']}ms] {line['words']}")
```
## tidal Module
### API Class
**Purpose:** Interact with TIDAL public API.
**Constructor:**
```python
tidal.API(
client_id: str = None,
client_secret: str = None,
access_token: str = None,
refresh_token: str = None
)
```
**Authentication:** OAuth 2.0 (client credentials or PKCE).
### PrivateAPI Class
**Purpose:** Interact with TIDAL private API (streaming URLs, lyrics, credits).
**Note:** Uses undocumented endpoints. May break without notice.
**Constructor:**
```python
tidal.PrivateAPI(
client_id: str = None,
client_secret: str = None,
access_token: str = None,
refresh_token: str = None,
user_id: int = None,
country_code: str = "US"
)
```
### Methods (Common to Both Classes)
#### search()
```python
api.search(
query: str,
type: str = "TRACKS",
limit: int = 50,
offset: int = 0
) -> dict
```
Search TIDAL catalog.
**Parameters:**
- `query`: Search query
- `type`: Result type (`"TRACKS"`, `"ALBUMS"`, `"ARTISTS"`, `"PLAYLISTS"`)
- `limit`: Max results
- `offset`: Pagination offset
#### get_artist()
```python
api.get_artist(artist_id: int) -> dict
```
Get artist details.
#### get_album()
```python
api.get_album(album_id: int) -> dict
```
Get album details.
#### get_track()
```python
api.get_track(track_id: int) -> dict
```
Get track details.
### PrivateAPI-Specific Methods
#### get_track_stream_url()
```python
api.get_track_stream_url(
track_id: int,
quality: str = "LOSSLESS"
) -> str
```
Get streaming URL for track.
**Parameters:**
- `track_id`: TIDAL track ID
- `quality`: Audio quality (`"LOW"`, `"HIGH"`, `"LOSSLESS"`, `"HI_RES"`, `"HI_RES_LOSSLESS"`)
**Returns:** Direct streaming URL (time-limited).
**Quality Levels:**
- `LOW`: 96 kbps AAC
- `HIGH`: 320 kbps AAC
- `LOSSLESS`: FLAC 16-bit/44.1kHz (CD quality)
- `HI_RES`: FLAC 24-bit/96kHz or higher
- `HI_RES_LOSSLESS`: MQA (Master Quality Authenticated)
**Requires:** Active TIDAL subscription. Quality availability depends on subscription tier (HiFi, HiFi Plus).
**Example:**
```python
api = tidal.PrivateAPI(client_id="...", client_secret="...")
api.set_flow("pkce")
api.set_access_token()
url = api.get_track_stream_url(12345678, quality="HI_RES")
import requests
audio_data = requests.get(url).content
with open("track.flac", "wb") as f:
f.write(audio_data)
```
#### get_track_lyrics()
```python
api.get_track_lyrics(track_id: int) -> dict
```
Get lyrics for track.
**Returns:** Dict with `lyrics` (plain text) and `subtitles` (LRC format with timestamps).
#### get_track_credits()
```python
api.get_track_credits(track_id: int) -> dict
```
Get credits for track (producers, engineers, musicians, etc.).
**Returns:** Dict with `credits` array containing `type` and `contributors`.
#### get_user_playlists()
```python
api.get_user_playlists(user_id: int = None, limit: int = 50, offset: int = 0) -> dict
```
Get user's playlists.
#### get_playlist()
```python
api.get_playlist(playlist_id: str) -> dict
```
Get playlist details with tracks.
#### create_playlist()
```python
api.create_playlist(title: str, description: str = "") -> dict
```
Create playlist.
#### add_playlist_items()
```python
api.add_playlist_items(playlist_id: str, track_ids: list[int]) -> dict
```
Add tracks to playlist.
## Error Handling
All API methods raise `RuntimeError` on HTTP errors:
```python
try:
track = api.get_track(12345)
except RuntimeError as e:
print(f"API error: {e}")
```
**Error Message Format:** `"{METHOD} {URL} failed: {status_code} {response_text}"`
**No Typed Exceptions:** All errors are generic `RuntimeError`. Parse error message to determine cause.
## Rate Limiting
**Not Implemented:** minim does not enforce rate limits. Caller must respect service limits:
- **Discogs:** 60 req/min (authenticated), 25 req/min (unauthenticated)
- **iTunes:** ~20 req/min (undocumented)
- **Qobuz:** Unknown (private API)
- **Spotify:** Varies by endpoint, typically 180 req/30sec
- **TIDAL:** Unknown (private API)
**Recommendation:** Implement exponential backoff and respect `Retry-After` headers.
## Summary
minim provides comprehensive Python APIs for five music services:
- **discogs:** Database, marketplace, collection, wantlist
- **itunes:** Public search and lookup
- **qobuz:** Catalog, streaming, playlists, favorites
- **spotify:** Full Web API coverage, playback control, audio features, lyrics
- **tidal:** Catalog, high-resolution streaming, lyrics, credits
All APIs follow consistent patterns (authentication, request handling, error raising) while exposing service-specific features. The `Audio` class bridges API responses to audio file metadata, enabling automated tagging workflows.