mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-13 20:12:54 -07:00
5feed360ca
* 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
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import type { AnilistMediaGuess } from '../../core/services/anilist/anilist-updater';
|
|
import { isYoutubeMediaPath } from './youtube-playback';
|
|
|
|
export type AnilistMediaGuessRuntimeState = {
|
|
mediaKey: string | null;
|
|
mediaDurationSec: number | null;
|
|
mediaGuess: AnilistMediaGuess | null;
|
|
mediaGuessPromise: Promise<AnilistMediaGuess | null> | null;
|
|
lastDurationProbeAtMs: number;
|
|
};
|
|
|
|
type GuessAnilistMediaInfo = (
|
|
mediaPath: string | null,
|
|
mediaTitle: string | null,
|
|
) => Promise<AnilistMediaGuess | null>;
|
|
|
|
export function createMaybeProbeAnilistDurationHandler(deps: {
|
|
getState: () => AnilistMediaGuessRuntimeState;
|
|
setState: (state: AnilistMediaGuessRuntimeState) => void;
|
|
durationRetryIntervalMs: number;
|
|
now: () => number;
|
|
requestMpvDuration: () => Promise<unknown>;
|
|
logWarn: (message: string, error: unknown) => void;
|
|
}) {
|
|
return async (mediaKey: string): Promise<number | null> => {
|
|
const state = deps.getState();
|
|
if (state.mediaKey !== mediaKey) {
|
|
return null;
|
|
}
|
|
if (isYoutubeMediaPath(mediaKey)) {
|
|
return null;
|
|
}
|
|
if (typeof state.mediaDurationSec === 'number' && state.mediaDurationSec > 0) {
|
|
return state.mediaDurationSec;
|
|
}
|
|
const now = deps.now();
|
|
if (now - state.lastDurationProbeAtMs < deps.durationRetryIntervalMs) {
|
|
return null;
|
|
}
|
|
|
|
deps.setState({
|
|
...state,
|
|
lastDurationProbeAtMs: now,
|
|
});
|
|
|
|
try {
|
|
const durationCandidate = await deps.requestMpvDuration();
|
|
const duration =
|
|
typeof durationCandidate === 'number' && Number.isFinite(durationCandidate)
|
|
? durationCandidate
|
|
: null;
|
|
const latestState = deps.getState();
|
|
if (duration && duration > 0 && latestState.mediaKey === mediaKey) {
|
|
deps.setState({
|
|
...latestState,
|
|
mediaDurationSec: duration,
|
|
});
|
|
return duration;
|
|
}
|
|
} catch (error) {
|
|
deps.logWarn('AniList duration probe failed:', error);
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
export function createEnsureAnilistMediaGuessHandler(deps: {
|
|
getState: () => AnilistMediaGuessRuntimeState;
|
|
setState: (state: AnilistMediaGuessRuntimeState) => void;
|
|
resolveMediaPathForJimaku: (currentMediaPath: string | null) => string | null;
|
|
getCurrentMediaPath: () => string | null;
|
|
getCurrentMediaTitle: () => string | null;
|
|
guessAnilistMediaInfo: GuessAnilistMediaInfo;
|
|
}) {
|
|
return async (mediaKey: string): Promise<AnilistMediaGuess | null> => {
|
|
const state = deps.getState();
|
|
if (state.mediaKey !== mediaKey) {
|
|
return null;
|
|
}
|
|
if (isYoutubeMediaPath(mediaKey)) {
|
|
return null;
|
|
}
|
|
if (state.mediaGuess) {
|
|
return state.mediaGuess;
|
|
}
|
|
if (state.mediaGuessPromise) {
|
|
return state.mediaGuessPromise;
|
|
}
|
|
|
|
const mediaPathForGuess = deps.resolveMediaPathForJimaku(deps.getCurrentMediaPath());
|
|
const promise = deps
|
|
.guessAnilistMediaInfo(mediaPathForGuess, deps.getCurrentMediaTitle())
|
|
.then((guess) => {
|
|
const latestState = deps.getState();
|
|
if (latestState.mediaKey === mediaKey) {
|
|
deps.setState({
|
|
...latestState,
|
|
mediaGuess: guess,
|
|
});
|
|
}
|
|
return guess;
|
|
})
|
|
.finally(() => {
|
|
const latestState = deps.getState();
|
|
if (latestState.mediaKey === mediaKey) {
|
|
deps.setState({
|
|
...latestState,
|
|
mediaGuessPromise: null,
|
|
});
|
|
}
|
|
});
|
|
|
|
deps.setState({
|
|
...state,
|
|
mediaGuessPromise: promise,
|
|
});
|
|
return promise;
|
|
};
|
|
}
|