diff --git a/Makefile b/Makefile index fbca22e..a6a79ca 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ postgres.schemadump: -v --dbname="koitodb" -f "/tmp/dump/schema.sql" postgres.run: - docker run --name koito-db -p 5432:5432 -e POSTGRES_PASSWORD=secret -d postgres + docker run --name koito-db -p 5432:5432 -e POSTGRES_PASSWORD=secret -e TZ=America/New_York -d postgres postgres.run-scratch: docker run --name koito-scratch -p 5433:5432 -e POSTGRES_PASSWORD=secret -d postgres diff --git a/db/queries/listen.sql b/db/queries/listen.sql index fc8c502..cba443a 100644 --- a/db/queries/listen.sql +++ b/db/queries/listen.sql @@ -4,7 +4,7 @@ VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING; -- name: GetLastListensPaginated :many -SELECT +SELECT l.*, t.title AS track_title, t.release_id AS release_id, @@ -16,31 +16,31 @@ ORDER BY l.listened_at DESC LIMIT $3 OFFSET $4; -- name: GetLastListensFromArtistPaginated :many -SELECT +SELECT l.*, t.title AS track_title, t.release_id AS release_id, get_artists_for_track(t.id) AS artists FROM listens l JOIN tracks_with_title t ON l.track_id = t.id -JOIN artist_tracks at ON t.id = at.track_id +JOIN artist_tracks at ON t.id = at.track_id WHERE at.artist_id = $5 AND l.listened_at BETWEEN $1 AND $2 ORDER BY l.listened_at DESC LIMIT $3 OFFSET $4; -- name: GetFirstListenFromArtist :one -SELECT +SELECT l.* FROM listens l JOIN tracks_with_title t ON l.track_id = t.id -JOIN artist_tracks at ON t.id = at.track_id +JOIN artist_tracks at ON t.id = at.track_id WHERE at.artist_id = $1 ORDER BY l.listened_at ASC LIMIT 1; -- name: GetLastListensFromReleasePaginated :many -SELECT +SELECT l.*, t.title AS track_title, t.release_id AS release_id, @@ -53,7 +53,7 @@ ORDER BY l.listened_at DESC LIMIT $3 OFFSET $4; -- name: GetFirstListenFromRelease :one -SELECT +SELECT l.* FROM listens l JOIN tracks t ON l.track_id = t.id @@ -62,7 +62,7 @@ ORDER BY l.listened_at ASC LIMIT 1; -- name: GetLastListensFromTrackPaginated :many -SELECT +SELECT l.*, t.title AS track_title, t.release_id AS release_id, @@ -75,7 +75,7 @@ ORDER BY l.listened_at DESC LIMIT $3 OFFSET $4; -- name: GetFirstListenFromTrack :one -SELECT +SELECT l.* FROM listens l JOIN tracks t ON l.track_id = t.id @@ -83,6 +83,13 @@ WHERE t.id = $1 ORDER BY l.listened_at ASC LIMIT 1; +-- name: GetFirstListen :one +SELECT + * +FROM listens +ORDER BY listened_at ASC +LIMIT 1; + -- name: CountListens :one SELECT COUNT(*) AS total_count FROM listens l @@ -138,20 +145,27 @@ WHERE l.listened_at BETWEEN $1 AND $2 -- name: ListenActivity :many WITH buckets AS ( - SELECT generate_series($1::timestamptz, $2::timestamptz, $3::interval) AS bucket_start + SELECT + d::date AS bucket_start_date, + (d + $3::interval)::date AS bucket_end_date + FROM generate_series( + $1::date, + $2::date, + $3::interval + ) AS d ), bucketed_listens AS ( SELECT - b.bucket_start, + b.bucket_start_date::timestamptz, COUNT(l.listened_at) AS listen_count FROM buckets b LEFT JOIN listens l - ON l.listened_at >= b.bucket_start - AND l.listened_at < b.bucket_start + $3::interval - GROUP BY b.bucket_start - ORDER BY b.bucket_start + ON l.listened_at >= b.bucket_start_date::timestamptz + AND l.listened_at < b.bucket_end_date::timestamptz + GROUP BY b.bucket_start_date + ORDER BY b.bucket_start_date ) -SELECT * FROM bucketed_listens; +SELECT bucketed_listens.bucket_start_date::timestamptz, listen_count FROM bucketed_listens; -- name: ListenActivityForArtist :many WITH buckets AS ( diff --git a/engine/handlers/get_listen_activity.go b/engine/handlers/get_listen_activity.go index 86cf71a..0e3526c 100644 --- a/engine/handlers/get_listen_activity.go +++ b/engine/handlers/get_listen_activity.go @@ -19,7 +19,7 @@ func GetListenActivityHandler(store db.DB) func(w http.ResponseWriter, r *http.R rangeStr := r.URL.Query().Get("range") _range, err := strconv.Atoi(rangeStr) - if err != nil { + if err != nil && rangeStr != "" { l.Debug().AnErr("error", err).Msg("GetListenActivityHandler: Invalid range parameter") utils.WriteError(w, "invalid range parameter", http.StatusBadRequest) return @@ -27,7 +27,7 @@ func GetListenActivityHandler(store db.DB) func(w http.ResponseWriter, r *http.R monthStr := r.URL.Query().Get("month") month, err := strconv.Atoi(monthStr) - if err != nil { + if err != nil && monthStr != "" { l.Debug().AnErr("error", err).Msg("GetListenActivityHandler: Invalid month parameter") utils.WriteError(w, "invalid month parameter", http.StatusBadRequest) return @@ -35,7 +35,7 @@ func GetListenActivityHandler(store db.DB) func(w http.ResponseWriter, r *http.R yearStr := r.URL.Query().Get("year") year, err := strconv.Atoi(yearStr) - if err != nil { + if err != nil && yearStr != "" { l.Debug().AnErr("error", err).Msg("GetListenActivityHandler: Invalid year parameter") utils.WriteError(w, "invalid year parameter", http.StatusBadRequest) return @@ -43,7 +43,7 @@ func GetListenActivityHandler(store db.DB) func(w http.ResponseWriter, r *http.R artistIdStr := r.URL.Query().Get("artist_id") artistId, err := strconv.Atoi(artistIdStr) - if err != nil { + if err != nil && artistIdStr != "" { l.Debug().AnErr("error", err).Msg("GetListenActivityHandler: Invalid artist ID parameter") utils.WriteError(w, "invalid artist ID parameter", http.StatusBadRequest) return @@ -51,7 +51,7 @@ func GetListenActivityHandler(store db.DB) func(w http.ResponseWriter, r *http.R albumIdStr := r.URL.Query().Get("album_id") albumId, err := strconv.Atoi(albumIdStr) - if err != nil { + if err != nil && albumIdStr != "" { l.Debug().AnErr("error", err).Msg("GetListenActivityHandler: Invalid album ID parameter") utils.WriteError(w, "invalid album ID parameter", http.StatusBadRequest) return @@ -59,7 +59,7 @@ func GetListenActivityHandler(store db.DB) func(w http.ResponseWriter, r *http.R trackIdStr := r.URL.Query().Get("track_id") trackId, err := strconv.Atoi(trackIdStr) - if err != nil { + if err != nil && trackIdStr != "" { l.Debug().AnErr("error", err).Msg("GetListenActivityHandler: Invalid track ID parameter") utils.WriteError(w, "invalid track ID parameter", http.StatusBadRequest) return diff --git a/internal/db/psql/listen_activity.go b/internal/db/psql/listen_activity.go index 47b1a13..1953766 100644 --- a/internal/db/psql/listen_activity.go +++ b/internal/db/psql/listen_activity.go @@ -8,6 +8,7 @@ import ( "github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/logger" "github.com/gabehf/koito/internal/repository" + "github.com/jackc/pgx/v5/pgtype" ) func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts) ([]db.ListenActivityItem, error) { @@ -88,8 +89,8 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts l.Debug().Msgf("Fetching listen activity for %d %s(s) from %v to %v", opts.Range, opts.Step, t1.Format("Jan 02, 2006 15:04:05"), t2.Format("Jan 02, 2006 15:04:05")) rows, err := d.q.ListenActivity(ctx, repository.ListenActivityParams{ - Column1: t1, - Column2: t2, + Column1: pgtype.Date{Time: t1, Valid: true}, + Column2: pgtype.Date{Time: t2, Valid: true}, Column3: stepToInterval(opts.Step), }) if err != nil { @@ -98,7 +99,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts listenActivity = make([]db.ListenActivityItem, len(rows)) for i, row := range rows { t := db.ListenActivityItem{ - Start: row.BucketStart, + Start: row.BucketedListensBucketStartDate, Listens: row.ListenCount, } listenActivity[i] = t diff --git a/internal/repository/listen.sql.go b/internal/repository/listen.sql.go index 027873a..d3ff0c8 100644 --- a/internal/repository/listen.sql.go +++ b/internal/repository/listen.sql.go @@ -190,12 +190,32 @@ func (q *Queries) DeleteListen(ctx context.Context, arg DeleteListenParams) erro return err } +const getFirstListen = `-- name: GetFirstListen :one +SELECT + track_id, listened_at, client, user_id +FROM listens +ORDER BY listened_at ASC +LIMIT 1 +` + +func (q *Queries) GetFirstListen(ctx context.Context) (Listen, error) { + row := q.db.QueryRow(ctx, getFirstListen) + var i Listen + err := row.Scan( + &i.TrackID, + &i.ListenedAt, + &i.Client, + &i.UserID, + ) + return i, err +} + const getFirstListenFromArtist = `-- name: GetFirstListenFromArtist :one -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id FROM listens l JOIN tracks_with_title t ON l.track_id = t.id -JOIN artist_tracks at ON t.id = at.track_id +JOIN artist_tracks at ON t.id = at.track_id WHERE at.artist_id = $1 ORDER BY l.listened_at ASC LIMIT 1 @@ -214,7 +234,7 @@ func (q *Queries) GetFirstListenFromArtist(ctx context.Context, artistID int32) } const getFirstListenFromRelease = `-- name: GetFirstListenFromRelease :one -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id FROM listens l JOIN tracks t ON l.track_id = t.id @@ -236,7 +256,7 @@ func (q *Queries) GetFirstListenFromRelease(ctx context.Context, releaseID int32 } const getFirstListenFromTrack = `-- name: GetFirstListenFromTrack :one -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id FROM listens l JOIN tracks t ON l.track_id = t.id @@ -258,14 +278,14 @@ func (q *Queries) GetFirstListenFromTrack(ctx context.Context, id int32) (Listen } const getLastListensFromArtistPaginated = `-- name: GetLastListensFromArtistPaginated :many -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id, t.title AS track_title, t.release_id AS release_id, get_artists_for_track(t.id) AS artists FROM listens l JOIN tracks_with_title t ON l.track_id = t.id -JOIN artist_tracks at ON t.id = at.track_id +JOIN artist_tracks at ON t.id = at.track_id WHERE at.artist_id = $5 AND l.listened_at BETWEEN $1 AND $2 ORDER BY l.listened_at DESC @@ -325,7 +345,7 @@ func (q *Queries) GetLastListensFromArtistPaginated(ctx context.Context, arg Get } const getLastListensFromReleasePaginated = `-- name: GetLastListensFromReleasePaginated :many -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id, t.title AS track_title, t.release_id AS release_id, @@ -391,7 +411,7 @@ func (q *Queries) GetLastListensFromReleasePaginated(ctx context.Context, arg Ge } const getLastListensFromTrackPaginated = `-- name: GetLastListensFromTrackPaginated :many -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id, t.title AS track_title, t.release_id AS release_id, @@ -457,7 +477,7 @@ func (q *Queries) GetLastListensFromTrackPaginated(ctx context.Context, arg GetL } const getLastListensPaginated = `-- name: GetLastListensPaginated :many -SELECT +SELECT l.track_id, l.listened_at, l.client, l.user_id, t.title AS track_title, t.release_id AS release_id, @@ -676,31 +696,38 @@ func (q *Queries) InsertListen(ctx context.Context, arg InsertListenParams) erro const listenActivity = `-- name: ListenActivity :many WITH buckets AS ( - SELECT generate_series($1::timestamptz, $2::timestamptz, $3::interval) AS bucket_start + SELECT + d::date AS bucket_start_date, + (d + $3::interval)::date AS bucket_end_date + FROM generate_series( + $1::date, + $2::date, + $3::interval + ) AS d ), bucketed_listens AS ( SELECT - b.bucket_start, + b.bucket_start_date::timestamptz, COUNT(l.listened_at) AS listen_count FROM buckets b LEFT JOIN listens l - ON l.listened_at >= b.bucket_start - AND l.listened_at < b.bucket_start + $3::interval - GROUP BY b.bucket_start - ORDER BY b.bucket_start + ON l.listened_at >= b.bucket_start_date::timestamptz + AND l.listened_at < b.bucket_end_date::timestamptz + GROUP BY b.bucket_start_date + ORDER BY b.bucket_start_date ) -SELECT bucket_start, listen_count FROM bucketed_listens +SELECT bucketed_listens.bucket_start_date::timestamptz, listen_count FROM bucketed_listens ` type ListenActivityParams struct { - Column1 time.Time - Column2 time.Time + Column1 pgtype.Date + Column2 pgtype.Date Column3 pgtype.Interval } type ListenActivityRow struct { - BucketStart time.Time - ListenCount int64 + BucketedListensBucketStartDate time.Time + ListenCount int64 } func (q *Queries) ListenActivity(ctx context.Context, arg ListenActivityParams) ([]ListenActivityRow, error) { @@ -712,7 +739,7 @@ func (q *Queries) ListenActivity(ctx context.Context, arg ListenActivityParams) var items []ListenActivityRow for rows.Next() { var i ListenActivityRow - if err := rows.Scan(&i.BucketStart, &i.ListenCount); err != nil { + if err := rows.Scan(&i.BucketedListensBucketStartDate, &i.ListenCount); err != nil { return nil, err } items = append(items, i) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 905ab41..eb56425 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -127,6 +127,12 @@ func DateRange(week, month, year int) (time.Time, time.Time, error) { return start, end, nil } +// Returns a time.Time that represents the first moment of the day of t. +func BeginningOfDay(t time.Time) time.Time { + year, month, day := t.Date() + return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) +} + // CopyFile copies a file from src to dst. If src and dst files exist, and are // the same, then return success. Otherise, attempt to create a hard link // between the two files. If that fail, copy the file contents from src to dst.