Skip to content

Latest commit

 

History

History
568 lines (452 loc) · 39 KB

File metadata and controls

568 lines (452 loc) · 39 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This guidance is aimed at Claude Code but may as well be suitable for other AI tooling, such as GitHub Copilot.

Instructions for an LLM (such as Claude) working with the code:

  • Take these instructions in mind
  • Look at existing code, type hints, docstrings and comments
  • Propose changes to extend this document with new learnings

Project Overview

deezer-python-gql is an async typed Python client for Deezer's Pipe GraphQL API (pipe.deezer.com/api). It is a standalone library designed to be consumed by Music Assistant's Deezer provider, replacing the official deezer-python REST wrapper with a type-safe GraphQL client.

All client methods and response models are generated from the GraphQL schema and .graphql query files using ariadne-codegen. The only hand-written code is the base client (auth), the schema conversion script, and tests.

Development Commands

Setup and Dependencies

  • make setup — Install all dependencies (runtime + test + dev/codegen) and configure pre-commit hooks
  • Uses uv as the package manager (not pip directly)
  • Always re-run make setup after pulling latest code

Code Generation

  • make generate — Fetch live schema from pipe.deezer.com, convert to SDL, run ariadne-codegen (full pipeline)
  • make codegen — Convert existing schema.json to SDL and run ariadne-codegen (no live fetch, faster)
  • make fetch-schema — Only fetch introspection JSON from the live API (no auth required)
  • make clean-generated — Delete all generated code (deezer_python_gql/generated/)

Exploring the API

  • uv run python scripts/explore.py queries/get_me.graphql — Run a .graphql file against the live API
  • uv run python scripts/explore.py -q '{ me { id } }' — Run an inline query
  • uv run python scripts/explore.py -q '...' -v '{"id": "123"}' — Pass variables as JSON
  • make explore Q=queries/get_me.graphql — Shorthand via Make
  • Requires a .env file with DEEZER_ARL=your_arl_value (.env is gitignored)
  • Handles JWT auth automatically via DeezerBaseClient

Testing and Quality

  • make test or uv run pytest — Run all tests (pytest with coverage enabled by default)
  • make lint or uv run pre-commit run --all-files — Run all linters and type checks
  • uv run pytest tests/test_client.py — Run a specific test file
  • uv run mypy — Type check only
  • uv run ruff check . — Lint only
  • uv run ruff format --check . — Format check only

Always run make lint after a code change to ensure the new code adheres to the project standards.

Pre-commit Hooks (in order)

  1. ruff format — Code formatting
  2. ruff check --fix — Linting with auto-fix
  3. codespell — Spell checking (skips *.json, schema.graphql)
  4. check-ast — Python AST validation
  5. check-json — JSON validation (excludes schema.json)
  6. check-toml — TOML validation
  7. mypy — Strict type checking

Architecture

Directory Structure

