mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-29 21:27:05 +02:00
Moved scrobblerlog parsing to separate package
This commit is contained in:
parent
69665bc286
commit
aeb3a56982
3 changed files with 74 additions and 56 deletions
|
@ -25,6 +25,7 @@ import (
|
|||
"go.uploadedlobster.com/scotty/internal/config"
|
||||
"go.uploadedlobster.com/scotty/internal/i18n"
|
||||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
"go.uploadedlobster.com/scotty/pkg/scrobblerlog"
|
||||
)
|
||||
|
||||
type ScrobblerLogBackend struct {
|
||||
|
@ -32,7 +33,7 @@ type ScrobblerLogBackend struct {
|
|||
includeSkipped bool
|
||||
append bool
|
||||
file *os.File
|
||||
log ScrobblerLog
|
||||
log scrobblerlog.ScrobblerLog
|
||||
}
|
||||
|
||||
func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" }
|
||||
|
@ -58,9 +59,9 @@ func (b *ScrobblerLogBackend) FromConfig(config *config.ServiceConfig) models.Ba
|
|||
b.filePath = config.GetString("file-path")
|
||||
b.includeSkipped = config.GetBool("include-skipped", false)
|
||||
b.append = config.GetBool("append", true)
|
||||
b.log = ScrobblerLog{
|
||||
Timezone: "UNKNOWN",
|
||||
Client: "Rockbox unknown $Revision$",
|
||||
b.log = scrobblerlog.ScrobblerLog{
|
||||
TZ: scrobblerlog.TZ_UTC,
|
||||
Client: "Rockbox unknown $Revision$",
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ func (b *ScrobblerLogBackend) StartImport() error {
|
|||
} else {
|
||||
// Verify existing file is a scrobbler log
|
||||
reader := bufio.NewReader(file)
|
||||
if err = ReadHeader(reader, &b.log); err != nil {
|
||||
if err = b.log.ReadHeader(reader); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
@ -99,7 +100,7 @@ func (b *ScrobblerLogBackend) StartImport() error {
|
|||
}
|
||||
|
||||
if !b.append {
|
||||
if err = WriteHeader(file, &b.log); err != nil {
|
||||
if err = b.log.WriteHeader(file); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
@ -124,21 +125,21 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c
|
|||
|
||||
defer file.Close()
|
||||
|
||||
log, err := Parse(file, b.includeSkipped)
|
||||
err = b.log.Parse(file, b.includeSkipped)
|
||||
if err != nil {
|
||||
progress <- models.Progress{}.Complete()
|
||||
results <- models.ListensResult{Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
listens := log.Listens.NewerThan(oldestTimestamp)
|
||||
listens := b.log.Listens.NewerThan(oldestTimestamp)
|
||||
sort.Sort(listens)
|
||||
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 := Write(b.file, export.Items)
|
||||
lastTimestamp, err := b.log.Append(b.file, export.Items)
|
||||
if err != nil {
|
||||
return importResult, err
|
||||
}
|
||||
|
|
|
@ -19,6 +19,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Package to parse and writer .scrobbler.log files as written by Rockbox.
|
||||
//
|
||||
// See
|
||||
// - https://www.rockbox.org/wiki/LastFMLog
|
||||
// - https://git.rockbox.org/cgit/rockbox.git/tree/apps/plugins/lastfm_scrobbler.c
|
||||
package scrobblerlog
|
||||
|
||||
import (
|
||||
|
@ -34,22 +40,31 @@ import (
|
|||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
)
|
||||
|
||||
// TZInfo is the timezone information in the header of the scrobbler log file.
|
||||
// It can be "UTC" or "UNKNOWN", if the device writing the scrobbler log file
|
||||
// knows the time, but not the timezone.
|
||||
type TZInfo string
|
||||
|
||||
const (
|
||||
TZ_UNKNOWN TZInfo = "UNKNOWN"
|
||||
TZ_UTC TZInfo = "UTC"
|
||||
)
|
||||
|
||||
// Represents a scrobbler log file.
|
||||
type ScrobblerLog struct {
|
||||
Timezone string
|
||||
TZ TZInfo
|
||||
Client string
|
||||
Listens models.ListensList
|
||||
location *time.Location
|
||||
}
|
||||
|
||||
func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
||||
result := ScrobblerLog{
|
||||
Listens: make(models.ListensList, 0),
|
||||
}
|
||||
func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
||||
l.Listens = make(models.ListensList, 0)
|
||||
|
||||
reader := bufio.NewReader(data)
|
||||
err := ReadHeader(reader, &result)
|
||||
err := l.ReadHeader(reader)
|
||||
if err != nil {
|
||||
return result, err
|
||||
return err
|
||||
}
|
||||
|
||||
tsvReader := csv.NewReader(reader)
|
||||
|
@ -64,7 +79,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
|||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return result, err
|
||||
return err
|
||||
}
|
||||
|
||||
// fmt.Printf("row: %v\n", row)
|
||||
|
@ -72,7 +87,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
|||
// We consider only the last field (recording MBID) optional
|
||||
if len(row) < 7 {
|
||||
line, _ := tsvReader.FieldPos(0)
|
||||
return result, fmt.Errorf("invalid record in scrobblerlog line %v", line)
|
||||
return fmt.Errorf("invalid record in scrobblerlog line %v", line)
|
||||
}
|
||||
|
||||
rating := row[5]
|
||||
|
@ -80,18 +95,18 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
|
|||
continue
|
||||
}
|
||||
|
||||
listen, err := result.rowToListen(row)
|
||||
listen, err := l.rowToListen(row)
|
||||
if err != nil {
|
||||
return result, err
|
||||
return err
|
||||
}
|
||||
|
||||
result.Listens = append(result.Listens, listen)
|
||||
l.Listens = append(l.Listens, listen)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) {
|
||||
func (l *ScrobblerLog) Append(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) {
|
||||
tsvWriter := csv.NewWriter(data)
|
||||
tsvWriter.Comma = '\t'
|
||||
|
||||
|
@ -122,7 +137,7 @@ func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time,
|
|||
return
|
||||
}
|
||||
|
||||
func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error {
|
||||
func (l *ScrobblerLog) ReadHeader(reader *bufio.Reader) error {
|
||||
// Skip header
|
||||
for i := 0; i < 3; i++ {
|
||||
line, _, err := reader.ReadLine()
|
||||
|
@ -142,14 +157,14 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error {
|
|||
// the log knows the time, but not the timezone.
|
||||
timezone, found := strings.CutPrefix(text, "#TZ/")
|
||||
if found {
|
||||
log.Timezone = timezone
|
||||
log.location = locationFromTimezone(log.Timezone)
|
||||
l.TZ = TZInfo(timezone)
|
||||
l.location = locationFromTimezone(l.TZ)
|
||||
continue
|
||||
}
|
||||
|
||||
client, found := strings.CutPrefix(text, "#CLIENT/")
|
||||
if found {
|
||||
log.Client = client
|
||||
l.Client = client
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -161,11 +176,11 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func WriteHeader(writer io.Writer, log *ScrobblerLog) error {
|
||||
func (l *ScrobblerLog) WriteHeader(writer io.Writer) error {
|
||||
headers := []string{
|
||||
"#AUDIOSCROBBLER/1.1\n",
|
||||
"#TZ/" + log.Timezone + "\n",
|
||||
"#CLIENT/" + log.Client + "\n",
|
||||
"#TZ/" + string(l.TZ) + "\n",
|
||||
"#CLIENT/" + l.Client + "\n",
|
||||
}
|
||||
for _, line := range headers {
|
||||
_, err := writer.Write([]byte(line))
|
||||
|
@ -219,8 +234,8 @@ func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) {
|
|||
// Convert the timezone string from the header to a time.Location.
|
||||
// Often this is set to "UNKNOWN" in the log file, in which case it defaults
|
||||
// to UTC.
|
||||
func locationFromTimezone(timezone string) *time.Location {
|
||||
location, err := time.LoadLocation(timezone)
|
||||
func locationFromTimezone(timezone TZInfo) *time.Location {
|
||||
location, err := time.LoadLocation(string(timezone))
|
||||
if err != nil {
|
||||
return time.UTC
|
||||
}
|
|
@ -31,8 +31,8 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/mbtypes"
|
||||
"go.uploadedlobster.com/scotty/internal/backends/scrobblerlog"
|
||||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
"go.uploadedlobster.com/scotty/pkg/scrobblerlog"
|
||||
)
|
||||
|
||||
var testScrobblerLog = `#AUDIOSCROBBLER/1.1
|
||||
|
@ -48,9 +48,10 @@ Teeth Agency You Don't Have To Live In Pain Wolfs Jam 2 107 L 1260359404 1262bea
|
|||
func TestParser(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
data := bytes.NewBufferString(testScrobblerLog)
|
||||
result, err := scrobblerlog.Parse(data, true)
|
||||
result := scrobblerlog.ScrobblerLog{}
|
||||
err := result.Parse(data, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal("UNKNOWN", result.Timezone)
|
||||
assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ)
|
||||
assert.Equal("Rockbox sansaclipplus $Revision$", result.Client)
|
||||
assert.Len(result.Listens, 5)
|
||||
listen1 := result.Listens[0]
|
||||
|
@ -70,7 +71,8 @@ func TestParser(t *testing.T) {
|
|||
func TestParserExcludeSkipped(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
data := bytes.NewBufferString(testScrobblerLog)
|
||||
result, err := scrobblerlog.Parse(data, false)
|
||||
result := scrobblerlog.ScrobblerLog{}
|
||||
err := result.Parse(data, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(result.Listens, 4)
|
||||
listen4 := result.Listens[3]
|
||||
|
@ -78,37 +80,37 @@ func TestParserExcludeSkipped(t *testing.T) {
|
|||
assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMBID)
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
func TestAppend(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
data := make([]byte, 0, 10)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
log := scrobblerlog.ScrobblerLog{
|
||||
Timezone: "Unknown",
|
||||
Client: "Rockbox foo $Revision$",
|
||||
Listens: []models.Listen{
|
||||
{
|
||||
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"},
|
||||
},
|
||||
TZ: scrobblerlog.TZ_UNKNOWN,
|
||||
Client: "Rockbox foo $Revision$",
|
||||
}
|
||||
listens := []models.Listen{
|
||||
{
|
||||
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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := scrobblerlog.WriteHeader(buffer, &log)
|
||||
err := log.WriteHeader(buffer)
|
||||
require.NoError(t, err)
|
||||
lastTimestamp, err := scrobblerlog.Write(buffer, log.Listens)
|
||||
lastTimestamp, err := log.Append(buffer, listens)
|
||||
require.NoError(t, err)
|
||||
result := buffer.String()
|
||||
lines := strings.Split(result, "\n")
|
||||
assert.Equal(5, len(lines))
|
||||
assert.Equal("#AUDIOSCROBBLER/1.1", lines[0])
|
||||
assert.Equal("#TZ/Unknown", lines[1])
|
||||
assert.Equal("#TZ/UNKNOWN", lines[1])
|
||||
assert.Equal("#CLIENT/Rockbox foo $Revision$", lines[2])
|
||||
assert.Equal(
|
||||
"Prinzhorn Dance School\tHome Economics\tReign\t1\t271\tL\t1699572072\tb59cf4e7-caee-4019-a844-79d2c58d4dff",
|
||||
|
@ -121,9 +123,9 @@ func TestReadHeader(t *testing.T) {
|
|||
data := bytes.NewBufferString(testScrobblerLog)
|
||||
reader := bufio.NewReader(data)
|
||||
log := scrobblerlog.ScrobblerLog{}
|
||||
err := scrobblerlog.ReadHeader(reader, &log)
|
||||
err := log.ReadHeader(reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, log.Timezone, "UNKNOWN")
|
||||
assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN)
|
||||
assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$")
|
||||
assert.Empty(t, log.Listens)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue