feat(debrid): don't make proxy required and add public URL field for nzbdav/altmount

This commit is contained in:
Viren070
2025-11-08 00:08:23 +00:00
parent a43fbec0d9
commit d71d53cc27
7 changed files with 185 additions and 190 deletions
+130 -130
View File
@@ -273,153 +273,153 @@ export abstract class BaseDebridAddon<T extends BaseDebridConfig> {
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;
+5 -9
View File
@@ -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<string> {
const parsedConfig = AltmountConfig.parse(
JSON.parse(fromUrlSafeBase64(this.config.token))
);
return `${parsedConfig.altmountUrl}/webdav${filePath}`;
}
}
+5 -9
View File
@@ -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<string> {
const parsedConfig = NzbDavConfig.parse(
JSON.parse(fromUrlSafeBase64(this.config.token))
);
return `${parsedConfig.nzbdavUrl}${filePath}`;
}
}
+8 -19
View File
@@ -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<DebridDownload[]> {
// 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<string>;
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
}
}
+1 -5
View File
@@ -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(
+2
View File
@@ -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,
+34 -18
View File
@@ -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,
},
],
},