mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
wip: endpoint working
This commit is contained in:
parent
3b585f748a
commit
6b73f83484
19 changed files with 510 additions and 243 deletions
4
Makefile
4
Makefile
|
|
@ -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:
|
||||||
|
|
|
||||||
28
engine/handlers/get_summary.go
Normal file
28
engine/handlers/get_summary.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue