mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-28 06:57:56 +02:00
Initial implementation for Spotify listens export
This commit is contained in:
parent
3ded679d80
commit
3d3685d8bc
11 changed files with 2355 additions and 3 deletions
168
backends/spotify/spotify.go
Normal file
168
backends/spotify/spotify.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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) 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 string) oauth2.Config {
|
||||
return oauth2.Config{
|
||||
ClientID: b.clientId,
|
||||
ClientSecret: b.clientSecret,
|
||||
Scopes: []string{"user-read-recently-played"},
|
||||
RedirectURL: redirectUrl,
|
||||
Endpoint: spotify.Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SpotifyApiBackend) OAuth2Setup(redirectUrl string, 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
|
||||
}
|
||||
|
||||
count := len(result.Items)
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
listens := make(models.ListensList, 0, len(result.Items))
|
||||
|
||||
// Set minTime to the newest returned listen
|
||||
after, err := strconv.ParseInt(result.Cursors.After, 10, 64)
|
||||
if err != nil && after <= minTime.Unix() {
|
||||
err = errors.New("new cursor timestamp did not progress")
|
||||
}
|
||||
if err != nil {
|
||||
progress <- p.Complete()
|
||||
results <- models.ListensResult{Error: err}
|
||||
return
|
||||
}
|
||||
minTime = time.Unix(after, 0)
|
||||
remainingTime := startTime.Sub(minTime)
|
||||
|
||||
for _, listen := range result.Items {
|
||||
listens = append(listens, listen.ToListen())
|
||||
}
|
||||
|
||||
sort.Sort(listens)
|
||||
p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds())
|
||||
progress <- p
|
||||
results <- models.ListensResult{Listens: listens, OldestTimestamp: oldestTimestamp}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue