deezer

package
v3.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

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

View Source
const (
	QualityNormal   = 0 // MP3 128
	QualityHigh     = 1 // MP3 320
	QualityLossless = 2 // FLAC (HiFi) — falls back to MP3 if unavailable
)

Quality levels.

Variables

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func BlowfishKey(trackID string) []byte

BlowfishKey derives the per-track Blowfish key: key[i] = md5hex[i] ^ md5hex[i+16] ^ secret[i].

func DecryptTrack

func DecryptTrack(trackID string, data []byte) ([]byte, error)

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

func DownloadTrackContext(ctx context.Context, plan *StreamPlan, w io.Writer) error

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

func FormatLabel(raw string) string

FormatLabel turns a raw Deezer media format into a human label for the UI.

func IsARLExpired

func IsARLExpired(err error) bool

IsARLExpired reports whether err is an expired/invalid-ARL auth failure.

func IsNoFullMedia

func IsNoFullMedia(err error) bool

IsNoFullMedia reports whether err indicates an entitlement rejection (no full track source) rather than a transient problem.

func IsNoNetwork

func IsNoNetwork(err error) bool

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

func IsPremiumRequired(err error) bool

IsPremiumRequired reports whether err is the premium-required download gate.

func IsPreviewOnly

func IsPreviewOnly(err error) bool

IsPreviewOnly reports whether err is the preview-only download refusal.

func TrackIDOf

func TrackIDOf(uri string) string

TrackIDOf extracts a numeric id from "deezer:track:123", a URL, or "123". Returns "" when no valid id is present.

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 Album

type Album struct {
	ID         string
	Name       string
	Artists    []Artist
	ArtworkURL string
}

Album is a search/browse result.

type Artist

type Artist struct {
	ID   string
	Name string
}

Artist is a track/album credit.

type ArtistInfo

type ArtistInfo struct {
	ID         string
	Name       string
	ArtworkURL string
	NbFans     int
}

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

type ArtworkFetcher func(ctx context.Context, artworkURL string) ([]byte, error)

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 New

func New(arl string) *Client

New builds a client for the given ARL (not yet logged in).

func (*Client) Account

func (c *Client) Account() Account

Account returns the logged-in user's plan + entitlement summary.

func (*Client) AdBreakDue

func (c *Client) AdBreakDue() bool

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

func (c *Client) AdCadenceInfo() AdCadence

AdCadenceInfo returns the current audio-ad schedule (lazily fetching it once).

func (*Client) AddFavoriteTrack

func (c *Client) AddFavoriteTrack(trackID string) error

AddFavoriteTrack likes a track (adds it to Liked Songs).

func (*Client) AddToPlaylist

func (c *Client) AddToPlaylist(playlistID, trackID string) error

AddToPlaylist appends a track to a playlist the user can edit.

func (*Client) AdsDisabled

func (c *Client) AdsDisabled() bool

AdsDisabled reports whether play-reporting/ads are turned off.

func (*Client) AlbumTracks

func (c *Client) AlbumTracks(id string) ([]Track, error)

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

func (c *Client) ArtistMix(artistID string) ([]Track, error)

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) ArtistTop

func (c *Client) ArtistTop(id string) ([]Track, error)

ArtistTop lists an artist's most popular tracks (public REST).

func (*Client) Charts

func (c *Client) Charts(genreID string) (*Chart, error)

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

func (c *Client) CreatePlaylist(title string, trackIDs []string) (string, error)

CreatePlaylist creates a new playlist (optionally seeded with tracks) and returns its id.

func (*Client) DeletePlaylist

func (c *Client) DeletePlaylist(playlistID string) error

DeletePlaylist deletes a playlist the user owns.

func (*Client) EpisodeMeta

func (c *Client) EpisodeMeta(id string) (Episode, error)

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) Favorites

func (c *Client) Favorites() ([]Track, error)

Favorites lists the user's liked songs (gw).

func (*Client) Flow

func (c *Client) Flow() ([]Track, error)

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

func (c *Client) FlowMore() ([]Track, error)

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

func (c *Client) HighQuality() bool

HighQuality reports whether the preference is at least MP3_320.

func (*Client) LoggedIn

func (c *Client) LoggedIn() bool

LoggedIn reports whether Login succeeded.

func (*Client) Login

func (c *Client) Login() error

Login authenticates and fetches api_token + license_token + sid + user id.

func (*Client) Lyrics

func (c *Client) Lyrics(trackID string) (*Lyrics, error)

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

func (c *Client) NowPlaying(trackID string) error

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

func (c *Client) PlaylistTracks(id string) ([]Track, error)

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

func (c *Client) Playlists() ([]Playlist, error)

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

func (c *Client) PodcastEpisodes(podcastID string) ([]Episode, error)

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) Quality

func (c *Client) Quality() int

Quality returns the current preference (0..2).

func (*Client) RemoveFavoriteTrack

func (c *Client) RemoveFavoriteTrack(trackID string) error

RemoveFavoriteTrack unlikes a track.

func (*Client) RemoveFromPlaylist

func (c *Client) RemoveFromPlaylist(playlistID, trackID string) error

RemoveFromPlaylist removes a track from a playlist.

func (*Client) RenamePlaylist

func (c *Client) RenamePlaylist(playlistID, title string) error

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

func (c *Client) SaveTrack(ctx context.Context, trackID, dir string) (string, error)

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

func (c *Client) SearchPodcasts(query string) ([]Podcast, error)

SearchPodcasts finds shows via the public REST /search/podcast endpoint.

func (*Client) SearchTracks

func (c *Client) SearchTracks(artist, title string) ([]Track, error)

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

func (c *Client) SetAdsDisabled(disabled bool)

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

func (c *Client) SetHighQuality(high bool)

SetHighQuality keeps the old bool API: true => MP3_320, false => MP3_128.

func (*Client) SetQuality

func (c *Client) SetQuality(q int)

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

func (c *Client) Track(id string) (Track, error)

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

func (c *Client) TrackByISRC(isrc string) (Track, error)

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.

func (*Client) TrackMix

func (c *Client) TrackMix(trackID string) ([]Track, error)

TrackMix returns a mix seeded from one track ("song radio"). Uses the gw song.getSearchTrackMix method; start_with_input_track keeps the seed track as the first entry so playback can begin from the song the user picked.

func (*Client) UserID

func (c *Client) UserID() string

UserID returns the numeric Deezer user id (after Login).

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.

func (Episode) AsTrack

func (e Episode) AsTrack() Track

AsTrack adapts an episode to a Track so the existing queue/player/now-playing paths can carry it (episodes play via a plain StreamPlan).

type LyricLine

type LyricLine struct {
	TimeMS int64
	Text   string
}

LyricLine is one synced lyric line (TimeMS is the line's start offset).

type Lyrics

type Lyrics struct {
	Plain  string
	Synced []LyricLine
}

Lyrics holds a track's plain text plus, when available, time-synced lines.

func (Lyrics) IsSynced

func (l Lyrics) IsSynced() bool

IsSynced reports whether time-synced lyrics are present.

type Playlist

type Playlist struct {
	ID         string
	Name       string
	Owner      string
	TrackCount int
	ArtworkURL string
}

Playlist is a search/browse result.

type Podcast

type Podcast struct {
	ID           string
	Name         string
	Description  string
	ArtworkURL   string
	EpisodeCount int
}

Podcast is a show (search/browse result).

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

func (t Track) ArtistLine() string

ArtistLine joins artist names: "Artist A, Artist B".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL