feat: search/merge items by id

pull/52/head
Gabe Farrell 6 months ago
parent 5537b6fb89
commit 1425f7f416

@ -23,12 +23,12 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
} }
} }
if (data === undefined) { if (!data) {
return <></> return <></>
} }
return ( return (
<div className="w-full"> <div className="w-full">
{ data.artists.length > 0 && { data.artists && data.artists.length > 0 &&
<> <>
<h3 className={hClasses}>Artists</h3> <h3 className={hClasses}>Artists</h3>
<div className={classes}> <div className={classes}>
@ -52,7 +52,7 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
</div> </div>
</> </>
} }
{ data.albums.length > 0 && { data.albums && data.albums.length > 0 &&
<> <>
<h3 className={hClasses}>Albums</h3> <h3 className={hClasses}>Albums</h3>
<div className={classes}> <div className={classes}>
@ -77,7 +77,7 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
</div> </div>
</> </>
} }
{ data.tracks.length > 0 && { data.tracks && data.tracks.length > 0 &&
<> <>
<h3 className={hClasses}>Tracks</h3> <h3 className={hClasses}>Tracks</h3>
<div className={classes}> <div className={classes}>

@ -4,7 +4,10 @@ VALUES ($1, $2, $3, $4)
RETURNING *; RETURNING *;
-- name: GetRelease :one -- name: GetRelease :one
SELECT * FROM releases_with_title SELECT
*,
get_artists_for_release(id) AS artists
FROM releases_with_title
WHERE id = $1 LIMIT 1; WHERE id = $1 LIMIT 1;
-- name: GetReleaseByMbzID :one -- name: GetReleaseByMbzID :one

@ -11,6 +11,7 @@ ON CONFLICT DO NOTHING;
-- name: GetTrack :one -- name: GetTrack :one
SELECT SELECT
t.*, t.*,
get_artists_for_track(t.id) AS artists,
r.image r.image
FROM tracks_with_title t FROM tracks_with_title t
JOIN releases r ON t.release_id = r.id JOIN releases r ON t.release_id = r.id

@ -2,6 +2,8 @@ package handlers
import ( import (
"net/http" "net/http"
"strconv"
"strings"
"github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger" "github.com/gabehf/koito/internal/logger"
@ -20,27 +22,62 @@ func SearchHandler(store db.DB) http.HandlerFunc {
ctx := r.Context() ctx := r.Context()
l := logger.FromContext(ctx) l := logger.FromContext(ctx)
q := r.URL.Query().Get("q") q := r.URL.Query().Get("q")
artists, err := store.SearchArtists(ctx, q)
l.Debug().Msgf("SearchHandler: Received search with query: %s", r.URL.Query().Encode()) l.Debug().Msgf("SearchHandler: Received search with query: %s", r.URL.Query().Encode())
if err != nil { var artists []*models.Artist
l.Err(err).Msg("Failed to search for artists") var albums []*models.Album
utils.WriteError(w, "failed to search in database", http.StatusInternalServerError) var tracks []*models.Track
return
} if strings.HasPrefix(q, "id:") {
albums, err := store.SearchAlbums(ctx, q) idStr := strings.TrimPrefix(q, "id:")
if err != nil { id, _ := strconv.Atoi(idStr)
l.Err(err).Msg("Failed to search for albums")
utils.WriteError(w, "failed to search in database", http.StatusInternalServerError) artist, err := store.GetArtist(ctx, db.GetArtistOpts{ID: int32(id)})
return if err != nil {
} l.Debug().Msg("No artists found with id")
tracks, err := store.SearchTracks(ctx, q) }
if err != nil { if artist != nil {
l.Err(err).Msg("Failed to search for tracks") artists = append(artists, artist)
utils.WriteError(w, "failed to search in database", http.StatusInternalServerError) }
return
album, err := store.GetAlbum(ctx, db.GetAlbumOpts{ID: int32(id)})
if err != nil {
l.Debug().Msg("No albums found with id")
}
if album != nil {
albums = append(albums, album)
}
track, err := store.GetTrack(ctx, db.GetTrackOpts{ID: int32(id)})
if err != nil {
l.Debug().Msg("No tracks found with id")
}
if track != nil {
tracks = append(tracks, track)
}
} else {
var err error
artists, err = store.SearchArtists(ctx, q)
if err != nil {
l.Err(err).Msg("Failed to search for artists")
utils.WriteError(w, "failed to search in database", http.StatusInternalServerError)
return
}
albums, err = store.SearchAlbums(ctx, q)
if err != nil {
l.Err(err).Msg("Failed to search for albums")
utils.WriteError(w, "failed to search in database", http.StatusInternalServerError)
return
}
tracks, err = store.SearchTracks(ctx, q)
if err != nil {
l.Err(err).Msg("Failed to search for tracks")
utils.WriteError(w, "failed to search in database", http.StatusInternalServerError)
return
}
} }
utils.WriteJSON(w, http.StatusOK, SearchResults{ utils.WriteJSON(w, http.StatusOK, SearchResults{
Artists: artists, Artists: artists,
Albums: albums, Albums: albums,

@ -2,6 +2,7 @@ package psql
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -19,40 +20,71 @@ import (
func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Album, error) { func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Album, error) {
l := logger.FromContext(ctx) l := logger.FromContext(ctx)
var row repository.ReleasesWithTitle
var err error var err error
var ret = new(models.Album)
if opts.ID != 0 { if opts.ID != 0 {
l.Debug().Msgf("Fetching album from DB with id %d", opts.ID) l.Debug().Msgf("Fetching album from DB with id %d", opts.ID)
row, err = d.q.GetRelease(ctx, opts.ID) row, err := d.q.GetRelease(ctx, opts.ID)
if err != nil {
return nil, fmt.Errorf("GetAlbum: %w", err)
}
ret.ID = row.ID
ret.MbzID = row.MusicBrainzID
ret.Title = row.Title
ret.Image = row.Image
ret.VariousArtists = row.VariousArtists
err = json.Unmarshal(row.Artists, &ret.Artists)
if err != nil {
return nil, fmt.Errorf("GetAlbum: json.Unmarshal: %w", err)
}
} else if opts.MusicBrainzID != uuid.Nil { } else if opts.MusicBrainzID != uuid.Nil {
l.Debug().Msgf("Fetching album from DB with MusicBrainz Release ID %s", opts.MusicBrainzID) l.Debug().Msgf("Fetching album from DB with MusicBrainz Release ID %s", opts.MusicBrainzID)
row, err = d.q.GetReleaseByMbzID(ctx, &opts.MusicBrainzID) row, err := d.q.GetReleaseByMbzID(ctx, &opts.MusicBrainzID)
if err != nil {
return nil, fmt.Errorf("GetAlbum: %w", err)
}
ret.ID = row.ID
ret.MbzID = row.MusicBrainzID
ret.Title = row.Title
ret.Image = row.Image
ret.VariousArtists = row.VariousArtists
} else if opts.ArtistID != 0 && opts.Title != "" { } else if opts.ArtistID != 0 && opts.Title != "" {
l.Debug().Msgf("Fetching album from DB with artist_id %d and title %s", opts.ArtistID, opts.Title) l.Debug().Msgf("Fetching album from DB with artist_id %d and title %s", opts.ArtistID, opts.Title)
row, err = d.q.GetReleaseByArtistAndTitle(ctx, repository.GetReleaseByArtistAndTitleParams{ row, err := d.q.GetReleaseByArtistAndTitle(ctx, repository.GetReleaseByArtistAndTitleParams{
ArtistID: opts.ArtistID, ArtistID: opts.ArtistID,
Title: opts.Title, Title: opts.Title,
}) })
if err != nil {
return nil, fmt.Errorf("GetAlbum: %w", err)
}
ret.ID = row.ID
ret.MbzID = row.MusicBrainzID
ret.Title = row.Title
ret.Image = row.Image
ret.VariousArtists = row.VariousArtists
} else if opts.ArtistID != 0 && len(opts.Titles) > 0 { } else if opts.ArtistID != 0 && len(opts.Titles) > 0 {
l.Debug().Msgf("Fetching release group from DB with artist_id %d and titles %v", opts.ArtistID, opts.Titles) l.Debug().Msgf("Fetching release group from DB with artist_id %d and titles %v", opts.ArtistID, opts.Titles)
row, err = d.q.GetReleaseByArtistAndTitles(ctx, repository.GetReleaseByArtistAndTitlesParams{ row, err := d.q.GetReleaseByArtistAndTitles(ctx, repository.GetReleaseByArtistAndTitlesParams{
ArtistID: opts.ArtistID, ArtistID: opts.ArtistID,
Column1: opts.Titles, Column1: opts.Titles,
}) })
if err != nil {
return nil, fmt.Errorf("GetAlbum: %w", err)
}
ret.ID = row.ID
ret.MbzID = row.MusicBrainzID
ret.Title = row.Title
ret.Image = row.Image
ret.VariousArtists = row.VariousArtists
} else { } else {
return nil, errors.New("GetAlbum: insufficient information to get album") return nil, errors.New("GetAlbum: insufficient information to get album")
} }
if err != nil {
return nil, fmt.Errorf("GetAlbum: %w", err)
}
count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{ count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{
ListenedAt: time.Unix(0, 0), ListenedAt: time.Unix(0, 0),
ListenedAt_2: time.Now(), ListenedAt_2: time.Now(),
ReleaseID: row.ID, ReleaseID: ret.ID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("GetAlbum: CountListensFromRelease: %w", err) return nil, fmt.Errorf("GetAlbum: CountListensFromRelease: %w", err)
@ -60,21 +92,16 @@ 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, Period: db.PeriodAllTime,
AlbumID: row.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)
} }
return &models.Album{ ret.ListenCount = count
ID: row.ID, ret.TimeListened = seconds
MbzID: row.MusicBrainzID,
Title: row.Title, return ret, nil
Image: row.Image,
VariousArtists: row.VariousArtists,
ListenCount: count,
TimeListened: seconds,
}, nil
} }
func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Album, error) { func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Album, error) {

@ -2,6 +2,7 @@ package psql
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -34,6 +35,10 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
Image: t.Image, Image: t.Image,
Duration: t.Duration, Duration: t.Duration,
} }
err = json.Unmarshal(t.Artists, &track.Artists)
if err != nil {
return nil, fmt.Errorf("GetTrack: json.Unmarshal: %w", err)
}
} else if opts.MusicBrainzID != uuid.Nil { } else if opts.MusicBrainzID != uuid.Nil {
l.Debug().Msgf("Fetching track from DB with MusicBrainz ID %s", opts.MusicBrainzID) l.Debug().Msgf("Fetching track from DB with MusicBrainz ID %s", opts.MusicBrainzID)
t, err := d.q.GetTrackByMbzID(ctx, &opts.MusicBrainzID) t, err := d.q.GetTrackByMbzID(ctx, &opts.MusicBrainzID)

@ -85,13 +85,26 @@ func (q *Queries) DeleteReleasesFromArtist(ctx context.Context, artistID int32)
} }
const getRelease = `-- name: GetRelease :one const getRelease = `-- name: GetRelease :one
SELECT id, musicbrainz_id, image, various_artists, image_source, title FROM releases_with_title SELECT
id, musicbrainz_id, image, various_artists, image_source, title,
get_artists_for_release(id) AS artists
FROM releases_with_title
WHERE id = $1 LIMIT 1 WHERE id = $1 LIMIT 1
` `
func (q *Queries) GetRelease(ctx context.Context, id int32) (ReleasesWithTitle, error) { type GetReleaseRow struct {
ID int32
MusicBrainzID *uuid.UUID
Image *uuid.UUID
VariousArtists bool
ImageSource pgtype.Text
Title string
Artists []byte
}
func (q *Queries) GetRelease(ctx context.Context, id int32) (GetReleaseRow, error) {
row := q.db.QueryRow(ctx, getRelease, id) row := q.db.QueryRow(ctx, getRelease, id)
var i ReleasesWithTitle var i GetReleaseRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.MusicBrainzID, &i.MusicBrainzID,
@ -99,6 +112,7 @@ func (q *Queries) GetRelease(ctx context.Context, id int32) (ReleasesWithTitle,
&i.VariousArtists, &i.VariousArtists,
&i.ImageSource, &i.ImageSource,
&i.Title, &i.Title,
&i.Artists,
) )
return i, err return i, err
} }

@ -344,6 +344,7 @@ func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPag
const getTrack = `-- name: GetTrack :one const getTrack = `-- name: GetTrack :one
SELECT SELECT
t.id, t.musicbrainz_id, t.duration, t.release_id, t.title, t.id, t.musicbrainz_id, t.duration, t.release_id, t.title,
get_artists_for_track(t.id) AS artists,
r.image r.image
FROM tracks_with_title t FROM tracks_with_title t
JOIN releases r ON t.release_id = r.id JOIN releases r ON t.release_id = r.id
@ -356,6 +357,7 @@ type GetTrackRow struct {
Duration int32 Duration int32
ReleaseID int32 ReleaseID int32
Title string Title string
Artists []byte
Image *uuid.UUID Image *uuid.UUID
} }
@ -368,6 +370,7 @@ func (q *Queries) GetTrack(ctx context.Context, id int32) (GetTrackRow, error) {
&i.Duration, &i.Duration,
&i.ReleaseID, &i.ReleaseID,
&i.Title, &i.Title,
&i.Artists,
&i.Image, &i.Image,
) )
return i, err return i, err

@ -0,0 +1,616 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.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
}
Loading…
Cancel
Save