mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 13:38:15 -08:00
feat: Rewind (#116)
* wip * chore: update counts to allow unix timeframe * feat: add db functions for counting new items * wip: endpoint working * wip * wip: initial ui done * add header, adjust ui * add time listened toggle * fix layout, year param * param fixes
This commit is contained in:
parent
c0a8c64243
commit
d4ac96f780
64 changed files with 2252 additions and 1055 deletions
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
type DB interface {
|
||||
// Get
|
||||
|
||||
GetArtist(ctx context.Context, opts GetArtistOpts) (*models.Artist, error)
|
||||
GetAlbum(ctx context.Context, opts GetAlbumOpts) (*models.Album, error)
|
||||
GetTrack(ctx context.Context, opts GetTrackOpts) (*models.Track, error)
|
||||
|
|
@ -28,7 +29,9 @@ type DB interface {
|
|||
GetUserBySession(ctx context.Context, sessionId uuid.UUID) (*models.User, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (*models.User, error)
|
||||
GetUserByApiKey(ctx context.Context, key string) (*models.User, error)
|
||||
|
||||
// Save
|
||||
|
||||
SaveArtist(ctx context.Context, opts SaveArtistOpts) (*models.Artist, error)
|
||||
SaveArtistAliases(ctx context.Context, id int32, aliases []string, source string) error
|
||||
SaveAlbum(ctx context.Context, opts SaveAlbumOpts) (*models.Album, error)
|
||||
|
|
@ -39,7 +42,9 @@ type DB interface {
|
|||
SaveUser(ctx context.Context, opts SaveUserOpts) (*models.User, error)
|
||||
SaveApiKey(ctx context.Context, opts SaveApiKeyOpts) (*models.ApiKey, error)
|
||||
SaveSession(ctx context.Context, userId int32, expiresAt time.Time, persistent bool) (*models.Session, error)
|
||||
|
||||
// Update
|
||||
|
||||
UpdateArtist(ctx context.Context, opts UpdateArtistOpts) error
|
||||
UpdateTrack(ctx context.Context, opts UpdateTrackOpts) error
|
||||
UpdateAlbum(ctx context.Context, opts UpdateAlbumOpts) error
|
||||
|
|
@ -52,7 +57,9 @@ type DB interface {
|
|||
SetPrimaryTrackAlias(ctx context.Context, id int32, alias string) error
|
||||
SetPrimaryAlbumArtist(ctx context.Context, id int32, artistId int32, value bool) error
|
||||
SetPrimaryTrackArtist(ctx context.Context, id int32, artistId int32, value bool) error
|
||||
|
||||
// Delete
|
||||
|
||||
DeleteArtist(ctx context.Context, id int32) error
|
||||
DeleteAlbum(ctx context.Context, id int32) error
|
||||
DeleteTrack(ctx context.Context, id int32) error
|
||||
|
|
@ -62,23 +69,36 @@ type DB interface {
|
|||
DeleteTrackAlias(ctx context.Context, id int32, alias string) error
|
||||
DeleteSession(ctx context.Context, sessionId uuid.UUID) error
|
||||
DeleteApiKey(ctx context.Context, id int32) error
|
||||
|
||||
// Count
|
||||
CountListens(ctx context.Context, period Period) (int64, error)
|
||||
CountTracks(ctx context.Context, period Period) (int64, error)
|
||||
CountAlbums(ctx context.Context, period Period) (int64, error)
|
||||
CountArtists(ctx context.Context, period Period) (int64, error)
|
||||
CountTimeListened(ctx context.Context, period Period) (int64, error)
|
||||
|
||||
CountListens(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
CountListensToItem(ctx context.Context, opts TimeListenedOpts) (int64, error)
|
||||
CountTracks(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
CountAlbums(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
CountArtists(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
CountNewTracks(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
CountNewAlbums(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
CountNewArtists(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
// in seconds
|
||||
CountTimeListened(ctx context.Context, timeframe Timeframe) (int64, error)
|
||||
// in seconds
|
||||
CountTimeListenedToItem(ctx context.Context, opts TimeListenedOpts) (int64, error)
|
||||
CountUsers(ctx context.Context) (int64, error)
|
||||
// Search
|
||||
|
||||
SearchArtists(ctx context.Context, q string) ([]*models.Artist, error)
|
||||
SearchAlbums(ctx context.Context, q string) ([]*models.Album, error)
|
||||
SearchTracks(ctx context.Context, q string) ([]*models.Track, error)
|
||||
|
||||
// Merge
|
||||
|
||||
MergeTracks(ctx context.Context, fromId, toId int32) error
|
||||
MergeAlbums(ctx context.Context, fromId, toId int32, replaceImage bool) error
|
||||
MergeArtists(ctx context.Context, fromId, toId int32, replaceImage bool) error
|
||||
|
||||
// Etc
|
||||
|
||||
ImageHasAssociation(ctx context.Context, image uuid.UUID) (bool, error)
|
||||
GetImageSource(ctx context.Context, image uuid.UUID) (string, error)
|
||||
AlbumsWithoutImages(ctx context.Context, from int32) ([]*models.Album, error)
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ type GetItemsOpts struct {
|
|||
Week int // 1-52
|
||||
Month int // 1-12
|
||||
Year int
|
||||
From int // unix timestamp
|
||||
To int // unix timestamp
|
||||
From int64 // unix timestamp
|
||||
To int64 // unix timestamp
|
||||
|
||||
// Used only for getting top tracks
|
||||
ArtistID int
|
||||
|
|
@ -144,10 +144,10 @@ type ListenActivityOpts struct {
|
|||
}
|
||||
|
||||
type TimeListenedOpts struct {
|
||||
Period Period
|
||||
AlbumID int32
|
||||
ArtistID int32
|
||||
TrackID int32
|
||||
Timeframe Timeframe
|
||||
AlbumID int32
|
||||
ArtistID int32
|
||||
TrackID int32
|
||||
}
|
||||
|
||||
type GetExportPageOpts struct {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,23 @@ import (
|
|||
|
||||
// should this be in db package ???
|
||||
|
||||
type Timeframe struct {
|
||||
Period Period
|
||||
T1u int64
|
||||
T2u int64
|
||||
}
|
||||
|
||||
func TimeframeToTimeRange(timeframe Timeframe) (t1, t2 time.Time) {
|
||||
if timeframe.T1u == 0 && timeframe.T2u == 0 {
|
||||
t2 = time.Now()
|
||||
t1 = StartTimeFromPeriod(timeframe.Period)
|
||||
} else {
|
||||
t1 = time.Unix(timeframe.T1u, 0)
|
||||
t2 = time.Unix(timeframe.T2u, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Period string
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
|
|||
}
|
||||
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
AlbumID: ret.ID,
|
||||
Timeframe: db.Timeframe{Period: db.PeriodAllTime},
|
||||
AlbumID: ret.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
|
||||
}
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
ArtistID: row.ID,
|
||||
Timeframe: db.Timeframe{Period: db.PeriodAllTime},
|
||||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
||||
|
|
@ -70,8 +70,8 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
|
||||
}
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
ArtistID: row.ID,
|
||||
Timeframe: db.Timeframe{Period: db.PeriodAllTime},
|
||||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
||||
|
|
@ -105,8 +105,8 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
|
||||
}
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
ArtistID: row.ID,
|
||||
Timeframe: db.Timeframe{Period: db.PeriodAllTime},
|
||||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
"github.com/gabehf/koito/internal/repository"
|
||||
)
|
||||
|
||||
func (p *Psql) CountListens(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
func (p *Psql) CountListens(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountListens(ctx, repository.CountListensParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
|
|
@ -23,9 +21,8 @@ func (p *Psql) CountListens(ctx context.Context, period db.Period) (int64, error
|
|||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountTracks(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
func (p *Psql) CountTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountTopTracks(ctx, repository.CountTopTracksParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
|
|
@ -36,9 +33,8 @@ func (p *Psql) CountTracks(ctx context.Context, period db.Period) (int64, error)
|
|||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountAlbums(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
func (p *Psql) CountAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountTopReleases(ctx, repository.CountTopReleasesParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
|
|
@ -49,9 +45,8 @@ func (p *Psql) CountAlbums(ctx context.Context, period db.Period) (int64, error)
|
|||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountArtists(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
func (p *Psql) CountArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountTopArtists(ctx, repository.CountTopArtistsParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
|
|
@ -62,9 +57,9 @@ func (p *Psql) CountArtists(ctx context.Context, period db.Period) (int64, error
|
|||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountTimeListened(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
// in seconds
|
||||
func (p *Psql) CountTimeListened(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountTimeListened(ctx, repository.CountTimeListenedParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
|
|
@ -75,9 +70,9 @@ func (p *Psql) CountTimeListened(ctx context.Context, period db.Period) (int64,
|
|||
return count, nil
|
||||
}
|
||||
|
||||
// in seconds
|
||||
func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListenedOpts) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(opts.Period)
|
||||
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
||||
|
||||
if opts.ArtistID > 0 {
|
||||
count, err := p.q.CountTimeListenedToArtist(ctx, repository.CountTimeListenedToArtistParams{
|
||||
|
|
@ -112,3 +107,76 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened
|
|||
}
|
||||
return 0, errors.New("CountTimeListenedToItem: an id must be provided")
|
||||
}
|
||||
|
||||
func (p *Psql) CountListensToItem(ctx context.Context, opts db.TimeListenedOpts) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
||||
|
||||
if opts.ArtistID > 0 {
|
||||
count, err := p.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
ArtistID: opts.ArtistID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountListensToItem (Artist): %w", err)
|
||||
}
|
||||
return count, nil
|
||||
} else if opts.AlbumID > 0 {
|
||||
count, err := p.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
ReleaseID: opts.AlbumID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountListensToItem (Album): %w", err)
|
||||
}
|
||||
return count, nil
|
||||
} else if opts.TrackID > 0 {
|
||||
count, err := p.q.CountListensFromTrack(ctx, repository.CountListensFromTrackParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
TrackID: opts.TrackID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountListensToItem (Track): %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
return 0, errors.New("CountListensToItem: an id must be provided")
|
||||
}
|
||||
|
||||
func (p *Psql) CountNewTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountNewTracks(ctx, repository.CountNewTracksParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountNewTracks: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountNewAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountNewReleases(ctx, repository.CountNewReleasesParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountNewAlbums: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountNewArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) {
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
count, err := p.q.CountNewArtists(ctx, repository.CountNewArtistsParams{
|
||||
ListenedAt: t1,
|
||||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountNewArtists: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql_test
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -15,7 +16,7 @@ func TestCountListens(t *testing.T) {
|
|||
|
||||
// Test CountListens
|
||||
period := db.PeriodWeek
|
||||
count, err := store.CountListens(ctx, period)
|
||||
count, err := store.CountListens(ctx, db.Timeframe{Period: period})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count, "expected listens count to match inserted data")
|
||||
|
||||
|
|
@ -28,46 +29,97 @@ func TestCountTracks(t *testing.T) {
|
|||
|
||||
// Test CountTracks
|
||||
period := db.PeriodMonth
|
||||
count, err := store.CountTracks(ctx, period)
|
||||
count, err := store.CountTracks(ctx, db.Timeframe{Period: period})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count, "expected tracks count to match inserted data")
|
||||
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestCountNewTracks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
testDataAbsoluteListenTimes(t)
|
||||
|
||||
// Test CountTracks
|
||||
t1, _ := time.Parse(time.DateOnly, "2025-01-01")
|
||||
t1u := t1.Unix()
|
||||
t2, _ := time.Parse(time.DateOnly, "2025-12-31")
|
||||
t2u := t2.Unix()
|
||||
count, err := store.CountNewTracks(ctx, db.Timeframe{T1u: t1u, T2u: t2u})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count, "expected tracks count to match inserted data")
|
||||
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestCountAlbums(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
|
||||
// Test CountAlbums
|
||||
period := db.PeriodYear
|
||||
count, err := store.CountAlbums(ctx, period)
|
||||
count, err := store.CountAlbums(ctx, db.Timeframe{Period: period})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count, "expected albums count to match inserted data")
|
||||
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestCountNewAlbums(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
testDataAbsoluteListenTimes(t)
|
||||
|
||||
// Test CountTracks
|
||||
t1, _ := time.Parse(time.DateOnly, "2025-01-01")
|
||||
t1u := t1.Unix()
|
||||
t2, _ := time.Parse(time.DateOnly, "2025-12-31")
|
||||
t2u := t2.Unix()
|
||||
count, err := store.CountNewAlbums(ctx, db.Timeframe{T1u: t1u, T2u: t2u})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count, "expected albums count to match inserted data")
|
||||
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestCountArtists(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
|
||||
// Test CountArtists
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountArtists(ctx, period)
|
||||
count, err := store.CountArtists(ctx, db.Timeframe{Period: period})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(4), count, "expected artists count to match inserted data")
|
||||
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestCountNewArtists(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
testDataAbsoluteListenTimes(t)
|
||||
|
||||
// Test CountTracks
|
||||
t1, _ := time.Parse(time.DateOnly, "2025-01-01")
|
||||
t1u := t1.Unix()
|
||||
t2, _ := time.Parse(time.DateOnly, "2025-12-31")
|
||||
t2u := t2.Unix()
|
||||
count, err := store.CountNewArtists(ctx, db.Timeframe{T1u: t1u, T2u: t2u})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count, "expected artists count to match inserted data")
|
||||
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestCountTimeListened(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
|
||||
// Test CountTimeListened
|
||||
period := db.PeriodMonth
|
||||
count, err := store.CountTimeListened(ctx, period)
|
||||
count, err := store.CountTimeListened(ctx, db.Timeframe{Period: period})
|
||||
require.NoError(t, err)
|
||||
// 3 listens in past month, each 100 seconds
|
||||
assert.Equal(t, int64(300), count, "expected total time listened to match inserted data")
|
||||
|
|
@ -79,7 +131,7 @@ func TestCountTimeListenedToArtist(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{Period: period, ArtistID: 1})
|
||||
count, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{Timeframe: db.Timeframe{Period: period}, ArtistID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 400, count)
|
||||
truncateTestData(t)
|
||||
|
|
@ -89,7 +141,7 @@ func TestCountTimeListenedToAlbum(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{Period: period, AlbumID: 2})
|
||||
count, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{Timeframe: db.Timeframe{Period: period}, AlbumID: 2})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 300, count)
|
||||
truncateTestData(t)
|
||||
|
|
@ -99,8 +151,38 @@ func TestCountTimeListenedToTrack(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{Period: period, TrackID: 3})
|
||||
count, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{Timeframe: db.Timeframe{Period: period}, TrackID: 3})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 200, count)
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestListensToArtist(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountListensToItem(ctx, db.TimeListenedOpts{Timeframe: db.Timeframe{Period: period}, ArtistID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestListensToAlbum(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountListensToItem(ctx, db.TimeListenedOpts{Timeframe: db.Timeframe{Period: period}, AlbumID: 2})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 3, count)
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
||||
func TestListensToTrack(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDataForTopItems(t)
|
||||
period := db.PeriodAllTime
|
||||
count, err := store.CountListensToItem(ctx, db.TimeListenedOpts{Timeframe: db.Timeframe{Period: period}, TrackID: 3})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
truncateTestData(t)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
t2 = time.Now()
|
||||
t1 = db.StartTimeFromPeriod(opts.Period)
|
||||
}
|
||||
if opts.From != 0 || opts.To != 0 {
|
||||
t1 = time.Unix(opts.From, 0)
|
||||
t2 = time.Unix(opts.To, 0)
|
||||
}
|
||||
if opts.Limit == 0 {
|
||||
opts.Limit = DefaultItemsPerPage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
t2 = time.Now()
|
||||
t1 = db.StartTimeFromPeriod(opts.Period)
|
||||
}
|
||||
if opts.From != 0 || opts.To != 0 {
|
||||
t1 = time.Unix(opts.From, 0)
|
||||
t2 = time.Unix(opts.To, 0)
|
||||
}
|
||||
if opts.Limit == 0 {
|
||||
opts.Limit = DefaultItemsPerPage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
t2 = time.Now()
|
||||
t1 = db.StartTimeFromPeriod(opts.Period)
|
||||
}
|
||||
if opts.From != 0 || opts.To != 0 {
|
||||
t1 = time.Unix(opts.From, 0)
|
||||
t2 = time.Unix(opts.To, 0)
|
||||
}
|
||||
if opts.Limit == 0 {
|
||||
opts.Limit = DefaultItemsPerPage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
}
|
||||
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
TrackID: track.ID,
|
||||
Timeframe: db.Timeframe{Period: db.PeriodAllTime},
|
||||
TrackID: track.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -34,15 +33,16 @@ func ImportListenBrainzExport(ctx context.Context, store db.DB, mbzc mbz.MusicBr
|
|||
for _, f := range r.File {
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
l.Debug().Msgf("File %s is dir, skipping...", f.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(f.Name, "listens/") && strings.HasSuffix(f.Name, ".jsonl") {
|
||||
fmt.Println("Found:", f.Name)
|
||||
l.Info().Msgf("Found: %s\n", f.Name)
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
log.Printf("Failed to open %s: %v\n", f.Name, err)
|
||||
l.Err(err).Msgf("Failed to open %s\n", f.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai
|
|||
payload := new(handlers.LbzSubmitListenPayload)
|
||||
err := json.Unmarshal(line, payload)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshaling JSON:", err)
|
||||
l.Err(err).Msg("Error unmarshaling JSON")
|
||||
continue
|
||||
}
|
||||
ts := time.Unix(payload.ListenedAt, 0)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,30 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countNewArtists = `-- name: CountNewArtists :one
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM (
|
||||
SELECT at.artist_id
|
||||
FROM listens l
|
||||
JOIN tracks t ON l.track_id = t.id
|
||||
JOIN artist_tracks at ON t.id = at.track_id
|
||||
GROUP BY at.artist_id
|
||||
HAVING MIN(l.listened_at) BETWEEN $1 AND $2
|
||||
) first_appearances
|
||||
`
|
||||
|
||||
type CountNewArtistsParams struct {
|
||||
ListenedAt time.Time
|
||||
ListenedAt_2 time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) CountNewArtists(ctx context.Context, arg CountNewArtistsParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countNewArtists, arg.ListenedAt, arg.ListenedAt_2)
|
||||
var total_count int64
|
||||
err := row.Scan(&total_count)
|
||||
return total_count, err
|
||||
}
|
||||
|
||||
const countTopArtists = `-- name: CountTopArtists :one
|
||||
SELECT COUNT(DISTINCT at.artist_id) AS total_count
|
||||
FROM listens l
|
||||
|
|
@ -78,7 +102,7 @@ func (q *Queries) DeleteConflictingArtistTracks(ctx context.Context, arg DeleteC
|
|||
}
|
||||
|
||||
const getArtist = `-- name: GetArtist :one
|
||||
SELECT
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
array_agg(aa.alias)::text[] AS aliases
|
||||
FROM artists_with_name a
|
||||
|
|
@ -127,7 +151,7 @@ func (q *Queries) GetArtistByImage(ctx context.Context, image *uuid.UUID) (Artis
|
|||
}
|
||||
|
||||
const getArtistByMbzID = `-- name: GetArtistByMbzID :one
|
||||
SELECT
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
array_agg(aa.alias)::text[] AS aliases
|
||||
FROM artists_with_name a
|
||||
|
|
@ -161,7 +185,7 @@ func (q *Queries) GetArtistByMbzID(ctx context.Context, musicbrainzID *uuid.UUID
|
|||
|
||||
const getArtistByName = `-- name: GetArtistByName :one
|
||||
WITH artist_with_aliases AS (
|
||||
SELECT
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
COALESCE(array_agg(aa.alias), '{}')::text[] AS aliases
|
||||
FROM artists_with_name a
|
||||
|
|
@ -198,7 +222,7 @@ func (q *Queries) GetArtistByName(ctx context.Context, alias string) (GetArtistB
|
|||
}
|
||||
|
||||
const getReleaseArtists = `-- name: GetReleaseArtists :many
|
||||
SELECT
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
ar.is_primary as is_primary
|
||||
FROM artists_with_name a
|
||||
|
|
@ -307,7 +331,7 @@ func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsP
|
|||
}
|
||||
|
||||
const getTrackArtists = `-- name: GetTrackArtists :many
|
||||
SELECT
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
at.is_primary as is_primary
|
||||
FROM artists_with_name a
|
||||
|
|
|
|||
|
|
@ -30,9 +30,32 @@ func (q *Queries) AssociateArtistToRelease(ctx context.Context, arg AssociateArt
|
|||
return err
|
||||
}
|
||||
|
||||
const countNewReleases = `-- name: CountNewReleases :one
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM (
|
||||
SELECT t.release_id
|
||||
FROM listens l
|
||||
JOIN tracks t ON l.track_id = t.id
|
||||
GROUP BY t.release_id
|
||||
HAVING MIN(l.listened_at) BETWEEN $1 AND $2
|
||||
) first_appearances
|
||||
`
|
||||
|
||||
type CountNewReleasesParams struct {
|
||||
ListenedAt time.Time
|
||||
ListenedAt_2 time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) CountNewReleases(ctx context.Context, arg CountNewReleasesParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countNewReleases, arg.ListenedAt, arg.ListenedAt_2)
|
||||
var total_count int64
|
||||
err := row.Scan(&total_count)
|
||||
return total_count, err
|
||||
}
|
||||
|
||||
const countReleasesFromArtist = `-- name: CountReleasesFromArtist :one
|
||||
SELECT COUNT(*)
|
||||
FROM releases r
|
||||
FROM releases r
|
||||
JOIN artist_releases ar ON r.id = ar.release_id
|
||||
WHERE ar.artist_id = $1
|
||||
`
|
||||
|
|
@ -86,7 +109,7 @@ func (q *Queries) DeleteReleasesFromArtist(ctx context.Context, artistID int32)
|
|||
}
|
||||
|
||||
const getRelease = `-- name: GetRelease :one
|
||||
SELECT
|
||||
SELECT
|
||||
id, musicbrainz_id, image, various_artists, image_source, title,
|
||||
get_artists_for_release(id) AS artists
|
||||
FROM releases_with_title
|
||||
|
|
@ -213,8 +236,8 @@ const getReleasesWithoutImages = `-- name: GetReleasesWithoutImages :many
|
|||
SELECT
|
||||
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
||||
get_artists_for_release(r.id) AS artists
|
||||
FROM releases_with_title r
|
||||
WHERE r.image IS NULL
|
||||
FROM releases_with_title r
|
||||
WHERE r.image IS NULL
|
||||
AND r.id > $2
|
||||
ORDER BY r.id ASC
|
||||
LIMIT $1
|
||||
|
|
|
|||
|
|
@ -29,6 +29,28 @@ func (q *Queries) AssociateArtistToTrack(ctx context.Context, arg AssociateArtis
|
|||
return err
|
||||
}
|
||||
|
||||
const countNewTracks = `-- name: CountNewTracks :one
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM (
|
||||
SELECT track_id
|
||||
FROM listens
|
||||
GROUP BY track_id
|
||||
HAVING MIN(listened_at) BETWEEN $1 AND $2
|
||||
) first_appearances
|
||||
`
|
||||
|
||||
type CountNewTracksParams struct {
|
||||
ListenedAt time.Time
|
||||
ListenedAt_2 time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) CountNewTracks(ctx context.Context, arg CountNewTracksParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countNewTracks, arg.ListenedAt, arg.ListenedAt_2)
|
||||
var total_count int64
|
||||
err := row.Scan(&total_count)
|
||||
return total_count, err
|
||||
}
|
||||
|
||||
const countTopTracks = `-- name: CountTopTracks :one
|
||||
SELECT COUNT(DISTINCT l.track_id) AS total_count
|
||||
FROM listens l
|
||||
|
|
@ -343,7 +365,7 @@ func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPag
|
|||
}
|
||||
|
||||
const getTrack = `-- name: GetTrack :one
|
||||
SELECT
|
||||
SELECT
|
||||
t.id, t.musicbrainz_id, t.duration, t.release_id, t.title,
|
||||
get_artists_for_track(t.id) AS artists,
|
||||
r.image
|
||||
|
|
|
|||
186
internal/summary/image.go
Normal file
186
internal/summary/image.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/jpeg"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
var (
|
||||
assetPath = path.Join("..", "..", "assets")
|
||||
titleFontPath = path.Join(assetPath, "LeagueSpartan-Medium.ttf")
|
||||
textFontPath = path.Join(assetPath, "Jost-Regular.ttf")
|
||||
paddingLg = 30
|
||||
paddingMd = 20
|
||||
paddingSm = 6
|
||||
featuredImageSize = 180
|
||||
titleFontSize = 48.0
|
||||
textFontSize = 16.0
|
||||
featureTextStart = paddingLg + paddingMd + featuredImageSize
|
||||
)
|
||||
|
||||
// lots of code borrowed from https://medium.com/@daniel.ruizcamacho/how-to-create-an-image-in-golang-step-by-step-4416affe088f
|
||||
// func GenerateImage(summary *Summary) error {
|
||||
// base := image.NewRGBA(image.Rect(0, 0, 750, 1100))
|
||||
// draw.Draw(base, base.Bounds(), image.NewUniform(color.Black), image.Pt(0, 0), draw.Over)
|
||||
|
||||
// file, err := os.Create(path.Join(cfg.ConfigDir(), "summary.png"))
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// defer file.Close()
|
||||
|
||||
// // add title
|
||||
// if err := addText(base, summary.Title, "", image.Pt(paddingLg, 60), titleFontPath, titleFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// // add images
|
||||
// if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120), featuredImageSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)), featuredImageSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)*2), featuredImageSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// // top artists text
|
||||
// if err := addText(base, "Top Artists", "", image.Pt(featureTextStart, 132), textFontPath, textFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// for rank, artist := range summary.TopArtists {
|
||||
// if rank == 0 {
|
||||
// if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// } else {
|
||||
// if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// // top albums text
|
||||
// if err := addText(base, "Top Albums", "", image.Pt(featureTextStart, 132+featuredImageSize+paddingLg), textFontPath, textFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// for rank, album := range summary.TopAlbums {
|
||||
// if rank == 0 {
|
||||
// if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// } else {
|
||||
// if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: %w", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// // top tracks text
|
||||
|
||||
// // stats text
|
||||
|
||||
// if err := png.Encode(file, base); err != nil {
|
||||
// return fmt.Errorf("GenerateImage: png.Encode: %w", err)
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func addImage(baseImage *image.RGBA, path string, point image.Point, height int) error {
|
||||
templateFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, _, err := image.Decode(templateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resized := resize(template, height, height)
|
||||
|
||||
draw.Draw(baseImage, baseImage.Bounds(), resized, point, draw.Over)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addText(baseImage *image.RGBA, text, subtext string, point image.Point, fontFile string, fontSize float64) error {
|
||||
fontBytes, err := os.ReadFile(fontFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ttf, err := opentype.Parse(fontBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
face, err := opentype.NewFace(ttf, &opentype.FaceOptions{
|
||||
Size: fontSize,
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
drawer := &font.Drawer{
|
||||
Dst: baseImage,
|
||||
Src: image.NewUniform(color.White),
|
||||
Face: face,
|
||||
Dot: fixed.Point26_6{
|
||||
X: fixed.I(point.X),
|
||||
Y: fixed.I(point.Y),
|
||||
},
|
||||
}
|
||||
|
||||
drawer.DrawString(text)
|
||||
if subtext != "" {
|
||||
face, err = opentype.NewFace(ttf, &opentype.FaceOptions{
|
||||
Size: textFontSize,
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
drawer.Face = face
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
drawer.Src = image.NewUniform(color.RGBA{200, 200, 200, 255})
|
||||
drawer.DrawString(" - ")
|
||||
drawer.DrawString(subtext)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resize(m image.Image, w, h int) *image.RGBA {
|
||||
if w < 0 || h < 0 {
|
||||
return nil
|
||||
}
|
||||
r := m.Bounds()
|
||||
if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 {
|
||||
return image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
}
|
||||
curw, curh := r.Dx(), r.Dy()
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := range h {
|
||||
for x := range w {
|
||||
// Get a source pixel.
|
||||
subx := x * curw / w
|
||||
suby := y * curh / h
|
||||
r32, g32, b32, a32 := m.At(subx, suby).RGBA()
|
||||
r := uint8(r32 >> 8)
|
||||
g := uint8(g32 >> 8)
|
||||
b := uint8(b32 >> 8)
|
||||
a := uint8(a32 >> 8)
|
||||
img.SetRGBA(x, y, color.RGBA{r, g, b, a})
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
141
internal/summary/summary.go
Normal file
141
internal/summary/summary.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
"github.com/gabehf/koito/internal/models"
|
||||
)
|
||||
|
||||
type Summary struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
TopArtists []*models.Artist `json:"top_artists"` // ListenCount and TimeListened are overriden with stats from timeframe
|
||||
TopAlbums []*models.Album `json:"top_albums"` // ListenCount and TimeListened are overriden with stats from timeframe
|
||||
TopTracks []*models.Track `json:"top_tracks"` // ListenCount and TimeListened are overriden with stats from timeframe
|
||||
MinutesListened int `json:"minutes_listened"`
|
||||
AvgMinutesPerDay int `json:"avg_minutes_listened_per_day"`
|
||||
Plays int `json:"plays"`
|
||||
AvgPlaysPerDay float32 `json:"avg_plays_per_day"`
|
||||
UniqueTracks int `json:"unique_tracks"`
|
||||
UniqueAlbums int `json:"unique_albums"`
|
||||
UniqueArtists int `json:"unique_artists"`
|
||||
NewTracks int `json:"new_tracks"`
|
||||
NewAlbums int `json:"new_albums"`
|
||||
NewArtists int `json:"new_artists"`
|
||||
}
|
||||
|
||||
func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe db.Timeframe, title string) (summary *Summary, err error) {
|
||||
// l := logger.FromContext(ctx)
|
||||
|
||||
summary = new(Summary)
|
||||
|
||||
topArtists, err := store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, From: timeframe.T1u, To: timeframe.T2u, Period: timeframe.Period})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.TopArtists = topArtists.Items
|
||||
// replace ListenCount and TimeListened with stats from timeframe
|
||||
for i, artist := range summary.TopArtists {
|
||||
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ArtistID: artist.ID, Timeframe: timeframe})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{ArtistID: artist.ID, Timeframe: timeframe})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.TopArtists[i].TimeListened = timelistened
|
||||
summary.TopArtists[i].ListenCount = listens
|
||||
}
|
||||
|
||||
topAlbums, err := store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, From: timeframe.T1u, To: timeframe.T2u, Period: timeframe.Period})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.TopAlbums = topAlbums.Items
|
||||
// replace ListenCount and TimeListened with stats from timeframe
|
||||
for i, album := range summary.TopAlbums {
|
||||
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{AlbumID: album.ID, Timeframe: timeframe})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{AlbumID: album.ID, Timeframe: timeframe})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.TopAlbums[i].TimeListened = timelistened
|
||||
summary.TopAlbums[i].ListenCount = listens
|
||||
}
|
||||
|
||||
topTracks, err := store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, From: timeframe.T1u, To: timeframe.T2u, Period: timeframe.Period})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.TopTracks = topTracks.Items
|
||||
// replace ListenCount and TimeListened with stats from timeframe
|
||||
for i, track := range summary.TopTracks {
|
||||
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{TrackID: track.ID, Timeframe: timeframe})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{TrackID: track.ID, Timeframe: timeframe})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.TopTracks[i].TimeListened = timelistened
|
||||
summary.TopTracks[i].ListenCount = listens
|
||||
}
|
||||
|
||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||
daycount := int(t2.Sub(t1).Hours() / 24)
|
||||
// bandaid
|
||||
if daycount == 0 {
|
||||
daycount = 1
|
||||
}
|
||||
|
||||
tmp, err := store.CountTimeListened(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.MinutesListened = int(tmp) / 60
|
||||
summary.AvgMinutesPerDay = summary.MinutesListened / daycount
|
||||
tmp, err = store.CountListens(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.Plays = int(tmp)
|
||||
summary.AvgPlaysPerDay = float32(summary.Plays) / float32(daycount)
|
||||
tmp, err = store.CountTracks(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.UniqueTracks = int(tmp)
|
||||
tmp, err = store.CountAlbums(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.UniqueAlbums = int(tmp)
|
||||
tmp, err = store.CountArtists(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.UniqueArtists = int(tmp)
|
||||
tmp, err = store.CountNewTracks(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.NewTracks = int(tmp)
|
||||
tmp, err = store.CountNewAlbums(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.NewAlbums = int(tmp)
|
||||
tmp, err = store.CountNewArtists(ctx, timeframe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||
}
|
||||
summary.NewArtists = int(tmp)
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
BIN
internal/summary/summary.png
Normal file
BIN
internal/summary/summary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
84
internal/summary/summary_test.go
Normal file
84
internal/summary/summary_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package summary_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gabehf/koito/internal/cfg"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.M) {
|
||||
// dir, err := utils.GenerateRandomString(8)
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
cfg.Load(func(env string) string {
|
||||
switch env {
|
||||
case cfg.ENABLE_STRUCTURED_LOGGING_ENV:
|
||||
return "true"
|
||||
case cfg.LOG_LEVEL_ENV:
|
||||
return "debug"
|
||||
case cfg.DATABASE_URL_ENV:
|
||||
return "postgres://postgres:secret@localhost"
|
||||
case cfg.CONFIG_DIR_ENV:
|
||||
return "."
|
||||
case cfg.DISABLE_DEEZER_ENV, cfg.DISABLE_COVER_ART_ARCHIVE_ENV, cfg.DISABLE_MUSICBRAINZ_ENV, cfg.ENABLE_FULL_IMAGE_CACHE_ENV:
|
||||
return "true"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}, "test")
|
||||
t.Run()
|
||||
}
|
||||
|
||||
func TestGenerateSummary(t *testing.T) {
|
||||
// s := summary.Summary{
|
||||
// Title: "20XX Rewind",
|
||||
// TopArtistImage: path.Join("..", "..", "test_assets", "yuu.jpg"),
|
||||
// TopArtists: []struct {
|
||||
// Name string
|
||||
// Plays int
|
||||
// MinutesListened int
|
||||
// }{
|
||||
// {"CHUU", 738, 7321},
|
||||
// {"Paramore", 738, 7321},
|
||||
// {"ano", 738, 7321},
|
||||
// {"NELKE", 738, 7321},
|
||||
// {"ILLIT", 738, 7321},
|
||||
// },
|
||||
// TopAlbumImage: "",
|
||||
// TopAlbums: []struct {
|
||||
// Title string
|
||||
// Plays int
|
||||
// MinutesListened int
|
||||
// }{
|
||||
// {"Only cry in the rain", 738, 7321},
|
||||
// {"Paramore", 738, 7321},
|
||||
// {"ano", 738, 7321},
|
||||
// {"NELKE", 738, 7321},
|
||||
// {"ILLIT", 738, 7321},
|
||||
// },
|
||||
// TopTrackImage: "",
|
||||
// TopTracks: []struct {
|
||||
// Title string
|
||||
// Plays int
|
||||
// MinutesListened int
|
||||
// }{
|
||||
// {"虹の色よ鮮やかであれ (NELKE ver.)", 321, 12351},
|
||||
// {"Paramore", 738, 7321},
|
||||
// {"ano", 738, 7321},
|
||||
// {"NELKE", 738, 7321},
|
||||
// {"ILLIT", 738, 7321},
|
||||
// },
|
||||
// MinutesListened: 0,
|
||||
// Plays: 0,
|
||||
// AvgPlaysPerDay: 0,
|
||||
// UniqueTracks: 0,
|
||||
// UniqueAlbums: 0,
|
||||
// UniqueArtists: 0,
|
||||
// NewTracks: 0,
|
||||
// NewAlbums: 0,
|
||||
// NewArtists: 0,
|
||||
// }
|
||||
|
||||
// assert.NoError(t, summary.GenerateImage(&s))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue