mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-18 11:09:29 +02:00
Updated all import/export interfaces
This commit is contained in:
parent
729a3d0ed0
commit
ab04eb1123
12 changed files with 247 additions and 167 deletions
backends
dump
funkwhale
jspf
listenbrainz
maloja
scrobblerlog
subsonic
models
|
@ -37,8 +37,6 @@ func (b DumpBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
|
|
||||||
func (b DumpBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
func (b DumpBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||||
importResult := models.ImportResult{
|
importResult := models.ImportResult{
|
||||||
TotalCount: 0,
|
|
||||||
ImportCount: 0,
|
|
||||||
LastTimestamp: oldestTimestamp,
|
LastTimestamp: oldestTimestamp,
|
||||||
}
|
}
|
||||||
for result := range results {
|
for result := range results {
|
||||||
|
@ -59,8 +57,6 @@ func (b DumpBackend) ImportListens(results chan models.ListensResult, oldestTime
|
||||||
|
|
||||||
func (b DumpBackend) ImportLoves(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
func (b DumpBackend) ImportLoves(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||||
importResult := models.ImportResult{
|
importResult := models.ImportResult{
|
||||||
TotalCount: 0,
|
|
||||||
ImportCount: 0,
|
|
||||||
LastTimestamp: oldestTimestamp,
|
LastTimestamp: oldestTimestamp,
|
||||||
}
|
}
|
||||||
for result := range results {
|
for result := range results {
|
||||||
|
|
|
@ -22,7 +22,7 @@ THE SOFTWARE.
|
||||||
package funkwhale
|
package funkwhale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -45,17 +45,19 @@ func (b FunkwhaleApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time) ([]models.Listen, error) {
|
func (b FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||||
page := 1
|
page := 1
|
||||||
perPage := MaxItemsPerGet
|
perPage := MaxItemsPerGet
|
||||||
|
|
||||||
listens := make([]models.Listen, 0, 2*MaxItemsPerGet)
|
// We need to gather the full list of listens in order to sort them
|
||||||
|
listens := make(models.ListensList, 0, 2*MaxItemsPerGet)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetHistoryListenings(b.username, page, perPage)
|
result, err := b.client.GetHistoryListenings(b.username, page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.ListensResult{Error: err}
|
||||||
|
close(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
count := len(result.Results)
|
count := len(result.Results)
|
||||||
|
@ -80,21 +82,26 @@ out:
|
||||||
page += 1
|
page += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Reverse(listens)
|
sort.Sort(listens)
|
||||||
return listens, nil
|
results <- models.ListensResult{Listens: listens}
|
||||||
|
close(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time) ([]models.Love, error) {
|
func (b FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||||
page := 1
|
page := 1
|
||||||
perPage := MaxItemsPerGet
|
perPage := MaxItemsPerGet
|
||||||
|
|
||||||
loves := make([]models.Love, 0, 2*MaxItemsPerGet)
|
defer close(results)
|
||||||
|
|
||||||
|
// We need to gather the full list of listens in order to sort them
|
||||||
|
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetFavoriteTracks(page, perPage)
|
result, err := b.client.GetFavoriteTracks(page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.LovesResult{Error: err}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count := len(result.Results)
|
count := len(result.Results)
|
||||||
|
@ -119,8 +126,8 @@ out:
|
||||||
page += 1
|
page += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Reverse(loves)
|
sort.Sort(loves)
|
||||||
return loves, nil
|
results <- models.LovesResult{Loves: loves}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l Listening) ToListen() models.Listen {
|
func (l Listening) ToListen() models.Listen {
|
||||||
|
|
|
@ -46,52 +46,65 @@ func (b JspfBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b JspfBackend) ImportLoves(loves []models.Love, oldestTimestamp time.Time) (models.ImportResult, error) {
|
func (b JspfBackend) ImportLoves(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||||
result := models.ImportResult{
|
importResult := models.ImportResult{
|
||||||
TotalCount: len(loves),
|
|
||||||
ImportCount: 0,
|
|
||||||
LastTimestamp: oldestTimestamp,
|
LastTimestamp: oldestTimestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks := make([]Track, 0, result.TotalCount)
|
tracks := make([]Track, 0, importResult.TotalCount)
|
||||||
for _, love := range loves {
|
for result := range results {
|
||||||
extension := MusicBrainzTrackExtension{
|
if result.Error != nil {
|
||||||
AddedAt: love.Created,
|
return importResult, result.Error
|
||||||
AddedBy: love.UserName,
|
|
||||||
AdditionalMetadata: love.AdditionalInfo,
|
|
||||||
ArtistIdentifiers: make([]string, len(love.ArtistMbids)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, mbid := range love.ArtistMbids {
|
importResult.TotalCount += len(result.Loves)
|
||||||
extension.ArtistIdentifiers[i] = "https://musicbrainz.org/artist/" + string(mbid)
|
for _, love := range result.Loves {
|
||||||
|
track := loveToTrack(love)
|
||||||
|
tracks = append(tracks, track)
|
||||||
|
oldestTimestamp = love.Created
|
||||||
|
importResult.ImportCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if love.ReleaseMbid != "" {
|
|
||||||
extension.ReleaseIdentifier = "https://musicbrainz.org/release/" + string(love.ReleaseMbid)
|
|
||||||
}
|
|
||||||
|
|
||||||
track := Track{
|
|
||||||
Title: love.TrackName,
|
|
||||||
Album: love.ReleaseName,
|
|
||||||
Creator: love.ArtistName(),
|
|
||||||
TrackNum: love.TrackNumber,
|
|
||||||
Extension: map[string]any{
|
|
||||||
"https://musicbrainz.org/doc/jspf#track": extension,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if love.RecordingMbid != "" {
|
|
||||||
track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(love.RecordingMbid))
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks = append(tracks, track)
|
|
||||||
|
|
||||||
result.UpdateTimestamp(love.Created)
|
|
||||||
result.ImportCount += 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.writeJspf(tracks)
|
err := b.writeJspf(tracks)
|
||||||
return result, err
|
if err != nil {
|
||||||
|
importResult.UpdateTimestamp(oldestTimestamp)
|
||||||
|
importResult.ImportCount = len(tracks)
|
||||||
|
}
|
||||||
|
return importResult, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loveToTrack(love models.Love) Track {
|
||||||
|
extension := MusicBrainzTrackExtension{
|
||||||
|
AddedAt: love.Created,
|
||||||
|
AddedBy: love.UserName,
|
||||||
|
AdditionalMetadata: love.AdditionalInfo,
|
||||||
|
ArtistIdentifiers: make([]string, len(love.ArtistMbids)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, mbid := range love.ArtistMbids {
|
||||||
|
extension.ArtistIdentifiers[i] = "https://musicbrainz.org/artist/" + string(mbid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if love.ReleaseMbid != "" {
|
||||||
|
extension.ReleaseIdentifier = "https://musicbrainz.org/release/" + string(love.ReleaseMbid)
|
||||||
|
}
|
||||||
|
|
||||||
|
track := Track{
|
||||||
|
Title: love.TrackName,
|
||||||
|
Album: love.ReleaseName,
|
||||||
|
Creator: love.ArtistName(),
|
||||||
|
TrackNum: love.TrackNumber,
|
||||||
|
Extension: map[string]any{
|
||||||
|
"https://musicbrainz.org/doc/jspf#track": extension,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if love.RecordingMbid != "" {
|
||||||
|
track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(love.RecordingMbid))
|
||||||
|
}
|
||||||
|
|
||||||
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b JspfBackend) writeJspf(tracks []Track) error {
|
func (b JspfBackend) writeJspf(tracks []Track) error {
|
||||||
|
|
|
@ -23,7 +23,7 @@ package listenbrainz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -42,16 +42,21 @@ func (b ListenBrainzApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time) ([]models.Listen, error) {
|
func (b ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||||
maxTime := time.Now()
|
maxTime := time.Now()
|
||||||
minTime := time.Unix(0, 0)
|
minTime := time.Unix(0, 0)
|
||||||
listens := make([]models.Listen, 0, 2*MaxItemsPerGet)
|
|
||||||
|
defer close(results)
|
||||||
|
|
||||||
|
// FIXME: Optimize by fetching the listens in reverse listen time order
|
||||||
|
listens := make(models.ListensList, 0, 2*MaxItemsPerGet)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetListens(b.username, maxTime, minTime)
|
result, err := b.client.GetListens(b.username, maxTime, minTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.ListensResult{Error: err}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count := len(result.Payload.Listens)
|
count := len(result.Payload.Listens)
|
||||||
|
@ -73,19 +78,21 @@ out:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Reverse(listens)
|
sort.Sort(listens)
|
||||||
return listens, nil
|
results <- models.ListensResult{Listens: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time) ([]models.Love, error) {
|
func (b ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||||
offset := 0
|
offset := 0
|
||||||
loves := make([]models.Love, 0, 2*MaxItemsPerGet)
|
defer close(results)
|
||||||
|
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetFeedback(b.username, 1, offset)
|
result, err := b.client.GetFeedback(b.username, 1, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.LovesResult{Error: err}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count := len(result.Feedback)
|
count := len(result.Feedback)
|
||||||
|
@ -105,69 +112,79 @@ out:
|
||||||
offset += MaxItemsPerGet
|
offset += MaxItemsPerGet
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Reverse(loves)
|
sort.Sort(loves)
|
||||||
return loves, nil
|
results <- models.LovesResult{Loves: loves}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b ListenBrainzApiBackend) ImportLoves(loves []models.Love, oldestTimestamp time.Time) (models.ImportResult, error) {
|
func (b ListenBrainzApiBackend) ImportLoves(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||||
result := models.ImportResult{
|
importResult := models.ImportResult{
|
||||||
TotalCount: len(loves),
|
|
||||||
ImportCount: 0,
|
|
||||||
LastTimestamp: oldestTimestamp,
|
LastTimestamp: oldestTimestamp,
|
||||||
ImportErrors: make([]string, 0),
|
ImportErrors: make([]string, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
existingLoves, err := b.ExportLoves(time.Unix(0, 0))
|
existingLovesChan := make(chan models.LovesResult)
|
||||||
if err != nil {
|
go b.ExportLoves(time.Unix(0, 0), existingLovesChan)
|
||||||
return result, err
|
existingLoves := <-existingLovesChan
|
||||||
|
if existingLoves.Error != nil {
|
||||||
|
results <- models.LovesResult{Error: existingLoves.Error}
|
||||||
|
close(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
existingMbids := make(map[string]bool, len(existingLoves))
|
existingMbids := make(map[string]bool, len(existingLoves.Loves))
|
||||||
for _, love := range existingLoves {
|
for _, love := range existingLoves.Loves {
|
||||||
existingMbids[string(love.RecordingMbid)] = true
|
existingMbids[string(love.RecordingMbid)] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, love := range loves {
|
for result := range results {
|
||||||
if love.Created.Unix() <= oldestTimestamp.Unix() {
|
if result.Error != nil {
|
||||||
continue
|
return importResult, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
recordingMbid := string(love.RecordingMbid)
|
importResult.TotalCount += len(result.Loves)
|
||||||
|
|
||||||
if recordingMbid == "" {
|
for _, love := range result.Loves {
|
||||||
lookup, err := b.client.Lookup(love.TrackName, love.ArtistName())
|
if love.Created.Unix() <= oldestTimestamp.Unix() {
|
||||||
if err == nil {
|
continue
|
||||||
recordingMbid = lookup.RecordingMbid
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if recordingMbid != "" {
|
recordingMbid := string(love.RecordingMbid)
|
||||||
ok := false
|
|
||||||
errMsg := ""
|
if recordingMbid == "" {
|
||||||
if existingMbids[recordingMbid] {
|
lookup, err := b.client.Lookup(love.TrackName, love.ArtistName())
|
||||||
ok = true
|
if err == nil {
|
||||||
} else {
|
recordingMbid = lookup.RecordingMbid
|
||||||
resp, err := b.client.SendFeedback(Feedback{
|
|
||||||
RecordingMbid: recordingMbid,
|
|
||||||
Score: 1,
|
|
||||||
})
|
|
||||||
ok = err == nil && resp.Status == "ok"
|
|
||||||
if err != nil {
|
|
||||||
errMsg = err.Error()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if recordingMbid != "" {
|
||||||
result.UpdateTimestamp(love.Created)
|
ok := false
|
||||||
result.ImportCount += 1
|
errMsg := ""
|
||||||
} else {
|
if existingMbids[recordingMbid] {
|
||||||
msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v",
|
ok = true
|
||||||
love.TrackName, love.ArtistName(), errMsg)
|
} else {
|
||||||
result.ImportErrors = append(result.ImportErrors, msg)
|
resp, err := b.client.SendFeedback(Feedback{
|
||||||
|
RecordingMbid: recordingMbid,
|
||||||
|
Score: 1,
|
||||||
|
})
|
||||||
|
ok = err == nil && resp.Status == "ok"
|
||||||
|
if err != nil {
|
||||||
|
errMsg = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
importResult.UpdateTimestamp(love.Created)
|
||||||
|
importResult.ImportCount += 1
|
||||||
|
} else {
|
||||||
|
msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v",
|
||||||
|
love.TrackName, love.ArtistName(), errMsg)
|
||||||
|
importResult.ImportErrors = append(importResult.ImportErrors, msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
|
||||||
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lbListen Listen) ToListen() models.Listen {
|
func (lbListen Listen) ToListen() models.Listen {
|
||||||
|
|
|
@ -28,6 +28,8 @@ import (
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MaxItemsPerGet = 1000
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
HttpClient *resty.Client
|
HttpClient *resty.Client
|
||||||
token string
|
token string
|
||||||
|
|
|
@ -23,7 +23,7 @@ package maloja
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"slices"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -45,17 +45,21 @@ func (b MalojaApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b MalojaApiBackend) ExportListens(oldestTimestamp time.Time) ([]models.Listen, error) {
|
func (b MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||||
page := 0
|
page := 0
|
||||||
perPage := 1000
|
perPage := MaxItemsPerGet
|
||||||
|
|
||||||
listens := make([]models.Listen, 0)
|
defer close(results)
|
||||||
|
|
||||||
|
// We need to gather the full list of listens in order to sort them
|
||||||
|
listens := make(models.ListensList, 0, 2*perPage)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetScrobbles(page, perPage)
|
result, err := b.client.GetScrobbles(page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.ListensResult{Error: err}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count := len(result.List)
|
count := len(result.List)
|
||||||
|
@ -74,44 +78,49 @@ out:
|
||||||
page += 1
|
page += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Reverse(listens)
|
sort.Sort(listens)
|
||||||
return listens, nil
|
results <- models.ListensResult{Listens: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b MalojaApiBackend) ImportListens(listens []models.Listen, oldestTimestamp time.Time) (models.ImportResult, error) {
|
func (b MalojaApiBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||||
result := models.ImportResult{
|
importResult := models.ImportResult{
|
||||||
TotalCount: len(listens),
|
|
||||||
ImportCount: 0,
|
|
||||||
LastTimestamp: oldestTimestamp,
|
LastTimestamp: oldestTimestamp,
|
||||||
}
|
}
|
||||||
for _, listen := range listens {
|
|
||||||
if listen.ListenedAt.Unix() <= oldestTimestamp.Unix() {
|
for result := range results {
|
||||||
continue
|
if result.Error != nil {
|
||||||
|
return importResult, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
scrobble := NewScrobble{
|
importResult.TotalCount += len(result.Listens)
|
||||||
Title: listen.TrackName,
|
for _, listen := range result.Listens {
|
||||||
Artists: listen.ArtistNames,
|
if listen.ListenedAt.Unix() <= oldestTimestamp.Unix() {
|
||||||
Album: listen.ReleaseName,
|
break
|
||||||
Duration: int64(listen.PlaybackDuration.Seconds()),
|
}
|
||||||
Length: int64(listen.Duration.Seconds()),
|
|
||||||
Time: listen.ListenedAt.Unix(),
|
|
||||||
Nofix: b.nofix,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := b.client.NewScrobble(scrobble)
|
scrobble := NewScrobble{
|
||||||
if err != nil {
|
Title: listen.TrackName,
|
||||||
return result, err
|
Artists: listen.ArtistNames,
|
||||||
} else if resp.Status != "success" {
|
Album: listen.ReleaseName,
|
||||||
return result, errors.New(resp.Error.Description)
|
Duration: int64(listen.PlaybackDuration.Seconds()),
|
||||||
}
|
Length: int64(listen.Duration.Seconds()),
|
||||||
|
Time: listen.ListenedAt.Unix(),
|
||||||
|
Nofix: b.nofix,
|
||||||
|
}
|
||||||
|
|
||||||
if listen.ListenedAt.Unix() > result.LastTimestamp.Unix() {
|
resp, err := b.client.NewScrobble(scrobble)
|
||||||
result.LastTimestamp = listen.ListenedAt
|
if err != nil {
|
||||||
|
return importResult, err
|
||||||
|
} else if resp.Status != "success" {
|
||||||
|
return importResult, errors.New(resp.Error.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
importResult.UpdateTimestamp(listen.ListenedAt)
|
||||||
|
importResult.ImportCount += 1
|
||||||
}
|
}
|
||||||
result.ImportCount += 1
|
|
||||||
}
|
}
|
||||||
return result, nil
|
|
||||||
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Scrobble) ToListen() models.Listen {
|
func (s Scrobble) ToListen() models.Listen {
|
||||||
|
|
|
@ -37,12 +37,12 @@ import (
|
||||||
type ScrobblerLog struct {
|
type ScrobblerLog struct {
|
||||||
Timezone string
|
Timezone string
|
||||||
Client string
|
Client string
|
||||||
Listens []models.Listen
|
Listens models.ListensList
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
||||||
result := ScrobblerLog{
|
result := ScrobblerLog{
|
||||||
Listens: make([]models.Listen, 0),
|
Listens: make(models.ListensList, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(data)
|
reader := bufio.NewReader(data)
|
||||||
|
@ -92,16 +92,11 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Write(data io.Writer, log *ScrobblerLog) (lastTimestamp time.Time, err error) {
|
func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) {
|
||||||
err = writeHeader(data, log)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tsvWriter := csv.NewWriter(data)
|
tsvWriter := csv.NewWriter(data)
|
||||||
tsvWriter.Comma = '\t'
|
tsvWriter.Comma = '\t'
|
||||||
|
|
||||||
for _, listen := range log.Listens {
|
for _, listen := range listens {
|
||||||
if listen.ListenedAt.Unix() > lastTimestamp.Unix() {
|
if listen.ListenedAt.Unix() > lastTimestamp.Unix() {
|
||||||
lastTimestamp = listen.ListenedAt
|
lastTimestamp = listen.ListenedAt
|
||||||
}
|
}
|
||||||
|
@ -162,7 +157,7 @@ func readHeader(reader *bufio.Reader, log *ScrobblerLog) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeHeader(writer io.Writer, log *ScrobblerLog) error {
|
func WriteHeader(writer io.Writer, log *ScrobblerLog) error {
|
||||||
headers := []string{
|
headers := []string{
|
||||||
"#AUDIOSCROBBLER/1.1\n",
|
"#AUDIOSCROBBLER/1.1\n",
|
||||||
"#TZ/" + log.Timezone + "\n",
|
"#TZ/" + log.Timezone + "\n",
|
||||||
|
|
|
@ -98,7 +98,9 @@ func TestWrite(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
lastTimestamp, err := scrobblerlog.Write(buffer, &log)
|
err := scrobblerlog.WriteHeader(buffer, &log)
|
||||||
|
require.NoError(t, err)
|
||||||
|
lastTimestamp, err := scrobblerlog.Write(buffer, log.Listens)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
result := string(buffer.Bytes())
|
result := string(buffer.Bytes())
|
||||||
lines := strings.Split(result, "\n")
|
lines := strings.Split(result, "\n")
|
||||||
|
|
|
@ -23,6 +23,7 @@ package scrobblerlog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -40,31 +41,36 @@ func (b ScrobblerLogBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time) ([]models.Listen, error) {
|
func (b ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||||
|
defer close(results)
|
||||||
file, err := os.Open(b.filePath)
|
file, err := os.Open(b.filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.ListensResult{Error: err}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
result, err := Parse(file, b.includeSkipped)
|
log, err := Parse(file, b.includeSkipped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
results <- models.ListensResult{Error: err}
|
||||||
|
close(results)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Listens, nil
|
listens := log.Listens.NewerThan(oldestTimestamp)
|
||||||
|
sort.Sort(listens)
|
||||||
|
results <- models.ListensResult{Listens: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b ScrobblerLogBackend) ImportListens(listens []models.Listen, oldestTimestamp time.Time) (models.ImportResult, error) {
|
func (b ScrobblerLogBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||||
result := models.ImportResult{
|
importResult := models.ImportResult{
|
||||||
TotalCount: len(listens),
|
|
||||||
LastTimestamp: oldestTimestamp,
|
LastTimestamp: oldestTimestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(b.filePath)
|
file, err := os.Create(b.filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return importResult, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
@ -72,16 +78,27 @@ func (b ScrobblerLogBackend) ImportListens(listens []models.Listen, oldestTimest
|
||||||
log := ScrobblerLog{
|
log := ScrobblerLog{
|
||||||
Timezone: "UNKNOWN",
|
Timezone: "UNKNOWN",
|
||||||
Client: "Rockbox unknown $Revision$",
|
Client: "Rockbox unknown $Revision$",
|
||||||
Listens: listens,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTimestamp, err := Write(file, &log)
|
err = WriteHeader(file, &log)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return importResult, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result.LastTimestamp = lastTimestamp
|
for result := range results {
|
||||||
result.ImportCount = len(listens)
|
if result.Error != nil {
|
||||||
return result, nil
|
return importResult, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
importResult.TotalCount += len(result.Listens)
|
||||||
|
lastTimestamp, err := Write(file, result.Listens)
|
||||||
|
if err != nil {
|
||||||
|
return importResult, err
|
||||||
|
}
|
||||||
|
|
||||||
|
importResult.UpdateTimestamp(lastTimestamp)
|
||||||
|
importResult.ImportCount += len(result.Listens)
|
||||||
|
}
|
||||||
|
|
||||||
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,23 +48,20 @@ func (b SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
func (b SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||||
|
defer close(results)
|
||||||
err := b.client.Authenticate(b.password)
|
err := b.client.Authenticate(b.password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results <- models.LovesResult{Error: err}
|
results <- models.LovesResult{Error: err}
|
||||||
close(results)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
starred, err := b.client.GetStarred2(map[string]string{})
|
starred, err := b.client.GetStarred2(map[string]string{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results <- models.LovesResult{Error: err}
|
results <- models.LovesResult{Error: err}
|
||||||
close(results)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results <- models.LovesResult{Loves: b.filterSongs(starred.Song, oldestTimestamp)}
|
results <- models.LovesResult{Loves: b.filterSongs(starred.Song, oldestTimestamp)}
|
||||||
close(results)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList {
|
func (b SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList {
|
||||||
|
|
|
@ -67,6 +67,17 @@ type Love struct {
|
||||||
|
|
||||||
type ListensList []Listen
|
type ListensList []Listen
|
||||||
|
|
||||||
|
// Returns a new ListensList with only elements that are newer than t.
|
||||||
|
func (l ListensList) NewerThan(t time.Time) ListensList {
|
||||||
|
result := make(ListensList, 0, len(l))
|
||||||
|
for _, item := range l {
|
||||||
|
if item.ListenedAt.Unix() > t.Unix() {
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (l ListensList) Len() int {
|
func (l ListensList) Len() int {
|
||||||
return len(l)
|
return len(l)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"go.uploadedlobster.com/scotty/models"
|
"go.uploadedlobster.com/scotty/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ func TestTrackArtistName(t *testing.T) {
|
||||||
assert.Equal(t, "Foo, Bar, Baz", track.ArtistName())
|
assert.Equal(t, "Foo, Bar, Baz", track.ArtistName())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListensListSort(t *testing.T) {
|
func TestListensListNewerThan(t *testing.T) {
|
||||||
listen1 := models.Listen{ListenedAt: time.Unix(3, 0)}
|
listen1 := models.Listen{ListenedAt: time.Unix(3, 0)}
|
||||||
listen2 := models.Listen{ListenedAt: time.Unix(0, 0)}
|
listen2 := models.Listen{ListenedAt: time.Unix(0, 0)}
|
||||||
listen3 := models.Listen{ListenedAt: time.Unix(2, 0)}
|
listen3 := models.Listen{ListenedAt: time.Unix(2, 0)}
|
||||||
|
@ -52,6 +53,19 @@ func TestListensListSort(t *testing.T) {
|
||||||
assert.Equal(t, listen3, list[1])
|
assert.Equal(t, listen3, list[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListensListSort(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
listen1 := models.Listen{UserName: "l1", ListenedAt: now.Add(-1 * time.Hour)}
|
||||||
|
listen2 := models.Listen{UserName: "l2", ListenedAt: now}
|
||||||
|
listen3 := models.Listen{UserName: "l3", ListenedAt: now.Add(1 * time.Hour)}
|
||||||
|
listen4 := models.Listen{UserName: "l4", ListenedAt: now.Add(2 * time.Hour)}
|
||||||
|
list := models.ListensList{listen1, listen2, listen3, listen4}
|
||||||
|
newList := list.NewerThan(now)
|
||||||
|
require.Len(t, newList, 2)
|
||||||
|
assert.Equal(t, listen3, newList[0])
|
||||||
|
assert.Equal(t, listen4, newList[1])
|
||||||
|
}
|
||||||
|
|
||||||
func TestLovesListSort(t *testing.T) {
|
func TestLovesListSort(t *testing.T) {
|
||||||
love1 := models.Love{Created: time.Unix(3, 0)}
|
love1 := models.Love{Created: time.Unix(3, 0)}
|
||||||
love2 := models.Love{Created: time.Unix(0, 0)}
|
love2 := models.Love{Created: time.Unix(0, 0)}
|
||||||
|
|
Loading…
Add table
Reference in a new issue