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"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uploadedlobster.com/scotty/internal/config"
|
||||
|
@ -132,14 +133,22 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c
|
|||
return
|
||||
}
|
||||
|
||||
listens := b.log.Listens.NewerThan(oldestTimestamp)
|
||||
sort.Sort(listens)
|
||||
listens := make(models.ListensList, 0, len(b.log.Records))
|
||||
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()
|
||||
results <- models.ListensResult{Items: listens}
|
||||
}
|
||||
|
||||
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 {
|
||||
return importResult, err
|
||||
}
|
||||
|
@ -150,3 +159,42 @@ func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importR
|
|||
|
||||
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"
|
||||
|
||||
"go.uploadedlobster.com/mbtypes"
|
||||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
)
|
||||
|
||||
// TZInfo is the timezone information in the header of the scrobbler log file.
|
||||
|
@ -50,16 +49,36 @@ const (
|
|||
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.
|
||||
type ScrobblerLog struct {
|
||||
TZ TZInfo
|
||||
Client string
|
||||
Listens models.ListensList
|
||||
Records []Record
|
||||
location *time.Location
|
||||
}
|
||||
|
||||
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)
|
||||
err := l.ReadHeader(reader)
|
||||
|
@ -95,41 +114,37 @@ func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
|||
continue
|
||||
}
|
||||
|
||||
listen, err := l.rowToListen(row)
|
||||
record, err := l.rowToRecord(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Listens = append(l.Listens, listen)
|
||||
l.Records = append(l.Records, record)
|
||||
}
|
||||
|
||||
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.Comma = '\t'
|
||||
|
||||
for _, listen := range listens {
|
||||
if listen.ListenedAt.Unix() > lastTimestamp.Unix() {
|
||||
lastTimestamp = listen.ListenedAt
|
||||
for _, record := range records {
|
||||
if record.Timestamp.After(lastTimestamp) {
|
||||
lastTimestamp = record.Timestamp
|
||||
}
|
||||
|
||||
// A row is:
|
||||
// artistName releaseName trackName trackNumber duration rating timestamp recordingMBID
|
||||
rating, ok := listen.AdditionalInfo["rockbox_rating"].(string)
|
||||
if !ok || rating == "" {
|
||||
rating = "L"
|
||||
}
|
||||
err = tsvWriter.Write([]string{
|
||||
listen.ArtistName(),
|
||||
listen.ReleaseName,
|
||||
listen.TrackName,
|
||||
strconv.Itoa(listen.TrackNumber),
|
||||
strconv.Itoa(int(listen.Duration.Seconds())),
|
||||
rating,
|
||||
strconv.Itoa(int(listen.ListenedAt.Unix())),
|
||||
string(listen.RecordingMBID),
|
||||
record.ArtistName,
|
||||
record.AlbumName,
|
||||
record.TrackName,
|
||||
strconv.Itoa(record.TrackNumber),
|
||||
strconv.Itoa(int(record.Duration.Seconds())),
|
||||
string(record.Rating),
|
||||
strconv.FormatInt(record.Timestamp.Unix(), 10),
|
||||
string(record.MusicBrainzRecordingID),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -191,44 +206,38 @@ func (l *ScrobblerLog) WriteHeader(writer io.Writer) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) {
|
||||
var listen models.Listen
|
||||
func (l ScrobblerLog) rowToRecord(row []string) (Record, error) {
|
||||
var record Record
|
||||
trackNumber, err := strconv.Atoi(row[3])
|
||||
if err != nil {
|
||||
return listen, err
|
||||
return record, err
|
||||
}
|
||||
|
||||
duration, err := strconv.Atoi(row[4])
|
||||
if err != nil {
|
||||
return listen, err
|
||||
return record, err
|
||||
}
|
||||
|
||||
timestamp, err := strconv.ParseInt(row[6], 10, 64)
|
||||
if err != nil {
|
||||
return listen, err
|
||||
return record, err
|
||||
}
|
||||
|
||||
client := strings.Split(l.Client, " ")[0]
|
||||
listen = models.Listen{
|
||||
Track: models.Track{
|
||||
ArtistNames: []string{row[0]},
|
||||
ReleaseName: row[1],
|
||||
TrackName: row[2],
|
||||
TrackNumber: trackNumber,
|
||||
Duration: time.Duration(duration * int(time.Second)),
|
||||
AdditionalInfo: models.AdditionalInfo{
|
||||
"rockbox_rating": row[5],
|
||||
"media_player": client,
|
||||
},
|
||||
},
|
||||
ListenedAt: timeFromLocalTimestamp(timestamp, l.location),
|
||||
record = Record{
|
||||
ArtistName: row[0],
|
||||
AlbumName: row[1],
|
||||
TrackName: row[2],
|
||||
TrackNumber: trackNumber,
|
||||
Duration: time.Duration(duration) * time.Second,
|
||||
Rating: Rating(row[5]),
|
||||
Timestamp: timeFromLocalTimestamp(timestamp, l.location),
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
@ -31,7 +31,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/mbtypes"
|
||||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
"go.uploadedlobster.com/scotty/pkg/scrobblerlog"
|
||||
)
|
||||
|
||||
|
@ -53,19 +52,20 @@ func TestParser(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ)
|
||||
assert.Equal("Rockbox sansaclipplus $Revision$", result.Client)
|
||||
assert.Len(result.Listens, 5)
|
||||
listen1 := result.Listens[0]
|
||||
assert.Equal("Özcan Deniz", listen1.ArtistName())
|
||||
assert.Equal("Ses ve Ayrilik", listen1.ReleaseName)
|
||||
assert.Equal("Sevdanin rengi (sipacik) byMrTurkey", listen1.TrackName)
|
||||
assert.Equal(5, listen1.TrackNumber)
|
||||
assert.Equal(time.Duration(306*time.Second), listen1.Duration)
|
||||
assert.Equal("L", listen1.AdditionalInfo["rockbox_rating"])
|
||||
assert.Equal(time.Unix(1260342084, 0), listen1.ListenedAt)
|
||||
assert.Equal(mbtypes.MBID(""), listen1.RecordingMBID)
|
||||
listen4 := result.Listens[3]
|
||||
assert.Equal("S", listen4.AdditionalInfo["rockbox_rating"])
|
||||
assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMBID)
|
||||
assert.Len(result.Records, 5)
|
||||
record1 := result.Records[0]
|
||||
assert.Equal("Özcan Deniz", record1.ArtistName)
|
||||
assert.Equal("Ses ve Ayrilik", record1.AlbumName)
|
||||
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(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(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"),
|
||||
record4.MusicBrainzRecordingID)
|
||||
}
|
||||
|
||||
func TestParserExcludeSkipped(t *testing.T) {
|
||||
|
@ -74,10 +74,11 @@ func TestParserExcludeSkipped(t *testing.T) {
|
|||
result := scrobblerlog.ScrobblerLog{}
|
||||
err := result.Parse(data, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(result.Listens, 4)
|
||||
listen4 := result.Listens[3]
|
||||
assert.Equal("L", listen4.AdditionalInfo["rockbox_rating"])
|
||||
assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMBID)
|
||||
assert.Len(result.Records, 4)
|
||||
record4 := result.Records[3]
|
||||
assert.Equal(scrobblerlog.RATING_LISTENED, record4.Rating)
|
||||
assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"),
|
||||
record4.MusicBrainzRecordingID)
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
|
@ -88,23 +89,21 @@ func TestAppend(t *testing.T) {
|
|||
TZ: scrobblerlog.TZ_UNKNOWN,
|
||||
Client: "Rockbox foo $Revision$",
|
||||
}
|
||||
listens := []models.Listen{
|
||||
records := []scrobblerlog.Record{
|
||||
{
|
||||
ListenedAt: time.Unix(1699572072, 0),
|
||||
Track: models.Track{
|
||||
ArtistNames: []string{"Prinzhorn Dance School"},
|
||||
ReleaseName: "Home Economics",
|
||||
TrackName: "Reign",
|
||||
TrackNumber: 1,
|
||||
Duration: 271 * time.Second,
|
||||
RecordingMBID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"),
|
||||
AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"},
|
||||
},
|
||||
ArtistName: "Prinzhorn Dance School",
|
||||
AlbumName: "Home Economics",
|
||||
TrackName: "Reign",
|
||||
TrackNumber: 1,
|
||||
Duration: 271 * time.Second,
|
||||
Rating: scrobblerlog.RATING_LISTENED,
|
||||
Timestamp: time.Unix(1699572072, 0),
|
||||
MusicBrainzRecordingID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"),
|
||||
},
|
||||
}
|
||||
err := log.WriteHeader(buffer)
|
||||
require.NoError(t, err)
|
||||
lastTimestamp, err := log.Append(buffer, listens)
|
||||
lastTimestamp, err := log.Append(buffer, records)
|
||||
require.NoError(t, err)
|
||||
result := buffer.String()
|
||||
lines := strings.Split(result, "\n")
|
||||
|
@ -127,5 +126,5 @@ func TestReadHeader(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN)
|
||||
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