diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cc3913e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,138 @@ +# Contributing + +## What this library is + +- A headless Spotify client, allowing you to **authenticate and retrieve a decrypted audio stream for any track**. +- *Not* a standalone audio player: the **provided stream must be piped to another application** (like `ffplay`) or handled by a server to be played. + +## Environment setup + +### Prerequisites +- Python 3.10+ + +### Install runtime packages + +```sh +pip install -r requirements.txt +``` + +### Install protoc + +> This step is **only needed if you're changing any `.proto` serialization schema files**, +> which will subsequently require using the protoc compiler to generate updated versions of +> the `*_pb2.py` Python stubs that implement serialization/deserialization for those schemas. + +- Go to the [protobuf release matching the version pinned in `requirements.txt`](https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.1). +- Download and install the `protoc-*.zip` file meant for your platform. + +After modifying the `.proto` files you need to, **make sure to follow [these steps](#protocol-buffer-generation) to regenerate the Python stubs**. + +## Protocol buffer generation + +> These steps are only necessary after changing `.proto` files. + +- From the repository root, conveniently recompile all `.proto` schema files with this command: + + ```bash + find proto -name "*.proto" | xargs protoc -I=proto --python_out=librespot/proto + ``` + +- Alternatively, to recompile a single file (e.g. `proto/metadata.proto`), run: + + ```bash + protoc -I=proto --python_out=librespot/proto proto/metadata.proto + ``` + +- Commit both the source `.proto` and the regenerated Python output **together** so they can +be compared easily. + +## Architecture + +The main components are: + +- **`Session` class** *(entrypoint)* + + - `Session.Builder` is used to configure and create a session, via one of: + + - username/password + - stored credentials + - OAuth + + - An active session is **required** for all other operations. + +- **`ApiClient` class** + + - A high-level client for making standard HTTPS requests to Spotify's Web API endpoints (e.g., `https://spclient.wg.spotify.com`). + - Accessed via `session.api()`, it provides convenient methods like `get_metadata_4_track()` and handles client tokens automatically. + +- **`MercuryClient` class** + + - The low-level client for Spotify's proprietary `mercury` protocol, which uses `hm://` URIs. + - Accessed via `session.mercury()`, it handles sending and receiving messages over the main session connection for metadata lookups and subscriptions that are not available via the standard Web API. + +- **`DealerClient` class** + + - Manages the persistent WebSocket (`wss://`) connection to Spotify's `dealer` service. + - Accessed via `session.dealer()`, it listens for and dispatches real-time, asynchronous JSON-based events, such as remote player state changes or notifications from other connected devices. + +- **`Session.Receiver` thread** + + - Spawned after authentication to read every encrypted packet coming from the access point. + - Routes decoded commands to subsystems (`MercuryClient`, `AudioKeyManager`, `ChannelManager`, etc.) and responds to keep-alive pings to hold the session open. + +- **Metadata types** + + - The `librespot.metadata` module provides typed identifiers (`TrackId`, `AlbumId`, `PlaylistId`, `EpisodeId`, etc.) used to reference Spotify content throughout the API. + - They are constructed from Spotify identifiers, typically using one of the following methods: + + - `from_uri()`: For all ID types. + - `from_base62()`: For most ID types (e.g., tracks, albums, artists). + +- **`PlayableContentFeeder` class** + + - Retrieves audio streams; is accessed via `session.content_feeder()`. + - `load(playable_id, audio_quality_picker, preload, halt_listener)`: + + - Accepts: + + - a `TrackId` or `EpisodeId` (any `PlayableId`) + - an `AudioQualityPicker` + - a `preload` flag + - an optional `HaltListener` callback (pass `None` if unneeded). + + - Returns a `LoadedStream` that contains the decrypted stream together with: + + - track/episode metadata + - normalization data + - transfer metrics + +- **`audio` module** + + - Contains tools for format selection, quality management, streaming, and decryption. + - `VorbisOnlyAudioQuality` and `LosslessOnlyAudioQuality` choose the best matching `Metadata.AudioFile` for a preferred container/quality combination. + - `CdnManager` acquires and refreshes signed CDN URLs, feeding a `Streamer` that decrypts chunks on the fly while staying seekable. + +- **`AudioKeyManager` and `ChannelManager`** + + - Handle the low-level transport for protected audio: `AudioKeyManager` requests AES keys, and `ChannelManager` can stream encrypted chunks directly from the access point when CDN delivery is unavailable. + - Both are driven transparently by `PlayableContentFeeder`/`CdnManager`, so callers only interact with the decrypted `LoadedStream`. + +- **`EventService` class** + + - Asynchronous publisher that emits telemetry (e.g., fetch metrics, playback events) to `hm://event-service/v1/events` via Mercury. + - Accessible through `session.event_service()` for consumers that need to forward custom events. + +- **`TokenProvider` class** + + - Caches Login5 access tokens per scope, refreshing them proactively as they near expiry. + - Used by `ApiClient` to supply the correct `Authorization` headers for Spotify Web API calls. + +- **`SearchManager` class** + + - High-level wrapper around `hm://searchview/km/v4/search/...` requests sent over Mercury. + - Fills in username, locale, and country defaults from the current session before dispatching the call. + +- **OAuth tokens for Spotify Web API** + + - Can be obtained via `session.tokens().get(scope)` + - Enable authenticated API calls for operations like search, playlist management, and user data access