diff --git a/package-lock.json b/package-lock.json index 7c787e66..9e2257f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7720,6 +7720,15 @@ "node": ">=0.10.0" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8180,6 +8189,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-torrent-title": { + "version": "1.3.0", + "resolved": "git+ssh://git@github.com/TheBeastLT/parse-torrent-title.git#1169487e316a4eed898dcfd900957f89a253604c", + "license": "MIT", + "dependencies": { + "moment": "^2.24.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/parse5": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", @@ -11979,7 +11999,8 @@ "name": "@aiostreams/parser", "version": "1.17.0", "dependencies": { - "@aiostreams/types": "^1.0.0" + "@aiostreams/types": "^1.0.0", + "parse-torrent-title": "github:TheBeastLT/parse-torrent-title" } }, "packages/types": { diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index fe8e66e6..f0b89203 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -609,9 +609,46 @@ export class AIOStreams { private async createStreamObjects( parsedStreams: ParsedStream[] ): Promise { - // Step 1: Format all stream metadata - let streamObjects: Stream[] = await Promise.all( - parsedStreams.map(async (parsedStream) => { + // Identify streams that require proxying + const streamsToProxy = parsedStreams + .map((stream, index) => ({ stream, index })) + .filter(({ stream }) => stream.url && this.shouldProxyStream(stream)); + + const proxiedUrls = streamsToProxy.length + ? await generateMediaFlowStreams( + getMediaFlowConfig(this.config), + streamsToProxy.map(({ stream }) => ({ + url: stream.url!, + filename: stream.filename, + headers: stream.stream?.behaviorHints?.proxyHeaders, + })) + ) + : null; + + const removeIndexes = new Set(); + + // Apply proxied URLs and mark as proxied + streamsToProxy.forEach(({ stream, index }, i) => { + const proxiedUrl = proxiedUrls?.[i]; + if (proxiedUrl) { + stream.url = proxiedUrl; + stream.proxied = true; + } else { + removeIndexes.add(index); + } + }); + + // Remove streams that failed to proxy + console.error( + `Failed to proxy ${removeIndexes.size} streams, removing them from the final list` + ); + parsedStreams = parsedStreams.filter( + (_, index) => !removeIndexes.has(index) + ); + + // Build final Stream objects + const streamObjects: Stream[] = await Promise.all( + parsedStreams.map((parsedStream) => { const { name, description } = this.getFormattedText(parsedStream); const combinedTags = [ @@ -628,14 +665,8 @@ export class AIOStreams { externalUrl: parsedStream.externalUrl, infoHash: parsedStream.torrent?.infoHash, fileIdx: parsedStream.torrent?.fileIdx, - name: this.config.addonNameInDescription - ? Settings.ADDON_NAME - : Settings.SHOW_DIE - ? `🎲 ${name}` - : name, - description: this.config.addonNameInDescription - ? `🎲 ${name.split('\n').join(' ')}\n${description}` - : description, + name, + description, subtitles: parsedStream.stream?.subtitles, sources: parsedStream.torrent?.sources, behaviorHints: { @@ -643,7 +674,7 @@ export class AIOStreams { ? Math.floor(parsedStream.size) : undefined, filename: parsedStream.filename, - bingeGroup: `${Settings.ADDON_ID}|${parsedStream.addon.name}|${combinedTags.join('|')}`, + bingeGroup: `${parsedStream.proxied ? 'mfp.' : ''}${Settings.ADDON_ID}|${parsedStream.addon.name}|${combinedTags.join('|')}`, proxyHeaders: parsedStream.stream?.behaviorHints?.proxyHeaders, notWebReady: parsedStream.stream?.behaviorHints?.notWebReady, }, @@ -651,61 +682,6 @@ export class AIOStreams { }) ); - // Determine which streams need proxying and remember their indexes - const streamsToProxy = parsedStreams - .map((stream, index) => ({ stream, index })) - .filter(({ stream }) => stream.url !== undefined) // cannot proxy a stream without a URL - .filter(({ stream }) => this.shouldProxyStream(stream)); - - // Generate proxied URLs - const proxiedUrls = - streamsToProxy.length > 0 - ? await generateMediaFlowStreams( - getMediaFlowConfig(this.config), - streamsToProxy.map(({ stream }) => ({ - url: stream.url!, - filename: stream.filename, - headers: stream.stream?.behaviorHints?.proxyHeaders, - })) - ) - : null; - - if ( - streamsToProxy && - proxiedUrls && - proxiedUrls.length !== streamsToProxy.length - ) { - logger.error( - `Proxied URLs length (${proxiedUrls.length}) does not match streamsToProxy length (${streamsToProxy.length})` - ); - return streamObjects; - } else if (streamsToProxy.length > 0 && !proxiedUrls) { - logger.error( - `Proxied URLs is null, but streamsToProxy length is ${streamsToProxy.length}, filtering out streams that needed proxying` - ); - streamObjects = streamObjects.filter( - (_, index) => !streamsToProxy.some((s) => s.index === index) - ); - } else if (proxiedUrls) { - // inject proxied URLs back into their original positions - streamsToProxy.forEach(({ index }, i) => { - streamObjects[index].url = proxiedUrls[i] || streamObjects[index].url; - - streamObjects[index].name = this.config.addonNameInDescription - ? Settings.ADDON_NAME - : `🕵️ ${streamObjects[index].name}`; - - streamObjects[index].description = this.config.addonNameInDescription - ? `🕵️ ${streamObjects[index].name.split('\n').join(' ')}\n${streamObjects[index].description}` - : streamObjects[index].description; - - streamObjects[index].behaviorHints = { - ...streamObjects[index].behaviorHints, - bingeGroup: `mfp.${streamObjects[index].behaviorHints?.bingeGroup}`, - }; - }); - } - return streamObjects; } diff --git a/packages/formatters/src/custom.ts b/packages/formatters/src/custom.ts index 4735323c..639bdea7 100644 --- a/packages/formatters/src/custom.ts +++ b/packages/formatters/src/custom.ts @@ -1,5 +1,5 @@ import { Config, CustomFormatter, ParsedStream } from '@aiostreams/types'; -import { serviceDetails } from '@aiostreams/utils'; +import { serviceDetails, Settings } from '@aiostreams/utils'; import { formatDuration, formatSize, languageToEmoji } from './utils'; /** @@ -65,6 +65,10 @@ export function customFormat( } export type ParseValue = { + config?: { + addonName: string | null; + showDie: boolean | null; + }; stream?: { name: string | null; size: number | null; @@ -78,11 +82,17 @@ export type ParseValue = { releaseGroup: string | null; encode: string | null; indexer: string | null; + year: string | null; + title: string | null; + season: number | null; + seasons: number[] | null; + episode: number | null; seeders: number | null; age: string | null; duration: number | null; infoHash: string | null; message: string | null; + proxied: boolean | null; }; provider?: { id: string | null; @@ -102,6 +112,10 @@ export type ParseValue = { const convertStreamToParseValue = (stream: ParsedStream): ParseValue => { return { + config: { + addonName: Settings.ADDON_NAME, + showDie: Settings.SHOW_DIE, + }, stream: { name: stream.filename || null, size: stream.size || null, @@ -121,10 +135,16 @@ const convertStreamToParseValue = (stream: ParsedStream): ParseValue => { encode: stream.encode === 'Unknown' ? null : stream.encode, indexer: stream.indexers || null, seeders: stream.torrent?.seeders || null, + year: stream.year || null, + title: stream.title || null, + season: stream.season || null, + seasons: stream.seasons || null, + episode: stream.episode || null, age: stream.usenet?.age || null, duration: stream.duration || null, infoHash: stream.torrent?.infoHash || null, message: stream.message || null, + proxied: stream.proxied !== undefined ? stream.proxied : null, }, addon: { id: stream.addon.id, @@ -157,6 +177,7 @@ function parseString(str: string, value: ParseValue) { stream: value.stream, provider: value.provider, addon: value.addon, + config: value.config, }; value.debug = { @@ -165,7 +186,7 @@ function parseString(str: string, value: ParseValue) { }; const re = - /\{(?stream|provider|debug|addon)\.(?\w+)(::(?(\w+(\([^)]*\))?|<|<=|=|>=|>|\^|\$|~|\/)+))?((::(?\S+?))|(?\[(?".*?")\|\|(?".*?")\]))?\}/gi; + /\{(?stream|provider|debug|addon|config)\.(?\w+)(::(?(\w+(\([^)]*\))?|<|<=|=|>=|>|\^|\$|~|\/)+))?((::(?\S+?))|(?\[(?".*?")\|\|(?".*?")\]))?\}/gi; let matches: RegExpExecArray | null; while ((matches = re.exec(str))) { diff --git a/packages/formatters/src/gdrive.ts b/packages/formatters/src/gdrive.ts index d1b4223d..f3066cd2 100644 --- a/packages/formatters/src/gdrive.ts +++ b/packages/formatters/src/gdrive.ts @@ -1,6 +1,6 @@ import { ParsedStream } from '@aiostreams/types'; import { formatDuration, formatSize, languageToEmoji } from './utils'; -import { serviceDetails } from '@aiostreams/utils'; +import { serviceDetails, Settings } from '@aiostreams/utils'; export function gdriveFormat( stream: ParsedStream, @@ -100,6 +100,13 @@ export function gdriveFormat( if (stream.message) { description += `📢 ${stream.message}`; } + + if (stream.proxied) { + name = `🕵️‍♂️ ${name}`; + } else if (Settings.SHOW_DIE) { + name = `🎲 ${name}`; + } + description = description.trim(); name = name.trim(); return { name, description }; diff --git a/packages/formatters/src/torbox.ts b/packages/formatters/src/torbox.ts index c32f131e..7c57e13c 100644 --- a/packages/formatters/src/torbox.ts +++ b/packages/formatters/src/torbox.ts @@ -1,6 +1,6 @@ import { ParsedStream } from '@aiostreams/types'; import { formatSize } from './utils'; -import { serviceDetails } from '@aiostreams/utils'; +import { serviceDetails, Settings } from '@aiostreams/utils'; export function torboxFormat(stream: ParsedStream): { name: string; @@ -41,5 +41,11 @@ export function torboxFormat(stream: ParsedStream): { description += `\n${stream.message}`; } + if (stream.proxied) { + name = `🕵️‍♂️ ${name}`; + } else if (Settings.SHOW_DIE) { + name = `🎲 ${name}`; + } + return { name, description }; } diff --git a/packages/formatters/src/torrentio.ts b/packages/formatters/src/torrentio.ts index f73fe34b..4c7f8413 100644 --- a/packages/formatters/src/torrentio.ts +++ b/packages/formatters/src/torrentio.ts @@ -1,6 +1,6 @@ import { ParsedStream } from '@aiostreams/types'; import { formatSize, languageToEmoji } from './utils'; -import { serviceDetails } from '@aiostreams/utils'; +import { serviceDetails, Settings } from '@aiostreams/utils'; export function torrentioFormat(stream: ParsedStream): { name: string; @@ -64,5 +64,12 @@ export function torrentioFormat(stream: ParsedStream): { if (languageEmojis.length > 0) { description += `\n${languageEmojis.join(' / ')}`; } + + if (stream.proxied) { + name = `🕵️‍♂️ ${name}`; + } else if (Settings.SHOW_DIE) { + name = `🎲 ${name}`; + } + return { name, description }; } diff --git a/packages/frontend/src/app/configure/page.tsx b/packages/frontend/src/app/configure/page.tsx index 77b3d25e..5a68ed35 100644 --- a/packages/frontend/src/app/configure/page.tsx +++ b/packages/frontend/src/app/configure/page.tsx @@ -177,8 +177,6 @@ export default function Configure() { const [minMovieSize, setMinMovieSize] = useState(null); const [maxEpisodeSize, setMaxEpisodeSize] = useState(null); const [minEpisodeSize, setMinEpisodeSize] = useState(null); - const [addonNameInDescription, setAddonNameInDescription] = - useState(false); const [cleanResults, setCleanResults] = useState(false); const [maxResultsPerResolution, setMaxResultsPerResolution] = useState< number | null @@ -257,7 +255,6 @@ export default function Configure() { minMovieSize, maxEpisodeSize, minEpisodeSize, - addonNameInDescription, cleanResults, maxResultsPerResolution, strictIncludeFilters: @@ -562,7 +559,6 @@ export default function Configure() { decodedConfig.minEpisodeSize || decodedConfig.minSize || null ); setAddons(loadValidAddons(decodedConfig.addons)); - setAddonNameInDescription(decodedConfig.addonNameInDescription || false); setCleanResults(decodedConfig.cleanResults || false); setMaxResultsPerResolution(decodedConfig.maxResultsPerResolution || null); setMediaFlowEnabled( @@ -1139,34 +1135,6 @@ export default function Configure() { -
-
-
-

