fix: use sql rank (#148)

This commit is contained in:
Gabe Farrell 2026-01-15 21:08:30 -05:00 committed by GitHub
parent aa7fddd518
commit d2d6924e05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 386 additions and 270 deletions

View file

@ -48,32 +48,32 @@ async function getLastListens(
async function getTopTracks(
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}`;
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
else if (args.album_id) url += `&album_id=${args.album_id}`;
const r = await fetch(url);
return handleJson<PaginatedResponse<Track>>(r);
return handleJson<PaginatedResponse<Ranked<Track>>>(r);
}
async function getTopAlbums(
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}`;
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
const r = await fetch(url);
return handleJson<PaginatedResponse<Album>>(r);
return handleJson<PaginatedResponse<Ranked<Album>>>(r);
}
async function getTopArtists(
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 r = await fetch(url);
return handleJson<PaginatedResponse<Artist>>(r);
return handleJson<PaginatedResponse<Ranked<Artist>>>(r);
}
async function getActivity(
@ -407,6 +407,10 @@ type PaginatedResponse<T> = {
current_page: number;
items_per_page: number;
};
type Ranked<T> = {
item: T;
rank: number;
};
type ListenActivityItem = {
start_time: Date;
listens: number;
@ -480,6 +484,7 @@ export type {
Listen,
SearchResponse,
PaginatedResponse,
Ranked,
ListenActivityItem,
InterestBucket,
User,

View file

@ -6,11 +6,12 @@ import {
type Artist,
type Track,
type PaginatedResponse,
type Ranked,
} from "api/api";
type Item = Album | Track | Artist;
interface Props<T extends Item> {
interface Props<T extends Ranked<Item>> {
data: PaginatedResponse<T>;
separators?: ConstrainBoolean;
ranked?: boolean;
@ -18,33 +19,17 @@ interface Props<T extends Item> {
className?: string;
}
export default function TopItemList<T extends Item>({
export default function TopItemList<T extends Ranked<Item>>({
data,
separators,
type,
className,
ranked,
}: 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 (
<div className={`flex flex-col gap-1 ${className} min-w-[200px]`}>
{data.items.map((item, index) => {
const key = `${type}-${item.id}`;
const key = `${type}-${item.item.id}`;
return (
<div
key={key}
@ -57,10 +42,10 @@ export default function TopItemList<T extends Item>({
>
<ItemCard
ranked={ranked}
rank={calculateRank(data.items, page, index)}
item={item}
rank={item.rank}
item={item.item}
type={type}
key={type + item.id}
key={type + item.item.id}
/>
</div>
);

View file

@ -1,7 +1,7 @@
import TopItemList from "~/components/TopItemList";
import ChartLayout from "./ChartLayout";
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) {
const url = new URL(request.url);
@ -21,7 +21,7 @@ export async function clientLoader({ request }: LoaderFunctionArgs) {
export default function AlbumChart() {
const { top_albums: initialData } = useLoaderData<{
top_albums: PaginatedResponse<Album>;
top_albums: PaginatedResponse<Ranked<Album>>;
}>();
return (

View file

@ -1,7 +1,7 @@
import TopItemList from "~/components/TopItemList";
import ChartLayout from "./ChartLayout";
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) {
const url = new URL(request.url);
@ -21,7 +21,7 @@ export async function clientLoader({ request }: LoaderFunctionArgs) {
export default function Artist() {
const { top_artists: initialData } = useLoaderData<{
top_artists: PaginatedResponse<Album>;
top_artists: PaginatedResponse<Ranked<Album>>;
}>();
return (

View file

@ -1,7 +1,7 @@
import TopItemList from "~/components/TopItemList";
import ChartLayout from "./ChartLayout";
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) {
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 });
}
const top_tracks: PaginatedResponse<Album> = await res.json();
const top_tracks: PaginatedResponse<Track> = await res.json();
return { top_tracks };
}
export default function TrackChart() {
const { top_tracks: initialData } = useLoaderData<{
top_tracks: PaginatedResponse<Album>;
top_tracks: PaginatedResponse<Ranked<Track>>;
}>();
return (