/* Copyright © 2023 Philipp Wolfer This file is part of Scotty. 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 spotify import ( "net/url" "sort" "strconv" "time" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/models" "golang.org/x/oauth2" "golang.org/x/oauth2/spotify" ) type SpotifyApiBackend struct { client Client clientId string clientSecret string } func (b *SpotifyApiBackend) Name() string { return "spotify" } func (b *SpotifyApiBackend) FromConfig(config *viper.Viper) models.Backend { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") return b } func (b *SpotifyApiBackend) OAuth2Config(redirectUrl *url.URL) oauth2.Config { return oauth2.Config{ ClientID: b.clientId, ClientSecret: b.clientSecret, Scopes: []string{ "user-read-currently-playing", "user-read-recently-played", "user-library-read", "user-library-modify", }, RedirectURL: redirectUrl.String(), Endpoint: spotify.Endpoint, } } func (b *SpotifyApiBackend) OAuth2Setup(redirectUrl *url.URL, token *oauth2.Token) error { config := b.OAuth2Config(redirectUrl) b.client = NewClient(config, token) return nil } func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { startTime := time.Now() minTime := oldestTimestamp totalDuration := startTime.Sub(oldestTimestamp) defer close(results) defer close(progress) p := models.Progress{Total: int64(totalDuration.Seconds())} for { result, err := b.client.RecentlyPlayedAfter(minTime, MaxItemsPerGet) if err != nil { progress <- p.Complete() results <- models.ListensResult{Error: err} return } if result.Cursors.After == "" { break } // Set minTime to the newest returned listen after, err := strconv.ParseInt(result.Cursors.After, 10, 64) if err != nil { progress <- p.Complete() results <- models.ListensResult{Error: err} return } else if after <= minTime.Unix() { // new cursor timestamp did not progress break } minTime = time.Unix(after, 0) remainingTime := startTime.Sub(minTime) count := len(result.Items) if count == 0 { break } listens := make(models.ListensList, 0, len(result.Items)) for _, listen := range result.Items { l := listen.ToListen() if l.ListenedAt.Unix() > oldestTimestamp.Unix() { listens = append(listens, l) } else { // result contains listens older then oldestTimestamp, // we can stop requesting more break } } sort.Sort(listens) p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds()) progress <- p results <- models.ListensResult{Listens: listens, OldestTimestamp: minTime} } results <- models.ListensResult{OldestTimestamp: minTime} progress <- p.Complete() } func (l Listen) ToListen() models.Listen { track := l.Track listen := models.Listen{ Track: models.Track{ TrackName: track.Name, ReleaseName: track.Album.Name, ArtistNames: make([]string, 0, len(track.Artists)), Duration: time.Duration(track.DurationMs * int(time.Millisecond)), TrackNumber: track.TrackNumber, Isrc: track.ExternalIds.ISRC, AdditionalInfo: map[string]any{}, }, } listen.ListenedAt, _ = time.Parse(time.RFC3339, l.PlayedAt) for _, artist := range track.Artists { listen.Track.ArtistNames = append(listen.Track.ArtistNames, artist.Name) } info := listen.AdditionalInfo if !l.Track.IsLocal { info["music_service"] = "spotify.com" } if track.ExternalUrls.Spotify != "" { info["origin_url"] = track.ExternalUrls.Spotify info["spotify_id"] = track.ExternalUrls.Spotify } if track.Album.ExternalUrls.Spotify != "" { info["spotify_album_id"] = track.Album.ExternalUrls.Spotify } if len(track.Artists) > 0 { info["spotify_artist_ids"] = extractArtistIds(track.Artists) } if len(track.Album.Artists) > 0 { info["spotify_album_artist_ids"] = extractArtistIds(track.Album.Artists) } return listen } func extractArtistIds(artists []Artist) []string { artistIds := make([]string, len(artists)) for i, artist := range artists { artistIds[i] = artist.ExternalUrls.Spotify } return artistIds }