mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-23 00:11:28 -07:00
fix: address latest review feedback
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useMediaLibrary } from '../../hooks/useMediaLibrary';
|
||||
import { formatDuration, formatNumber } from '../../lib/formatters';
|
||||
import { groupMediaLibraryItems } from '../../lib/media-library-grouping';
|
||||
import { groupMediaLibraryItems, summarizeMediaLibraryGroups } from '../../lib/media-library-grouping';
|
||||
import { CoverImage } from './CoverImage';
|
||||
import { MediaCard } from './MediaCard';
|
||||
import { MediaDetailView } from './MediaDetailView';
|
||||
@@ -30,8 +30,7 @@ export function LibraryTab({ onNavigateToSession }: LibraryTabProps) {
|
||||
});
|
||||
}, [media, search]);
|
||||
const grouped = useMemo(() => groupMediaLibraryItems(filtered), [filtered]);
|
||||
|
||||
const totalMs = media.reduce((sum, m) => sum + m.totalActiveMs, 0);
|
||||
const summary = useMemo(() => summarizeMediaLibraryGroups(grouped), [grouped]);
|
||||
|
||||
if (selectedVideoId !== null) {
|
||||
return <MediaDetailView videoId={selectedVideoId} onBack={() => setSelectedVideoId(null)} />;
|
||||
@@ -51,8 +50,8 @@ export function LibraryTab({ onNavigateToSession }: LibraryTabProps) {
|
||||
className="flex-1 bg-ctp-surface0 border border-ctp-surface1 rounded-lg px-3 py-2 text-sm text-ctp-text placeholder:text-ctp-overlay2 focus:outline-none focus:border-ctp-blue"
|
||||
/>
|
||||
<div className="text-xs text-ctp-overlay2 shrink-0">
|
||||
{grouped.length} channel{grouped.length !== 1 ? 's' : ''} · {filtered.length} video
|
||||
{filtered.length !== 1 ? 's' : ''} · {formatDuration(totalMs)}
|
||||
{grouped.length} group{grouped.length !== 1 ? 's' : ''} · {summary.totalVideos} video
|
||||
{summary.totalVideos !== 1 ? 's' : ''} · {formatDuration(summary.totalMs)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CoverImage } from './CoverImage';
|
||||
import { formatDuration, formatNumber } from '../../lib/formatters';
|
||||
import { resolveMediaArtworkUrl } from '../../lib/media-library-grouping';
|
||||
import { CoverImage } from './CoverImage';
|
||||
import type { MediaLibraryItem } from '../../types/stats';
|
||||
|
||||
interface MediaCardProps {
|
||||
@@ -18,7 +17,6 @@ export function MediaCard({ item, onClick }: MediaCardProps) {
|
||||
<CoverImage
|
||||
videoId={item.videoId}
|
||||
title={item.canonicalTitle}
|
||||
src={resolveMediaArtworkUrl(item, 'video')}
|
||||
className="w-full aspect-[3/4] rounded-t-lg"
|
||||
/>
|
||||
<div className="p-3">
|
||||
|
||||
@@ -2,8 +2,13 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import type { MediaLibraryItem } from '../types/stats';
|
||||
import { groupMediaLibraryItems, resolveMediaArtworkUrl } from './media-library-grouping';
|
||||
import {
|
||||
groupMediaLibraryItems,
|
||||
resolveMediaArtworkUrl,
|
||||
summarizeMediaLibraryGroups,
|
||||
} from './media-library-grouping';
|
||||
import { CoverImage } from '../components/library/CoverImage';
|
||||
import { MediaCard } from '../components/library/MediaCard';
|
||||
|
||||
const youtubeEpisodeA: MediaLibraryItem = {
|
||||
videoId: 1,
|
||||
@@ -85,6 +90,16 @@ test('resolveMediaArtworkUrl prefers youtube thumbnails for video and channel im
|
||||
assert.equal(resolveMediaArtworkUrl(localVideo, 'channel'), null);
|
||||
});
|
||||
|
||||
test('summarizeMediaLibraryGroups stays aligned with rendered group buckets', () => {
|
||||
const groups = groupMediaLibraryItems([youtubeEpisodeA, localVideo, youtubeEpisodeB]);
|
||||
const summary = summarizeMediaLibraryGroups(groups);
|
||||
|
||||
assert.deepEqual(summary, {
|
||||
totalMs: 29_000,
|
||||
totalVideos: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('CoverImage renders explicit remote artwork when src is provided', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<CoverImage
|
||||
@@ -97,3 +112,12 @@ test('CoverImage renders explicit remote artwork when src is provided', () => {
|
||||
|
||||
assert.match(markup, /src="https:\/\/i\.ytimg\.com\/vi\/yt-1\/hqdefault\.jpg"/);
|
||||
});
|
||||
|
||||
test('MediaCard uses the proxied cover endpoint instead of metadata artwork urls', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<MediaCard item={youtubeEpisodeA} onClick={() => {}} />,
|
||||
);
|
||||
|
||||
assert.match(markup, /src="http:\/\/127\.0\.0\.1:6969\/api\/stats\/media\/1\/cover"/);
|
||||
assert.doesNotMatch(markup, /https:\/\/i\.ytimg\.com\/vi\/yt-1\/hqdefault\.jpg/);
|
||||
});
|
||||
|
||||
@@ -27,21 +27,35 @@ export function resolveMediaCoverApiUrl(videoId: number): string {
|
||||
return `${BASE_URL}/api/stats/media/${videoId}/cover`;
|
||||
}
|
||||
|
||||
export function summarizeMediaLibraryGroups(groups: MediaLibraryGroup[]): {
|
||||
totalMs: number;
|
||||
totalVideos: number;
|
||||
} {
|
||||
return groups.reduce(
|
||||
(summary, group) => ({
|
||||
totalMs: summary.totalMs + group.totalActiveMs,
|
||||
totalVideos: summary.totalVideos + group.items.length,
|
||||
}),
|
||||
{ totalMs: 0, totalVideos: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
export function groupMediaLibraryItems(items: MediaLibraryItem[]): MediaLibraryGroup[] {
|
||||
const groups = new Map<string, MediaLibraryGroup>();
|
||||
|
||||
for (const item of items) {
|
||||
const key = item.channelId?.trim() || `video:${item.videoId}`;
|
||||
const channelId = item.channelId?.trim() || null;
|
||||
const channelName = item.channelName?.trim() || null;
|
||||
const uploaderId = item.uploaderId?.trim() || null;
|
||||
const videoTitle = item.videoTitle?.trim() || null;
|
||||
const key = channelId || `video:${item.videoId}`;
|
||||
const title =
|
||||
item.channelName?.trim() ||
|
||||
item.uploaderId?.trim() ||
|
||||
item.videoTitle?.trim() ||
|
||||
item.canonicalTitle;
|
||||
channelName || uploaderId || videoTitle || item.canonicalTitle;
|
||||
const subtitle =
|
||||
item.channelId?.trim() != null && item.channelId?.trim() !== ''
|
||||
? `${item.channelId}`
|
||||
: item.videoTitle?.trim() && item.videoTitle !== item.canonicalTitle
|
||||
? item.videoTitle
|
||||
channelId
|
||||
? channelId
|
||||
: videoTitle && videoTitle !== item.canonicalTitle
|
||||
? videoTitle
|
||||
: null;
|
||||
const existing = groups.get(key);
|
||||
if (existing) {
|
||||
|
||||
Reference in New Issue
Block a user