mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-03 18:12:07 -07:00
feat: add app-owned YouTube subtitle flow with absPlayer-style parsing (#31)
* fix: harden preload argv parsing for popup windows * fix: align youtube playback with shared overlay startup * fix: unwrap mpv youtube streams for anki media mining * docs: update docs for youtube subtitle and mining flow * refactor: unify cli and runtime wiring for startup and youtube flow * feat: update subtitle sidebar overlay behavior * chore: add shared log-file source for diagnostics * fix(ci): add changelog fragment for immersion changes * fix: address CodeRabbit review feedback * fix: persist canonical title from youtube metadata * style: format stats library tab * fix: address latest review feedback * style: format stats library files * test: stub launcher youtube deps in CI * test: isolate launcher youtube flow deps * test: stub launcher youtube deps in failing case * test: force x11 backend in launcher ci harness * test: address latest review feedback * fix(launcher): preserve user YouTube ytdl raw options * docs(backlog): update task tracking notes * fix(immersion): special-case youtube media paths in runtime and tracking * feat(stats): improve YouTube media metadata and picker key handling * fix(ci): format stats media library hook * fix: address latest CodeRabbit review items * docs: update youtube release notes and docs * feat: auto-load youtube subtitles before manual picker * fix: restore app-owned youtube subtitle flow * docs: update youtube playback docs and config copy * refactor: remove legacy youtube launcher mode plumbing * fix: refine youtube subtitle startup binding * docs: clarify youtube subtitle startup behavior * fix: address PR #31 latest review follow-ups * fix: address PR #31 follow-up review comments * test: harden youtube picker test harness * udpate backlog * fix: add timeout to youtube metadata probe * docs: refresh youtube and stats docs * update backlog * update backlog * chore: release v0.9.0
This commit is contained in:
57
stats/src/hooks/useMediaLibrary.test.ts
Normal file
57
stats/src/hooks/useMediaLibrary.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { MediaLibraryItem } from '../types/stats';
|
||||
import { shouldRefreshMediaLibraryRows } from './useMediaLibrary';
|
||||
|
||||
const baseItem: MediaLibraryItem = {
|
||||
videoId: 1,
|
||||
canonicalTitle: 'watch?v=abc123',
|
||||
totalSessions: 1,
|
||||
totalActiveMs: 60_000,
|
||||
totalCards: 0,
|
||||
totalTokensSeen: 10,
|
||||
lastWatchedMs: 1_000,
|
||||
hasCoverArt: 0,
|
||||
youtubeVideoId: 'abc123',
|
||||
videoUrl: 'https://www.youtube.com/watch?v=abc123',
|
||||
videoTitle: null,
|
||||
videoThumbnailUrl: 'https://i.ytimg.com/vi/abc123/hqdefault.jpg',
|
||||
channelId: null,
|
||||
channelName: null,
|
||||
channelUrl: null,
|
||||
channelThumbnailUrl: null,
|
||||
uploaderId: null,
|
||||
uploaderUrl: null,
|
||||
description: null,
|
||||
};
|
||||
|
||||
test('shouldRefreshMediaLibraryRows requests a follow-up fetch for incomplete youtube metadata', () => {
|
||||
assert.equal(shouldRefreshMediaLibraryRows([baseItem]), true);
|
||||
});
|
||||
|
||||
test('shouldRefreshMediaLibraryRows skips follow-up fetch when youtube metadata is complete', () => {
|
||||
assert.equal(
|
||||
shouldRefreshMediaLibraryRows([
|
||||
{
|
||||
...baseItem,
|
||||
videoTitle: 'Video Name',
|
||||
channelName: 'Creator Name',
|
||||
channelThumbnailUrl: 'https://yt3.googleusercontent.com/channel-avatar=s88',
|
||||
},
|
||||
]),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldRefreshMediaLibraryRows ignores non-youtube rows', () => {
|
||||
assert.equal(
|
||||
shouldRefreshMediaLibraryRows([
|
||||
{
|
||||
...baseItem,
|
||||
youtubeVideoId: null,
|
||||
videoUrl: null,
|
||||
},
|
||||
]),
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -2,6 +2,18 @@ import { useState, useEffect } from 'react';
|
||||
import { getStatsClient } from './useStatsApi';
|
||||
import type { MediaLibraryItem } from '../types/stats';
|
||||
|
||||
const MEDIA_LIBRARY_REFRESH_DELAY_MS = 1_500;
|
||||
const MEDIA_LIBRARY_MAX_RETRIES = 3;
|
||||
|
||||
export function shouldRefreshMediaLibraryRows(rows: MediaLibraryItem[]): boolean {
|
||||
return rows.some((row) => {
|
||||
if (!row.youtubeVideoId) {
|
||||
return false;
|
||||
}
|
||||
return !row.videoTitle?.trim() || !row.channelName?.trim() || !row.channelThumbnailUrl?.trim();
|
||||
});
|
||||
}
|
||||
|
||||
export function useMediaLibrary() {
|
||||
const [media, setMedia] = useState<MediaLibraryItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -9,24 +21,43 @@ export function useMediaLibrary() {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
getStatsClient()
|
||||
.getMediaLibrary()
|
||||
.then((rows) => {
|
||||
if (cancelled) return;
|
||||
setMedia(rows);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (cancelled) return;
|
||||
setError(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return;
|
||||
setLoading(false);
|
||||
});
|
||||
let retryCount = 0;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const load = (isInitial = false) => {
|
||||
if (isInitial) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
}
|
||||
getStatsClient()
|
||||
.getMediaLibrary()
|
||||
.then((rows) => {
|
||||
if (cancelled) return;
|
||||
setMedia(rows);
|
||||
if (shouldRefreshMediaLibraryRows(rows) && retryCount < MEDIA_LIBRARY_MAX_RETRIES) {
|
||||
retryCount += 1;
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = null;
|
||||
load(false);
|
||||
}, MEDIA_LIBRARY_REFRESH_DELAY_MS);
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (cancelled) return;
|
||||
setError(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled || !isInitial) return;
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
load(true);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retryTimer) {
|
||||
clearTimeout(retryTimer);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user