diff --git a/db/queries/artist.sql b/db/queries/artist.sql index 0571d71..e20326d 100644 --- a/db/queries/artist.sql +++ b/db/queries/artist.sql @@ -4,7 +4,7 @@ VALUES ($1, $2, $3) RETURNING *; -- name: GetArtist :one -SELECT +SELECT a.*, array_agg(aa.alias)::text[] AS aliases FROM artists_with_name a @@ -13,7 +13,7 @@ WHERE a.id = $1 GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name; -- name: GetTrackArtists :many -SELECT +SELECT a.*, at.is_primary as is_primary FROM artists_with_name a @@ -25,7 +25,7 @@ GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name, at.is_primary; SELECT * FROM artists WHERE image = $1 LIMIT 1; -- name: GetReleaseArtists :many -SELECT +SELECT a.*, ar.is_primary as is_primary FROM artists_with_name a @@ -35,7 +35,7 @@ GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name, ar.is_primary; -- name: GetArtistByName :one WITH artist_with_aliases AS ( - SELECT + SELECT a.*, COALESCE(array_agg(aa.alias), '{}')::text[] AS aliases FROM artists_with_name a @@ -48,7 +48,7 @@ WITH artist_with_aliases AS ( SELECT * FROM artist_with_aliases; -- name: GetArtistByMbzID :one -SELECT +SELECT a.*, array_agg(aa.alias)::text[] AS aliases FROM artists_with_name a @@ -78,6 +78,17 @@ FROM listens l JOIN artist_tracks at ON l.track_id = at.track_id WHERE l.listened_at BETWEEN $1 AND $2; +-- name: CountNewArtists :one +SELECT COUNT(*) AS total_count +FROM ( + SELECT at.artist_id + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN artist_tracks at ON t.id = at.track_id + GROUP BY at.artist_id + HAVING MIN(l.listened_at) BETWEEN $1 AND $2 +) first_appearances; + -- name: UpdateArtistMbzID :exec UPDATE artists SET musicbrainz_id = $2 WHERE id = $1; @@ -111,4 +122,4 @@ SET artist_id = $2 WHERE artist_id = $1; -- name: DeleteArtist :exec -DELETE FROM artists WHERE id = $1; \ No newline at end of file +DELETE FROM artists WHERE id = $1; diff --git a/db/queries/release.sql b/db/queries/release.sql index ebf7ff5..86727f4 100644 --- a/db/queries/release.sql +++ b/db/queries/release.sql @@ -4,7 +4,7 @@ VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetRelease :one -SELECT +SELECT *, get_artists_for_release(id) AS artists FROM releases_with_title @@ -69,10 +69,20 @@ WHERE l.listened_at BETWEEN $1 AND $2; -- name: CountReleasesFromArtist :one SELECT COUNT(*) -FROM releases r +FROM releases r JOIN artist_releases ar ON r.id = ar.release_id WHERE ar.artist_id = $1; +-- name: CountNewReleases :one +SELECT COUNT(*) AS total_count +FROM ( + SELECT t.release_id + FROM listens l + JOIN tracks t ON l.track_id = t.id + GROUP BY t.release_id + HAVING MIN(l.listened_at) BETWEEN $1 AND $2 +) first_appearances; + -- name: AssociateArtistToRelease :exec INSERT INTO artist_releases (artist_id, release_id, is_primary) VALUES ($1, $2, $3) @@ -82,8 +92,8 @@ ON CONFLICT DO NOTHING; SELECT r.*, get_artists_for_release(r.id) AS artists -FROM releases_with_title r -WHERE r.image IS NULL +FROM releases_with_title r +WHERE r.image IS NULL AND r.id > $2 ORDER BY r.id ASC LIMIT $1; @@ -107,8 +117,8 @@ WHERE id = $1; -- name: DeleteRelease :exec DELETE FROM releases WHERE id = $1; --- name: DeleteReleasesFromArtist :exec +-- name: DeleteReleasesFromArtist :exec DELETE FROM releases r USING artist_releases ar WHERE ar.release_id = r.id - AND ar.artist_id = $1; \ No newline at end of file + AND ar.artist_id = $1; diff --git a/db/queries/track.sql b/db/queries/track.sql index 2aafd55..a9fc425 100644 --- a/db/queries/track.sql +++ b/db/queries/track.sql @@ -9,7 +9,7 @@ VALUES ($1, $2, $3) ON CONFLICT DO NOTHING; -- name: GetTrack :one -SELECT +SELECT t.*, get_artists_for_track(t.id) AS artists, r.image @@ -109,6 +109,15 @@ JOIN tracks t ON l.track_id = t.id WHERE l.listened_at BETWEEN $1 AND $2 AND t.release_id = $3; +-- name: CountNewTracks :one +SELECT COUNT(*) AS total_count +FROM ( + SELECT track_id + FROM listens + GROUP BY track_id + HAVING MIN(listened_at) BETWEEN $1 AND $2 +) first_appearances; + -- name: UpdateTrackMbzID :exec UPDATE tracks SET musicbrainz_id = $2 WHERE id = $1; @@ -126,4 +135,4 @@ UPDATE artist_tracks SET is_primary = $3 WHERE artist_id = $1 AND track_id = $2; -- name: DeleteTrack :exec -DELETE FROM tracks WHERE id = $1; \ No newline at end of file +DELETE FROM tracks WHERE id = $1; diff --git a/internal/db/db.go b/internal/db/db.go index 71e64f9..fed2d23 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -67,6 +67,9 @@ type DB interface { CountTracks(ctx context.Context, timeframe Timeframe) (int64, error) CountAlbums(ctx context.Context, timeframe Timeframe) (int64, error) CountArtists(ctx context.Context, timeframe Timeframe) (int64, error) + CountNewTracks(ctx context.Context, timeframe Timeframe) (int64, error) + CountNewAlbums(ctx context.Context, timeframe Timeframe) (int64, error) + CountNewArtists(ctx context.Context, timeframe Timeframe) (int64, error) CountTimeListened(ctx context.Context, timeframe Timeframe) (int64, error) CountTimeListenedToItem(ctx context.Context, opts TimeListenedOpts) (int64, error) CountUsers(ctx context.Context) (int64, error) diff --git a/internal/db/psql/counts.go b/internal/db/psql/counts.go index 821e05b..86b41c4 100644 --- a/internal/db/psql/counts.go +++ b/internal/db/psql/counts.go @@ -142,3 +142,60 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened } return 0, errors.New("CountTimeListenedToItem: an id must be provided") } + +func (p *Psql) CountNewTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) { + var t1, t2 time.Time + 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.CountNewTracks(ctx, repository.CountNewTracksParams{ + ListenedAt: t1, + ListenedAt_2: t2, + }) + if err != nil { + return 0, fmt.Errorf("CountNewTracks: %w", err) + } + return count, nil +} + +func (p *Psql) CountNewAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) { + var t1, t2 time.Time + 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{ + ListenedAt: t1, + ListenedAt_2: t2, + }) + if err != nil { + return 0, fmt.Errorf("CountNewAlbums: %w", err) + } + return count, nil +} + +func (p *Psql) CountNewArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) { + var t1, t2 time.Time + 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{ + ListenedAt: t1, + ListenedAt_2: t2, + }) + if err != nil { + return 0, fmt.Errorf("CountNewArtists: %w", err) + } + return count, nil +} diff --git a/internal/db/psql/counts_test.go b/internal/db/psql/counts_test.go index 8686571..0273967 100644 --- a/internal/db/psql/counts_test.go +++ b/internal/db/psql/counts_test.go @@ -3,6 +3,7 @@ package psql_test import ( "context" "testing" + "time" "github.com/gabehf/koito/internal/db" "github.com/stretchr/testify/assert" @@ -35,6 +36,23 @@ func TestCountTracks(t *testing.T) { truncateTestData(t) } +func TestCountNewTracks(t *testing.T) { + ctx := context.Background() + testDataForTopItems(t) + testDataAbsoluteListenTimes(t) + + // Test CountTracks + t1, _ := time.Parse(time.DateOnly, "2025-01-01") + t1u := t1.Unix() + t2, _ := time.Parse(time.DateOnly, "2025-12-31") + t2u := t2.Unix() + count, err := store.CountNewTracks(ctx, db.Timeframe{T1u: t1u, T2u: t2u}) + require.NoError(t, err) + assert.Equal(t, int64(1), count, "expected tracks count to match inserted data") + + truncateTestData(t) +} + func TestCountAlbums(t *testing.T) { ctx := context.Background() testDataForTopItems(t) @@ -48,6 +66,23 @@ func TestCountAlbums(t *testing.T) { truncateTestData(t) } +func TestCountNewAlbums(t *testing.T) { + ctx := context.Background() + testDataForTopItems(t) + testDataAbsoluteListenTimes(t) + + // Test CountTracks + t1, _ := time.Parse(time.DateOnly, "2025-01-01") + t1u := t1.Unix() + t2, _ := time.Parse(time.DateOnly, "2025-12-31") + t2u := t2.Unix() + count, err := store.CountNewAlbums(ctx, db.Timeframe{T1u: t1u, T2u: t2u}) + require.NoError(t, err) + assert.Equal(t, int64(1), count, "expected albums count to match inserted data") + + truncateTestData(t) +} + func TestCountArtists(t *testing.T) { ctx := context.Background() testDataForTopItems(t) @@ -61,6 +96,23 @@ func TestCountArtists(t *testing.T) { truncateTestData(t) } +func TestCountNewArtists(t *testing.T) { + ctx := context.Background() + testDataForTopItems(t) + testDataAbsoluteListenTimes(t) + + // Test CountTracks + t1, _ := time.Parse(time.DateOnly, "2025-01-01") + t1u := t1.Unix() + t2, _ := time.Parse(time.DateOnly, "2025-12-31") + t2u := t2.Unix() + count, err := store.CountNewArtists(ctx, db.Timeframe{T1u: t1u, T2u: t2u}) + require.NoError(t, err) + assert.Equal(t, int64(1), count, "expected artists count to match inserted data") + + truncateTestData(t) +} + func TestCountTimeListened(t *testing.T) { ctx := context.Background() testDataForTopItems(t) diff --git a/internal/repository/artist.sql.go b/internal/repository/artist.sql.go index 4926a5f..3d33446 100644 --- a/internal/repository/artist.sql.go +++ b/internal/repository/artist.sql.go @@ -13,6 +13,30 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const countNewArtists = `-- name: CountNewArtists :one +SELECT COUNT(*) AS total_count +FROM ( + SELECT at.artist_id + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN artist_tracks at ON t.id = at.track_id + GROUP BY at.artist_id + HAVING MIN(l.listened_at) BETWEEN $1 AND $2 +) first_appearances +` + +type CountNewArtistsParams struct { + ListenedAt time.Time + ListenedAt_2 time.Time +} + +func (q *Queries) CountNewArtists(ctx context.Context, arg CountNewArtistsParams) (int64, error) { + row := q.db.QueryRow(ctx, countNewArtists, arg.ListenedAt, arg.ListenedAt_2) + var total_count int64 + err := row.Scan(&total_count) + return total_count, err +} + const countTopArtists = `-- name: CountTopArtists :one SELECT COUNT(DISTINCT at.artist_id) AS total_count FROM listens l @@ -78,7 +102,7 @@ func (q *Queries) DeleteConflictingArtistTracks(ctx context.Context, arg DeleteC } const getArtist = `-- name: GetArtist :one -SELECT +SELECT a.id, a.musicbrainz_id, a.image, a.image_source, a.name, array_agg(aa.alias)::text[] AS aliases FROM artists_with_name a @@ -127,7 +151,7 @@ func (q *Queries) GetArtistByImage(ctx context.Context, image *uuid.UUID) (Artis } const getArtistByMbzID = `-- name: GetArtistByMbzID :one -SELECT +SELECT a.id, a.musicbrainz_id, a.image, a.image_source, a.name, array_agg(aa.alias)::text[] AS aliases FROM artists_with_name a @@ -161,7 +185,7 @@ func (q *Queries) GetArtistByMbzID(ctx context.Context, musicbrainzID *uuid.UUID const getArtistByName = `-- name: GetArtistByName :one WITH artist_with_aliases AS ( - SELECT + SELECT a.id, a.musicbrainz_id, a.image, a.image_source, a.name, COALESCE(array_agg(aa.alias), '{}')::text[] AS aliases FROM artists_with_name a @@ -198,7 +222,7 @@ func (q *Queries) GetArtistByName(ctx context.Context, alias string) (GetArtistB } const getReleaseArtists = `-- name: GetReleaseArtists :many -SELECT +SELECT a.id, a.musicbrainz_id, a.image, a.image_source, a.name, ar.is_primary as is_primary FROM artists_with_name a @@ -307,7 +331,7 @@ func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsP } const getTrackArtists = `-- name: GetTrackArtists :many -SELECT +SELECT a.id, a.musicbrainz_id, a.image, a.image_source, a.name, at.is_primary as is_primary FROM artists_with_name a diff --git a/internal/repository/release.sql.go b/internal/repository/release.sql.go index 6404231..aa791e6 100644 --- a/internal/repository/release.sql.go +++ b/internal/repository/release.sql.go @@ -30,9 +30,32 @@ func (q *Queries) AssociateArtistToRelease(ctx context.Context, arg AssociateArt return err } +const countNewReleases = `-- name: CountNewReleases :one +SELECT COUNT(*) AS total_count +FROM ( + SELECT t.release_id + FROM listens l + JOIN tracks t ON l.track_id = t.id + GROUP BY t.release_id + HAVING MIN(l.listened_at) BETWEEN $1 AND $2 +) first_appearances +` + +type CountNewReleasesParams struct { + ListenedAt time.Time + ListenedAt_2 time.Time +} + +func (q *Queries) CountNewReleases(ctx context.Context, arg CountNewReleasesParams) (int64, error) { + row := q.db.QueryRow(ctx, countNewReleases, arg.ListenedAt, arg.ListenedAt_2) + var total_count int64 + err := row.Scan(&total_count) + return total_count, err +} + const countReleasesFromArtist = `-- name: CountReleasesFromArtist :one SELECT COUNT(*) -FROM releases r +FROM releases r JOIN artist_releases ar ON r.id = ar.release_id WHERE ar.artist_id = $1 ` @@ -86,7 +109,7 @@ func (q *Queries) DeleteReleasesFromArtist(ctx context.Context, artistID int32) } const getRelease = `-- name: GetRelease :one -SELECT +SELECT id, musicbrainz_id, image, various_artists, image_source, title, get_artists_for_release(id) AS artists FROM releases_with_title @@ -213,8 +236,8 @@ const getReleasesWithoutImages = `-- name: GetReleasesWithoutImages :many SELECT r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title, get_artists_for_release(r.id) AS artists -FROM releases_with_title r -WHERE r.image IS NULL +FROM releases_with_title r +WHERE r.image IS NULL AND r.id > $2 ORDER BY r.id ASC LIMIT $1 diff --git a/internal/repository/track.sql.go b/internal/repository/track.sql.go index c7166c7..c531210 100644 --- a/internal/repository/track.sql.go +++ b/internal/repository/track.sql.go @@ -29,6 +29,28 @@ func (q *Queries) AssociateArtistToTrack(ctx context.Context, arg AssociateArtis return err } +const countNewTracks = `-- name: CountNewTracks :one +SELECT COUNT(*) AS total_count +FROM ( + SELECT track_id + FROM listens + GROUP BY track_id + HAVING MIN(listened_at) BETWEEN $1 AND $2 +) first_appearances +` + +type CountNewTracksParams struct { + ListenedAt time.Time + ListenedAt_2 time.Time +} + +func (q *Queries) CountNewTracks(ctx context.Context, arg CountNewTracksParams) (int64, error) { + row := q.db.QueryRow(ctx, countNewTracks, arg.ListenedAt, arg.ListenedAt_2) + var total_count int64 + err := row.Scan(&total_count) + return total_count, err +} + const countTopTracks = `-- name: CountTopTracks :one SELECT COUNT(DISTINCT l.track_id) AS total_count FROM listens l @@ -343,7 +365,7 @@ func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPag } const getTrack = `-- name: GetTrack :one -SELECT +SELECT t.id, t.musicbrainz_id, t.duration, t.release_id, t.title, get_artists_for_track(t.id) AS artists, r.image