mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(server): handle Prowlarr torznab endpoint responses
Preserve Prowlarr's REST feed route and accept diagnostic responses so a server URL reports a useful Torznab mismatch instead of an opaque 406. Fixes #1244
This commit is contained in:
@@ -165,6 +165,16 @@ describe('TorznabAdapter', () => {
|
||||
expect(lastUrl().pathname).toBe('/api');
|
||||
});
|
||||
|
||||
it('uses Prowlarr REST Torznab feed URLs without appending /api', async () => {
|
||||
fetchMock.mockResolvedValue(new Response('<caps><server title="Prowlarr" /></caps>'));
|
||||
|
||||
await expect(new TorznabAdapter().test(config({ baseUrl: 'http://127.0.0.1:9696/api/v1/indexer/7/newznab' }))).resolves.toEqual({
|
||||
success: true,
|
||||
indexerName: 'Prowlarr',
|
||||
});
|
||||
expect(lastUrl().pathname).toBe('/api/v1/indexer/7/newznab');
|
||||
});
|
||||
|
||||
it('reads seeders, size, infohash and freeleech off the torznab attributes', async () => {
|
||||
fetchMock.mockResolvedValue(new Response(feed(ITEM)));
|
||||
|
||||
@@ -231,6 +241,17 @@ describe('TorznabAdapter', () => {
|
||||
await expect(new TorznabAdapter().test(config())).resolves.toMatchObject({ success: false });
|
||||
});
|
||||
|
||||
it('reports a Prowlarr server URL as a non-torznab response instead of an opaque 406', async () => {
|
||||
fetchMock.mockResolvedValue(new Response(JSON.stringify({ current: 'v1', deprecated: [] }), { headers: { 'content-type': 'application/json' } }));
|
||||
|
||||
await expect(new TorznabAdapter().test(config({ name: 'Prowlarr', baseUrl: 'http://127.0.0.1:9696' }))).resolves.toEqual({
|
||||
success: false,
|
||||
error: 'That URL answered, but not with a torznab capabilities document',
|
||||
});
|
||||
expect(lastUrl().pathname).toBe('/api');
|
||||
expect(new Headers(fetchMock.mock.calls.at(-1)![1].headers).get('accept')).toBe('application/rss+xml, application/xml, text/xml, */*;q=0.1');
|
||||
});
|
||||
|
||||
/**
|
||||
* Real Prowlarr output. `booktitle` is the bare work title where the release name is decorated,
|
||||
* and the seed figures are the tracker's own requirements passed through.
|
||||
|
||||
@@ -35,6 +35,7 @@ const MAX_FEED_FIELD_CHARS = MAX_INDEXER_RELEASE_GUID_LENGTH;
|
||||
const REQUEST_TIMEOUT_MS = 25_000;
|
||||
/** The same ceiling the direct fetcher uses, and for the same reason: a chain, not a loop. */
|
||||
const MAX_REDIRECTS = 5;
|
||||
const XML_ACCEPT = 'application/rss+xml, application/xml, text/xml, */*;q=0.1';
|
||||
|
||||
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', textNodeName: '#text' });
|
||||
|
||||
@@ -162,7 +163,7 @@ export class TorznabAdapter implements IndexerAdapter {
|
||||
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
try {
|
||||
response = await safeFetch(current.href, { signal, redirect: 'manual', headers: { Accept: 'application/xml, text/xml' } }, options);
|
||||
response = await safeFetch(current.href, { signal, redirect: 'manual', headers: { Accept: XML_ACCEPT } }, options);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
||||
throw new IndexerSearchException('timeout', `${config.name} did not answer in time`);
|
||||
@@ -200,7 +201,8 @@ export class TorznabAdapter implements IndexerAdapter {
|
||||
/**
|
||||
* `new URL('/api', base)` throws away any path the base carries, which breaks every Jackett
|
||||
* deployment (its endpoints live under `/api/v2.0/indexers/<id>/results/torznab`). Join under
|
||||
* the base's own path instead, and accept a base that already ends in `/api`.
|
||||
* the base's own path instead, and accept either the conventional endpoint ending in `/api` or
|
||||
* Prowlarr's REST-shaped `/api/v1/indexer/<id>/newznab` endpoint as complete.
|
||||
*
|
||||
* Anything the base's own query string carries is kept underneath what this call asks for. Some
|
||||
* proxies hand the operator a base with the key already in it (`.../api?apikey=...`), and
|
||||
@@ -212,7 +214,8 @@ export class TorznabAdapter implements IndexerAdapter {
|
||||
const base = await ensureSafeUrl(config.baseUrl, { allowPrivate: config.allowPrivateAddress });
|
||||
const prefix = base.pathname.replace(/\/+$/, '');
|
||||
const target = new URL(base.href);
|
||||
target.pathname = prefix.endsWith('/api') ? prefix : `${prefix}/api`;
|
||||
const completeEndpoint = prefix.endsWith('/api') || /\/api\/v\d+\/indexer\/\d+\/newznab$/i.test(prefix);
|
||||
target.pathname = completeEndpoint ? prefix : `${prefix}/api`;
|
||||
|
||||
const merged = new URLSearchParams(base.search);
|
||||
for (const key of new Set(params.keys())) merged.delete(key);
|
||||
|
||||
Reference in New Issue
Block a user