mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 07:21:33 -07:00
feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached - Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config - Support searching all sources at once with streaming, per-source results and status - Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese - Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon - Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyPreferenceValue,
|
||||
isSecretPreference,
|
||||
parsePreferences,
|
||||
type SourcePreferenceView,
|
||||
} from './preferences';
|
||||
import type { BridgePreference } from './types';
|
||||
|
||||
// Shapes taken from the real Jellyfin extension's preferencesAnime response.
|
||||
const RAW: BridgePreference[] = [
|
||||
{
|
||||
key: 'host_url',
|
||||
editTextPreference: {
|
||||
title: 'Address',
|
||||
summary: 'The server address',
|
||||
value: '',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
editTextPreference: { title: 'Password', summary: 'The user account password', value: '' },
|
||||
},
|
||||
{
|
||||
key: 'pref_quality',
|
||||
listPreference: {
|
||||
title: 'Preferred quality',
|
||||
summary: 'Preferred quality.',
|
||||
valueIndex: 0,
|
||||
entries: ['Source', '20 Mbps'],
|
||||
entryValues: ['source', '20000000'],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'pref_episode_details_key',
|
||||
multiSelectListPreference: {
|
||||
title: 'Additional details for episodes',
|
||||
values: [],
|
||||
entries: ['Overview', 'Runtime'],
|
||||
entryValues: ['overview', 'runtime'],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'pref_trust_cert',
|
||||
switchPreferenceCompat: { title: 'Trust certificate', value: false },
|
||||
},
|
||||
];
|
||||
|
||||
function view(views: SourcePreferenceView[], key: string): SourcePreferenceView {
|
||||
const found = views.find((candidate) => candidate.key === key);
|
||||
assert.ok(found, `missing preference ${key}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
test('parsePreferences flattens each widget type', () => {
|
||||
const views = parsePreferences(RAW);
|
||||
assert.equal(views.length, 5);
|
||||
|
||||
assert.equal(view(views, 'host_url').kind, 'text');
|
||||
assert.equal(view(views, 'host_url').title, 'Address');
|
||||
assert.equal(view(views, 'host_url').value, '');
|
||||
|
||||
const quality = view(views, 'pref_quality');
|
||||
assert.equal(quality.kind, 'list');
|
||||
// valueIndex 0 resolves through entryValues, not entries.
|
||||
assert.equal(quality.value, 'source');
|
||||
assert.deepEqual(quality.entries, ['Source', '20 Mbps']);
|
||||
|
||||
assert.deepEqual(view(views, 'pref_episode_details_key').value, []);
|
||||
assert.equal(view(views, 'pref_trust_cert').value, false);
|
||||
});
|
||||
|
||||
test('parsePreferences skips the bridge context entry and unknown widgets', () => {
|
||||
const views = parsePreferences([
|
||||
{ key: '__mangatan_bridge_context__', sourceId: '1' },
|
||||
{ key: 'mystery', someFutureWidget: { title: 'X' } },
|
||||
...RAW.slice(0, 1),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
views.map((v) => v.key),
|
||||
['host_url'],
|
||||
);
|
||||
});
|
||||
|
||||
test('a list preference with no selection reads as empty', () => {
|
||||
const views = parsePreferences([
|
||||
{
|
||||
key: 'library_pref',
|
||||
listPreference: {
|
||||
title: 'Select media library',
|
||||
valueIndex: -1,
|
||||
entries: [],
|
||||
entryValues: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.equal(view(views, 'library_pref').value, '');
|
||||
});
|
||||
|
||||
test('applyPreferenceValue writes text into both value and text', () => {
|
||||
const updated = applyPreferenceValue(RAW, 'host_url', 'https://media.example');
|
||||
const body = updated.find((e) => e.key === 'host_url')!.editTextPreference as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(body.value, 'https://media.example');
|
||||
assert.equal(body.text, 'https://media.example');
|
||||
// Other entries are untouched.
|
||||
assert.equal(parsePreferences(updated).length, RAW.length);
|
||||
});
|
||||
|
||||
test('applyPreferenceValue moves a list preference by entry value', () => {
|
||||
const updated = applyPreferenceValue(RAW, 'pref_quality', '20000000');
|
||||
const body = updated.find((e) => e.key === 'pref_quality')!.listPreference as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(body.valueIndex, 1);
|
||||
assert.equal(parsePreferences(updated).find((v) => v.key === 'pref_quality')?.value, '20000000');
|
||||
});
|
||||
|
||||
test('applyPreferenceValue handles multi-select and switch widgets', () => {
|
||||
const multi = applyPreferenceValue(RAW, 'pref_episode_details_key', ['overview']);
|
||||
assert.deepEqual(
|
||||
parsePreferences(multi).find((v) => v.key === 'pref_episode_details_key')?.value,
|
||||
['overview'],
|
||||
);
|
||||
|
||||
const toggled = applyPreferenceValue(RAW, 'pref_trust_cert', true);
|
||||
assert.equal(parsePreferences(toggled).find((v) => v.key === 'pref_trust_cert')?.value, true);
|
||||
});
|
||||
|
||||
test('applyPreferenceValue leaves unknown keys alone', () => {
|
||||
assert.deepEqual(applyPreferenceValue(RAW, 'not-a-key', 'x'), RAW);
|
||||
});
|
||||
|
||||
test('secrets are recognised by key or title', () => {
|
||||
const views = parsePreferences(RAW);
|
||||
assert.equal(isSecretPreference(view(views, 'password')), true);
|
||||
assert.equal(isSecretPreference(view(views, 'host_url')), false);
|
||||
});
|
||||
Reference in New Issue
Block a user