mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: catalog.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getControls = `-- name: GetControls :one
|
||||
select user_id, allow_explicit, excluded_tracks, excluded_artists, excluded_genres, postponed_tracks
|
||||
from user_controls
|
||||
where user_id = $1
|
||||
`
|
||||
|
||||
type GetControlsRow struct {
|
||||
UserID string `json:"user_id"`
|
||||
AllowExplicit bool `json:"allow_explicit"`
|
||||
ExcludedTracks []byte `json:"excluded_tracks"`
|
||||
ExcludedArtists []byte `json:"excluded_artists"`
|
||||
ExcludedGenres []byte `json:"excluded_genres"`
|
||||
PostponedTracks []byte `json:"postponed_tracks"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetControls(ctx context.Context, userID string) (GetControlsRow, error) {
|
||||
row := q.db.QueryRow(ctx, getControls, userID)
|
||||
var i GetControlsRow
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.AllowExplicit,
|
||||
&i.ExcludedTracks,
|
||||
&i.ExcludedArtists,
|
||||
&i.ExcludedGenres,
|
||||
&i.PostponedTracks,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTracksByIDs = `-- name: GetTracksByIDs :many
|
||||
select id, title, artist, album, genres, release_date, duration_ms, popularity,
|
||||
explicit, features, external, created_at, updated_at, commercial_boost, quality_penalty, discovery_allowed
|
||||
from tracks
|
||||
where id = any($1::text[])
|
||||
order by id
|
||||
`
|
||||
|
||||
type GetTracksByIDsRow struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
Genres []byte `json:"genres"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
DurationMs int32 `json:"duration_ms"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
Explicit bool `json:"explicit"`
|
||||
Features []byte `json:"features"`
|
||||
External []byte `json:"external"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
CommercialBoost float64 `json:"commercial_boost"`
|
||||
QualityPenalty float64 `json:"quality_penalty"`
|
||||
DiscoveryAllowed bool `json:"discovery_allowed"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTracksByIDs(ctx context.Context, dollar_1 []string) ([]GetTracksByIDsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getTracksByIDs, dollar_1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTracksByIDsRow
|
||||
for rows.Next() {
|
||||
var i GetTracksByIDsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Artist,
|
||||
&i.Album,
|
||||
&i.Genres,
|
||||
&i.ReleaseDate,
|
||||
&i.DurationMs,
|
||||
&i.Popularity,
|
||||
&i.Explicit,
|
||||
&i.Features,
|
||||
&i.External,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.CommercialBoost,
|
||||
&i.QualityPenalty,
|
||||
&i.DiscoveryAllowed,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRecentInteractions = `-- name: ListRecentInteractions :many
|
||||
select user_id, track_id, type, weight, occurred_at, context, completed_ms
|
||||
from interactions
|
||||
where occurred_at >= now() - interval '365 days'
|
||||
order by occurred_at desc
|
||||
limit 250000
|
||||
`
|
||||
|
||||
type ListRecentInteractionsRow struct {
|
||||
UserID string `json:"user_id"`
|
||||
TrackID string `json:"track_id"`
|
||||
Type string `json:"type"`
|
||||
Weight float64 `json:"weight"`
|
||||
OccurredAt pgtype.Timestamptz `json:"occurred_at"`
|
||||
Context []byte `json:"context"`
|
||||
CompletedMs int32 `json:"completed_ms"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListRecentInteractions(ctx context.Context) ([]ListRecentInteractionsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listRecentInteractions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListRecentInteractionsRow
|
||||
for rows.Next() {
|
||||
var i ListRecentInteractionsRow
|
||||
if err := rows.Scan(
|
||||
&i.UserID,
|
||||
&i.TrackID,
|
||||
&i.Type,
|
||||
&i.Weight,
|
||||
&i.OccurredAt,
|
||||
&i.Context,
|
||||
&i.CompletedMs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listTracks = `-- name: ListTracks :many
|
||||
select id, title, artist, album, genres, release_date, duration_ms, popularity,
|
||||
explicit, features, external, created_at, updated_at, commercial_boost, quality_penalty, discovery_allowed
|
||||
from tracks
|
||||
order by id
|
||||
`
|
||||
|
||||
type ListTracksRow struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
Genres []byte `json:"genres"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
DurationMs int32 `json:"duration_ms"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
Explicit bool `json:"explicit"`
|
||||
Features []byte `json:"features"`
|
||||
External []byte `json:"external"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
CommercialBoost float64 `json:"commercial_boost"`
|
||||
QualityPenalty float64 `json:"quality_penalty"`
|
||||
DiscoveryAllowed bool `json:"discovery_allowed"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListTracks(ctx context.Context) ([]ListTracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, listTracks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListTracksRow
|
||||
for rows.Next() {
|
||||
var i ListTracksRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Artist,
|
||||
&i.Album,
|
||||
&i.Genres,
|
||||
&i.ReleaseDate,
|
||||
&i.DurationMs,
|
||||
&i.Popularity,
|
||||
&i.Explicit,
|
||||
&i.Features,
|
||||
&i.External,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.CommercialBoost,
|
||||
&i.QualityPenalty,
|
||||
&i.DiscoveryAllowed,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const recordInteraction = `-- name: RecordInteraction :exec
|
||||
insert into interactions (user_id, track_id, type, weight, occurred_at, context, completed_ms)
|
||||
values ($1, $2, $3, $4, $5, $6::jsonb, $7)
|
||||
`
|
||||
|
||||
type RecordInteractionParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
TrackID string `json:"track_id"`
|
||||
Type string `json:"type"`
|
||||
Weight float64 `json:"weight"`
|
||||
OccurredAt pgtype.Timestamptz `json:"occurred_at"`
|
||||
Column6 []byte `json:"column_6"`
|
||||
CompletedMs int32 `json:"completed_ms"`
|
||||
}
|
||||
|
||||
func (q *Queries) RecordInteraction(ctx context.Context, arg RecordInteractionParams) error {
|
||||
_, err := q.db.Exec(ctx, recordInteraction,
|
||||
arg.UserID,
|
||||
arg.TrackID,
|
||||
arg.Type,
|
||||
arg.Weight,
|
||||
arg.OccurredAt,
|
||||
arg.Column6,
|
||||
arg.CompletedMs,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertControls = `-- name: UpsertControls :exec
|
||||
insert into user_controls (user_id, allow_explicit, excluded_tracks, excluded_artists, excluded_genres, postponed_tracks)
|
||||
values ($1, $2, $3::jsonb, $4::jsonb, $5::jsonb, $6::jsonb)
|
||||
on conflict (user_id) do update set
|
||||
allow_explicit = excluded.allow_explicit,
|
||||
excluded_tracks = excluded.excluded_tracks,
|
||||
excluded_artists = excluded.excluded_artists,
|
||||
excluded_genres = excluded.excluded_genres,
|
||||
postponed_tracks = excluded.postponed_tracks,
|
||||
updated_at = now()
|
||||
`
|
||||
|
||||
type UpsertControlsParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
AllowExplicit bool `json:"allow_explicit"`
|
||||
Column3 []byte `json:"column_3"`
|
||||
Column4 []byte `json:"column_4"`
|
||||
Column5 []byte `json:"column_5"`
|
||||
Column6 []byte `json:"column_6"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertControls(ctx context.Context, arg UpsertControlsParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertControls,
|
||||
arg.UserID,
|
||||
arg.AllowExplicit,
|
||||
arg.Column3,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
arg.Column6,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertTrack = `-- name: UpsertTrack :exec
|
||||
insert into tracks (
|
||||
id, title, artist, album, genres, release_date, duration_ms, popularity,
|
||||
explicit, features, external, commercial_boost, quality_penalty, discovery_allowed
|
||||
) values (
|
||||
$1, $2, $3, $4, $5::jsonb, $6, $7, $8,
|
||||
$9, $10::jsonb, $11::jsonb, $12, $13, $14
|
||||
)
|
||||
on conflict (id) do update set
|
||||
title = excluded.title,
|
||||
artist = excluded.artist,
|
||||
album = excluded.album,
|
||||
genres = excluded.genres,
|
||||
release_date = excluded.release_date,
|
||||
duration_ms = excluded.duration_ms,
|
||||
popularity = excluded.popularity,
|
||||
explicit = excluded.explicit,
|
||||
features = excluded.features,
|
||||
external = excluded.external,
|
||||
commercial_boost = excluded.commercial_boost,
|
||||
quality_penalty = excluded.quality_penalty,
|
||||
discovery_allowed = excluded.discovery_allowed,
|
||||
updated_at = now()
|
||||
`
|
||||
|
||||
type UpsertTrackParams struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
Column5 []byte `json:"column_5"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
DurationMs int32 `json:"duration_ms"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
Explicit bool `json:"explicit"`
|
||||
Column10 []byte `json:"column_10"`
|
||||
Column11 []byte `json:"column_11"`
|
||||
CommercialBoost float64 `json:"commercial_boost"`
|
||||
QualityPenalty float64 `json:"quality_penalty"`
|
||||
DiscoveryAllowed bool `json:"discovery_allowed"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertTrack(ctx context.Context, arg UpsertTrackParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertTrack,
|
||||
arg.ID,
|
||||
arg.Title,
|
||||
arg.Artist,
|
||||
arg.Album,
|
||||
arg.Column5,
|
||||
arg.ReleaseDate,
|
||||
arg.DurationMs,
|
||||
arg.Popularity,
|
||||
arg.Explicit,
|
||||
arg.Column10,
|
||||
arg.Column11,
|
||||
arg.CommercialBoost,
|
||||
arg.QualityPenalty,
|
||||
arg.DiscoveryAllowed,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type ImportJob struct {
|
||||
ID string `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
SourceType string `json:"source_type"`
|
||||
SourceValue string `json:"source_value"`
|
||||
Market string `json:"market"`
|
||||
Status string `json:"status"`
|
||||
ImportedTracks int32 `json:"imported_tracks"`
|
||||
UpdatedTracks int32 `json:"updated_tracks"`
|
||||
Skipped int32 `json:"skipped"`
|
||||
Warnings []byte `json:"warnings"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
FinishedAt pgtype.Timestamptz `json:"finished_at"`
|
||||
}
|
||||
|
||||
type Interaction struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
TrackID string `json:"track_id"`
|
||||
Type string `json:"type"`
|
||||
Weight float64 `json:"weight"`
|
||||
OccurredAt pgtype.Timestamptz `json:"occurred_at"`
|
||||
Context []byte `json:"context"`
|
||||
CompletedMs int32 `json:"completed_ms"`
|
||||
}
|
||||
|
||||
type ProviderCache struct {
|
||||
Provider string `json:"provider"`
|
||||
ItemType string `json:"item_type"`
|
||||
ItemID string `json:"item_id"`
|
||||
Market string `json:"market"`
|
||||
Payload []byte `json:"payload"`
|
||||
FetchedAt pgtype.Timestamptz `json:"fetched_at"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
LastError pgtype.Text `json:"last_error"`
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
Genres []byte `json:"genres"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
DurationMs int32 `json:"duration_ms"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
Explicit bool `json:"explicit"`
|
||||
Features []byte `json:"features"`
|
||||
External []byte `json:"external"`
|
||||
CommercialBoost float64 `json:"commercial_boost"`
|
||||
QualityPenalty float64 `json:"quality_penalty"`
|
||||
DiscoveryAllowed bool `json:"discovery_allowed"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TrackEnrichment struct {
|
||||
TrackID string `json:"track_id"`
|
||||
Provider string `json:"provider"`
|
||||
MusicbrainzRecordingID string `json:"musicbrainz_recording_id"`
|
||||
MusicbrainzArtistID string `json:"musicbrainz_artist_id"`
|
||||
Isrc string `json:"isrc"`
|
||||
Payload []byte `json:"payload"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserControl struct {
|
||||
UserID string `json:"user_id"`
|
||||
AllowExplicit bool `json:"allow_explicit"`
|
||||
ExcludedTracks []byte `json:"excluded_tracks"`
|
||||
ExcludedArtists []byte `json:"excluded_artists"`
|
||||
ExcludedGenres []byte `json:"excluded_genres"`
|
||||
PostponedTracks []byte `json:"postponed_tracks"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: provider.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createImportJob = `-- name: CreateImportJob :exec
|
||||
insert into import_jobs (id, provider, source_type, source_value, market, status, imported_tracks, updated_tracks, skipped, warnings, started_at)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11)
|
||||
`
|
||||
|
||||
type CreateImportJobParams struct {
|
||||
ID string `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
SourceType string `json:"source_type"`
|
||||
SourceValue string `json:"source_value"`
|
||||
Market string `json:"market"`
|
||||
Status string `json:"status"`
|
||||
ImportedTracks int32 `json:"imported_tracks"`
|
||||
UpdatedTracks int32 `json:"updated_tracks"`
|
||||
Skipped int32 `json:"skipped"`
|
||||
Column10 []byte `json:"column_10"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateImportJob(ctx context.Context, arg CreateImportJobParams) error {
|
||||
_, err := q.db.Exec(ctx, createImportJob,
|
||||
arg.ID,
|
||||
arg.Provider,
|
||||
arg.SourceType,
|
||||
arg.SourceValue,
|
||||
arg.Market,
|
||||
arg.Status,
|
||||
arg.ImportedTracks,
|
||||
arg.UpdatedTracks,
|
||||
arg.Skipped,
|
||||
arg.Column10,
|
||||
arg.StartedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const finishImportJob = `-- name: FinishImportJob :exec
|
||||
update import_jobs
|
||||
set status = $2,
|
||||
imported_tracks = $3,
|
||||
updated_tracks = $4,
|
||||
skipped = $5,
|
||||
warnings = $6::jsonb,
|
||||
finished_at = $7
|
||||
where id = $1
|
||||
`
|
||||
|
||||
type FinishImportJobParams struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
ImportedTracks int32 `json:"imported_tracks"`
|
||||
UpdatedTracks int32 `json:"updated_tracks"`
|
||||
Skipped int32 `json:"skipped"`
|
||||
Column6 []byte `json:"column_6"`
|
||||
FinishedAt pgtype.Timestamptz `json:"finished_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) FinishImportJob(ctx context.Context, arg FinishImportJobParams) error {
|
||||
_, err := q.db.Exec(ctx, finishImportJob,
|
||||
arg.ID,
|
||||
arg.Status,
|
||||
arg.ImportedTracks,
|
||||
arg.UpdatedTracks,
|
||||
arg.Skipped,
|
||||
arg.Column6,
|
||||
arg.FinishedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getProviderCache = `-- name: GetProviderCache :one
|
||||
select provider, item_type, item_id, market, payload, fetched_at, expires_at, coalesce(last_error, '') as last_error
|
||||
from provider_cache
|
||||
where provider = $1 and item_type = $2 and item_id = $3 and market = $4
|
||||
`
|
||||
|
||||
type GetProviderCacheParams struct {
|
||||
Provider string `json:"provider"`
|
||||
ItemType string `json:"item_type"`
|
||||
ItemID string `json:"item_id"`
|
||||
Market string `json:"market"`
|
||||
}
|
||||
|
||||
type GetProviderCacheRow struct {
|
||||
Provider string `json:"provider"`
|
||||
ItemType string `json:"item_type"`
|
||||
ItemID string `json:"item_id"`
|
||||
Market string `json:"market"`
|
||||
Payload []byte `json:"payload"`
|
||||
FetchedAt pgtype.Timestamptz `json:"fetched_at"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
LastError string `json:"last_error"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetProviderCache(ctx context.Context, arg GetProviderCacheParams) (GetProviderCacheRow, error) {
|
||||
row := q.db.QueryRow(ctx, getProviderCache,
|
||||
arg.Provider,
|
||||
arg.ItemType,
|
||||
arg.ItemID,
|
||||
arg.Market,
|
||||
)
|
||||
var i GetProviderCacheRow
|
||||
err := row.Scan(
|
||||
&i.Provider,
|
||||
&i.ItemType,
|
||||
&i.ItemID,
|
||||
&i.Market,
|
||||
&i.Payload,
|
||||
&i.FetchedAt,
|
||||
&i.ExpiresAt,
|
||||
&i.LastError,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const providerCacheStats = `-- name: ProviderCacheStats :one
|
||||
select count(*)::bigint as entries,
|
||||
count(*) filter (where expires_at > now())::bigint as fresh_entries,
|
||||
count(*) filter (where expires_at <= now())::bigint as stale_entries
|
||||
from provider_cache
|
||||
`
|
||||
|
||||
type ProviderCacheStatsRow struct {
|
||||
Entries int64 `json:"entries"`
|
||||
FreshEntries int64 `json:"fresh_entries"`
|
||||
StaleEntries int64 `json:"stale_entries"`
|
||||
}
|
||||
|
||||
func (q *Queries) ProviderCacheStats(ctx context.Context) (ProviderCacheStatsRow, error) {
|
||||
row := q.db.QueryRow(ctx, providerCacheStats)
|
||||
var i ProviderCacheStatsRow
|
||||
err := row.Scan(&i.Entries, &i.FreshEntries, &i.StaleEntries)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertProviderCache = `-- name: UpsertProviderCache :exec
|
||||
insert into provider_cache (provider, item_type, item_id, market, payload, fetched_at, expires_at, last_error)
|
||||
values ($1, $2, $3, $4, $5::jsonb, $6, $7, nullif($8, ''))
|
||||
on conflict (provider, item_type, item_id, market) do update set
|
||||
payload = excluded.payload,
|
||||
fetched_at = excluded.fetched_at,
|
||||
expires_at = excluded.expires_at,
|
||||
last_error = excluded.last_error
|
||||
`
|
||||
|
||||
type UpsertProviderCacheParams struct {
|
||||
Provider string `json:"provider"`
|
||||
ItemType string `json:"item_type"`
|
||||
ItemID string `json:"item_id"`
|
||||
Market string `json:"market"`
|
||||
Column5 []byte `json:"column_5"`
|
||||
FetchedAt pgtype.Timestamptz `json:"fetched_at"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
Column8 interface{} `json:"column_8"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertProviderCache(ctx context.Context, arg UpsertProviderCacheParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertProviderCache,
|
||||
arg.Provider,
|
||||
arg.ItemType,
|
||||
arg.ItemID,
|
||||
arg.Market,
|
||||
arg.Column5,
|
||||
arg.FetchedAt,
|
||||
arg.ExpiresAt,
|
||||
arg.Column8,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertTrackEnrichment = `-- name: UpsertTrackEnrichment :exec
|
||||
insert into track_enrichment (track_id, provider, musicbrainz_recording_id, musicbrainz_artist_id, isrc, payload, updated_at)
|
||||
values ($1, $2, $3, $4, $5, $6::jsonb, $7)
|
||||
on conflict (track_id, provider) do update set
|
||||
musicbrainz_recording_id = excluded.musicbrainz_recording_id,
|
||||
musicbrainz_artist_id = excluded.musicbrainz_artist_id,
|
||||
isrc = excluded.isrc,
|
||||
payload = excluded.payload,
|
||||
updated_at = excluded.updated_at
|
||||
`
|
||||
|
||||
type UpsertTrackEnrichmentParams struct {
|
||||
TrackID string `json:"track_id"`
|
||||
Provider string `json:"provider"`
|
||||
MusicbrainzRecordingID string `json:"musicbrainz_recording_id"`
|
||||
MusicbrainzArtistID string `json:"musicbrainz_artist_id"`
|
||||
Isrc string `json:"isrc"`
|
||||
Column6 []byte `json:"column_6"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertTrackEnrichment(ctx context.Context, arg UpsertTrackEnrichmentParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertTrackEnrichment,
|
||||
arg.TrackID,
|
||||
arg.Provider,
|
||||
arg.MusicbrainzRecordingID,
|
||||
arg.MusicbrainzArtistID,
|
||||
arg.Isrc,
|
||||
arg.Column6,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/provider"
|
||||
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/recommendation"
|
||||
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/storage/postgres/db"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func New(pool *pgxpool.Pool) *Store {
|
||||
return &Store{pool: pool, queries: db.New(pool)}
|
||||
}
|
||||
|
||||
func (s *Store) Ping(ctx context.Context) error {
|
||||
return s.pool.Ping(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) UpsertTrack(ctx context.Context, track recommendation.Track) error {
|
||||
params, err := upsertTrackParams(track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.queries.UpsertTrack(ctx, params)
|
||||
}
|
||||
|
||||
func (s *Store) UpsertTracks(ctx context.Context, tracks []recommendation.Track) error {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
queries := s.queries.WithTx(tx)
|
||||
for _, track := range tracks {
|
||||
params, err := upsertTrackParams(track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := queries.UpsertTrack(ctx, params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) GetTracksByIDs(ctx context.Context, ids []string) ([]recommendation.Track, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
select id, title, artist, album, genres, release_date, duration_ms, popularity,
|
||||
explicit, features, external, created_at, updated_at, commercial_boost, quality_penalty, discovery_allowed
|
||||
from tracks
|
||||
where id = any($1)
|
||||
order by id`, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tracks := make([]recommendation.Track, 0, len(ids))
|
||||
for rows.Next() {
|
||||
track, err := scanTrack(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
return tracks, rows.Err()
|
||||
}
|
||||
|
||||
func upsertTrackParams(track recommendation.Track) (db.UpsertTrackParams, error) {
|
||||
features, err := json.Marshal(track.Features)
|
||||
if err != nil {
|
||||
return db.UpsertTrackParams{}, fmt.Errorf("marshal features: %w", err)
|
||||
}
|
||||
genres, err := json.Marshal(track.Genres)
|
||||
if err != nil {
|
||||
return db.UpsertTrackParams{}, fmt.Errorf("marshal genres: %w", err)
|
||||
}
|
||||
external, err := json.Marshal(track.External)
|
||||
if err != nil {
|
||||
return db.UpsertTrackParams{}, fmt.Errorf("marshal external ids: %w", err)
|
||||
}
|
||||
return db.UpsertTrackParams{
|
||||
ID: track.ID,
|
||||
Title: track.Title,
|
||||
Artist: track.Artist,
|
||||
Album: track.Album,
|
||||
Column5: genres,
|
||||
ReleaseDate: track.ReleaseDate,
|
||||
DurationMs: int32(track.DurationMS),
|
||||
Popularity: track.Popularity,
|
||||
Explicit: track.Explicit,
|
||||
Column10: features,
|
||||
Column11: external,
|
||||
CommercialBoost: track.CommercialBoost,
|
||||
QualityPenalty: track.QualityPenalty,
|
||||
DiscoveryAllowed: track.DiscoveryAllowed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) RecordInteraction(ctx context.Context, interaction recommendation.Interaction) error {
|
||||
if interaction.OccurredAt.IsZero() {
|
||||
interaction.OccurredAt = time.Now().UTC()
|
||||
}
|
||||
contextJSON, err := json.Marshal(interaction.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal interaction context: %w", err)
|
||||
}
|
||||
return s.queries.RecordInteraction(ctx, db.RecordInteractionParams{
|
||||
UserID: interaction.UserID,
|
||||
TrackID: interaction.TrackID,
|
||||
Type: string(interaction.Type),
|
||||
Weight: interaction.Weight,
|
||||
OccurredAt: pgtype.Timestamptz{Time: interaction.OccurredAt, Valid: true},
|
||||
Column6: contextJSON,
|
||||
CompletedMs: int32(interaction.CompletedMS),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) GetControls(ctx context.Context, userID string) (recommendation.UserControls, error) {
|
||||
row, err := s.queries.GetControls(ctx, userID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return recommendation.UserControls{UserID: userID, AllowExplicit: true}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return recommendation.UserControls{}, err
|
||||
}
|
||||
controls := recommendation.UserControls{UserID: row.UserID, AllowExplicit: row.AllowExplicit}
|
||||
if err := unmarshalStringSlice(row.ExcludedTracks, &controls.ExcludedTracks); err != nil {
|
||||
return recommendation.UserControls{}, err
|
||||
}
|
||||
if err := unmarshalStringSlice(row.ExcludedArtists, &controls.ExcludedArtists); err != nil {
|
||||
return recommendation.UserControls{}, err
|
||||
}
|
||||
if err := unmarshalStringSlice(row.ExcludedGenres, &controls.ExcludedGenres); err != nil {
|
||||
return recommendation.UserControls{}, err
|
||||
}
|
||||
if err := unmarshalStringSlice(row.PostponedTracks, &controls.PostponedTracks); err != nil {
|
||||
return recommendation.UserControls{}, err
|
||||
}
|
||||
return controls, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertControls(ctx context.Context, controls recommendation.UserControls) error {
|
||||
excludedTracks, err := json.Marshal(controls.ExcludedTracks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
excludedArtists, err := json.Marshal(controls.ExcludedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
excludedGenres, err := json.Marshal(controls.ExcludedGenres)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
postponedTracks, err := json.Marshal(controls.PostponedTracks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.queries.UpsertControls(ctx, db.UpsertControlsParams{
|
||||
UserID: controls.UserID,
|
||||
AllowExplicit: controls.AllowExplicit,
|
||||
Column3: excludedTracks,
|
||||
Column4: excludedArtists,
|
||||
Column5: excludedGenres,
|
||||
Column6: postponedTracks,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) Snapshot(ctx context.Context, userID string) (recommendation.CatalogSnapshot, error) {
|
||||
tracks, err := s.listTracks(ctx)
|
||||
if err != nil {
|
||||
return recommendation.CatalogSnapshot{}, err
|
||||
}
|
||||
interactions, err := s.listRecentInteractions(ctx)
|
||||
if err != nil {
|
||||
return recommendation.CatalogSnapshot{}, err
|
||||
}
|
||||
controls, err := s.GetControls(ctx, userID)
|
||||
if err != nil {
|
||||
return recommendation.CatalogSnapshot{}, err
|
||||
}
|
||||
return recommendation.CatalogSnapshot{
|
||||
Tracks: tracks,
|
||||
Interactions: interactions,
|
||||
Controls: controls,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) listTracks(ctx context.Context) ([]recommendation.Track, error) {
|
||||
rows, err := s.queries.ListTracks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracks := make([]recommendation.Track, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
track, err := trackFromListRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
func trackFromListRow(row db.ListTracksRow) (recommendation.Track, error) {
|
||||
track := recommendation.Track{
|
||||
ID: row.ID,
|
||||
Title: row.Title,
|
||||
Artist: row.Artist,
|
||||
Album: row.Album,
|
||||
ReleaseDate: row.ReleaseDate,
|
||||
DurationMS: int(row.DurationMs),
|
||||
Popularity: row.Popularity,
|
||||
Explicit: row.Explicit,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
CommercialBoost: row.CommercialBoost,
|
||||
QualityPenalty: row.QualityPenalty,
|
||||
DiscoveryAllowed: row.DiscoveryAllowed,
|
||||
}
|
||||
if err := unmarshalStringSlice(row.Genres, &track.Genres); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
if err := json.Unmarshal(row.Features, &track.Features); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
if err := unmarshalStringMap(row.External, &track.External); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
return track, nil
|
||||
}
|
||||
|
||||
type rowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanTrack(scanner rowScanner) (recommendation.Track, error) {
|
||||
var (
|
||||
genres, features, external []byte
|
||||
createdAt, updatedAt pgtype.Timestamptz
|
||||
track recommendation.Track
|
||||
)
|
||||
if err := scanner.Scan(
|
||||
&track.ID,
|
||||
&track.Title,
|
||||
&track.Artist,
|
||||
&track.Album,
|
||||
&genres,
|
||||
&track.ReleaseDate,
|
||||
&track.DurationMS,
|
||||
&track.Popularity,
|
||||
&track.Explicit,
|
||||
&features,
|
||||
&external,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
&track.CommercialBoost,
|
||||
&track.QualityPenalty,
|
||||
&track.DiscoveryAllowed,
|
||||
); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
track.CreatedAt = createdAt.Time
|
||||
track.UpdatedAt = updatedAt.Time
|
||||
if err := unmarshalStringSlice(genres, &track.Genres); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
if err := json.Unmarshal(features, &track.Features); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
if err := unmarshalStringMap(external, &track.External); err != nil {
|
||||
return recommendation.Track{}, err
|
||||
}
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetProviderCache(ctx context.Context, providerName, itemType, itemID, market string) (provider.CacheEntry, bool, error) {
|
||||
var entry provider.CacheEntry
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
select provider, item_type, item_id, market, payload, fetched_at, expires_at, coalesce(last_error, '')
|
||||
from provider_cache
|
||||
where provider = $1 and item_type = $2 and item_id = $3 and market = $4`,
|
||||
providerName, itemType, itemID, market,
|
||||
).Scan(&entry.Provider, &entry.ItemType, &entry.ItemID, &entry.Market, &entry.Payload, &entry.FetchedAt, &entry.ExpiresAt, &entry.LastError)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return provider.CacheEntry{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return provider.CacheEntry{}, false, err
|
||||
}
|
||||
return entry, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertProviderCache(ctx context.Context, entry provider.CacheEntry) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
insert into provider_cache (provider, item_type, item_id, market, payload, fetched_at, expires_at, last_error)
|
||||
values ($1, $2, $3, $4, $5::jsonb, $6, $7, nullif($8, ''))
|
||||
on conflict (provider, item_type, item_id, market) do update set
|
||||
payload = excluded.payload,
|
||||
fetched_at = excluded.fetched_at,
|
||||
expires_at = excluded.expires_at,
|
||||
last_error = excluded.last_error`,
|
||||
entry.Provider,
|
||||
entry.ItemType,
|
||||
entry.ItemID,
|
||||
entry.Market,
|
||||
emptyObjectIfNil(entry.Payload),
|
||||
entry.FetchedAt,
|
||||
entry.ExpiresAt,
|
||||
entry.LastError,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) ProviderCacheStats(ctx context.Context) (provider.CacheStats, error) {
|
||||
var stats provider.CacheStats
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
select count(*)::bigint,
|
||||
count(*) filter (where expires_at > now())::bigint,
|
||||
count(*) filter (where expires_at <= now())::bigint
|
||||
from provider_cache`,
|
||||
).Scan(&stats.Entries, &stats.FreshEntries, &stats.StaleEntries)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateImportJob(ctx context.Context, job provider.ImportJob) error {
|
||||
warnings, err := json.Marshal(job.Warnings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
insert into import_jobs (id, provider, source_type, source_value, market, status, imported_tracks, updated_tracks, skipped, warnings, started_at)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11)`,
|
||||
job.ID, job.Provider, job.SourceType, job.SourceValue, job.Market, job.Status,
|
||||
job.ImportedTracks, job.UpdatedTracks, job.Skipped, warnings, job.StartedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) FinishImportJob(ctx context.Context, job provider.ImportJob) error {
|
||||
warnings, err := json.Marshal(job.Warnings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
update import_jobs
|
||||
set status = $2,
|
||||
imported_tracks = $3,
|
||||
updated_tracks = $4,
|
||||
skipped = $5,
|
||||
warnings = $6::jsonb,
|
||||
finished_at = $7
|
||||
where id = $1`,
|
||||
job.ID, job.Status, job.ImportedTracks, job.UpdatedTracks, job.Skipped, warnings, job.FinishedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) UpsertTrackEnrichment(ctx context.Context, enrichment provider.TrackEnrichment) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
insert into track_enrichment (track_id, provider, musicbrainz_recording_id, musicbrainz_artist_id, isrc, payload, updated_at)
|
||||
values ($1, $2, $3, $4, $5, $6::jsonb, $7)
|
||||
on conflict (track_id, provider) do update set
|
||||
musicbrainz_recording_id = excluded.musicbrainz_recording_id,
|
||||
musicbrainz_artist_id = excluded.musicbrainz_artist_id,
|
||||
isrc = excluded.isrc,
|
||||
payload = excluded.payload,
|
||||
updated_at = excluded.updated_at`,
|
||||
enrichment.TrackID,
|
||||
enrichment.Provider,
|
||||
enrichment.MusicBrainzRecordingID,
|
||||
enrichment.MusicBrainzArtistID,
|
||||
enrichment.ISRC,
|
||||
emptyObjectIfNil(enrichment.Payload),
|
||||
enrichment.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func emptyObjectIfNil(payload []byte) []byte {
|
||||
if len(payload) == 0 {
|
||||
return []byte(`{}`)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func (s *Store) listRecentInteractions(ctx context.Context) ([]recommendation.Interaction, error) {
|
||||
rows, err := s.queries.ListRecentInteractions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
interactions := make([]recommendation.Interaction, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
interaction := recommendation.Interaction{
|
||||
UserID: row.UserID,
|
||||
TrackID: row.TrackID,
|
||||
Type: recommendation.InteractionType(row.Type),
|
||||
Weight: row.Weight,
|
||||
OccurredAt: row.OccurredAt.Time,
|
||||
CompletedMS: int(row.CompletedMs),
|
||||
}
|
||||
if len(row.Context) > 0 {
|
||||
if err := json.Unmarshal(row.Context, &interaction.Context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
interactions = append(interactions, interaction)
|
||||
}
|
||||
return interactions, nil
|
||||
}
|
||||
|
||||
func unmarshalStringSlice(raw []byte, out *[]string) error {
|
||||
if len(raw) == 0 {
|
||||
*out = nil
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(raw, out)
|
||||
}
|
||||
|
||||
func unmarshalStringMap(raw []byte, out *map[string]string) error {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
*out = nil
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(raw, out)
|
||||
}
|
||||
Reference in New Issue
Block a user