mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat(formatter): add formattedSeason and formattedEpisode attributes
feat: handle multi-season/episode files in season/episode matching feat: assume season is 1 in strict mode for season/episode matching when episode is present
This commit is contained in:
@@ -651,11 +651,9 @@ export const ParsedFileSchema = z.object({
|
||||
languages: z.array(z.string()),
|
||||
title: z.string().optional(),
|
||||
year: z.coerce.string().optional(),
|
||||
season: z.number().optional(),
|
||||
seasons: z.array(z.number()).optional(),
|
||||
episode: z.number().optional(),
|
||||
episodes: z.array(z.number()).optional(),
|
||||
seasonEpisode: z.array(z.string()).optional(),
|
||||
// seasonEpisode: z.array(z.string()).optional(),
|
||||
edition: z.string().optional(),
|
||||
remastered: z.boolean().optional(),
|
||||
repack: z.boolean().optional(),
|
||||
|
||||
@@ -85,10 +85,12 @@ export interface ParseValue {
|
||||
indexer: string | null;
|
||||
year: string | null;
|
||||
title: string | null;
|
||||
season: number | null;
|
||||
seasons: number[] | null;
|
||||
episode: number | null;
|
||||
season: number | null;
|
||||
formattedSeasons: string | null;
|
||||
episodes: number[] | null;
|
||||
episode: number | null;
|
||||
formattedEpisodes: string | null;
|
||||
seasonEpisode: string[] | null;
|
||||
seeders: number | null;
|
||||
age: string | null;
|
||||
@@ -185,6 +187,22 @@ export abstract class BaseFormatter {
|
||||
...(this.userData.includedLanguages || []),
|
||||
]),
|
||||
];
|
||||
const getPaddedNumber = (number: number, length: number) =>
|
||||
number.toString().padStart(length, '0');
|
||||
const formattedSeasonString = stream.parsedFile?.seasons?.length
|
||||
? stream.parsedFile.seasons.length === 1
|
||||
? `S${getPaddedNumber(stream.parsedFile.seasons[0], 2)}`
|
||||
: `S${getPaddedNumber(stream.parsedFile.seasons[0], 2)}-${getPaddedNumber(stream.parsedFile.seasons[stream.parsedFile.seasons.length - 1], 2)}`
|
||||
: undefined;
|
||||
const formattedEpisodeString = stream.parsedFile?.episodes?.length
|
||||
? stream.parsedFile.episodes.length === 1
|
||||
? `E${getPaddedNumber(stream.parsedFile.episodes[0], 2)}`
|
||||
: `E${getPaddedNumber(stream.parsedFile.episodes[0], 2)}-${getPaddedNumber(stream.parsedFile.episodes[stream.parsedFile.episodes.length - 1], 2)}`
|
||||
: undefined;
|
||||
const seasonEpisode = [
|
||||
formattedSeasonString,
|
||||
formattedEpisodeString,
|
||||
].filter((v) => v !== undefined);
|
||||
|
||||
const sortedLanguages = languages
|
||||
? [...languages].sort((a, b) => {
|
||||
@@ -276,11 +294,13 @@ export abstract class BaseFormatter {
|
||||
year: stream.parsedFile?.year || null,
|
||||
type: stream.type || null,
|
||||
title: stream.parsedFile?.title || null,
|
||||
season: stream.parsedFile?.season || null,
|
||||
season: stream.parsedFile?.seasons?.[0] || null,
|
||||
formattedSeasons: formattedSeasonString || null,
|
||||
seasons: stream.parsedFile?.seasons || null,
|
||||
episode: stream.parsedFile?.episode || null,
|
||||
episode: stream.parsedFile?.episodes?.[0] || null,
|
||||
formattedEpisodes: formattedEpisodeString || null,
|
||||
episodes: stream.parsedFile?.episodes || null,
|
||||
seasonEpisode: stream.parsedFile?.seasonEpisode || null,
|
||||
seasonEpisode: seasonEpisode || null,
|
||||
duration: stream.duration || null,
|
||||
infoHash: stream.torrent?.infoHash || null,
|
||||
age: stream.age || null,
|
||||
|
||||
@@ -60,41 +60,6 @@ class FileParser {
|
||||
filename.match(PARSE_REGEX.releaseGroup)?.[1] ?? parsed.group;
|
||||
const title = parsed.title;
|
||||
const year = parsed.year ? parsed.year.toString() : undefined;
|
||||
// const season = parsed.season;
|
||||
// const seasons = parsed.seasons;
|
||||
// const episode = parsed.episode;
|
||||
// const formattedSeasonString = seasons?.length
|
||||
// ? seasons.length === 1
|
||||
// ? `S${getPaddedNumber(seasons[0], 2)}`
|
||||
// : `S${getPaddedNumber(seasons[0], 2)}-${getPaddedNumber(
|
||||
// seasons[seasons.length - 1],
|
||||
// 2
|
||||
// )}`
|
||||
// : season
|
||||
// ? `S${getPaddedNumber(season, 2)}`
|
||||
// : undefined;
|
||||
// const formattedEpisodeString = episode
|
||||
// ? `E${getPaddedNumber(episode, 2)}`
|
||||
// : undefined;
|
||||
|
||||
// const seasonEpisode = [
|
||||
// formattedSeasonString,
|
||||
// formattedEpisodeString,
|
||||
// ].filter((v) => v !== undefined);
|
||||
const formattedSeasonString = parsed.seasons?.length
|
||||
? parsed.seasons.length === 1
|
||||
? `S${getPaddedNumber(parsed.seasons[0], 2)}`
|
||||
: `S${getPaddedNumber(parsed.seasons[0], 2)}-${getPaddedNumber(parsed.seasons[parsed.seasons.length - 1], 2)}`
|
||||
: undefined;
|
||||
const formattedEpisodeString = parsed.episodes?.length
|
||||
? parsed.episodes.length === 1
|
||||
? `E${getPaddedNumber(parsed.episodes[0], 2)}`
|
||||
: `E${getPaddedNumber(parsed.episodes[0], 2)}-${getPaddedNumber(parsed.episodes[parsed.episodes.length - 1], 2)}`
|
||||
: undefined;
|
||||
const seasonEpisode = [
|
||||
formattedSeasonString,
|
||||
formattedEpisodeString,
|
||||
].filter((v) => v !== undefined);
|
||||
|
||||
return {
|
||||
resolution,
|
||||
@@ -115,11 +80,8 @@ class FileParser {
|
||||
upscaled: parsed.upscaled ?? false,
|
||||
network: parsed.network,
|
||||
container: parsed.container,
|
||||
season: parsed.seasons?.[0],
|
||||
seasons: parsed.seasons,
|
||||
episode: parsed.episodes?.[0],
|
||||
episodes: parsed.episodes,
|
||||
seasonEpisode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,14 +559,12 @@ export abstract class StreamExpressionEngine {
|
||||
parsedFile: {
|
||||
title: 'Test Title',
|
||||
year: '2024',
|
||||
season: 1,
|
||||
episode: 1,
|
||||
seasons: [1],
|
||||
episodes: [1],
|
||||
resolution: '1080p',
|
||||
quality: 'BluRay',
|
||||
encode: 'x264',
|
||||
releaseGroup: 'TEST',
|
||||
seasonEpisode: ['S01', 'E01'],
|
||||
visualTags: ['HDR'],
|
||||
audioTags: ['AAC'],
|
||||
audioChannels: ['2.0'],
|
||||
|
||||
@@ -220,7 +220,7 @@ class StreamParser {
|
||||
|
||||
for (const line of potentialFilenames) {
|
||||
const parsed = FileParser.parse(line);
|
||||
if (parsed.year || parsed.season || parsed.episode) {
|
||||
if (parsed.year || parsed.seasons?.length || parsed.episodes?.length) {
|
||||
filename = line;
|
||||
break;
|
||||
}
|
||||
@@ -430,8 +430,6 @@ class StreamParser {
|
||||
return {
|
||||
title: folderParsed?.title || fileParsed?.title,
|
||||
year: fileParsed?.year || folderParsed?.year,
|
||||
season: fileParsed?.season || folderParsed?.season,
|
||||
episode: fileParsed?.episode || folderParsed?.episode,
|
||||
seasons: fileParsed?.seasons || folderParsed?.seasons,
|
||||
episodes: fileParsed?.episodes || folderParsed?.episodes,
|
||||
resolution: fileParsed?.resolution || folderParsed?.resolution,
|
||||
@@ -446,13 +444,13 @@ class StreamParser {
|
||||
upscaled: fileParsed?.upscaled || folderParsed?.upscaled,
|
||||
network: fileParsed?.network || folderParsed?.network,
|
||||
container: fileParsed?.container || folderParsed?.container,
|
||||
seasonEpisode:
|
||||
fileParsed?.seasonEpisode && fileParsed?.seasonEpisode.length > 0
|
||||
? fileParsed?.seasonEpisode
|
||||
: folderParsed?.seasonEpisode &&
|
||||
folderParsed?.seasonEpisode.length > 0
|
||||
? folderParsed?.seasonEpisode
|
||||
: undefined,
|
||||
// seasonEpisode:
|
||||
// fileParsed?.seasonEpisode && fileParsed?.seasonEpisode.length > 0
|
||||
// ? fileParsed?.seasonEpisode
|
||||
// : folderParsed?.seasonEpisode &&
|
||||
// folderParsed?.seasonEpisode.length > 0
|
||||
// ? folderParsed?.seasonEpisode
|
||||
// : undefined,
|
||||
visualTags: Array.from(
|
||||
new Set([
|
||||
...(folderParsed?.visualTags ?? []),
|
||||
|
||||
@@ -652,35 +652,44 @@ class StreamFilterer {
|
||||
// if the requested content is a movie and season/episode is present, filter out
|
||||
if (
|
||||
type === 'movie' &&
|
||||
(stream.parsedFile?.season || stream.parsedFile?.episode)
|
||||
(stream.parsedFile?.seasons?.length ||
|
||||
stream.parsedFile?.episodes?.length)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let seasons = stream.parsedFile?.seasons;
|
||||
|
||||
// if the requested content is series and no season or episode info is present, filter out if strict is true
|
||||
if (
|
||||
type === 'series' &&
|
||||
seasonEpisodeMatchingOptions.strict &&
|
||||
!stream.parsedFile?.season &&
|
||||
!stream.parsedFile?.episode
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (type === 'series' && seasonEpisodeMatchingOptions.strict) {
|
||||
if (
|
||||
!stream.parsedFile?.seasons?.length &&
|
||||
!stream.parsedFile?.episodes?.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// is requested season present
|
||||
if (
|
||||
!stream.parsedFile.seasons?.length &&
|
||||
stream.parsedFile.episodes?.length
|
||||
) {
|
||||
// assume season is 1 when empty and episode is present in strict mode.
|
||||
seasons = [1];
|
||||
}
|
||||
}
|
||||
if (
|
||||
requestedSeason &&
|
||||
((stream.parsedFile?.season &&
|
||||
stream.parsedFile.season !== requestedSeason) ||
|
||||
(stream.parsedFile?.seasons &&
|
||||
!stream.parsedFile.seasons.includes(requestedSeason)))
|
||||
seasons &&
|
||||
seasons.length > 0 &&
|
||||
!seasons.includes(requestedSeason)
|
||||
) {
|
||||
// If absolute episode matches, and parsed season is 1, allow even if season is incorrect
|
||||
if (
|
||||
stream.parsedFile?.season === 1 &&
|
||||
stream.parsedFile?.episode &&
|
||||
seasons?.[0] === 1 &&
|
||||
stream.parsedFile?.episodes?.length &&
|
||||
requestedMetadata?.absoluteEpisode &&
|
||||
stream.parsedFile.episode === requestedMetadata.absoluteEpisode
|
||||
stream.parsedFile?.episodes?.includes(
|
||||
requestedMetadata.absoluteEpisode
|
||||
)
|
||||
) {
|
||||
// allow
|
||||
} else {
|
||||
@@ -691,15 +700,19 @@ class StreamFilterer {
|
||||
// is the present episode incorrect (does not match either the requested episode or absolute episode if present)
|
||||
if (
|
||||
requestedEpisode &&
|
||||
stream.parsedFile?.episode &&
|
||||
stream.parsedFile.episode !== requestedEpisode &&
|
||||
stream.parsedFile?.episodes?.length &&
|
||||
!stream.parsedFile?.episodes?.includes(requestedEpisode) &&
|
||||
(requestedMetadata?.absoluteEpisode
|
||||
? stream.parsedFile.episode !== requestedMetadata.absoluteEpisode
|
||||
? !stream.parsedFile?.episodes?.includes(
|
||||
requestedMetadata.absoluteEpisode
|
||||
)
|
||||
: true)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if episode is present, but season is not
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1333,12 +1346,19 @@ class StreamFilterer {
|
||||
if (
|
||||
this.userData.excludeSeasonPacks &&
|
||||
type === 'series' &&
|
||||
stream.parsedFile?.season &&
|
||||
!stream.parsedFile?.episode
|
||||
stream.parsedFile?.seasons?.length &&
|
||||
!stream.parsedFile?.episodes?.length
|
||||
) {
|
||||
const seasons = stream.parsedFile?.seasons;
|
||||
const seasonStr =
|
||||
seasons?.length === 1
|
||||
? `S${String(seasons[0]).padStart(2, '0')}`
|
||||
: seasons?.length
|
||||
? `S${String(seasons[0]).padStart(2, '0')}-${String(seasons[seasons.length - 1]).padStart(2, '0')}`
|
||||
: undefined;
|
||||
this.incrementRemovalReason(
|
||||
'excludeSeasonPacks',
|
||||
`${stream.parsedFile.title} - ${stream.parsedFile.season}`
|
||||
`${stream.parsedFile.title} - ${seasonStr}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -1449,10 +1469,23 @@ class StreamFilterer {
|
||||
}
|
||||
|
||||
if (!performSeasonEpisodeMatch(stream)) {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
const s = stream.parsedFile?.seasons;
|
||||
const e = stream.parsedFile?.episodes;
|
||||
const formattedSeasonString = s?.length
|
||||
? `S${pad(s[0])}${s.length > 1 ? `-${pad(s[s.length - 1])}` : ''}`
|
||||
: undefined;
|
||||
const formattedEpisodeString = e?.length
|
||||
? `E${pad(e[0])}${e.length > 1 ? `-${pad(e[e.length - 1])}` : ''}`
|
||||
: undefined;
|
||||
const seasonEpisode = [
|
||||
formattedSeasonString,
|
||||
formattedEpisodeString,
|
||||
].filter(Boolean);
|
||||
const detail =
|
||||
stream.parsedFile?.title +
|
||||
' ' +
|
||||
(stream.parsedFile?.seasonEpisode?.join(' x ') || 'Unknown');
|
||||
(seasonEpisode?.join(' • ') || 'Unknown');
|
||||
|
||||
this.incrementRemovalReason('seasonEpisodeMatching', detail);
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user