From 30d6741d7ce930a3019031590435ff3a561387ce Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 13 Jul 2026 00:27:00 -0700 Subject: [PATCH] feat(animetosho): swap backend to TsukiHime API (animetosho successor) Animetosho stops updating May 2026; TsukiHime mirrors its index and attachment storage. Internal identifiers, IPC channels, settings keys, and the openAnimetosho shortcut keep the legacy name so user configs stay valid; user-facing labels now say TsukiHime. - API base https://api.tsukihime.org/v1 (/search/torrents, /torrents/{id}) - attachment type is numeric (1 = subtitle); sizes are no longer reported - storage URLs split: native /attach/ vs /tosho/attach/ for imported entries (flagged animetosho: true or id >= 1e9) - download redirect allowlist covers *.tsukihime.org and *.animetosho.org (the /tosho/ mirror currently 302s to storage.animetosho.org) - search results now expose sublangs; modal shows them per release --- README.md | 1 + src/animetosho/utils.test.ts | 200 ++++++++++++------ src/animetosho/utils.ts | Bin 10527 -> 12017 bytes .../definitions/defaults-integrations.ts | 2 +- src/config/definitions/options-core.ts | 2 +- .../definitions/options-integrations.ts | 5 +- src/config/definitions/template-sections.ts | 7 +- src/core/services/anki-jimaku-ipc.test.ts | 12 +- src/core/services/anki-jimaku-ipc.ts | 8 +- src/core/services/anki-jimaku.test.ts | 32 +-- src/core/services/anki-jimaku.ts | 20 +- src/main.ts | 4 +- src/renderer/index.html | 2 +- src/renderer/modals/animetosho.test.ts | 95 ++++++++- src/renderer/modals/animetosho.ts | 6 +- src/renderer/modals/session-help-sections.ts | 4 +- src/types/integrations.ts | 1 + 17 files changed, 286 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 505784f8..2d86385d 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ SubMiner builds on the work of these open-source projects: | [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration | | [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads | | [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui) | Base for the WebSocket texthooker integration | +| [TsukiHime](https://tsukihime.org) | Release-track subtitle search and downloads (Animetosho successor) | | [Yomitan](https://github.com/yomidevs/yomitan) | Dictionary engine powering all lookups and the morphological parser | | [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab) | JLPT level tags for vocabulary | diff --git a/src/animetosho/utils.test.ts b/src/animetosho/utils.test.ts index 3eab9ea0..005548e1 100644 --- a/src/animetosho/utils.test.ts +++ b/src/animetosho/utils.test.ts @@ -10,13 +10,21 @@ import { buildAnimetoshoAttachmentUrl, decompressXzFile, extractAnimetoshoSubtitleFiles, + isAnimetoshoDownloadUrl, mapAnimetoshoSearchResults, } from './utils.js'; test('buildAnimetoshoAttachmentUrl pads the attachment id to 8 hex digits', () => { assert.equal( - buildAnimetoshoAttachmentUrl(1955356), - 'https://animetosho.org/storage/attach/001dd61c/1955356.xz', + buildAnimetoshoAttachmentUrl(96412), + 'https://storage.tsukihime.org/attach/0001789c/96412.xz', + ); +}); + +test('buildAnimetoshoAttachmentUrl uses the tosho mirror path for imported entries', () => { + assert.equal( + buildAnimetoshoAttachmentUrl(2826332, { imported: true }), + 'https://storage.tsukihime.org/tosho/attach/002b205c/2826332.xz', ); }); @@ -28,6 +36,13 @@ test('animetoshoLangToFilenameSuffix maps common ISO 639-2 codes to two-letter s assert.equal(animetoshoLangToFilenameSuffix('POR'), 'pt'); }); +test('animetoshoLangToFilenameSuffix handles TsukiHime BCP-47 style codes', () => { + assert.equal(animetoshoLangToFilenameSuffix('en-US'), 'en'); + assert.equal(animetoshoLangToFilenameSuffix('es-419'), 'es'); + assert.equal(animetoshoLangToFilenameSuffix('zh-Hant'), 'zh'); + assert.equal(animetoshoLangToFilenameSuffix('ja'), 'ja'); +}); + test('animetoshoLangToFilenameSuffix falls back to the raw code, and to en when unknown', () => { assert.equal(animetoshoLangToFilenameSuffix('vie'), 'vie'); assert.equal(animetoshoLangToFilenameSuffix(''), 'en'); @@ -42,59 +57,88 @@ test('buildAnimetoshoAttachmentUrl rejects non-positive and non-integer ids', () assert.equal(buildAnimetoshoAttachmentUrl(Number.NaN), null); }); -test('mapAnimetoshoSearchResults maps valid entries and caps to maxResults', () => { - const payload = [ - { - id: 606713, - title: '[SubsPlease] Sousou no Frieren - 28 (1080p) [8BBBC28C].mkv', - timestamp: 1710000000, - total_size: 1490354395, - num_files: 1, - }, - { id: 'bogus', title: 'missing numeric id' }, - { id: 606714, title: '[Erai-raws] Sousou no Frieren - 28 [1080p].mkv' }, - { id: 606715, title: 'capped away' }, - ]; +test('isAnimetoshoDownloadUrl accepts tsukihime.org and animetosho.org hosts only', () => { + assert.equal(isAnimetoshoDownloadUrl('https://storage.tsukihime.org/attach/0001789c/1.xz'), true); + assert.equal(isAnimetoshoDownloadUrl('https://tsukihime.org/view/1'), true); + // The tosho mirror currently 302s to storage.animetosho.org; the hop must stay allowed. + assert.equal( + isAnimetoshoDownloadUrl('https://storage.animetosho.org/attach/002b205c/2826332.xz'), + true, + ); + assert.equal(isAnimetoshoDownloadUrl('http://storage.tsukihime.org/attach/1/1.xz'), false); + assert.equal(isAnimetoshoDownloadUrl('https://eviltsukihime.org/attach/1/1.xz'), false); + assert.equal(isAnimetoshoDownloadUrl('https://example.com/1.xz'), false); + assert.equal(isAnimetoshoDownloadUrl('not a url'), false); +}); + +test('mapAnimetoshoSearchResults maps valid results and caps to maxResults', () => { + const payload = { + total: 4, + start: 0, + limit: 50, + error: false, + results: [ + { + id: 1000752022, + state: 'completed', + name: '[SubsPlease] Sousou no Frieren S2 - 10 (1080p) [7D35515E].mkv', + totalsize: 1457009569, + filecount: 1, + sublangs: ['en'], + audiolangs: ['ja'], + source_date: 1774623761, + added_date: 1774624861, + animetosho: true, + }, + { id: 'bogus', name: 'missing numeric id' }, + { id: 12255, name: '[DKB] Futsutsuka na Akujo - S01E01 [Multi-Subs]' }, + { id: 12256, name: 'capped away' }, + ], + }; const entries = mapAnimetoshoSearchResults(payload, 2); assert.equal(entries.length, 2); assert.deepEqual(entries[0], { - id: 606713, - title: '[SubsPlease] Sousou no Frieren - 28 (1080p) [8BBBC28C].mkv', - timestamp: 1710000000, - totalSize: 1490354395, + id: 1000752022, + title: '[SubsPlease] Sousou no Frieren S2 - 10 (1080p) [7D35515E].mkv', + timestamp: 1774624861, + totalSize: 1457009569, numFiles: 1, + sublangs: ['en'], }); - assert.equal(entries[1]!.id, 606714); + assert.equal(entries[1]!.id, 12255); assert.equal(entries[1]!.totalSize, null); assert.equal(entries[1]!.numFiles, null); + assert.deepEqual(entries[1]!.sublangs, []); }); -test('mapAnimetoshoSearchResults returns empty list for non-array payloads', () => { +test('mapAnimetoshoSearchResults returns empty list for malformed payloads', () => { assert.deepEqual(mapAnimetoshoSearchResults({ error: 'nope' }, 10), []); + assert.deepEqual(mapAnimetoshoSearchResults({ results: 'nope' }, 10), []); assert.deepEqual(mapAnimetoshoSearchResults(null, 10), []); + assert.deepEqual(mapAnimetoshoSearchResults([], 10), []); }); +// Trimmed from a real /v1/torrents/{id} response (native entry). const DETAIL_PAYLOAD = { - id: 606713, - title: '[SubsPlease] Sousou no Frieren - 28 (1080p) [8BBBC28C].mkv', + id: 12255, + name: '[DKB] Futsutsuka na Akujo dewa Gozaimasu ga - S01E01 [Multi-Subs]', files: [ { - id: 1151711, - filename: '[SubsPlease] Sousou no Frieren - 28 (1080p) [8BBBC28C].mkv', + id: 16179, + filename: '[DKB] Futsutsuka na Akujo dewa Gozaimasu ga - S01E01 [Multi-Subs].mkv', attachments: [ { - id: 1955355, - type: 'font', - info: { name: 'arial.ttf' }, - size: 300000, + id: 96400, + type: 0, + info: { name: 'arial.ttf', mime: 'application/x-truetype-font' }, }, { - id: 1955356, - type: 'subtitle', - info: { codec: 'ASS', lang: 'eng', name: 'English subs', trackid: 2 }, - size: 33075, + id: 96412, + type: 1, + info: { codec: 'ASS', lang: 'en-US', name: 'English subs', trackid: 2, tracknum: 3 }, }, + { id: 96499, type: 3 }, ], }, ], @@ -104,25 +148,64 @@ test('extractAnimetoshoSubtitleFiles keeps only text subtitle attachments with d const files = extractAnimetoshoSubtitleFiles(DETAIL_PAYLOAD); assert.equal(files.length, 1); const file = files[0]!; - assert.equal(file.attachmentId, 1955356); - assert.equal(file.lang, 'eng'); + assert.equal(file.attachmentId, 96412); + assert.equal(file.lang, 'en-us'); assert.equal(file.trackName, 'English subs'); - assert.equal(file.size, 33075); - assert.equal(file.url, 'https://animetosho.org/storage/attach/001dd61c/1955356.xz'); - assert.equal(file.sourceFilename, '[SubsPlease] Sousou no Frieren - 28 (1080p) [8BBBC28C].mkv'); - assert.equal(file.filename, '[SubsPlease] Sousou no Frieren - 28 (1080p) [8BBBC28C].eng.ass'); + assert.equal(file.size, 0); + assert.equal(file.url, 'https://storage.tsukihime.org/attach/0001789c/96412.xz'); + assert.equal( + file.sourceFilename, + '[DKB] Futsutsuka na Akujo dewa Gozaimasu ga - S01E01 [Multi-Subs].mkv', + ); + assert.equal( + file.filename, + '[DKB] Futsutsuka na Akujo dewa Gozaimasu ga - S01E01 [Multi-Subs].en-us.ass', + ); +}); + +test('extractAnimetoshoSubtitleFiles uses the tosho mirror for animetosho-imported entries', () => { + const files = extractAnimetoshoSubtitleFiles({ + id: 1000752022, + animetosho: true, + files: [ + { + id: 1001361385, + filename: '[SubsPlease] Sousou no Frieren S2 - 10 (1080p) [7D35515E].mkv', + attachments: [ + { id: 2826332, type: 1, info: { codec: 'ASS', lang: 'en', name: 'English subs' } }, + ], + }, + ], + }); + assert.equal(files.length, 1); + assert.equal(files[0]!.url, 'https://storage.tsukihime.org/tosho/attach/002b205c/2826332.xz'); +}); + +test('extractAnimetoshoSubtitleFiles detects imported entries by id when the flag is absent', () => { + const files = extractAnimetoshoSubtitleFiles({ + id: 1000752022, + files: [ + { + id: 1001361385, + filename: 'episode.mkv', + attachments: [{ id: 2826332, type: 1, info: { codec: 'ASS', lang: 'en' } }], + }, + ], + }); + assert.equal(files[0]!.url, 'https://storage.tsukihime.org/tosho/attach/002b205c/2826332.xz'); }); test('extractAnimetoshoSubtitleFiles skips image-based subtitle codecs', () => { const files = extractAnimetoshoSubtitleFiles({ + id: 1, files: [ { id: 1, filename: 'movie.mkv', attachments: [ - { id: 10, type: 'subtitle', info: { codec: 'PGS', lang: 'eng' }, size: 100 }, - { id: 11, type: 'subtitle', info: { codec: 'VobSub', lang: 'eng' }, size: 100 }, - { id: 12, type: 'subtitle', info: { codec: 'SRT', lang: 'eng' }, size: 100 }, + { id: 10, type: 1, info: { codec: 'PGS', lang: 'en' } }, + { id: 11, type: 1, info: { codec: 'VobSub', lang: 'en' } }, + { id: 12, type: 1, info: { codec: 'SRT', lang: 'en' } }, ], }, ], @@ -131,34 +214,20 @@ test('extractAnimetoshoSubtitleFiles skips image-based subtitle codecs', () => { files.map((f) => f.attachmentId), [12], ); - assert.equal(files[0]!.filename, 'movie.eng.srt'); + assert.equal(files[0]!.filename, 'movie.en.srt'); }); test('extractAnimetoshoSubtitleFiles sorts English tracks first and disambiguates duplicates', () => { const files = extractAnimetoshoSubtitleFiles({ + id: 1, files: [ { id: 1, filename: 'episode.mkv', attachments: [ - { - id: 21, - type: 'subtitle', - info: { codec: 'ASS', lang: 'ger', name: 'Deutsch' }, - size: 1, - }, - { - id: 22, - type: 'subtitle', - info: { codec: 'ASS', lang: 'eng', name: 'Signs & Songs' }, - size: 2, - }, - { - id: 23, - type: 'subtitle', - info: { codec: 'ASS', lang: 'eng', name: 'Full Subtitles' }, - size: 3, - }, + { id: 21, type: 1, info: { codec: 'ASS', lang: 'de', name: 'Deutsch' } }, + { id: 22, type: 1, info: { codec: 'ASS', lang: 'en', name: 'Signs & Songs' } }, + { id: 23, type: 1, info: { codec: 'ASS', lang: 'en', name: 'Full Subtitles' } }, ], }, ], @@ -168,17 +237,18 @@ test('extractAnimetoshoSubtitleFiles sorts English tracks first and disambiguate files.map((f) => f.attachmentId), [22, 23, 21], ); - assert.equal(files[0]!.filename, 'episode.eng.signs-songs.ass'); - assert.equal(files[1]!.filename, 'episode.eng.full-subtitles.ass'); - assert.equal(files[2]!.filename, 'episode.ger.ass'); + assert.equal(files[0]!.filename, 'episode.en.signs-songs.ass'); + assert.equal(files[1]!.filename, 'episode.en.full-subtitles.ass'); + assert.equal(files[2]!.filename, 'episode.de.ass'); }); test('extractAnimetoshoSubtitleFiles tolerates missing info fields', () => { const files = extractAnimetoshoSubtitleFiles({ + id: 1, files: [ { id: 1, - attachments: [{ id: 31, type: 'subtitle', info: { codec: 'ASS' }, size: 5 }], + attachments: [{ id: 31, type: 1, info: { codec: 'ASS' } }], }, ], }); diff --git a/src/animetosho/utils.ts b/src/animetosho/utils.ts index 3368d9926e5706027c2ae370dd6b931417da212e..08b1d05c0b47080415d0a84c87ec30f9b61828c5 100644 GIT binary patch delta 2023 zcmah~O=x3P6sDazCYjElt5#vknNI0TnDnL70T&t5lD4T0Hcd%WM8=xlymyma`f~5{ z-utEvr34Y&>E`X+41x=HDiK#M1b2eE61TI|r8^N3&wcqV<6s~p=iPh0bI$jjbKaB6 zAID$rPjxdH*d)#=7=Xg^K)e?}Dl(;K`dmU&<(S{P~z}o8CYbhIwTvN%32h62iu33Xoso<1S zKZ5sYlQbY}fMp$Wpa@xLd9jw`)6l<#GjSx?fveT^)XVFx{OtZB>0H z5IBL9m=E^EgVi;KO#?$Z#!8z;LrDeHg>EGPKZs?LQP2*MW~H%{i06qj!;fh_j3cSb zBy}`i^lfzN!a$V4#ofq12al{PDq3loWa;Ahy?VKH zF%E=Ai=OvZu75QZ{xqAuE~yGU9sV;r7sg|`nBBKkxD)%}8Wtb^7<()H;^vFppzdKm zD$Fx4CF$5qMZZa=Y1>wV)6{^<4Y=~yZZHq`Lo#U~QduxRfK9G&H1HoXt~)*zjsAgQ z+v*=#L%{4ccyt^t#A^xSy40(9OONISvt@`#s!8ALLY9ru|Q4aGe4n}o6pk|0WNuP8)L2#*7Fr0K> z@8_7tAw1>cjIU#5Lzyp-2-bkerHelHxFx~Vr)5A>L-CAZwCwi;PJRxE+F`hk>3-0z zBXNVsHgi)GqT|gwh{XfXgXLwjGS+kO>|7_tgB`v%``OBkCwYpQqSrZ+YJJnnk$ z-}-*)R(hh<;j*{=(z`LUEVcx9P$rIF+|f$o%L7njPR{^WaS~rozF=IZp-vPvMcYi#rdfTE#7&*T>qHwMWKjt(h_pKsf#-rN#VdZaM@T(7q6RJs zhHr-_^K%byTn2M$o8U*%!@l=D;?QK~oy@(Z3@#BXg`+a`yFBUrJpcDheDpdEf4TkZ z*y??=@Z0p*3_ri)O;`(yjS1`H#ZM=!Z|_>+$=%A8Cx4Bt-gisu6TrQgH8=e)7Rs_S delta 622 zcmewuJ3nZHALC?SCcAn^KTluR5dUBg|9Cf7SC@Dv$6(j^&>$ZLTLtxul9GaAD}DX6 z)YKHc#JtSh)RO$-jC{TPqI7j@uGETx{Gt+tIWDi%aqnb5j*; zZGlchm;iQ-LQM_2I=$4ql;ZHrk_-)Xbhl4#VGk97*p-`_SK^ssrI1&eo0M8KS&U)nfywYi$A}hBoi0|dP#}Jsi8$Vn^(#GVc~Lw z$JONZ%GZ&&oBLH>Fd^|LH>l?zbHA#mA#+nS79w*eXsS;()eJ%v`h~>ZJXy;Q+4#-3 Hbi|ne_K4or diff --git a/src/config/definitions/defaults-integrations.ts b/src/config/definitions/defaults-integrations.ts index 6e566bee..9abe2374 100644 --- a/src/config/definitions/defaults-integrations.ts +++ b/src/config/definitions/defaults-integrations.ts @@ -98,7 +98,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick< maxEntryResults: 10, }, animetosho: { - apiBaseUrl: 'https://feed.animetosho.org', + apiBaseUrl: 'https://api.tsukihime.org/v1', maxSearchResults: 10, }, mpv: { diff --git a/src/config/definitions/options-core.ts b/src/config/definitions/options-core.ts index d2858918..057c4f18 100644 --- a/src/config/definitions/options-core.ts +++ b/src/config/definitions/options-core.ts @@ -620,7 +620,7 @@ export function buildCoreConfigOptionRegistry( kind: 'string', defaultValue: defaultConfig.shortcuts.openAnimetosho, description: - 'Accelerator that opens the Animetosho subtitle search modal (English/Japanese tabs).', + 'Accelerator that opens the TsukiHime subtitle search modal (English/Japanese tabs).', }, { path: 'shortcuts.openSessionHelp', diff --git a/src/config/definitions/options-integrations.ts b/src/config/definitions/options-integrations.ts index eeed95a3..39e99f8f 100644 --- a/src/config/definitions/options-integrations.ts +++ b/src/config/definitions/options-integrations.ts @@ -404,13 +404,14 @@ export function buildIntegrationConfigOptionRegistry( path: 'animetosho.apiBaseUrl', kind: 'string', defaultValue: defaultConfig.animetosho.apiBaseUrl, - description: 'Base URL of the Animetosho JSON feed API. No API key required.', + description: + 'Base URL of the TsukiHime API (Animetosho successor). No API key required.', }, { path: 'animetosho.maxSearchResults', kind: 'number', defaultValue: defaultConfig.animetosho.maxSearchResults, - description: 'Maximum Animetosho search results returned.', + description: 'Maximum TsukiHime search results returned.', }, { path: 'anilist.enabled', diff --git a/src/config/definitions/template-sections.ts b/src/config/definitions/template-sections.ts index fcc07845..521c7305 100644 --- a/src/config/definitions/template-sections.ts +++ b/src/config/definitions/template-sections.ts @@ -148,11 +148,12 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [ key: 'jimaku', }, { - title: 'Animetosho', + title: 'TsukiHime', description: [ - 'Animetosho subtitle search configuration (English and Japanese). No API key required.', + 'TsukiHime subtitle search configuration (English and Japanese). No API key required.', + 'TsukiHime is the successor to Animetosho; the config key keeps the legacy name.', ], - notes: ['Hot-reload: Animetosho changes apply to the next Animetosho request.'], + notes: ['Hot-reload: TsukiHime changes apply to the next TsukiHime request.'], key: 'animetosho', }, { diff --git a/src/core/services/anki-jimaku-ipc.test.ts b/src/core/services/anki-jimaku-ipc.test.ts index e9a555e3..4cb30a0e 100644 --- a/src/core/services/anki-jimaku-ipc.test.ts +++ b/src/core/services/anki-jimaku-ipc.test.ts @@ -103,7 +103,7 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => { const invalidAnimetoshoSearch = await animetoshoSearchHandler!({}, { query: 12 }); assert.deepEqual(invalidAnimetoshoSearch, { ok: false, - error: { error: 'Invalid Animetosho search query payload', code: 400 }, + error: { error: 'Invalid TsukiHime search query payload', code: 400 }, }); const animetoshoFilesHandler = handleHandlers.get(IPC_CHANNELS.request.animetoshoListFiles); @@ -111,7 +111,7 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => { const invalidAnimetoshoFiles = await animetoshoFilesHandler!({}, { entryId: 'x' }); assert.deepEqual(invalidAnimetoshoFiles, { ok: false, - error: { error: 'Invalid Animetosho files query payload', code: 400 }, + error: { error: 'Invalid TsukiHime files query payload', code: 400 }, }); const animetoshoDownloadHandler = handleHandlers.get(IPC_CHANNELS.request.animetoshoDownloadFile); @@ -119,7 +119,7 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => { const invalidAnimetoshoDownload = await animetoshoDownloadHandler!({}, { entryId: 1, url: '/x' }); assert.deepEqual(invalidAnimetoshoDownload, { ok: false, - error: { error: 'Invalid Animetosho download query payload', code: 400 }, + error: { error: 'Invalid TsukiHime download query payload', code: 400 }, }); const foreignUrlDownload = await animetoshoDownloadHandler!( @@ -128,7 +128,7 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => { ); assert.deepEqual(foreignUrlDownload, { ok: false, - error: { error: 'Refusing to download subtitle from a non-Animetosho URL.', code: 400 }, + error: { error: 'Refusing to download subtitle from a non-TsukiHime URL.', code: 400 }, }); }); @@ -177,7 +177,7 @@ test('animetosho downloads route by language: secondary for eng, primary for jpn {}, { entryId: 1, - url: 'https://animetosho.org/storage/attach/00000001/1.xz', + url: 'https://storage.tsukihime.org/attach/00000001/1.xz', name: 'episode.eng.ass', lang: 'eng', }, @@ -191,7 +191,7 @@ test('animetosho downloads route by language: secondary for eng, primary for jpn {}, { entryId: 1, - url: 'https://animetosho.org/storage/attach/00000002/2.xz', + url: 'https://storage.tsukihime.org/attach/00000002/2.xz', name: 'episode.jpn.ass', lang: 'jpn', }, diff --git a/src/core/services/anki-jimaku-ipc.ts b/src/core/services/anki-jimaku-ipc.ts index e06b2286..294128da 100644 --- a/src/core/services/anki-jimaku-ipc.ts +++ b/src/core/services/anki-jimaku-ipc.ts @@ -217,7 +217,7 @@ export function registerAnkiJimakuIpcHandlers( if (!parsedQuery) { return { ok: false, - error: { error: 'Invalid Animetosho search query payload', code: 400 }, + error: { error: 'Invalid TsukiHime search query payload', code: 400 }, }; } return deps.searchAnimetoshoEntries(parsedQuery); @@ -229,7 +229,7 @@ export function registerAnkiJimakuIpcHandlers( async (_event, query: unknown): Promise> => { const parsedQuery = parseAnimetoshoFilesQuery(query); if (!parsedQuery) { - return { ok: false, error: { error: 'Invalid Animetosho files query payload', code: 400 } }; + return { ok: false, error: { error: 'Invalid TsukiHime files query payload', code: 400 } }; } return deps.listAnimetoshoFiles(parsedQuery); }, @@ -242,14 +242,14 @@ export function registerAnkiJimakuIpcHandlers( if (!parsedQuery) { return { ok: false, - error: { error: 'Invalid Animetosho download query payload', code: 400 }, + error: { error: 'Invalid TsukiHime download query payload', code: 400 }, }; } if (!isAnimetoshoDownloadUrl(parsedQuery.url)) { return { ok: false, - error: { error: 'Refusing to download subtitle from a non-Animetosho URL.', code: 400 }, + error: { error: 'Refusing to download subtitle from a non-TsukiHime URL.', code: 400 }, }; } diff --git a/src/core/services/anki-jimaku.test.ts b/src/core/services/anki-jimaku.test.ts index f1553109..e5980c99 100644 --- a/src/core/services/anki-jimaku.test.ts +++ b/src/core/services/anki-jimaku.test.ts @@ -44,10 +44,11 @@ function createHarness(): RuntimeHarness { endpoint, query: query as Record, }); - if ((query as Record)?.show === 'torrent') { + if (endpoint.startsWith('/torrents/')) { return { ok: true, data: { + id: 606713, files: [ { id: 9, @@ -55,9 +56,8 @@ function createHarness(): RuntimeHarness { attachments: [ { id: 1955356, - type: 'subtitle', - info: { codec: 'ASS', lang: 'eng', name: 'English subs' }, - size: 33075, + type: 1, + info: { codec: 'ASS', lang: 'en', name: 'English subs' }, }, ], }, @@ -67,11 +67,13 @@ function createHarness(): RuntimeHarness { } return { ok: true, - data: [ - { id: 1, title: 'release a' }, - { id: 2, title: 'release b' }, - { id: 3, title: 'release c' }, - ] as never, + data: { + results: [ + { id: 1, name: 'release a' }, + { id: 2, name: 'release b' }, + { id: 3, name: 'release c' }, + ], + } as never, }; }, getSubtitleTimingTracker: () => null, @@ -371,8 +373,8 @@ test('searchAnimetoshoEntries caps results using animetosho.maxSearchResults', a const searchResult = await registered.searchAnimetoshoEntries!({ query: 'frieren 28' }); assert.deepEqual(state.animetoshoFetchCalls, [ { - endpoint: '/json', - query: { q: 'frieren 28', qx: 1 }, + endpoint: '/search/torrents', + query: { q: 'frieren 28', limit: 2 }, }, ]); assert.equal((searchResult as { ok: boolean }).ok, true); @@ -390,14 +392,14 @@ test('listAnimetoshoFiles extracts subtitle attachments from torrent detail', as const filesResult = await registered.listAnimetoshoFiles!({ entryId: 606713 }); assert.deepEqual(state.animetoshoFetchCalls, [ { - endpoint: '/json', - query: { show: 'torrent', id: 606713 }, + endpoint: '/torrents/606713', + query: {}, }, ]); assert.equal((filesResult as { ok: boolean }).ok, true); const files = (filesResult as { data: Array> }).data; assert.equal(files.length, 1); assert.equal(files[0]!.attachmentId, 1955356); - assert.equal(files[0]!.filename, 'episode.eng.ass'); - assert.equal(files[0]!.url, 'https://animetosho.org/storage/attach/001dd61c/1955356.xz'); + assert.equal(files[0]!.filename, 'episode.en.ass'); + assert.equal(files[0]!.url, 'https://storage.tsukihime.org/attach/001dd61c/1955356.xz'); }); diff --git a/src/core/services/anki-jimaku.ts b/src/core/services/anki-jimaku.ts index fe427459..0c35b714 100644 --- a/src/core/services/anki-jimaku.ts +++ b/src/core/services/anki-jimaku.ts @@ -17,7 +17,7 @@ import { } from '../../types'; import { sortJimakuFiles } from '../../jimaku/utils'; import { - ANIMETOSHO_FEED_BASE_URL, + TSUKIHIME_API_BASE_URL, animetoshoFetchJson as animetoshoFetchJsonRequest, decompressXzFile, extractAnimetoshoSubtitleFiles, @@ -125,7 +125,7 @@ function animetoshoFetch( if (options.animetoshoFetchJson) { return options.animetoshoFetchJson(endpoint, query); } - const baseUrl = options.getResolvedConfig().animetosho?.apiBaseUrl || ANIMETOSHO_FEED_BASE_URL; + const baseUrl = options.getResolvedConfig().animetosho?.apiBaseUrl || TSUKIHIME_API_BASE_URL; return animetoshoFetchJsonRequest(endpoint, query, { baseUrl }); } @@ -244,22 +244,24 @@ export function registerAnkiJimakuIpcRuntime( searchAnimetoshoEntries: async (query) => { logger.info(`[animetosho] search-entries query: "${query.query}"`); - const response = await animetoshoFetch(options, '/json', { + const maxResults = getAnimetoshoMaxSearchResults(options); + const response = await animetoshoFetch(options, '/search/torrents', { q: query.query, - qx: 1, + // The API caps limit at 100. + limit: Math.min(maxResults, 100), }); if (!response.ok) return response; - const maxResults = getAnimetoshoMaxSearchResults(options); const entries = mapAnimetoshoSearchResults(response.data, maxResults); logger.info(`[animetosho] search-entries returned ${entries.length} results`); return { ok: true, data: entries }; }, listAnimetoshoFiles: async (query) => { logger.info(`[animetosho] list-files entryId=${query.entryId}`); - const response = await animetoshoFetch(options, '/json', { - show: 'torrent', - id: query.entryId, - }); + const response = await animetoshoFetch( + options, + `/torrents/${encodeURIComponent(query.entryId)}`, + {}, + ); if (!response.ok) return response; const files = extractAnimetoshoSubtitleFiles(response.data); logger.info(`[animetosho] list-files returned ${files.length} subtitle attachments`); diff --git a/src/main.ts b/src/main.ts index e937b454..8f8b8173 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2928,8 +2928,8 @@ function openJimakuOverlay(): void { function openAnimetoshoOverlay(): void { openOverlayHostedModalWithOsd( openAnimetoshoModalRuntime, - 'Animetosho overlay unavailable.', - 'Failed to open Animetosho overlay.', + 'TsukiHime overlay unavailable.', + 'Failed to open TsukiHime overlay.', ); } diff --git a/src/renderer/index.html b/src/renderer/index.html index 3049f4c0..6961d96d 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -119,7 +119,7 @@