wip: endpoint working

This commit is contained in:
Gabe Farrell 2025-12-30 03:33:02 -05:00
parent 3b585f748a
commit 6b73f83484
19 changed files with 510 additions and 243 deletions

View file

@ -27,10 +27,10 @@ postgres.remove:
postgres.remove-scratch: postgres.remove-scratch:
docker stop koito-scratch && docker rm koito-scratch docker stop koito-scratch && docker rm koito-scratch
api.debug: api.debug: postgres.start
KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5432?sslmode=disable go run cmd/api/main.go KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5432?sslmode=disable go run cmd/api/main.go
api.scratch: api.scratch: postgres.run-scratch
KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir/scratch KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5433?sslmode=disable go run cmd/api/main.go KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir/scratch KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5433?sslmode=disable go run cmd/api/main.go
api.test: api.test:

View file

@ -0,0 +1,28 @@
package handlers
import (
"net/http"
"github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/internal/summary"
"github.com/gabehf/koito/internal/utils"
)
func SummaryHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("GetTopAlbumsHandler: Received request to retrieve top albums")
timeframe := TimeframeFromRequest(r)
summary, err := summary.GenerateSummary(ctx, store, 1, timeframe, "")
if err != nil {
l.Err(err).Int("userid", 1).Any("timeframe", timeframe).Msgf("SummaryHandler: Failed to generate summary")
utils.WriteError(w, "failed to generate summary", http.StatusInternalServerError)
return
}
utils.WriteJSON(w, http.StatusOK, summary)
}
}

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger" "github.com/gabehf/koito/internal/logger"
@ -81,10 +82,93 @@ func OptsFromRequest(r *http.Request) db.GetItemsOpts {
Week: week, Week: week,
Month: month, Month: month,
Year: year, Year: year,
From: from, From: int64(from),
To: to, To: int64(to),
ArtistID: artistId, ArtistID: artistId,
AlbumID: albumId, AlbumID: albumId,
TrackID: trackId, TrackID: trackId,
} }
} }
// Takes a request and returns a db.Timeframe representing the week, month, year, period, or unix
// time range specified by the request parameters
func TimeframeFromRequest(r *http.Request) db.Timeframe {
opts := OptsFromRequest(r)
now := time.Now()
loc := now.Location()
// if 'from' is set, but 'to' is not set, assume 'to' should be now
if opts.From != 0 && opts.To == 0 {
opts.To = now.Unix()
}
// YEAR
if opts.Year != 0 && opts.Month == 0 && opts.Week == 0 {
start := time.Date(opts.Year, 1, 1, 0, 0, 0, 0, loc)
end := time.Date(opts.Year+1, 1, 1, 0, 0, 0, 0, loc).Add(-time.Second)
opts.From = start.Unix()
opts.To = end.Unix()
}
// MONTH (+ optional year)
if opts.Month != 0 {
year := opts.Year
if year == 0 {
year = now.Year()
if int(now.Month()) < opts.Month {
year--
}
}
start := time.Date(year, time.Month(opts.Month), 1, 0, 0, 0, 0, loc)
end := endOfMonth(year, time.Month(opts.Month), loc)
opts.From = start.Unix()
opts.To = end.Unix()
}
// WEEK (+ optional year)
if opts.Week != 0 {
year := opts.Year
if year == 0 {
year = now.Year()
_, currentWeek := now.ISOWeek()
if currentWeek < opts.Week {
year--
}
}
// ISO week 1 is defined as the week with Jan 4 in it
jan4 := time.Date(year, 1, 4, 0, 0, 0, 0, loc)
week1Start := startOfWeek(jan4)
start := week1Start.AddDate(0, 0, (opts.Week-1)*7)
end := endOfWeek(start)
opts.From = start.Unix()
opts.To = end.Unix()
}
return db.Timeframe{
Period: opts.Period,
T1u: opts.From,
T2u: opts.To,
}
}
func startOfWeek(t time.Time) time.Time {
// ISO week: Monday = 1
weekday := int(t.Weekday())
if weekday == 0 { // Sunday
weekday = 7
}
return time.Date(t.Year(), t.Month(), t.Day()-weekday+1, 0, 0, 0, 0, t.Location())
}
func endOfWeek(t time.Time) time.Time {
return startOfWeek(t).AddDate(0, 0, 7).Add(-time.Second)
}
func endOfMonth(year int, month time.Month, loc *time.Location) time.Time {
startNextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, loc)
return startNextMonth.Add(-time.Second)
}

