mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 21:48:18 -08:00
fix: use sql rank (#148)
This commit is contained in:
parent
aa7fddd518
commit
d2d6924e05
20 changed files with 386 additions and 270 deletions
|
|
@ -48,32 +48,32 @@ async function getLastListens(
|
||||||
|
|
||||||
async function getTopTracks(
|
async function getTopTracks(
|
||||||
args: getItemsArgs
|
args: getItemsArgs
|
||||||
): Promise<PaginatedResponse<Track>> {
|
): Promise<PaginatedResponse<Ranked<Track>>> {
|
||||||
let url = `/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
let url = `/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
||||||
|
|
||||||
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
|
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
|
||||||
else if (args.album_id) url += `&album_id=${args.album_id}`;
|
else if (args.album_id) url += `&album_id=${args.album_id}`;
|
||||||
|
|
||||||
const r = await fetch(url);
|
const r = await fetch(url);
|
||||||
return handleJson<PaginatedResponse<Track>>(r);
|
return handleJson<PaginatedResponse<Ranked<Track>>>(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTopAlbums(
|
async function getTopAlbums(
|
||||||
args: getItemsArgs
|
args: getItemsArgs
|
||||||
): Promise<PaginatedResponse<Album>> {
|
): Promise<PaginatedResponse<Ranked<Album>>> {
|
||||||
let url = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
let url = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
||||||
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
|
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
|
||||||
|
|
||||||
const r = await fetch(url);
|
const r = await fetch(url);
|
||||||
return handleJson<PaginatedResponse<Album>>(r);
|
return handleJson<PaginatedResponse<Ranked<Album>>>(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTopArtists(
|
async function getTopArtists(
|
||||||
args: getItemsArgs
|
args: getItemsArgs
|
||||||
): Promise<PaginatedResponse<Artist>> {
|
): Promise<PaginatedResponse<Ranked<Artist>>> {
|
||||||
const url = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
const url = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
||||||
const r = await fetch(url);
|
const r = await fetch(url);
|
||||||
return handleJson<PaginatedResponse<Artist>>(r);
|
return handleJson<PaginatedResponse<Ranked<Artist>>>(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getActivity(
|
async function getActivity(
|
||||||
|
|
@ -407,6 +407,10 @@ type PaginatedResponse<T> = {
|
||||||
current_page: number;
|
current_page: number;
|
||||||
items_per_page: number;
|
items_per_page: number;
|
||||||
};
|
};
|
||||||
|
type Ranked<T> = {
|
||||||
|
item: T;
|
||||||
|
rank: number;
|
||||||
|
};
|
||||||
type ListenActivityItem = {
|
type ListenActivityItem = {
|
||||||
start_time: Date;
|
start_time: Date;
|
||||||
listens: number;
|
listens: number;
|
||||||
|
|
@ -480,6 +484,7 @@ export type {
|
||||||
Listen,
|
Listen,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
|
Ranked,
|
||||||
ListenActivityItem,
|
ListenActivityItem,
|
||||||
InterestBucket,
|
InterestBucket,
|
||||||
User,
|
User,
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ import {
|
||||||
type Artist,
|
type Artist,
|
||||||
type Track,
|
type Track,
|
||||||
type PaginatedResponse,
|
type PaginatedResponse,
|
||||||
|
type Ranked,
|
||||||
} from "api/api";
|
} from "api/api";
|
||||||
|
|
||||||
type Item = Album | Track | Artist;
|
type Item = Album | Track | Artist;
|
||||||
|
|
||||||
interface Props<T extends Item> {
|
interface Props<T extends Ranked<Item>> {
|
||||||
data: PaginatedResponse<T>;
|
data: PaginatedResponse<T>;
|
||||||
separators?: ConstrainBoolean;
|
separators?: ConstrainBoolean;
|
||||||
ranked?: boolean;
|
ranked?: boolean;
|
||||||
|
|
@ -18,33 +19,17 @@ interface Props<T extends Item> {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TopItemList<T extends Item>({
|
export default function TopItemList<T extends Ranked<Item>>({
|
||||||
data,
|
data,
|
||||||
separators,
|
separators,
|
||||||
type,
|
type,
|
||||||
className,
|
className,
|
||||||
ranked,
|
ranked,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const currentParams = new URLSearchParams(location.search);
|
|
||||||
const page = Math.max(parseInt(currentParams.get("page") || "1"), 1);
|
|
||||||
|
|
||||||
let lastRank = 0;
|
|
||||||
|
|
||||||
const calculateRank = (data: Item[], page: number, index: number): number => {
|
|
||||||
if (
|
|
||||||
index === 0 ||
|
|
||||||
data[index] == undefined ||
|
|
||||||
!(data[index].listen_count === data[index - 1].listen_count)
|
|
||||||
) {
|
|
||||||
lastRank = index + 1 + (page - 1) * 100;
|
|
||||||
}
|
|
||||||
return lastRank;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-1 ${className} min-w-[200px]`}>
|
<div className={`flex flex-col gap-1 ${className} min-w-[200px]`}>
|
||||||
{data.items.map((item, index) => {
|
{data.items.map((item, index) => {
|
||||||
const key = `${type}-${item.id}`;
|
const key = `${type}-${item.item.id}`;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
|
|
@ -57,10 +42,10 @@ export default function TopItemList<T extends Item>({
|
||||||
>
|
>
|
||||||
<ItemCard
|
<ItemCard
|
||||||
ranked={ranked}
|
ranked={ranked}
|
||||||
rank={calculateRank(data.items, page, index)}
|
rank={item.rank}
|
||||||
item={item}
|
item={item.item}
|
||||||
type={type}
|
type={type}
|
||||||
key={type + item.id}
|
key={type + item.item.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import TopItemList from "~/components/TopItemList";
|
import TopItemList from "~/components/TopItemList";
|
||||||
import ChartLayout from "./ChartLayout";
|
import ChartLayout from "./ChartLayout";
|
||||||
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
|
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
|
||||||
import { type Album, type PaginatedResponse } from "api/api";
|
import { type Album, type PaginatedResponse, type Ranked } from "api/api";
|
||||||
|
|
||||||
export async function clientLoader({ request }: LoaderFunctionArgs) {
|
export async function clientLoader({ request }: LoaderFunctionArgs) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
@ -21,7 +21,7 @@ export async function clientLoader({ request }: LoaderFunctionArgs) {
|
||||||
|
|
||||||
export default function AlbumChart() {
|
export default function AlbumChart() {
|
||||||
const { top_albums: initialData } = useLoaderData<{
|
const { top_albums: initialData } = useLoaderData<{
|
||||||
top_albums: PaginatedResponse<Album>;
|
top_albums: PaginatedResponse<Ranked<Album>>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import TopItemList from "~/components/TopItemList";
|
import TopItemList from "~/components/TopItemList";
|
||||||
import ChartLayout from "./ChartLayout";
|
import ChartLayout from "./ChartLayout";
|
||||||
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
|
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
|
||||||
import { type Album, type PaginatedResponse } from "api/api";
|
import { type Album, type PaginatedResponse, type Ranked } from "api/api";
|
||||||
|
|
||||||
export async function clientLoader({ request }: LoaderFunctionArgs) {
|
export async function clientLoader({ request }: LoaderFunctionArgs) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
@ -21,7 +21,7 @@ export async function clientLoader({ request }: LoaderFunctionArgs) {
|
||||||
|
|
||||||
export default function Artist() {
|
export default function Artist() {
|
||||||
const { top_artists: initialData } = useLoaderData<{
|
const { top_artists: initialData } = useLoaderData<{
|
||||||
top_artists: PaginatedResponse<Album>;
|
top_artists: PaginatedResponse<Ranked<Album>>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import TopItemList from "~/components/TopItemList";
|
import TopItemList from "~/components/TopItemList";
|
||||||
import ChartLayout from "./ChartLayout";
|
import ChartLayout from "./ChartLayout";
|
||||||
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
|
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
|
||||||
import { type Album, type PaginatedResponse } from "api/api";
|
import { type Track, type PaginatedResponse, type Ranked } from "api/api";
|
||||||
|
|
||||||
export async function clientLoader({ request }: LoaderFunctionArgs) {
|
export async function clientLoader({ request }: LoaderFunctionArgs) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
@ -15,13 +15,13 @@ export async function clientLoader({ request }: LoaderFunctionArgs) {
|
||||||
throw new Response("Failed to load top tracks", { status: 500 });
|
throw new Response("Failed to load top tracks", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const top_tracks: PaginatedResponse<Album> = await res.json();
|
const top_tracks: PaginatedResponse<Track> = await res.json();
|
||||||
return { top_tracks };
|
return { top_tracks };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TrackChart() {
|
export default function TrackChart() {
|
||||||
const { top_tracks: initialData } = useLoaderData<{
|
const { top_tracks: initialData } = useLoaderData<{
|
||||||
top_tracks: PaginatedResponse<Album>;
|
top_tracks: PaginatedResponse<Ranked<Track>>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ WHERE a.musicbrainz_id = $1
|
||||||
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name;
|
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name;
|
||||||
|
|
||||||
-- name: GetTopArtistsPaginated :many
|
-- name: GetTopArtistsPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.name,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
a.id,
|
a.id,
|
||||||
a.name,
|
a.name,
|
||||||
|
|
@ -68,8 +76,9 @@ JOIN tracks t ON l.track_id = t.id
|
||||||
JOIN artist_tracks at ON at.track_id = t.id
|
JOIN artist_tracks at ON at.track_id = t.id
|
||||||
JOIN artists_with_name a ON a.id = at.artist_id
|
JOIN artists_with_name a ON a.id = at.artist_id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY a.id, a.name, a.musicbrainz_id, a.image, a.image_source, a.name
|
GROUP BY a.id, a.name, a.musicbrainz_id, a.image
|
||||||
ORDER BY listen_count DESC, a.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: CountTopArtists :one
|
-- name: CountTopArtists :one
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ WHERE r.title = ANY ($1::TEXT[])
|
||||||
);
|
);
|
||||||
|
|
||||||
-- name: GetTopReleasesFromArtist :many
|
-- name: GetTopReleasesFromArtist :many
|
||||||
|
SELECT
|
||||||
|
x.*,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
r.*,
|
r.*,
|
||||||
COUNT(*) AS listen_count,
|
COUNT(*) AS listen_count,
|
||||||
|
|
@ -57,10 +61,15 @@ JOIN artist_releases ar ON r.id = ar.release_id
|
||||||
WHERE ar.artist_id = $5
|
WHERE ar.artist_id = $5
|
||||||
AND l.listened_at BETWEEN $1 AND $2
|
AND l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
||||||
ORDER BY listen_count DESC, r.id
|
) x
|
||||||
|
ORDER BY listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: GetTopReleasesPaginated :many
|
-- name: GetTopReleasesPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.*,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
r.*,
|
r.*,
|
||||||
COUNT(*) AS listen_count,
|
COUNT(*) AS listen_count,
|
||||||
|
|
@ -70,7 +79,8 @@ JOIN tracks t ON l.track_id = t.id
|
||||||
JOIN releases_with_title r ON t.release_id = r.id
|
JOIN releases_with_title r ON t.release_id = r.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
||||||
ORDER BY listen_count DESC, r.id
|
) x
|
||||||
|
ORDER BY listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: CountTopReleases :one
|
-- name: CountTopReleases :one
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,16 @@ GROUP BY t.id, t.title, t.musicbrainz_id, t.duration, t.release_id
|
||||||
HAVING COUNT(DISTINCT at.artist_id) = cardinality($3::int[]);
|
HAVING COUNT(DISTINCT at.artist_id) = cardinality($3::int[]);
|
||||||
|
|
||||||
-- name: GetTopTracksPaginated :many
|
-- name: GetTopTracksPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.title,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.release_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.title,
|
t.title,
|
||||||
|
|
@ -51,10 +61,21 @@ JOIN tracks_with_title t ON l.track_id = t.id
|
||||||
JOIN releases r ON t.release_id = r.id
|
JOIN releases r ON t.release_id = r.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
||||||
ORDER BY listen_count DESC, t.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: GetTopTracksByArtistPaginated :many
|
-- name: GetTopTracksByArtistPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.title,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.release_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.title,
|
t.title,
|
||||||
|
|
@ -70,10 +91,21 @@ JOIN artist_tracks at ON at.track_id = t.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
AND at.artist_id = $5
|
AND at.artist_id = $5
|
||||||
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
||||||
ORDER BY listen_count DESC, t.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: GetTopTracksInReleasePaginated :many
|
-- name: GetTopTracksInReleasePaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.title,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.release_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.title,
|
t.title,
|
||||||
|
|
@ -88,7 +120,8 @@ JOIN releases r ON t.release_id = r.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
AND t.release_id = $5
|
AND t.release_id = $5
|
||||||
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
||||||
ORDER BY listen_count DESC, t.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: CountTopTracks :one
|
-- name: CountTopTracks :one
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ type DB interface {
|
||||||
GetTracksWithNoDurationButHaveMbzID(ctx context.Context, from int32) ([]*models.Track, error)
|
GetTracksWithNoDurationButHaveMbzID(ctx context.Context, from int32) ([]*models.Track, error)
|
||||||
GetArtistsForAlbum(ctx context.Context, id int32) ([]*models.Artist, error)
|
GetArtistsForAlbum(ctx context.Context, id int32) ([]*models.Artist, error)
|
||||||
GetArtistsForTrack(ctx context.Context, id int32) ([]*models.Artist, error)
|
GetArtistsForTrack(ctx context.Context, id int32) ([]*models.Artist, error)
|
||||||
GetTopTracksPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Track], error)
|
GetTopTracksPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[RankedItem[*models.Track]], error)
|
||||||
GetTopArtistsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Artist], error)
|
GetTopArtistsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[RankedItem[*models.Artist]], error)
|
||||||
GetTopAlbumsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Album], error)
|
GetTopAlbumsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[RankedItem[*models.Album]], error)
|
||||||
GetListensPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Listen], error)
|
GetListensPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Listen], error)
|
||||||
GetListenActivity(ctx context.Context, opts ListenActivityOpts) ([]ListenActivityItem, error)
|
GetListenActivity(ctx context.Context, opts ListenActivityOpts) ([]ListenActivityItem, error)
|
||||||
GetAllArtistAliases(ctx context.Context, id int32) ([]models.Alias, error)
|
GetAllArtistAliases(ctx context.Context, id int32) ([]models.Alias, error)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/gabehf/koito/internal/repository"
|
"github.com/gabehf/koito/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[*models.Album], error) {
|
func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[db.RankedItem[*models.Album]], error) {
|
||||||
l := logger.FromContext(ctx)
|
l := logger.FromContext(ctx)
|
||||||
offset := (opts.Page - 1) * opts.Limit
|
offset := (opts.Page - 1) * opts.Limit
|
||||||
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
||||||
|
|
@ -19,7 +19,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
opts.Limit = DefaultItemsPerPage
|
opts.Limit = DefaultItemsPerPage
|
||||||
}
|
}
|
||||||
|
|
||||||
var rgs []*models.Album
|
var rgs []db.RankedItem[*models.Album]
|
||||||
var count int64
|
var count int64
|
||||||
|
|
||||||
if opts.ArtistID != 0 {
|
if opts.ArtistID != 0 {
|
||||||
|
|
@ -36,7 +36,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesFromArtist: %w", err)
|
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesFromArtist: %w", err)
|
||||||
}
|
}
|
||||||
rgs = make([]*models.Album, len(rows))
|
rgs = make([]db.RankedItem[*models.Album], len(rows))
|
||||||
l.Debug().Msgf("Database responded with %d items", len(rows))
|
l.Debug().Msgf("Database responded with %d items", len(rows))
|
||||||
for i, v := range rows {
|
for i, v := range rows {
|
||||||
artists := make([]models.SimpleArtist, 0)
|
artists := make([]models.SimpleArtist, 0)
|
||||||
|
|
@ -45,7 +45,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", v.ID)
|
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", v.ID)
|
||||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
|
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
|
||||||
}
|
}
|
||||||
rgs[i] = &models.Album{
|
rgs[i].Item = &models.Album{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
MbzID: v.MusicBrainzID,
|
MbzID: v.MusicBrainzID,
|
||||||
Title: v.Title,
|
Title: v.Title,
|
||||||
|
|
@ -54,6 +54,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
VariousArtists: v.VariousArtists,
|
VariousArtists: v.VariousArtists,
|
||||||
ListenCount: v.ListenCount,
|
ListenCount: v.ListenCount,
|
||||||
}
|
}
|
||||||
|
rgs[i].Rank = v.Rank
|
||||||
}
|
}
|
||||||
count, err = d.q.CountReleasesFromArtist(ctx, int32(opts.ArtistID))
|
count, err = d.q.CountReleasesFromArtist(ctx, int32(opts.ArtistID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -71,7 +72,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesPaginated: %w", err)
|
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesPaginated: %w", err)
|
||||||
}
|
}
|
||||||
rgs = make([]*models.Album, len(rows))
|
rgs = make([]db.RankedItem[*models.Album], len(rows))
|
||||||
l.Debug().Msgf("Database responded with %d items", len(rows))
|
l.Debug().Msgf("Database responded with %d items", len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
artists := make([]models.SimpleArtist, 0)
|
artists := make([]models.SimpleArtist, 0)
|
||||||
|
|
@ -80,16 +81,16 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", row.ID)
|
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", row.ID)
|
||||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
|
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
|
||||||
}
|
}
|
||||||
t := &models.Album{
|
rgs[i].Item = &models.Album{
|
||||||
Title: row.Title,
|
|
||||||
MbzID: row.MusicBrainzID,
|
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
|
MbzID: row.MusicBrainzID,
|
||||||
|
Title: row.Title,
|
||||||
Image: row.Image,
|
Image: row.Image,
|
||||||
Artists: artists,
|
Artists: artists,
|
||||||
VariousArtists: row.VariousArtists,
|
VariousArtists: row.VariousArtists,
|
||||||
ListenCount: row.ListenCount,
|
ListenCount: row.ListenCount,
|
||||||
}
|
}
|
||||||
rgs[i] = t
|
rgs[i].Rank = row.Rank
|
||||||
}
|
}
|
||||||
count, err = d.q.CountTopReleases(ctx, repository.CountTopReleasesParams{
|
count, err = d.q.CountTopReleases(ctx, repository.CountTopReleasesParams{
|
||||||
ListenedAt: t1,
|
ListenedAt: t1,
|
||||||
|
|
@ -100,7 +101,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
}
|
}
|
||||||
l.Debug().Msgf("Database responded with %d albums out of a total %d", len(rows), count)
|
l.Debug().Msgf("Database responded with %d albums out of a total %d", len(rows), count)
|
||||||
}
|
}
|
||||||
return &db.PaginatedResponse[*models.Album]{
|
return &db.PaginatedResponse[db.RankedItem[*models.Album]]{
|
||||||
Items: rgs,
|
Items: rgs,
|
||||||
TotalCount: count,
|
TotalCount: count,
|
||||||
ItemsPerPage: int32(opts.Limit),
|
ItemsPerPage: int32(opts.Limit),
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,16 @@ func TestGetTopAlbumsPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 4)
|
require.Len(t, resp.Items, 4)
|
||||||
assert.Equal(t, int64(4), resp.TotalCount)
|
assert.Equal(t, int64(4), resp.TotalCount)
|
||||||
assert.Equal(t, "Release One", resp.Items[0].Title)
|
assert.Equal(t, "Release One", resp.Items[0].Item.Title)
|
||||||
assert.Equal(t, "Release Two", resp.Items[1].Title)
|
assert.Equal(t, "Release Two", resp.Items[1].Item.Title)
|
||||||
assert.Equal(t, "Release Three", resp.Items[2].Title)
|
assert.Equal(t, "Release Three", resp.Items[2].Item.Title)
|
||||||
assert.Equal(t, "Release Four", resp.Items[3].Title)
|
assert.Equal(t, "Release Four", resp.Items[3].Item.Title)
|
||||||
|
|
||||||
// Test pagination
|
// Test pagination
|
||||||
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, "Release Two", resp.Items[0].Title)
|
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
// Test page out of range
|
// Test page out of range
|
||||||
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
||||||
|
|
@ -57,29 +57,29 @@ func TestGetTopAlbumsPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Release Four", resp.Items[0].Title)
|
assert.Equal(t, "Release Four", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
|
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 2)
|
require.Len(t, resp.Items, 2)
|
||||||
assert.Equal(t, int64(2), resp.TotalCount)
|
assert.Equal(t, int64(2), resp.TotalCount)
|
||||||
assert.Equal(t, "Release Three", resp.Items[0].Title)
|
assert.Equal(t, "Release Three", resp.Items[0].Item.Title)
|
||||||
assert.Equal(t, "Release Four", resp.Items[1].Title)
|
assert.Equal(t, "Release Four", resp.Items[1].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
|
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 3)
|
require.Len(t, resp.Items, 3)
|
||||||
assert.Equal(t, int64(3), resp.TotalCount)
|
assert.Equal(t, int64(3), resp.TotalCount)
|
||||||
assert.Equal(t, "Release Two", resp.Items[0].Title)
|
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
|
||||||
assert.Equal(t, "Release Three", resp.Items[1].Title)
|
assert.Equal(t, "Release Three", resp.Items[1].Item.Title)
|
||||||
assert.Equal(t, "Release Four", resp.Items[2].Title)
|
assert.Equal(t, "Release Four", resp.Items[2].Item.Title)
|
||||||
|
|
||||||
// test specific artist
|
// test specific artist
|
||||||
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}, ArtistID: 2})
|
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}, ArtistID: 2})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Release Two", resp.Items[0].Title)
|
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
// Test specify dates
|
// Test specify dates
|
||||||
|
|
||||||
|
|
@ -89,11 +89,11 @@ func TestGetTopAlbumsPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Release One", resp.Items[0].Title)
|
assert.Equal(t, "Release One", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
|
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Release Two", resp.Items[0].Title)
|
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/gabehf/koito/internal/repository"
|
"github.com/gabehf/koito/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[*models.Artist], error) {
|
func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[db.RankedItem[*models.Artist]], error) {
|
||||||
l := logger.FromContext(ctx)
|
l := logger.FromContext(ctx)
|
||||||
offset := (opts.Page - 1) * opts.Limit
|
offset := (opts.Page - 1) * opts.Limit
|
||||||
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
||||||
|
|
@ -28,7 +28,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTopArtistsPaginated: GetTopArtistsPaginated: %w", err)
|
return nil, fmt.Errorf("GetTopArtistsPaginated: GetTopArtistsPaginated: %w", err)
|
||||||
}
|
}
|
||||||
rgs := make([]*models.Artist, len(rows))
|
rgs := make([]db.RankedItem[*models.Artist], len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
t := &models.Artist{
|
t := &models.Artist{
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
|
|
@ -37,7 +37,8 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
Image: row.Image,
|
Image: row.Image,
|
||||||
ListenCount: row.ListenCount,
|
ListenCount: row.ListenCount,
|
||||||
}
|
}
|
||||||
rgs[i] = t
|
rgs[i].Item = t
|
||||||
|
rgs[i].Rank = row.Rank
|
||||||
}
|
}
|
||||||
count, err := d.q.CountTopArtists(ctx, repository.CountTopArtistsParams{
|
count, err := d.q.CountTopArtists(ctx, repository.CountTopArtistsParams{
|
||||||
ListenedAt: t1,
|
ListenedAt: t1,
|
||||||
|
|
@ -48,7 +49,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
}
|
}
|
||||||
l.Debug().Msgf("Database responded with %d artists out of a total %d", len(rows), count)
|
l.Debug().Msgf("Database responded with %d artists out of a total %d", len(rows), count)
|
||||||
|
|
||||||
return &db.PaginatedResponse[*models.Artist]{
|
return &db.PaginatedResponse[db.RankedItem[*models.Artist]]{
|
||||||
Items: rgs,
|
Items: rgs,
|
||||||
TotalCount: count,
|
TotalCount: count,
|
||||||
ItemsPerPage: int32(opts.Limit),
|
ItemsPerPage: int32(opts.Limit),
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,16 @@ func TestGetTopArtistsPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 4)
|
require.Len(t, resp.Items, 4)
|
||||||
assert.Equal(t, int64(4), resp.TotalCount)
|
assert.Equal(t, int64(4), resp.TotalCount)
|
||||||
assert.Equal(t, "Artist One", resp.Items[0].Name)
|
assert.Equal(t, "Artist One", resp.Items[0].Item.Name)
|
||||||
assert.Equal(t, "Artist Two", resp.Items[1].Name)
|
assert.Equal(t, "Artist Two", resp.Items[1].Item.Name)
|
||||||
assert.Equal(t, "Artist Three", resp.Items[2].Name)
|
assert.Equal(t, "Artist Three", resp.Items[2].Item.Name)
|
||||||
assert.Equal(t, "Artist Four", resp.Items[3].Name)
|
assert.Equal(t, "Artist Four", resp.Items[3].Item.Name)
|
||||||
|
|
||||||
// Test pagination
|
// Test pagination
|
||||||
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, "Artist Two", resp.Items[0].Name)
|
assert.Equal(t, "Artist Two", resp.Items[0].Item.Name)
|
||||||
|
|
||||||
// Test page out of range
|
// Test page out of range
|
||||||
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
||||||
|
|
@ -57,22 +57,22 @@ func TestGetTopArtistsPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Artist Four", resp.Items[0].Name)
|
assert.Equal(t, "Artist Four", resp.Items[0].Item.Name)
|
||||||
|
|
||||||
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
|
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 2)
|
require.Len(t, resp.Items, 2)
|
||||||
assert.Equal(t, int64(2), resp.TotalCount)
|
assert.Equal(t, int64(2), resp.TotalCount)
|
||||||
assert.Equal(t, "Artist Three", resp.Items[0].Name)
|
assert.Equal(t, "Artist Three", resp.Items[0].Item.Name)
|
||||||
assert.Equal(t, "Artist Four", resp.Items[1].Name)
|
assert.Equal(t, "Artist Four", resp.Items[1].Item.Name)
|
||||||
|
|
||||||
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
|
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 3)
|
require.Len(t, resp.Items, 3)
|
||||||
assert.Equal(t, int64(3), resp.TotalCount)
|
assert.Equal(t, int64(3), resp.TotalCount)
|
||||||
assert.Equal(t, "Artist Two", resp.Items[0].Name)
|
assert.Equal(t, "Artist Two", resp.Items[0].Item.Name)
|
||||||
assert.Equal(t, "Artist Three", resp.Items[1].Name)
|
assert.Equal(t, "Artist Three", resp.Items[1].Item.Name)
|
||||||
assert.Equal(t, "Artist Four", resp.Items[2].Name)
|
assert.Equal(t, "Artist Four", resp.Items[2].Item.Name)
|
||||||
|
|
||||||
// Test specify dates
|
// Test specify dates
|
||||||
|
|
||||||
|
|
@ -82,11 +82,11 @@ func TestGetTopArtistsPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Artist One", resp.Items[0].Name)
|
assert.Equal(t, "Artist One", resp.Items[0].Item.Name)
|
||||||
|
|
||||||
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
|
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Artist Two", resp.Items[0].Name)
|
assert.Equal(t, "Artist Two", resp.Items[0].Item.Name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ import (
|
||||||
"github.com/gabehf/koito/internal/repository"
|
"github.com/gabehf/koito/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[*models.Track], error) {
|
func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[db.RankedItem[*models.Track]], error) {
|
||||||
l := logger.FromContext(ctx)
|
l := logger.FromContext(ctx)
|
||||||
offset := (opts.Page - 1) * opts.Limit
|
offset := (opts.Page - 1) * opts.Limit
|
||||||
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
|
||||||
if opts.Limit == 0 {
|
if opts.Limit == 0 {
|
||||||
opts.Limit = DefaultItemsPerPage
|
opts.Limit = DefaultItemsPerPage
|
||||||
}
|
}
|
||||||
var tracks []*models.Track
|
var tracks []db.RankedItem[*models.Track]
|
||||||
var count int64
|
var count int64
|
||||||
if opts.AlbumID > 0 {
|
if opts.AlbumID > 0 {
|
||||||
l.Debug().Msgf("Fetching top %d tracks on page %d from range %v to %v",
|
l.Debug().Msgf("Fetching top %d tracks on page %d from range %v to %v",
|
||||||
|
|
@ -33,7 +33,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksInReleasePaginated: %w", err)
|
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksInReleasePaginated: %w", err)
|
||||||
}
|
}
|
||||||
tracks = make([]*models.Track, len(rows))
|
tracks = make([]db.RankedItem[*models.Track], len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
artists := make([]models.SimpleArtist, 0)
|
artists := make([]models.SimpleArtist, 0)
|
||||||
err = json.Unmarshal(row.Artists, &artists)
|
err = json.Unmarshal(row.Artists, &artists)
|
||||||
|
|
@ -50,7 +50,8 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
AlbumID: row.ReleaseID,
|
AlbumID: row.ReleaseID,
|
||||||
Artists: artists,
|
Artists: artists,
|
||||||
}
|
}
|
||||||
tracks[i] = t
|
tracks[i].Item = t
|
||||||
|
tracks[i].Rank = row.Rank
|
||||||
}
|
}
|
||||||
count, err = d.q.CountTopTracksByRelease(ctx, repository.CountTopTracksByReleaseParams{
|
count, err = d.q.CountTopTracksByRelease(ctx, repository.CountTopTracksByReleaseParams{
|
||||||
ListenedAt: t1,
|
ListenedAt: t1,
|
||||||
|
|
@ -73,7 +74,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksByArtistPaginated: %w", err)
|
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksByArtistPaginated: %w", err)
|
||||||
}
|
}
|
||||||
tracks = make([]*models.Track, len(rows))
|
tracks = make([]db.RankedItem[*models.Track], len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
artists := make([]models.SimpleArtist, 0)
|
artists := make([]models.SimpleArtist, 0)
|
||||||
err = json.Unmarshal(row.Artists, &artists)
|
err = json.Unmarshal(row.Artists, &artists)
|
||||||
|
|
@ -90,7 +91,8 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
AlbumID: row.ReleaseID,
|
AlbumID: row.ReleaseID,
|
||||||
Artists: artists,
|
Artists: artists,
|
||||||
}
|
}
|
||||||
tracks[i] = t
|
tracks[i].Item = t
|
||||||
|
tracks[i].Rank = row.Rank
|
||||||
}
|
}
|
||||||
count, err = d.q.CountTopTracksByArtist(ctx, repository.CountTopTracksByArtistParams{
|
count, err = d.q.CountTopTracksByArtist(ctx, repository.CountTopTracksByArtistParams{
|
||||||
ListenedAt: t1,
|
ListenedAt: t1,
|
||||||
|
|
@ -112,7 +114,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksPaginated: %w", err)
|
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksPaginated: %w", err)
|
||||||
}
|
}
|
||||||
tracks = make([]*models.Track, len(rows))
|
tracks = make([]db.RankedItem[*models.Track], len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
artists := make([]models.SimpleArtist, 0)
|
artists := make([]models.SimpleArtist, 0)
|
||||||
err = json.Unmarshal(row.Artists, &artists)
|
err = json.Unmarshal(row.Artists, &artists)
|
||||||
|
|
@ -129,7 +131,8 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
AlbumID: row.ReleaseID,
|
AlbumID: row.ReleaseID,
|
||||||
Artists: artists,
|
Artists: artists,
|
||||||
}
|
}
|
||||||
tracks[i] = t
|
tracks[i].Item = t
|
||||||
|
tracks[i].Rank = row.Rank
|
||||||
}
|
}
|
||||||
count, err = d.q.CountTopTracks(ctx, repository.CountTopTracksParams{
|
count, err = d.q.CountTopTracks(ctx, repository.CountTopTracksParams{
|
||||||
ListenedAt: t1,
|
ListenedAt: t1,
|
||||||
|
|
@ -141,7 +144,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
||||||
l.Debug().Msgf("Database responded with %d tracks out of a total %d", len(rows), count)
|
l.Debug().Msgf("Database responded with %d tracks out of a total %d", len(rows), count)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &db.PaginatedResponse[*models.Track]{
|
return &db.PaginatedResponse[db.RankedItem[*models.Track]]{
|
||||||
Items: tracks,
|
Items: tracks,
|
||||||
TotalCount: count,
|
TotalCount: count,
|
||||||
ItemsPerPage: int32(opts.Limit),
|
ItemsPerPage: int32(opts.Limit),
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,19 @@ func TestGetTopTracksPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 4)
|
require.Len(t, resp.Items, 4)
|
||||||
assert.Equal(t, int64(4), resp.TotalCount)
|
assert.Equal(t, int64(4), resp.TotalCount)
|
||||||
assert.Equal(t, "Track One", resp.Items[0].Title)
|
assert.Equal(t, "Track One", resp.Items[0].Item.Title)
|
||||||
assert.Equal(t, "Track Two", resp.Items[1].Title)
|
assert.Equal(t, "Track Two", resp.Items[1].Item.Title)
|
||||||
assert.Equal(t, "Track Three", resp.Items[2].Title)
|
assert.Equal(t, "Track Three", resp.Items[2].Item.Title)
|
||||||
assert.Equal(t, "Track Four", resp.Items[3].Title)
|
assert.Equal(t, "Track Four", resp.Items[3].Item.Title)
|
||||||
// ensure artists are included
|
// ensure artists are included
|
||||||
require.Len(t, resp.Items[0].Artists, 1)
|
require.Len(t, resp.Items[0].Item.Artists, 1)
|
||||||
assert.Equal(t, "Artist One", resp.Items[0].Artists[0].Name)
|
assert.Equal(t, "Artist One", resp.Items[0].Item.Artists[0].Name)
|
||||||
|
|
||||||
// Test pagination
|
// Test pagination
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, "Track Two", resp.Items[0].Title)
|
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
// Test page out of range
|
// Test page out of range
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
|
||||||
|
|
@ -60,41 +60,41 @@ func TestGetTopTracksPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Track Four", resp.Items[0].Title)
|
assert.Equal(t, "Track Four", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 2)
|
require.Len(t, resp.Items, 2)
|
||||||
assert.Equal(t, int64(2), resp.TotalCount)
|
assert.Equal(t, int64(2), resp.TotalCount)
|
||||||
assert.Equal(t, "Track Three", resp.Items[0].Title)
|
assert.Equal(t, "Track Three", resp.Items[0].Item.Title)
|
||||||
assert.Equal(t, "Track Four", resp.Items[1].Title)
|
assert.Equal(t, "Track Four", resp.Items[1].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 3)
|
require.Len(t, resp.Items, 3)
|
||||||
assert.Equal(t, int64(3), resp.TotalCount)
|
assert.Equal(t, int64(3), resp.TotalCount)
|
||||||
assert.Equal(t, "Track Two", resp.Items[0].Title)
|
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
|
||||||
assert.Equal(t, "Track Three", resp.Items[1].Title)
|
assert.Equal(t, "Track Three", resp.Items[1].Item.Title)
|
||||||
assert.Equal(t, "Track Four", resp.Items[2].Title)
|
assert.Equal(t, "Track Four", resp.Items[2].Item.Title)
|
||||||
|
|
||||||
// Test filter by artists and releases
|
// Test filter by artists and releases
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, ArtistID: 1})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, ArtistID: 1})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Track One", resp.Items[0].Title)
|
assert.Equal(t, "Track One", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, AlbumID: 2})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, AlbumID: 2})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Track Two", resp.Items[0].Title)
|
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
|
||||||
// when both artistID and albumID are specified, artist id is ignored
|
// when both artistID and albumID are specified, artist id is ignored
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, AlbumID: 2, ArtistID: 1})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, AlbumID: 2, ArtistID: 1})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Track Two", resp.Items[0].Title)
|
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
// Test specify dates
|
// Test specify dates
|
||||||
|
|
||||||
|
|
@ -104,11 +104,11 @@ func TestGetTopTracksPaginated(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Track One", resp.Items[0].Title)
|
assert.Equal(t, "Track One", resp.Items[0].Item.Title)
|
||||||
|
|
||||||
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
|
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
assert.Equal(t, int64(1), resp.TotalCount)
|
assert.Equal(t, int64(1), resp.TotalCount)
|
||||||
assert.Equal(t, "Track Two", resp.Items[0].Title)
|
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ type PaginatedResponse[T any] struct {
|
||||||
CurrentPage int32 `json:"current_page"`
|
CurrentPage int32 `json:"current_page"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RankedItem[T any] struct {
|
||||||
|
Item T `json:"item"`
|
||||||
|
Rank int64 `json:"rank"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExportItem struct {
|
type ExportItem struct {
|
||||||
ListenedAt time.Time
|
ListenedAt time.Time
|
||||||
UserID int32
|
UserID int32
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,14 @@ func (q *Queries) GetReleaseArtists(ctx context.Context, releaseID int32) ([]Get
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopArtistsPaginated = `-- name: GetTopArtistsPaginated :many
|
const getTopArtistsPaginated = `-- name: GetTopArtistsPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.name,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
a.id,
|
a.id,
|
||||||
a.name,
|
a.name,
|
||||||
|
|
@ -279,8 +287,9 @@ JOIN tracks t ON l.track_id = t.id
|
||||||
JOIN artist_tracks at ON at.track_id = t.id
|
JOIN artist_tracks at ON at.track_id = t.id
|
||||||
JOIN artists_with_name a ON a.id = at.artist_id
|
JOIN artists_with_name a ON a.id = at.artist_id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY a.id, a.name, a.musicbrainz_id, a.image, a.image_source, a.name
|
GROUP BY a.id, a.name, a.musicbrainz_id, a.image
|
||||||
ORDER BY listen_count DESC, a.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -297,6 +306,7 @@ type GetTopArtistsPaginatedRow struct {
|
||||||
MusicBrainzID *uuid.UUID
|
MusicBrainzID *uuid.UUID
|
||||||
Image *uuid.UUID
|
Image *uuid.UUID
|
||||||
ListenCount int64
|
ListenCount int64
|
||||||
|
Rank int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsPaginatedParams) ([]GetTopArtistsPaginatedRow, error) {
|
func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsPaginatedParams) ([]GetTopArtistsPaginatedRow, error) {
|
||||||
|
|
@ -319,6 +329,7 @@ func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsP
|
||||||
&i.MusicBrainzID,
|
&i.MusicBrainzID,
|
||||||
&i.Image,
|
&i.Image,
|
||||||
&i.ListenCount,
|
&i.ListenCount,
|
||||||
|
&i.Rank,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,10 @@ func (q *Queries) GetReleasesWithoutImages(ctx context.Context, arg GetReleasesW
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopReleasesFromArtist = `-- name: GetTopReleasesFromArtist :many
|
const getTopReleasesFromArtist = `-- name: GetTopReleasesFromArtist :many
|
||||||
|
SELECT
|
||||||
|
x.id, x.musicbrainz_id, x.image, x.various_artists, x.image_source, x.title, x.listen_count, x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
||||||
COUNT(*) AS listen_count,
|
COUNT(*) AS listen_count,
|
||||||
|
|
@ -331,7 +335,8 @@ JOIN artist_releases ar ON r.id = ar.release_id
|
||||||
WHERE ar.artist_id = $5
|
WHERE ar.artist_id = $5
|
||||||
AND l.listened_at BETWEEN $1 AND $2
|
AND l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
||||||
ORDER BY listen_count DESC, r.id
|
) x
|
||||||
|
ORDER BY listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -352,6 +357,7 @@ type GetTopReleasesFromArtistRow struct {
|
||||||
Title string
|
Title string
|
||||||
ListenCount int64
|
ListenCount int64
|
||||||
Artists []byte
|
Artists []byte
|
||||||
|
Rank int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleasesFromArtistParams) ([]GetTopReleasesFromArtistRow, error) {
|
func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleasesFromArtistParams) ([]GetTopReleasesFromArtistRow, error) {
|
||||||
|
|
@ -378,6 +384,7 @@ func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleas
|
||||||
&i.Title,
|
&i.Title,
|
||||||
&i.ListenCount,
|
&i.ListenCount,
|
||||||
&i.Artists,
|
&i.Artists,
|
||||||
|
&i.Rank,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -390,6 +397,10 @@ func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleas
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopReleasesPaginated = `-- name: GetTopReleasesPaginated :many
|
const getTopReleasesPaginated = `-- name: GetTopReleasesPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id, x.musicbrainz_id, x.image, x.various_artists, x.image_source, x.title, x.listen_count, x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
||||||
COUNT(*) AS listen_count,
|
COUNT(*) AS listen_count,
|
||||||
|
|
@ -399,7 +410,8 @@ JOIN tracks t ON l.track_id = t.id
|
||||||
JOIN releases_with_title r ON t.release_id = r.id
|
JOIN releases_with_title r ON t.release_id = r.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source
|
||||||
ORDER BY listen_count DESC, r.id
|
) x
|
||||||
|
ORDER BY listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -419,6 +431,7 @@ type GetTopReleasesPaginatedRow struct {
|
||||||
Title string
|
Title string
|
||||||
ListenCount int64
|
ListenCount int64
|
||||||
Artists []byte
|
Artists []byte
|
||||||
|
Rank int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetTopReleasesPaginated(ctx context.Context, arg GetTopReleasesPaginatedParams) ([]GetTopReleasesPaginatedRow, error) {
|
func (q *Queries) GetTopReleasesPaginated(ctx context.Context, arg GetTopReleasesPaginatedParams) ([]GetTopReleasesPaginatedRow, error) {
|
||||||
|
|
@ -444,6 +457,7 @@ func (q *Queries) GetTopReleasesPaginated(ctx context.Context, arg GetTopRelease
|
||||||
&i.Title,
|
&i.Title,
|
||||||
&i.ListenCount,
|
&i.ListenCount,
|
||||||
&i.Artists,
|
&i.Artists,
|
||||||
|
&i.Rank,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,16 @@ func (q *Queries) GetAllTracksFromArtist(ctx context.Context, artistID int32) ([
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopTracksByArtistPaginated = `-- name: GetTopTracksByArtistPaginated :many
|
const getTopTracksByArtistPaginated = `-- name: GetTopTracksByArtistPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.title,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.release_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.title,
|
t.title,
|
||||||
|
|
@ -169,7 +179,8 @@ JOIN artist_tracks at ON at.track_id = t.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
AND at.artist_id = $5
|
AND at.artist_id = $5
|
||||||
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
||||||
ORDER BY listen_count DESC, t.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -189,6 +200,7 @@ type GetTopTracksByArtistPaginatedRow struct {
|
||||||
Image *uuid.UUID
|
Image *uuid.UUID
|
||||||
ListenCount int64
|
ListenCount int64
|
||||||
Artists []byte
|
Artists []byte
|
||||||
|
Rank int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopTracksByArtistPaginatedParams) ([]GetTopTracksByArtistPaginatedRow, error) {
|
func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopTracksByArtistPaginatedParams) ([]GetTopTracksByArtistPaginatedRow, error) {
|
||||||
|
|
@ -214,6 +226,7 @@ func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopT
|
||||||
&i.Image,
|
&i.Image,
|
||||||
&i.ListenCount,
|
&i.ListenCount,
|
||||||
&i.Artists,
|
&i.Artists,
|
||||||
|
&i.Rank,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -226,6 +239,16 @@ func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopT
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopTracksInReleasePaginated = `-- name: GetTopTracksInReleasePaginated :many
|
const getTopTracksInReleasePaginated = `-- name: GetTopTracksInReleasePaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.title,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.release_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.title,
|
t.title,
|
||||||
|
|
@ -240,7 +263,8 @@ JOIN releases r ON t.release_id = r.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
AND t.release_id = $5
|
AND t.release_id = $5
|
||||||
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
||||||
ORDER BY listen_count DESC, t.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -260,6 +284,7 @@ type GetTopTracksInReleasePaginatedRow struct {
|
||||||
Image *uuid.UUID
|
Image *uuid.UUID
|
||||||
ListenCount int64
|
ListenCount int64
|
||||||
Artists []byte
|
Artists []byte
|
||||||
|
Rank int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTopTracksInReleasePaginatedParams) ([]GetTopTracksInReleasePaginatedRow, error) {
|
func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTopTracksInReleasePaginatedParams) ([]GetTopTracksInReleasePaginatedRow, error) {
|
||||||
|
|
@ -285,6 +310,7 @@ func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTop
|
||||||
&i.Image,
|
&i.Image,
|
||||||
&i.ListenCount,
|
&i.ListenCount,
|
||||||
&i.Artists,
|
&i.Artists,
|
||||||
|
&i.Rank,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +323,16 @@ func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTop
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopTracksPaginated = `-- name: GetTopTracksPaginated :many
|
const getTopTracksPaginated = `-- name: GetTopTracksPaginated :many
|
||||||
|
SELECT
|
||||||
|
x.id,
|
||||||
|
x.title,
|
||||||
|
x.musicbrainz_id,
|
||||||
|
x.release_id,
|
||||||
|
x.image,
|
||||||
|
x.listen_count,
|
||||||
|
x.artists,
|
||||||
|
RANK() OVER (ORDER BY x.listen_count DESC) AS rank
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.title,
|
t.title,
|
||||||
|
|
@ -310,7 +346,8 @@ JOIN tracks_with_title t ON l.track_id = t.id
|
||||||
JOIN releases r ON t.release_id = r.id
|
JOIN releases r ON t.release_id = r.id
|
||||||
WHERE l.listened_at BETWEEN $1 AND $2
|
WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image
|
||||||
ORDER BY listen_count DESC, t.id
|
) x
|
||||||
|
ORDER BY x.listen_count DESC, x.id
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -329,6 +366,7 @@ type GetTopTracksPaginatedRow struct {
|
||||||
Image *uuid.UUID
|
Image *uuid.UUID
|
||||||
ListenCount int64
|
ListenCount int64
|
||||||
Artists []byte
|
Artists []byte
|
||||||
|
Rank int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPaginatedParams) ([]GetTopTracksPaginatedRow, error) {
|
func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPaginatedParams) ([]GetTopTracksPaginatedRow, error) {
|
||||||
|
|
@ -353,6 +391,7 @@ func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPag
|
||||||
&i.Image,
|
&i.Image,
|
||||||
&i.ListenCount,
|
&i.ListenCount,
|
||||||
&i.Artists,
|
&i.Artists,
|
||||||
|
&i.Rank,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import (
|
||||||
|
|
||||||
type Summary struct {
|
type Summary struct {
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
TopArtists []*models.Artist `json:"top_artists"` // ListenCount and TimeListened are overriden with stats from timeframe
|
TopArtists []db.RankedItem[*models.Artist] `json:"top_artists"` // ListenCount and TimeListened are overriden with stats from timeframe
|
||||||
TopAlbums []*models.Album `json:"top_albums"` // ListenCount and TimeListened are overriden with stats from timeframe
|
TopAlbums []db.RankedItem[*models.Album] `json:"top_albums"` // ListenCount and TimeListened are overriden with stats from timeframe
|
||||||
TopTracks []*models.Track `json:"top_tracks"` // ListenCount and TimeListened are overriden with stats from timeframe
|
TopTracks []db.RankedItem[*models.Track] `json:"top_tracks"` // ListenCount and TimeListened are overriden with stats from timeframe
|
||||||
MinutesListened int `json:"minutes_listened"`
|
MinutesListened int `json:"minutes_listened"`
|
||||||
AvgMinutesPerDay int `json:"avg_minutes_listened_per_day"`
|
AvgMinutesPerDay int `json:"avg_minutes_listened_per_day"`
|
||||||
Plays int `json:"plays"`
|
Plays int `json:"plays"`
|
||||||
|
|
@ -37,16 +37,16 @@ func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe d
|
||||||
summary.TopArtists = topArtists.Items
|
summary.TopArtists = topArtists.Items
|
||||||
// replace ListenCount and TimeListened with stats from timeframe
|
// replace ListenCount and TimeListened with stats from timeframe
|
||||||
for i, artist := range summary.TopArtists {
|
for i, artist := range summary.TopArtists {
|
||||||
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ArtistID: artist.ID, Timeframe: timeframe})
|
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ArtistID: artist.Item.ID, Timeframe: timeframe})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||||
}
|
}
|
||||||
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{ArtistID: artist.ID, Timeframe: timeframe})
|
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{ArtistID: artist.Item.ID, Timeframe: timeframe})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||||
}
|
}
|
||||||
summary.TopArtists[i].TimeListened = timelistened
|
summary.TopArtists[i].Item.TimeListened = timelistened
|
||||||
summary.TopArtists[i].ListenCount = listens
|
summary.TopArtists[i].Item.ListenCount = listens
|
||||||
}
|
}
|
||||||
|
|
||||||
topAlbums, err := store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, Timeframe: timeframe})
|
topAlbums, err := store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, Timeframe: timeframe})
|
||||||
|
|
@ -56,16 +56,16 @@ func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe d
|
||||||
summary.TopAlbums = topAlbums.Items
|
summary.TopAlbums = topAlbums.Items
|
||||||
// replace ListenCount and TimeListened with stats from timeframe
|
// replace ListenCount and TimeListened with stats from timeframe
|
||||||
for i, album := range summary.TopAlbums {
|
for i, album := range summary.TopAlbums {
|
||||||
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{AlbumID: album.ID, Timeframe: timeframe})
|
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{AlbumID: album.Item.ID, Timeframe: timeframe})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||||
}
|
}
|
||||||
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{AlbumID: album.ID, Timeframe: timeframe})
|
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{AlbumID: album.Item.ID, Timeframe: timeframe})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||||
}
|
}
|
||||||
summary.TopAlbums[i].TimeListened = timelistened
|
summary.TopAlbums[i].Item.TimeListened = timelistened
|
||||||
summary.TopAlbums[i].ListenCount = listens
|
summary.TopAlbums[i].Item.ListenCount = listens
|
||||||
}
|
}
|
||||||
|
|
||||||
topTracks, err := store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, Timeframe: timeframe})
|
topTracks, err := store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, Timeframe: timeframe})
|
||||||
|
|
@ -75,16 +75,16 @@ func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe d
|
||||||
summary.TopTracks = topTracks.Items
|
summary.TopTracks = topTracks.Items
|
||||||
// replace ListenCount and TimeListened with stats from timeframe
|
// replace ListenCount and TimeListened with stats from timeframe
|
||||||
for i, track := range summary.TopTracks {
|
for i, track := range summary.TopTracks {
|
||||||
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{TrackID: track.ID, Timeframe: timeframe})
|
timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{TrackID: track.Item.ID, Timeframe: timeframe})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||||
}
|
}
|
||||||
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{TrackID: track.ID, Timeframe: timeframe})
|
listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{TrackID: track.Item.ID, Timeframe: timeframe})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
return nil, fmt.Errorf("GenerateSummary: %w", err)
|
||||||
}
|
}
|
||||||
summary.TopTracks[i].TimeListened = timelistened
|
summary.TopTracks[i].Item.TimeListened = timelistened
|
||||||
summary.TopTracks[i].ListenCount = listens
|
summary.TopTracks[i].Item.ListenCount = listens
|
||||||
}
|
}
|
||||||
|
|
||||||
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
t1, t2 := db.TimeframeToTimeRange(timeframe)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue