mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-30 05:37:05 +02:00
parent
0f4b04c641
commit
ed191d2f15
5 changed files with 68 additions and 22 deletions
|
@ -61,6 +61,13 @@ include-skipped = true
|
||||||
# If true (default), new listens will be appended to the existing file. Set to
|
# If true (default), new listens will be appended to the existing file. Set to
|
||||||
# false to overwrite the file and create a new scrobbler log on every run.
|
# false to overwrite the file and create a new scrobbler log on every run.
|
||||||
append = true
|
append = true
|
||||||
|
# Specify the time zone of the listens in the scrobbler log. While the log files
|
||||||
|
# are supposed to contain Unix timestamps, which are always in UTC, the player
|
||||||
|
# writing the log might not be time zone aware. This can cause the timestamps
|
||||||
|
# to be in a different time zone. Use the time-zone setting to specify a
|
||||||
|
# different time zone, e.g. "Europe/Berlin" or "America/New_York".
|
||||||
|
# The default is UTC.
|
||||||
|
time-zone = "UTC"
|
||||||
|
|
||||||
[service.jspf]
|
[service.jspf]
|
||||||
# Write listens and loves to JSPF playlist files (https://xspf.org/jspf)
|
# Write listens and loves to JSPF playlist files (https://xspf.org/jspf)
|
||||||
|
|
|
@ -18,6 +18,7 @@ package scrobblerlog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -34,6 +35,7 @@ type ScrobblerLogBackend struct {
|
||||||
includeSkipped bool
|
includeSkipped bool
|
||||||
append bool
|
append bool
|
||||||
file *os.File
|
file *os.File
|
||||||
|
timezone *time.Location
|
||||||
log scrobblerlog.ScrobblerLog
|
log scrobblerlog.ScrobblerLog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +55,10 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption {
|
||||||
Label: i18n.Tr("Append to file"),
|
Label: i18n.Tr("Append to file"),
|
||||||
Type: models.Bool,
|
Type: models.Bool,
|
||||||
Default: "true",
|
Default: "true",
|
||||||
|
}, {
|
||||||
|
Name: "time-zone",
|
||||||
|
Label: i18n.Tr("Specify a time zone for the listen timestamps"),
|
||||||
|
Type: models.String,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +66,14 @@ func (b *ScrobblerLogBackend) InitConfig(config *config.ServiceConfig) error {
|
||||||
b.filePath = config.GetString("file-path")
|
b.filePath = config.GetString("file-path")
|
||||||
b.includeSkipped = config.GetBool("include-skipped", false)
|
b.includeSkipped = config.GetBool("include-skipped", false)
|
||||||
b.append = config.GetBool("append", true)
|
b.append = config.GetBool("append", true)
|
||||||
|
timezone := config.GetString("time-zone")
|
||||||
|
if timezone != "" {
|
||||||
|
location, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid time-zone %q: %w", timezone, err)
|
||||||
|
}
|
||||||
|
b.log.FallbackTimezone = location
|
||||||
|
}
|
||||||
b.log = scrobblerlog.ScrobblerLog{
|
b.log = scrobblerlog.ScrobblerLog{
|
||||||
TZ: scrobblerlog.TZ_UTC,
|
TZ: scrobblerlog.TZ_UTC,
|
||||||
Client: "Rockbox unknown $Revision$",
|
Client: "Rockbox unknown $Revision$",
|
||||||
|
|
|
@ -33,3 +33,13 @@ func TestInitConfig(t *testing.T) {
|
||||||
err := backend.InitConfig(&service)
|
err := backend.InitConfig(&service)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInitConfigInvalidTimezone(t *testing.T) {
|
||||||
|
c := viper.New()
|
||||||
|
configuredTimezone := "Invalid/Timezone"
|
||||||
|
c.Set("time-zone", configuredTimezone)
|
||||||
|
service := config.NewServiceConfig("test", c)
|
||||||
|
backend := scrobblerlog.ScrobblerLogBackend{}
|
||||||
|
err := backend.InitConfig(&service)
|
||||||
|
assert.ErrorContains(t, err, `Invalid time-zone "Invalid/Timezone"`)
|
||||||
|
}
|
||||||
|
|
|
@ -74,7 +74,9 @@ type ScrobblerLog struct {
|
||||||
TZ TZInfo
|
TZ TZInfo
|
||||||
Client string
|
Client string
|
||||||
Records []Record
|
Records []Record
|
||||||
location *time.Location
|
// Timezone to be used for timestamps in the log file,
|
||||||
|
// if TZ is set to [TZ_UNKNOWN].
|
||||||
|
FallbackTimezone *time.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error {
|
||||||
|
@ -173,7 +175,6 @@ func (l *ScrobblerLog) ReadHeader(reader *bufio.Reader) error {
|
||||||
timezone, found := strings.CutPrefix(text, "#TZ/")
|
timezone, found := strings.CutPrefix(text, "#TZ/")
|
||||||
if found {
|
if found {
|
||||||
l.TZ = TZInfo(timezone)
|
l.TZ = TZInfo(timezone)
|
||||||
l.location = locationFromTimezone(l.TZ)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,6 +224,11 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) {
|
||||||
return record, err
|
return record, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timezone *time.Location = nil
|
||||||
|
if l.TZ == TZ_UNKNOWN {
|
||||||
|
timezone = l.FallbackTimezone
|
||||||
|
}
|
||||||
|
|
||||||
record = Record{
|
record = Record{
|
||||||
ArtistName: row[0],
|
ArtistName: row[0],
|
||||||
AlbumName: row[1],
|
AlbumName: row[1],
|
||||||
|
@ -230,7 +236,7 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) {
|
||||||
TrackNumber: trackNumber,
|
TrackNumber: trackNumber,
|
||||||
Duration: time.Duration(duration) * time.Second,
|
Duration: time.Duration(duration) * time.Second,
|
||||||
Rating: Rating(row[5]),
|
Rating: Rating(row[5]),
|
||||||
Timestamp: timeFromLocalTimestamp(timestamp, l.location),
|
Timestamp: timeFromLocalTimestamp(timestamp, timezone),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(row) > 7 {
|
if len(row) > 7 {
|
||||||
|
@ -240,26 +246,20 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) {
|
||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the timezone string from the header to a time.Location.
|
// Convert a Unix timestamp to a [time.Time] object, but treat the timestamp
|
||||||
// Often this is set to "UNKNOWN" in the log file, in which case it defaults
|
|
||||||
// to UTC.
|
|
||||||
func locationFromTimezone(timezone TZInfo) *time.Location {
|
|
||||||
location, err := time.LoadLocation(string(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.
|
// as being in the given location's timezone instead of UTC.
|
||||||
|
// If location is nil, the timestamp is returned as UTC.
|
||||||
func timeFromLocalTimestamp(timestamp int64, location *time.Location) time.Time {
|
func timeFromLocalTimestamp(timestamp int64, location *time.Location) time.Time {
|
||||||
t := time.Unix(timestamp, 0)
|
t := time.Unix(timestamp, 0)
|
||||||
|
|
||||||
// The time is now in UTC. Get the offset to the requested timezone.
|
// The time is now in UTC. Get the offset to the requested timezone
|
||||||
|
// and shift the time accordingly.
|
||||||
|
if location != nil {
|
||||||
_, offset := t.In(location).Zone()
|
_, offset := t.In(location).Zone()
|
||||||
if offset != 0 {
|
if offset != 0 {
|
||||||
t = t.Add(time.Duration(offset) * time.Second)
|
t = t.Add(time.Duration(offset) * time.Second)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,21 @@ func TestParserExcludeSkipped(t *testing.T) {
|
||||||
record4.MusicBrainzRecordingID)
|
record4.MusicBrainzRecordingID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParserFallbackTimezone(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
data := bytes.NewBufferString(testScrobblerLog)
|
||||||
|
result := scrobblerlog.ScrobblerLog{
|
||||||
|
FallbackTimezone: time.FixedZone("UTC+2", 7200),
|
||||||
|
}
|
||||||
|
err := result.Parse(data, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
record1 := result.Records[0]
|
||||||
|
assert.Equal(
|
||||||
|
time.Unix(1260342084, 0).Add(2*time.Hour),
|
||||||
|
record1.Timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAppend(t *testing.T) {
|
func TestAppend(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
data := make([]byte, 0, 10)
|
data := make([]byte, 0, 10)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue