mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
Merge pull request #7 from ksyasuda/feature/add-anilist-tracking
Add AniList Tracking
This commit is contained in:
502
src/main.ts
502
src/main.ts
@@ -132,6 +132,13 @@ import {
|
||||
triggerFieldGroupingService,
|
||||
updateLastCardFromClipboardService,
|
||||
} from "./core/services";
|
||||
import {
|
||||
guessAnilistMediaInfo,
|
||||
type AnilistMediaGuess,
|
||||
updateAnilistPostWatchProgress,
|
||||
} from "./core/services/anilist/anilist-updater";
|
||||
import { createAnilistTokenStore } from "./core/services/anilist/anilist-token-store";
|
||||
import { createAnilistUpdateQueue } from "./core/services/anilist/anilist-update-queue";
|
||||
import { applyRuntimeOptionResultRuntimeService } from "./core/services/runtime-options-ipc-service";
|
||||
import {
|
||||
createAppReadyRuntimeRunner,
|
||||
@@ -169,9 +176,14 @@ import {
|
||||
import { createMediaRuntimeService } from "./main/media-runtime";
|
||||
import { createOverlayVisibilityRuntimeService } from "./main/overlay-visibility-runtime";
|
||||
import {
|
||||
type AppState,
|
||||
applyStartupState,
|
||||
createAppState,
|
||||
} from "./main/state";
|
||||
import {
|
||||
isAllowedAnilistExternalUrl,
|
||||
isAllowedAnilistSetupNavigationUrl,
|
||||
} from "./main/anilist-url-guard";
|
||||
import { createStartupBootstrapRuntimeDeps } from "./main/startup";
|
||||
import { createAppLifecycleRuntimeRunner } from "./main/startup-lifecycle";
|
||||
import {
|
||||
@@ -192,6 +204,27 @@ const DEFAULT_MPV_LOG_FILE = path.join(
|
||||
"SubMiner",
|
||||
"mp.log",
|
||||
);
|
||||
const ANILIST_SETUP_CLIENT_ID_URL = "https://anilist.co/api/v2/oauth/authorize";
|
||||
const ANILIST_SETUP_RESPONSE_TYPE = "token";
|
||||
const ANILIST_DEFAULT_CLIENT_ID = "36084";
|
||||
const ANILIST_REDIRECT_URI = "https://anilist.subminer.moe/";
|
||||
const ANILIST_DEVELOPER_SETTINGS_URL = "https://anilist.co/settings/developer";
|
||||
const ANILIST_UPDATE_MIN_WATCH_RATIO = 0.85;
|
||||
const ANILIST_UPDATE_MIN_WATCH_SECONDS = 10 * 60;
|
||||
const ANILIST_DURATION_RETRY_INTERVAL_MS = 15_000;
|
||||
const ANILIST_MAX_ATTEMPTED_UPDATE_KEYS = 1000;
|
||||
const ANILIST_TOKEN_STORE_FILE = "anilist-token-store.json";
|
||||
const ANILIST_RETRY_QUEUE_FILE = "anilist-retry-queue.json";
|
||||
|
||||
let anilistCurrentMediaKey: string | null = null;
|
||||
let anilistCurrentMediaDurationSec: number | null = null;
|
||||
let anilistCurrentMediaGuess: AnilistMediaGuess | null = null;
|
||||
let anilistCurrentMediaGuessPromise: Promise<AnilistMediaGuess | null> | null = null;
|
||||
let anilistLastDurationProbeAtMs = 0;
|
||||
let anilistUpdateInFlight = false;
|
||||
const anilistAttemptedUpdateKeys = new Set<string>();
|
||||
let anilistCachedAccessToken: string | null = null;
|
||||
|
||||
function resolveConfigDir(): string {
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
|
||||
const baseDirs = Array.from(
|
||||
@@ -230,6 +263,22 @@ const CONFIG_DIR = resolveConfigDir();
|
||||
const USER_DATA_PATH = CONFIG_DIR;
|
||||
const DEFAULT_MPV_LOG_PATH = process.env.SUBMINER_MPV_LOG?.trim() || DEFAULT_MPV_LOG_FILE;
|
||||
const configService = new ConfigService(CONFIG_DIR);
|
||||
const anilistTokenStore = createAnilistTokenStore(
|
||||
path.join(USER_DATA_PATH, ANILIST_TOKEN_STORE_FILE),
|
||||
{
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
},
|
||||
);
|
||||
const anilistUpdateQueue = createAnilistUpdateQueue(
|
||||
path.join(USER_DATA_PATH, ANILIST_RETRY_QUEUE_FILE),
|
||||
{
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
},
|
||||
);
|
||||
const isDev =
|
||||
process.argv.includes("--dev") || process.argv.includes("--debug");
|
||||
const texthookerService = new TexthookerService();
|
||||
@@ -572,6 +621,436 @@ async function jimakuFetchJson<T>(
|
||||
});
|
||||
}
|
||||
|
||||
function setAnilistClientSecretState(partial: Partial<AppState["anilistClientSecretState"]>): void {
|
||||
appState.anilistClientSecretState = {
|
||||
...appState.anilistClientSecretState,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
function refreshAnilistRetryQueueState(): void {
|
||||
appState.anilistRetryQueueState = {
|
||||
...appState.anilistRetryQueueState,
|
||||
...anilistUpdateQueue.getSnapshot(),
|
||||
};
|
||||
}
|
||||
|
||||
function isAnilistTrackingEnabled(resolved: ResolvedConfig): boolean {
|
||||
return resolved.anilist.enabled;
|
||||
}
|
||||
|
||||
function buildAnilistSetupUrl(): string {
|
||||
const authorizeUrl = new URL(ANILIST_SETUP_CLIENT_ID_URL);
|
||||
authorizeUrl.searchParams.set("client_id", ANILIST_DEFAULT_CLIENT_ID);
|
||||
authorizeUrl.searchParams.set("response_type", ANILIST_SETUP_RESPONSE_TYPE);
|
||||
authorizeUrl.searchParams.set("redirect_uri", ANILIST_REDIRECT_URI);
|
||||
return authorizeUrl.toString();
|
||||
}
|
||||
|
||||
function openAnilistSetupInBrowser(): void {
|
||||
const authorizeUrl = buildAnilistSetupUrl();
|
||||
void shell.openExternal(authorizeUrl).catch((error) => {
|
||||
logger.error("Failed to open AniList authorize URL in browser", error);
|
||||
});
|
||||
}
|
||||
|
||||
function loadAnilistSetupFallback(setupWindow: BrowserWindow, reason: string): void {
|
||||
const authorizeUrl = buildAnilistSetupUrl();
|
||||
const fallbackHtml = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>AniList Setup</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 24px; background: #0b1020; color: #e5e7eb; }
|
||||
h1 { margin: 0 0 12px; font-size: 22px; }
|
||||
p { margin: 10px 0; line-height: 1.45; color: #cbd5e1; }
|
||||
a { color: #93c5fd; word-break: break-all; }
|
||||
.box { background: #111827; border: 1px solid #1f2937; border-radius: 10px; padding: 16px; }
|
||||
.reason { color: #fca5a5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AniList Setup</h1>
|
||||
<div class="box">
|
||||
<p class="reason">Embedded AniList page did not render: ${reason}</p>
|
||||
<p>We attempted to open the authorize URL in your default browser automatically.</p>
|
||||
<p>Use one of these links to continue setup:</p>
|
||||
<p><a href="${authorizeUrl}">${authorizeUrl}</a></p>
|
||||
<p><a href="${ANILIST_DEVELOPER_SETTINGS_URL}">${ANILIST_DEVELOPER_SETTINGS_URL}</a></p>
|
||||
<p>After login/authorization, copy the token into <code>anilist.accessToken</code>.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
void setupWindow.loadURL(
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent(fallbackHtml)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function openAnilistSetupWindow(): void {
|
||||
if (appState.anilistSetupWindow) {
|
||||
appState.anilistSetupWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const setupWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 760,
|
||||
title: "Anilist Setup",
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
setupWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (!isAllowedAnilistExternalUrl(url)) {
|
||||
logger.warn("Blocked unsafe AniList setup external URL", { url });
|
||||
return { action: "deny" };
|
||||
}
|
||||
void shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
setupWindow.webContents.on("will-navigate", (event, url) => {
|
||||
if (isAllowedAnilistSetupNavigationUrl(url)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
logger.warn("Blocked unsafe AniList setup navigation URL", { url });
|
||||
});
|
||||
|
||||
setupWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription, validatedURL) => {
|
||||
logger.error("AniList setup window failed to load", {
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
});
|
||||
openAnilistSetupInBrowser();
|
||||
if (!setupWindow.isDestroyed()) {
|
||||
loadAnilistSetupFallback(
|
||||
setupWindow,
|
||||
`${errorDescription} (${errorCode})`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setupWindow.webContents.on("did-finish-load", () => {
|
||||
const loadedUrl = setupWindow.webContents.getURL();
|
||||
if (!loadedUrl || loadedUrl === "about:blank") {
|
||||
logger.warn("AniList setup loaded a blank page; using fallback");
|
||||
openAnilistSetupInBrowser();
|
||||
if (!setupWindow.isDestroyed()) {
|
||||
loadAnilistSetupFallback(setupWindow, "blank page");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void setupWindow.loadURL(buildAnilistSetupUrl()).catch((error) => {
|
||||
logger.error("AniList setup loadURL rejected", error);
|
||||
openAnilistSetupInBrowser();
|
||||
if (!setupWindow.isDestroyed()) {
|
||||
loadAnilistSetupFallback(
|
||||
setupWindow,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
setupWindow.on("closed", () => {
|
||||
appState.anilistSetupWindow = null;
|
||||
appState.anilistSetupPageOpened = false;
|
||||
});
|
||||
|
||||
appState.anilistSetupWindow = setupWindow;
|
||||
appState.anilistSetupPageOpened = true;
|
||||
}
|
||||
|
||||
async function refreshAnilistClientSecretState(options?: { force?: boolean }): Promise<string | null> {
|
||||
const resolved = getResolvedConfig();
|
||||
const now = Date.now();
|
||||
if (!isAnilistTrackingEnabled(resolved)) {
|
||||
anilistCachedAccessToken = null;
|
||||
setAnilistClientSecretState({
|
||||
status: "not_checked",
|
||||
source: "none",
|
||||
message: "anilist tracking disabled",
|
||||
resolvedAt: null,
|
||||
errorAt: null,
|
||||
});
|
||||
appState.anilistSetupPageOpened = false;
|
||||
return null;
|
||||
}
|
||||
const rawAccessToken = resolved.anilist.accessToken.trim();
|
||||
if (rawAccessToken.length > 0) {
|
||||
if (options?.force || rawAccessToken !== anilistCachedAccessToken) {
|
||||
anilistTokenStore.saveToken(rawAccessToken);
|
||||
}
|
||||
anilistCachedAccessToken = rawAccessToken;
|
||||
setAnilistClientSecretState({
|
||||
status: "resolved",
|
||||
source: "literal",
|
||||
message: "using configured anilist.accessToken",
|
||||
resolvedAt: now,
|
||||
errorAt: null,
|
||||
});
|
||||
appState.anilistSetupPageOpened = false;
|
||||
return rawAccessToken;
|
||||
}
|
||||
|
||||
if (!options?.force && anilistCachedAccessToken && anilistCachedAccessToken.length > 0) {
|
||||
return anilistCachedAccessToken;
|
||||
}
|
||||
|
||||
const storedToken = anilistTokenStore.loadToken()?.trim() ?? "";
|
||||
if (storedToken.length > 0) {
|
||||
anilistCachedAccessToken = storedToken;
|
||||
setAnilistClientSecretState({
|
||||
status: "resolved",
|
||||
source: "stored",
|
||||
message: "using stored anilist access token",
|
||||
resolvedAt: now,
|
||||
errorAt: null,
|
||||
});
|
||||
appState.anilistSetupPageOpened = false;
|
||||
return storedToken;
|
||||
}
|
||||
|
||||
anilistCachedAccessToken = null;
|
||||
setAnilistClientSecretState({
|
||||
status: "error",
|
||||
source: "none",
|
||||
message: "cannot authenticate without anilist.accessToken",
|
||||
resolvedAt: null,
|
||||
errorAt: now,
|
||||
});
|
||||
if (
|
||||
isAnilistTrackingEnabled(resolved) &&
|
||||
!appState.anilistSetupPageOpened
|
||||
) {
|
||||
openAnilistSetupWindow();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCurrentAnilistMediaKey(): string | null {
|
||||
const path = appState.currentMediaPath?.trim();
|
||||
return path && path.length > 0 ? path : null;
|
||||
}
|
||||
|
||||
function resetAnilistMediaTracking(mediaKey: string | null): void {
|
||||
anilistCurrentMediaKey = mediaKey;
|
||||
anilistCurrentMediaDurationSec = null;
|
||||
anilistCurrentMediaGuess = null;
|
||||
anilistCurrentMediaGuessPromise = null;
|
||||
anilistLastDurationProbeAtMs = 0;
|
||||
}
|
||||
|
||||
async function maybeProbeAnilistDuration(mediaKey: string): Promise<number | null> {
|
||||
if (anilistCurrentMediaKey !== mediaKey) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
typeof anilistCurrentMediaDurationSec === "number" &&
|
||||
anilistCurrentMediaDurationSec > 0
|
||||
) {
|
||||
return anilistCurrentMediaDurationSec;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - anilistLastDurationProbeAtMs < ANILIST_DURATION_RETRY_INTERVAL_MS) {
|
||||
return null;
|
||||
}
|
||||
anilistLastDurationProbeAtMs = now;
|
||||
|
||||
try {
|
||||
const durationCandidate = await appState.mpvClient?.requestProperty("duration");
|
||||
const duration =
|
||||
typeof durationCandidate === "number" && Number.isFinite(durationCandidate)
|
||||
? durationCandidate
|
||||
: null;
|
||||
if (duration && duration > 0 && anilistCurrentMediaKey === mediaKey) {
|
||||
anilistCurrentMediaDurationSec = duration;
|
||||
return duration;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("AniList duration probe failed:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureAnilistMediaGuess(mediaKey: string): Promise<AnilistMediaGuess | null> {
|
||||
if (anilistCurrentMediaKey !== mediaKey) {
|
||||
return null;
|
||||
}
|
||||
if (anilistCurrentMediaGuess) {
|
||||
return anilistCurrentMediaGuess;
|
||||
}
|
||||
if (anilistCurrentMediaGuessPromise) {
|
||||
return anilistCurrentMediaGuessPromise;
|
||||
}
|
||||
|
||||
const mediaPathForGuess = mediaRuntime.resolveMediaPathForJimaku(
|
||||
appState.currentMediaPath,
|
||||
);
|
||||
anilistCurrentMediaGuessPromise = guessAnilistMediaInfo(
|
||||
mediaPathForGuess,
|
||||
appState.currentMediaTitle,
|
||||
)
|
||||
.then((guess) => {
|
||||
if (anilistCurrentMediaKey === mediaKey) {
|
||||
anilistCurrentMediaGuess = guess;
|
||||
}
|
||||
return guess;
|
||||
})
|
||||
.finally(() => {
|
||||
if (anilistCurrentMediaKey === mediaKey) {
|
||||
anilistCurrentMediaGuessPromise = null;
|
||||
}
|
||||
});
|
||||
return anilistCurrentMediaGuessPromise;
|
||||
}
|
||||
|
||||
function buildAnilistAttemptKey(mediaKey: string, episode: number): string {
|
||||
return `${mediaKey}::${episode}`;
|
||||
}
|
||||
|
||||
function rememberAnilistAttemptedUpdateKey(key: string): void {
|
||||
anilistAttemptedUpdateKeys.add(key);
|
||||
if (anilistAttemptedUpdateKeys.size <= ANILIST_MAX_ATTEMPTED_UPDATE_KEYS) {
|
||||
return;
|
||||
}
|
||||
const oldestKey = anilistAttemptedUpdateKeys.values().next().value;
|
||||
if (typeof oldestKey === "string") {
|
||||
anilistAttemptedUpdateKeys.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
async function processNextAnilistRetryUpdate(): Promise<void> {
|
||||
const queued = anilistUpdateQueue.nextReady();
|
||||
refreshAnilistRetryQueueState();
|
||||
if (!queued) {
|
||||
return;
|
||||
}
|
||||
|
||||
appState.anilistRetryQueueState.lastAttemptAt = Date.now();
|
||||
const accessToken = await refreshAnilistClientSecretState();
|
||||
if (!accessToken) {
|
||||
appState.anilistRetryQueueState.lastError = "AniList token unavailable for queued retry.";
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await updateAnilistPostWatchProgress(
|
||||
accessToken,
|
||||
queued.title,
|
||||
queued.episode,
|
||||
);
|
||||
if (result.status === "updated" || result.status === "skipped") {
|
||||
anilistUpdateQueue.markSuccess(queued.key);
|
||||
rememberAnilistAttemptedUpdateKey(queued.key);
|
||||
appState.anilistRetryQueueState.lastError = null;
|
||||
refreshAnilistRetryQueueState();
|
||||
logger.info(`[AniList queue] ${result.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
anilistUpdateQueue.markFailure(queued.key, result.message);
|
||||
appState.anilistRetryQueueState.lastError = result.message;
|
||||
refreshAnilistRetryQueueState();
|
||||
}
|
||||
|
||||
async function maybeRunAnilistPostWatchUpdate(): Promise<void> {
|
||||
if (anilistUpdateInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = getResolvedConfig();
|
||||
if (!isAnilistTrackingEnabled(resolved)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaKey = getCurrentAnilistMediaKey();
|
||||
if (!mediaKey || !appState.mpvClient) {
|
||||
return;
|
||||
}
|
||||
if (anilistCurrentMediaKey !== mediaKey) {
|
||||
resetAnilistMediaTracking(mediaKey);
|
||||
}
|
||||
|
||||
const watchedSeconds = appState.mpvClient.currentTimePos;
|
||||
if (
|
||||
!Number.isFinite(watchedSeconds) ||
|
||||
watchedSeconds < ANILIST_UPDATE_MIN_WATCH_SECONDS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = await maybeProbeAnilistDuration(mediaKey);
|
||||
if (!duration || duration <= 0) {
|
||||
return;
|
||||
}
|
||||
if (watchedSeconds / duration < ANILIST_UPDATE_MIN_WATCH_RATIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guess = await ensureAnilistMediaGuess(mediaKey);
|
||||
if (!guess?.title || !guess.episode || guess.episode <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptKey = buildAnilistAttemptKey(mediaKey, guess.episode);
|
||||
if (anilistAttemptedUpdateKeys.has(attemptKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
anilistUpdateInFlight = true;
|
||||
try {
|
||||
await processNextAnilistRetryUpdate();
|
||||
|
||||
const accessToken = await refreshAnilistClientSecretState();
|
||||
if (!accessToken) {
|
||||
anilistUpdateQueue.enqueue(attemptKey, guess.title, guess.episode);
|
||||
anilistUpdateQueue.markFailure(
|
||||
attemptKey,
|
||||
"cannot authenticate without anilist.accessToken",
|
||||
);
|
||||
refreshAnilistRetryQueueState();
|
||||
showMpvOsd("AniList: access token not configured");
|
||||
return;
|
||||
}
|
||||
const result = await updateAnilistPostWatchProgress(
|
||||
accessToken,
|
||||
guess.title,
|
||||
guess.episode,
|
||||
);
|
||||
if (result.status === "updated") {
|
||||
rememberAnilistAttemptedUpdateKey(attemptKey);
|
||||
anilistUpdateQueue.markSuccess(attemptKey);
|
||||
refreshAnilistRetryQueueState();
|
||||
showMpvOsd(result.message);
|
||||
logger.info(result.message);
|
||||
return;
|
||||
}
|
||||
if (result.status === "skipped") {
|
||||
rememberAnilistAttemptedUpdateKey(attemptKey);
|
||||
anilistUpdateQueue.markSuccess(attemptKey);
|
||||
refreshAnilistRetryQueueState();
|
||||
logger.info(result.message);
|
||||
return;
|
||||
}
|
||||
anilistUpdateQueue.enqueue(attemptKey, guess.title, guess.episode);
|
||||
anilistUpdateQueue.markFailure(attemptKey, result.message);
|
||||
refreshAnilistRetryQueueState();
|
||||
showMpvOsd(`AniList: ${result.message}`);
|
||||
logger.warn(result.message);
|
||||
} finally {
|
||||
anilistUpdateInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadSubtitlePosition(): SubtitlePosition | null {
|
||||
appState.subtitlePosition = loadSubtitlePositionService({
|
||||
currentMediaPath: appState.currentMediaPath,
|
||||
@@ -655,6 +1134,7 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
reloadConfig: () => {
|
||||
configService.reloadConfig();
|
||||
appLogger.logInfo(`Using config file: ${configService.getConfigPath()}`);
|
||||
void refreshAnilistClientSecretState({ force: true });
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
@@ -731,6 +1211,10 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
if (appState.ankiIntegration) {
|
||||
appState.ankiIntegration.destroy();
|
||||
}
|
||||
if (appState.anilistSetupWindow) {
|
||||
appState.anilistSetupWindow.destroy();
|
||||
}
|
||||
appState.anilistSetupWindow = null;
|
||||
},
|
||||
shouldRestoreWindowsOnActivate: () =>
|
||||
appState.overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,
|
||||
@@ -745,6 +1229,8 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
);
|
||||
|
||||
applyStartupState(appState, startupState);
|
||||
void refreshAnilistClientSecretState({ force: true });
|
||||
refreshAnilistRetryQueueState();
|
||||
|
||||
function handleCliCommand(
|
||||
args: CliArgs,
|
||||
@@ -828,16 +1314,26 @@ function bindMpvClientEventHandlers(mpvClient: MpvIpcClient): void {
|
||||
broadcastToOverlayWindows("secondary-subtitle:set", text);
|
||||
});
|
||||
mpvClient.on("subtitle-timing", ({ text, start, end }) => {
|
||||
if (!text.trim() || !appState.subtitleTimingTracker) {
|
||||
return;
|
||||
if (text.trim() && appState.subtitleTimingTracker) {
|
||||
appState.subtitleTimingTracker.recordSubtitle(text, start, end);
|
||||
}
|
||||
appState.subtitleTimingTracker.recordSubtitle(text, start, end);
|
||||
void maybeRunAnilistPostWatchUpdate().catch((error) => {
|
||||
logger.error("AniList post-watch update failed unexpectedly", error);
|
||||
});
|
||||
});
|
||||
mpvClient.on("media-path-change", ({ path }) => {
|
||||
mediaRuntime.updateCurrentMediaPath(path);
|
||||
const mediaKey = getCurrentAnilistMediaKey();
|
||||
resetAnilistMediaTracking(mediaKey);
|
||||
if (mediaKey) {
|
||||
void maybeProbeAnilistDuration(mediaKey);
|
||||
void ensureAnilistMediaGuess(mediaKey);
|
||||
}
|
||||
});
|
||||
mpvClient.on("media-title-change", ({ title }) => {
|
||||
mediaRuntime.updateCurrentMediaTitle(title);
|
||||
anilistCurrentMediaGuess = null;
|
||||
anilistCurrentMediaGuessPromise = null;
|
||||
});
|
||||
mpvClient.on("subtitle-metrics-change", ({ patch }) => {
|
||||
updateMpvSubtitleRenderMetrics(patch);
|
||||
|
||||
Reference in New Issue
Block a user