Koito/internal/repository/year.sql.go
Gabe Farrell 36f984a1a2
Pre-release version v0.0.14 (#96)
* add dev branch container to workflow

* correctly set the default range of ActivityGrid

* fix: set name/short_name to koito (#61)

* fix dev container push workflow

* fix: race condition with using getComputedStyle primary color for dynamic activity grid darkening (#76)

* Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening

Instead just use the color from the current theme directly. Tested works on initial load and theme changes.
Fixes https://github.com/gabehf/Koito/issues/75

* Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name
Split name out of the Theme struct to simplify custom theme saving/reading

* fix: set first artist listed as primary by default (#81)

* feat: add server-side configuration with default theme (#90)

* docs: add example for usage of the main listenbrainz instance (#71)

* docs: add example for usage of the main listenbrainz instance

* Update scrobbler.md

---------

Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com>

* feat: add server-side cfg and default theme

* fix: repair custom theme

---------

Co-authored-by: m0d3rnX <jesper@posteo.de>

* docs: add default theme cfg option to docs

* feat: add ability to manually scrobble track (#91)

* feat: add button to manually scrobble from ui

* fix: ensure timestamp is in the past, log fix

* test: add integration test

* feat: add first listened to dates for media items (#92)

* fix: ensure error checks for ErrNoRows

* feat: add now playing endpoint and ui (#93)

* wip

* feat: add now playing

* fix: set default theme when config is not set

* feat: fetch images from subsonic server (#94)

* fix: useQuery instead of useEffect for now playing

* feat: custom artist separator regex (#95)

* Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening

Instead just use the color from the current theme directly. Tested works on initial load and theme changes.
Fixes https://github.com/gabehf/Koito/issues/75

* Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name
Split name out of the Theme struct to simplify custom theme saving/reading

* feat: add server-side configuration with default theme (#90)

* docs: add example for usage of the main listenbrainz instance (#71)

* docs: add example for usage of the main listenbrainz instance

* Update scrobbler.md

---------

Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com>

* feat: add server-side cfg and default theme

* fix: repair custom theme

---------

Co-authored-by: m0d3rnX <jesper@posteo.de>

* fix: rebase errors

---------

Co-authored-by: pet <128837728+againstpetra@users.noreply.github.com>
Co-authored-by: mlandry <mike.landry@gmail.com>
Co-authored-by: m0d3rnX <jesper@posteo.de>
2025-11-19 20:26:56 -05:00

616 lines
16 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: year.sql
package repository
import (
"context"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const artistsOnlyPlayedOnceInYear = `-- name: ArtistsOnlyPlayedOnceInYear :many
SELECT
a.id AS artist_id,
a.name,
COUNT(l.*) AS listen_count
FROM listens l
JOIN artist_tracks at ON at.track_id = l.track_id
JOIN artists_with_name a ON a.id = at.artist_id
WHERE EXTRACT(YEAR FROM l.listened_at) = $1::int AND l.user_id = $2::int
GROUP BY a.id, a.name
HAVING COUNT(*) = 1
`
type ArtistsOnlyPlayedOnceInYearParams struct {
Year int32
UserID int32
}
type ArtistsOnlyPlayedOnceInYearRow struct {
ArtistID int32
Name string
ListenCount int64
}
func (q *Queries) ArtistsOnlyPlayedOnceInYear(ctx context.Context, arg ArtistsOnlyPlayedOnceInYearParams) ([]ArtistsOnlyPlayedOnceInYearRow, error) {
rows, err := q.db.Query(ctx, artistsOnlyPlayedOnceInYear, arg.Year, arg.UserID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ArtistsOnlyPlayedOnceInYearRow
for rows.Next() {
var i ArtistsOnlyPlayedOnceInYearRow
if err := rows.Scan(&i.ArtistID, &i.Name, &i.ListenCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getArtistCountInYear = `-- name: GetArtistCountInYear :one
SELECT
COUNT(DISTINCT at.artist_id) AS artist_count
FROM listens l
JOIN artist_tracks at ON at.track_id = l.track_id
WHERE l.user_id = $1::int
AND EXTRACT(YEAR FROM l.listened_at) = $2::int
`
type GetArtistCountInYearParams struct {
UserID int32
Year int32
}
func (q *Queries) GetArtistCountInYear(ctx context.Context, arg GetArtistCountInYearParams) (int64, error) {
row := q.db.QueryRow(ctx, getArtistCountInYear, arg.UserID, arg.Year)
var artist_count int64
err := row.Scan(&artist_count)
return artist_count, err
}
const getArtistWithLongestGapInYear = `-- name: GetArtistWithLongestGapInYear :one
WITH first_listens AS (
SELECT
l.user_id,
at.artist_id,
MIN(l.listened_at::date) AS first_listen_of_year
FROM listens l
JOIN artist_tracks at ON at.track_id = l.track_id
WHERE EXTRACT(YEAR FROM l.listened_at) = $1::int
GROUP BY l.user_id, at.artist_id
),
last_listens AS (
SELECT
l.user_id,
at.artist_id,
MAX(l.listened_at::date) AS last_listen
FROM listens l
JOIN artist_tracks at ON at.track_id = l.track_id
WHERE l.listened_at < $2::date
GROUP BY l.user_id, at.artist_id
),
comebacks AS (
SELECT
f.user_id,
f.artist_id,
f.first_listen_of_year,
p.last_listen,
(f.first_listen_of_year - p.last_listen) AS gap_days
FROM first_listens f
JOIN last_listens p
ON f.user_id = p.user_id AND f.artist_id = p.artist_id
),
ranked AS (
SELECT user_id, artist_id, first_listen_of_year, last_listen, gap_days,
RANK() OVER (PARTITION BY user_id ORDER BY gap_days DESC) AS r
FROM comebacks
)
SELECT
c.user_id,
c.artist_id,
awn.name AS artist_name,
c.last_listen,
c.first_listen_of_year,
c.gap_days
FROM ranked c
JOIN artists_with_name awn ON awn.id = c.artist_id
WHERE r = 1
`
type GetArtistWithLongestGapInYearParams struct {
Year int32
FirstDayOfYear pgtype.Date
}
type GetArtistWithLongestGapInYearRow struct {
UserID int32
ArtistID int32
ArtistName string
LastListen interface{}
FirstListenOfYear interface{}
GapDays int32
}
func (q *Queries) GetArtistWithLongestGapInYear(ctx context.Context, arg GetArtistWithLongestGapInYearParams) (GetArtistWithLongestGapInYearRow, error) {
row := q.db.QueryRow(ctx, getArtistWithLongestGapInYear, arg.Year, arg.FirstDayOfYear)
var i GetArtistWithLongestGapInYearRow
err := row.Scan(
&i.UserID,
&i.ArtistID,
&i.ArtistName,
&i.LastListen,
&i.FirstListenOfYear,
&i.GapDays,
)
return i, err
}
const getArtistsWithOnlyOnePlayInYear = `-- name: GetArtistsWithOnlyOnePlayInYear :many
WITH first_artist_plays_in_year AS (
SELECT
l.user_id,
at.artist_id,
MIN(l.listened_at) AS first_listen
FROM listens l
JOIN artist_tracks at ON at.track_id = l.track_id
WHERE EXTRACT(YEAR FROM l.listened_at) = 2024
AND NOT EXISTS (
SELECT 1
FROM listens l2
JOIN artist_tracks at2 ON at2.track_id = l2.track_id
WHERE l2.user_id = l.user_id
AND at2.artist_id = at.artist_id
AND l2.listened_at < DATE '2024-01-01'
)
GROUP BY l.user_id, at.artist_id
)
SELECT
f.user_id,
f.artist_id,
f.first_listen, a.name,
COUNT(l.*) AS total_plays_in_year
FROM first_artist_plays_in_year f
JOIN listens l ON l.user_id = f.user_id
JOIN artist_tracks at ON at.track_id = l.track_id JOIN artists_with_name a ON at.artist_id = a.id
WHERE at.artist_id = f.artist_id
AND EXTRACT(YEAR FROM l.listened_at) = 2024
GROUP BY f.user_id, f.artist_id, f.first_listen, a.name HAVING COUNT(*) = 1
`
type GetArtistsWithOnlyOnePlayInYearRow struct {
UserID int32
ArtistID int32
FirstListen interface{}
Name string
TotalPlaysInYear int64
}
func (q *Queries) GetArtistsWithOnlyOnePlayInYear(ctx context.Context) ([]GetArtistsWithOnlyOnePlayInYearRow, error) {
rows, err := q.db.Query(ctx, getArtistsWithOnlyOnePlayInYear)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetArtistsWithOnlyOnePlayInYearRow
for rows.Next() {
var i GetArtistsWithOnlyOnePlayInYearRow
if err := rows.Scan(
&i.UserID,
&i.ArtistID,
&i.FirstListen,
&i.Name,
&i.TotalPlaysInYear,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getFirstListenInYear = `-- name: GetFirstListenInYear :one
SELECT
l.track_id, l.listened_at, l.client, l.user_id,
t.id, t.musicbrainz_id, t.duration, t.release_id, t.title,
get_artists_for_track(t.id) as artists
FROM listens l
LEFT JOIN tracks_with_title t ON l.track_id = t.id
WHERE EXTRACT(YEAR FROM l.listened_at) = 2025
ORDER BY l.listened_at ASC
LIMIT 1
`
type GetFirstListenInYearRow struct {
TrackID int32
ListenedAt time.Time
Client *string
UserID int32
ID pgtype.Int4
MusicBrainzID *uuid.UUID
Duration pgtype.Int4
ReleaseID pgtype.Int4
Title pgtype.Text
Artists []byte
}
func (q *Queries) GetFirstListenInYear(ctx context.Context) (GetFirstListenInYearRow, error) {
row := q.db.QueryRow(ctx, getFirstListenInYear)
var i GetFirstListenInYearRow
err := row.Scan(
&i.TrackID,
&i.ListenedAt,
&i.Client,
&i.UserID,
&i.ID,
&i.MusicBrainzID,
&i.Duration,
&i.ReleaseID,
&i.Title,
&i.Artists,
)
return i, err
}
const getListenPercentageInTimeWindowInYear = `-- name: GetListenPercentageInTimeWindowInYear :one
WITH user_listens_in_year AS (
SELECT
listened_at
FROM listens
WHERE user_id = $1::int
AND EXTRACT(YEAR FROM listened_at) = $2::int
),
windowed AS (
SELECT
COUNT(*) AS in_window
FROM user_listens_in_year
WHERE EXTRACT(HOUR FROM listened_at) >= $3::int
AND EXTRACT(HOUR FROM listened_at) < $4::int
),
total AS (
SELECT COUNT(*) AS total_listens
FROM user_listens_in_year
)
SELECT
w.in_window,
t.total_listens,
ROUND((w.in_window::decimal / t.total_listens) * 100, 2) AS percent_of_total
FROM windowed w, total t
`
type GetListenPercentageInTimeWindowInYearParams struct {
UserID int32
Year int32
HourWindowStart int32
HourWindowEnd int32
}
type GetListenPercentageInTimeWindowInYearRow struct {
InWindow int64
TotalListens int64
PercentOfTotal pgtype.Numeric
}
func (q *Queries) GetListenPercentageInTimeWindowInYear(ctx context.Context, arg GetListenPercentageInTimeWindowInYearParams) (GetListenPercentageInTimeWindowInYearRow, error) {
row := q.db.QueryRow(ctx, getListenPercentageInTimeWindowInYear,
arg.UserID,
arg.Year,
arg.HourWindowStart,
arg.HourWindowEnd,
)
var i GetListenPercentageInTimeWindowInYearRow
err := row.Scan(&i.InWindow, &i.TotalListens, &i.PercentOfTotal)
return i, err
}
const getMostReplayedTrackInYear = `-- name: GetMostReplayedTrackInYear :one
WITH ordered_listens AS (
SELECT
user_id,
track_id,
listened_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY listened_at) AS rn
FROM listens
WHERE EXTRACT(YEAR FROM listened_at) = $2::int
),
streaks AS (
SELECT
user_id,
track_id,
listened_at,
rn,
ROW_NUMBER() OVER (PARTITION BY user_id, track_id ORDER BY listened_at) AS track_rn
FROM ordered_listens
),
grouped_streaks AS (
SELECT
user_id,
track_id,
rn - track_rn AS group_id,
COUNT(*) AS streak_length
FROM streaks
GROUP BY user_id, track_id, rn - track_rn
),
ranked_streaks AS (
SELECT user_id, track_id, group_id, streak_length,
RANK() OVER (PARTITION BY user_id ORDER BY streak_length DESC) AS r
FROM grouped_streaks
)
SELECT
t.id, t.musicbrainz_id, t.duration, t.release_id, t.title,
get_artists_for_track(t.id) as artists,
streak_length
FROM ranked_streaks rs JOIN tracks_with_title t ON rs.track_id = t.id
WHERE user_id = $1::int AND r = 1
`
type GetMostReplayedTrackInYearParams struct {
UserID int32
Year int32
}
type GetMostReplayedTrackInYearRow struct {
ID int32
MusicBrainzID *uuid.UUID
Duration int32
ReleaseID int32
Title string
Artists []byte
StreakLength int64
}
func (q *Queries) GetMostReplayedTrackInYear(ctx context.Context, arg GetMostReplayedTrackInYearParams) (GetMostReplayedTrackInYearRow, error) {
row := q.db.QueryRow(ctx, getMostReplayedTrackInYear, arg.UserID, arg.Year)
var i GetMostReplayedTrackInYearRow
err := row.Scan(
&i.ID,
&i.MusicBrainzID,
&i.Duration,
&i.ReleaseID,
&i.Title,
&i.Artists,
&i.StreakLength,
)
return i, err
}
const getPercentageOfTotalListensFromTopArtistsInYear = `-- name: GetPercentageOfTotalListensFromTopArtistsInYear :one
WITH user_artist_listens AS (
SELECT
at.artist_id,
COUNT(*) AS listen_count
FROM listens l
JOIN artist_tracks at ON at.track_id = l.track_id
WHERE l.user_id = $2::int
AND EXTRACT(YEAR FROM l.listened_at) = $3::int
GROUP BY at.artist_id
),
top_artists AS (
SELECT
artist_id,
listen_count
FROM user_artist_listens
ORDER BY listen_count DESC
LIMIT $1
),
totals AS (
SELECT
(SELECT SUM(listen_count) FROM top_artists) AS top_artist_total,
(SELECT SUM(listen_count) FROM user_artist_listens) AS overall_total
)
SELECT
top_artist_total,
overall_total,
ROUND((top_artist_total::decimal / overall_total) * 100, 2) AS percent_of_total
FROM totals
`
type GetPercentageOfTotalListensFromTopArtistsInYearParams struct {
Limit int32
UserID int32
Year int32
}
type GetPercentageOfTotalListensFromTopArtistsInYearRow struct {
TopArtistTotal int64
OverallTotal int64
PercentOfTotal pgtype.Numeric
}
func (q *Queries) GetPercentageOfTotalListensFromTopArtistsInYear(ctx context.Context, arg GetPercentageOfTotalListensFromTopArtistsInYearParams) (GetPercentageOfTotalListensFromTopArtistsInYearRow, error) {
row := q.db.QueryRow(ctx, getPercentageOfTotalListensFromTopArtistsInYear, arg.Limit, arg.UserID, arg.Year)
var i GetPercentageOfTotalListensFromTopArtistsInYearRow
err := row.Scan(&i.TopArtistTotal, &i.OverallTotal, &i.PercentOfTotal)
return i, err
}
const getPercentageOfTotalListensFromTopTracksInYear = `-- name: GetPercentageOfTotalListensFromTopTracksInYear :one
WITH user_listens AS (
SELECT
l.track_id,
COUNT(*) AS listen_count
FROM listens l
WHERE l.user_id = $2::int
AND EXTRACT(YEAR FROM l.listened_at) = $3::int
GROUP BY l.track_id
),
top_tracks AS (
SELECT
track_id,
listen_count
FROM user_listens
ORDER BY listen_count DESC
LIMIT $1
),
totals AS (
SELECT
(SELECT SUM(listen_count) FROM top_tracks) AS top_tracks_total,
(SELECT SUM(listen_count) FROM user_listens) AS overall_total
)
SELECT
top_tracks_total,
overall_total,
ROUND((top_tracks_total::decimal / overall_total) * 100, 2) AS percent_of_total
FROM totals
`
type GetPercentageOfTotalListensFromTopTracksInYearParams struct {
Limit int32
UserID int32
Year int32
}
type GetPercentageOfTotalListensFromTopTracksInYearRow struct {
TopTracksTotal int64
OverallTotal int64
PercentOfTotal pgtype.Numeric
}
func (q *Queries) GetPercentageOfTotalListensFromTopTracksInYear(ctx context.Context, arg GetPercentageOfTotalListensFromTopTracksInYearParams) (GetPercentageOfTotalListensFromTopTracksInYearRow, error) {
row := q.db.QueryRow(ctx, getPercentageOfTotalListensFromTopTracksInYear, arg.Limit, arg.UserID, arg.Year)
var i GetPercentageOfTotalListensFromTopTracksInYearRow
err := row.Scan(&i.TopTracksTotal, &i.OverallTotal, &i.PercentOfTotal)
return i, err
}
const getTracksPlayedAtLeastOncePerMonthInYear = `-- name: GetTracksPlayedAtLeastOncePerMonthInYear :many
WITH monthly_plays AS (
SELECT
l.track_id,
EXTRACT(MONTH FROM l.listened_at) AS month
FROM listens l
WHERE EXTRACT(YEAR FROM l.listened_at) = $1::int
GROUP BY l.track_id, EXTRACT(MONTH FROM l.listened_at)
),
monthly_counts AS (
SELECT
track_id,
COUNT(DISTINCT month) AS months_played
FROM monthly_plays
GROUP BY track_id
)
SELECT
t.id AS track_id,
t.title
FROM monthly_counts mc
JOIN tracks_with_title t ON t.id = mc.track_id
WHERE mc.months_played = 12
`
type GetTracksPlayedAtLeastOncePerMonthInYearRow struct {
TrackID int32
Title string
}
func (q *Queries) GetTracksPlayedAtLeastOncePerMonthInYear(ctx context.Context, userID int32) ([]GetTracksPlayedAtLeastOncePerMonthInYearRow, error) {
rows, err := q.db.Query(ctx, getTracksPlayedAtLeastOncePerMonthInYear, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTracksPlayedAtLeastOncePerMonthInYearRow
for rows.Next() {
var i GetTracksPlayedAtLeastOncePerMonthInYearRow
if err := rows.Scan(&i.TrackID, &i.Title); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWeekWithMostListensInYear = `-- name: GetWeekWithMostListensInYear :one
SELECT
DATE_TRUNC('week', listened_at + INTERVAL '1 day') - INTERVAL '1 day' AS week_start,
COUNT(*) AS listen_count
FROM listens
WHERE EXTRACT(YEAR FROM listened_at) = $1::int
AND user_id = $2::int
GROUP BY week_start
ORDER BY listen_count DESC
LIMIT 1
`
type GetWeekWithMostListensInYearParams struct {
Year int32
UserID int32
}
type GetWeekWithMostListensInYearRow struct {
WeekStart int32
ListenCount int64
}
func (q *Queries) GetWeekWithMostListensInYear(ctx context.Context, arg GetWeekWithMostListensInYearParams) (GetWeekWithMostListensInYearRow, error) {
row := q.db.QueryRow(ctx, getWeekWithMostListensInYear, arg.Year, arg.UserID)
var i GetWeekWithMostListensInYearRow
err := row.Scan(&i.WeekStart, &i.ListenCount)
return i, err
}
const tracksOnlyPlayedOnceInYear = `-- name: TracksOnlyPlayedOnceInYear :many
SELECT
t.id AS track_id,
t.title,
get_artists_for_track(t.id) as artists,
COUNT(l.*) AS listen_count
FROM listens l
JOIN tracks_with_title t ON t.id = l.track_id
WHERE EXTRACT(YEAR FROM l.listened_at) = $2::int AND l.user_id = $3::int
GROUP BY t.id, t.title
HAVING COUNT(*) = 1
LIMIT $1
`
type TracksOnlyPlayedOnceInYearParams struct {
Limit int32
Year int32
UserID int32
}
type TracksOnlyPlayedOnceInYearRow struct {
TrackID int32
Title string
Artists []byte
ListenCount int64
}
func (q *Queries) TracksOnlyPlayedOnceInYear(ctx context.Context, arg TracksOnlyPlayedOnceInYearParams) ([]TracksOnlyPlayedOnceInYearRow, error) {
rows, err := q.db.Query(ctx, tracksOnlyPlayedOnceInYear, arg.Limit, arg.Year, arg.UserID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TracksOnlyPlayedOnceInYearRow
for rows.Next() {
var i TracksOnlyPlayedOnceInYearRow
if err := rows.Scan(
&i.TrackID,
&i.Title,
&i.Artists,
&i.ListenCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}