View file

@ -54,6 +54,7 @@ func bindRoutes(
r.Get("/stats", handlers.StatsHandler(db)) r.Get("/stats", handlers.StatsHandler(db))
r.Get("/search", handlers.SearchHandler(db)) r.Get("/search", handlers.SearchHandler(db))
r.Get("/aliases", handlers.GetAliasesHandler(db)) r.Get("/aliases", handlers.GetAliasesHandler(db))
r.Get("/summary", handlers.SummaryHandler(db))
}) })
r.Post("/logout", handlers.LogoutHandler(db)) r.Post("/logout", handlers.LogoutHandler(db))
if !cfg.RateLimitDisabled() { if !cfg.RateLimitDisabled() {

View file

@ -11,6 +11,7 @@ import (
type DB interface { type DB interface {
// Get // Get
GetArtist(ctx context.Context, opts GetArtistOpts) (*models.Artist, error) GetArtist(ctx context.Context, opts GetArtistOpts) (*models.Artist, error)
GetAlbum(ctx context.Context, opts GetAlbumOpts) (*models.Album, error) GetAlbum(ctx context.Context, opts GetAlbumOpts) (*models.Album, error)
GetTrack(ctx context.Context, opts GetTrackOpts) (*models.Track, 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) GetUserBySession(ctx context.Context, sessionId uuid.UUID) (*models.User, error)
GetUserByUsername(ctx context.Context, username string) (*models.User, error) GetUserByUsername(ctx context.Context, username string) (*models.User, error)
GetUserByApiKey(ctx context.Context, key string) (*models.User, error) GetUserByApiKey(ctx context.Context, key string) (*models.User, error)
// Save // Save
SaveArtist(ctx context.Context, opts SaveArtistOpts) (*models.Artist, error) SaveArtist(ctx context.Context, opts SaveArtistOpts) (*models.Artist, error)
SaveArtistAliases(ctx context.Context, id int32, aliases []string, source string) error SaveArtistAliases(ctx context.Context, id int32, aliases []string, source string) error
SaveAlbum(ctx context.Context, opts SaveAlbumOpts) (*models.Album, 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) SaveUser(ctx context.Context, opts SaveUserOpts) (*models.User, error)
SaveApiKey(ctx context.Context, opts SaveApiKeyOpts) (*models.ApiKey, error) SaveApiKey(ctx context.Context, opts SaveApiKeyOpts) (*models.ApiKey, error)
SaveSession(ctx context.Context, userId int32, expiresAt time.Time, persistent bool) (*models.Session, error) SaveSession(ctx context.Context, userId int32, expiresAt time.Time, persistent bool) (*models.Session, error)
// Update // Update
UpdateArtist(ctx context.Context, opts UpdateArtistOpts) error UpdateArtist(ctx context.Context, opts UpdateArtistOpts) error
UpdateTrack(ctx context.Context, opts UpdateTrackOpts) error UpdateTrack(ctx context.Context, opts UpdateTrackOpts) error
UpdateAlbum(ctx context.Context, opts UpdateAlbumOpts) error UpdateAlbum(ctx context.Context, opts UpdateAlbumOpts) error
@ -52,7 +57,9 @@ type DB interface {
SetPrimaryTrackAlias(ctx context.Context, id int32, alias string) error SetPrimaryTrackAlias(ctx context.Context, id int32, alias string) error
SetPrimaryAlbumArtist(ctx context.Context, id int32, artistId int32, value bool) error SetPrimaryAlbumArtist(ctx context.Context, id int32, artistId int32, value bool) error
SetPrimaryTrackArtist(ctx context.Context, id int32, artistId int32, value bool) error SetPrimaryTrackArtist(ctx context.Context, id int32, artistId int32, value bool) error
// Delete // Delete
DeleteArtist(ctx context.Context, id int32) error DeleteArtist(ctx context.Context, id int32) error
DeleteAlbum(ctx context.Context, id int32) error DeleteAlbum(ctx context.Context, id int32) error
DeleteTrack(ctx context.Context, id int32) error DeleteTrack(ctx context.Context, id int32) error
@ -62,26 +69,36 @@ type DB interface {
DeleteTrackAlias(ctx context.Context, id int32, alias string) error DeleteTrackAlias(ctx context.Context, id int32, alias string) error
DeleteSession(ctx context.Context, sessionId uuid.UUID) error DeleteSession(ctx context.Context, sessionId uuid.UUID) error
DeleteApiKey(ctx context.Context, id int32) error DeleteApiKey(ctx context.Context, id int32) error
// Count // Count
CountListens(ctx context.Context, timeframe Timeframe) (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) CountTracks(ctx context.Context, timeframe Timeframe) (int64, error)
CountAlbums(ctx context.Context, timeframe Timeframe) (int64, error) CountAlbums(ctx context.Context, timeframe Timeframe) (int64, error)
CountArtists(ctx context.Context, timeframe Timeframe) (int64, error) CountArtists(ctx context.Context, timeframe Timeframe) (int64, error)
CountNewTracks(ctx context.Context, timeframe Timeframe) (int64, error) CountNewTracks(ctx context.Context, timeframe Timeframe) (int64, error)
CountNewAlbums(ctx context.Context, timeframe Timeframe) (int64, error) CountNewAlbums(ctx context.Context, timeframe Timeframe) (int64, error)
CountNewArtists(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) CountTimeListened(ctx context.Context, timeframe Timeframe) (int64, error)
// in seconds
CountTimeListenedToItem(ctx context.Context, opts TimeListenedOpts) (int64, error) CountTimeListenedToItem(ctx context.Context, opts TimeListenedOpts) (int64, error)
CountUsers(ctx context.Context) (int64, error) CountUsers(ctx context.Context) (int64, error)
// Search // Search
SearchArtists(ctx context.Context, q string) ([]*models.Artist, error) SearchArtists(ctx context.Context, q string) ([]*models.Artist, error)
SearchAlbums(ctx context.Context, q string) ([]*models.Album, error) SearchAlbums(ctx context.Context, q string) ([]*models.Album, error)
SearchTracks(ctx context.Context, q string) ([]*models.Track, error) SearchTracks(ctx context.Context, q string) ([]*models.Track, error)
// Merge // Merge
MergeTracks(ctx context.Context, fromId, toId int32) error MergeTracks(ctx context.Context, fromId, toId int32) error
MergeAlbums(ctx context.Context, fromId, toId int32, replaceImage bool) error MergeAlbums(ctx context.Context, fromId, toId int32, replaceImage bool) error
MergeArtists(ctx context.Context, fromId, toId int32, replaceImage bool) error MergeArtists(ctx context.Context, fromId, toId int32, replaceImage bool) error
// Etc // Etc
ImageHasAssociation(ctx context.Context, image uuid.UUID) (bool, error) ImageHasAssociation(ctx context.Context, image uuid.UUID) (bool, error)
GetImageSource(ctx context.Context, image uuid.UUID) (string, error) GetImageSource(ctx context.Context, image uuid.UUID) (string, error)
AlbumsWithoutImages(ctx context.Context, from int32) ([]*models.Album, error) AlbumsWithoutImages(ctx context.Context, from int32) ([]*models.Album, error)

View file

@ -122,8 +122,8 @@ type GetItemsOpts struct {
Week int // 1-52 Week int // 1-52
Month int // 1-12 Month int // 1-12
Year int Year int
From int // unix timestamp From int64 // unix timestamp
To int // unix timestamp To int64 // unix timestamp
// Used only for getting top tracks // Used only for getting top tracks
ArtistID int ArtistID int
@ -144,10 +144,10 @@ type ListenActivityOpts struct {
} }
type TimeListenedOpts struct { type TimeListenedOpts struct {
Period Period Timeframe Timeframe
AlbumID int32 AlbumID int32
ArtistID int32 ArtistID int32
TrackID int32 TrackID int32
} }
type GetExportPageOpts struct { type GetExportPageOpts struct {

View file

@ -12,6 +12,17 @@ type Timeframe struct {
T2u 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 type Period string
const ( const (

View file

@ -91,8 +91,8 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
} }
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
Period: db.PeriodAllTime, Timeframe: db.Timeframe{Period: db.PeriodAllTime},
AlbumID: ret.ID, AlbumID: ret.ID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err) return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)

View file

@ -35,8 +35,8 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err) return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
} }
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
Period: db.PeriodAllTime, Timeframe: db.Timeframe{Period: db.PeriodAllTime},
ArtistID: row.ID, ArtistID: row.ID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) 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) return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
} }
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
Period: db.PeriodAllTime, Timeframe: db.Timeframe{Period: db.PeriodAllTime},
ArtistID: row.ID, ArtistID: row.ID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) 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) return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
} }
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
Period: db.PeriodAllTime, Timeframe: db.Timeframe{Period: db.PeriodAllTime},
ArtistID: row.ID, ArtistID: row.ID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)

