mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-21 17:16:20 -07:00
fix(anime): keep browser state in sync and alias HLS segments
- Toggle the player Anime Browser without losing its state - Share active playback state and support rotating fake segment extensions
This commit is contained in:
@@ -12,6 +12,7 @@ export interface AnimeBrowserIpcDeps {
|
||||
handle(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): unknown;
|
||||
};
|
||||
runtime: AnimeBrowserRuntime;
|
||||
getPlaybackState?: () => unknown;
|
||||
registerSession?: (sessionId: string, sender: AnimeBrowserIpcSender) => void;
|
||||
}
|
||||
|
||||
@@ -97,6 +98,7 @@ export function registerAnimeBrowserIpcHandlers(deps: AnimeBrowserIpcDeps): void
|
||||
);
|
||||
handle(channels.animeBrowserClearQueue, () => runtime.clearQueue());
|
||||
handle(channels.animeBrowserGetQueue, () => runtime.getQueue());
|
||||
handle(channels.animeBrowserGetPlaybackState, () => deps.getPlaybackState?.() ?? null);
|
||||
handle(channels.animeBrowserIsPlaying, () => runtime.isPlaying());
|
||||
handle(channels.animeBrowserGetPreferences, (_event, sourceId) =>
|
||||
runtime.getPreferences(String(sourceId)),
|
||||
|
||||
@@ -19,6 +19,7 @@ test('anime browser open uses a dedicated player-bounded modal window', async ()
|
||||
return true;
|
||||
},
|
||||
waitForModalOpen: async () => true,
|
||||
isModalOpen: () => false,
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
@@ -36,9 +37,35 @@ test('anime browser open retries on a fresh modal window after a missed acknowle
|
||||
attempts += 1;
|
||||
return attempts === 2;
|
||||
},
|
||||
isModalOpen: () => false,
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
assert.equal(opened, true);
|
||||
assert.equal(attempts, 2);
|
||||
});
|
||||
|
||||
test('anime browser shortcut closes the open modal without waiting for another open', async () => {
|
||||
const calls: string[] = [];
|
||||
const toggled = await openAnimeBrowserModal({
|
||||
ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
|
||||
ensureOverlayWindowsReadyForVisibilityActions: () => calls.push('windows'),
|
||||
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) => {
|
||||
calls.push(channel);
|
||||
assert.equal(payload, undefined);
|
||||
assert.deepEqual(runtimeOptions, {
|
||||
restoreOnModalClose: 'anime-browser',
|
||||
preferModalWindow: true,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
waitForModalOpen: async () => {
|
||||
assert.fail('closing must not wait for an open acknowledgement');
|
||||
},
|
||||
isModalOpen: (modal) => modal === 'anime-browser',
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
assert.equal(toggled, true);
|
||||
assert.deepEqual(calls, [IPC_CHANNELS.event.animeBrowserClose]);
|
||||
});
|
||||
|
||||
@@ -17,8 +17,16 @@ export async function openAnimeBrowserModal(deps: {
|
||||
},
|
||||
) => boolean;
|
||||
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||
isModalOpen: (modal: OverlayHostedModal) => boolean;
|
||||
logWarn: (message: string) => void;
|
||||
}): Promise<boolean> {
|
||||
if (deps.isModalOpen(ANIME_BROWSER_MODAL)) {
|
||||
return deps.sendToActiveOverlayWindow(IPC_CHANNELS.event.animeBrowserClose, undefined, {
|
||||
restoreOnModalClose: ANIME_BROWSER_MODAL,
|
||||
preferModalWindow: true,
|
||||
});
|
||||
}
|
||||
|
||||
return await retryOverlayModalOpen(
|
||||
{
|
||||
waitForModalOpen: deps.waitForModalOpen,
|
||||
|
||||
@@ -25,6 +25,9 @@ function prepared(input: AnimeBrowserPlayRequest): PreparedAnimeBrowserPlayback
|
||||
request: input,
|
||||
stream: { url: mediaPath, quality: '1080p', headers: {}, audios: [], subtitles: [] },
|
||||
metadata: {
|
||||
sourceId: input.sourceId,
|
||||
animeUrl: input.animeUrl,
|
||||
episodeUrl: input.episodeUrl,
|
||||
mediaPath,
|
||||
statsPath: `animebrowser://${encodeURIComponent(input.episodeUrl)}`,
|
||||
seriesTitle: input.animeTitle,
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
createStreamPlaybackMetadataStore,
|
||||
matchRequestedStreamPlaybackMetadata,
|
||||
toAnimeBrowserPlaybackState,
|
||||
toAnilistMediaGuess,
|
||||
toJimakuMediaInfo,
|
||||
} from './stream-playback-metadata';
|
||||
@@ -10,6 +11,9 @@ import type { AnimeStreamMetadata } from '../../anime-bridge/episode-metadata';
|
||||
|
||||
function metadata(overrides: Partial<AnimeStreamMetadata> = {}): AnimeStreamMetadata {
|
||||
return {
|
||||
sourceId: '9001',
|
||||
animeUrl: '/anime/mushoku',
|
||||
episodeUrl: '/watch/ep-4',
|
||||
mediaPath: 'http://127.0.0.1:41234/video/abc.m3u8',
|
||||
statsPath: 'animebrowser://9001/%2Fanime%2Fmushoku/%2Fwatch%2Fep-4',
|
||||
seriesTitle: 'Mushoku Tensei: Jobless Reincarnation',
|
||||
@@ -21,6 +25,15 @@ function metadata(overrides: Partial<AnimeStreamMetadata> = {}): AnimeStreamMeta
|
||||
};
|
||||
}
|
||||
|
||||
test('browser playback state keeps the source episode identity and drops other media', () => {
|
||||
assert.deepEqual(toAnimeBrowserPlaybackState(metadata()), {
|
||||
sourceId: '9001',
|
||||
animeUrl: '/anime/mushoku',
|
||||
episodeUrl: '/watch/ep-4',
|
||||
});
|
||||
assert.equal(toAnimeBrowserPlaybackState(null), null);
|
||||
});
|
||||
|
||||
test('the store answers for the stream URL and for the stats path', () => {
|
||||
const store = createStreamPlaybackMetadataStore();
|
||||
const current = metadata();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AnimeStreamMetadata } from '../../anime-bridge/episode-metadata';
|
||||
import type { AnilistMediaGuess } from '../../core/services/anilist/anilist-updater';
|
||||
import type { JimakuMediaInfo } from '../../types';
|
||||
import type { AnimeBrowserPlaybackState } from '../../types/anime-browser';
|
||||
|
||||
/**
|
||||
* Holds what the anime browser resolved for active and queued streams.
|
||||
@@ -53,6 +54,17 @@ export function matchRequestedStreamPlaybackMetadata(
|
||||
return store.match(requestedMediaPath ?? currentMediaPath);
|
||||
}
|
||||
|
||||
export function toAnimeBrowserPlaybackState(
|
||||
metadata: AnimeStreamMetadata | null,
|
||||
): AnimeBrowserPlaybackState | null {
|
||||
if (!metadata) return null;
|
||||
return {
|
||||
sourceId: metadata.sourceId,
|
||||
animeUrl: metadata.animeUrl,
|
||||
episodeUrl: metadata.episodeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/** AniList counts whole episodes, so a special numbered 6.5 cannot drive it. */
|
||||
function wholeEpisode(episode: number | null): number | null {
|
||||
return typeof episode === 'number' && Number.isInteger(episode) && episode > 0 ? episode : null;
|
||||
|
||||
Reference in New Issue
Block a user