mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-16 10:09:28 +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
|
@ -34,6 +34,7 @@ import (
|
||||||
"go.uploadedlobster.com/scotty/backends/listenbrainz"
|
"go.uploadedlobster.com/scotty/backends/listenbrainz"
|
||||||
"go.uploadedlobster.com/scotty/backends/maloja"
|
"go.uploadedlobster.com/scotty/backends/maloja"
|
||||||
"go.uploadedlobster.com/scotty/backends/scrobblerlog"
|
"go.uploadedlobster.com/scotty/backends/scrobblerlog"
|
||||||
|
"go.uploadedlobster.com/scotty/backends/spotify"
|
||||||
"go.uploadedlobster.com/scotty/backends/subsonic"
|
"go.uploadedlobster.com/scotty/backends/subsonic"
|
||||||
"go.uploadedlobster.com/scotty/models"
|
"go.uploadedlobster.com/scotty/models"
|
||||||
)
|
)
|
||||||
|
@ -84,6 +85,7 @@ var knownBackends = map[string]func() models.Backend{
|
||||||
"listenbrainz": func() models.Backend { return &listenbrainz.ListenBrainzApiBackend{} },
|
"listenbrainz": func() models.Backend { return &listenbrainz.ListenBrainzApiBackend{} },
|
||||||
"maloja": func() models.Backend { return &maloja.MalojaApiBackend{} },
|
"maloja": func() models.Backend { return &maloja.MalojaApiBackend{} },
|
||||||
"scrobbler-log": func() models.Backend { return &scrobblerlog.ScrobblerLogBackend{} },
|
"scrobbler-log": func() models.Backend { return &scrobblerlog.ScrobblerLogBackend{} },
|
||||||
|
"spotify": func() models.Backend { return &spotify.SpotifyApiBackend{} },
|
||||||
"subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} },
|
"subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
80
backends/spotify/client.go
Normal file
80
backends/spotify/client.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseURL = "https://api.spotify.com/v1/"
|
||||||
|
const MaxItemsPerGet = 50
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
HttpClient *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(conf oauth2.Config, token *oauth2.Token) Client {
|
||||||
|
ctx := context.Background()
|
||||||
|
httpClient := conf.Client(ctx, token)
|
||||||
|
client := resty.NewWithClient(httpClient)
|
||||||
|
client.SetBaseURL(baseURL)
|
||||||
|
client.SetHeader("Accept", "application/json")
|
||||||
|
client.SetRetryCount(5)
|
||||||
|
return Client{
|
||||||
|
HttpClient: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) RecentlyPlayedAfter(after time.Time, limit int) (RecentlyPlayedResult, error) {
|
||||||
|
return c.recentlyPlayed(&after, nil, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) RecentlyPlayedBefore(before time.Time, limit int) (RecentlyPlayedResult, error) {
|
||||||
|
return c.recentlyPlayed(nil, &before, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) (result RecentlyPlayedResult, err error) {
|
||||||
|
const path = "/me/player/recently-played"
|
||||||
|
request := c.HttpClient.R().
|
||||||
|
SetQueryParam("limit", strconv.Itoa(limit)).
|
||||||
|
SetResult(&result)
|
||||||
|
if after != nil {
|
||||||
|
request.SetQueryParam("after", strconv.FormatInt(after.Unix(), 10))
|
||||||
|
} else if before != nil {
|
||||||
|
request.SetQueryParam("before", strconv.FormatInt(before.Unix(), 10))
|
||||||
|
|
||||||
|
}
|
||||||
|
response, err := request.Get(path)
|
||||||
|
|
||||||
|
if response.StatusCode() != 200 {
|
||||||
|
err = errors.New(response.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
37
backends/spotify/client_test.go
Normal file
37
backends/spotify/client_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uploadedlobster.com/scotty/backends/spotify"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewClient(t *testing.T) {
|
||||||
|
conf := oauth2.Config{}
|
||||||
|
token := &oauth2.Token{}
|
||||||
|
client := spotify.NewClient(conf, token)
|
||||||
|
assert.IsType(t, spotify.Client{}, client)
|
||||||
|
}
|
99
backends/spotify/models.go
Normal file
99
backends/spotify/models.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
type RecentlyPlayedResult struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Next string `json:"next"`
|
||||||
|
Cursors Cursors `json:"cursors"`
|
||||||
|
Items []Listen `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cursors struct {
|
||||||
|
After string `json:"after"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listen struct {
|
||||||
|
Track Track `json:"track"`
|
||||||
|
PlayedAt string `json:"played_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Track struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
DiscNumber int `json:"disc_number"`
|
||||||
|
TrackNumber int `json:"track_number"`
|
||||||
|
DurationMs int `json:"duration_ms"`
|
||||||
|
Explicit bool `json:"explicit"`
|
||||||
|
IsLocal bool `json:"is_local"`
|
||||||
|
Popularity int `json:"popularity"`
|
||||||
|
ExternalIds ExternalIds `json:"external_ids"`
|
||||||
|
ExternalUrls ExternalUrls `json:"external_urls"`
|
||||||
|
Album Album `json:"album"`
|
||||||
|
Artists []Artist `json:"artists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Album struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TotalTracks int `json:"total_tracks"`
|
||||||
|
ReleaseDate string `json:"release_date"`
|
||||||
|
ReleaseDatePrecision string `json:"release_date_precision"`
|
||||||
|
AlbumType string `json:"album_type"`
|
||||||
|
ExternalUrls ExternalUrls `json:"external_urls"`
|
||||||
|
Artists []Artist `json:"artists"`
|
||||||
|
Images []Image `json:"images"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artist struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ExternalUrls ExternalUrls `json:"external_urls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalIds struct {
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
|
EAN string `json:"ean"`
|
||||||
|
UPC string `json:"upc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalUrls struct {
|
||||||
|
Spotify string `json:"spotify"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
}
|
51
backends/spotify/models_test.go
Normal file
51
backends/spotify/models_test.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uploadedlobster.com/scotty/backends/spotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecentlyPlayedResult(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("testdata/recently-played.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := spotify.RecentlyPlayedResult{}
|
||||||
|
err = json.Unmarshal(data, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Equal(3, result.Limit)
|
||||||
|
assert.Equal("1700578807229", result.Cursors.After)
|
||||||
|
require.Len(t, result.Items, 3)
|
||||||
|
track1 := result.Items[0].Track
|
||||||
|
assert.Equal("Evidence", track1.Name)
|
||||||
|
assert.Equal("Viva Emptiness", track1.Album.Name)
|
||||||
|
require.Len(t, track1.Artists, 1)
|
||||||
|
assert.Equal("Katatonia", track1.Artists[0].Name)
|
||||||
|
}
|
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
|
||||||
|
}
|
58
backends/spotify/spotify_test.go
Normal file
58
backends/spotify/spotify_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uploadedlobster.com/scotty/backends/spotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpotifyListenToListen(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("testdata/listen.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
spListen := spotify.Listen{}
|
||||||
|
err = json.Unmarshal(data, &spListen)
|
||||||
|
require.NoError(t, err)
|
||||||
|
listen := spListen.ToListen()
|
||||||
|
listenedAt, _ := time.Parse(time.RFC3339, "2023-11-21T15:24:33.361Z")
|
||||||
|
assert.Equal(t, listenedAt, listen.ListenedAt)
|
||||||
|
assert.Equal(t, time.Duration(413826*time.Millisecond), listen.Duration)
|
||||||
|
assert.Equal(t, "Oweynagat", listen.TrackName)
|
||||||
|
assert.Equal(t, "Here Now, There Then", listen.ReleaseName)
|
||||||
|
assert.Equal(t, []string{"Dool"}, listen.ArtistNames)
|
||||||
|
assert.Equal(t, 5, listen.TrackNumber)
|
||||||
|
assert.Equal(t, "DES561620801", listen.Isrc)
|
||||||
|
info := listen.AdditionalInfo
|
||||||
|
assert.Equal(t, "spotify.com", info["music_service"])
|
||||||
|
assert.Equal(t, "https://open.spotify.com/track/2JKUgGuXK3dEvyuIJ4Yj2V", info["origin_url"])
|
||||||
|
assert.Equal(t, "https://open.spotify.com/track/2JKUgGuXK3dEvyuIJ4Yj2V", info["spotify_id"])
|
||||||
|
assert.Equal(t, "https://open.spotify.com/album/5U1umzRH4EONHWsFgPtRbA", info["spotify_album_id"])
|
||||||
|
assert.Equal(t, []string{"https://open.spotify.com/artist/101HSR6JTJqe3DBh6rb8kz"}, info["spotify_artist_ids"])
|
||||||
|
assert.Equal(t, []string{"https://open.spotify.com/artist/101HSR6JTJqe3DBh6rb8kz"}, info["spotify_album_artist_ids"])
|
||||||
|
}
|
458
backends/spotify/testdata/listen.json
vendored
Normal file
458
backends/spotify/testdata/listen.json
vendored
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
{
|
||||||
|
"track": {
|
||||||
|
"album": {
|
||||||
|
"album_type": "album",
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"external_urls": {
|
||||||
|
"spotify": "https://open.spotify.com/artist/101HSR6JTJqe3DBh6rb8kz"
|
||||||
|
},
|
||||||
|
"href": "https://api.spotify.com/v1/artists/101HSR6JTJqe3DBh6rb8kz",
|
||||||
|
"id": "101HSR6JTJqe3DBh6rb8kz",
|
||||||
|
"name": "Dool",
|
||||||
|
"type": "artist",
|
||||||
|
"uri": "spotify:artist:101HSR6JTJqe3DBh6rb8kz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"available_markets": [
|
||||||
|
"AR",
|
||||||
|
"AU",
|
||||||
|
"AT",
|
||||||
|
"BE",
|
||||||
|
"BO",
|
||||||
|
"BR",
|
||||||
|
"BG",
|
||||||
|
"CA",
|
||||||
|
"CL",
|
||||||
|
"CO",
|
||||||
|
"CR",
|
||||||
|
"CY",
|
||||||
|
"CZ",
|
||||||
|
"DK",
|
||||||
|
"DO",
|
||||||
|
"DE",
|
||||||
|
"EC",
|
||||||
|
"EE",
|
||||||
|
"SV",
|
||||||
|
"FI",
|
||||||
|
"FR",
|
||||||
|
"GR",
|
||||||
|
"GT",
|
||||||
|
"HN",
|
||||||
|
"HK",
|
||||||
|
"HU",
|
||||||
|
"IS",
|
||||||
|
"IE",
|
||||||
|
"IT",
|
||||||
|
"LV",
|
||||||
|
"LT",
|
||||||
|
"LU",
|
||||||
|
"MY",
|
||||||
|
"MT",
|
||||||
|
"MX",
|
||||||
|
"NL",
|
||||||
|
"NZ",
|
||||||
|
"NI",
|
||||||
|
"NO",
|
||||||
|
"PA",
|
||||||
|
"PY",
|
||||||
|
"PE",
|
||||||
|
"PH",
|
||||||
|
"PL",
|
||||||
|
"PT",
|
||||||
|
"SG",
|
||||||
|
"SK",
|
||||||
|
"ES",
|
||||||
|
"SE",
|
||||||
|
"CH",
|
||||||
|
"TW",
|
||||||
|
"TR",
|
||||||
|
"UY",
|
||||||
|
"US",
|
||||||
|
"GB",
|
||||||
|
"AD",
|
||||||
|
"LI",
|
||||||
|
"MC",
|
||||||
|
"ID",
|
||||||
|
"JP",
|
||||||
|
"TH",
|
||||||
|
"VN",
|
||||||
|
"RO",
|
||||||
|
"IL",
|
||||||
|
"ZA",
|
||||||
|
"SA",
|
||||||
|
"AE",
|
||||||
|
"BH",
|
||||||
|
"QA",
|
||||||
|
"OM",
|
||||||
|
"KW",
|
||||||
|
"EG",
|
||||||
|
"MA",
|
||||||
|
"DZ",
|
||||||
|
"TN",
|
||||||
|
"LB",
|
||||||
|
"JO",
|
||||||
|
"PS",
|
||||||
|
"IN",
|
||||||
|
"BY",
|
||||||
|
"KZ",
|
||||||
|
"MD",
|
||||||
|
"UA",
|
||||||
|
"AL",
|
||||||
|
"BA",
|
||||||
|
"HR",
|
||||||
|
"ME",
|
||||||
|
"MK",
|
||||||
|
"RS",
|
||||||
|
"SI",
|
||||||
|
"KR",
|
||||||
|
"BD",
|
||||||
|
"PK",
|
||||||
|
"LK",
|
||||||
|
"GH",
|
||||||
|
"KE",
|
||||||
|
"NG",
|
||||||
|
"TZ",
|
||||||
|
"UG",
|
||||||
|
"AG",
|
||||||
|
"AM",
|
||||||
|
"BS",
|
||||||
|
"BB",
|
||||||
|
"BZ",
|
||||||
|
"BT",
|
||||||
|
"BW",
|
||||||
|
"BF",
|
||||||
|
"CV",
|
||||||
|
"CW",
|
||||||
|
"DM",
|
||||||
|
"FJ",
|
||||||
|
"GM",
|
||||||
|
"GE",
|
||||||
|
"GD",
|
||||||
|
"GW",
|
||||||
|
"GY",
|
||||||
|
"HT",
|
||||||
|
"JM",
|
||||||
|
"KI",
|
||||||
|
"LS",
|
||||||
|
"LR",
|
||||||
|
"MW",
|
||||||
|
"MV",
|
||||||
|
"ML",
|
||||||
|
"MH",
|
||||||
|
"FM",
|
||||||
|
"NA",
|
||||||
|
"NR",
|
||||||
|
"NE",
|
||||||
|
"PW",
|
||||||
|
"PG",
|
||||||
|
"WS",
|
||||||
|
"SM",
|
||||||
|
"ST",
|
||||||
|
"SN",
|
||||||
|
"SC",
|
||||||
|
"SL",
|
||||||
|
"SB",
|
||||||
|
"KN",
|
||||||
|
"LC",
|
||||||
|
"VC",
|
||||||
|
"SR",
|
||||||
|
"TL",
|
||||||
|
"TO",
|
||||||
|
"TT",
|
||||||
|
"TV",
|
||||||
|
"VU",
|
||||||
|
"AZ",
|
||||||
|
"BN",
|
||||||
|
"BI",
|
||||||
|
"KH",
|
||||||
|
"CM",
|
||||||
|
"TD",
|
||||||
|
"KM",
|
||||||
|
"GQ",
|
||||||
|
"SZ",
|
||||||
|
"GA",
|
||||||
|
"GN",
|
||||||
|
"KG",
|
||||||
|
"LA",
|
||||||
|
"MO",
|
||||||
|
"MR",
|
||||||
|
"MN",
|
||||||
|
"NP",
|
||||||
|
"RW",
|
||||||
|
"TG",
|
||||||
|
"UZ",
|
||||||
|
"ZW",
|
||||||
|
"BJ",
|
||||||
|
"MG",
|
||||||
|
"MU",
|
||||||
|
"MZ",
|
||||||
|
"AO",
|
||||||
|
"CI",
|
||||||
|
"DJ",
|
||||||
|
"ZM",
|
||||||
|
"CD",
|
||||||
|
"CG",
|
||||||
|
"IQ",
|
||||||
|
"LY",
|
||||||
|
"TJ",
|
||||||
|
"VE",
|
||||||
|
"ET",
|
||||||
|
"XK"
|
||||||
|
],
|
||||||
|
"external_urls": {
|
||||||
|
"spotify": "https://open.spotify.com/album/5U1umzRH4EONHWsFgPtRbA"
|
||||||
|
},
|
||||||
|
"href": "https://api.spotify.com/v1/albums/5U1umzRH4EONHWsFgPtRbA",
|
||||||
|
"id": "5U1umzRH4EONHWsFgPtRbA",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"height": 640,
|
||||||
|
"url": "https://i.scdn.co/image/ab67616d0000b273c7b579ace1f3f56381d83aad",
|
||||||
|
"width": 640
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height": 300,
|
||||||
|
"url": "https://i.scdn.co/image/ab67616d00001e02c7b579ace1f3f56381d83aad",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height": 64,
|
||||||
|
"url": "https://i.scdn.co/image/ab67616d00004851c7b579ace1f3f56381d83aad",
|
||||||
|
"width": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Here Now, There Then",
|
||||||
|
"release_date": "2017-02-17",
|
||||||
|
"release_date_precision": "day",
|
||||||
|
"total_tracks": 8,
|
||||||
|
"type": "album",
|
||||||
|
"uri": "spotify:album:5U1umzRH4EONHWsFgPtRbA"
|
||||||
|
},
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"external_urls": {
|
||||||
|
"spotify": "https://open.spotify.com/artist/101HSR6JTJqe3DBh6rb8kz"
|
||||||
|
},
|
||||||
|
"href": "https://api.spotify.com/v1/artists/101HSR6JTJqe3DBh6rb8kz",
|
||||||
|
"id": "101HSR6JTJqe3DBh6rb8kz",
|
||||||
|
"name": "Dool",
|
||||||
|
"type": "artist",
|
||||||
|
"uri": "spotify:artist:101HSR6JTJqe3DBh6rb8kz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"available_markets": [
|
||||||
|
"AR",
|
||||||
|
"AU",
|
||||||
|
"AT",
|
||||||
|
"BE",
|
||||||
|
"BO",
|
||||||
|
"BR",
|
||||||
|
"BG",
|
||||||
|
"CA",
|
||||||
|
"CL",
|
||||||
|
"CO",
|
||||||
|
"CR",
|
||||||
|
"CY",
|
||||||
|
"CZ",
|
||||||
|
"DK",
|
||||||
|
"DO",
|
||||||
|
"DE",
|
||||||
|
"EC",
|
||||||
|
"EE",
|
||||||
|
"SV",
|
||||||
|
"FI",
|
||||||
|
"FR",
|
||||||
|
"GR",
|
||||||
|
"GT",
|
||||||
|
"HN",
|
||||||
|
"HK",
|
||||||
|
"HU",
|
||||||
|
"IS",
|
||||||
|
"IE",
|
||||||
|
"IT",
|
||||||
|
"LV",
|
||||||
|
"LT",
|
||||||
|
"LU",
|
||||||
|
"MY",
|
||||||
|
"MT",
|
||||||
|
"MX",
|
||||||
|
"NL",
|
||||||
|
"NZ",
|
||||||
|
"NI",
|
||||||
|
"NO",
|
||||||
|
"PA",
|
||||||
|
"PY",
|
||||||
|
"PE",
|
||||||
|
"PH",
|
||||||
|
"PL",
|
||||||
|
"PT",
|
||||||
|
"SG",
|
||||||
|
"SK",
|
||||||
|
"ES",
|
||||||
|
"SE",
|
||||||
|
"CH",
|
||||||
|
"TW",
|
||||||
|
"TR",
|
||||||
|
"UY",
|
||||||
|
"US",
|
||||||
|
"GB",
|
||||||
|
"AD",
|
||||||
|
"LI",
|
||||||
|
"MC",
|
||||||
|
"ID",
|
||||||
|
"JP",
|
||||||
|
"TH",
|
||||||
|
"VN",
|
||||||
|
"RO",
|
||||||
|
"IL",
|
||||||
|
"ZA",
|
||||||
|
"SA",
|
||||||
|
"AE",
|
||||||
|
"BH",
|
||||||
|
"QA",
|
||||||
|
"OM",
|
||||||
|
"KW",
|
||||||
|
"EG",
|
||||||
|
"MA",
|
||||||
|
"DZ",
|
||||||
|
"TN",
|
||||||
|
"LB",
|
||||||
|
"JO",
|
||||||
|
"PS",
|
||||||
|
"IN",
|
||||||
|
"BY",
|
||||||
|
"KZ",
|
||||||
|
"MD",
|
||||||
|
"UA",
|
||||||
|
"AL",
|
||||||
|
"BA",
|
||||||
|
"HR",
|
||||||
|
"ME",
|
||||||
|
"MK",
|
||||||
|
"RS",
|
||||||
|
"SI",
|
||||||
|
"KR",
|
||||||
|
"BD",
|
||||||
|
"PK",
|
||||||
|
"LK",
|
||||||
|
"GH",
|
||||||
|
"KE",
|
||||||
|
"NG",
|
||||||
|
"TZ",
|
||||||
|
"UG",
|
||||||
|
"AG",
|
||||||
|
"AM",
|
||||||
|
"BS",
|
||||||
|
"BB",
|
||||||
|
"BZ",
|
||||||
|
"BT",
|
||||||
|
"BW",
|
||||||
|
"BF",
|
||||||
|
"CV",
|
||||||
|
"CW",
|
||||||
|
"DM",
|
||||||
|
"FJ",
|
||||||
|
"GM",
|
||||||
|
"GE",
|
||||||
|
"GD",
|
||||||
|
"GW",
|
||||||
|
"GY",
|
||||||
|
"HT",
|
||||||
|
"JM",
|
||||||
|
"KI",
|
||||||
|
"LS",
|
||||||
|
"LR",
|
||||||
|
"MW",
|
||||||
|
"MV",
|
||||||
|
"ML",
|
||||||
|
"MH",
|
||||||
|
"FM",
|
||||||
|
"NA",
|
||||||
|
"NR",
|
||||||
|
"NE",
|
||||||
|
"PW",
|
||||||
|
"PG",
|
||||||
|
"WS",
|
||||||
|
"SM",
|
||||||
|
"ST",
|
||||||
|
"SN",
|
||||||
|
"SC",
|
||||||
|
"SL",
|
||||||
|
"SB",
|
||||||
|
"KN",
|
||||||
|
"LC",
|
||||||
|
"VC",
|
||||||
|
"SR",
|
||||||
|
"TL",
|
||||||
|
"TO",
|
||||||
|
"TT",
|
||||||
|
"TV",
|
||||||
|
"VU",
|
||||||
|
"AZ",
|
||||||
|
"BN",
|
||||||
|
"BI",
|
||||||
|
"KH",
|
||||||
|
"CM",
|
||||||
|
"TD",
|
||||||
|
"KM",
|
||||||
|
"GQ",
|
||||||
|
"SZ",
|
||||||
|
"GA",
|
||||||
|
"GN",
|
||||||
|
"KG",
|
||||||
|
"LA",
|
||||||
|
"MO",
|
||||||
|
"MR",
|
||||||
|
"MN",
|
||||||
|
"NP",
|
||||||
|
"RW",
|
||||||
|
"TG",
|
||||||
|
"UZ",
|
||||||
|
"ZW",
|
||||||
|
"BJ",
|
||||||
|
"MG",
|
||||||
|
"MU",
|
||||||
|
"MZ",
|
||||||
|
"AO",
|
||||||
|
"CI",
|
||||||
|
"DJ",
|
||||||
|
"ZM",
|
||||||
|
"CD",
|
||||||
|
"CG",
|
||||||
|
"IQ",
|
||||||
|
"LY",
|
||||||
|
"TJ",
|
||||||
|
"VE",
|
||||||
|
"ET",
|
||||||
|
"XK"
|
||||||
|
],
|
||||||
|
"disc_number": 1,
|
||||||
|
"duration_ms": 413826,
|
||||||
|
"explicit": false,
|
||||||
|
"external_ids": {
|
||||||
|
"isrc": "DES561620801"
|
||||||
|
},
|
||||||
|
"external_urls": {
|
||||||
|
"spotify": "https://open.spotify.com/track/2JKUgGuXK3dEvyuIJ4Yj2V"
|
||||||
|
},
|
||||||
|
"href": "https://api.spotify.com/v1/tracks/2JKUgGuXK3dEvyuIJ4Yj2V",
|
||||||
|
"id": "2JKUgGuXK3dEvyuIJ4Yj2V",
|
||||||
|
"is_local": false,
|
||||||
|
"name": "Oweynagat",
|
||||||
|
"popularity": 28,
|
||||||
|
"preview_url": "https://p.scdn.co/mp3-preview/5f01ec09c7a470b9899dc4b4bb427302dc648f24?cid=5433a04d90a946f2a0e5175b1383604a",
|
||||||
|
"track_number": 5,
|
||||||
|
"type": "track",
|
||||||
|
"uri": "spotify:track:2JKUgGuXK3dEvyuIJ4Yj2V"
|
||||||
|
},
|
||||||
|
"played_at": "2023-11-21T15:24:33.361Z",
|
||||||
|
"context": {
|
||||||
|
"type": "playlist",
|
||||||
|
"external_urls": {
|
||||||
|
"spotify": "https://open.spotify.com/playlist/37i9dQZF1E4odrTa1nmoN6"
|
||||||
|
},
|
||||||
|
"href": "https://api.spotify.com/v1/playlists/37i9dQZF1E4odrTa1nmoN6",
|
||||||
|
"uri": "spotify:playlist:37i9dQZF1E4odrTa1nmoN6"
|
||||||
|
}
|
||||||
|
}
|
1385
backends/spotify/testdata/recently-played.json
vendored
Normal file
1385
backends/spotify/testdata/recently-played.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -43,15 +43,19 @@ var listensCmd = &cobra.Command{
|
||||||
targetName, targetConfig := getConfigFromFlag(cmd, "to")
|
targetName, targetConfig := getConfigFromFlag(cmd, "to")
|
||||||
fmt.Printf("Transferring listens from %s to %s...\n", sourceName, targetName)
|
fmt.Printf("Transferring listens from %s to %s...\n", sourceName, targetName)
|
||||||
|
|
||||||
|
// Setup database
|
||||||
|
db, err := storage.New(viper.GetString("database"))
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
// Initialize backends
|
// Initialize backends
|
||||||
exportBackend, err := backends.ResolveBackend[models.ListensExport](sourceConfig)
|
exportBackend, err := backends.ResolveBackend[models.ListensExport](sourceConfig)
|
||||||
cobra.CheckErr(err)
|
cobra.CheckErr(err)
|
||||||
importBackend, err := backends.ResolveBackend[models.ListensImport](targetConfig)
|
importBackend, err := backends.ResolveBackend[models.ListensImport](targetConfig)
|
||||||
cobra.CheckErr(err)
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
// Setup database
|
// Authenticate backends, if needed
|
||||||
db, err := storage.New(viper.GetString("database"))
|
backends.Authenticate(exportBackend, db, viper.GetViper())
|
||||||
cobra.CheckErr(err)
|
backends.Authenticate(importBackend, db, viper.GetViper())
|
||||||
|
|
||||||
// Read timestamp
|
// Read timestamp
|
||||||
timestamp := time.Unix(getInt64FromFlag(cmd, "timestamp"), 0)
|
timestamp := time.Unix(getInt64FromFlag(cmd, "timestamp"), 0)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A listen service backend.
|
// A listen service backend.
|
||||||
|
@ -34,6 +35,15 @@ type Backend interface {
|
||||||
FromConfig(config *viper.Viper) Backend
|
FromConfig(config *viper.Viper) Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OAuth2Backend interface {
|
||||||
|
Backend
|
||||||
|
// Returns OAuth2 config suitable for this backend
|
||||||
|
OAuth2Config(redirectUrl string) oauth2.Config
|
||||||
|
|
||||||
|
// Setup the OAuth2 client
|
||||||
|
OAuth2Setup(redirectUrl string, token *oauth2.Token) error
|
||||||
|
}
|
||||||
|
|
||||||
type ImportBackend interface {
|
type ImportBackend interface {
|
||||||
Backend
|
Backend
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue