feat: inject bundled mpv plugin for managed launches, remove legacy glob (#62)

* feat: inject bundled mpv plugin for managed launches, remove legacy glob

- SubMiner-managed launcher and Windows shortcut launches inject the bundled plugin when no global plugin is detected
- First-run setup detects and removes legacy global plugin files via OS trash before managed playback starts
- Makefile `install-plugin` target and Windows config-rewrite script removed; Linux/macOS install now copies plugin to app data dir
- AniList stats search and post-watch tracking now go through the shared rate limiter
- Stats cover-art lookup reuses cached AniList data before issuing a new request
- Closing mpv in a launcher-managed session now terminates the background Electron app

* harden bootstrap version load and clean plugin on uninstall

- Use pcall for version.lua in bootstrap.lua so missing version module does not crash plugin startup
- Remove plugin/subminer from app-data dirs in uninstall-linux and uninstall-macos targets
- Add Lua compat test asserting bootstrap uses defensive pcall for version load
- Add release-workflow test asserting uninstall targets clean bundled plugin dirs
- Delete completed planning document
This commit is contained in:
2026-05-12 23:11:19 -07:00
committed by GitHub
parent e5c1135501
commit 7c9b65db8b
43 changed files with 2116 additions and 481 deletions
@@ -1025,6 +1025,46 @@ describe('stats server API routes', () => {
assert.equal(res.status, 400);
});
it('GET /api/stats/anilist/search uses the configured AniList rate limiter', async () => {
const originalFetch = globalThis.fetch;
let acquireCalls = 0;
let recordCalls = 0;
globalThis.fetch = (async () =>
new Response(
JSON.stringify({
data: {
Page: {
media: [{ id: 21858, title: { romaji: 'Little Witch Academia' } }],
},
},
}),
{
status: 200,
headers: { 'Content-Type': 'application/json', 'X-RateLimit-Remaining': '29' },
},
)) as typeof fetch;
try {
const app = createStatsApp(createMockTracker(), {
anilistRateLimiter: {
acquire: async () => {
acquireCalls += 1;
},
recordResponse: () => {
recordCalls += 1;
},
},
});
const res = await app.request('/api/stats/anilist/search?q=Little%20Witch%20Academia');
assert.equal(res.status, 200);
assert.equal(acquireCalls, 1);
assert.equal(recordCalls, 1);
} finally {
globalThis.fetch = originalFetch;
}
});
it('POST /api/stats/anki/notesInfo resolves stale note ids through the configured alias resolver', async () => {
const originalFetch = globalThis.fetch;
const requests: unknown[] = [];