fix: respect client timezone for requests (#119)

* maybe fixed for total listen activity

* maybe actually fixed now

* fix unset location panics
This commit is contained in:
Gabe Farrell 2026-01-10 01:45:31 -05:00 committed by GitHub
parent 2925425750
commit f48dd6c039
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 368 additions and 343 deletions

View file

@ -134,6 +134,7 @@ type ListenActivityOpts struct {
Range int
Month int
Year int
Timezone *time.Location
AlbumID int32
ArtistID int32
TrackID int32

View file

@ -58,16 +58,20 @@ const (
// If opts.Year (or opts.Year + opts.Month) is provided, start and end will simply by the start and end times of that year/month.
func ListenActivityOptsToTimes(opts ListenActivityOpts) (start, end time.Time) {
now := time.Now()
loc := opts.Timezone
if loc == nil {
loc, _ = time.LoadLocation("UTC")
}
// If Year (and optionally Month) are specified, use calendar boundaries
if opts.Year != 0 {
if opts.Month != 0 {
// Specific month of a specific year
start = time.Date(opts.Year, time.Month(opts.Month), 1, 0, 0, 0, 0, now.Location())
start = time.Date(opts.Year, time.Month(opts.Month), 1, 0, 0, 0, 0, loc)
end = start.AddDate(0, 1, 0).Add(-time.Nanosecond)
} else {
// Whole year
start = time.Date(opts.Year, 1, 1, 0, 0, 0, 0, now.Location())
start = time.Date(opts.Year, 1, 1, 0, 0, 0, 0, loc)
end = start.AddDate(1, 0, 0).Add(-time.Nanosecond)
}
return start, end
@ -79,30 +83,30 @@ func ListenActivityOptsToTimes(opts ListenActivityOpts) (start, end time.Time) {
// Determine step and align accordingly
switch opts.Step {
case StepDay:
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
start = today.AddDate(0, 0, -opts.Range)
end = today.AddDate(0, 0, 1).Add(-time.Nanosecond)
case StepWeek:
// Align to most recent Sunday
weekday := int(now.Weekday()) // Sunday = 0
startOfThisWeek := time.Date(now.Year(), now.Month(), now.Day()-weekday, 0, 0, 0, 0, now.Location())
startOfThisWeek := time.Date(now.Year(), now.Month(), now.Day()-weekday, 0, 0, 0, 0, loc)
start = startOfThisWeek.AddDate(0, 0, -7*opts.Range)
end = startOfThisWeek.AddDate(0, 0, 7).Add(-time.Nanosecond)
case StepMonth:
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
start = firstOfThisMonth.AddDate(0, -opts.Range, 0)
end = firstOfThisMonth.AddDate(0, 1, 0).Add(-time.Nanosecond)
case StepYear:
firstOfThisYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
firstOfThisYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, loc)
start = firstOfThisYear.AddDate(-opts.Range, 0, 0)
end = firstOfThisYear.AddDate(1, 0, 0).Add(-time.Nanosecond)
default:
// Default to daily
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
start = today.AddDate(0, 0, -opts.Range)
end = today.AddDate(0, 0, 1).Add(-time.Nanosecond)
}

View file

@ -25,10 +25,10 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
l.Debug().Msgf("Fetching listen activity for %d %s(s) from %v to %v for release group %d",
opts.Range, opts.Step, t1.Format("Jan 02, 2006 15:04:05"), t2.Format("Jan 02, 2006 15:04:05"), opts.AlbumID)
rows, err := d.q.ListenActivityForRelease(ctx, repository.ListenActivityForReleaseParams{
Column1: t1,
Column2: t2,
Column3: stepToInterval(opts.Step),
ReleaseID: opts.AlbumID,
Column1: opts.Timezone.String(),
ListenedAt: t1,
ListenedAt_2: t2,
ReleaseID: opts.AlbumID,
})
if err != nil {
return nil, fmt.Errorf("GetListenActivity: ListenActivityForRelease: %w", err)
@ -36,7 +36,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
listenActivity = make([]db.ListenActivityItem, len(rows))
for i, row := range rows {
t := db.ListenActivityItem{
Start: row.BucketStart,
Start: row.Day.Time,
Listens: row.ListenCount,
}
listenActivity[i] = t
@ -46,10 +46,10 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
l.Debug().Msgf("Fetching listen activity for %d %s(s) from %v to %v for artist %d",
opts.Range, opts.Step, t1.Format("Jan 02, 2006 15:04:05"), t2.Format("Jan 02, 2006 15:04:05"), opts.ArtistID)
rows, err := d.q.ListenActivityForArtist(ctx, repository.ListenActivityForArtistParams{
Column1: t1,
Column2: t2,
Column3: stepToInterval(opts.Step),
ArtistID: opts.ArtistID,
Column1: opts.Timezone.String(),
ListenedAt: t1,
ListenedAt_2: t2,
ArtistID: opts.ArtistID,
})
if err != nil {
return nil, fmt.Errorf("GetListenActivity: ListenActivityForArtist: %w", err)
@ -57,7 +57,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
listenActivity = make([]db.ListenActivityItem, len(rows))
for i, row := range rows {
t := db.ListenActivityItem{
Start: row.BucketStart,
Start: row.Day.Time,
Listens: row.ListenCount,
}
listenActivity[i] = t
@ -67,10 +67,10 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
l.Debug().Msgf("Fetching listen activity for %d %s(s) from %v to %v for track %d",
opts.Range, opts.Step, t1.Format("Jan 02, 2006 15:04:05"), t2.Format("Jan 02, 2006 15:04:05"), opts.TrackID)
rows, err := d.q.ListenActivityForTrack(ctx, repository.ListenActivityForTrackParams{
Column1: t1,
Column2: t2,
Column3: stepToInterval(opts.Step),
ID: opts.TrackID,
Column1: opts.Timezone.String(),
ListenedAt: t1,
ListenedAt_2: t2,
ID: opts.TrackID,
})
if err != nil {
return nil, fmt.Errorf("GetListenActivity: ListenActivityForTrack: %w", err)
@ -78,7 +78,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
listenActivity = make([]db.ListenActivityItem, len(rows))
for i, row := range rows {
t := db.ListenActivityItem{
Start: row.BucketStart,
Start: row.Day.Time,
Listens: row.ListenCount,
}
listenActivity[i] = t
@ -88,9 +88,9 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
l.Debug().Msgf("Fetching listen activity for %d %s(s) from %v to %v",
opts.Range, opts.Step, t1.Format("Jan 02, 2006 15:04:05"), t2.Format("Jan 02, 2006 15:04:05"))
rows, err := d.q.ListenActivity(ctx, repository.ListenActivityParams{
Column1: t1,
Column2: t2,
Column3: stepToInterval(opts.Step),
Column1: opts.Timezone.String(),
ListenedAt: t1,
ListenedAt_2: t2,
})
if err != nil {
return nil, fmt.Errorf("GetListenActivity: ListenActivity: %w", err)
@ -98,7 +98,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
listenActivity = make([]db.ListenActivityItem, len(rows))
for i, row := range rows {
t := db.ListenActivityItem{
Start: row.BucketStart,
Start: row.Day.Time,
Listens: row.ListenCount,
}
listenActivity[i] = t

View file

@ -88,8 +88,8 @@ func TestListenActivity(t *testing.T) {
// Test for opts.Step = db.StepDay
activity, err := store.GetListenActivity(ctx, db.ListenActivityOpts{Step: db.StepDay})
require.NoError(t, err)
require.Len(t, activity, db.DefaultRange)
assert.Equal(t, []int64{0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 0}, flattenListenCounts(activity))
require.Len(t, activity, 3)
assert.Equal(t, []int64{2, 2, 2}, flattenListenCounts(activity))
// Truncate listens table and insert specific dates for testing opts.Step = db.StepMonth
err = store.Exec(context.Background(), `TRUNCATE TABLE listens`)
@ -126,8 +126,8 @@ func TestListenActivity(t *testing.T) {
activity, err = store.GetListenActivity(ctx, db.ListenActivityOpts{Step: db.StepYear})
require.NoError(t, err)
require.Len(t, activity, db.DefaultRange)
assert.Equal(t, []int64{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 0}, flattenListenCounts(activity))
require.Len(t, activity, 3)
assert.Equal(t, []int64{1, 1, 2}, flattenListenCounts(activity))
// Truncate and insert data for a specific month/year
err = store.Exec(context.Background(), `TRUNCATE TABLE listens RESTART IDENTITY`)
require.NoError(t, err)
@ -144,10 +144,10 @@ func TestListenActivity(t *testing.T) {
Year: 2024,
})
require.NoError(t, err)
require.Len(t, activity, 31) // number of days in march
require.Len(t, activity, 2) // number of days in march
t.Log(activity)
assert.EqualValues(t, 1, activity[9].Listens)
assert.EqualValues(t, 1, activity[19].Listens)
assert.EqualValues(t, 1, activity[0].Listens)
assert.EqualValues(t, 1, activity[1].Listens)
// Truncate and insert listens associated with two different albums
err = store.Exec(context.Background(), `TRUNCATE TABLE listens RESTART IDENTITY`)
@ -164,53 +164,29 @@ func TestListenActivity(t *testing.T) {
AlbumID: 1, // Track 1 only
})
require.NoError(t, err)
require.Len(t, activity, db.DefaultRange)
assert.Equal(t, []int64{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0}, flattenListenCounts(activity))
require.Len(t, activity, 2)
assert.Equal(t, []int64{1, 1}, flattenListenCounts(activity))
activity, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
Step: db.StepDay,
TrackID: 1, // Track 1 only
})
require.NoError(t, err)
require.Len(t, activity, db.DefaultRange)
assert.Equal(t, []int64{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0}, flattenListenCounts(activity))
require.Len(t, activity, 2)
assert.Equal(t, []int64{1, 1}, flattenListenCounts(activity))
activity, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
Step: db.StepDay,
ArtistID: 2, // Should only include listens to Track 2
})
require.NoError(t, err)
require.Len(t, activity, db.DefaultRange)
assert.Equal(t, []int64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}, flattenListenCounts(activity))
require.Len(t, activity, 1)
assert.Equal(t, []int64{1}, flattenListenCounts(activity))
// month without year is disallowed
_, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
Step: db.StepDay,
Month: 5,
})
require.Error(t, err)
// invalid options
_, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
Year: -10,
})
require.Error(t, err)
_, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
Year: 2025,
Month: -10,
})
require.Error(t, err)
_, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
Range: -1,
})
require.Error(t, err)
_, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
AlbumID: -1,
})
require.Error(t, err)
_, err = store.GetListenActivity(ctx, db.ListenActivityOpts{
ArtistID: -1,
})
require.Error(t, err)
assert.Error(t, err)
}

View file

@ -13,11 +13,15 @@ type Timeframe struct {
ToUnix int64
From time.Time
To time.Time
Timezone *time.Location
}
func TimeframeToTimeRange(tf Timeframe) (t1, t2 time.Time) {
now := time.Now()
loc := now.Location()
loc := tf.Timezone
if loc == nil {
loc, _ = time.LoadLocation("UTC")
}
// ---------------------------------------------------------------------
// 1. Explicit From / To (time.Time) — highest precedence