mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
refactor(nzbdav): adjustments
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
encryptString,
|
||||
Env,
|
||||
formatZodError,
|
||||
fromUrlSafeBase64,
|
||||
getSimpleTextHash,
|
||||
getTimeTakenSincePoint,
|
||||
SERVICE_DETAILS,
|
||||
@@ -40,6 +41,8 @@ import { MetadataService } from '../../metadata/service.js';
|
||||
import { Logger } from 'winston';
|
||||
import pLimit from 'p-limit';
|
||||
import { cleanTitle } from '../../parser/utils.js';
|
||||
import { NzbDavConfig, NzbDAVService } from '../../debrid/nzbdav.js';
|
||||
import { createProxy } from '../../proxy/index.js';
|
||||
|
||||
export interface SearchMetadata extends TitleMetadata {
|
||||
primaryTitle?: string;
|
||||
@@ -249,12 +252,68 @@ export abstract class BaseDebridAddon<T extends BaseDebridConfig> {
|
||||
Env.BUILTIN_PLAYBACK_LINK_VALIDITY
|
||||
);
|
||||
|
||||
const results = [...processedTorrents.results, ...processedNzbs.results];
|
||||
let nzbdavAuth;
|
||||
const usingNzbDav = this.userData.services.some((s) => s.id === 'nzbdav');
|
||||
const encodedNzbdavAuth = this.userData.services.find(
|
||||
(s) => s.id === 'nzbdav'
|
||||
)?.credential;
|
||||
if (usingNzbDav && encodedNzbdavAuth) {
|
||||
const { success, data } = NzbDavConfig.safeParse(
|
||||
JSON.parse(fromUrlSafeBase64(encodedNzbdavAuth))
|
||||
);
|
||||
|
||||
nzbdavAuth = data;
|
||||
}
|
||||
let proxyIndices: number[] = [];
|
||||
if (nzbdavAuth) {
|
||||
proxyIndices = results
|
||||
.map((result, index) => ({ result, index }))
|
||||
.filter(({ result }) => result.service?.id === 'nzbdav')
|
||||
.map(({ index }) => index);
|
||||
}
|
||||
|
||||
const resultStreams = await Promise.all(
|
||||
[...processedTorrents.results, ...processedNzbs.results].map((result) =>
|
||||
results.map((result) =>
|
||||
this._createStream(result, encryptedStoreAuths, metadataId)
|
||||
)
|
||||
);
|
||||
|
||||
// proxy the indexes in streamsToProxy
|
||||
if (proxyIndices.length > 0 && nzbdavAuth) {
|
||||
const proxy = createProxy({
|
||||
id: 'builtin',
|
||||
enabled: true,
|
||||
credentials: nzbdavAuth.aiostreamsAuth,
|
||||
});
|
||||
|
||||
const proxiedStreams = await proxy.generateUrls(
|
||||
proxyIndices
|
||||
.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 < proxyIndices.length; i++) {
|
||||
const index = proxyIndices[i];
|
||||
const proxiedUrl = proxiedStreams[i];
|
||||
if (proxiedUrl) {
|
||||
resultStreams[index].url = proxiedUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processingErrors = [
|
||||
...processedTorrents.errors,
|
||||
...processedNzbs.errors,
|
||||
|
||||
@@ -281,7 +281,7 @@ class NzbDAVApi {
|
||||
}
|
||||
}
|
||||
|
||||
const NzbDavConfig = z.object({
|
||||
export const NzbDavConfig = z.object({
|
||||
nzbdavUrl: z
|
||||
.string()
|
||||
.transform((s) => s.trim().replace(/^\/+/, '').replace(/\/+$/, '')),
|
||||
@@ -314,7 +314,29 @@ export class NzbDAVService implements DebridService {
|
||||
this.nzbdavApi = new NzbDAVApi(this.auth.nzbdavUrl, this.auth.nzbdavApiKey);
|
||||
}
|
||||
|
||||
private async collectFilesRecursively(
|
||||
private async collectFiles(
|
||||
path: string
|
||||
): Promise<{ files: FileStat[]; depth: number }> {
|
||||
// First, try using deep mode (recursive)
|
||||
try {
|
||||
const contents = (await this.webdavClient.getDirectoryContents(path, {
|
||||
deep: true,
|
||||
})) as FileStat[];
|
||||
|
||||
const files = contents.filter((item) => item.type === 'file');
|
||||
|
||||
return { files, depth: 0 };
|
||||
} catch (error) {
|
||||
logger.warn(`Deep listing failed, falling back to manual traversal`, {
|
||||
path,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
// Fall back to manual traversal
|
||||
return this.collectFilesManually(path, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async collectFilesManually(
|
||||
path: string,
|
||||
currentDepth: number = 0
|
||||
): Promise<{ files: FileStat[]; depth: number }> {
|
||||
@@ -367,7 +389,7 @@ export class NzbDAVService implements DebridService {
|
||||
const allFiles: FileStat[] = [...files];
|
||||
|
||||
for (const dir of directories) {
|
||||
const { files: subFiles } = await this.collectFilesRecursively(
|
||||
const { files: subFiles } = await this.collectFilesManually(
|
||||
dir.filename,
|
||||
currentDepth + 1
|
||||
);
|
||||
@@ -453,12 +475,17 @@ export class NzbDAVService implements DebridService {
|
||||
return cachedLink;
|
||||
}
|
||||
|
||||
logger.debug(`Adding NZB to NzbDAV for ${nzb}`, { hash, filename });
|
||||
logger.debug(`Resolving NZB`, {
|
||||
hash,
|
||||
filename,
|
||||
nzbUrl: maskSensitiveInfo(nzb),
|
||||
});
|
||||
|
||||
const category = metadata?.season || metadata?.episode ? 'Tv' : 'Movies';
|
||||
|
||||
// Add NZB and get nzoId
|
||||
const { nzoId } = await this.nzbdavApi.addUrl(nzb, category, filename);
|
||||
const addResult = await this.nzbdavApi.addUrl(nzb, category, filename);
|
||||
const nzoId = addResult.nzoId;
|
||||
|
||||
// Poll history until download is complete
|
||||
const pollStartTime = Date.now();
|
||||
@@ -468,19 +495,17 @@ export class NzbDAVService implements DebridService {
|
||||
const jobCategory = slot.category || category;
|
||||
|
||||
logger.debug(`NZB download completed`, {
|
||||
nzoId,
|
||||
jobName,
|
||||
jobCategory,
|
||||
nzoId,
|
||||
time: getTimeTakenSincePoint(pollStartTime),
|
||||
});
|
||||
|
||||
// Get list of all files in the content folder recursively, stopping when we find video files
|
||||
const contentPath = `/content/${jobCategory}/${jobName}`;
|
||||
|
||||
const listStartTime = Date.now();
|
||||
|
||||
const { files: allFiles, depth } =
|
||||
await this.collectFilesRecursively(contentPath);
|
||||
const { files: allFiles, depth } = await this.collectFiles(contentPath);
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
throw new DebridError('No files found in NZB download', {
|
||||
@@ -519,33 +544,39 @@ export class NzbDAVService implements DebridService {
|
||||
files: debridFiles,
|
||||
};
|
||||
|
||||
// Parse all file names for matching
|
||||
const allStrings = [jobName, ...debridFiles.map((f) => f.name ?? '')];
|
||||
const parseResults: ParsedResult[] = allStrings.map((string) =>
|
||||
parseTorrentTitle(string)
|
||||
);
|
||||
const parsedFiles = new Map<string, ParsedResult>();
|
||||
for (const [index, result] of parseResults.entries()) {
|
||||
parsedFiles.set(allStrings[index], result);
|
||||
let selectedFile;
|
||||
|
||||
if (debridFiles.length === 1) {
|
||||
selectedFile = debridFiles[0];
|
||||
} else {
|
||||
// Parse all file names for matching
|
||||
const allStrings = [jobName, ...debridFiles.map((f) => f.name ?? '')];
|
||||
const parseResults: ParsedResult[] = allStrings.map((string) =>
|
||||
parseTorrentTitle(string)
|
||||
);
|
||||
const parsedFiles = new Map<string, ParsedResult>();
|
||||
for (const [index, result] of parseResults.entries()) {
|
||||
parsedFiles.set(allStrings[index], result);
|
||||
}
|
||||
|
||||
const nzbInfo = {
|
||||
type: 'usenet' as const,
|
||||
nzb,
|
||||
hash,
|
||||
title: jobName,
|
||||
metadata,
|
||||
size: debridFiles.reduce((sum, f) => sum + f.size, 0),
|
||||
};
|
||||
|
||||
// Select a file based on the available metadata and files
|
||||
selectedFile = await selectFileInTorrentOrNZB(
|
||||
nzbInfo,
|
||||
debridDownload,
|
||||
parsedFiles,
|
||||
metadata
|
||||
);
|
||||
}
|
||||
|
||||
const nzbInfo = {
|
||||
type: 'usenet' as const,
|
||||
nzb,
|
||||
hash,
|
||||
title: jobName,
|
||||
metadata,
|
||||
size: debridFiles.reduce((sum, f) => sum + f.size, 0),
|
||||
};
|
||||
|
||||
// Select a file based on the available metadata and files
|
||||
const selectedFile = await selectFileInTorrentOrNZB(
|
||||
nzbInfo,
|
||||
debridDownload,
|
||||
parsedFiles,
|
||||
metadata
|
||||
);
|
||||
|
||||
if (!selectedFile) {
|
||||
throw new DebridError('No matching file found', {
|
||||
statusCode: 400,
|
||||
@@ -564,38 +595,11 @@ export class NzbDAVService implements DebridService {
|
||||
});
|
||||
|
||||
const filePath = selectedFile.path || `${contentPath}/${selectedFile.name}`;
|
||||
const webdavLink = `${this.auth.nzbdavUrl}${filePath}`;
|
||||
|
||||
const playbackLink = (
|
||||
await new BuiltinProxy({
|
||||
enabled: true,
|
||||
id: 'builtin',
|
||||
credentials: this.auth.aiostreamsAuth,
|
||||
}).generateUrls([
|
||||
{
|
||||
url: webdavLink,
|
||||
filename: selectedFile.name || filename,
|
||||
headers: {
|
||||
request: {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${this.auth.webdavUser}:${this.auth.webdavPassword}`
|
||||
).toString('base64')}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
)?.[0];
|
||||
|
||||
if (!playbackLink) {
|
||||
throw new DebridError('Failed to generate proxied playback link', {
|
||||
statusCode: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
code: 'UNKNOWN',
|
||||
headers: {},
|
||||
body: { webdavLink },
|
||||
type: 'api_error',
|
||||
});
|
||||
}
|
||||
let playbackLink = `${this.auth.nzbdavUrl}${filePath}`;
|
||||
// const playbackUrl = new URL(playbackLink);
|
||||
// playbackUrl.username = this.auth.webdavUser;
|
||||
// playbackUrl.password = encodeURIComponent(this.auth.webdavPassword);
|
||||
// playbackLink = playbackUrl.toString();
|
||||
|
||||
logger.debug(`Generated playback link`, { playbackLink });
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import { BuiltinAddonPreset } from './builtin.js';
|
||||
export class NewznabPreset extends BuiltinAddonPreset {
|
||||
static override get METADATA() {
|
||||
const supportedResources = [constants.STREAM_RESOURCE];
|
||||
const supportedServices = [
|
||||
constants.TORBOX_SERVICE,
|
||||
constants.NZBDAV_SERVICE,
|
||||
] as const;
|
||||
const options: Option[] = [
|
||||
{
|
||||
id: 'name',
|
||||
@@ -82,6 +86,21 @@ export class NewznabPreset extends BuiltinAddonPreset {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'services',
|
||||
name: 'Services',
|
||||
description:
|
||||
'Optionally override the services that are used. If not specified, then the services that are enabled and supported will be used.',
|
||||
type: 'multi-select',
|
||||
required: false,
|
||||
showInSimpleMode: false,
|
||||
options: supportedServices.map((service) => ({
|
||||
value: service,
|
||||
label: constants.SERVICE_DETAILS[service].name,
|
||||
})),
|
||||
default: undefined,
|
||||
emptyIsUndefined: true,
|
||||
},
|
||||
{
|
||||
id: 'forceQuerySearch',
|
||||
name: 'Force Query Search',
|
||||
@@ -90,6 +109,15 @@ export class NewznabPreset extends BuiltinAddonPreset {
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: 'useMultipleInstances',
|
||||
name: 'Use Multiple Instances',
|
||||
description:
|
||||
'Newznab supports multiple services in one instance of the addon - which is used by default. If this is enabled, then the addon will be created for each service.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
showInSimpleMode: false,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
@@ -120,6 +148,11 @@ export class NewznabPreset extends BuiltinAddonPreset {
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (options.useMultipleInstances) {
|
||||
return usableServices.map((service) =>
|
||||
this.generateAddon(userData, options, [service.id])
|
||||
);
|
||||
}
|
||||
return [
|
||||
this.generateAddon(
|
||||
userData,
|
||||
|
||||
@@ -6,6 +6,10 @@ import { Env } from '../utils/index.js';
|
||||
export class NZBHydraPreset extends NewznabPreset {
|
||||
static override get METADATA() {
|
||||
const supportedResources = [constants.STREAM_RESOURCE];
|
||||
const supportedServices = [
|
||||
constants.TORBOX_SERVICE,
|
||||
constants.NZBDAV_SERVICE,
|
||||
] as const;
|
||||
const options: Option[] = [
|
||||
{
|
||||
id: 'name',
|
||||
@@ -27,6 +31,21 @@ export class NZBHydraPreset extends NewznabPreset {
|
||||
forceInUi: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'services',
|
||||
name: 'Services',
|
||||
description:
|
||||
'Optionally override the services that are used. If not specified, then the services that are enabled and supported will be used.',
|
||||
type: 'multi-select',
|
||||
required: false,
|
||||
showInSimpleMode: false,
|
||||
options: supportedServices.map((service) => ({
|
||||
value: service,
|
||||
label: constants.SERVICE_DETAILS[service].name,
|
||||
})),
|
||||
default: undefined,
|
||||
emptyIsUndefined: true,
|
||||
},
|
||||
{
|
||||
id: 'mediaTypes',
|
||||
name: 'Media Types',
|
||||
@@ -78,6 +97,16 @@ export class NZBHydraPreset extends NewznabPreset {
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'useMultipleInstances',
|
||||
name: 'Use Multiple Instances',
|
||||
description:
|
||||
'Newznab supports multiple services in one instance of the addon - which is used by default. If this is enabled, then the addon will be created for each service.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
showInSimpleMode: false,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,11 +14,6 @@ class Proxifier {
|
||||
private shouldProxyStream(stream: ParsedStream): boolean {
|
||||
const streamService = stream.service ? stream.service.id : 'none';
|
||||
const proxy = this.userData.proxy;
|
||||
// all nzb dav streams are proxied
|
||||
if (stream.service?.id === 'nzbdav') {
|
||||
stream.proxied = true;
|
||||
return false;
|
||||
}
|
||||
if (!stream.url || !proxy?.enabled || !proxy.url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ const SERVICE_DETAILS: Record<
|
||||
id: 'aiostreamsAuth',
|
||||
name: 'AIOStreams Proxy Auth',
|
||||
description:
|
||||
'The AIOStreams Proxy Auth username:password pair from the `AIOSTREAMS_AUTH` environment variable. It is required to play streams from Nzb DAV.',
|
||||
'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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user