/* Copyright © 2023 Philipp Wolfer Scotty is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Scotty is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Scotty. If not, see . */ package deezer import ( "fmt" "math" "net/url" "sort" "time" "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" ) type DeezerApiBackend struct { client Client clientId string clientSecret string } func (b *DeezerApiBackend) Name() string { return "deezer" } func (b *DeezerApiBackend) Options() *[]models.BackendOption { return &[]models.BackendOption{{ Name: "client-id", Label: "Client ID", Type: models.String, }, { Name: "client-secret", Label: "Client secret", Type: models.Secret, }} } func (b *DeezerApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") return b } func (b *DeezerApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { conf := oauth2.Config{ ClientID: b.clientId, ClientSecret: b.clientSecret, Scopes: []string{ "offline_access,basic_access,listening_history", }, RedirectURL: redirectUrl.String(), Endpoint: oauth2.Endpoint{ AuthURL: "https://connect.deezer.com/oauth/auth.php", TokenURL: "https://connect.deezer.com/oauth/access_token.php", }, } return deezerStrategy{conf: conf} } func (b *DeezerApiBackend) OAuth2Setup(token oauth2.TokenSource) error { b.client = NewClient(token) return nil } func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 perPage := MaxItemsPerGet defer close(results) p := models.Progress{Total: int64(perPage)} var totalCount int out: for { result, err := b.client.UserHistory(offset, perPage) if err != nil { progress <- p.Complete() results <- models.ListensResult{Error: err} return } // The offset was higher then the actual number of tracks. Adjust the offset // and continue. if offset >= result.Total { p.Total = int64(result.Total) totalCount = result.Total offset = result.Total - perPage if offset < 0 { offset = 0 } continue } count := len(result.Tracks) if count == 0 { break out } listens := make(models.ListensList, 0, perPage) for _, track := range result.Tracks { listen := track.AsListen() if listen.ListenedAt.Unix() > oldestTimestamp.Unix() { listens = append(listens, listen) } else { totalCount -= 1 break } } sort.Sort(listens) results <- models.ListensResult{Items: listens, Total: totalCount} p.Elapsed += int64(count) progress <- p if offset <= 0 { // This was the last request, no further results break out } offset -= perPage if offset < 0 { offset = 0 } } progress <- p.Complete() } func (b *DeezerApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 perPage := MaxItemsPerGet defer close(results) p := models.Progress{Total: int64(perPage)} var totalCount int out: for { result, err := b.client.UserTracks(offset, perPage) if err != nil { progress <- p.Complete() results <- models.LovesResult{Error: err} return } // The offset was higher then the actual number of tracks. Adjust the offset // and continue. if offset >= result.Total { p.Total = int64(result.Total) totalCount = result.Total offset = result.Total - perPage if offset < 0 { offset = 0 } continue } count := len(result.Tracks) if count == 0 { break out } loves := make(models.LovesList, 0, perPage) for _, track := range result.Tracks { love := track.AsLove() if love.Created.Unix() > oldestTimestamp.Unix() { loves = append(loves, love) } else { totalCount -= 1 break } } sort.Sort(loves) results <- models.LovesResult{Items: loves, Total: totalCount} p.Elapsed += int64(count) progress <- p if offset <= 0 { // This was the last request, no further results break out } offset -= perPage if offset < 0 { offset = 0 } } progress <- p.Complete() } func (t Listen) AsListen() models.Listen { love := models.Listen{ ListenedAt: time.Unix(t.Timestamp, 0), Track: t.Track.AsTrack(), } return love } func (t LovedTrack) AsLove() models.Love { love := models.Love{ Created: time.Unix(t.AddedAt, 0), Track: t.Track.AsTrack(), } return love } func (t Track) AsTrack() models.Track { track := models.Track{ TrackName: t.Title, ReleaseName: t.Album.Title, ArtistNames: []string{t.Artist.Name}, Duration: time.Duration(t.Duration * int(time.Second)), AdditionalInfo: map[string]any{}, } info := track.AdditionalInfo info["music_service"] = "deezer.com" info["origin_url"] = t.Link info["deezer_id"] = t.Link info["deezer_album_id"] = fmt.Sprintf("https://www.deezer.com/track/%v", t.Album.Id) info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/track/%v", t.Artist.Id) return track }