deezer-python-gql/
├── deezer_python_gql/           # Package source
│   ├── __init__.py              # Public API — exports DeezerGQLClient
│   ├── base_client.py           # Hand-written: ARL→JWT auth, execute(), get_data(), check_audiobook_ids()
│   ├── py.typed                 # PEP 561 marker for type checkers
│   └── generated/               # AUTO-GENERATED by ariadne-codegen — never edit manually
│       ├── __init__.py
│       ├── base_client.py       # Generated copy of base client (codegen artifact)
│       ├── base_model.py        # Pydantic base model configuration
│       ├── client.py            # DeezerGQLClient with typed methods (one per .graphql query)
│       ├── enums.py             # GraphQL enum types used by queries
│       ├── fragments.py         # Pydantic base classes from shared GraphQL fragments
│       ├── input_types.py       # GraphQL input types used by mutations
│       ├── get_me.py            # Response models for GetMe query
│       ├── get_track.py         # Response models for GetTrack query
│       ├── get_album.py         # Response models for GetAlbum query
│       ├── get_artist.py        # Response models for GetArtist query
│       ├── get_playlist.py      # Response models for GetPlaylist query
│       ├── get_livestream.py    # Response models: livestream (radio station) details
│       ├── get_podcast.py       # Response models: podcast with episodes and rights
│       ├── get_podcast_episode.py  # Response models: single podcast episode with media
│       ├── get_audiobook.py     # Response models: audiobook with paginated chapters
│       ├── get_audiobook_chapter.py  # Response models: audiobook chapter with media
│       ├── search.py            # Response models for Search query
│       ├── search_flows.py      # Response models for SearchFlows query
│       ├── get_flow.py          # Response models: user's default Flow
│       ├── get_flow_batch.py    # Response models: 4 batched Flow track sets (aliased)
│       ├── get_flow_configs.py  # Response models: mood & genre flow configs
│       ├── get_flow_config_tracks.py  # Response models: tracks for a flow config
│       ├── get_made_for_me.py   # Response models: SmartTracklist/Flow items
│       ├── get_smart_tracklist.py  # Response models: smart tracklist with tracks
│       ├── get_similar_tracks.py  # Response models: recommended/similar tracks
│       ├── get_artist_mix.py    # Response models: artist mix tracks
│       ├── get_track_mix.py     # Response models: track mix tracks
│       ├── get_charts.py        # Response models: country charts
│       ├── get_recommendations.py  # Response models: personalized recommendations
│       ├── get_user_charts.py   # Response models: personal top tracks/artists/albums
│       ├── get_recently_played.py  # Response models: recently played content
│       ├── get_favorite_artists.py  # Response models: favorite artists
│       ├── get_favorite_albums.py   # Response models: favorite albums
│       ├── get_favorite_tracks.py   # Response models: favorite tracks
│       ├── get_favorite_playlists.py  # Response models: favorite playlists
│       ├── get_favorite_podcasts.py  # Response models: favorite podcasts
│       ├── get_favorite_audiobooks.py  # Response models: favorite audiobook IDs
│       ├── get_podcast_episode_bookmarks.py  # Response models: podcast bookmarks
│       ├── get_music_together_groups.py  # Response models: Music Together groups
│       ├── get_music_together_group.py  # Response models: single Music Together group
│       ├── get_music_together_affinity.py  # Response models: Music Together affinity
│       ├── add_*_to_favorite.py  # Mutation models: add artist/album/track/playlist/podcast/audiobook
│       ├── remove_*_from_favorite.py  # Mutation models: remove favorites
│       ├── create_playlist.py   # Mutation model: create playlist
│       ├── update_playlist.py   # Mutation model: update playlist
│       ├── delete_playlist.py   # Mutation model: delete playlist
│       ├── add_tracks_to_playlist.py  # Mutation model: add tracks to playlist
│       ├── remove_tracks_from_playlist.py  # Mutation model: remove tracks from playlist
│       ├── bookmark_podcast_episode.py  # Mutation model: bookmark episode
│       ├── unbookmark_podcast_episode.py  # Mutation model: unbookmark episode
│       ├── mark_as_played_podcast_episode.py  # Mutation model: mark played
│       ├── mark_as_not_played_podcast_episode.py  # Mutation model: mark not played
│       ├── music_together_create_group.py  # Mutation model: create Music Together group
│       ├── music_together_join_group.py  # Mutation model: join group
│       ├── music_together_leave_group.py  # Mutation model: leave group
│       ├── music_together_refresh_suggested_tracklist.py  # Mutation model: refresh tracks
│       ├── music_together_update_group_settings.py  # Mutation model: update settings
│       └── music_together_generate_group_name.py  # Mutation model: generate name
├── queries/                     # GraphQL query/mutation documents (.graphql files)
│   ├── fragments.graphql        # Shared fragments (TrackFields, ArtistFields, etc.)
│   ├── get_me.graphql           # GetMe: current authenticated user
│   ├── get_track.graphql        # GetTrack: full track details with media/lyrics
│   ├── get_album.graphql        # GetAlbum: album details with paginated tracks
│   ├── get_artist.graphql       # GetArtist: artist with top tracks and albums
│   ├── get_playlist.graphql     # GetPlaylist: playlist with paginated tracks
│   ├── get_livestream.graphql   # GetLivestream: livestream (radio station) details
│   ├── get_podcast.graphql      # GetPodcast: podcast with episodes and rights
│   ├── get_podcast_episode.graphql  # GetPodcastEpisode: episode with media and podcast ref
│   ├── get_audiobook.graphql    # GetAudiobook: audiobook with paginated chapters
│   ├── get_audiobook_chapter.graphql  # GetAudiobookChapter: chapter with media token
│   ├── search.graphql           # Search: unified search across entity types
│   ├── search_flows.graphql     # SearchFlows: discover all available Deezer flows via search
│   ├── get_flow.graphql         # GetFlow: user's default Flow with tracks
│   ├── get_flow_batch.graphql   # GetFlowBatch: 4 batched Flow track sets using aliases
│   ├── get_flow_configs.graphql # GetFlowConfigs: mood & genre flow config lists
│   ├── get_flow_config_tracks.graphql  # GetFlowConfigTracks: tracks for a specific flow config
│   ├── get_made_for_me.graphql  # GetMadeForMe: SmartTracklist & Flow items
│   ├── get_smart_tracklist.graphql  # GetSmartTracklist: tracklist with paginated tracks
│   ├── get_similar_tracks.graphql  # GetSimilarTracks: recommended tracks for a given track
│   ├── get_artist_mix.graphql   # GetArtistMix: track mix from given artists
│   ├── get_track_mix.graphql    # GetTrackMix: track mix from given tracks
│   ├── get_charts.graphql       # GetCharts: country charts (tracks/albums/artists/playlists)
│   ├── get_recommendations.graphql  # GetRecommendations: personalized recommendations
│   ├── get_user_charts.graphql  # GetUserCharts: personal top tracks/artists/albums
│   ├── get_recently_played.graphql  # GetRecentlyPlayed: recently played mixed content
│   ├── get_favorite_artists.graphql  # GetFavoriteArtists: paginated favorite artists
│   ├── get_favorite_albums.graphql   # GetFavoriteAlbums: paginated favorite albums
│   ├── get_favorite_tracks.graphql   # GetFavoriteTracks: paginated favorite tracks
│   ├── get_favorite_playlists.graphql  # GetFavoritePlaylists: paginated favorite playlists
│   ├── get_favorite_podcasts.graphql  # GetFavoritePodcasts: paginated favorite podcasts
│   ├── get_favorite_audiobooks.graphql  # GetFavoriteAudiobooks: favorite audiobook IDs
│   ├── get_podcast_episode_bookmarks.graphql  # GetPodcastEpisodeBookmarks: bookmarked episodes
│   ├── get_music_together_groups.graphql  # GetMusicTogetherGroups: user's Music Together groups
│   ├── get_music_together_group.graphql  # GetMusicTogetherGroup: single group with tracks
│   ├── get_music_together_affinity.graphql  # GetMusicTogetherAffinity: group member affinity
│   ├── favorites.graphql        # Mutations: add/remove favorites (all entity types)
│   ├── playlists.graphql        # Mutations: create/update/delete/add/remove playlist tracks
│   └── music_together.graphql   # Mutations: create/join/leave/refresh/update/generate groups
├── schema.graphql               # Full SDL schema (16,668 lines, ~915 types) — generated
├── schema.json                  # Raw introspection JSON — generated
├── scripts/
│   ├── convert_schema.py        # Fetch introspection + fix broken types + convert to SDL
│   └── explore.py               # Ad-hoc query runner for development (reads ARL from .env)
├── tests/
│   ├── test_client.py           # Unit tests: client setup, model parsing from fixtures
│   └── fixtures/                # Recorded API responses (sanitized, never from live calls)
│       ├── get_me.json
│       ├── get_track.json
│       ├── get_album.json
│       ├── get_artist.json
│       ├── get_playlist.json
│       ├── get_livestream.json
│       ├── get_podcast.json
│       ├── get_podcast_episode.json
│       ├── get_audiobook.json
│       ├── get_audiobook_chapter.json
│       ├── search.json
│       ├── search_flows.json
│       ├── get_flow.json
│       ├── get_flow_batch.json
│       ├── get_flow_configs.json
│       ├── get_flow_config_tracks.json
│       ├── get_similar_tracks.json
│       ├── get_artist_mix.json
│       ├── get_track_mix.json
│       ├── get_made_for_me.json
│       ├── get_smart_tracklist.json
│       ├── get_charts.json
│       ├── get_recommendations.json
│       ├── get_user_charts.json
│       ├── get_user_playlists.json
│       ├── get_recently_played.json
│       ├── get_favorite_artists.json
│       ├── get_favorite_albums.json
│       ├── get_favorite_tracks.json
│       ├── get_favorite_playlists.json
│       ├── get_favorite_podcasts.json
│       ├── get_favorite_audiobooks.json
│       ├── get_podcast_episode_bookmarks.json
│       ├── get_music_together_groups.json
│       ├── get_music_together_group.json
│       ├── get_music_together_affinity.json
│       ├── add_*_to_favorite.json  # One per entity type (artist, album, track, etc.)
│       ├── remove_*_from_favorite.json  # One per entity type
│       ├── create_playlist.json
│       ├── update_playlist.json
│       ├── delete_playlist.json
│       ├── add_tracks_to_playlist.json
│       ├── remove_tracks_from_playlist.json
│       ├── bookmark_podcast_episode.json
│       ├── unbookmark_podcast_episode.json
│       ├── mark_as_played_podcast_episode.json
│       ├── mark_as_not_played_podcast_episode.json
│       ├── music_together_create_group.json
│       ├── music_together_join_group.json
│       ├── music_together_leave_group.json
│       └── music_together_generate_group_name.json
├── pyproject.toml               # Project config (dependencies, ruff, mypy, pytest, codegen)
├── Makefile                     # Dev commands
├── .pre-commit-config.yaml      # Local hooks (no remote repos)
├── cliff.toml                   # git-cliff changelog config
└── .github/workflows/           # CI: test.yml, publish-to-pypi.yml, release.yml

