mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-28 12:55:17 -07:00
fix(jellyfin): show overlay, inject plugin, and fix stats title on playback (#77)
* fix(jellyfin): show overlay, inject plugin, and fix stats title on playb - Show visible overlay automatically during Jellyfin playback so subtitleStyle applies - Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus - Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles - Mark ffsubsync unavailable in subsync modal for remote media paths - Drain queued second-instance commands even when onReady throws * fix(overlay): stabilize macOS focus handoff and sidebar Yomitan pause - Keep overlay visible during macOS foreground probe after overlay blur - Hold sidebar hover-pause while a Yomitan lookup popup remains open * fix(jellyfin): fix discovery loop, device identity, tray state, and Disc - Derive device identity from OS hostname; remove legacy configurable client/device fields - Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores - Restart stale tray discovery sessions without re-login when server drops SubMiner cast target - Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes - Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads - Fix picker library discovery when log level is above info - Fix config.example.jsonc trailing commas and array formatting * docs(release): trim and consolidate prerelease notes for 0.15.0 - Remove breaking changes section and several redundant bullet points - Consolidate per-platform updater notes into a single entry - Normalize em-dash separators to hyphens in section headers * fix(config): remove trailing commas from config.example.jsonc - Strip trailing commas throughout both config.example.jsonc copies - Reformat inline arrays to multi-line for JSON strictness - Update Jellyfin subtitle preload and playback launch tests and impl * fix(tokenizer): preserve known-word highlight when POS filters suppress - Known-word cache matches now set isKnown=true even for tokens excluded by POS filters - POS exclusion gate suppresses N+1, frequency, and JLPT only; known status is computed before the gate - Jellyfin subtitle preload continues after cleanup failures instead of aborting - Update config docs and option description to document the known-word bypass behavior * fix(jellyfin): send explicit hide/show overlay instead of toggle - Track overlay visibility in plugin state; y-t uses explicit hide/show commands when state is known - Prevent paused Jellyfin playback from resuming on overlay hide - Fix subtitle cache cleanup to only remove dirs after successful cleanup * fix(jellyfin): fix remote progress sync, seek reporting, and startup sto - arm active playback before loadfile with loadedMediaPath: null to suppress premature stop events - force immediate progress report on seek-like position jumps at the mpv time-pos level - send positionTicks and failed=false in reportStopped payload - remove EventName from HTTP timeline payloads (websocket-only field) - add startup grace window to drop stop events before media finishes loading * fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi - Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift - Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress - Preserve manual hide across Jellyfin path-changing redirects even when media-title drops - Rearm managed subtitle defaults on path-changing redirects - Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC - Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus - Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary - Add stats window layer management so delete/update dialogs appear above stats window - Fix Jellyfin remote progress sync during Linux websocket reconnect windows * Fix CodeRabbit review feedback * fix(jellyfin): subtitle timing, resume progress, and overlay sync - Add per-stream subtitle delay persistence and auto timeline-offset correction - Strip server-selected subtitle stream from mpv load URL; suppress plugin subtitle rearm and auto-start during app-managed preload - Fix resume position lost when mpv resets on stop; use last known position for final progress/stopped reports - Keep Play vs Resume distinct to avoid early seek race on normal play - Fix discovery resume when remote play sends StartPositionTicks=0 despite saved progress - Deduplicate show/hide overlay commands using recorded visibility state - Rewrite docs-site Jellyfin page around cast-to-device UX * test: update lifecycle cleanup assertion * fix: clear aborted playback state, fix overlay passthrough, and guard du - Reset app_managed_playback_pending on lifecycle cleanup to prevent state leak into next item - Record visible overlay action only after command succeeds, not before - Non-native passive overlay now always click-through on re-show (fix isNonNativePassiveOverlay ordering) - Defer activeParsedSubtitleMediaPath assignment until after prefetch completes - Move autoplay gate release into the hide branch of toggleVisibleOverlay - Clear active Jellyfin playback when stopping media that never loaded - Reset managed subtitle delay and delay key when no external tracks are available - Await async removeDir in subtitle cache cleanup - Guard duplicate delete clicks in MediaDetailView and SessionsTab with refs - Escape key in DeleteConfirmDialog now calls stopPropagation and stopImmediatePropagation
This commit is contained in:
+181
-21
@@ -35,6 +35,10 @@ import { applyControllerConfigUpdate } from './main/controller-config-update.js'
|
||||
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
|
||||
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
|
||||
import { startAppControlServer } from './main/runtime/app-control-server';
|
||||
import {
|
||||
markJellyfinRemotePlaybackLoaded as markJellyfinRemotePlaybackLoadedState,
|
||||
shouldAutoLoadSecondarySubTrackForJellyfinPlayback,
|
||||
} from './main/runtime/jellyfin-remote-playback';
|
||||
import { getAppControlSocketPath } from './shared/app-control';
|
||||
import {
|
||||
type CancelLinuxMpvFullscreenOverlayRefreshBurst,
|
||||
@@ -44,6 +48,7 @@ import {
|
||||
import { mergeAiConfig } from './ai/config';
|
||||
|
||||
function getPasswordStoreArg(argv: string[]): string | null {
|
||||
let resolved: string | null = null;
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg?.startsWith('--password-store')) {
|
||||
@@ -53,17 +58,18 @@ function getPasswordStoreArg(argv: string[]): string | null {
|
||||
if (arg === '--password-store') {
|
||||
const value = argv[i + 1];
|
||||
if (value && !value.startsWith('--')) {
|
||||
return value;
|
||||
resolved = value.trim();
|
||||
i += 1;
|
||||
}
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const [prefix, value] = arg.split('=', 2);
|
||||
if (prefix === '--password-store' && value && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
resolved = value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function normalizePasswordStoreArg(value: string): string {
|
||||
@@ -319,6 +325,7 @@ import {
|
||||
listJellyfinItemsRuntime,
|
||||
listJellyfinLibrariesRuntime,
|
||||
listJellyfinSubtitleTracksRuntime,
|
||||
loadJellyfinSubtitleDelay,
|
||||
loadSubtitlePosition as loadSubtitlePositionCore,
|
||||
loadYomitanExtension as loadYomitanExtensionCore,
|
||||
markLastCardAsAudioCard as markLastCardAsAudioCardCore,
|
||||
@@ -329,6 +336,7 @@ import {
|
||||
replayCurrentSubtitleRuntime,
|
||||
resolveJellyfinPlaybackPlanRuntime,
|
||||
runStartupBootstrapRuntime,
|
||||
saveJellyfinSubtitleDelay,
|
||||
saveSubtitlePosition as saveSubtitlePositionCore,
|
||||
addYomitanNoteViaSearch,
|
||||
clearYomitanParserCachesForWindow,
|
||||
@@ -356,6 +364,7 @@ import {
|
||||
promoteStatsOverlayAbovePlayback,
|
||||
registerStatsOverlayToggle,
|
||||
toggleStatsOverlay as toggleStatsOverlayWindow,
|
||||
withStatsWindowLayerSuspendedForNativeDialog,
|
||||
} from './core/services/stats-window.js';
|
||||
import {
|
||||
createFirstRunSetupService,
|
||||
@@ -403,6 +412,11 @@ import {
|
||||
launchWindowsMpv,
|
||||
} from './main/runtime/windows-mpv-launch';
|
||||
import { createWaitForMpvConnectedHandler } from './main/runtime/jellyfin-remote-connection';
|
||||
import {
|
||||
DEFAULT_JELLYFIN_CLIENT_NAME,
|
||||
DEFAULT_JELLYFIN_CLIENT_VERSION,
|
||||
createHostDerivedJellyfinDeviceId,
|
||||
} from './main/runtime/jellyfin-device-identity';
|
||||
import {
|
||||
clearJellyfinAuthSessionAndRefreshTray as clearJellyfinAuthSessionAndRefreshTrayRuntime,
|
||||
isJellyfinConfiguredForTray as isJellyfinConfiguredForTrayRuntime,
|
||||
@@ -508,6 +522,7 @@ import { handleCharacterDictionaryAutoSyncComplete } from './main/runtime/charac
|
||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||
import { resolveCurrentSubtitleForRenderer } from './main/runtime/current-subtitle-snapshot';
|
||||
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
|
||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||
import {
|
||||
createElectronAppUpdater,
|
||||
@@ -619,6 +634,15 @@ const DEFAULT_MPV_LOG_FILE = resolveDefaultLogFilePath({
|
||||
appDataDir: process.env.APPDATA,
|
||||
});
|
||||
const ANILIST_SETUP_CLIENT_ID_URL = 'https://anilist.co/api/v2/oauth/authorize';
|
||||
const jellyfinSubtitleCacheIo = createJellyfinSubtitleCacheIo({
|
||||
tmpDir: () => os.tmpdir(),
|
||||
makeTempDir: (prefix) => fs.promises.mkdtemp(prefix),
|
||||
writeFile: (filePath, bytes) => fs.promises.writeFile(filePath, bytes),
|
||||
removeDir: (dir, options) => {
|
||||
fs.rmSync(dir, options);
|
||||
},
|
||||
fetch: (url) => fetch(url),
|
||||
});
|
||||
const ANILIST_SETUP_RESPONSE_TYPE = 'token';
|
||||
const ANILIST_DEFAULT_CLIENT_ID = '36084';
|
||||
const ANILIST_REDIRECT_URI = 'https://anilist.subminer.moe/';
|
||||
@@ -639,6 +663,7 @@ let jellyfinPlayQuitOnDisconnectArmed = false;
|
||||
const JELLYFIN_LANG_PREF = 'ja,jp,jpn,japanese,en,eng,english,enUS,en-US';
|
||||
const JELLYFIN_TICKS_PER_SECOND = 10_000_000;
|
||||
const JELLYFIN_REMOTE_PROGRESS_INTERVAL_MS = 3000;
|
||||
const JELLYFIN_REMOTE_STARTUP_STOP_GRACE_MS = 10_000;
|
||||
const DISCORD_PRESENCE_APP_ID = '1475264834730856619';
|
||||
const JELLYFIN_MPV_CONNECT_TIMEOUT_MS = 3000;
|
||||
const JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS = 20000;
|
||||
@@ -647,16 +672,18 @@ const YOUTUBE_MPV_AUTO_LAUNCH_TIMEOUT_MS = 10000;
|
||||
const YOUTUBE_MPV_YTDL_FORMAT = 'bestvideo*+bestaudio/best';
|
||||
const YOUTUBE_DIRECT_PLAYBACK_FORMAT = 'b';
|
||||
const MPV_JELLYFIN_DEFAULT_ARGS = [
|
||||
'--sub-auto=fuzzy',
|
||||
'--sub-auto=no',
|
||||
'--sub-file-paths=.;subs;subtitles',
|
||||
'--sid=auto',
|
||||
'--secondary-sid=auto',
|
||||
'--sid=no',
|
||||
'--secondary-sid=no',
|
||||
'--sub-visibility=no',
|
||||
'--secondary-sub-visibility=no',
|
||||
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||
] as const;
|
||||
|
||||
let activeJellyfinRemotePlayback: ActiveJellyfinRemotePlaybackState | null = null;
|
||||
let activeJellyfinSubtitleDelayKey: { itemId: string; streamIndex: number } | null = null;
|
||||
let jellyfinRemoteLastProgressAtMs = 0;
|
||||
let jellyfinMpvAutoLaunchInFlight: Promise<boolean> | null = null;
|
||||
let backgroundWarmupsStarted = false;
|
||||
@@ -1803,12 +1830,13 @@ async function refreshSubtitleSidebarFromSource(
|
||||
if (!normalizedSourcePath) {
|
||||
return;
|
||||
}
|
||||
appState.activeParsedSubtitleMediaPath = mediaPath?.trim() || getCurrentAutoplayMediaPath();
|
||||
const nextMediaPath = mediaPath?.trim() || getCurrentAutoplayMediaPath();
|
||||
await subtitlePrefetchInitController.initSubtitlePrefetch(
|
||||
normalizedSourcePath,
|
||||
lastObservedTimePos,
|
||||
normalizedSourcePath,
|
||||
);
|
||||
appState.activeParsedSubtitleMediaPath = nextMediaPath;
|
||||
}
|
||||
const refreshSubtitlePrefetchFromActiveTrackHandler =
|
||||
createRefreshSubtitlePrefetchFromActiveTrackHandler({
|
||||
@@ -2115,6 +2143,7 @@ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHos
|
||||
const createFieldGroupingCallback = fieldGroupingOverlayRuntime.createFieldGroupingCallback;
|
||||
|
||||
const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, 'subtitle-positions');
|
||||
const JELLYFIN_SUBTITLE_DELAYS_PATH = path.join(CONFIG_DIR, 'jellyfin-subtitle-delays.json');
|
||||
|
||||
const mediaRuntime = createMediaRuntimeService(
|
||||
createBuildMediaRuntimeMainDepsHandler({
|
||||
@@ -2280,6 +2309,7 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
||||
getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName,
|
||||
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
|
||||
getWindowsFocusHandoffGraceActive: () => hasWindowsVisibleOverlayFocusHandoffGrace(),
|
||||
getMacOSForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive,
|
||||
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
||||
appState.trackerNotReadyWarningShown = shown;
|
||||
@@ -2323,6 +2353,7 @@ const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as const;
|
||||
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
||||
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
||||
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
||||
const MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS = 1_200;
|
||||
let visibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
||||
@@ -2331,6 +2362,9 @@ let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval>
|
||||
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||
let visibleOverlayInteractionActive = false;
|
||||
let macOSVisibleOverlayForegroundProbeActive = false;
|
||||
let macOSVisibleOverlayForegroundProbeToken = 0;
|
||||
let macOSVisibleOverlayForegroundProbeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const handleStatsOverlayVisibilityChanged = createStatsOverlayVisibilityChangeHandler({
|
||||
setStatsOverlayVisibleState: (visible) => {
|
||||
@@ -2357,6 +2391,49 @@ function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void {
|
||||
windowsVisibleOverlayZOrderRetryTimeouts = [];
|
||||
}
|
||||
|
||||
function finishMacOSVisibleOverlayForegroundProbe(token: number): void {
|
||||
if (token !== macOSVisibleOverlayForegroundProbeToken) {
|
||||
return;
|
||||
}
|
||||
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
||||
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
||||
macOSVisibleOverlayForegroundProbeTimeout = null;
|
||||
}
|
||||
if (!macOSVisibleOverlayForegroundProbeActive) {
|
||||
return;
|
||||
}
|
||||
macOSVisibleOverlayForegroundProbeActive = false;
|
||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||
}
|
||||
|
||||
function startMacOSVisibleOverlayForegroundProbe(): void {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
const tracker = appState.windowTracker;
|
||||
if (!tracker) {
|
||||
return;
|
||||
}
|
||||
|
||||
macOSVisibleOverlayForegroundProbeActive = true;
|
||||
const token = ++macOSVisibleOverlayForegroundProbeToken;
|
||||
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
||||
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
||||
}
|
||||
macOSVisibleOverlayForegroundProbeTimeout = setTimeout(() => {
|
||||
finishMacOSVisibleOverlayForegroundProbe(token);
|
||||
}, MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS);
|
||||
|
||||
void tracker
|
||||
.refreshNow()
|
||||
.catch((error) => {
|
||||
logger.warn('Failed to refresh macOS frontmost app after overlay blur', error);
|
||||
})
|
||||
.finally(() => {
|
||||
finishMacOSVisibleOverlayForegroundProbe(token);
|
||||
});
|
||||
}
|
||||
|
||||
function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
||||
const handle = window.getNativeWindowHandle();
|
||||
return handle.length >= 8
|
||||
@@ -2555,6 +2632,7 @@ function scheduleVisibleOverlayBlurRefresh(): void {
|
||||
if (process.platform === 'win32') {
|
||||
lastWindowsVisibleOverlayBlurredAtMs = Date.now();
|
||||
}
|
||||
startMacOSVisibleOverlayForegroundProbe();
|
||||
clearVisibleOverlayBlurRefreshTimeouts();
|
||||
for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) {
|
||||
const refreshTimeout = setTimeout(() => {
|
||||
@@ -2801,6 +2879,7 @@ const {
|
||||
reportJellyfinRemoteStopped,
|
||||
startJellyfinRemoteSession,
|
||||
stopJellyfinRemoteSession,
|
||||
cleanupJellyfinSubtitleCache,
|
||||
runJellyfinCommand,
|
||||
openJellyfinSetupWindow,
|
||||
getJellyfinClientInfo,
|
||||
@@ -2812,7 +2891,9 @@ const {
|
||||
},
|
||||
getJellyfinClientInfoMainDeps: {
|
||||
getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
getDefaultJellyfinConfig: () => DEFAULT_CONFIG.jellyfin,
|
||||
getHostName: () => os.hostname(),
|
||||
defaultClientName: DEFAULT_JELLYFIN_CLIENT_NAME,
|
||||
defaultClientVersion: DEFAULT_JELLYFIN_CLIENT_VERSION,
|
||||
},
|
||||
waitForMpvConnectedMainDeps: {
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
@@ -2824,6 +2905,15 @@ const {
|
||||
getLaunchMode: () => getResolvedConfig().mpv.launchMode,
|
||||
platform: process.platform,
|
||||
execPath: process.execPath,
|
||||
getRuntimePluginEntrypoint: () => resolveBundledMpvRuntimePluginEntrypoint(),
|
||||
getInstalledPluginDetection: () =>
|
||||
detectInstalledMpvPlugin({
|
||||
platform: process.platform,
|
||||
homeDir: os.homedir(),
|
||||
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
||||
appDataDir: app.getPath('appData'),
|
||||
mpvExecutablePath: getResolvedConfig().mpv.executablePath,
|
||||
}),
|
||||
getPluginRuntimeConfig: () => getMpvPluginRuntimeConfig(),
|
||||
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
|
||||
defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS,
|
||||
@@ -2859,6 +2949,25 @@ const {
|
||||
sendMpvCommandRuntime(appState.mpvClient, command);
|
||||
},
|
||||
wait: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
|
||||
cacheSubtitleTrack: (track) => jellyfinSubtitleCacheIo.cacheSubtitleTrack(track),
|
||||
cleanupCachedSubtitles: (dirs) => jellyfinSubtitleCacheIo.cleanupCachedSubtitles(dirs),
|
||||
getSavedSubtitleDelay: (itemId, streamIndex) =>
|
||||
loadJellyfinSubtitleDelay({
|
||||
filePath: JELLYFIN_SUBTITLE_DELAYS_PATH,
|
||||
itemId,
|
||||
streamIndex,
|
||||
}),
|
||||
setActiveSubtitleDelayKey: (key) => {
|
||||
activeJellyfinSubtitleDelayKey = key;
|
||||
},
|
||||
loadSubtitleSourceText,
|
||||
saveSubtitleDelay: (itemId, streamIndex, delaySeconds) =>
|
||||
saveJellyfinSubtitleDelay({
|
||||
filePath: JELLYFIN_SUBTITLE_DELAYS_PATH,
|
||||
itemId,
|
||||
streamIndex,
|
||||
delaySeconds,
|
||||
}),
|
||||
logDebug: (message, error) => {
|
||||
logger.debug(message, error);
|
||||
},
|
||||
@@ -2877,6 +2986,7 @@ const {
|
||||
},
|
||||
),
|
||||
applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient),
|
||||
showVisibleOverlay: () => setVisibleOverlayVisible(true),
|
||||
sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
|
||||
armQuitOnDisconnect: () => {
|
||||
jellyfinPlayQuitOnDisconnectArmed = false;
|
||||
@@ -2889,7 +2999,11 @@ const {
|
||||
},
|
||||
convertTicksToSeconds: (ticks) => jellyfinTicksToSecondsRuntime(ticks),
|
||||
setActivePlayback: (state) => {
|
||||
activeJellyfinRemotePlayback = state as ActiveJellyfinRemotePlaybackState;
|
||||
activeJellyfinRemotePlayback = {
|
||||
...(state as ActiveJellyfinRemotePlaybackState),
|
||||
stopReportsAfterMs:
|
||||
state.stopReportsAfterMs ?? Date.now() + JELLYFIN_REMOTE_STARTUP_STOP_GRACE_MS,
|
||||
};
|
||||
},
|
||||
setLastProgressAtMs: (value) => {
|
||||
jellyfinRemoteLastProgressAtMs = value;
|
||||
@@ -2900,6 +3014,13 @@ const {
|
||||
showMpvOsd: (text) => {
|
||||
showMpvOsd(text);
|
||||
},
|
||||
updateCurrentMediaTitle: (title) => {
|
||||
mediaRuntime.updateCurrentMediaTitle(title);
|
||||
},
|
||||
recordJellyfinPlaybackMetadata: (metadata) => {
|
||||
ensureImmersionTrackerStarted();
|
||||
appState.immersionTracker?.recordJellyfinPlaybackMetadata(metadata);
|
||||
},
|
||||
},
|
||||
remoteComposerOptions: {
|
||||
getConfiguredSession: () => getConfiguredJellyfinSession(getResolvedJellyfinConfig()),
|
||||
@@ -2910,6 +3031,7 @@ const {
|
||||
getActivePlayback: () => activeJellyfinRemotePlayback,
|
||||
clearActivePlayback: () => {
|
||||
activeJellyfinRemotePlayback = null;
|
||||
activeJellyfinSubtitleDelayKey = null;
|
||||
},
|
||||
getSession: () => appState.jellyfinRemoteSession,
|
||||
getNow: () => Date.now(),
|
||||
@@ -2960,11 +3082,13 @@ const {
|
||||
appState.jellyfinRemoteSession = session as typeof appState.jellyfinRemoteSession;
|
||||
},
|
||||
createRemoteSessionService: (options) => new JellyfinRemoteSessionService(options),
|
||||
defaultDeviceId: DEFAULT_CONFIG.jellyfin.deviceId,
|
||||
defaultClientName: DEFAULT_CONFIG.jellyfin.clientName,
|
||||
defaultClientVersion: DEFAULT_CONFIG.jellyfin.clientVersion,
|
||||
getHostName: () => os.hostname(),
|
||||
defaultDeviceId: createHostDerivedJellyfinDeviceId(os.hostname()),
|
||||
defaultClientName: DEFAULT_JELLYFIN_CLIENT_NAME,
|
||||
defaultClientVersion: DEFAULT_JELLYFIN_CLIENT_VERSION,
|
||||
logInfo: (message) => logger.info(message),
|
||||
logWarn: (message, details) => logger.warn(message, details),
|
||||
onSessionStateChanged: () => refreshTrayMenuIfPresent(),
|
||||
},
|
||||
stopJellyfinRemoteSessionMainDeps: {
|
||||
getCurrentSession: () => appState.jellyfinRemoteSession,
|
||||
@@ -2974,6 +3098,7 @@ const {
|
||||
clearActivePlayback: () => {
|
||||
activeJellyfinRemotePlayback = null;
|
||||
},
|
||||
onSessionStateChanged: () => refreshTrayMenuIfPresent(),
|
||||
},
|
||||
runJellyfinCommandMainDeps: {
|
||||
defaultServerUrl: DEFAULT_CONFIG.jellyfin.serverUrl,
|
||||
@@ -2994,7 +3119,6 @@ const {
|
||||
clearStoredSession: () =>
|
||||
clearJellyfinAuthSessionAndRefreshTrayRuntime(getJellyfinTrayDiscoveryDeps()),
|
||||
patchJellyfinConfig: (session) => {
|
||||
const clientInfo = getJellyfinClientInfo();
|
||||
const recentServers = mergeJellyfinRecentServers(
|
||||
session.serverUrl,
|
||||
getResolvedConfig().jellyfin.recentServers || [],
|
||||
@@ -3004,9 +3128,6 @@ const {
|
||||
enabled: true,
|
||||
serverUrl: session.serverUrl,
|
||||
username: session.username,
|
||||
deviceId: clientInfo.deviceId,
|
||||
clientName: clientInfo.clientName,
|
||||
clientVersion: clientInfo.clientVersion,
|
||||
recentServers,
|
||||
},
|
||||
});
|
||||
@@ -3670,6 +3791,7 @@ const {
|
||||
},
|
||||
stopJellyfinRemoteSession: () => stopJellyfinRemoteSession(),
|
||||
cleanupYoutubeSubtitleTempDirs: () => youtubeFlowRuntime.cleanupSubtitleTempDirs(),
|
||||
cleanupJellyfinSubtitleCache: () => cleanupJellyfinSubtitleCache(),
|
||||
stopDiscordPresenceService: () => {
|
||||
void appState.discordPresenceService?.stop();
|
||||
appState.discordPresenceService = null;
|
||||
@@ -4331,11 +4453,12 @@ const {
|
||||
appState.activeParsedSubtitleSource = null;
|
||||
appState.activeParsedSubtitleMediaPath = null;
|
||||
}
|
||||
activeJellyfinSubtitleDelayKey = null;
|
||||
broadcastToOverlayWindows('subtitle:set', resetSubtitlePayload);
|
||||
subtitleWsService.broadcast(resetSubtitlePayload, frequencyOptions);
|
||||
annotationSubtitleWsService.broadcast(resetSubtitlePayload, frequencyOptions);
|
||||
autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks();
|
||||
}
|
||||
autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks();
|
||||
currentMediaTokenizationGate.updateCurrentMediaPath(path);
|
||||
managedLocalSubtitleSelectionRuntime.handleMediaPathChange(path);
|
||||
startupOsdSequencer.reset();
|
||||
@@ -4372,6 +4495,9 @@ const {
|
||||
immersionMediaRuntime.syncFromCurrentMediaState();
|
||||
},
|
||||
signalAutoplayReadyIfWarm: (path) => signalAutoplayReadyFromWarmTokenization?.(path),
|
||||
markJellyfinRemotePlaybackLoaded: (path) => {
|
||||
markJellyfinRemotePlaybackLoadedState(activeJellyfinRemotePlayback, path);
|
||||
},
|
||||
scheduleCharacterDictionarySync: () => {
|
||||
if (!yomitanProfilePolicy.isCharacterDictionaryEnabled() || isYoutubePlaybackActiveNow()) {
|
||||
return;
|
||||
@@ -4435,6 +4561,8 @@ const {
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
|
||||
appState.reconnectTimer = timer;
|
||||
},
|
||||
shouldAutoLoadSecondarySubTrack: (path: string) =>
|
||||
shouldAutoLoadSecondarySubTrackForJellyfinPlayback(activeJellyfinRemotePlayback, path),
|
||||
shouldQuitOnMpvShutdown: () =>
|
||||
shouldQuitOnMpvShutdownForTrayState({
|
||||
managedPlayback: appState.initialArgs?.managedPlayback === true,
|
||||
@@ -5088,6 +5216,8 @@ function getUpdateService() {
|
||||
});
|
||||
app.focus({ steal: true });
|
||||
},
|
||||
withStatsWindowLayerSuspended: (showDialog) =>
|
||||
withStatsWindowLayerSuspendedForNativeDialog(showDialog),
|
||||
showMessageBox: (options) => dialog.showMessageBox(options),
|
||||
});
|
||||
updateService = createUpdateService({
|
||||
@@ -5418,6 +5548,19 @@ const shiftSubtitleDelayToAdjacentCueHandler = createShiftSubtitleDelayToAdjacen
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
loadSubtitleSourceText,
|
||||
sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
|
||||
onSubtitleDelayShifted: (delaySeconds) => {
|
||||
const key = activeJellyfinSubtitleDelayKey;
|
||||
if (!key) return;
|
||||
const saved = saveJellyfinSubtitleDelay({
|
||||
filePath: JELLYFIN_SUBTITLE_DELAYS_PATH,
|
||||
itemId: key.itemId,
|
||||
streamIndex: key.streamIndex,
|
||||
delaySeconds,
|
||||
});
|
||||
if (!saved) {
|
||||
logger.warn('Failed to save Jellyfin subtitle delay.');
|
||||
}
|
||||
},
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
});
|
||||
|
||||
@@ -6062,6 +6205,7 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
||||
},
|
||||
buildTrayMenuTemplateDeps: {
|
||||
buildTrayMenuTemplateRuntime,
|
||||
platform: process.platform,
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
openSessionHelpModal: () => openSessionHelpOverlay(),
|
||||
@@ -6077,8 +6221,10 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
||||
isJellyfinConfigured: () =>
|
||||
isJellyfinConfiguredForTrayRuntime(getJellyfinTrayDiscoveryDeps()),
|
||||
isJellyfinDiscoveryActive: () => Boolean(appState.jellyfinRemoteSession),
|
||||
toggleJellyfinDiscovery: () =>
|
||||
toggleJellyfinDiscoveryFromTrayRuntime(getJellyfinTrayDiscoveryDeps()),
|
||||
toggleJellyfinDiscovery: (checked: boolean) =>
|
||||
toggleJellyfinDiscoveryFromTrayRuntime(getJellyfinTrayDiscoveryDeps(), {
|
||||
desiredActive: checked,
|
||||
}),
|
||||
openAnilistSetupWindow: () => openAnilistSetupWindow(),
|
||||
checkForUpdates: () => {
|
||||
void getUpdateService().checkForUpdates({ source: 'manual' });
|
||||
@@ -6307,36 +6453,50 @@ function ensureOverlayWindowsReadyForVisibilityActions(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function notifyMpvPluginVisibleOverlayVisibility(visible: boolean): void {
|
||||
sendMpvCommandRuntime(appState.mpvClient, [
|
||||
'script-message',
|
||||
visible ? 'subminer-visible-overlay-shown' : 'subminer-visible-overlay-hidden',
|
||||
]);
|
||||
}
|
||||
|
||||
function setVisibleOverlayVisible(visible: boolean): void {
|
||||
ensureOverlayWindowsReadyForVisibilityActions();
|
||||
if (!visible) {
|
||||
autoplayReadyGate.markCurrentMediaAutoplayReady();
|
||||
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
|
||||
}
|
||||
if (visible) {
|
||||
void ensureOverlayMpvSubtitlesHidden();
|
||||
}
|
||||
setVisibleOverlayVisibleHandler(visible);
|
||||
notifyMpvPluginVisibleOverlayVisibility(visible);
|
||||
syncOverlayMpvSubtitleSuppression();
|
||||
}
|
||||
|
||||
function toggleVisibleOverlay(): void {
|
||||
ensureOverlayWindowsReadyForVisibilityActions();
|
||||
if (overlayManager.getVisibleOverlayVisible()) {
|
||||
const nextVisible = !overlayManager.getVisibleOverlayVisible();
|
||||
if (!nextVisible) {
|
||||
autoplayReadyGate.markCurrentMediaAutoplayReady();
|
||||
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
|
||||
} else {
|
||||
void ensureOverlayMpvSubtitlesHidden();
|
||||
}
|
||||
toggleVisibleOverlayHandler();
|
||||
notifyMpvPluginVisibleOverlayVisibility(nextVisible);
|
||||
syncOverlayMpvSubtitleSuppression();
|
||||
}
|
||||
function setOverlayVisible(visible: boolean): void {
|
||||
if (!visible) {
|
||||
autoplayReadyGate.markCurrentMediaAutoplayReady();
|
||||
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
|
||||
}
|
||||
if (visible) {
|
||||
void ensureOverlayMpvSubtitlesHidden();
|
||||
}
|
||||
setOverlayVisibleHandler(visible);
|
||||
notifyMpvPluginVisibleOverlayVisibility(visible);
|
||||
syncOverlayMpvSubtitleSuppression();
|
||||
}
|
||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||
|
||||
Reference in New Issue
Block a user