feat(stats): improve YouTube media metadata and picker key handling

This commit is contained in:
2026-03-23 00:36:23 -07:00
parent 2e43d95396
commit e9fc6bf8ec
13 changed files with 336 additions and 29 deletions

View File

@@ -8,6 +8,10 @@ interface MediaCardProps {
}
export function MediaCard({ item, onClick }: MediaCardProps) {
const primaryTitle = item.videoTitle?.trim() || item.canonicalTitle;
const secondaryTitle =
item.videoTitle?.trim() && item.videoTitle !== item.canonicalTitle ? item.canonicalTitle : null;
return (
<button
type="button"
@@ -20,9 +24,9 @@ export function MediaCard({ item, onClick }: MediaCardProps) {
className="w-full aspect-[3/4] rounded-t-lg"
/>
<div className="p-3">
<div className="text-sm font-medium text-ctp-text truncate">{item.canonicalTitle}</div>
{item.videoTitle && item.videoTitle !== item.canonicalTitle ? (
<div className="text-xs text-ctp-subtext1 truncate mt-1">{item.videoTitle}</div>
<div className="text-sm font-medium text-ctp-text truncate">{primaryTitle}</div>
{secondaryTitle ? (
<div className="text-xs text-ctp-subtext1 truncate mt-1">{secondaryTitle}</div>
) : null}
<div className="text-xs text-ctp-overlay2 mt-1">
{formatDuration(item.totalActiveMs)} · {formatNumber(item.totalCards)} cards

View File

@@ -0,0 +1,41 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { getRelatedCollectionLabel } from './MediaDetailView';
test('getRelatedCollectionLabel returns View Channel for youtube-backed media', () => {
assert.equal(
getRelatedCollectionLabel({
animeId: 1,
canonicalTitle: 'Video',
totalSessions: 1,
totalActiveMs: 1,
totalCards: 0,
totalTokensSeen: 0,
totalLinesSeen: 0,
totalLookupCount: 0,
totalLookupHits: 0,
totalYomitanLookupCount: 0,
channelName: 'Creator',
}),
'View Channel',
);
});
test('getRelatedCollectionLabel returns View Anime for non-youtube media', () => {
assert.equal(
getRelatedCollectionLabel({
animeId: 1,
canonicalTitle: 'Episode 5',
totalSessions: 1,
totalActiveMs: 1,
totalCards: 0,
totalTokensSeen: 0,
totalLinesSeen: 0,
totalLookupCount: 0,
totalLookupHits: 0,
totalYomitanLookupCount: 0,
channelName: null,
}),
'View Anime',
);
});

View File

@@ -5,7 +5,14 @@ import { confirmSessionDelete } from '../../lib/delete-confirm';
import { getSessionDisplayWordCount } from '../../lib/session-word-count';
import { MediaHeader } from './MediaHeader';
import { MediaSessionList } from './MediaSessionList';
import type { SessionSummary } from '../../types/stats';
import type { MediaDetailData, SessionSummary } from '../../types/stats';
export function getRelatedCollectionLabel(detail: MediaDetailData['detail']): string {
if (detail?.channelName?.trim()) {
return 'View Channel';
}
return 'View Anime';
}
interface MediaDetailViewProps {
videoId: number;
@@ -53,6 +60,7 @@ export function MediaDetailView({
totalLookupHits: sessions.reduce((sum, session) => sum + session.lookupHits, 0),
totalYomitanLookupCount: sessions.reduce((sum, session) => sum + session.yomitanLookupCount, 0),
};
const relatedCollectionLabel = getRelatedCollectionLabel(detail);
const handleDeleteSession = async (session: SessionSummary) => {
if (!confirmSessionDelete()) return;
@@ -87,7 +95,7 @@ export function MediaDetailView({
onClick={() => onNavigateToAnime(animeId)}
className="text-sm text-ctp-blue hover:text-ctp-sapphire transition-colors"
>
View Anime &rarr;
{relatedCollectionLabel} &rarr;
</button>
) : null}
</div>