From d71d53cc273a29c449edcc63b470f1ac991652fc Mon Sep 17 00:00:00 2001 From: Viren070 Date: Sat, 8 Nov 2025 00:08:23 +0000 Subject: [PATCH] feat(debrid): don't make proxy required and add public URL field for nzbdav/altmount --- packages/core/src/builtins/base/debrid.ts | 260 +++++++++--------- packages/core/src/debrid/altmount.ts | 14 +- packages/core/src/debrid/nzbdav.ts | 14 +- .../core/src/debrid/usenet-stream-base.ts | 27 +- packages/core/src/parser/streams.ts | 6 +- packages/core/src/presets/builtin.ts | 2 + packages/core/src/utils/constants.ts | 52 ++-- 7 files changed, 185 insertions(+), 190 deletions(-) diff --git a/packages/core/src/builtins/base/debrid.ts b/packages/core/src/builtins/base/debrid.ts index 5af265a3..4aa9721e 100644 --- a/packages/core/src/builtins/base/debrid.ts +++ b/packages/core/src/builtins/base/debrid.ts @@ -273,153 +273,153 @@ export abstract class BaseDebridAddon { const results = [...processedTorrents.results, ...processedNzbs.results]; - // Setup auth for both NzbDAV and Altmount - let nzbdavAuth; - let altmountAuth; + // // Setup auth for both NzbDAV and Altmount + // let nzbdavAuth; + // let altmountAuth; - const encodedNzbdavAuth = this.userData.services.find( - (s) => s.id === 'nzbdav' - )?.credential; - const encodedAltmountAuth = this.userData.services.find( - (s) => s.id === 'altmount' - )?.credential; + // const encodedNzbdavAuth = this.userData.services.find( + // (s) => s.id === 'nzbdav' + // )?.credential; + // const encodedAltmountAuth = this.userData.services.find( + // (s) => s.id === 'altmount' + // )?.credential; - if (encodedNzbdavAuth) { - const { success, data } = NzbDavConfig.safeParse( - JSON.parse(fromUrlSafeBase64(encodedNzbdavAuth)) - ); - if (success) { - nzbdavAuth = data; - } - } + // if (encodedNzbdavAuth) { + // const { success, data } = NzbDavConfig.safeParse( + // JSON.parse(fromUrlSafeBase64(encodedNzbdavAuth)) + // ); + // if (success) { + // nzbdavAuth = data; + // } + // } - if (encodedAltmountAuth) { - const { success, data } = AltmountConfig.safeParse( - JSON.parse(fromUrlSafeBase64(encodedAltmountAuth)) - ); - if (success) { - altmountAuth = data; - } - } + // if (encodedAltmountAuth) { + // const { success, data } = AltmountConfig.safeParse( + // JSON.parse(fromUrlSafeBase64(encodedAltmountAuth)) + // ); + // if (success) { + // altmountAuth = data; + // } + // } - // Collect indices for proxying - const nzbdavProxyIndices: number[] = []; - const altmountProxyIndices: number[] = []; + // // Collect indices for proxying + // const nzbdavProxyIndices: number[] = []; + // const altmountProxyIndices: number[] = []; - if (nzbdavAuth) { - nzbdavProxyIndices.push( - ...results - .map((result, index) => ({ result, index })) - .filter(({ result }) => result.service?.id === 'nzbdav') - .map(({ index }) => index) - ); - } + // if (nzbdavAuth) { + // nzbdavProxyIndices.push( + // ...results + // .map((result, index) => ({ result, index })) + // .filter(({ result }) => result.service?.id === 'nzbdav') + // .map(({ index }) => index) + // ); + // } - if (altmountAuth) { - altmountProxyIndices.push( - ...results - .map((result, index) => ({ result, index })) - .filter(({ result }) => result.service?.id === 'altmount') - .map(({ index }) => index) - ); - } + // if (altmountAuth) { + // altmountProxyIndices.push( + // ...results + // .map((result, index) => ({ result, index })) + // .filter(({ result }) => result.service?.id === 'altmount') + // .map(({ index }) => index) + // ); + // } let resultStreams = await Promise.all( results.map((result) => this._createStream(result, encryptedStoreAuths, metadataId) ) ); - // Proxy NzbDAV streams - if (nzbdavProxyIndices.length > 0 && nzbdavAuth) { - const proxy = createProxy({ - id: 'builtin', - enabled: true, - credentials: nzbdavAuth.aiostreamsAuth, - }); + // // Proxy NzbDAV streams + // if (nzbdavProxyIndices.length > 0 && nzbdavAuth) { + // const proxy = createProxy({ + // id: 'builtin', + // enabled: true, + // credentials: nzbdavAuth.aiostreamsAuth, + // }); - const proxiedStreams = await proxy.generateUrls( - nzbdavProxyIndices - .map((i) => resultStreams[i]) - .map((stream) => ({ - url: stream.url!, - filename: stream.behaviorHints?.filename ?? undefined, - headers: { - request: { - Authorization: `Basic ${Buffer.from( - `${nzbdavAuth.webdavUser}:${nzbdavAuth.webdavPassword}` - ).toString('base64')}`, - }, - }, - })) - ); + // const proxiedStreams = await proxy.generateUrls( + // nzbdavProxyIndices + // .map((i) => resultStreams[i]) + // .map((stream) => ({ + // url: stream.url!, + // filename: stream.behaviorHints?.filename ?? undefined, + // headers: { + // request: { + // Authorization: `Basic ${Buffer.from( + // `${nzbdavAuth.webdavUser}:${nzbdavAuth.webdavPassword}` + // ).toString('base64')}`, + // }, + // }, + // })) + // ); - if (proxiedStreams) { - for (let i = 0; i < nzbdavProxyIndices.length; i++) { - const index = nzbdavProxyIndices[i]; - const proxiedUrl = proxiedStreams[i]; - if (proxiedUrl) { - resultStreams[index].url = proxiedUrl; - } - } - } else { - errorStreams.push( - this._createErrorStream({ - title: `${this.name}`, - description: `Failed to proxy NzbDAV streams, ensure your proxy auth is correct.`, - }) - ); - // remove all nzbdav streams - resultStreams = resultStreams.filter( - (_, i) => !nzbdavProxyIndices.includes(i) - ); - } - } + // if (proxiedStreams) { + // for (let i = 0; i < nzbdavProxyIndices.length; i++) { + // const index = nzbdavProxyIndices[i]; + // const proxiedUrl = proxiedStreams[i]; + // if (proxiedUrl) { + // resultStreams[index].url = proxiedUrl; + // } + // } + // } else { + // errorStreams.push( + // this._createErrorStream({ + // title: `${this.name}`, + // description: `Failed to proxy NzbDAV streams, ensure your proxy auth is correct.`, + // }) + // ); + // // remove all nzbdav streams + // resultStreams = resultStreams.filter( + // (_, i) => !nzbdavProxyIndices.includes(i) + // ); + // } + // } - // Proxy Altmount streams - if (altmountProxyIndices.length > 0 && altmountAuth) { - const proxy = createProxy({ - id: 'builtin', - enabled: true, - credentials: altmountAuth.aiostreamsAuth, - }); + // // Proxy Altmount streams + // if (altmountProxyIndices.length > 0 && altmountAuth) { + // const proxy = createProxy({ + // id: 'builtin', + // enabled: true, + // credentials: altmountAuth.aiostreamsAuth, + // }); - const proxiedStreams = await proxy.generateUrls( - altmountProxyIndices - .map((i) => resultStreams[i]) - .map((stream) => ({ - url: stream.url!, - filename: stream.behaviorHints?.filename ?? undefined, - headers: { - request: { - Authorization: `Basic ${Buffer.from( - `${altmountAuth.webdavUser}:${altmountAuth.webdavPassword}` - ).toString('base64')}`, - }, - }, - })) - ); + // const proxiedStreams = await proxy.generateUrls( + // altmountProxyIndices + // .map((i) => resultStreams[i]) + // .map((stream) => ({ + // url: stream.url!, + // filename: stream.behaviorHints?.filename ?? undefined, + // headers: { + // request: { + // Authorization: `Basic ${Buffer.from( + // `${altmountAuth.webdavUser}:${altmountAuth.webdavPassword}` + // ).toString('base64')}`, + // }, + // }, + // })) + // ); - if (proxiedStreams) { - for (let i = 0; i < altmountProxyIndices.length; i++) { - const index = altmountProxyIndices[i]; - const proxiedUrl = proxiedStreams[i]; - if (proxiedUrl) { - resultStreams[index].url = proxiedUrl; - } - } - } else { - errorStreams.push( - this._createErrorStream({ - title: `${this.name}`, - description: `Failed to proxy Altmount streams, ensure your proxy auth is correct.`, - }) - ); - // remove all altmount streams - resultStreams = resultStreams.filter( - (_, i) => !altmountProxyIndices.includes(i) - ); - } - } + // if (proxiedStreams) { + // for (let i = 0; i < altmountProxyIndices.length; i++) { + // const index = altmountProxyIndices[i]; + // const proxiedUrl = proxiedStreams[i]; + // if (proxiedUrl) { + // resultStreams[index].url = proxiedUrl; + // } + // } + // } else { + // errorStreams.push( + // this._createErrorStream({ + // title: `${this.name}`, + // description: `Failed to proxy Altmount streams, ensure your proxy auth is correct.`, + // }) + // ); + // // remove all altmount streams + // resultStreams = resultStreams.filter( + // (_, i) => !altmountProxyIndices.includes(i) + // ); + // } + // } [...processedTorrents.errors, ...processedNzbs.errors].forEach((error) => { let errMsg = error.error.message; diff --git a/packages/core/src/debrid/altmount.ts b/packages/core/src/debrid/altmount.ts index 3bc9352e..7e5f62eb 100644 --- a/packages/core/src/debrid/altmount.ts +++ b/packages/core/src/debrid/altmount.ts @@ -13,10 +13,13 @@ export const AltmountConfig = z.object({ altmountUrl: z .string() .transform((s) => s.trim().replace(/^\/+/, '').replace(/\/+$/, '')), + publicAltmountUrl: z + .string() + .optional() + .transform((s) => s?.trim().replace(/^\/+/, '').replace(/\/+$/, '')), altmountApiKey: z.string(), webdavUser: z.string(), webdavPassword: z.string(), - aiostreamsAuth: z.string(), }); export class AltmountService extends UsenetStreamService { @@ -30,11 +33,11 @@ export class AltmountService extends UsenetStreamService { const auth: UsenetStreamServiceConfig = { webdavUrl: `${parsedConfig.altmountUrl}/webdav/`, + publicWebdavUrl: `${parsedConfig.publicAltmountUrl ?? parsedConfig.altmountUrl}/webdav/`, webdavUser: parsedConfig.webdavUser, webdavPassword: parsedConfig.webdavPassword, apiUrl: `${parsedConfig.altmountUrl}/sabnzbd/api`, apiKey: parsedConfig.altmountApiKey, - aiostreamsAuth: parsedConfig.aiostreamsAuth, }; super(config, auth, 'altmount'); @@ -50,11 +53,4 @@ export class AltmountService extends UsenetStreamService { ? basename(nzbUrl, '.nzb') : basename(nzbUrl); } - - protected async generatePlaybackLink(filePath: string): Promise { - const parsedConfig = AltmountConfig.parse( - JSON.parse(fromUrlSafeBase64(this.config.token)) - ); - return `${parsedConfig.altmountUrl}/webdav${filePath}`; - } } diff --git a/packages/core/src/debrid/nzbdav.ts b/packages/core/src/debrid/nzbdav.ts index 6c2bd8c9..2db399fe 100644 --- a/packages/core/src/debrid/nzbdav.ts +++ b/packages/core/src/debrid/nzbdav.ts @@ -15,10 +15,13 @@ export const NzbDavConfig = z.object({ nzbdavUrl: z .string() .transform((s) => s.trim().replace(/^\/+/, '').replace(/\/+$/, '')), + publicNzbdavUrl: z + .string() + .optional() + .transform((s) => s?.trim().replace(/^\/+/, '').replace(/\/+$/, '')), nzbdavApiKey: z.string(), webdavUser: z.string(), webdavPassword: z.string(), - aiostreamsAuth: z.string(), }); export class NzbDAVService extends UsenetStreamService { @@ -32,11 +35,11 @@ export class NzbDAVService extends UsenetStreamService { const auth: UsenetStreamServiceConfig = { webdavUrl: `${parsedConfig.nzbdavUrl}/`, + publicWebdavUrl: `${parsedConfig.publicNzbdavUrl ?? parsedConfig.nzbdavUrl}/`, webdavUser: parsedConfig.webdavUser, webdavPassword: parsedConfig.webdavPassword, apiUrl: `${parsedConfig.nzbdavUrl}/api`, apiKey: parsedConfig.nzbdavApiKey, - aiostreamsAuth: parsedConfig.aiostreamsAuth, }; super(config, auth, 'nzbdav'); @@ -50,11 +53,4 @@ export class NzbDAVService extends UsenetStreamService { // NzbDAV uses the filename parameter return filename; } - - protected async generatePlaybackLink(filePath: string): Promise { - const parsedConfig = NzbDavConfig.parse( - JSON.parse(fromUrlSafeBase64(this.config.token)) - ); - return `${parsedConfig.nzbdavUrl}${filePath}`; - } } diff --git a/packages/core/src/debrid/usenet-stream-base.ts b/packages/core/src/debrid/usenet-stream-base.ts index 54eaf8ed..e338178c 100644 --- a/packages/core/src/debrid/usenet-stream-base.ts +++ b/packages/core/src/debrid/usenet-stream-base.ts @@ -382,11 +382,11 @@ export class SABnzbdApi { */ export interface UsenetStreamServiceConfig { webdavUrl: string; + publicWebdavUrl: string; webdavUser: string; webdavPassword: string; apiUrl: string; apiKey: string; - aiostreamsAuth: string; } /** @@ -566,18 +566,6 @@ export abstract class UsenetStreamService implements DebridService { } public async checkNzbs(hashes: string[]): Promise { - // validate proxy auth - try { - BuiltinProxy.validateAuth(this.auth.aiostreamsAuth); - } catch (error) { - throw new DebridError(`Invalid AIOStreams proxy auth`, { - statusCode: 401, - statusText: 'Unauthorized', - code: 'UNAUTHORIZED', - headers: {}, - type: 'api_error', - }); - } // All NZBs are "cached" since it's streaming-based return hashes.map((h, index) => ({ id: index, @@ -785,7 +773,7 @@ export abstract class UsenetStreamService implements DebridService { }); const filePath = selectedFile.path || `${contentPath}/${selectedFile.name}`; - const playbackLink = await this.generatePlaybackLink(filePath); + const playbackLink = `${this.getPublicWebdavUrlWithAuth()}${filePath}`; this.serviceLogger.debug(`Generated playback link`, { playbackLink }); @@ -800,9 +788,10 @@ export abstract class UsenetStreamService implements DebridService { return playbackLink; } - /** - * Generate the playback link for a given file path - * This method can be overridden by subclasses to customize the URL format - */ - protected abstract generatePlaybackLink(filePath: string): Promise; + protected getPublicWebdavUrlWithAuth(): string { + let url = new URL(this.auth.publicWebdavUrl); + url.username = this.auth.webdavUser; + url.password = encodeURIComponent(this.auth.webdavPassword); + return url.toString().replace(/\/+$/, ''); // Remove trailing slash + } } diff --git a/packages/core/src/parser/streams.ts b/packages/core/src/parser/streams.ts index 6306c850..8eca2de5 100644 --- a/packages/core/src/parser/streams.ts +++ b/packages/core/src/parser/streams.ts @@ -374,11 +374,7 @@ class StreamParser { stream: Stream, currentParsedStream: ParsedStream ): ParsedStream['service'] | undefined { - const service = this.parseServiceData(stream.name || ''); - if (service?.id === 'nzbdav') { - currentParsedStream.proxied = true; - } - return service; + return this.parseServiceData(stream.name || ''); } protected getInfoHash( diff --git a/packages/core/src/presets/builtin.ts b/packages/core/src/presets/builtin.ts index 3d899403..0481d1ee 100644 --- a/packages/core/src/presets/builtin.ts +++ b/packages/core/src/presets/builtin.ts @@ -70,6 +70,7 @@ export class BuiltinAddonPreset extends Preset { toUrlSafeBase64( JSON.stringify({ nzbdavUrl: credentials.url, + publicNzbdavUrl: credentials.publicUrl, nzbdavApiKey: credentials.apiKey, webdavUser: credentials.username, webdavPassword: credentials.password, @@ -84,6 +85,7 @@ export class BuiltinAddonPreset extends Preset { toUrlSafeBase64( JSON.stringify({ altmountUrl: credentials.url, + publicAltmountUrl: credentials.publicUrl, altmountApiKey: credentials.apiKey, webdavUser: credentials.username, webdavPassword: credentials.password, diff --git a/packages/core/src/utils/constants.ts b/packages/core/src/utils/constants.ts index 23ab6c79..53e4128d 100644 --- a/packages/core/src/utils/constants.ts +++ b/packages/core/src/utils/constants.ts @@ -393,14 +393,30 @@ const SERVICE_DETAILS: Record< knownNames: ['ND'], signUpText: 'Stream usenet directly from your provider via Nzb DAV.', credentials: [ + { + id: 'note', + name: 'What do I put for URL and Public URL?', + description: `\n**URL:** Use internal URL for local setups (e.g., http://nzbdav:3000), otherwise use a public URL here.\n\n**Public URL:** Only needed if streams use local URL but you need public access. Leave blank if URL is already public or if using a proxy.\n\n**Note:** WebDAV URL/credentials are exposed in the stream URLs if not using a proxy.`, + type: 'alert', + intent: 'info', + required: false, + }, { id: 'url', name: 'NzbDAV URL', description: - 'The base URL of your NZB DAV instance. E.g., http://nzbdav:3000 or https://nzbdav.example.com', + 'The base URL of your NZB DAV instance. E.g., http://nzbdav:3000', type: 'string', required: true, }, + { + id: 'publicUrl', + name: 'Public NzbDAV URL (Optional)', + description: + 'The public URL of your NzbDAV instance. Optional, see note above for details.', + type: 'string', + required: false, + }, { id: 'apiKey', name: 'NzbDAV API Key', @@ -425,14 +441,6 @@ const SERVICE_DETAILS: Record< type: 'password', required: true, }, - { - id: 'aiostreamsAuth', - name: 'AIOStreams Proxy Auth', - description: - 'It is required to proxy the NzbDAV streams through AIOStreams. Provide a username:password pair from the `AIOSTREAMS_AUTH` environment variable.', - type: 'password', - required: true, - }, ], }, [ALTMOUNT_SERVICE]: { @@ -442,14 +450,30 @@ const SERVICE_DETAILS: Record< knownNames: ['AM'], signUpText: 'Stream usenet directly from your provider via AltMount.', credentials: [ + { + id: 'note', + name: 'What do I put for URL and Public URL?', + description: `\n**URL:** Use internal URL for local setups (e.g., http://altmount:8000), otherwise use a public URL here.\n\n**Public URL:** Only needed if streams use local URL but you need public access. Leave blank if URL is already public or if using a proxy.\n\n**Note:** WebDAV URL/credentials are exposed in the stream URLs if not using a proxy.`, + type: 'alert', + intent: 'info', + required: false, + }, { id: 'url', name: 'Altmount URL', description: - 'The base URL of your AltMount instance. E.g., http://altmount:8080 or https://altmount.example.com', + 'The base URL of your AltMount instance used for requests. e.g., http://altmount:8080', type: 'string', required: true, }, + { + id: 'publicUrl', + name: 'Public Altmount URL', + description: + 'The public URL of your AltMount instance. Optional, see note above for details.', + type: 'string', + required: false, + }, { id: 'apiKey', name: 'AltMount API Key', @@ -474,14 +498,6 @@ const SERVICE_DETAILS: Record< type: 'password', required: true, }, - { - id: 'aiostreamsAuth', - name: 'AIOStreams Proxy Auth', - description: - 'It is required to proxy the AltMount streams through AIOStreams. Provide a username:password pair from the `AIOSTREAMS_AUTH` environment variable.', - type: 'password', - required: true, - }, ], },