View file

@ -4,21 +4,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/repository" "github.com/gabehf/koito/internal/repository"
) )
func (p *Psql) CountListens(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountListens(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountListens(ctx, repository.CountListensParams{ count, err := p.q.CountListens(ctx, repository.CountListensParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -30,14 +22,7 @@ func (p *Psql) CountListens(ctx context.Context, timeframe db.Timeframe) (int64,
} }
func (p *Psql) CountTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountTopTracks(ctx, repository.CountTopTracksParams{ count, err := p.q.CountTopTracks(ctx, repository.CountTopTracksParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -49,14 +34,7 @@ func (p *Psql) CountTracks(ctx context.Context, timeframe db.Timeframe) (int64,
} }
func (p *Psql) CountAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountTopReleases(ctx, repository.CountTopReleasesParams{ count, err := p.q.CountTopReleases(ctx, repository.CountTopReleasesParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -68,14 +46,7 @@ func (p *Psql) CountAlbums(ctx context.Context, timeframe db.Timeframe) (int64,
} }
func (p *Psql) CountArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountTopArtists(ctx, repository.CountTopArtistsParams{ count, err := p.q.CountTopArtists(ctx, repository.CountTopArtistsParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -86,15 +57,9 @@ func (p *Psql) CountArtists(ctx context.Context, timeframe db.Timeframe) (int64,
return count, nil return count, nil
} }
// in seconds
func (p *Psql) CountTimeListened(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountTimeListened(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountTimeListened(ctx, repository.CountTimeListenedParams{ count, err := p.q.CountTimeListened(ctx, repository.CountTimeListenedParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -105,9 +70,9 @@ func (p *Psql) CountTimeListened(ctx context.Context, timeframe db.Timeframe) (i
return count, nil return count, nil
} }
// in seconds
func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListenedOpts) (int64, error) { func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListenedOpts) (int64, error) {
t2 := time.Now() t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
t1 := db.StartTimeFromPeriod(opts.Period)
if opts.ArtistID > 0 { if opts.ArtistID > 0 {
count, err := p.q.CountTimeListenedToArtist(ctx, repository.CountTimeListenedToArtistParams{ count, err := p.q.CountTimeListenedToArtist(ctx, repository.CountTimeListenedToArtistParams{
@ -143,15 +108,45 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened
return 0, errors.New("CountTimeListenedToItem: an id must be provided") return 0, errors.New("CountTimeListenedToItem: an id must be provided")
} }
func (p *Psql) CountNewTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountListensToItem(ctx context.Context, opts db.TimeListenedOpts) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now() if opts.ArtistID > 0 {
t1 = db.StartTimeFromPeriod(timeframe.Period) count, err := p.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{
} else { ListenedAt: t1,
t1 = time.Unix(timeframe.T1u, 0) ListenedAt_2: t2,
t2 = time.Unix(timeframe.T2u, 0) 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{ count, err := p.q.CountNewTracks(ctx, repository.CountNewTracksParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -163,14 +158,7 @@ func (p *Psql) CountNewTracks(ctx context.Context, timeframe db.Timeframe) (int6
} }
func (p *Psql) CountNewAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountNewAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountNewReleases(ctx, repository.CountNewReleasesParams{ count, err := p.q.CountNewReleases(ctx, repository.CountNewReleasesParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,
@ -182,14 +170,7 @@ func (p *Psql) CountNewAlbums(ctx context.Context, timeframe db.Timeframe) (int6
} }
func (p *Psql) CountNewArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) { func (p *Psql) CountNewArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time t1, t2 := db.TimeframeToTimeRange(timeframe)
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountNewArtists(ctx, repository.CountNewArtistsParams{ count, err := p.q.CountNewArtists(ctx, repository.CountNewArtistsParams{
ListenedAt: t1, ListenedAt: t1,
ListenedAt_2: t2, ListenedAt_2: t2,

View file

@ -131,7 +131,7 @@ func TestCountTimeListenedToArtist(t *testing.T) {
ctx := context.Background() ctx := context.Background()
testDataForTopItems(t) testDataForTopItems(t)
period := db.PeriodAllTime 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) require.NoError(t, err)
assert.EqualValues(t, 400, count) assert.EqualValues(t, 400, count)
truncateTestData(t) truncateTestData(t)
@ -141,7 +141,7 @@ func TestCountTimeListenedToAlbum(t *testing.T) {
ctx := context.Background() ctx := context.Background()
testDataForTopItems(t) testDataForTopItems(t)
period := db.PeriodAllTime 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) require.NoError(t, err)
assert.EqualValues(t, 300, count) assert.EqualValues(t, 300, count)
truncateTestData(t) truncateTestData(t)
@ -151,8 +151,38 @@ func TestCountTimeListenedToTrack(t *testing.T) {
ctx := context.Background() ctx := context.Background()
testDataForTopItems(t) testDataForTopItems(t)
period := db.PeriodAllTime 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) require.NoError(t, err)
assert.EqualValues(t, 200, count) assert.EqualValues(t, 200, count)
truncateTestData(t) 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)
}

View file

@ -25,6 +25,10 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
t2 = time.Now() t2 = time.Now()
t1 = db.StartTimeFromPeriod(opts.Period) 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 { if opts.Limit == 0 {
opts.Limit = DefaultItemsPerPage opts.Limit = DefaultItemsPerPage
} }

View file

@ -24,6 +24,10 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
t2 = time.Now() t2 = time.Now()
t1 = db.StartTimeFromPeriod(opts.Period) 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 { if opts.Limit == 0 {
opts.Limit = DefaultItemsPerPage opts.Limit = DefaultItemsPerPage
} }

View file

@ -25,6 +25,10 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
t2 = time.Now() t2 = time.Now()
t1 = db.StartTimeFromPeriod(opts.Period) 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 { if opts.Limit == 0 {
opts.Limit = DefaultItemsPerPage opts.Limit = DefaultItemsPerPage
} }

View file

@ -82,8 +82,8 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
} }
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
Period: db.PeriodAllTime, Timeframe: db.Timeframe{Period: db.PeriodAllTime},
TrackID: track.ID, TrackID: track.ID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err) return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err)

View file

@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"path" "path"
"strings" "strings"
"time" "time"
@ -34,15 +33,16 @@ func ImportListenBrainzExport(ctx context.Context, store db.DB, mbzc mbz.MusicBr
for _, f := range r.File { for _, f := range r.File {
if f.FileInfo().IsDir() { if f.FileInfo().IsDir() {
l.Debug().Msgf("File %s is dir, skipping...", f.Name)
continue continue
} }
if strings.HasPrefix(f.Name, "listens/") && strings.HasSuffix(f.Name, ".jsonl") { 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() rc, err := f.Open()
if err != nil { 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 continue
} }
@ -75,7 +75,7 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai
payload := new(handlers.LbzSubmitListenPayload) payload := new(handlers.LbzSubmitListenPayload)
err := json.Unmarshal(line, payload) err := json.Unmarshal(line, payload)
if err != nil { if err != nil {
fmt.Println("Error unmarshaling JSON:", err) l.Err(err).Msg("Error unmarshaling JSON")
continue continue
} }
ts := time.Unix(payload.ListenedAt, 0) ts := time.Unix(payload.ListenedAt, 0)

View file

@ -1,54 +1,19 @@
package summary package summary
import ( import (
"fmt"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
_ "image/jpeg" _ "image/jpeg"
"image/png"
"os" "os"
"path" "path"
"strconv"
"github.com/gabehf/koito/internal/cfg"
"golang.org/x/image/font" "golang.org/x/image/font"
"golang.org/x/image/font/opentype" "golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
_ "golang.org/x/image/webp" _ "golang.org/x/image/webp"
) )
type Summary struct {
Title string
TopArtistImage string
TopArtists []struct {
Name string
Plays int
MinutesListened int
}
TopAlbumImage string
TopAlbums []struct {
Title string
Plays int
MinutesListened int
}
TopTrackImage string
TopTracks []struct {
Title string
Plays int
MinutesListened int
}
MinutesListened int
Plays int
AvgPlaysPerDay float32
UniqueTracks int32
UniqueAlbums int32
UniqueArtists int32
NewTracks int32
NewAlbums int32
NewArtists int32
}
var ( var (
assetPath = path.Join("..", "..", "assets") assetPath = path.Join("..", "..", "assets")
titleFontPath = path.Join(assetPath, "LeagueSpartan-Medium.ttf") titleFontPath = path.Join(assetPath, "LeagueSpartan-Medium.ttf")
@ -63,69 +28,69 @@ var (
) )
// lots of code borrowed from https://medium.com/@daniel.ruizcamacho/how-to-create-an-image-in-golang-step-by-step-4416affe088f // 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 { // func GenerateImage(summary *Summary) error {
base := image.NewRGBA(image.Rect(0, 0, 750, 1100)) // base := image.NewRGBA(image.Rect(0, 0, 750, 1100))
draw.Draw(base, base.Bounds(), image.NewUniform(color.Black), image.Pt(0, 0), draw.Over) // 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")) // file, err := os.Create(path.Join(cfg.ConfigDir(), "summary.png"))
if err != nil { // if err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
defer file.Close() // defer file.Close()
// add title // // add title
if err := addText(base, summary.Title, "", image.Pt(paddingLg, 60), titleFontPath, titleFontSize); err != nil { // if err := addText(base, summary.Title, "", image.Pt(paddingLg, 60), titleFontPath, titleFontSize); err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
// add images // // add images
if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120), featuredImageSize); err != nil { // if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120), featuredImageSize); err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)), featuredImageSize); err != nil { // if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)), featuredImageSize); err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)*2), featuredImageSize); err != nil { // if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)*2), featuredImageSize); err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
// top artists text // // top artists text
if err := addText(base, "Top Artists", "", image.Pt(featureTextStart, 132), textFontPath, textFontSize); err != nil { // if err := addText(base, "Top Artists", "", image.Pt(featureTextStart, 132), textFontPath, textFontSize); err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
for rank, artist := range summary.TopArtists { // for rank, artist := range summary.TopArtists {
if rank == 0 { // if rank == 0 {
if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil { // 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) // return fmt.Errorf("GenerateImage: %w", err)
} // }
} else { // } else {
if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil { // 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) // return fmt.Errorf("GenerateImage: %w", err)
} // }
} // }
} // }
// top albums text // // top albums text
if err := addText(base, "Top Albums", "", image.Pt(featureTextStart, 132+featuredImageSize+paddingLg), textFontPath, textFontSize); err != nil { // if err := addText(base, "Top Albums", "", image.Pt(featureTextStart, 132+featuredImageSize+paddingLg), textFontPath, textFontSize); err != nil {
return fmt.Errorf("GenerateImage: %w", err) // return fmt.Errorf("GenerateImage: %w", err)
} // }
for rank, album := range summary.TopAlbums { // for rank, album := range summary.TopAlbums {
if rank == 0 { // if rank == 0 {
if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil { // 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) // return fmt.Errorf("GenerateImage: %w", err)
} // }
} else { // } else {
if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil { // 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) // return fmt.Errorf("GenerateImage: %w", err)
} // }
} // }
} // }
// top tracks text // // top tracks text
// stats text // // stats text
if err := png.Encode(file, base); err != nil { // if err := png.Encode(file, base); err != nil {
return fmt.Errorf("GenerateImage: png.Encode: %w", err) // return fmt.Errorf("GenerateImage: png.Encode: %w", err)
} // }
return nil // return nil
} // }
func addImage(baseImage *image.RGBA, path string, point image.Point, height int) error { func addImage(baseImage *image.RGBA, path string, point image.Point, height int) error {
templateFile, err := os.Open(path) templateFile, err := os.Open(path)

141
internal/summary/summary.go Normal file
View 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
}

View file

@ -1,12 +1,9 @@
package summary_test package summary_test
import ( import (
"path"
"testing" "testing"
"github.com/gabehf/koito/internal/cfg" "github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/summary"
"github.com/stretchr/testify/assert"
) )
func TestMain(t *testing.M) { func TestMain(t *testing.M) {
@ -33,55 +30,55 @@ func TestMain(t *testing.M) {
t.Run() t.Run()
} }
func TestGenerateImage(t *testing.T) { func TestGenerateSummary(t *testing.T) {
s := summary.Summary{ // s := summary.Summary{
Title: "20XX Rewind", // Title: "20XX Rewind",
TopArtistImage: path.Join("..", "..", "test_assets", "yuu.jpg"), // TopArtistImage: path.Join("..", "..", "test_assets", "yuu.jpg"),
TopArtists: []struct { // TopArtists: []struct {
Name string // Name string
Plays int // Plays int
MinutesListened int // MinutesListened int
}{ // }{
{"CHUU", 738, 7321}, // {"CHUU", 738, 7321},
{"Paramore", 738, 7321}, // {"Paramore", 738, 7321},
{"ano", 738, 7321}, // {"ano", 738, 7321},
{"NELKE", 738, 7321}, // {"NELKE", 738, 7321},
{"ILLIT", 738, 7321}, // {"ILLIT", 738, 7321},
}, // },
TopAlbumImage: "", // TopAlbumImage: "",
TopAlbums: []struct { // TopAlbums: []struct {
Title string // Title string
Plays int // Plays int
MinutesListened int // MinutesListened int
}{ // }{
{"Only cry in the rain", 738, 7321}, // {"Only cry in the rain", 738, 7321},
{"Paramore", 738, 7321}, // {"Paramore", 738, 7321},
{"ano", 738, 7321}, // {"ano", 738, 7321},
{"NELKE", 738, 7321}, // {"NELKE", 738, 7321},
{"ILLIT", 738, 7321}, // {"ILLIT", 738, 7321},
}, // },
TopTrackImage: "", // TopTrackImage: "",
TopTracks: []struct { // TopTracks: []struct {
Title string // Title string
Plays int // Plays int
MinutesListened int // MinutesListened int
}{ // }{
{"虹の色よ鮮やかであれ (NELKE ver.)", 321, 12351}, // {"虹の色よ鮮やかであれ (NELKE ver.)", 321, 12351},
{"Paramore", 738, 7321}, // {"Paramore", 738, 7321},
{"ano", 738, 7321}, // {"ano", 738, 7321},
{"NELKE", 738, 7321}, // {"NELKE", 738, 7321},
{"ILLIT", 738, 7321}, // {"ILLIT", 738, 7321},
}, // },
MinutesListened: 0, // MinutesListened: 0,
Plays: 0, // Plays: 0,
AvgPlaysPerDay: 0, // AvgPlaysPerDay: 0,
UniqueTracks: 0, // UniqueTracks: 0,
UniqueAlbums: 0, // UniqueAlbums: 0,
UniqueArtists: 0, // UniqueArtists: 0,
NewTracks: 0, // NewTracks: 0,
NewAlbums: 0, // NewAlbums: 0,
NewArtists: 0, // NewArtists: 0,
} // }
assert.NoError(t, summary.GenerateImage(&s)) // assert.NoError(t, summary.GenerateImage(&s))
} }