mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-30 05:37:05 +02:00
scrobblerlog: Use specific Record type
This makes the interface more generic and easier to reuse in other projects.
This commit is contained in:
parent
aeb3a56982
commit
aad542850a
3 changed files with 132 additions and 76 deletions
|
@ -20,6 +20,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uploadedlobster.com/scotty/internal/config"
|
"go.uploadedlobster.com/scotty/internal/config"
|
||||||
|
@ -132,14 +133,22 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
listens := b.log.Listens.NewerThan(oldestTimestamp)
|
listens := make(models.ListensList, 0, len(b.log.Records))
|
||||||
sort.Sort(listens)
|
client := strings.Split(b.log.Client, " ")[0]
|
||||||
|
for _, record := range b.log.Records {
|
||||||
|
listens = append(listens, recordToListen(record, client))
|
||||||
|
}
|
||||||
|
sort.Sort(listens.NewerThan(oldestTimestamp))
|
||||||
progress <- models.Progress{Elapsed: int64(len(listens))}.Complete()
|
progress <- models.Progress{Elapsed: int64(len(listens))}.Complete()
|
||||||
results <- models.ListensResult{Items: listens}
|
results <- models.ListensResult{Items: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
||||||
lastTimestamp, err := b.log.Append(b.file, export.Items)
|
records := make([]scrobblerlog.Record, len(export.Items))
|
||||||
|
for i, listen := range export.Items {
|
||||||
|
records[i] = listenToRecord(listen)
|
||||||
|
}
|
||||||
|
lastTimestamp, err := b.log.Append(b.file, records)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return importResult, err
|
return importResult, err
|
||||||
}
|
}
|
||||||
|
@ -150,3 +159,42 @@ func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importR
|
||||||
|
|
||||||
return importResult, nil
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordToListen(record scrobblerlog.Record, client string) models.Listen {
|
||||||
|
return models.Listen{
|
||||||
|
ListenedAt: record.Timestamp,
|
||||||
|
Track: models.Track{
|
||||||
|
ArtistNames: []string{record.ArtistName},
|
||||||
|
ReleaseName: record.AlbumName,
|
||||||
|
TrackName: record.TrackName,
|
||||||
|
TrackNumber: record.TrackNumber,
|
||||||
|
Duration: record.Duration,
|
||||||
|
RecordingMBID: record.MusicBrainzRecordingID,
|
||||||
|
AdditionalInfo: models.AdditionalInfo{
|
||||||
|
"rockbox_rating": record.Rating,
|
||||||
|
"media_player": client,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
rating = scrobblerlog.Rating(rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
return scrobblerlog.Record{
|
||||||
|
ArtistName: listen.ArtistName(),
|
||||||
|
AlbumName: listen.ReleaseName,
|
||||||
|
TrackName: listen.TrackName,
|
||||||
|
TrackNumber: listen.TrackNumber,
|
||||||
|
Duration: listen.Duration,
|
||||||
|
Rating: rating,
|
||||||
|
Timestamp: listen.ListenedAt,
|
||||||
|
MusicBrainzRecordingID: listen.RecordingMBID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uploadedlobster.com/mbtypes"
|
"go.uploadedlobster.com/mbtypes"
|
||||||
"go.uploadedlobster.com/scotty/internal/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TZInfo is the timezone information in the header of the scrobbler log file.
|
// TZInfo is the timezone information in the header of the scrobbler log file.
|
||||||
|
@ -50,16 +49,36 @@ const (
|
||||||
TZ_UTC TZInfo = "UTC"
|
TZ_UTC TZInfo = "UTC"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// L if listened at least 50% or S if skipped
|
||||||
|
type Rating string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RATING_LISTENED Rating = "L"
|
||||||
|
RATING_SKIPPED Rating = "S"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A single entry of a track in the scrobbler log file.
|
||||||
|
type Record struct {
|
||||||
|
ArtistName string
|
||||||
|
AlbumName string
|
||||||
|
TrackName string
|
||||||
|
TrackNumber int
|
||||||
|
Duration time.Duration
|
||||||
|
Rating Rating
|
||||||
|
Timestamp time.Time
|
||||||
|
MusicBrainzRecordingID mbtypes.MBID
|
||||||
|
}
|
||||||
|
|
||||||
// Represents a scrobbler log file.
|
// Represents a scrobbler log file.
|
||||||
type ScrobblerLog struct {
|
type ScrobblerLog struct {
|
||||||
TZ TZInfo
|
TZ TZInfo
|
||||||
Client string
|
Client string
|
||||||
Listens models.ListensList
|
Records []Record
|
||||||
location *time.Location
|
location *time.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
||||||
l.Listens = make(models.ListensList, 0)
|
l.Records = make([]Record, 0)
|
||||||
|
|
||||||
reader := bufio.NewReader(data)
|
reader := bufio.NewReader(data)
|
||||||
err := l.ReadHeader(reader)
|
err := l.ReadHeader(reader)
|
||||||
|
@ -95,41 +114,37 @@ func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
listen, err := l.rowToListen(row)
|
record, err := l.rowToRecord(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Listens = append(l.Listens, listen)
|
l.Records = append(l.Records, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ScrobblerLog) Append(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) {
|
func (l *ScrobblerLog) Append(data io.Writer, records []Record) (lastTimestamp time.Time, err error) {
|
||||||
tsvWriter := csv.NewWriter(data)
|
tsvWriter := csv.NewWriter(data)
|
||||||
tsvWriter.Comma = '\t'
|
tsvWriter.Comma = '\t'
|
||||||
|
|
||||||
for _, listen := range listens {
|
for _, record := range records {
|
||||||
if listen.ListenedAt.Unix() > lastTimestamp.Unix() {
|
if record.Timestamp.After(lastTimestamp) {
|
||||||
lastTimestamp = listen.ListenedAt
|
lastTimestamp = record.Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// A row is:
|
// A row is:
|
||||||
// artistName releaseName trackName trackNumber duration rating timestamp recordingMBID
|
// artistName releaseName trackName trackNumber duration rating timestamp recordingMBID
|
||||||
rating, ok := listen.AdditionalInfo["rockbox_rating"].(string)
|
|
||||||
if !ok || rating == "" {
|
|
||||||
rating = "L"
|
|
||||||
}
|
|
||||||
err = tsvWriter.Write([]string{
|
err = tsvWriter.Write([]string{
|
||||||
listen.ArtistName(),
|
record.ArtistName,
|
||||||
listen.ReleaseName,
|
record.AlbumName,
|
||||||
listen.TrackName,
|
record.TrackName,
|
||||||
strconv.Itoa(listen.TrackNumber),
|
strconv.Itoa(record.TrackNumber),
|
||||||
strconv.Itoa(int(listen.Duration.Seconds())),
|
strconv.Itoa(int(record.Duration.Seconds())),
|
||||||
rating,
|
string(record.Rating),
|
||||||
strconv.Itoa(int(listen.ListenedAt.Unix())),
|
strconv.FormatInt(record.Timestamp.Unix(), 10),
|
||||||
string(listen.RecordingMBID),
|
string(record.MusicBrainzRecordingID),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,44 +206,38 @@ func (l *ScrobblerLog) WriteHeader(writer io.Writer) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) {
|
func (l ScrobblerLog) rowToRecord(row []string) (Record, error) {
|
||||||
var listen models.Listen
|
var record Record
|
||||||
trackNumber, err := strconv.Atoi(row[3])
|
trackNumber, err := strconv.Atoi(row[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return listen, err
|
return record, err
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := strconv.Atoi(row[4])
|
duration, err := strconv.Atoi(row[4])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return listen, err
|
return record, err
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp, err := strconv.ParseInt(row[6], 10, 64)
|
timestamp, err := strconv.ParseInt(row[6], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return listen, err
|
return record, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := strings.Split(l.Client, " ")[0]
|
record = Record{
|
||||||
listen = models.Listen{
|
ArtistName: row[0],
|
||||||
Track: models.Track{
|
AlbumName: row[1],
|
||||||
ArtistNames: []string{row[0]},
|
|
||||||
ReleaseName: row[1],
|
|
||||||
TrackName: row[2],
|
TrackName: row[2],
|
||||||
TrackNumber: trackNumber,
|
TrackNumber: trackNumber,
|
||||||
Duration: time.Duration(duration * int(time.Second)),
|
Duration: time.Duration(duration) * time.Second,
|
||||||
AdditionalInfo: models.AdditionalInfo{
|
Rating: Rating(row[5]),
|
||||||
"rockbox_rating": row[5],
|
Timestamp: timeFromLocalTimestamp(timestamp, l.location),
|
||||||
"media_player": client,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ListenedAt: timeFromLocalTimestamp(timestamp, l.location),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(row) > 7 {
|
if len(row) > 7 {
|
||||||
listen.Track.RecordingMBID = mbtypes.MBID(row[7])
|
record.MusicBrainzRecordingID = mbtypes.MBID(row[7])
|
||||||
}
|
}
|
||||||
|
|
||||||
return listen, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the timezone string from the header to a time.Location.
|
// Convert the timezone string from the header to a time.Location.
|
||||||
|
|
|
@ -31,7 +31,6 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uploadedlobster.com/mbtypes"
|
"go.uploadedlobster.com/mbtypes"
|
||||||
"go.uploadedlobster.com/scotty/internal/models"
|
|
||||||
"go.uploadedlobster.com/scotty/pkg/scrobblerlog"
|
"go.uploadedlobster.com/scotty/pkg/scrobblerlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,19 +52,20 @@ func TestParser(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ)
|
assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ)
|
||||||
assert.Equal("Rockbox sansaclipplus $Revision$", result.Client)
|
assert.Equal("Rockbox sansaclipplus $Revision$", result.Client)
|
||||||
assert.Len(result.Listens, 5)
|
assert.Len(result.Records, 5)
|
||||||
listen1 := result.Listens[0]
|
record1 := result.Records[0]
|
||||||
assert.Equal("Özcan Deniz", listen1.ArtistName())
|
assert.Equal("Özcan Deniz", record1.ArtistName)
|
||||||
assert.Equal("Ses ve Ayrilik", listen1.ReleaseName)
|
assert.Equal("Ses ve Ayrilik", record1.AlbumName)
|
||||||
assert.Equal("Sevdanin rengi (sipacik) byMrTurkey", listen1.TrackName)
|
assert.Equal("Sevdanin rengi (sipacik) byMrTurkey", record1.TrackName)
|
||||||
assert.Equal(5, listen1.TrackNumber)
|
assert.Equal(5, record1.TrackNumber)
|
||||||
assert.Equal(time.Duration(306*time.Second), listen1.Duration)
|
assert.Equal(time.Duration(306*time.Second), record1.Duration)
|
||||||
assert.Equal("L", listen1.AdditionalInfo["rockbox_rating"])
|
assert.Equal(scrobblerlog.RATING_LISTENED, record1.Rating)
|
||||||
assert.Equal(time.Unix(1260342084, 0), listen1.ListenedAt)
|
assert.Equal(time.Unix(1260342084, 0), record1.Timestamp)
|
||||||
assert.Equal(mbtypes.MBID(""), listen1.RecordingMBID)
|
assert.Equal(mbtypes.MBID(""), record1.MusicBrainzRecordingID)
|
||||||
listen4 := result.Listens[3]
|
record4 := result.Records[3]
|
||||||
assert.Equal("S", listen4.AdditionalInfo["rockbox_rating"])
|
assert.Equal(scrobblerlog.RATING_SKIPPED, record4.Rating)
|
||||||
assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMBID)
|
assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"),
|
||||||
|
record4.MusicBrainzRecordingID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParserExcludeSkipped(t *testing.T) {
|
func TestParserExcludeSkipped(t *testing.T) {
|
||||||
|
@ -74,10 +74,11 @@ func TestParserExcludeSkipped(t *testing.T) {
|
||||||
result := scrobblerlog.ScrobblerLog{}
|
result := scrobblerlog.ScrobblerLog{}
|
||||||
err := result.Parse(data, false)
|
err := result.Parse(data, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(result.Listens, 4)
|
assert.Len(result.Records, 4)
|
||||||
listen4 := result.Listens[3]
|
record4 := result.Records[3]
|
||||||
assert.Equal("L", listen4.AdditionalInfo["rockbox_rating"])
|
assert.Equal(scrobblerlog.RATING_LISTENED, record4.Rating)
|
||||||
assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMBID)
|
assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"),
|
||||||
|
record4.MusicBrainzRecordingID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppend(t *testing.T) {
|
func TestAppend(t *testing.T) {
|
||||||
|
@ -88,23 +89,21 @@ func TestAppend(t *testing.T) {
|
||||||
TZ: scrobblerlog.TZ_UNKNOWN,
|
TZ: scrobblerlog.TZ_UNKNOWN,
|
||||||
Client: "Rockbox foo $Revision$",
|
Client: "Rockbox foo $Revision$",
|
||||||
}
|
}
|
||||||
listens := []models.Listen{
|
records := []scrobblerlog.Record{
|
||||||
{
|
{
|
||||||
ListenedAt: time.Unix(1699572072, 0),
|
ArtistName: "Prinzhorn Dance School",
|
||||||
Track: models.Track{
|
AlbumName: "Home Economics",
|
||||||
ArtistNames: []string{"Prinzhorn Dance School"},
|
|
||||||
ReleaseName: "Home Economics",
|
|
||||||
TrackName: "Reign",
|
TrackName: "Reign",
|
||||||
TrackNumber: 1,
|
TrackNumber: 1,
|
||||||
Duration: 271 * time.Second,
|
Duration: 271 * time.Second,
|
||||||
RecordingMBID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"),
|
Rating: scrobblerlog.RATING_LISTENED,
|
||||||
AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"},
|
Timestamp: time.Unix(1699572072, 0),
|
||||||
},
|
MusicBrainzRecordingID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := log.WriteHeader(buffer)
|
err := log.WriteHeader(buffer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
lastTimestamp, err := log.Append(buffer, listens)
|
lastTimestamp, err := log.Append(buffer, records)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
result := buffer.String()
|
result := buffer.String()
|
||||||
lines := strings.Split(result, "\n")
|
lines := strings.Split(result, "\n")
|
||||||
|
@ -127,5 +126,5 @@ func TestReadHeader(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN)
|
assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN)
|
||||||
assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$")
|
assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$")
|
||||||
assert.Empty(t, log.Listens)
|
assert.Empty(t, log.Records)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue