1a7515e6ea
Per-source play outcomes so the operator can see whether each recommendation
surface is landing and tune the now-operator-tunable taste weights.
Server:
- query RecommendationSourceMetricsForUser: groups the user's play_events by
source (system-playlist surface), reporting plays / skips / avg completion
over a window; NULL-source (library/radio) plays excluded.
- GET /api/me/recommendation-metrics?days=30 (default 30, capped 365) →
{window_days, sources:[{source, plays, skips, skip_rate, avg_completion}]}.
- handler test: 401 unauth; per-source aggregation + NULL-source exclusion +
skip_rate / avg_completion math.
Web:
- lib/api/metrics.ts: query + friendly source labels.
- settings page gains a "Recommendation metrics" card (table of surface / plays
/ skip rate / avg completion), with loading/error/empty states.
- settings tests mock the new query (manual subscribe-store, hoisting-safe).
Note: You-might-like plays aren't source-tagged (it's a Home row, not a system
playlist), so this covers For-You / Discover / the mixes. Tagging YML plays
would be a client follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
47 lines
1.2 KiB
TypeScript
47 lines
1.2 KiB
TypeScript
import { createQuery } from '@tanstack/svelte-query';
|
|
import { api } from './client';
|
|
|
|
// Mirrors internal/api/me_recommendation_metrics.go.
|
|
export type RecommendationMetric = {
|
|
source: string;
|
|
plays: number;
|
|
skips: number;
|
|
skip_rate: number;
|
|
avg_completion: number;
|
|
};
|
|
|
|
export type RecommendationMetrics = {
|
|
window_days: number;
|
|
sources: RecommendationMetric[];
|
|
};
|
|
|
|
export function getRecommendationMetrics(): Promise<RecommendationMetrics> {
|
|
return api.get<RecommendationMetrics>('/api/me/recommendation-metrics');
|
|
}
|
|
|
|
export const REC_METRICS_QUERY_KEY = ['settings', 'recommendation-metrics'] as const;
|
|
|
|
export function createRecommendationMetricsQuery() {
|
|
return createQuery({
|
|
queryKey: REC_METRICS_QUERY_KEY,
|
|
queryFn: getRecommendationMetrics,
|
|
staleTime: 60_000
|
|
});
|
|
}
|
|
|
|
// Friendly labels for the system-playlist source keys (play_events.source).
|
|
const SOURCE_LABELS: Record<string, string> = {
|
|
for_you: 'For You',
|
|
discover: 'Discover',
|
|
deep_cuts: 'Deep cuts',
|
|
rediscover: 'Rediscover',
|
|
new_for_you: 'New for you',
|
|
on_this_day: 'On this day',
|
|
first_listens: 'First listens',
|
|
songs_like_artist: 'Songs like…'
|
|
};
|
|
|
|
export function sourceLabel(source: string): string {
|
|
return SOURCE_LABELS[source] ?? source;
|
|
}
|