scrobblerlog: consider timezone from parsed file

This commit is contained in:
Philipp Wolfer 2025-04-28 08:54:17 +02:00
parent 9184d2c3cf
commit 69665bc286
No known key found for this signature in database
GPG key ID: 8FDF744D4919943B

View file

@ -1,5 +1,5 @@
/* /*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com> Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -38,6 +38,7 @@ type ScrobblerLog struct {
Timezone string Timezone string
Client string Client string
Listens models.ListensList Listens models.ListensList
location *time.Location
} }
func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
@ -79,8 +80,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) {
continue continue
} }
client := strings.Split(result.Client, " ")[0] listen, err := result.rowToListen(row)
listen, err := rowToListen(row, client)
if err != nil { if err != nil {
return result, err return result, err
} }
@ -138,14 +138,19 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error {
err = fmt.Errorf("not a scrobbler log file") err = fmt.Errorf("not a scrobbler log file")
} }
// The timezone can be set to "UTC" or "UNKNOWN", if the device writing
// the log knows the time, but not the timezone.
timezone, found := strings.CutPrefix(text, "#TZ/") timezone, found := strings.CutPrefix(text, "#TZ/")
if found { if found {
log.Timezone = timezone log.Timezone = timezone
log.location = locationFromTimezone(log.Timezone)
continue
} }
client, found := strings.CutPrefix(text, "#CLIENT/") client, found := strings.CutPrefix(text, "#CLIENT/")
if found { if found {
log.Client = client log.Client = client
continue
} }
} }
@ -171,7 +176,7 @@ func WriteHeader(writer io.Writer, log *ScrobblerLog) error {
return nil return nil
} }
func rowToListen(row []string, client string) (models.Listen, error) { func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) {
var listen models.Listen var listen models.Listen
trackNumber, err := strconv.Atoi(row[3]) trackNumber, err := strconv.Atoi(row[3])
if err != nil { if err != nil {
@ -183,11 +188,12 @@ func rowToListen(row []string, client string) (models.Listen, error) {
return listen, err return listen, err
} }
timestamp, err := strconv.Atoi(row[6]) timestamp, err := strconv.ParseInt(row[6], 10, 64)
if err != nil { if err != nil {
return listen, err return listen, err
} }
client := strings.Split(l.Client, " ")[0]
listen = models.Listen{ listen = models.Listen{
Track: models.Track{ Track: models.Track{
ArtistNames: []string{row[0]}, ArtistNames: []string{row[0]},
@ -200,7 +206,7 @@ func rowToListen(row []string, client string) (models.Listen, error) {
"media_player": client, "media_player": client,
}, },
}, },
ListenedAt: time.Unix(int64(timestamp), 0), ListenedAt: timeFromLocalTimestamp(timestamp, l.location),
} }
if len(row) > 7 { if len(row) > 7 {
@ -209,3 +215,27 @@ func rowToListen(row []string, client string) (models.Listen, error) {
return listen, nil return listen, nil
} }
// 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)
if err != nil {
return time.UTC
}
return location
}
// Convert a Unix timestamp to a time.Time object, but treat the timestamp
// as being in the given location's timezone instead of UTC.
func timeFromLocalTimestamp(timestamp int64, location *time.Location) time.Time {
t := time.Unix(timestamp, 0)
// The time is now in UTC. Get the offset to the requested timezone.
_, offset := t.In(location).Zone()
if offset != 0 {
t = t.Add(time.Duration(offset) * time.Second)
}
return t
}