feat: changes

This commit removes the 'Move Addon Name to description' setting in favour of the custom formatter

Adds the following fields for use in a custom formatter:

config.addonName (returns the value of `ADDON_NAME`)
config.showDie (returns the value of `SHOW_DIE`)

stream.title (string)
stream.year (string)
stream.season (number)
stream.seasons (number[])
stream.episode (number)
stream.proxied (boolean)

you now control 100% of the stream output, including the game die emoji and the detective emoji for proxied streams.

Adds a proxied switch in the preview.

Adjusted default addon name to Torrentio to account for config.addonName

refactored the way streams were generated.
This commit is contained in:
Viren070
2025-05-01 17:57:30 +01:00
parent 22395d0949
commit f14ac825cf
12 changed files with 145 additions and 110 deletions
+22 -1
View File
@@ -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": {
+43 -67
View File
@@ -609,9 +609,46 @@ export class AIOStreams {
private async createStreamObjects(
parsedStreams: ParsedStream[]
): Promise<Stream[]> {
// 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<number>();
// 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;
}
+23 -2
View File
@@ -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 =
/\{(?<type>stream|provider|debug|addon)\.(?<prop>\w+)(::(?<mod>(\w+(\([^)]*\))?|<|<=|=|>=|>|\^|\$|~|\/)+))?((::(?<mod_tzlocale>\S+?))|(?<mod_check>\[(?<mod_check_true>".*?")\|\|(?<mod_check_false>".*?")\]))?\}/gi;
/\{(?<type>stream|provider|debug|addon|config)\.(?<prop>\w+)(::(?<mod>(\w+(\([^)]*\))?|<|<=|=|>=|>|\^|\$|~|\/)+))?((::(?<mod_tzlocale>\S+?))|(?<mod_check>\[(?<mod_check_true>".*?")\|\|(?<mod_check_false>".*?")\]))?\}/gi;
let matches: RegExpExecArray | null;
while ((matches = re.exec(str))) {
+8 -1
View File
@@ -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 };
+7 -1
View File
@@ -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 };
}
+8 -1
View File
@@ -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 };
}
@@ -177,8 +177,6 @@ export default function Configure() {
const [minMovieSize, setMinMovieSize] = useState<number | null>(null);
const [maxEpisodeSize, setMaxEpisodeSize] = useState<number | null>(null);
const [minEpisodeSize, setMinEpisodeSize] = useState<number | null>(null);
const [addonNameInDescription, setAddonNameInDescription] =
useState<boolean>(false);
const [cleanResults, setCleanResults] = useState<boolean>(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() {
<FormatterPreview formatter={formatter || 'gdrive'} />
</div>
<div className={styles.section}>
<div className={styles.setting}>
<div className={styles.settingDescription}>
<h2 style={{ padding: '5px' }}>Move Addon Name to Description</h2>
<p style={{ padding: '5px' }}>
Move the addon name to the description of the stream. This will
show <code>AIOStreams</code> 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.
</p>
</div>
<div className={styles.checkboxSettingInput}>
<input
type="checkbox"
checked={addonNameInDescription}
onChange={(e) => setAddonNameInDescription(e.target.checked)}
// move to the right
style={{
marginLeft: 'auto',
marginRight: '20px',
width: '25px',
height: '25px',
}}
/>
</div>
</div>
</div>
<div className={styles.section}>
<div className={styles.setting}>
<div className={styles.ettingDescription}>
@@ -24,20 +24,21 @@ const FormatterPreview: React.FC<FormatterPreviewProps> = ({ formatter }) => {
const [indexers, setIndexers] = React.useState<string>('RARBG');
const [seeders, setSeeders] = React.useState<number>(125);
const [usenetAge, setUsenetAge] = React.useState<string>('10d'); // Days
const [addonName, setAddonName] = React.useState<string>('AIOStreams');
const [addonName, setAddonName] = React.useState<string>('Torrentio');
const [providerId, setProviderId] = React.useState<string>('realdebrid');
const [isCached, setIsCached] = React.useState<boolean>(true);
const [isP2P, setIsP2P] = React.useState<boolean>(false);
const [isPersonal, setIsPersonal] = React.useState<boolean>(false);
const [duration, setDuration] = React.useState<number>(9120000); // 2h 32m
const [fileSize, setFileSize] = React.useState<number>(62500000000); // 58.2 GB
const [proxied, setProxied] = React.useState<boolean>(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<FormatterPreviewProps> = ({ formatter }) => {
},
type: providerId === 'usenet' ? 'usenet' : 'debrid',
personal: isPersonal,
proxied: proxied,
};
const getFormatterExample = () => {
@@ -314,6 +316,12 @@ const FormatterPreview: React.FC<FormatterPreviewProps> = ({ formatter }) => {
isChecked={isPersonal}
setChecked={setIsPersonal}
/>
<ToggleSwitch
label="Proxied"
isChecked={proxied}
setChecked={setProxied}
/>
</div>
</div>
)}
+2 -1
View File
@@ -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"
}
}
+15 -1
View File
@@ -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,
};
}
+6 -1
View File
@@ -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;
+1
View File
@@ -253,6 +253,7 @@ export class BaseWrapper {
type: 'stream',
result: {
...parsedInfo,
proxied: false,
message: message,
addon: { name: this.addonName, id: this.addonId },
filename: filename,