Code Generation Pipeline

pipe.deezer.com/api   →   schema.json   →   schema.graphql   →   deezer_python_gql/generated/
  (introspection)       (raw JSON)          (SDL, fixed)          (typed client + models)
       ↑                     ↑                    ↑                        ↑
  fetch_introspection   fix_introspection    convert_to_sdl         ariadne-codegen
  (scripts/convert_schema.py)                                     (reads queries/*.graphql)
  1. Introspection (no auth): scripts/convert_schema.py --fetch sends the standard introspection query to pipe.deezer.com/api. No authentication required.
  2. Fix: Patches broken introspection results — missing possibleTypes on unions, missing interfaces on objects, truncated NON_NULL/LIST type wrappers.
  3. SDL conversion: Uses graphql-core's build_client_schema + print_schema to produce clean SDL.
  4. Codegen: ariadne-codegen reads schema.graphql + all queries/*.graphql files and generates the typed async client class with Pydantic response models.

Shared Fragments

queries/fragments.graphql defines reusable field sets shared across multiple queries:

Fragment Used by Key fields
TrackFields search, get_track, get_album, get_artist, get_playlist, flow, flow_batch, flow_config_tracks, smart_tracklist, charts, user_charts, fav_tracks, similar_tracks, artist_mix, track_mix, music_together id, title, ISRC, diskInfo, duration, isExplicit, isFavorite, popularity, album, contributors
ArtistFields search, get_artist, charts, user_charts, recommendations, fav_artists, recently_played id, name, picture, fansCount, isFavorite, bio (summary + full)
AlbumFields search, get_album, get_artist, charts, user_charts, recommendations, fav_albums, recently_played id, displayTitle, type, cover, contributors, releaseDate, isExplicit, isFavorite, fansCount, label, copyright
PlaylistFields search, get_playlist, charts, recommendations, fav_playlists, recently_played id, title, picture, estimatedTracksCount, fansCount, isFavorite, description, owner
LivestreamFields search, get_livestream id, name, language, description, isOnStream, country, cover, media (url + codec)
PodcastFields search, get_podcast, fav_podcasts id, displayTitle, cover, description, isExplicit, isFavorite, type
PodcastEpisodeFields search, get_podcast, get_podcast_episode, podcast_episode_bookmarks id, title, description, duration, cover, publicationDate, media (url + codec)
AudiobookFields get_audiobook id, displayTitle, cover, description, duration, releaseDate, fansCount, isExplicit, isFavorite, chaptersCount, discsCount, producerLine, publisher, contributors
AudiobookChapterFields get_audiobook, get_audiobook_chapter id, isrc, displayTitle, diskInfo, duration, isExplicit, isFavorite
PageInfoFields All paginated queries hasNextPage, endCursor

All entity queries use shared fragments. Every query that returns tracks, albums, artists, or playlists uses the corresponding fragment via ...TrackFields etc. Some queries (e.g., get_track.graphql, get_album.graphql) extend the fragment with additional fields like media, lyrics, url, or tracksCount.

ariadne-codegen generates fragments.py with Pydantic base classes (TrackFields, ArtistFields, etc.). Query model classes inherit from these via multiple inheritance, e.g., GetChartsChartsCountryTracksEdgesNode(TrackFields). This means fragment fields are type-safe and IDE-discoverable on all inheriting models. Consumers can type method parameters as TrackFields and accept any query-specific track model.

To use a fragment in a new query, reference it with ...TrackFields in the .graphql file.

Adding a New Query

  1. Create a .graphql file in queries/ (e.g., queries/get_user_favorites.graphql)
  2. Define the query using types from schema.graphql — reuse existing fragments where applicable
  3. Run make codegen (or make generate for a fresh schema)
  4. ariadne-codegen produces:
    • A new method on DeezerGQLClient (e.g., get_user_favorites())
    • A new response model file in generated/ (e.g., get_user_favorites.py)
  5. Add tests in tests/

Adding a New Mutation

Same as queries — create a .graphql file with a mutation operation, run make codegen.

Authentication Flow

ARL cookie → POST auth.deezer.com/login/arl?jo=p&rto=c&i=c → JWT (6 min TTL)
                                                                     ↓
                                                            Bearer token in
                                                            Authorization header
                                                                     ↓
                                                            POST pipe.deezer.com/api
  • The ARL cookie must be set on the auth.deezer.com domain (not www.deezer.com)
  • Response is Content-Type: text/plain containing JSON — parse with json.loads(resp.text), not resp.json()
  • JWT expiration is decoded from the token payload (base64url-encoded second segment)
  • Auto-refresh triggers 30 seconds before expiry

Response Handling

The DeezerBaseClient.get_data() method handles two important edge cases:

  1. Partial errors: When the GraphQL response contains both data and errors (e.g., deleted albums in a favorites list), the errors are logged but valid data is returned. Only responses with errors and no data raise GraphQLClientGraphQLMultiError.

  2. Missing __typename injection: The Deezer API omits __typename for single-member union types (e.g., Contributor is always Artist). Since Pydantic discriminated unions require this field, _inject_missing_typenames() recursively patches it into the response before model validation. The mapping is defined in _TYPENAME_PATCHES.

Connection Pooling

DeezerBaseClient self-manages its own httpx.AsyncClient, created lazily on first request. No external setup needed:

# Recommended: use as async context manager
async with DeezerGQLClient(arl="your_arl") as client:
    me = await client.get_me()

# Or manage lifecycle manually
client = DeezerGQLClient(arl="your_arl")
try:
    me = await client.get_me()
finally:
    await client.close()

An external http_client can still be passed for advanced use cases (e.g., sharing a pool across multiple clients), but the caller is then responsible for closing it.

Audiobook ID Checking

The check_audiobook_ids(album_ids) method on DeezerBaseClient batch-checks whether album IDs are actually audiobooks. It builds a single aliased GraphQL query:

{ a0: audiobook(audiobookId: "123") { id displayTitle } a1: audiobook(audiobookId: "456") { id displayTitle } }

Important: Querying only { id } is insufficient — the API echoes back the input ID for any valid album, regardless of whether it's an audiobook. The displayTitle field returns null (with AudiobookNotFoundError) for non-audiobooks, so the method checks displayTitle is not None to distinguish real audiobooks.

Key Configuration

  • Python: 3.12+ required (tested on 3.12, 3.13)
  • Runtime dependencies: httpx>=0.27.0, pydantic>=2.0.0 — intentionally minimal
  • Dev dependencies (extras [dev]): ariadne-codegen[subscriptions] — only needed for code generation
  • Test dependencies (extras [test]): ruff, mypy, pytest, pytest-asyncio, pytest-cov, codespell, pre-commit
  • Package manager: uv (not pip)
  • Build system: setuptools (declared in pyproject.toml)

ariadne-codegen Configuration (in pyproject.toml)

Setting Value Purpose
schema_path schema.graphql SDL schema input
queries_path queries Directory of .graphql query files
target_package_name generated Output package name
target_package_path deezer_python_gql Output parent directory
client_name DeezerGQLClient Generated client class name
base_client_name DeezerBaseClient Custom base client class name
base_client_file_path deezer_python_gql/base_client.py Custom base client file
async_client true Generate async methods
include_all_inputs false Only generate inputs used by queries
include_all_enums false Only generate enums used by queries
plugins ShorterResultsPlugin Unwrap single-field responses for cleaner API

Code Style Guidelines

General Rules

  • ruff handles all formatting and linting (config in pyproject.toml, select = ["ALL"] with targeted ignores)
  • Line length: 100 characters
  • Target: Python 3.12
  • deezer_python_gql/generated and scripts/ are excluded from ruff

Never Edit Generated Code

The deezer_python_gql/generated/ directory is entirely auto-generated by ariadne-codegen. Never edit files in this directory manually. To change the generated output:

  • Modify the .graphql query files in queries/
  • Or modify base_client.py (which codegen copies into generated/)
  • Then run make codegen

Docstring Format

Uses Sphinx-style docstrings consistent with Music Assistant:

def my_function(param1: str, param2: int) -> str:
    """Brief one-line description.

    Optional longer description.

    :param param1: Description of param1.
    :param param2: Description of param2.
    """

For simple functions, a single-line docstring is sufficient:

def get_item(self, item_id: str) -> Item:
    """Get an item by its ID."""

Comments

Use comments only to explain "why", not "what". Reserve inline comments for complex multi-line blocks.

Type Hints

  • All functions must have complete type annotations (enforced by mypy strict mode)
  • Use from __future__ import annotations for modern syntax in all files
  • Prefer X | None over Optional[X]
  • Use cast() sparingly and only when the type system genuinely can't infer

Testing

  • Tests are in tests/
  • Framework: pytest with pytest-asyncio (asyncio_mode = "auto")
  • Coverage: enabled by default via addopts = "--cov deezer_python_gql" in pyproject.toml
  • No live API calls — all tests use mocked HTTP or recorded JSON fixtures

Test Structure

Tests are organized into five layers:

Layer Tests What's validated
Client setup 3 Import, instantiation with ARL, presence of all generated methods
Lifecycle 3 close() on internal client, skip close on external client, context manager
Auth flow (mocked) 5 JWT acquisition, token reuse, refresh on expiry, cookie domain, text/plain parsing
Error handling 5 HTTP errors, invalid JSON, missing data key, GraphQL errors, success path
Audiobook ID checking 2 Batch alias query returns correct subset; empty input returns empty set
Model smoke tests 62 One per query/mutation — fixture parses correctly with key fields accessible

Auth Flow Tests

Auth tests mock httpx.AsyncClient to verify the hand-written DeezerBaseClient logic:

  • JWT is acquired from auth.deezer.com/login/arl on first request
  • Valid JWT is reused without re-auth
  • Expiring JWT (within 30s margin) triggers refresh
  • ARL cookie is sent to the correct domain (auth.deezer.com, not www.deezer.com)
  • text/plain response body is correctly parsed as JSON

Error Handling Tests

Error tests call get_data() directly with crafted httpx.Response objects:

  • GraphQLClientHttpError for non-2xx status codes
  • GraphQLClientInvalidResponseError for non-JSON or structurally invalid responses
  • GraphQLClientGraphQLMultiError for GraphQL-level errors with message/location/path

Model Smoke Tests

One test per query verifying the recorded JSON fixture parses through the generated Pydantic models. These catch regressions when re-running codegen after schema changes.

Fixtures live in tests/fixtures/ — recorded from the live API, then sanitized (user IDs, media tokens).

Fixture Requirements

Fixtures must include __typename fields for discriminated union types:

  • "__typename": "Artist" on all Contributor union nodes
  • "__typename": "Url" on all DeezerUrl interface nodes

The explore.py script does not add __typename automatically — patch fixtures manually or with a script after recording.

Test Naming Convention

def test_<what_is_being_tested>() -> None:
    """Describe the expected behavior."""

CI/CD

Commit Messages

Use Conventional Commits format:

<type>: <short summary>

<optional body>

Common types: feat, fix, docs, chore, refactor, test, ci.

git-cliff uses these prefixes to categorize changelog entries automatically (configured in cliff.toml).

Workflows

Workflow Trigger Purpose
test.yml Push to main, PRs, or reusable Lint + type check + test on Python 3.12 & 3.13
release.yml Manual (workflow_dispatch) Run tests → bump version → generate changelog → GitHub Release
publish-to-pypi.yml GitHub Release published Build and publish to PyPI via pypa/gh-action-pypi-publish

Release Process

  1. Push conventional commits to main (directly or via PR)
  2. Go to Actions → Release → Run workflow (optionally force major/minor/patch)
  3. git-cliff auto-detects next version from commit types, generates changelog
  4. GitHub Release is created with tag → triggers PyPI publish automatically

Branching

  • main — single development branch, all PRs target this
  • No dev/stable split (simple library, not a server)

Deezer Pipe API Reference

Key Facts

  • Endpoint: https://pipe.deezer.com/api (POST, standard GraphQL)
  • Introspection: Enabled without auth (~915 types, 16,668 lines SDL)
  • Auth: ARL → JWT Bearer token (6 min TTL, auto-refresh)
  • Rate limiting: Not documented; GraphQL naturally batches, reducing request volume
  • Schema stability: Undocumented, unofficial API — may change without notice

Commonly Used Types

Type Key Fields
Track id, title, ISRC, diskInfo, duration, album, contributors, lyrics, media (token + sizes), isFavorite, recommendedTracks, isAtmos, isExplicit
Album id, displayTitle, type, cover, label, contributors, releaseDate, tracks, isFavorite, fallback
Artist id, name, bio, picture, albums(types, roles), topTracks, relatedArtist, isFavorite
Playlist id, title, description, picture, tracks(first, after, order), rawTracks, owner, isFavorite
Livestream id, name, language, description, cover, country, media: [ExternalMedia!]!
Podcast id, displayTitle, description, cover, type, isExplicit, isFavorite, episodes(first, after), hasRights
PodcastEpisode id, title, description, duration, cover, publicationDate, media (url + codec), url, podcast
Audiobook id, displayTitle, cover, description, duration, releaseDate, chaptersCount, discsCount, publisher, contributors, chapters(first, after)
AudiobookChapter id, isrc, displayTitle, diskInfo, duration, gain, media (token + sizes + rights), audiobook
Lyrics synchronizedLines, synchronizedWordByWordLines, text, copyright, writers
Flow id, title, cover, tracks: [FlowTrack!]! (each call returns fresh batch)
FlowConfig id, title, visuals, tracks: [FlowConfigTrack!]!
TrackMedia id, version, token, estimatedSizes, rights: MediaRights!
Search results.{tracks, artists, albums, playlists, podcasts, podcastEpisodes, livestreams}, topResult, mixedResults
PrivateUser id, playlists, userFavorites, favorites (deprecated, used for audiobooks), flow, flowConfigs, recommendations, madeForMe, recentlyPlayed, charts, musicTogetherGroups, podcastEpisodeBookmarks
MusicTogetherGroup id, name, isReady, isFamily, members, suggestedTracklist(mood), curatedTracklist, estimatedMembersCount

Key Mutations

  • Favorites: add/remove{Album,Artist,Track,Playlist,Podcast,Audiobook}ToFavorite/FromFavorite (audiobook mutations deprecated but functional)
  • Playlists: createPlaylist, updatePlaylist, deletePlaylist, addTracksToPlaylist, removeTracksFromPlaylist
  • Podcasts: bookmarkPodcastEpisode, unbookmarkPodcastEpisode, markAsPlayedPodcastEpisode, markAsNotPlayedPodcastEpisode
  • Music Together: musicTogetherCreateGroup, musicTogetherJoinGroup, musicTogetherLeaveGroup, musicTogetherRefreshSuggestedTracklist, musicTogetherUpdateGroupSettings, musicTogetherGenerateGroupName
  • Not available: No listen logging mutation (log.listen only exists in GW API)

Auth Gotchas

  1. ARL cookie domain must be auth.deezer.com, not www.deezer.com
  2. Auth response is Content-Type: text/plain — use json.loads(resp.text)
  3. Endpoint is login/arl, not login/renew (which requires OAuth refresh token)
  4. JWT TTL is 360 seconds — refresh proactively (30s margin implemented in base client)

Schema Quirks

The live introspection result has known issues that scripts/convert_schema.py auto-fixes:

  • Union types may have empty possibleTypes
  • Object types may be missing interfaces field
  • NON_NULL/LIST type wrappers may be truncated (missing ofType)
  • These are fixed during the JSON→SDL conversion step