Move Addon Name to Description

-

- Move the addon name to the description of the stream. This will - show AIOStreams as the stream title, but move the - name of the addon that the stream is from to the description. - This is useful for Vidi users. -

-
-
- setAddonNameInDescription(e.target.checked)} - // move to the right - style={{ - marginLeft: 'auto', - marginRight: '20px', - width: '25px', - height: '25px', - }} - /> -
-
-
-
diff --git a/packages/frontend/src/components/FormatterPreview.tsx b/packages/frontend/src/components/FormatterPreview.tsx index 74b62af0..270b74e2 100644 --- a/packages/frontend/src/components/FormatterPreview.tsx +++ b/packages/frontend/src/components/FormatterPreview.tsx @@ -24,20 +24,21 @@ const FormatterPreview: React.FC = ({ formatter }) => { const [indexers, setIndexers] = React.useState('RARBG'); const [seeders, setSeeders] = React.useState(125); const [usenetAge, setUsenetAge] = React.useState('10d'); // Days - const [addonName, setAddonName] = React.useState('AIOStreams'); + const [addonName, setAddonName] = React.useState('Torrentio'); const [providerId, setProviderId] = React.useState('realdebrid'); const [isCached, setIsCached] = React.useState(true); const [isP2P, setIsP2P] = React.useState(false); const [isPersonal, setIsPersonal] = React.useState(false); const [duration, setDuration] = React.useState(9120000); // 2h 32m const [fileSize, setFileSize] = React.useState(62500000000); // 58.2 GB + const [proxied, setProxied] = React.useState(false); // Proxied or not const parsedInfo = parseFilename(filename); console.log(`Formatter: ${formatter}`); const sampleStream: ParsedStream = { ...parsedInfo, addon: { - id: 'aiostreams', + id: 'test-addon', name: addonName, }, filename: filename, @@ -55,6 +56,7 @@ const FormatterPreview: React.FC = ({ formatter }) => { }, type: providerId === 'usenet' ? 'usenet' : 'debrid', personal: isPersonal, + proxied: proxied, }; const getFormatterExample = () => { @@ -314,6 +316,12 @@ const FormatterPreview: React.FC = ({ formatter }) => { isChecked={isPersonal} setChecked={setIsPersonal} /> + +
)} diff --git a/packages/parser/package.json b/packages/parser/package.json index 7f713914..8606add8 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -8,6 +8,7 @@ }, "description": "Library to parse a torrent filename ", "dependencies": { - "@aiostreams/types": "^1.0.0" + "@aiostreams/types": "^1.0.0", + "parse-torrent-title": "github:TheBeastLT/parse-torrent-title" } } diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 27952538..0339f883 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -1,5 +1,6 @@ import { ParsedNameData } from '@aiostreams/types'; import { PARSE_REGEX } from './regex'; +import * as PTT from 'parse-torrent-title'; function matchPattern( filename: string, @@ -33,7 +34,15 @@ export function parseFilename(filename: string): ParsedNameData { const visualTags = matchMultiplePatterns(filename, PARSE_REGEX.visualTags); const audioTags = matchMultiplePatterns(filename, PARSE_REGEX.audioTags); const languages = matchMultiplePatterns(filename, PARSE_REGEX.languages); - const releaseGroup = getMatchingPattern(filename, PARSE_REGEX.releaseGroup); + // const releaseGroup = getMatchingPattern(filename, PARSE_REGEX.releaseGroup); + + const parsed = PTT.parse(filename); + const releaseGroup = parsed.group || 'Unknown'; + const title = parsed.title; + const year = parsed.year ? parsed.year.toString() : undefined; + const season = parsed.season; + const seasons = parsed.seasons; + const episode = parsed.episode; return { resolution, @@ -43,5 +52,10 @@ export function parseFilename(filename: string): ParsedNameData { audioTags, visualTags, releaseGroup, + title, + year, + season, + seasons, + episode, }; } diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index a7385e43..4c76e803 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -7,10 +7,16 @@ export interface ParsedNameData { visualTags: string[]; audioTags: string[]; languages: string[]; + title?: string; + year?: string; + season?: number; + seasons?: number[]; + episode?: number; } // the parsed stream data which is to be used to create the final stream object export interface ParsedStream extends ParsedNameData { + proxied: boolean; // if the stream is proxied or not addon: { id: string; name: string; @@ -148,7 +154,6 @@ export interface Config { minMovieSize: number | null; maxEpisodeSize: number | null; minEpisodeSize: number | null; - addonNameInDescription?: boolean; cleanResults: boolean; maxResultsPerResolution: number | null; excludeFilters: string[] | null; diff --git a/packages/wrappers/src/base.ts b/packages/wrappers/src/base.ts index 8666bd6b..b04a507d 100644 --- a/packages/wrappers/src/base.ts +++ b/packages/wrappers/src/base.ts @@ -253,6 +253,7 @@ export class BaseWrapper { type: 'stream', result: { ...parsedInfo, + proxied: false, message: message, addon: { name: this.addonName, id: this.addonId }, filename: filename,