mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-23 05:16:23 -07:00
feat(overlay): add subtitle selection modal and Jellyfin 12 fixes
- Add an optional subtitle selection modal for primary/secondary mpv tracks (subtitleSelection.enabled, g-s sequence shortcut) with key-sequence conflict handling - Authenticate Jellyfin URLs with the ApiKey query, answer remote keep-alives, clear now-playing on stop, and restore episode titles in Anki misc info - Honor the configured mpv executable when Jellyfin starts playback via a shared mpv-process launcher - Bump electron-builder to 26.16.1 - Condense and reconcile changelog fragments; update config example and docs
This commit is contained in:
@@ -59,10 +59,13 @@ function listFiles(root, prefix = '') {
|
||||
});
|
||||
}
|
||||
|
||||
// asar resolves lookups with the platform separator, so stat with the listed
|
||||
// native path and only normalize the reported name.
|
||||
function listAppFiles(archive) {
|
||||
return asar.listPackage(archive).flatMap((entry) => {
|
||||
const name = entry.replaceAll('\\', '/').replace(/^\//, '');
|
||||
const stat = asar.statFile(archive, name);
|
||||
const native = entry.replace(/^[\\/]/, '');
|
||||
const stat = asar.statFile(archive, native);
|
||||
const name = native.replaceAll('\\', '/');
|
||||
return 'size' in stat ? [{ path: name, bytes: stat.size }] : [];
|
||||
});
|
||||
}
|
||||
@@ -137,7 +140,7 @@ function verifyContents(archive, resources, platform, arch) {
|
||||
assert(!name.path.startsWith('minecard'), `Demo media shipped: ${name.path}`);
|
||||
}
|
||||
for (const ui of ['renderer', 'settings', 'syncui']) {
|
||||
const css = asar.extractFile(archive, `dist/${ui}/style.css`).toString();
|
||||
const css = asar.extractFile(archive, path.join('dist', ui, 'style.css')).toString();
|
||||
assert(css.includes('../fonts/MPLUS1[wght].ttf'), `Shared font missing from ${ui} CSS`);
|
||||
}
|
||||
return entries;
|
||||
|
||||
@@ -134,10 +134,12 @@ test('archive inventory handles native files without counting them twice on disk
|
||||
mkdirSync(output);
|
||||
writeFileSync(path.join(input, 'main.js'), 'hello');
|
||||
writeFileSync(path.join(input, 'native.node'), 'native');
|
||||
mkdirSync(path.join(input, 'dist', 'ai'), { recursive: true });
|
||||
writeFileSync(path.join(input, 'dist', 'ai', 'client.js'), 'nested');
|
||||
const archive = path.join(output, 'app.asar');
|
||||
await createPackageFromStreams(
|
||||
archive,
|
||||
['main.js', 'native.node'].map((name) => ({
|
||||
['main.js', 'native.node', 'dist/ai/client.js'].map((name) => ({
|
||||
path: name,
|
||||
type: 'file',
|
||||
unpacked: name.endsWith('.node'),
|
||||
@@ -148,6 +150,7 @@ test('archive inventory handles native files without counting them twice on disk
|
||||
assert.deepEqual(listAppFiles(archive), [
|
||||
{ path: 'main.js', bytes: 5 },
|
||||
{ path: 'native.node', bytes: 6 },
|
||||
{ path: 'dist/ai/client.js', bytes: 6 },
|
||||
]);
|
||||
assert.equal(
|
||||
listFiles(output).reduce((sum: number, entry: { bytes: number }) => sum + entry.bytes, 0),
|
||||
|
||||
@@ -14,7 +14,12 @@ delete env.ELECTRON_RUN_AS_NODE;
|
||||
try {
|
||||
const result = spawnSync(
|
||||
require('electron'),
|
||||
[fileURLToPath(new URL('./smoke-package.cjs', import.meta.url)), path.resolve(resources)],
|
||||
[
|
||||
fileURLToPath(new URL('./smoke-package.cjs', import.meta.url)),
|
||||
path.resolve(resources),
|
||||
// CI runners lack a setuid chrome-sandbox; this harness never loads remote content.
|
||||
...(process.platform === 'linux' ? ['--no-sandbox'] : []),
|
||||
],
|
||||
{ env, stdio: 'inherit', timeout: 75_000 },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Run with the pinned Electron runtime against a finished app's resources folder.
|
||||
const { app, BrowserWindow, session } = require('electron');
|
||||
const fs = require('node:fs');
|
||||
const http = require('node:http');
|
||||
const path = require('node:path');
|
||||
const { createRequire } = require('node:module');
|
||||
const assert = require('node:assert/strict');
|
||||
const { once } = require('node:events');
|
||||
const http = require('node:http');
|
||||
|
||||
const resources = path.resolve(process.argv[2]);
|
||||
const archive = path.join(resources, 'app.asar');
|
||||
@@ -55,10 +55,15 @@ async function smoke() {
|
||||
.extensions.loadExtension(path.join(resources, 'hachidori'), { allowFileAccess: true });
|
||||
assert(hachidori.id, 'Hachidori extension failed to load');
|
||||
const failedRequests = [];
|
||||
session.defaultSession.webRequest.onErrorOccurred({ urls: ['file://*/*'] }, (details) => {
|
||||
if (details.error !== 'net::ERR_ABORTED')
|
||||
failedRequests.push(`${details.url}: ${details.error}`);
|
||||
});
|
||||
session.defaultSession.webRequest.onErrorOccurred(
|
||||
{ urls: ['file://*/*', 'http://127.0.0.1/*'] },
|
||||
(details) => {
|
||||
// Chromium probes the cache before fetching @font-face fonts; an uncached
|
||||
// font reports ERR_CACHE_MISS and is then fetched normally.
|
||||
if (!['net::ERR_ABORTED', 'net::ERR_CACHE_MISS'].includes(details.error))
|
||||
failedRequests.push(`${details.url}: ${details.error}`);
|
||||
},
|
||||
);
|
||||
for (const ui of ['renderer', 'settings', 'syncui']) {
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
@@ -99,6 +104,8 @@ async function smoke() {
|
||||
if (details.statusCode >= 400) failedRequests.push(`${details.url}: ${details.statusCode}`);
|
||||
});
|
||||
await statsWindow.loadURL(url);
|
||||
// Let in-flight font requests settle before the window goes away.
|
||||
await statsWindow.webContents.executeJavaScript('document.fonts.ready.then(() => true)');
|
||||
for (const endpoint of ['overview', 'sessions']) {
|
||||
const response = await fetch(`${url}/api/stats/${endpoint}`);
|
||||
assert.equal(response.status, 200, `Stats ${endpoint} request failed`);
|
||||
|
||||
@@ -69,6 +69,12 @@ local ctx = {
|
||||
return {
|
||||
numericSelectionTimeoutMs = 3000,
|
||||
bindings = {
|
||||
{
|
||||
key = { code = "KeyG-KeyS", modifiers = {} },
|
||||
actionType = "session-action",
|
||||
actionId = "openSubtitleSelection",
|
||||
cliArgs = { "--session-action", '{"actionId":"openSubtitleSelection"}' },
|
||||
},
|
||||
{
|
||||
key = {
|
||||
code = "KeyO",
|
||||
@@ -312,7 +318,8 @@ local ctx = {
|
||||
cliArgs = { "--session-action", '{"actionId":"openFuturePanel"}' },
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
nil
|
||||
end,
|
||||
},
|
||||
state = {
|
||||
@@ -430,17 +437,11 @@ assert_true(play_next_call ~= nil, "play-next binding should invoke CLI action")
|
||||
assert_true(play_next_call[2] == "--play-next-subtitle", "play-next binding should pass CLI flag")
|
||||
|
||||
local character_dictionary_manager = find_binding("Ctrl+d")
|
||||
assert_true(
|
||||
character_dictionary_manager ~= nil,
|
||||
"character dictionary manager binding should be registered"
|
||||
)
|
||||
assert_true(character_dictionary_manager ~= nil, "character dictionary manager binding should be registered")
|
||||
|
||||
character_dictionary_manager.fn()
|
||||
local character_dictionary_manager_call = recorded.async_calls[#recorded.async_calls]
|
||||
assert_true(
|
||||
character_dictionary_manager_call ~= nil,
|
||||
"character dictionary manager binding should invoke CLI action"
|
||||
)
|
||||
assert_true(character_dictionary_manager_call ~= nil, "character dictionary manager binding should invoke CLI action")
|
||||
assert_true(
|
||||
character_dictionary_manager_call[2] == "--session-action",
|
||||
"character dictionary manager binding should use generic session action CLI flag"
|
||||
@@ -474,3 +475,35 @@ assert_true(call[2] == "--mine-sentence-multiple", "CLI action should enter mine
|
||||
assert_true(call[3] == nil, "CLI action should not bind a plugin-side digit count")
|
||||
|
||||
print("plugin session binding regression tests: OK")
|
||||
|
||||
local selector = find_binding("g-s")
|
||||
assert_true(selector ~= nil, "subtitle selection should override mpv g-s with a forced sequence")
|
||||
selector.fn()
|
||||
local selection_call = recorded.async_calls[#recorded.async_calls]
|
||||
assert_true(
|
||||
selection_call[3] == '{"actionId":"openSubtitleSelection"}',
|
||||
"subtitle selection should dispatch its session action"
|
||||
)
|
||||
|
||||
local native_bindings = {}
|
||||
function mp.get_property_native(name)
|
||||
assert_true(name == "input-bindings", "only native input bindings should be queried")
|
||||
return native_bindings
|
||||
end
|
||||
|
||||
for _, case in ipairs({
|
||||
{ key = "g", priority = 1, enabled = false },
|
||||
{ key = "G", priority = 1, enabled = true },
|
||||
{ key = "Shift+g", priority = 1, enabled = true },
|
||||
{ key = "Ctrl+g", priority = 1, enabled = true },
|
||||
{ key = "g", priority = -1, enabled = true },
|
||||
{ key = "g", priority = 1, cmd = "ignore", enabled = true },
|
||||
{ key = "g", priority = 1, cmd = "no-osd ignore", enabled = true },
|
||||
}) do
|
||||
native_bindings = { { key = case.key, cmd = case.cmd or "show-text single", priority = case.priority } }
|
||||
recorded.bindings = {}
|
||||
assert_true(bindings.reload_bindings(), "binding reload should succeed")
|
||||
assert_true((find_binding("g-s") ~= nil) == case.enabled, "sequence prefix conflict: " .. case.key)
|
||||
end
|
||||
assert_true(#recorded.osd > 0, "native prefix conflicts should be visible")
|
||||
print("plugin sequence conflict tests: OK")
|
||||
|
||||
Reference in New Issue
Block a user