mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
387 lines
9.6 KiB
Go
387 lines
9.6 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
func (c *DeezerClient) GetAlbum(ctx context.Context, albumID string) (*AlbumResponsePayload, error) {
|
|
c.cacheMu.RLock()
|
|
if entry, ok := c.albumCache[albumID]; ok && !entry.isExpired() {
|
|
c.cacheMu.RUnlock()
|
|
return entry.data.(*AlbumResponsePayload), nil
|
|
}
|
|
c.cacheMu.RUnlock()
|
|
|
|
albumURL := fmt.Sprintf(deezerAlbumURL, albumID)
|
|
|
|
var album deezerAlbumFull
|
|
if err := c.getJSON(ctx, albumURL, &album); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
albumImage := c.getBestAlbumImage(album)
|
|
artistName := album.Artist.Name
|
|
if len(album.Contributors) > 0 {
|
|
names := make([]string, len(album.Contributors))
|
|
for i, a := range album.Contributors {
|
|
names[i] = a.Name
|
|
}
|
|
artistName = strings.Join(names, ", ")
|
|
}
|
|
|
|
var genres []string
|
|
for _, g := range album.Genres.Data {
|
|
if g.Name != "" {
|
|
genres = append(genres, g.Name)
|
|
}
|
|
}
|
|
genreStr := strings.Join(genres, ", ")
|
|
|
|
info := AlbumInfoMetadata{
|
|
TotalTracks: album.NbTracks,
|
|
Name: album.Title,
|
|
ReleaseDate: album.ReleaseDate,
|
|
Artists: artistName,
|
|
ArtistId: fmt.Sprintf("deezer:%d", album.Artist.ID),
|
|
Images: albumImage,
|
|
Genre: genreStr,
|
|
Label: album.Label,
|
|
}
|
|
|
|
allTracks := album.Tracks.Data
|
|
|
|
if album.NbTracks > len(allTracks) {
|
|
GoLog("[Deezer] Album has %d tracks but only got %d, fetching remaining...", album.NbTracks, len(allTracks))
|
|
|
|
tracksURL := fmt.Sprintf("%s/tracks?limit=100&index=%d", fmt.Sprintf(deezerAlbumURL, albumID), len(allTracks))
|
|
|
|
for len(allTracks) < album.NbTracks {
|
|
var tracksResp struct {
|
|
Data []deezerTrack `json:"data"`
|
|
Next string `json:"next"`
|
|
}
|
|
|
|
if err := c.getJSON(ctx, tracksURL, &tracksResp); err != nil {
|
|
GoLog("[Deezer] Warning: failed to fetch album tracks page: %v", err)
|
|
break
|
|
}
|
|
|
|
if len(tracksResp.Data) == 0 {
|
|
break
|
|
}
|
|
|
|
allTracks = append(allTracks, tracksResp.Data...)
|
|
|
|
if tracksResp.Next == "" {
|
|
break
|
|
}
|
|
tracksURL = tracksResp.Next
|
|
}
|
|
|
|
GoLog("[Deezer] Fetched total %d tracks for album", len(allTracks))
|
|
}
|
|
|
|
isrcMap := c.fetchISRCsParallel(ctx, allTracks)
|
|
totalDiscs := 0
|
|
for _, track := range allTracks {
|
|
if track.DiskNumber > totalDiscs {
|
|
totalDiscs = track.DiskNumber
|
|
}
|
|
}
|
|
|
|
tracks := make([]AlbumTrackMetadata, 0, len(allTracks))
|
|
albumType := album.RecordType
|
|
if albumType == "compile" {
|
|
albumType = "compilation"
|
|
}
|
|
|
|
for i, track := range allTracks {
|
|
trackIDStr := fmt.Sprintf("%d", track.ID)
|
|
isrc := isrcMap[trackIDStr]
|
|
|
|
trackNum := track.TrackPosition
|
|
if trackNum == 0 {
|
|
trackNum = i + 1
|
|
}
|
|
|
|
tracks = append(tracks, AlbumTrackMetadata{
|
|
SpotifyID: fmt.Sprintf("deezer:%d", track.ID),
|
|
Artists: deezerTrackArtistDisplay(track),
|
|
Name: track.Title,
|
|
AlbumName: album.Title,
|
|
AlbumArtist: artistName,
|
|
DurationMS: track.Duration * 1000,
|
|
Images: albumImage,
|
|
ReleaseDate: album.ReleaseDate,
|
|
TrackNumber: trackNum,
|
|
TotalTracks: album.NbTracks,
|
|
DiscNumber: track.DiskNumber,
|
|
TotalDiscs: totalDiscs,
|
|
ExternalURL: track.Link,
|
|
ISRC: isrc,
|
|
AlbumID: fmt.Sprintf("deezer:%d", album.ID),
|
|
AlbumType: albumType,
|
|
Explicit: deezerTrackIsExplicit(track),
|
|
})
|
|
}
|
|
|
|
result := &AlbumResponsePayload{
|
|
AlbumInfo: info,
|
|
TrackList: tracks,
|
|
}
|
|
|
|
c.cacheMu.Lock()
|
|
now := time.Now()
|
|
c.albumCache[albumID] = &cacheEntry{
|
|
data: result,
|
|
expiresAt: now.Add(deezerCacheTTL),
|
|
}
|
|
c.maybeCleanupCachesLocked(now)
|
|
c.cacheMu.Unlock()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *DeezerClient) GetArtist(ctx context.Context, artistID string) (*ArtistResponsePayload, error) {
|
|
c.cacheMu.RLock()
|
|
if entry, ok := c.artistCache[artistID]; ok && !entry.isExpired() {
|
|
c.cacheMu.RUnlock()
|
|
return entry.data.(*ArtistResponsePayload), nil
|
|
}
|
|
c.cacheMu.RUnlock()
|
|
|
|
artistURL := fmt.Sprintf(deezerArtistURL, artistID)
|
|
var artist deezerArtistFull
|
|
if err := c.getJSON(ctx, artistURL, &artist); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
artistInfo := ArtistInfoMetadata{
|
|
ID: fmt.Sprintf("deezer:%d", artist.ID),
|
|
Name: artist.Name,
|
|
Images: c.getBestArtistImageFull(artist),
|
|
Followers: artist.NbFan,
|
|
Popularity: 0,
|
|
}
|
|
|
|
albumsURL := fmt.Sprintf("%s/albums?limit=100", fmt.Sprintf(deezerArtistURL, artistID))
|
|
var albumsResp struct {
|
|
Data []struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
ReleaseDate string `json:"release_date"`
|
|
NbTracks int `json:"nb_tracks"`
|
|
Cover string `json:"cover"`
|
|
CoverMedium string `json:"cover_medium"`
|
|
CoverBig string `json:"cover_big"`
|
|
CoverXL string `json:"cover_xl"`
|
|
RecordType string `json:"record_type"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
albums := make([]ArtistAlbumMetadata, 0)
|
|
if err := c.getJSON(ctx, albumsURL, &albumsResp); err == nil {
|
|
for _, album := range albumsResp.Data {
|
|
albumType := album.RecordType
|
|
if albumType == "compile" {
|
|
albumType = "compilation"
|
|
}
|
|
|
|
coverURL := album.CoverXL
|
|
if coverURL == "" {
|
|
coverURL = album.CoverBig
|
|
}
|
|
if coverURL == "" {
|
|
coverURL = album.CoverMedium
|
|
}
|
|
if coverURL == "" {
|
|
coverURL = album.Cover
|
|
}
|
|
|
|
albums = append(albums, ArtistAlbumMetadata{
|
|
ID: fmt.Sprintf("deezer:%d", album.ID),
|
|
Name: album.Title,
|
|
ReleaseDate: album.ReleaseDate,
|
|
TotalTracks: album.NbTracks,
|
|
Images: coverURL,
|
|
AlbumType: albumType,
|
|
Artists: artist.Name,
|
|
})
|
|
}
|
|
|
|
// The Deezer /artist/{id}/albums endpoint does not return nb_tracks.
|
|
// Fetch track counts in parallel from individual /album/{id} endpoints.
|
|
c.fetchAlbumTrackCounts(ctx, albums)
|
|
}
|
|
|
|
result := &ArtistResponsePayload{
|
|
ArtistInfo: artistInfo,
|
|
Albums: albums,
|
|
}
|
|
|
|
c.cacheMu.Lock()
|
|
now := time.Now()
|
|
c.artistCache[artistID] = &cacheEntry{
|
|
data: result,
|
|
expiresAt: now.Add(deezerCacheTTL),
|
|
}
|
|
c.maybeCleanupCachesLocked(now)
|
|
c.cacheMu.Unlock()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// fetchAlbumTrackCounts fetches nb_tracks for each album in parallel using
|
|
// individual /album/{id} calls, since the /artist/{id}/albums endpoint does
|
|
// not include this field. Albums whose track count is already known (non-zero)
|
|
// are skipped.
|
|
func (c *DeezerClient) fetchAlbumTrackCounts(ctx context.Context, albums []ArtistAlbumMetadata) {
|
|
type indexedID struct {
|
|
idx int
|
|
albumID string
|
|
}
|
|
var toFetch []indexedID
|
|
for i, a := range albums {
|
|
if a.TotalTracks == 0 {
|
|
rawID := strings.TrimPrefix(a.ID, "deezer:")
|
|
if rawID != "" {
|
|
toFetch = append(toFetch, indexedID{idx: i, albumID: rawID})
|
|
}
|
|
}
|
|
}
|
|
if len(toFetch) == 0 {
|
|
return
|
|
}
|
|
|
|
const maxParallel = 10
|
|
sem := make(chan struct{}, maxParallel)
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
|
|
for _, item := range toFetch {
|
|
wg.Add(1)
|
|
go func(it indexedID) {
|
|
defer wg.Done()
|
|
|
|
select {
|
|
case sem <- struct{}{}:
|
|
defer func() { <-sem }()
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
|
|
albumURL := fmt.Sprintf(deezerAlbumURL, it.albumID)
|
|
var resp struct {
|
|
NbTracks int `json:"nb_tracks"`
|
|
}
|
|
if err := c.getJSON(ctx, albumURL, &resp); err != nil {
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
albums[it.idx].TotalTracks = resp.NbTracks
|
|
mu.Unlock()
|
|
}(item)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func (c *DeezerClient) GetPlaylist(ctx context.Context, playlistID string) (*PlaylistResponsePayload, error) {
|
|
playlistURL := fmt.Sprintf(deezerPlaylistURL, playlistID)
|
|
|
|
var playlist deezerPlaylistFull
|
|
if err := c.getJSON(ctx, playlistURL, &playlist); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
playlistImage := playlist.PictureXL
|
|
if playlistImage == "" {
|
|
playlistImage = playlist.PictureBig
|
|
}
|
|
if playlistImage == "" {
|
|
playlistImage = playlist.PictureMedium
|
|
}
|
|
|
|
var info PlaylistInfoMetadata
|
|
info.Tracks.Total = playlist.NbTracks
|
|
info.Owner.DisplayName = playlist.Creator.Name
|
|
info.Owner.Name = playlist.Title
|
|
info.Owner.Images = playlistImage
|
|
|
|
allTracks := playlist.Tracks.Data
|
|
|
|
if playlist.NbTracks > len(allTracks) {
|
|
GoLog("[Deezer] Playlist has %d tracks but only got %d, fetching remaining...", playlist.NbTracks, len(allTracks))
|
|
|
|
tracksURL := fmt.Sprintf("%s/tracks?limit=100&index=%d", fmt.Sprintf(deezerPlaylistURL, playlistID), len(allTracks))
|
|
|
|
for len(allTracks) < playlist.NbTracks {
|
|
var tracksResp struct {
|
|
Data []deezerTrack `json:"data"`
|
|
Next string `json:"next"`
|
|
}
|
|
|
|
if err := c.getJSON(ctx, tracksURL, &tracksResp); err != nil {
|
|
GoLog("[Deezer] Warning: failed to fetch playlist tracks page: %v", err)
|
|
break
|
|
}
|
|
|
|
if len(tracksResp.Data) == 0 {
|
|
break
|
|
}
|
|
|
|
allTracks = append(allTracks, tracksResp.Data...)
|
|
|
|
if tracksResp.Next == "" {
|
|
break
|
|
}
|
|
tracksURL = tracksResp.Next
|
|
}
|
|
|
|
GoLog("[Deezer] Fetched total %d tracks for playlist", len(allTracks))
|
|
}
|
|
|
|
isrcMap := c.fetchISRCsParallel(ctx, allTracks)
|
|
|
|
tracks := make([]AlbumTrackMetadata, 0, len(allTracks))
|
|
for _, track := range allTracks {
|
|
albumImage := track.Album.CoverXL
|
|
if albumImage == "" {
|
|
albumImage = track.Album.CoverBig
|
|
}
|
|
if albumImage == "" {
|
|
albumImage = track.Album.CoverMedium
|
|
}
|
|
|
|
trackIDStr := fmt.Sprintf("%d", track.ID)
|
|
isrc := isrcMap[trackIDStr]
|
|
|
|
tracks = append(tracks, AlbumTrackMetadata{
|
|
SpotifyID: fmt.Sprintf("deezer:%d", track.ID),
|
|
Artists: deezerTrackArtistDisplay(track),
|
|
Name: track.Title,
|
|
AlbumName: track.Album.Title,
|
|
AlbumArtist: track.Artist.Name,
|
|
DurationMS: track.Duration * 1000,
|
|
Images: albumImage,
|
|
ReleaseDate: "",
|
|
TrackNumber: track.TrackPosition,
|
|
DiscNumber: track.DiskNumber,
|
|
ExternalURL: track.Link,
|
|
ISRC: isrc,
|
|
AlbumID: fmt.Sprintf("deezer:%d", track.Album.ID),
|
|
Explicit: deezerTrackIsExplicit(track),
|
|
})
|
|
}
|
|
|
|
return &PlaylistResponsePayload{
|
|
PlaylistInfo: info,
|
|
TrackList: tracks,
|
|
}, nil
|
|
}
|