diff --git a/internal/backends/scrobblerlog/parser.go b/internal/backends/scrobblerlog/parser.go index 1ef08f7..eeb603b 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/internal/backends/scrobblerlog/parser.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -38,6 +38,7 @@ type ScrobblerLog struct { Timezone string Client string Listens models.ListensList + location *time.Location } func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { @@ -79,8 +80,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { continue } - client := strings.Split(result.Client, " ")[0] - listen, err := rowToListen(row, client) + listen, err := result.rowToListen(row) if err != nil { return result, err } @@ -138,14 +138,19 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error { 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/") if found { log.Timezone = timezone + log.location = locationFromTimezone(log.Timezone) + continue } client, found := strings.CutPrefix(text, "#CLIENT/") if found { log.Client = client + continue } } @@ -171,7 +176,7 @@ func WriteHeader(writer io.Writer, log *ScrobblerLog) error { return nil } -func rowToListen(row []string, client string) (models.Listen, error) { +func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) { var listen models.Listen trackNumber, err := strconv.Atoi(row[3]) if err != nil { @@ -183,11 +188,12 @@ func rowToListen(row []string, client string) (models.Listen, error) { return listen, err } - timestamp, err := strconv.Atoi(row[6]) + timestamp, err := strconv.ParseInt(row[6], 10, 64) if err != nil { return listen, err } + client := strings.Split(l.Client, " ")[0] listen = models.Listen{ Track: models.Track{ ArtistNames: []string{row[0]}, @@ -200,7 +206,7 @@ func rowToListen(row []string, client string) (models.Listen, error) { "media_player": client, }, }, - ListenedAt: time.Unix(int64(timestamp), 0), + ListenedAt: timeFromLocalTimestamp(timestamp, l.location), } if len(row) > 7 { @@ -209,3 +215,27 @@ func rowToListen(row []string, client string) (models.Listen, error) { 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 +}