Documentation
¶
Overview ¶
Package deezer is a Deezer client: ARL login, gw-light + public REST browse, and track -> CDN-url resolution. The ARL never leaves the machine beyond the requests it makes to Deezer.
Package deezer: Blowfish BF_CBC_STRIPE decryption for Deezer CDN streams.
Index ¶
- Constants
- Variables
- func BlowfishKey(trackID string) []byte
- func DecryptTrack(trackID string, data []byte) ([]byte, error)
- func DownloadTrack(plan *StreamPlan, w io.Writer) error
- func DownloadTrackContext(ctx context.Context, plan *StreamPlan, w io.Writer) error
- func FormatLabel(raw string) string
- func IsARLExpired(err error) bool
- func IsNoFullMedia(err error) bool
- func IsNoNetwork(err error) bool
- func IsPremiumRequired(err error) bool
- func IsPreviewOnly(err error) bool
- func TrackIDOf(uri string) string
- type Account
- type AdCadence
- type Album
- type Artist
- type ArtistInfo
- type ArtistPage
- type ArtworkFetcher
- type Chart
- type Client
- func (c *Client) Account() Account
- func (c *Client) AdBreakDue() bool
- func (c *Client) AdCadenceInfo() AdCadence
- func (c *Client) AddFavoriteTrack(trackID string) error
- func (c *Client) AddToPlaylist(playlistID, trackID string) error
- func (c *Client) AdsDisabled() bool
- func (c *Client) AlbumTracks(id string) ([]Track, error)
- func (c *Client) ArtistMix(artistID string) ([]Track, error)
- func (c *Client) ArtistProfile(id string) (*ArtistPage, error)
- func (c *Client) ArtistTop(id string) ([]Track, error)
- func (c *Client) Charts(genreID string) (*Chart, error)
- func (c *Client) CreatePlaylist(title string, trackIDs []string) (string, error)
- func (c *Client) DeletePlaylist(playlistID string) error
- func (c *Client) EpisodeMeta(id string) (Episode, error)
- func (c *Client) Favorites() ([]Track, error)
- func (c *Client) Flow() ([]Track, error)
- func (c *Client) FlowMore() ([]Track, error)
- func (c *Client) HighQuality() bool
- func (c *Client) LoggedIn() bool
- func (c *Client) Login() error
- func (c *Client) Lyrics(trackID string) (*Lyrics, error)
- func (c *Client) NowPlaying(trackID string) error
- func (c *Client) PlaylistTracks(id string) ([]Track, error)
- func (c *Client) Playlists() ([]Playlist, error)
- func (c *Client) PodcastEpisodeStream(episodeID string) (*StreamPlan, error)
- func (c *Client) PodcastEpisodes(podcastID string) ([]Episode, error)
- func (c *Client) PrepareStream(trackID string) (*StreamPlan, error)
- func (c *Client) PrepareStreamCached(trackID string, cache *mediacache.Cache) (*StreamPlan, error)
- func (c *Client) Quality() int
- func (c *Client) RemoveFavoriteTrack(trackID string) error
- func (c *Client) RemoveFromPlaylist(playlistID, trackID string) error
- func (c *Client) RenamePlaylist(playlistID, title string) error
- func (c *Client) SaveAlbum(ctx context.Context, albumID, dir string, opts DownloadOptions) ([]string, error)
- func (c *Client) SavePlaylist(ctx context.Context, playlistID, dir string, opts DownloadOptions) ([]string, error)
- func (c *Client) SaveTrack(ctx context.Context, trackID, dir string) (string, error)
- func (c *Client) Search(query string) (*SearchResults, error)
- func (c *Client) SearchPodcasts(query string) ([]Podcast, error)
- func (c *Client) SearchTracks(artist, title string) ([]Track, error)
- func (c *Client) SetAdsDisabled(disabled bool)
- func (c *Client) SetHighQuality(high bool)
- func (c *Client) SetQuality(q int)
- func (c *Client) Track(id string) (Track, error)
- func (c *Client) TrackByISRC(isrc string) (Track, error)
- func (c *Client) TrackMix(trackID string) ([]Track, error)
- func (c *Client) UserID() string
- type DownloadOptions
- type DownloadProgress
- type Episode
- type LyricLine
- type Lyrics
- type Playlist
- type Podcast
- type SearchResults
- type StreamPlan
- type StripeDecryptor
- type Track
Constants ¶
const ( QualityNormal = 0 // MP3 128 QualityHigh = 1 // MP3 320 QualityLossless = 2 // FLAC (HiFi) — falls back to MP3 if unavailable )
Quality levels.
Variables ¶
var ErrARLExpired = errors.New("ARL expired or invalid — re-login required")
ErrARLExpired is returned by Login when the ARL cookie is missing/expired or otherwise rejected, so callers can distinguish "re-auth needed" from a transient network failure and prompt the user accordingly.
var ErrNoFullMedia = errors.New("no full-length media available for this account")
ErrNoFullMedia signals that resolveMediaURL obtained no full-length source because the account is not entitled (Free tier, geo, licensing, etc.). It is distinct from transient transport, 5xx, or rate-limit errors. PrepareStream falls back to the 30 s preview ONLY on ErrNoFullMedia (wrapped); any other error from resolve (after retries) is returned as-is so callers see the real failure instead of a silent clip downgrade.
var ErrNoNetwork = errors.New("no internet connection")
ErrNoNetwork wraps any transport-level failure reaching Deezer (DNS failure, connection refused, host/network unreachable, TLS/dial timeout, context deadline). Callers use errors.Is(err, ErrNoNetwork) to show a "No Internet" screen and offer a retry, instead of mistaking an outage for an expired ARL (ErrARLExpired) and forcing the user to re-authenticate. The two are mutually exclusive: a reachable Deezer that rejects the ARL yields ErrARLExpired; an unreachable Deezer yields ErrNoNetwork.
var ErrPartialDownload = errors.New("batch download had failures")
ErrPartialDownload is returned (wrapped) by SaveAlbum/SavePlaylist when one or more tracks could not be downloaded. The first error and counts are included in the message; successfully downloaded paths are still returned.
var ErrPremiumRequired = errors.New("downloads require a paid Deezer plan")
ErrPremiumRequired is returned by SaveTrack when the logged-in account is not entitled to full-track streaming (Deezer Free). Downloads save the full, decrypted track, which a free account cannot resolve — so the feature is gated to paid plans. Callers use errors.Is to show a "requires a paid plan" hint and hide the download action.
var ErrPreviewOnly = errors.New("full track not available for download")
ErrPreviewOnly is returned by SaveTrack when the only source Deezer offers for a track is the public 30-second preview (e.g. the track is geo-restricted or otherwise unavailable at full length for this account). We refuse to write a 30-second clip to disk under a full-track filename.
Functions ¶
func BlowfishKey ¶
BlowfishKey derives the per-track Blowfish key: key[i] = md5hex[i] ^ md5hex[i+16] ^ secret[i].
func DecryptTrack ¶
DecryptTrack decrypts a whole in-memory buffer (used by tests).
func DownloadTrack ¶
func DownloadTrack(plan *StreamPlan, w io.Writer) error
DownloadTrack fetches and decrypts a Deezer stream to w. plan must come from Client.PrepareStream (tracks) or Client.PodcastEpisodeStream (episodes, which are unencrypted). The bytes written form a valid MP3 or FLAC file.
func DownloadTrackContext ¶
DownloadTrackContext is DownloadTrack with cancellation. Cancelling ctx aborts the in-flight CDN read and unblocks the copy.
The implementation is resilient: it performs HTTP Range resume (with If-Range validator) on mid-body connection drops, a few retries + backoff, calls plan.Refresh on 403/410 to obtain a fresh URL (validating that the refreshed plan describes the identical stream), and verifies that the final received byte count matches the declared Content-Length (rejecting short files so callers can redownload).
func FormatLabel ¶
FormatLabel turns a raw Deezer media format into a human label for the UI.
func IsARLExpired ¶
IsARLExpired reports whether err is an expired/invalid-ARL auth failure.
func IsNoFullMedia ¶
IsNoFullMedia reports whether err indicates an entitlement rejection (no full track source) rather than a transient problem.
func IsNoNetwork ¶
IsNoNetwork reports whether err (or anything it wraps) is a transport-level connectivity failure. Exported for callers outside this package (the FFI bindings, the TUI) that classify a login/browse failure.
func IsPremiumRequired ¶
IsPremiumRequired reports whether err is the premium-required download gate.
func IsPreviewOnly ¶
IsPreviewOnly reports whether err is the preview-only download refusal.
Types ¶
type Account ¶
type Account struct {
UserID string `json:"userId"`
Name string `json:"name"`
Offer string `json:"offer"`
CanHQ bool `json:"canHq"` // entitled to MP3 320
CanHiFi bool `json:"canHifi"` // entitled to lossless FLAC
Premium bool `json:"premium"` // a paid plan that can stream on-demand
LoggedIn bool `json:"loggedIn"`
}
Account summarizes the logged-in user's plan and entitlements.
type AdCadence ¶
type AdCadence struct {
Enabled bool `json:"enabled"` // account is ad-supported (Deezer Free)
Start int `json:"start"` // first audio ad after this many tracks
Interval int `json:"interval"` // one audio ad every this many tracks after that
}
AdCadence is Deezer's free-tier audio-ad schedule, read from the gw deezer.adConfig method. On a paid (Premium/HiFi) account there are no audio ads, so Enabled is false.
type ArtistInfo ¶
ArtistInfo is an artist profile (search result / browse target).
type ArtistPage ¶
type ArtistPage struct {
Artist ArtistInfo
Top []Track
Albums []Album
Related []ArtistInfo
}
ArtistPage bundles an artist's profile with their top tracks, albums and related artists for a one-shot profile view.
type ArtworkFetcher ¶
ArtworkFetcher fetches bytes for a Deezer artwork URL (or upgraded size). Supply a no-op or fake for tests so no network calls are made.
type Chart ¶
type Chart struct {
Tracks []Track
Albums []Album
Artists []ArtistInfo
Playlists []Playlist
}
Chart is the global/genre top lists from the public REST /chart endpoint.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client holds an authenticated Deezer session.
func (*Client) AdBreakDue ¶
AdBreakDue reports whether an audio ad is scheduled before the current track, per the free-tier cadence (deezer.adConfig). The audio-ad itself is served by a third-party ad network, so OpenDeezer does not fetch or play it; this hook exposes the schedule for a future ad-audio integration and for the UI. Always false when ads are disabled or on a paid account.
func (*Client) AdCadenceInfo ¶
AdCadenceInfo returns the current audio-ad schedule (lazily fetching it once).
func (*Client) AddFavoriteTrack ¶
AddFavoriteTrack likes a track (adds it to Liked Songs).
func (*Client) AddToPlaylist ¶
AddToPlaylist appends a track to a playlist the user can edit.
func (*Client) AdsDisabled ¶
AdsDisabled reports whether play-reporting/ads are turned off.
func (*Client) AlbumTracks ¶
AlbumTracks lists an album's tracks via the public REST API, paging until the album is exhausted. A single page caps at 100 tracks, and albums (compilations, box sets) can exceed that; without pagination they would be silently truncated. Any page error fails the whole listing (SaveAlbum depends on a complete tracklist). Bounded by maxTracks like gwTrackAll.
func (*Client) ArtistMix ¶
ArtistMix returns Deezer's "smart radio" seeded from an artist ("artist radio"). Uses the gw smart.getSmartRadio method.
func (*Client) ArtistProfile ¶
func (c *Client) ArtistProfile(id string) (*ArtistPage, error)
ArtistProfile fetches an artist's profile plus top tracks, albums and related artists in one call (all public REST). A failure on any sub-list is tolerated so a partial profile still renders.
func (*Client) Charts ¶
Charts fetches the global top tracks/albums/artists/playlists from the public REST /chart endpoint (no auth). Pass genreID "0" for the global chart.
func (*Client) CreatePlaylist ¶
CreatePlaylist creates a new playlist (optionally seeded with tracks) and returns its id.
func (*Client) DeletePlaylist ¶
DeletePlaylist deletes a playlist the user owns.
func (*Client) EpisodeMeta ¶
EpisodeMeta fetches a single episode's metadata (title, podcast name, artwork) via the public REST /episode/{id} endpoint. Used to enrich now-playing after DZPlayEpisode (which only receives the id + duration from the caller).
func (*Client) Flow ¶
Flow returns Deezer's personalized "Flow" radio: an endless, taste-based track stream. Uses the gw radio.getUserRadio method (community convention); needs live validation against a real account.
Flow (and FlowMore) may be called repeatedly; each invocation performs a fresh radio.getUserRadio call and returns a new batch. The engine side is intentionally a finite batch fetch — the frontend is responsible for re-calling when its local playback queue is running low (on queue-tail) to "refill". No signature change; FlowMore is provided as a readable alias for the refill use case.
func (*Client) FlowMore ¶
FlowMore is a semantic alias for Flow. It exists so calling code can express "get the next batch for flow refill" without changing behavior or Flow's signature. The frontend should invoke this (or Flow) when its queue drains toward the tail.
func (*Client) HighQuality ¶
HighQuality reports whether the preference is at least MP3_320.
func (*Client) Lyrics ¶
Lyrics fetches a track's lyrics via gw song.getLyrics, returning both the plain text and, when Deezer provides them, time-synced lines.
func (*Client) NowPlaying ¶
NowPlaying tells Deezer a track has started (gw log.listen), mirroring the web player so plays are counted server-side — the free tier's ad accounting and artist play-counts both depend on it. It also advances the local ad-cadence counter. Fire-and-forget: the error is returned only for logging and must never block playback. A no-op when the user has disabled ads/reporting.
func (*Client) PlaylistTracks ¶
PlaylistTracks lists a playlist's tracks (gw, works for private playlists). A mid-fetch page error is tolerated (partial list, nil error) — acceptable for browse/UI. Batch downloads must use playlistTracksStrict instead.
func (*Client) Playlists ¶
Playlists lists the user's own playlists (gw pageProfile). It paginates using index/nb (mirrors the style of gw track pagination and AlbumTracks' index/limit) until a short page. The previous single-request nb=100 silently dropped playlists beyond the first page.
func (*Client) PodcastEpisodeStream ¶
func (c *Client) PodcastEpisodeStream(episodeID string) (*StreamPlan, error)
PodcastEpisodeStream resolves an episode to a plain (unencrypted) MP3 stream via gw episode.getData. The player decodes it directly (no Blowfish).
func (*Client) PodcastEpisodes ¶
PodcastEpisodes lists a show's episodes (public REST). It pages with index/limit (mirrors AlbumTracks) until a short page or absent "next", bounded by maxTracks. Without pagination only the first 100 episodes would be returned and the rest silently lost.
func (*Client) PrepareStream ¶
func (c *Client) PrepareStream(trackID string) (*StreamPlan, error)
PrepareStream resolves a track id to a playable stream. It first tries the full, entitled track (encrypted CDN URL). When full-track resolution is refused with an entitlement rejection (ErrNoFullMedia) it falls back to Deezer's public 30-second preview (plain MP3, .Preview==true) so free accounts can still play. Transient errors (5xx, transport, rate limits) from resolveMediaURL are retried inside the resolver and, if still failing, returned as-is — a premium user never gets a 30 s clip during a CDN hiccup.
func (*Client) PrepareStreamCached ¶
func (c *Client) PrepareStreamCached(trackID string, cache *mediacache.Cache) (*StreamPlan, error)
PrepareStreamCached returns a playable *StreamPlan sourced from cached metadata (mediacache.PutMeta/GetMeta/GetMetaForTrack) when available for the track. This lets callers (playback, future offline UI) obtain the necessary fields (format, Encrypted, GainDB, Preview) with zero network calls — no token or get_url round-trip.
If cache is non-nil and a meta entry exists for any format under the track ID, the returned plan has CDNURL=="" (body bytes must be served from the cache by the caller) and Refresh==nil. Otherwise it falls back to PrepareStream (which requires login and performs the network work).
The audio layer (when wired) should prefer this helper when a cache is attached:
plan, err := c.PrepareStreamCached(id, mc)
key := plan.TrackID + "." + plan.Format
if rc, sz, hit := mc.Get(key); hit { ... use cached ciphertext ... }
Duration is intentionally not part of StreamPlan (passed separately to Player.Play today) but can be carried in the meta for future use.
func (*Client) RemoveFavoriteTrack ¶
RemoveFavoriteTrack unlikes a track.
func (*Client) RemoveFromPlaylist ¶
RemoveFromPlaylist removes a track from a playlist.
func (*Client) RenamePlaylist ¶
RenamePlaylist changes a playlist's title.
func (*Client) SaveAlbum ¶
func (c *Client) SaveAlbum(ctx context.Context, albumID, dir string, opts DownloadOptions) ([]string, error)
SaveAlbum downloads every track belonging to the album (via AlbumTracks) to files inside dir. Downloads are sequential. It respects the same premium requirement as SaveTrack. Per-track errors are collected; an aggregate error wrapping ErrPartialDownload is returned if any track failed, while successfully saved paths are still returned. Cancelling ctx stops the batch between tracks and returns the paths saved so far alongside the ctx error.
func (*Client) SavePlaylist ¶
func (c *Client) SavePlaylist(ctx context.Context, playlistID, dir string, opts DownloadOptions) ([]string, error)
SavePlaylist downloads every track in the playlist (via PlaylistTracks) to files inside dir. Same semantics as SaveAlbum.
func (*Client) SaveTrack ¶
SaveTrack downloads trackID to a file inside dir, choosing the filename from the track's title/artist and the resolved format's extension, and returns the written path. Downloads are premium-only (ErrPremiumRequired); a track whose only source is the 30-second preview is refused (ErrPreviewOnly) rather than saved as a clip. A partial file is removed on failure. If dir is empty the current working directory is used.
After writing the audio, best-effort metadata tagging (ID3v2.4 or FLAC Vorbis+PICTURE) is performed using the track's ArtworkURL (upgraded size). Tagging failure is non-fatal: a warning is logged and the raw audio file is returned untouched.
func (*Client) Search ¶
func (c *Client) Search(query string) (*SearchResults, error)
Search queries tracks, albums, artists, and playlists concurrently. A failing sub-search (due to network or JSON parsing issues) is tolerated as long as at least one category succeeds. If all four categories fail, the underlying errors are joined and returned, so callers can tell "no matches" from "all requests failed".
func (*Client) SearchPodcasts ¶
SearchPodcasts finds shows via the public REST /search/podcast endpoint.
func (*Client) SearchTracks ¶
SearchTracks searches tracks only. With a non-empty artist it uses Deezer's advanced query syntax (artist:"..." track:"...") for a targeted match; otherwise it falls back to a plain track search on the title.
Live-observed quirk (2026-07): the advanced syntax is precise when it hits (artist:"eminem" track:"lose yourself" -> exact match) but returns zero results for some multi-word artist phrases (artist:"daft punk"), while a plain combined query finds those tracks fine. Callers that get an empty result from a targeted query should retry with artist=="" and the artist folded into the title (impex.ImportPlaylist's resolver does exactly that).
func (*Client) SetAdsDisabled ¶
SetAdsDisabled turns off play-reporting (gw log.listen) and the ad cadence.
This is an opt-out for FREE accounts only. Deezer's free tier is ad-supported: reporting each play (like the official web player) is what lets Deezer count listens — which credits artists and drives the ad schedule. Disabling it makes playback ad-free but stops reporting plays, which denies artists their play count and breaks Deezer's terms for the free tier. It is exposed only so the user can make that choice explicitly, at their own risk. Paid accounts are unaffected (they have no ads either way).
func (*Client) SetHighQuality ¶
SetHighQuality keeps the old bool API: true => MP3_320, false => MP3_128.
func (*Client) SetQuality ¶
SetQuality selects the preferred stream quality (0..2). Deezer returns the first format the account+track is entitled to, so an unentitled FLAC/320 automatically falls back to a lower MP3 format.
func (*Client) Track ¶
Track fetches a single track's metadata by id (gw song.getData). Used by the control API / MCP to "play track <id>".
func (*Client) TrackByISRC ¶
TrackByISRC resolves a track by its ISRC via the public REST API (/track/isrc:<code>). A code Deezer doesn't know yields an error (the REST error envelope surfaces through restGet), never an empty Track.
type DownloadOptions ¶
type DownloadOptions struct {
SkipExisting bool
// Progress is invoked after each track (success, skip, or error). Safe to be nil.
Progress func(DownloadProgress)
// ArtworkFetcher overrides cover art retrieval (for tests; nil = default Deezer CDN).
ArtworkFetcher ArtworkFetcher
// Concurrency is the maximum number of tracks to download in parallel
// inside SaveAlbum/SavePlaylist (bounded worker pool). <=0 selects the
// default (3). Adding the field keeps all prior call sites compatible.
Concurrency int
// contains filtered or unexported fields
}
DownloadOptions tunes batch downloads. All fields are optional.
type DownloadProgress ¶
type DownloadProgress struct {
Index int // 1-based index in the batch
Total int
TrackID string
Title string
Err error // per-track error (nil on success or skip)
}
DownloadProgress reports status for one track in a batch (SaveAlbum / SavePlaylist). It is delivered to the optional Progress callback even for skipped or errored tracks.
type Episode ¶
type Episode struct {
ID string
Title string
Description string
ArtworkURL string
DurationMS int64
ReleaseDate string
PodcastName string
}
Episode is one podcast episode. Episodes stream as plain (unencrypted) MP3.
type SearchResults ¶
type SearchResults struct {
Tracks []Track
Albums []Album
Artists []ArtistInfo
Playlists []Playlist
}
SearchResults groups the searched entity kinds.
type StreamPlan ¶
type StreamPlan struct {
CDNURL string
TrackID string
Format string
GainDB float64 // track ReplayGain in dB (0 if unknown)
Encrypted bool // false for plain CDN streams (e.g. podcast episodes)
Preview bool // true when this is Deezer's public 30-second preview
// Refresh re-resolves this plan's CDN URL when the current one expires
// mid-stream (the CDN answers 403/410 because the signed media URL's token
// rotated). Optional; populated by Client.PrepareStream so the audio engine
// can recover a long download without depending on a *deezer.Client. Returns
// a fresh plan for the same track. nil when re-resolution isn't wired up
// (podcasts, SDK callers that build a StreamPlan directly). Excluded from
// JSON since a func can't be serialized.
Refresh func() (*StreamPlan, error) `json:"-"`
}
StreamPlan is the resolved CDN URL + track id for decryption.
type StripeDecryptor ¶
type StripeDecryptor struct {
// contains filtered or unexported fields
}
StripeDecryptor streams BF_CBC_STRIPE plaintext out of ciphertext fed in arbitrary-sized pieces. Mirrors C++ DeezerStripeDecryptor.
func NewStripeDecryptor ¶
func NewStripeDecryptor(trackID string) (*StripeDecryptor, error)
NewStripeDecryptor builds a decryptor keyed for trackID.
func (*StripeDecryptor) Consumed ¶
func (d *StripeDecryptor) Consumed() int64
Consumed reports how many ciphertext bytes have been fed so far (full chunks already emitted plus the partial chunk still buffered). The stripe cipher is byte-for-byte 1:1, so this is exactly the offset to resume a dropped CDN download from via an HTTP Range request: re-issuing the GET with `Range: bytes=<Consumed()>-` and continuing to Feed the same decryptor yields output identical to an uninterrupted download.
func (*StripeDecryptor) Feed ¶
func (d *StripeDecryptor) Feed(data []byte, out []byte) []byte
Feed appends decrypted/passthrough output for n input bytes.
func (*StripeDecryptor) Finish ¶
func (d *StripeDecryptor) Finish(out []byte) []byte
Finish flushes the trailing partial chunk (always plaintext).
type Track ¶
type Track struct {
ID string
Name string
DurationMS int64
Artists []Artist
AlbumName string
ArtworkURL string
Explicit bool // explicit lyrics/content (show an "E" badge)
// TrackNumber is the 1-based position of the track within its album, when
// known (from the album-tracks listing's track_position). Zero means
// unknown/not applicable (single-track lookups, playlist context) and the
// download tagger skips the ID3 TRCK / FLAC TRACKNUMBER field.
TrackNumber int
// Year is the release year as a 4-digit string ("2021"), parsed from the
// track/album release_date when present. Empty means unknown and the
// tagger skips the ID3 TYER / FLAC DATE field.
Year string
}
Track mirrors the C++ core::Track.
func (Track) ArtistLine ¶
ArtistLine joins artist names: "Artist A, Artist B".