mirror of
https://git.sr.ht/~phw/scotty
synced 2025-06-02 19:58:33 +02:00
Compare commits
9 commits
39b31fc664
...
0a411fe2fa
Author | SHA1 | Date | |
---|---|---|---|
|
0a411fe2fa | ||
|
1e91b684cb | ||
|
19852be68b | ||
|
a6cc8d49ac | ||
|
a5442b477e | ||
|
90e101080f | ||
|
dff34b249c | ||
|
bcb1834994 | ||
|
d51c97c648 |
38 changed files with 322 additions and 322 deletions
|
@ -1,6 +1,6 @@
|
|||
# Scotty Changelog
|
||||
|
||||
## 0.5.0 - (not yet released)
|
||||
## 0.5.0 - 2025-04-29
|
||||
- ListenBrainz: handle missing loves metadata in case of merged recordings
|
||||
- ListenBrainz: fix loves import loading all existing loves
|
||||
- ListenBrainz: fixed progress for loves import
|
||||
|
|
|
@ -27,7 +27,7 @@ type OAuth2Authenticator interface {
|
|||
models.Backend
|
||||
|
||||
// Returns OAuth2 config suitable for this backend
|
||||
OAuth2Strategy(redirectUrl *url.URL) OAuth2Strategy
|
||||
OAuth2Strategy(redirectURL *url.URL) OAuth2Strategy
|
||||
|
||||
// Setup the OAuth2 client
|
||||
OAuth2Setup(token oauth2.TokenSource) error
|
||||
|
|
|
@ -24,14 +24,14 @@ import (
|
|||
type OAuth2Strategy interface {
|
||||
Config() oauth2.Config
|
||||
|
||||
AuthCodeURL(verifier string, state string) AuthUrl
|
||||
AuthCodeURL(verifier string, state string) AuthURL
|
||||
|
||||
ExchangeToken(code CodeResponse, verifier string) (*oauth2.Token, error)
|
||||
}
|
||||
|
||||
type AuthUrl struct {
|
||||
type AuthURL struct {
|
||||
// The URL the user must visit to approve access
|
||||
Url string
|
||||
URL string
|
||||
// Random state string passed on to the callback.
|
||||
// Leave empty if the service does not support state.
|
||||
State string
|
||||
|
@ -56,10 +56,10 @@ func (s StandardStrategy) Config() oauth2.Config {
|
|||
return s.conf
|
||||
}
|
||||
|
||||
func (s StandardStrategy) AuthCodeURL(verifier string, state string) AuthUrl {
|
||||
func (s StandardStrategy) AuthCodeURL(verifier string, state string) AuthURL {
|
||||
url := s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
|
||||
return AuthUrl{
|
||||
Url: url,
|
||||
return AuthURL{
|
||||
URL: url,
|
||||
State: state,
|
||||
Param: "code",
|
||||
}
|
||||
|
|
|
@ -33,10 +33,10 @@ func (s deezerStrategy) Config() oauth2.Config {
|
|||
return s.conf
|
||||
}
|
||||
|
||||
func (s deezerStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl {
|
||||
func (s deezerStrategy) AuthCodeURL(verifier string, state string) auth.AuthURL {
|
||||
url := s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
|
||||
return auth.AuthUrl{
|
||||
Url: url,
|
||||
return auth.AuthURL{
|
||||
URL: url,
|
||||
State: state,
|
||||
Param: "code",
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ const MaxItemsPerGet = 1000
|
|||
const DefaultRateLimitWaitSeconds = 5
|
||||
|
||||
type Client struct {
|
||||
HttpClient *resty.Client
|
||||
HTTPClient *resty.Client
|
||||
token oauth2.TokenSource
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ func NewClient(token oauth2.TokenSource) Client {
|
|||
client.SetHeader("User-Agent", version.UserAgent())
|
||||
client.SetRetryCount(5)
|
||||
return Client{
|
||||
HttpClient: client,
|
||||
HTTPClient: client,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func (c Client) setToken(req *resty.Request) error {
|
|||
}
|
||||
|
||||
func listRequest[T Result](c Client, path string, offset int, limit int) (result T, err error) {
|
||||
request := c.HttpClient.R().
|
||||
request := c.HTTPClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"index": strconv.Itoa(offset),
|
||||
"limit": strconv.Itoa(limit),
|
||||
|
|
|
@ -44,7 +44,7 @@ func TestGetUserHistory(t *testing.T) {
|
|||
|
||||
token := oauth2.StaticTokenSource(&oauth2.Token{})
|
||||
client := deezer.NewClient(token)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.deezer.com/user/me/history",
|
||||
"testdata/user-history.json")
|
||||
|
||||
|
@ -65,7 +65,7 @@ func TestGetUserTracks(t *testing.T) {
|
|||
|
||||
token := oauth2.StaticTokenSource(&oauth2.Token{})
|
||||
client := deezer.NewClient(token)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.deezer.com/user/me/tracks",
|
||||
"testdata/user-tracks.json")
|
||||
|
||||
|
@ -81,7 +81,7 @@ func TestGetUserTracks(t *testing.T) {
|
|||
assert.Equal("Outland", track1.Track.Album.Title)
|
||||
}
|
||||
|
||||
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
httpmock.ActivateNonDefault(client)
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath))
|
||||
|
|
|
@ -31,7 +31,7 @@ import (
|
|||
|
||||
type DeezerApiBackend struct {
|
||||
client Client
|
||||
clientId string
|
||||
clientID string
|
||||
clientSecret string
|
||||
}
|
||||
|
||||
|
@ -50,19 +50,19 @@ func (b *DeezerApiBackend) Options() []models.BackendOption {
|
|||
}
|
||||
|
||||
func (b *DeezerApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||
b.clientId = config.GetString("client-id")
|
||||
b.clientID = config.GetString("client-id")
|
||||
b.clientSecret = config.GetString("client-secret")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DeezerApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy {
|
||||
func (b *DeezerApiBackend) OAuth2Strategy(redirectURL *url.URL) auth.OAuth2Strategy {
|
||||
conf := oauth2.Config{
|
||||
ClientID: b.clientId,
|
||||
ClientID: b.clientID,
|
||||
ClientSecret: b.clientSecret,
|
||||
Scopes: []string{
|
||||
"offline_access,basic_access,listening_history",
|
||||
},
|
||||
RedirectURL: redirectUrl.String(),
|
||||
RedirectURL: redirectURL.String(),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://connect.deezer.com/oauth/auth.php",
|
||||
TokenURL: "https://connect.deezer.com/oauth/access_token.php",
|
||||
|
@ -244,8 +244,8 @@ func (t Track) AsTrack() models.Track {
|
|||
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/album/%v", t.Album.Id)
|
||||
info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/artist/%v", t.Artist.Id)
|
||||
info["deezer_album_id"] = fmt.Sprintf("https://www.deezer.com/album/%v", t.Album.ID)
|
||||
info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/artist/%v", t.Artist.ID)
|
||||
|
||||
return track
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ type HistoryResult struct {
|
|||
}
|
||||
|
||||
type Track struct {
|
||||
Id int `json:"id"`
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
|
@ -75,7 +75,7 @@ type LovedTrack struct {
|
|||
}
|
||||
|
||||
type Album struct {
|
||||
Id int `json:"id"`
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
|
@ -83,7 +83,7 @@ type Album struct {
|
|||
}
|
||||
|
||||
type Artist struct {
|
||||
Id int `json:"id"`
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Link string `json:"link"`
|
||||
Name string `json:"name"`
|
||||
|
|
|
@ -33,13 +33,13 @@ import (
|
|||
const MaxItemsPerGet = 50
|
||||
|
||||
type Client struct {
|
||||
HttpClient *resty.Client
|
||||
HTTPClient *resty.Client
|
||||
token string
|
||||
}
|
||||
|
||||
func NewClient(serverUrl string, token string) Client {
|
||||
func NewClient(serverURL string, token string) Client {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(serverUrl)
|
||||
client.SetBaseURL(serverURL)
|
||||
client.SetAuthScheme("Bearer")
|
||||
client.SetAuthToken(token)
|
||||
client.SetHeader("Accept", "application/json")
|
||||
|
@ -49,14 +49,14 @@ func NewClient(serverUrl string, token string) Client {
|
|||
ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After")
|
||||
|
||||
return Client{
|
||||
HttpClient: client,
|
||||
HTTPClient: client,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Client) GetHistoryListenings(user string, page int, perPage int) (result ListeningsResult, err error) {
|
||||
const path = "/api/v1/history/listenings"
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"username": user,
|
||||
"page": strconv.Itoa(page),
|
||||
|
@ -75,7 +75,7 @@ func (c Client) GetHistoryListenings(user string, page int, perPage int) (result
|
|||
|
||||
func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksResult, err error) {
|
||||
const path = "/api/v1/favorites/tracks"
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"page": strconv.Itoa(page),
|
||||
"page_size": strconv.Itoa(perPage),
|
||||
|
|
|
@ -32,20 +32,20 @@ import (
|
|||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
serverUrl := "https://funkwhale.example.com"
|
||||
serverURL := "https://funkwhale.example.com"
|
||||
token := "foobar123"
|
||||
client := funkwhale.NewClient(serverUrl, token)
|
||||
assert.Equal(t, serverUrl, client.HttpClient.BaseURL)
|
||||
assert.Equal(t, token, client.HttpClient.Token)
|
||||
client := funkwhale.NewClient(serverURL, token)
|
||||
assert.Equal(t, serverURL, client.HTTPClient.BaseURL)
|
||||
assert.Equal(t, token, client.HTTPClient.Token)
|
||||
}
|
||||
|
||||
func TestGetHistoryListenings(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
serverUrl := "https://funkwhale.example.com"
|
||||
serverURL := "https://funkwhale.example.com"
|
||||
token := "thetoken"
|
||||
client := funkwhale.NewClient(serverUrl, token)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
client := funkwhale.NewClient(serverURL, token)
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://funkwhale.example.com/api/v1/history/listenings",
|
||||
"testdata/listenings.json")
|
||||
|
||||
|
@ -67,9 +67,9 @@ func TestGetFavoriteTracks(t *testing.T) {
|
|||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
token := "thetoken"
|
||||
serverUrl := "https://funkwhale.example.com"
|
||||
client := funkwhale.NewClient(serverUrl, token)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
serverURL := "https://funkwhale.example.com"
|
||||
client := funkwhale.NewClient(serverURL, token)
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://funkwhale.example.com/api/v1/favorites/tracks",
|
||||
"testdata/favorite-tracks.json")
|
||||
|
||||
|
@ -87,7 +87,7 @@ func TestGetFavoriteTracks(t *testing.T) {
|
|||
assert.Equal("phw", fav1.User.UserName)
|
||||
}
|
||||
|
||||
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
httpmock.ActivateNonDefault(client)
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath))
|
||||
|
|
|
@ -31,7 +31,7 @@ type ListeningsResult struct {
|
|||
}
|
||||
|
||||
type Listening struct {
|
||||
Id int `json:"int"`
|
||||
ID int `json:"int"`
|
||||
User User `json:"user"`
|
||||
Track Track `json:"track"`
|
||||
CreationDate string `json:"creation_date"`
|
||||
|
@ -45,14 +45,14 @@ type FavoriteTracksResult struct {
|
|||
}
|
||||
|
||||
type FavoriteTrack struct {
|
||||
Id int `json:"int"`
|
||||
ID int `json:"int"`
|
||||
User User `json:"user"`
|
||||
Track Track `json:"track"`
|
||||
CreationDate string `json:"creation_date"`
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
Id int `json:"int"`
|
||||
ID int `json:"int"`
|
||||
Artist Artist `json:"artist"`
|
||||
Album Album `json:"album"`
|
||||
Title string `json:"title"`
|
||||
|
@ -64,13 +64,13 @@ type Track struct {
|
|||
}
|
||||
|
||||
type Artist struct {
|
||||
Id int `json:"int"`
|
||||
ID int `json:"int"`
|
||||
Name string `json:"name"`
|
||||
ArtistMBID mbtypes.MBID `json:"mbid"`
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
Id int `json:"int"`
|
||||
ID int `json:"int"`
|
||||
Title string `json:"title"`
|
||||
AlbumArtist Artist `json:"artist"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
|
@ -79,7 +79,7 @@ type Album struct {
|
|||
}
|
||||
|
||||
type User struct {
|
||||
Id int `json:"int"`
|
||||
ID int `json:"int"`
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ func (b *JSPFBackend) InitConfig(config *config.ServiceConfig) error {
|
|||
Identifier: config.GetString("identifier"),
|
||||
Tracks: make([]jspf.Track, 0),
|
||||
Extension: map[string]any{
|
||||
jspf.MusicBrainzPlaylistExtensionId: jspf.MusicBrainzPlaylistExtension{
|
||||
jspf.MusicBrainzPlaylistExtensionID: jspf.MusicBrainzPlaylistExtension{
|
||||
LastModifiedAt: time.Now(),
|
||||
Public: true,
|
||||
},
|
||||
|
@ -116,7 +116,7 @@ func listenAsTrack(l models.Listen) jspf.Track {
|
|||
extension := makeMusicBrainzExtension(l.Track)
|
||||
extension.AddedAt = l.ListenedAt
|
||||
extension.AddedBy = l.UserName
|
||||
track.Extension[jspf.MusicBrainzTrackExtensionId] = extension
|
||||
track.Extension[jspf.MusicBrainzTrackExtensionID] = extension
|
||||
|
||||
if l.RecordingMBID != "" {
|
||||
track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMBID))
|
||||
|
@ -131,7 +131,7 @@ func loveAsTrack(l models.Love) jspf.Track {
|
|||
extension := makeMusicBrainzExtension(l.Track)
|
||||
extension.AddedAt = l.Created
|
||||
extension.AddedBy = l.UserName
|
||||
track.Extension[jspf.MusicBrainzTrackExtensionId] = extension
|
||||
track.Extension[jspf.MusicBrainzTrackExtensionID] = extension
|
||||
|
||||
recordingMBID := l.Track.RecordingMBID
|
||||
if l.RecordingMBID != "" {
|
||||
|
|
|
@ -25,21 +25,21 @@ import (
|
|||
|
||||
type lastfmStrategy struct {
|
||||
client *lastfm.Api
|
||||
redirectUrl *url.URL
|
||||
redirectURL *url.URL
|
||||
}
|
||||
|
||||
func (s lastfmStrategy) Config() oauth2.Config {
|
||||
return oauth2.Config{}
|
||||
}
|
||||
|
||||
func (s lastfmStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl {
|
||||
func (s lastfmStrategy) AuthCodeURL(verifier string, state string) auth.AuthURL {
|
||||
// Last.fm does not use OAuth2, but the provided authorization flow with
|
||||
// callback URL is close enough we can shoehorn it into the existing
|
||||
// authentication strategy.
|
||||
// TODO: Investigate and use callback-less flow with api.GetAuthTokenUrl(token)
|
||||
url := s.client.GetAuthRequestUrl(s.redirectUrl.String())
|
||||
return auth.AuthUrl{
|
||||
Url: url,
|
||||
url := s.client.GetAuthRequestUrl(s.redirectURL.String())
|
||||
return auth.AuthURL{
|
||||
URL: url,
|
||||
State: "", // last.fm does not use state
|
||||
Param: "token",
|
||||
}
|
||||
|
|
|
@ -62,9 +62,9 @@ func (b *LastfmApiBackend) Options() []models.BackendOption {
|
|||
}
|
||||
|
||||
func (b *LastfmApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||
clientId := config.GetString("client-id")
|
||||
clientID := config.GetString("client-id")
|
||||
clientSecret := config.GetString("client-secret")
|
||||
b.client = lastfm.New(clientId, clientSecret)
|
||||
b.client = lastfm.New(clientID, clientSecret)
|
||||
b.username = config.GetString("username")
|
||||
return nil
|
||||
}
|
||||
|
@ -72,10 +72,10 @@ func (b *LastfmApiBackend) InitConfig(config *config.ServiceConfig) error {
|
|||
func (b *LastfmApiBackend) StartImport() error { return nil }
|
||||
func (b *LastfmApiBackend) FinishImport() error { return nil }
|
||||
|
||||
func (b *LastfmApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy {
|
||||
func (b *LastfmApiBackend) OAuth2Strategy(redirectURL *url.URL) auth.OAuth2Strategy {
|
||||
return lastfmStrategy{
|
||||
client: b.client,
|
||||
redirectUrl: redirectUrl,
|
||||
redirectURL: redirectURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ const (
|
|||
)
|
||||
|
||||
type Client struct {
|
||||
HttpClient *resty.Client
|
||||
HTTPClient *resty.Client
|
||||
MaxResults int
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ func NewClient(token string) Client {
|
|||
ratelimit.EnableHTTPHeaderRateLimit(client, "X-RateLimit-Reset-In")
|
||||
|
||||
return Client{
|
||||
HttpClient: client,
|
||||
HTTPClient: client,
|
||||
MaxResults: DefaultItemsPerGet,
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ func NewClient(token string) Client {
|
|||
func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) {
|
||||
const path = "/user/{username}/listens"
|
||||
errorResult := ErrorResult{}
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetPathParam("username", user).
|
||||
SetQueryParams(map[string]string{
|
||||
"max_ts": strconv.FormatInt(maxTime.Unix(), 10),
|
||||
|
@ -84,7 +84,7 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
|
|||
func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, err error) {
|
||||
const path = "/submit-listens"
|
||||
errorResult := ErrorResult{}
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetBody(listens).
|
||||
SetResult(&result).
|
||||
SetError(&errorResult).
|
||||
|
@ -100,7 +100,7 @@ func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, er
|
|||
func (c Client) GetFeedback(user string, status int, offset int) (result GetFeedbackResult, err error) {
|
||||
const path = "/feedback/user/{username}/get-feedback"
|
||||
errorResult := ErrorResult{}
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetPathParam("username", user).
|
||||
SetQueryParams(map[string]string{
|
||||
"status": strconv.Itoa(status),
|
||||
|
@ -122,7 +122,7 @@ func (c Client) GetFeedback(user string, status int, offset int) (result GetFeed
|
|||
func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) {
|
||||
const path = "/feedback/recording-feedback"
|
||||
errorResult := ErrorResult{}
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetBody(feedback).
|
||||
SetResult(&result).
|
||||
SetError(&errorResult).
|
||||
|
@ -138,7 +138,7 @@ func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error)
|
|||
func (c Client) Lookup(recordingName string, artistName string) (result LookupResult, err error) {
|
||||
const path = "/metadata/lookup"
|
||||
errorResult := ErrorResult{}
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"recording_name": recordingName,
|
||||
"artist_name": artistName,
|
||||
|
|
|
@ -36,7 +36,7 @@ import (
|
|||
func TestNewClient(t *testing.T) {
|
||||
token := "foobar123"
|
||||
client := listenbrainz.NewClient(token)
|
||||
assert.Equal(t, token, client.HttpClient.Token)
|
||||
assert.Equal(t, token, client.HTTPClient.Token)
|
||||
assert.Equal(t, listenbrainz.DefaultItemsPerGet, client.MaxResults)
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ func TestGetListens(t *testing.T) {
|
|||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client.MaxResults = 2
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
||||
"testdata/listens.json")
|
||||
|
||||
|
@ -62,7 +62,7 @@ func TestGetListens(t *testing.T) {
|
|||
|
||||
func TestSubmitListens(t *testing.T) {
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
httpmock.ActivateNonDefault(client.HttpClient.GetClient())
|
||||
httpmock.ActivateNonDefault(client.HTTPClient.GetClient())
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{
|
||||
Status: "ok",
|
||||
|
@ -103,7 +103,7 @@ func TestGetFeedback(t *testing.T) {
|
|||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client.MaxResults = 2
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback",
|
||||
"testdata/feedback.json")
|
||||
|
||||
|
@ -120,7 +120,7 @@ func TestGetFeedback(t *testing.T) {
|
|||
|
||||
func TestSendFeedback(t *testing.T) {
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
httpmock.ActivateNonDefault(client.HttpClient.GetClient())
|
||||
httpmock.ActivateNonDefault(client.HTTPClient.GetClient())
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{
|
||||
Status: "ok",
|
||||
|
@ -145,7 +145,7 @@ func TestLookup(t *testing.T) {
|
|||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/metadata/lookup",
|
||||
"testdata/lookup.json")
|
||||
|
||||
|
@ -158,7 +158,7 @@ func TestLookup(t *testing.T) {
|
|||
assert.Equal(mbtypes.MBID("569436a1-234a-44bc-a370-8f4d252bef21"), result.RecordingMBID)
|
||||
}
|
||||
|
||||
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
httpmock.ActivateNonDefault(client)
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath))
|
||||
|
|
|
@ -131,7 +131,7 @@ func TestTrackTrackNumberString(t *testing.T) {
|
|||
assert.Equal(t, 12, track.TrackNumber())
|
||||
}
|
||||
|
||||
func TestTrackIsrc(t *testing.T) {
|
||||
func TestTrackISRC(t *testing.T) {
|
||||
expected := mbtypes.ISRC("TCAEJ1934417")
|
||||
track := listenbrainz.Track{
|
||||
AdditionalInfo: map[string]any{
|
||||
|
|
|
@ -32,25 +32,25 @@ import (
|
|||
const MaxItemsPerGet = 1000
|
||||
|
||||
type Client struct {
|
||||
HttpClient *resty.Client
|
||||
HTTPClient *resty.Client
|
||||
token string
|
||||
}
|
||||
|
||||
func NewClient(serverUrl string, token string) Client {
|
||||
func NewClient(serverURL string, token string) Client {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(serverUrl)
|
||||
client.SetBaseURL(serverURL)
|
||||
client.SetHeader("Accept", "application/json")
|
||||
client.SetHeader("User-Agent", version.UserAgent())
|
||||
client.SetRetryCount(5)
|
||||
return Client{
|
||||
HttpClient: client,
|
||||
HTTPClient: client,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, err error) {
|
||||
const path = "/apis/mlj_1/scrobbles"
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"page": strconv.Itoa(page),
|
||||
"perpage": strconv.Itoa(perPage),
|
||||
|
@ -68,7 +68,7 @@ func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult,
|
|||
func (c Client) NewScrobble(scrobble NewScrobble) (result NewScrobbleResult, err error) {
|
||||
const path = "/apis/mlj_1/newscrobble"
|
||||
scrobble.Key = c.token
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetBody(scrobble).
|
||||
SetResult(&result).
|
||||
Post(path)
|
||||
|
|
|
@ -32,19 +32,19 @@ import (
|
|||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
serverUrl := "https://maloja.example.com"
|
||||
serverURL := "https://maloja.example.com"
|
||||
token := "foobar123"
|
||||
client := maloja.NewClient(serverUrl, token)
|
||||
assert.Equal(t, serverUrl, client.HttpClient.BaseURL)
|
||||
client := maloja.NewClient(serverURL, token)
|
||||
assert.Equal(t, serverURL, client.HTTPClient.BaseURL)
|
||||
}
|
||||
|
||||
func TestGetScrobbles(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
serverUrl := "https://maloja.example.com"
|
||||
serverURL := "https://maloja.example.com"
|
||||
token := "thetoken"
|
||||
client := maloja.NewClient(serverUrl, token)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
client := maloja.NewClient(serverURL, token)
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://maloja.example.com/apis/mlj_1/scrobbles",
|
||||
"testdata/scrobbles.json")
|
||||
|
||||
|
@ -60,7 +60,7 @@ func TestGetScrobbles(t *testing.T) {
|
|||
func TestNewScrobble(t *testing.T) {
|
||||
server := "https://maloja.example.com"
|
||||
client := maloja.NewClient(server, "thetoken")
|
||||
httpmock.ActivateNonDefault(client.HttpClient.GetClient())
|
||||
httpmock.ActivateNonDefault(client.HTTPClient.GetClient())
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, httpmock.File("testdata/newscrobble-result.json"))
|
||||
if err != nil {
|
||||
|
@ -80,7 +80,7 @@ func TestNewScrobble(t *testing.T) {
|
|||
assert.Equal(t, "success", result.Status)
|
||||
}
|
||||
|
||||
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
httpmock.ActivateNonDefault(client)
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath))
|
||||
|
|
|
@ -76,7 +76,7 @@ func (b *ScrobblerLogBackend) InitConfig(config *config.ServiceConfig) error {
|
|||
b.log.FallbackTimezone = location
|
||||
}
|
||||
b.log = scrobblerlog.ScrobblerLog{
|
||||
TZ: scrobblerlog.TZ_UTC,
|
||||
TZ: scrobblerlog.TimezoneUTC,
|
||||
Client: "Rockbox unknown $Revision$",
|
||||
}
|
||||
return nil
|
||||
|
@ -197,7 +197,7 @@ func listenToRecord(listen models.Listen) scrobblerlog.Record {
|
|||
var rating scrobblerlog.Rating
|
||||
rockboxRating, ok := listen.AdditionalInfo["rockbox_rating"].(string)
|
||||
if !ok || rockboxRating == "" {
|
||||
rating = scrobblerlog.RATING_LISTENED
|
||||
rating = scrobblerlog.RatingListened
|
||||
} else {
|
||||
rating = scrobblerlog.Rating(rating)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ const (
|
|||
)
|
||||
|
||||
type Client struct {
|
||||
HttpClient *resty.Client
|
||||
HTTPClient *resty.Client
|
||||
}
|
||||
|
||||
func NewClient(token oauth2.TokenSource) Client {
|
||||
|
@ -55,7 +55,7 @@ func NewClient(token oauth2.TokenSource) Client {
|
|||
ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After")
|
||||
|
||||
return Client{
|
||||
HttpClient: client,
|
||||
HTTPClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ func (c Client) RecentlyPlayedBefore(before time.Time, limit int) (RecentlyPlaye
|
|||
|
||||
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().
|
||||
request := c.HTTPClient.R().
|
||||
SetQueryParam("limit", strconv.Itoa(limit)).
|
||||
SetResult(&result)
|
||||
if after != nil {
|
||||
|
@ -87,7 +87,7 @@ func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) (
|
|||
|
||||
func (c Client) UserTracks(offset int, limit int) (result TracksResult, err error) {
|
||||
const path = "/me/tracks"
|
||||
response, err := c.HttpClient.R().
|
||||
response, err := c.HTTPClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"offset": strconv.Itoa(offset),
|
||||
"limit": strconv.Itoa(limit),
|
||||
|
|
|
@ -43,7 +43,7 @@ func TestRecentlyPlayedAfter(t *testing.T) {
|
|||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := spotify.NewClient(nil)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.spotify.com/v1/me/player/recently-played",
|
||||
"testdata/recently-played.json")
|
||||
|
||||
|
@ -63,7 +63,7 @@ func TestGetUserTracks(t *testing.T) {
|
|||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := spotify.NewClient(nil)
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.spotify.com/v1/me/tracks",
|
||||
"testdata/user-tracks.json")
|
||||
|
||||
|
@ -79,7 +79,7 @@ func TestGetUserTracks(t *testing.T) {
|
|||
assert.Equal("Zeal & Ardor", track1.Track.Album.Name)
|
||||
}
|
||||
|
||||
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
httpmock.ActivateNonDefault(client)
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath))
|
||||
|
|
|
@ -58,7 +58,7 @@ type Listen struct {
|
|||
}
|
||||
|
||||
type Track struct {
|
||||
Id string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Href string `json:"href"`
|
||||
Uri string `json:"uri"`
|
||||
|
@ -69,14 +69,14 @@ type Track struct {
|
|||
Explicit bool `json:"explicit"`
|
||||
IsLocal bool `json:"is_local"`
|
||||
Popularity int `json:"popularity"`
|
||||
ExternalIds ExternalIds `json:"external_ids"`
|
||||
ExternalUrls ExternalUrls `json:"external_urls"`
|
||||
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"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Href string `json:"href"`
|
||||
Uri string `json:"uri"`
|
||||
|
@ -85,32 +85,32 @@ type Album struct {
|
|||
ReleaseDate string `json:"release_date"`
|
||||
ReleaseDatePrecision string `json:"release_date_precision"`
|
||||
AlbumType string `json:"album_type"`
|
||||
ExternalUrls ExternalUrls `json:"external_urls"`
|
||||
ExternalURLs ExternalURLs `json:"external_urls"`
|
||||
Artists []Artist `json:"artists"`
|
||||
Images []Image `json:"images"`
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
Id string `json:"id"`
|
||||
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"`
|
||||
ExternalURLs ExternalURLs `json:"external_urls"`
|
||||
}
|
||||
|
||||
type ExternalIds struct {
|
||||
type ExternalIDs struct {
|
||||
ISRC mbtypes.ISRC `json:"isrc"`
|
||||
EAN string `json:"ean"`
|
||||
UPC string `json:"upc"`
|
||||
}
|
||||
|
||||
type ExternalUrls struct {
|
||||
type ExternalURLs struct {
|
||||
Spotify string `json:"spotify"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Url string `json:"url"`
|
||||
URL string `json:"url"`
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import (
|
|||
|
||||
type SpotifyApiBackend struct {
|
||||
client Client
|
||||
clientId string
|
||||
clientID string
|
||||
clientSecret string
|
||||
}
|
||||
|
||||
|
@ -53,14 +53,14 @@ func (b *SpotifyApiBackend) Options() []models.BackendOption {
|
|||
}
|
||||
|
||||
func (b *SpotifyApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||
b.clientId = config.GetString("client-id")
|
||||
b.clientID = config.GetString("client-id")
|
||||
b.clientSecret = config.GetString("client-secret")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SpotifyApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy {
|
||||
func (b *SpotifyApiBackend) OAuth2Strategy(redirectURL *url.URL) auth.OAuth2Strategy {
|
||||
conf := oauth2.Config{
|
||||
ClientID: b.clientId,
|
||||
ClientID: b.clientID,
|
||||
ClientSecret: b.clientSecret,
|
||||
Scopes: []string{
|
||||
"user-read-currently-playing",
|
||||
|
@ -68,16 +68,16 @@ func (b *SpotifyApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Stra
|
|||
"user-library-read",
|
||||
"user-library-modify",
|
||||
},
|
||||
RedirectURL: redirectUrl.String(),
|
||||
RedirectURL: redirectURL.String(),
|
||||
Endpoint: spotify.Endpoint,
|
||||
}
|
||||
|
||||
return auth.NewStandardStrategy(conf)
|
||||
}
|
||||
|
||||
func (b *SpotifyApiBackend) OAuth2Config(redirectUrl *url.URL) oauth2.Config {
|
||||
func (b *SpotifyApiBackend) OAuth2Config(redirectURL *url.URL) oauth2.Config {
|
||||
return oauth2.Config{
|
||||
ClientID: b.clientId,
|
||||
ClientID: b.clientID,
|
||||
ClientSecret: b.clientSecret,
|
||||
Scopes: []string{
|
||||
"user-read-currently-playing",
|
||||
|
@ -85,7 +85,7 @@ func (b *SpotifyApiBackend) OAuth2Config(redirectUrl *url.URL) oauth2.Config {
|
|||
"user-library-read",
|
||||
"user-library-modify",
|
||||
},
|
||||
RedirectURL: redirectUrl.String(),
|
||||
RedirectURL: redirectURL.String(),
|
||||
Endpoint: spotify.Endpoint,
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +251,7 @@ func (t Track) AsTrack() models.Track {
|
|||
Duration: time.Duration(t.DurationMs * int(time.Millisecond)),
|
||||
TrackNumber: t.TrackNumber,
|
||||
DiscNumber: t.DiscNumber,
|
||||
ISRC: t.ExternalIds.ISRC,
|
||||
ISRC: t.ExternalIDs.ISRC,
|
||||
AdditionalInfo: map[string]any{},
|
||||
}
|
||||
|
||||
|
@ -264,30 +264,30 @@ func (t Track) AsTrack() models.Track {
|
|||
info["music_service"] = "spotify.com"
|
||||
}
|
||||
|
||||
if t.ExternalUrls.Spotify != "" {
|
||||
info["origin_url"] = t.ExternalUrls.Spotify
|
||||
info["spotify_id"] = t.ExternalUrls.Spotify
|
||||
if t.ExternalURLs.Spotify != "" {
|
||||
info["origin_url"] = t.ExternalURLs.Spotify
|
||||
info["spotify_id"] = t.ExternalURLs.Spotify
|
||||
}
|
||||
|
||||
if t.Album.ExternalUrls.Spotify != "" {
|
||||
info["spotify_album_id"] = t.Album.ExternalUrls.Spotify
|
||||
if t.Album.ExternalURLs.Spotify != "" {
|
||||
info["spotify_album_id"] = t.Album.ExternalURLs.Spotify
|
||||
}
|
||||
|
||||
if len(t.Artists) > 0 {
|
||||
info["spotify_artist_ids"] = extractArtistIds(t.Artists)
|
||||
info["spotify_artist_ids"] = extractArtistIDs(t.Artists)
|
||||
}
|
||||
|
||||
if len(t.Album.Artists) > 0 {
|
||||
info["spotify_album_artist_ids"] = extractArtistIds(t.Album.Artists)
|
||||
info["spotify_album_artist_ids"] = extractArtistIDs(t.Album.Artists)
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
func extractArtistIds(artists []Artist) []string {
|
||||
artistIds := make([]string, len(artists))
|
||||
func extractArtistIDs(artists []Artist) []string {
|
||||
artistIDs := make([]string, len(artists))
|
||||
for i, artist := range artists {
|
||||
artistIds[i] = artist.ExternalUrls.Spotify
|
||||
artistIDs[i] = artist.ExternalURLs.Spotify
|
||||
}
|
||||
return artistIds
|
||||
return artistIDs
|
||||
}
|
||||
|
|
|
@ -92,8 +92,8 @@ func (i HistoryItem) AsListen() models.Listen {
|
|||
PlaybackDuration: time.Duration(i.MillisecondsPlayed * int(time.Millisecond)),
|
||||
UserName: i.UserName,
|
||||
}
|
||||
if trackUrl, err := formatSpotifyUri(i.SpotifyTrackUri); err != nil {
|
||||
listen.AdditionalInfo["spotify_id"] = trackUrl
|
||||
if trackURL, err := formatSpotifyUri(i.SpotifyTrackUri); err != nil {
|
||||
listen.AdditionalInfo["spotify_id"] = trackURL
|
||||
}
|
||||
return listen
|
||||
}
|
||||
|
|
|
@ -43,20 +43,20 @@ func AuthenticationFlow(service config.ServiceConfig, backend auth.OAuth2Authent
|
|||
|
||||
state := auth.RandomState()
|
||||
// Redirect user to consent page to ask for permission specified scopes.
|
||||
authUrl := strategy.AuthCodeURL(verifier, state)
|
||||
authURL := strategy.AuthCodeURL(verifier, state)
|
||||
|
||||
// Start an HTTP server to listen for the response
|
||||
responseChan := make(chan auth.CodeResponse)
|
||||
auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan)
|
||||
auth.RunOauth2CallbackServer(*redirectURL, authURL.Param, responseChan)
|
||||
|
||||
// Open the URL
|
||||
fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url))
|
||||
err = browser.OpenURL(authUrl.Url)
|
||||
fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authURL.URL))
|
||||
err = browser.OpenURL(authURL.URL)
|
||||
cobra.CheckErr(err)
|
||||
|
||||
// Retrieve the code from the authentication callback
|
||||
code := <-responseChan
|
||||
if code.State != authUrl.State {
|
||||
if code.State != authURL.State {
|
||||
cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) err
|
|||
}
|
||||
|
||||
func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp backends.ImportProcessor[R]) error {
|
||||
fmt.Println(i18n.Tr("Transferring %s from %s to %s...", c.entity, c.sourceName, c.targetName))
|
||||
fmt.Println(i18n.Tr("Transferring %s from %s to %s…", c.entity, c.sourceName, c.targetName))
|
||||
|
||||
// Authenticate backends, if needed
|
||||
config := viper.GetViper()
|
||||
|
|
|
@ -16,11 +16,10 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
|||
package i18n
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/Xuanwo/go-locale"
|
||||
_ "go.uploadedlobster.com/scotty/internal/translations"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
|
@ -29,7 +28,7 @@ var localizer Localizer
|
|||
func init() {
|
||||
tag, err := locale.Detect()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
tag = language.English
|
||||
}
|
||||
localizer = New(tag)
|
||||
}
|
||||
|
|
|
@ -42,56 +42,56 @@ var messageKeyToIndex = map[string]int{
|
|||
"\tbackend: %v": 11,
|
||||
"\texport: %s": 0,
|
||||
"\timport: %s\n": 1,
|
||||
"%v: %v": 47,
|
||||
"%v: %v": 48,
|
||||
"Aborted": 8,
|
||||
"Access token": 19,
|
||||
"Access token received, you can use %v now.\n": 33,
|
||||
"Access token received, you can use %v now.\n": 34,
|
||||
"Append to file": 21,
|
||||
"Backend": 41,
|
||||
"Backend": 42,
|
||||
"Check for duplicate listens on import (slower)": 24,
|
||||
"Client ID": 15,
|
||||
"Client secret": 16,
|
||||
"Delete the service configuration \"%v\"?": 7,
|
||||
"Directory path": 27,
|
||||
"Disable auto correction of submitted listens": 25,
|
||||
"Error: OAuth state mismatch": 32,
|
||||
"Directory path": 29,
|
||||
"Disable auto correction of submitted listens": 26,
|
||||
"Error: OAuth state mismatch": 33,
|
||||
"Failed reading config: %v": 2,
|
||||
"File path": 20,
|
||||
"From timestamp: %v (%v)": 43,
|
||||
"Ignore listens in incognito mode": 28,
|
||||
"Ignore skipped listens": 29,
|
||||
"Ignored duplicate listen %v: \"%v\" by %v (%v)": 53,
|
||||
"Import failed, last reported timestamp was %v (%s)": 44,
|
||||
"Import log:": 46,
|
||||
"Imported %v of %v %s into %v.": 45,
|
||||
"Include skipped listens": 26,
|
||||
"Latest timestamp: %v (%v)": 49,
|
||||
"Minimum playback duration for skipped tracks (seconds)": 30,
|
||||
"No": 38,
|
||||
"From timestamp: %v (%v)": 44,
|
||||
"Ignore listens in incognito mode": 30,
|
||||
"Ignore skipped listens": 27,
|
||||
"Ignored duplicate listen %v: \"%v\" by %v (%v)": 25,
|
||||
"Import failed, last reported timestamp was %v (%s)": 45,
|
||||
"Import log:": 47,
|
||||
"Imported %v of %v %s into %v.": 46,
|
||||
"Latest timestamp: %v (%v)": 50,
|
||||
"Minimum playback duration for skipped tracks (seconds)": 31,
|
||||
"No": 39,
|
||||
"Playlist title": 22,
|
||||
"Saved service %v using backend %v": 5,
|
||||
"Server URL": 17,
|
||||
"Service": 40,
|
||||
"Service": 41,
|
||||
"Service \"%v\" deleted\n": 9,
|
||||
"Service name": 3,
|
||||
"Specify a time zone for the listen timestamps": 28,
|
||||
"The backend %v requires authentication. Authenticate now?": 6,
|
||||
"Token received, you can close this window now.": 12,
|
||||
"Transferring %s from %s to %s...": 42,
|
||||
"Transferring %s from %s to %s…": 43,
|
||||
"Unique playlist identifier": 23,
|
||||
"Updated service %v using backend %v\n": 10,
|
||||
"User name": 18,
|
||||
"Visit the URL for authorization: %v": 31,
|
||||
"Yes": 37,
|
||||
"Visit the URL for authorization: %v": 32,
|
||||
"Yes": 38,
|
||||
"a service with this name already exists": 4,
|
||||
"backend %s does not implement %s": 13,
|
||||
"done": 36,
|
||||
"exporting": 34,
|
||||
"importing": 35,
|
||||
"invalid timestamp string \"%v\"": 48,
|
||||
"key must only consist of A-Za-z0-9_-": 51,
|
||||
"no configuration file defined, cannot write config": 50,
|
||||
"no existing service configurations": 39,
|
||||
"no service configuration \"%v\"": 52,
|
||||
"done": 37,
|
||||
"exporting": 35,
|
||||
"importing": 36,
|
||||
"invalid timestamp string \"%v\"": 49,
|
||||
"key must only consist of A-Za-z0-9_-": 52,
|
||||
"no configuration file defined, cannot write config": 51,
|
||||
"no existing service configurations": 40,
|
||||
"no service configuration \"%v\"": 53,
|
||||
"unknown backend \"%s\"": 14,
|
||||
}
|
||||
|
||||
|
@ -103,18 +103,18 @@ var deIndex = []uint32{ // 55 elements
|
|||
0x000001ac, 0x000001e7, 0x00000213, 0x00000233,
|
||||
0x0000023d, 0x0000024b, 0x00000256, 0x00000263,
|
||||
0x00000271, 0x0000027b, 0x0000028e, 0x000002a1,
|
||||
0x000002b8, 0x000002ed, 0x00000321, 0x00000342,
|
||||
0x00000352, 0x00000378, 0x0000039a, 0x000003d8,
|
||||
0x000002b8, 0x000002ed, 0x00000328, 0x0000035c,
|
||||
0x0000037e, 0x000003a4, 0x000003b4, 0x000003da,
|
||||
// Entry 20 - 3F
|
||||
0x000003fe, 0x00000428, 0x00000468, 0x00000473,
|
||||
0x0000047e, 0x00000485, 0x00000488, 0x0000048d,
|
||||
0x000004b6, 0x000004be, 0x000004c6, 0x000004ef,
|
||||
0x0000050d, 0x0000054a, 0x00000575, 0x00000580,
|
||||
0x0000058d, 0x000005b1, 0x000005d4, 0x00000625,
|
||||
0x0000065c, 0x00000683, 0x00000683,
|
||||
0x00000418, 0x00000443, 0x0000046d, 0x000004ad,
|
||||
0x000004b8, 0x000004c3, 0x000004ca, 0x000004cd,
|
||||
0x000004d2, 0x000004fb, 0x00000503, 0x0000050b,
|
||||
0x00000534, 0x00000552, 0x0000058f, 0x000005ba,
|
||||
0x000005c5, 0x000005d2, 0x000005f6, 0x00000619,
|
||||
0x0000066a, 0x000006a1, 0x000006c8,
|
||||
} // Size: 244 bytes
|
||||
|
||||
const deData string = "" + // Size: 1667 bytes
|
||||
const deData string = "" + // Size: 1736 bytes
|
||||
"\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" +
|
||||
" %[1]s\x02Fehler beim Lesen der Konfiguration: %[1]v\x02Servicename\x02e" +
|
||||
"in Service mit diesem Namen existiert bereits\x02Service %[1]v mit dem B" +
|
||||
|
@ -123,25 +123,26 @@ const deData string = "" + // Size: 1667 bytes
|
|||
"\x02Abgebrochen\x04\x00\x01\x0a\x1e\x02Service „%[1]v“ gelöscht\x04\x00" +
|
||||
"\x01\x0a1\x02Service %[1]v mit dem Backend %[2]v aktualisiert\x04\x01" +
|
||||
"\x09\x00\x0f\x02Backend: %[1]v\x02Token erhalten, das Fenster kann jetzt" +
|
||||
" geschlossen werden.\x02das backend %[1]s implementiert %[2]s nicht\x02u" +
|
||||
" geschlossen werden.\x02das Backend %[1]s implementiert %[2]s nicht\x02u" +
|
||||
"nbekanntes Backend „%[1]s“\x02Client-ID\x02Client-Secret\x02Server-URL" +
|
||||
"\x02Benutzername\x02Zugriffstoken\x02Dateipfad\x02An Datei anhängen\x02T" +
|
||||
"itel der Playlist\x02Eindeutige Playlist-ID\x02Beim Import auf Listen-Du" +
|
||||
"plikate prüfen (langsamer)\x02Autokorrektur für übermittelte Titel deakt" +
|
||||
"ivieren\x02Übersprungene Titel einbeziehen\x02Verzeichnispfad\x02Listens" +
|
||||
" im Inkognito-Modus ignorieren\x02Übersprungene Listens ignorieren\x02Mi" +
|
||||
"nimale Wiedergabedauer für übersprungene Titel (Sekunden)\x02URL für Aut" +
|
||||
"orisierung öffnen: %[1]v\x02Fehler: OAuth-State stimmt nicht überein\x04" +
|
||||
"\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwendet werd" +
|
||||
"en.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine bestehe" +
|
||||
"nden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %[1]s von" +
|
||||
" %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehlgesc" +
|
||||
"hlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3]s i" +
|
||||
"n %[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Zeitstem" +
|
||||
"pel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfiguration" +
|
||||
"sdatei definiert, Konfiguration kann nicht geschrieben werden\x02Schlüss" +
|
||||
"el darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekonfigura" +
|
||||
"tion „%[1]v“"
|
||||
"plikate prüfen (langsamer)\x02Listen-Duplikat ignoriert %[1]v: \x22%[2]v" +
|
||||
"\x22 von %[3]v (%[4]v)\x02Autokorrektur für übermittelte Titel deaktivie" +
|
||||
"ren\x02Übersprungene Listens ignorieren\x02Zeitzone für den Abspiel-Zeit" +
|
||||
"stempel\x02Verzeichnispfad\x02Listens im Inkognito-Modus ignorieren\x02M" +
|
||||
"inimale Wiedergabedauer für übersprungene Titel (Sekunden)\x02Zur Anmeld" +
|
||||
"ung folgende URL aufrufen: %[1]v\x02Fehler: OAuth-State stimmt nicht übe" +
|
||||
"rein\x04\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwen" +
|
||||
"det werden.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine" +
|
||||
" bestehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %" +
|
||||
"[1]s von %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fe" +
|
||||
"hlgeschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %" +
|
||||
"[3]s in %[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Ze" +
|
||||
"itstempel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfigu" +
|
||||
"rationsdatei definiert, Konfiguration kann nicht geschrieben werden\x02S" +
|
||||
"chlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekon" +
|
||||
"figuration „%[1]v“"
|
||||
|
||||
var enIndex = []uint32{ // 55 elements
|
||||
// Entry 0 - 1F
|
||||
|
@ -151,18 +152,18 @@ var enIndex = []uint32{ // 55 elements
|
|||
0x00000170, 0x0000019f, 0x000001c6, 0x000001de,
|
||||
0x000001e8, 0x000001f6, 0x00000201, 0x0000020b,
|
||||
0x00000218, 0x00000222, 0x00000231, 0x00000240,
|
||||
0x0000025b, 0x0000028a, 0x000002b7, 0x000002cf,
|
||||
0x000002de, 0x000002ff, 0x00000316, 0x0000034d,
|
||||
0x0000025b, 0x0000028a, 0x000002c3, 0x000002f0,
|
||||
0x00000307, 0x00000335, 0x00000344, 0x00000365,
|
||||
// Entry 20 - 3F
|
||||
0x00000374, 0x00000390, 0x000003c3, 0x000003cd,
|
||||
0x000003d7, 0x000003dc, 0x000003e0, 0x000003e3,
|
||||
0x00000406, 0x0000040e, 0x00000416, 0x00000440,
|
||||
0x0000045e, 0x00000497, 0x000004c1, 0x000004cd,
|
||||
0x000004da, 0x000004fb, 0x0000051b, 0x0000054e,
|
||||
0x00000573, 0x00000594, 0x000005cd,
|
||||
0x0000039c, 0x000003c3, 0x000003df, 0x00000412,
|
||||
0x0000041c, 0x00000426, 0x0000042b, 0x0000042f,
|
||||
0x00000432, 0x00000455, 0x0000045d, 0x00000465,
|
||||
0x0000048f, 0x000004ad, 0x000004e6, 0x00000510,
|
||||
0x0000051c, 0x00000529, 0x0000054a, 0x0000056a,
|
||||
0x0000059d, 0x000005c2, 0x000005e3,
|
||||
} // Size: 244 bytes
|
||||
|
||||
const enData string = "" + // Size: 1485 bytes
|
||||
const enData string = "" + // Size: 1507 bytes
|
||||
"\x04\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import:" +
|
||||
" %[1]s\x02Failed reading config: %[1]v\x02Service name\x02a service with" +
|
||||
" this name already exists\x02Saved service %[1]v using backend %[2]v\x02" +
|
||||
|
@ -174,19 +175,19 @@ const enData string = "" + // Size: 1485 bytes
|
|||
"ent %[2]s\x02unknown backend \x22%[1]s\x22\x02Client ID\x02Client secret" +
|
||||
"\x02Server URL\x02User name\x02Access token\x02File path\x02Append to fi" +
|
||||
"le\x02Playlist title\x02Unique playlist identifier\x02Check for duplicat" +
|
||||
"e listens on import (slower)\x02Disable auto correction of submitted lis" +
|
||||
"tens\x02Include skipped listens\x02Directory path\x02Ignore listens in i" +
|
||||
"ncognito mode\x02Ignore skipped listens\x02Minimum playback duration for" +
|
||||
" skipped tracks (seconds)\x02Visit the URL for authorization: %[1]v\x02E" +
|
||||
"rror: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token received, yo" +
|
||||
"u can use %[1]v now.\x02exporting\x02importing\x02done\x02Yes\x02No\x02n" +
|
||||
"o existing service configurations\x02Service\x02Backend\x02Transferring " +
|
||||
"%[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Import " +
|
||||
"failed, last reported timestamp was %[1]v (%[2]s)\x02Imported %[1]v of %" +
|
||||
"[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v\x02invalid timesta" +
|
||||
"mp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%[2]v)\x02no configu" +
|
||||
"ration file defined, cannot write config\x02key must only consist of A-Z" +
|
||||
"a-z0-9_-\x02no service configuration \x22%[1]v\x22\x02Ignored duplicate " +
|
||||
"listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)"
|
||||
"e listens on import (slower)\x02Ignored duplicate listen %[1]v: \x22%[2]" +
|
||||
"v\x22 by %[3]v (%[4]v)\x02Disable auto correction of submitted listens" +
|
||||
"\x02Ignore skipped listens\x02Specify a time zone for the listen timesta" +
|
||||
"mps\x02Directory path\x02Ignore listens in incognito mode\x02Minimum pla" +
|
||||
"yback duration for skipped tracks (seconds)\x02Visit the URL for authori" +
|
||||
"zation: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access " +
|
||||
"token received, you can use %[1]v now.\x02exporting\x02importing\x02done" +
|
||||
"\x02Yes\x02No\x02no existing service configurations\x02Service\x02Backen" +
|
||||
"d\x02Transferring %[1]s from %[2]s to %[3]s…\x02From timestamp: %[1]v (%" +
|
||||
"[2]v)\x02Import failed, last reported timestamp was %[1]v (%[2]s)\x02Imp" +
|
||||
"orted %[1]v of %[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v" +
|
||||
"\x02invalid timestamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%" +
|
||||
"[2]v)\x02no configuration file defined, cannot write config\x02key must " +
|
||||
"only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22"
|
||||
|
||||
// Total table size 3640 bytes (3KiB); checksum: 719A868A
|
||||
// Total table size 3731 bytes (3KiB); checksum: F7951710
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
{
|
||||
"id": "backend {Backend} does not implement {InterfaceName}",
|
||||
"message": "backend {Backend} does not implement {InterfaceName}",
|
||||
"translation": "das backend {Backend} implementiert {InterfaceName} nicht",
|
||||
"translation": "das Backend {Backend} implementiert {InterfaceName} nicht",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Backend",
|
||||
|
@ -261,9 +261,9 @@
|
|||
"translation": "Beim Import auf Listen-Duplikate prüfen (langsamer)"
|
||||
},
|
||||
{
|
||||
"id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})",
|
||||
"message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})",
|
||||
"translation": "Listen-Duplikat ignoriert {ListenedAt}: „{TrackName}“ von {ArtistName} ({RecordingMbid})",
|
||||
"id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"translation": "Listen-Duplikat ignoriert {ListenedAt}: \"{TrackName}\" von {ArtistName} ({RecordingMBID})",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ListenedAt",
|
||||
|
@ -290,12 +290,12 @@
|
|||
"expr": "l.ArtistName()"
|
||||
},
|
||||
{
|
||||
"id": "RecordingMbid",
|
||||
"id": "RecordingMBID",
|
||||
"string": "%[4]v",
|
||||
"type": "go.uploadedlobster.com/scotty/internal/models.MBID",
|
||||
"type": "go.uploadedlobster.com/mbtypes.MBID",
|
||||
"underlyingType": "string",
|
||||
"argNum": 4,
|
||||
"expr": "l.RecordingMbid"
|
||||
"expr": "l.RecordingMBID"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -305,9 +305,14 @@
|
|||
"translation": "Autokorrektur für übermittelte Titel deaktivieren"
|
||||
},
|
||||
{
|
||||
"id": "Include skipped listens",
|
||||
"message": "Include skipped listens",
|
||||
"translation": "Übersprungene Titel einbeziehen"
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Übersprungene Listens ignorieren"
|
||||
},
|
||||
{
|
||||
"id": "Specify a time zone for the listen timestamps",
|
||||
"message": "Specify a time zone for the listen timestamps",
|
||||
"translation": "Zeitzone für den Abspiel-Zeitstempel"
|
||||
},
|
||||
{
|
||||
"id": "Directory path",
|
||||
|
@ -319,28 +324,23 @@
|
|||
"message": "Ignore listens in incognito mode",
|
||||
"translation": "Listens im Inkognito-Modus ignorieren"
|
||||
},
|
||||
{
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Übersprungene Listens ignorieren"
|
||||
},
|
||||
{
|
||||
"id": "Minimum playback duration for skipped tracks (seconds)",
|
||||
"message": "Minimum playback duration for skipped tracks (seconds)",
|
||||
"translation": "Minimale Wiedergabedauer für übersprungene Titel (Sekunden)"
|
||||
},
|
||||
{
|
||||
"id": "Visit the URL for authorization: {Url}",
|
||||
"message": "Visit the URL for authorization: {Url}",
|
||||
"translation": "URL für Autorisierung öffnen: {Url}",
|
||||
"id": "Visit the URL for authorization: {URL}",
|
||||
"message": "Visit the URL for authorization: {URL}",
|
||||
"translation": "Zur Anmeldung folgende URL aufrufen: {URL}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Url",
|
||||
"id": "URL",
|
||||
"string": "%[1]v",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "authUrl.Url"
|
||||
"expr": "authURL.URL"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -411,9 +411,9 @@
|
|||
"translation": "Backend"
|
||||
},
|
||||
{
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...",
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"translation": "Übertrage {Entity} von {SourceName} nach {TargetName}…",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Entity",
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
{
|
||||
"id": "backend {Backend} does not implement {InterfaceName}",
|
||||
"message": "backend {Backend} does not implement {InterfaceName}",
|
||||
"translation": "das backend {Backend} implementiert {InterfaceName} nicht",
|
||||
"translation": "das Backend {Backend} implementiert {InterfaceName} nicht",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Backend",
|
||||
|
@ -263,7 +263,7 @@
|
|||
{
|
||||
"id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"translation": "",
|
||||
"translation": "Listen-Duplikat ignoriert {ListenedAt}: \"{TrackName}\" von {ArtistName} ({RecordingMBID})",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ListenedAt",
|
||||
|
@ -305,9 +305,14 @@
|
|||
"translation": "Autokorrektur für übermittelte Titel deaktivieren"
|
||||
},
|
||||
{
|
||||
"id": "Include skipped listens",
|
||||
"message": "Include skipped listens",
|
||||
"translation": "Übersprungene Titel einbeziehen"
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Übersprungene Listens ignorieren"
|
||||
},
|
||||
{
|
||||
"id": "Specify a time zone for the listen timestamps",
|
||||
"message": "Specify a time zone for the listen timestamps",
|
||||
"translation": "Zeitzone für den Abspiel-Zeitstempel"
|
||||
},
|
||||
{
|
||||
"id": "Directory path",
|
||||
|
@ -319,28 +324,23 @@
|
|||
"message": "Ignore listens in incognito mode",
|
||||
"translation": "Listens im Inkognito-Modus ignorieren"
|
||||
},
|
||||
{
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Übersprungene Listens ignorieren"
|
||||
},
|
||||
{
|
||||
"id": "Minimum playback duration for skipped tracks (seconds)",
|
||||
"message": "Minimum playback duration for skipped tracks (seconds)",
|
||||
"translation": "Minimale Wiedergabedauer für übersprungene Titel (Sekunden)"
|
||||
},
|
||||
{
|
||||
"id": "Visit the URL for authorization: {Url}",
|
||||
"message": "Visit the URL for authorization: {Url}",
|
||||
"translation": "URL für Autorisierung öffnen: {Url}",
|
||||
"id": "Visit the URL for authorization: {URL}",
|
||||
"message": "Visit the URL for authorization: {URL}",
|
||||
"translation": "Zur Anmeldung folgende URL aufrufen: {URL}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Url",
|
||||
"id": "URL",
|
||||
"string": "%[1]v",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "authUrl.Url"
|
||||
"expr": "authURL.URL"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -411,9 +411,9 @@
|
|||
"translation": "Backend"
|
||||
},
|
||||
{
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...",
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"translation": "Übertrage {Entity} von {SourceName} nach {TargetName}…",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Entity",
|
||||
|
|
|
@ -311,9 +311,9 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})",
|
||||
"message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})",
|
||||
"translation": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})",
|
||||
"id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"translation": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
|
@ -341,12 +341,12 @@
|
|||
"expr": "l.ArtistName()"
|
||||
},
|
||||
{
|
||||
"id": "RecordingMbid",
|
||||
"id": "RecordingMBID",
|
||||
"string": "%[4]v",
|
||||
"type": "go.uploadedlobster.com/scotty/internal/models.MBID",
|
||||
"type": "go.uploadedlobster.com/mbtypes.MBID",
|
||||
"underlyingType": "string",
|
||||
"argNum": 4,
|
||||
"expr": "l.RecordingMbid"
|
||||
"expr": "l.RecordingMBID"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
|
@ -359,9 +359,16 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Include skipped listens",
|
||||
"message": "Include skipped listens",
|
||||
"translation": "Include skipped listens",
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Ignore skipped listens",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Specify a time zone for the listen timestamps",
|
||||
"message": "Specify a time zone for the listen timestamps",
|
||||
"translation": "Specify a time zone for the listen timestamps",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
|
@ -379,13 +386,6 @@
|
|||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Ignore skipped listens",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Minimum playback duration for skipped tracks (seconds)",
|
||||
"message": "Minimum playback duration for skipped tracks (seconds)",
|
||||
|
@ -394,18 +394,18 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Visit the URL for authorization: {Url}",
|
||||
"message": "Visit the URL for authorization: {Url}",
|
||||
"translation": "Visit the URL for authorization: {Url}",
|
||||
"id": "Visit the URL for authorization: {URL}",
|
||||
"message": "Visit the URL for authorization: {URL}",
|
||||
"translation": "Visit the URL for authorization: {URL}",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Url",
|
||||
"id": "URL",
|
||||
"string": "%[1]v",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "authUrl.Url"
|
||||
"expr": "authURL.URL"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
|
@ -491,9 +491,9 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"translation": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"translation": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
|
@ -714,4 +714,4 @@
|
|||
"fuzzy": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -359,9 +359,16 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Include skipped listens",
|
||||
"message": "Include skipped listens",
|
||||
"translation": "Include skipped listens",
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Ignore skipped listens",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Specify a time zone for the listen timestamps",
|
||||
"message": "Specify a time zone for the listen timestamps",
|
||||
"translation": "Specify a time zone for the listen timestamps",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
|
@ -379,13 +386,6 @@
|
|||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Ignore skipped listens",
|
||||
"message": "Ignore skipped listens",
|
||||
"translation": "Ignore skipped listens",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Minimum playback duration for skipped tracks (seconds)",
|
||||
"message": "Minimum playback duration for skipped tracks (seconds)",
|
||||
|
@ -394,18 +394,18 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Visit the URL for authorization: {Url}",
|
||||
"message": "Visit the URL for authorization: {Url}",
|
||||
"translation": "Visit the URL for authorization: {Url}",
|
||||
"id": "Visit the URL for authorization: {URL}",
|
||||
"message": "Visit the URL for authorization: {URL}",
|
||||
"translation": "Visit the URL for authorization: {URL}",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Url",
|
||||
"id": "URL",
|
||||
"string": "%[1]v",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "authUrl.Url"
|
||||
"expr": "authURL.URL"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
|
@ -491,9 +491,9 @@
|
|||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"translation": "Transferring {Entity} from {SourceName} to {TargetName}...",
|
||||
"id": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"message": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"translation": "Transferring {Entity} from {SourceName} to {TargetName}…",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright © 2023-2024 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
|
||||
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
|
||||
|
@ -17,7 +17,7 @@ package version
|
|||
|
||||
const (
|
||||
AppName = "scotty"
|
||||
AppVersion = "0.4.1"
|
||||
AppVersion = "0.5.0"
|
||||
AppURL = "https://git.sr.ht/~phw/scotty/"
|
||||
)
|
||||
|
||||
|
|
|
@ -26,9 +26,9 @@ import "time"
|
|||
|
||||
const (
|
||||
// The identifier for the MusicBrainz / ListenBrainz JSPF playlist extension
|
||||
MusicBrainzPlaylistExtensionId = "https://musicbrainz.org/doc/jspf#playlist"
|
||||
MusicBrainzPlaylistExtensionID = "https://musicbrainz.org/doc/jspf#playlist"
|
||||
// The identifier for the MusicBrainz / ListenBrainz JSPF track extension
|
||||
MusicBrainzTrackExtensionId = "https://musicbrainz.org/doc/jspf#track"
|
||||
MusicBrainzTrackExtensionID = "https://musicbrainz.org/doc/jspf#track"
|
||||
)
|
||||
|
||||
// MusicBrainz / ListenBrainz JSPF track extension
|
||||
|
|
|
@ -39,7 +39,7 @@ func ExampleMusicBrainzTrackExtension() {
|
|||
{
|
||||
Title: "Oweynagat",
|
||||
Extension: map[string]any{
|
||||
jspf.MusicBrainzTrackExtensionId: jspf.MusicBrainzTrackExtension{
|
||||
jspf.MusicBrainzTrackExtensionID: jspf.MusicBrainzTrackExtension{
|
||||
AddedAt: time.Date(2023, 11, 24, 07, 47, 50, 0, time.UTC),
|
||||
AddedBy: "scotty",
|
||||
},
|
||||
|
|
|
@ -45,16 +45,16 @@ import (
|
|||
type TZInfo string
|
||||
|
||||
const (
|
||||
TZ_UNKNOWN TZInfo = "UNKNOWN"
|
||||
TZ_UTC TZInfo = "UTC"
|
||||
TimezoneUnknown TZInfo = "UNKNOWN"
|
||||
TimezoneUTC TZInfo = "UTC"
|
||||
)
|
||||
|
||||
// L if listened at least 50% or S if skipped
|
||||
type Rating string
|
||||
|
||||
const (
|
||||
RATING_LISTENED Rating = "L"
|
||||
RATING_SKIPPED Rating = "S"
|
||||
RatingListened Rating = "L"
|
||||
RatingSkipped Rating = "S"
|
||||
)
|
||||
|
||||
// A single entry of a track in the scrobbler log file.
|
||||
|
@ -75,7 +75,7 @@ type ScrobblerLog struct {
|
|||
Client string
|
||||
Records []Record
|
||||
// Timezone to be used for timestamps in the log file,
|
||||
// if TZ is set to [TZ_UNKNOWN].
|
||||
// if TZ is set to [TimezoneUnknown].
|
||||
FallbackTimezone *time.Location
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ func (l *ScrobblerLog) Parse(data io.Reader, ignoreSkipped bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if ignoreSkipped && record.Rating == RATING_SKIPPED {
|
||||
if ignoreSkipped && record.Rating == RatingSkipped {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -224,7 +224,7 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) {
|
|||
}
|
||||
|
||||
var timezone *time.Location = nil
|
||||
if l.TZ == TZ_UNKNOWN {
|
||||
if l.TZ == TimezoneUnknown {
|
||||
timezone = l.FallbackTimezone
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ func TestParser(t *testing.T) {
|
|||
result := scrobblerlog.ScrobblerLog{}
|
||||
err := result.Parse(data, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ)
|
||||
assert.Equal(scrobblerlog.TimezoneUnknown, result.TZ)
|
||||
assert.Equal("Rockbox sansaclipplus $Revision$", result.Client)
|
||||
assert.Len(result.Records, 5)
|
||||
record1 := result.Records[0]
|
||||
|
@ -59,11 +59,11 @@ func TestParser(t *testing.T) {
|
|||
assert.Equal("Sevdanin rengi (sipacik) byMrTurkey", record1.TrackName)
|
||||
assert.Equal(5, record1.TrackNumber)
|
||||
assert.Equal(time.Duration(306*time.Second), record1.Duration)
|
||||
assert.Equal(scrobblerlog.RATING_LISTENED, record1.Rating)
|
||||
assert.Equal(scrobblerlog.RatingListened, record1.Rating)
|
||||
assert.Equal(time.Unix(1260342084, 0), record1.Timestamp)
|
||||
assert.Equal(mbtypes.MBID(""), record1.MusicBrainzRecordingID)
|
||||
record4 := result.Records[3]
|
||||
assert.Equal(scrobblerlog.RATING_SKIPPED, record4.Rating)
|
||||
assert.Equal(scrobblerlog.RatingSkipped, record4.Rating)
|
||||
assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"),
|
||||
record4.MusicBrainzRecordingID)
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ func TestParserIgnoreSkipped(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Len(result.Records, 4)
|
||||
record4 := result.Records[3]
|
||||
assert.Equal(scrobblerlog.RATING_LISTENED, record4.Rating)
|
||||
assert.Equal(scrobblerlog.RatingListened, record4.Rating)
|
||||
assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"),
|
||||
record4.MusicBrainzRecordingID)
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ func TestAppend(t *testing.T) {
|
|||
data := make([]byte, 0, 10)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
log := scrobblerlog.ScrobblerLog{
|
||||
TZ: scrobblerlog.TZ_UNKNOWN,
|
||||
TZ: scrobblerlog.TimezoneUnknown,
|
||||
Client: "Rockbox foo $Revision$",
|
||||
}
|
||||
records := []scrobblerlog.Record{
|
||||
|
@ -111,7 +111,7 @@ func TestAppend(t *testing.T) {
|
|||
TrackName: "Reign",
|
||||
TrackNumber: 1,
|
||||
Duration: 271 * time.Second,
|
||||
Rating: scrobblerlog.RATING_LISTENED,
|
||||
Rating: scrobblerlog.RatingListened,
|
||||
Timestamp: time.Unix(1699572072, 0),
|
||||
MusicBrainzRecordingID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"),
|
||||
},
|
||||
|
@ -139,7 +139,7 @@ func TestReadHeader(t *testing.T) {
|
|||
log := scrobblerlog.ScrobblerLog{}
|
||||
err := log.ReadHeader(reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN)
|
||||
assert.Equal(t, log.TZ, scrobblerlog.TimezoneUnknown)
|
||||
assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$")
|
||||
assert.Empty(t, log.Records)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue