mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat: add digital release filter to miscellaneous filter tab
closes #280
This commit is contained in:
@@ -356,6 +356,7 @@ export const UserDataSchema = z.object({
|
||||
.tuple([z.number().min(0), z.number().min(0)])
|
||||
.optional(),
|
||||
seederRangeTypes: z.array(z.enum(['p2p', 'cached', 'uncached'])).optional(),
|
||||
digitalReleaseFilter: z.boolean().optional(),
|
||||
excludeSeasonPacks: z.boolean().optional(),
|
||||
excludeCached: z.boolean().optional(),
|
||||
excludeCachedFromAddons: z.array(z.string().min(1)).optional(),
|
||||
|
||||
@@ -38,6 +38,7 @@ export class MetadataService {
|
||||
async () => {
|
||||
const start = Date.now();
|
||||
const titles: string[] = [];
|
||||
let releaseDate: string | undefined;
|
||||
let year: number | undefined;
|
||||
let yearEnd: number | undefined;
|
||||
let seasons:
|
||||
@@ -155,6 +156,8 @@ export class MetadataService {
|
||||
if (tmdbMetadata.titles) titles.push(...tmdbMetadata.titles);
|
||||
if (tmdbMetadata.year) year = tmdbMetadata.year;
|
||||
if (tmdbMetadata.yearEnd) yearEnd = tmdbMetadata.yearEnd;
|
||||
if (tmdbMetadata.releaseDate)
|
||||
releaseDate = tmdbMetadata.releaseDate;
|
||||
if (tmdbMetadata.seasons)
|
||||
seasons = tmdbMetadata.seasons.sort(
|
||||
(a, b) => a.season_number - b.season_number
|
||||
@@ -275,6 +278,7 @@ export class MetadataService {
|
||||
year,
|
||||
yearEnd,
|
||||
seasons,
|
||||
releaseDate,
|
||||
tmdbId,
|
||||
tvdbId,
|
||||
};
|
||||
|
||||
@@ -103,6 +103,23 @@ const FindResultsSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
const ReleaseDateSchema = z.object({
|
||||
release_date: z.string(),
|
||||
type: z.number().min(0).max(6),
|
||||
});
|
||||
|
||||
export type ReleaseDate = z.infer<typeof ReleaseDateSchema>;
|
||||
|
||||
const ReleaseDatesResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
results: z.array(
|
||||
z.object({
|
||||
iso_3166_1: z.string(),
|
||||
release_dates: z.array(ReleaseDateSchema),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const IdTypeMap: Partial<Record<IdType, TMDBIdType>> = {
|
||||
imdbId: 'imdb_id',
|
||||
thetvdbId: 'tvdb_id',
|
||||
@@ -240,11 +257,11 @@ export class TMDBMetadata {
|
||||
parsedId.mediaType === 'movie'
|
||||
? (detailsData as z.infer<typeof MovieDetailsSchema>).title
|
||||
: (detailsData as z.infer<typeof TVDetailsSchema>).name;
|
||||
const year = this.parseReleaseDate(
|
||||
const releaseDate =
|
||||
parsedId.mediaType === 'movie'
|
||||
? (detailsData as z.infer<typeof MovieDetailsSchema>).release_date
|
||||
: (detailsData as z.infer<typeof TVDetailsSchema>).first_air_date
|
||||
);
|
||||
: (detailsData as z.infer<typeof TVDetailsSchema>).first_air_date;
|
||||
const year = this.parseReleaseDate(releaseDate);
|
||||
const yearEnd =
|
||||
parsedId.mediaType !== 'movie'
|
||||
? (detailsData as z.infer<typeof TVDetailsSchema>).last_air_date
|
||||
@@ -350,6 +367,7 @@ export class TMDBMetadata {
|
||||
const metadata: Metadata = {
|
||||
title: primaryTitle,
|
||||
titles: uniqueTitles,
|
||||
releaseDate: releaseDate,
|
||||
year: Number(year),
|
||||
yearEnd: yearEnd ? Number(yearEnd) : undefined,
|
||||
seasons,
|
||||
@@ -367,6 +385,21 @@ export class TMDBMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
public async getReleaseDates(tmdbId: number): Promise<ReleaseDate[]> {
|
||||
const url = new URL(API_BASE_URL + `/movie/${tmdbId}/release_dates`);
|
||||
this.addSearchParams(url);
|
||||
const response = await makeRequest(url.toString(), {
|
||||
timeout: 5000,
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch release dates: ${response.statusText}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
const data = ReleaseDatesResponseSchema.parse(json);
|
||||
return data.results.flatMap((result) => result.release_dates);
|
||||
}
|
||||
|
||||
public async validateAuthorisation() {
|
||||
const cacheKey = this.accessToken || this.apiKey;
|
||||
if (!cacheKey) {
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface Metadata {
|
||||
titles?: string[];
|
||||
year?: number;
|
||||
yearEnd?: number;
|
||||
releaseDate?: string;
|
||||
seasons?: {
|
||||
season_number: number;
|
||||
episode_count: number;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { titleMatch } from '../parser/utils.js';
|
||||
import { partial_ratio } from 'fuzzball';
|
||||
import { calculateAbsoluteEpisode } from '../builtins/utils/general.js';
|
||||
import { formatBytes } from '../formatters/utils.js';
|
||||
import { ReleaseDate, TMDBMetadata } from '../metadata/tmdb.js';
|
||||
|
||||
const logger = createLogger('filterer');
|
||||
|
||||
@@ -33,6 +34,7 @@ export interface FilterStatistics {
|
||||
yearMatching: Reason;
|
||||
seasonEpisodeMatching: Reason;
|
||||
excludeSeasonPacks: Reason;
|
||||
noDigitalRelease: Reason;
|
||||
excludedStreamType: Reason;
|
||||
requiredStreamType: Reason;
|
||||
excludedResolution: Reason;
|
||||
@@ -91,6 +93,7 @@ class StreamFilterer {
|
||||
yearMatching: { total: 0, details: {} },
|
||||
seasonEpisodeMatching: { total: 0, details: {} },
|
||||
excludeSeasonPacks: { total: 0, details: {} },
|
||||
noDigitalRelease: { total: 0, details: {} },
|
||||
excludedStreamType: { total: 0, details: {} },
|
||||
requiredStreamType: { total: 0, details: {} },
|
||||
excludedResolution: { total: 0, details: {} },
|
||||
@@ -164,6 +167,38 @@ class StreamFilterer {
|
||||
return this.filterStatistics;
|
||||
}
|
||||
|
||||
private generateFilterSummary(
|
||||
streams: ParsedStream[],
|
||||
finalStreams: ParsedStream[],
|
||||
start: number
|
||||
): void {
|
||||
const totalFiltered = streams.length - finalStreams.length;
|
||||
const summary = [
|
||||
'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
||||
` 🔍 Filter Summary`,
|
||||
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
||||
` 📊 Total Streams : ${streams.length}`,
|
||||
` ✔️ Kept : ${finalStreams.length}`,
|
||||
` ❌ Filtered : ${totalFiltered}`,
|
||||
];
|
||||
|
||||
// Add filter details if any streams were filtered
|
||||
const { filterDetails, includedDetails } = this.getFormattedFilterDetails();
|
||||
|
||||
if (filterDetails.length > 0) {
|
||||
summary.push('\n 🔎 Filter Details:');
|
||||
summary.push(...filterDetails);
|
||||
}
|
||||
if (includedDetails.length > 0) {
|
||||
summary.push('\n 🔎 Included Details:');
|
||||
summary.push(...includedDetails);
|
||||
}
|
||||
summary.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
logger.info(summary.join('\n'));
|
||||
|
||||
logger.info(`Applied filters in ${getTimeTakenSincePoint(start)}`);
|
||||
}
|
||||
|
||||
public getFormattedFilterDetails(): {
|
||||
filterDetails: string[];
|
||||
includedDetails: string[];
|
||||
@@ -227,8 +262,10 @@ class StreamFilterer {
|
||||
| undefined;
|
||||
let yearWithinTitle: string | undefined;
|
||||
let yearWithinTitleRegex: RegExp | undefined;
|
||||
let releaseDates: ReleaseDate[] | undefined;
|
||||
if (
|
||||
(this.userData.titleMatching?.enabled ||
|
||||
(this.userData.digitalReleaseFilter && type === 'movie') ||
|
||||
this.userData.yearMatching?.enabled ||
|
||||
this.userData.seasonEpisodeMatching?.enabled) &&
|
||||
constants.TYPES.includes(type as any)
|
||||
@@ -293,6 +330,23 @@ class StreamFilterer {
|
||||
requestedMetadata.absoluteEpisode = absoluteEpisode;
|
||||
}
|
||||
|
||||
if (
|
||||
this.userData.digitalReleaseFilter &&
|
||||
requestedMetadata.tmdbId &&
|
||||
type === 'movie'
|
||||
) {
|
||||
try {
|
||||
releaseDates = await new TMDBMetadata({
|
||||
accessToken: this.userData.tmdbAccessToken,
|
||||
apiKey: this.userData.tmdbApiKey,
|
||||
}).getReleaseDates(requestedMetadata.tmdbId);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Error fetching release dates for ${id} (tmdb: ${requestedMetadata.tmdbId}): ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
yearWithinTitle = requestedMetadata.title.match(
|
||||
/\b(19\d{2}|20[012]\d{1})\b/
|
||||
)?.[0];
|
||||
@@ -315,6 +369,83 @@ class StreamFilterer {
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
const applyDigitalReleaseFilter = () => {
|
||||
logger.debug(`Applying digital release filter for ${id}`, {
|
||||
releaseDate: requestedMetadata?.releaseDate,
|
||||
type,
|
||||
tmdbId: requestedMetadata?.tmdbId,
|
||||
digitalReleaseFilter: this.userData.digitalReleaseFilter,
|
||||
releaseDates: releaseDates?.length,
|
||||
});
|
||||
if (!this.userData.digitalReleaseFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type !== 'movie') {
|
||||
// only appply digital release filter to movies
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!requestedMetadata?.releaseDate) {
|
||||
// if no release date is present, keep due to lack of data
|
||||
return true;
|
||||
}
|
||||
|
||||
const releaseDate = new Date(requestedMetadata.releaseDate);
|
||||
if (isNaN(releaseDate.getTime())) {
|
||||
// if the release date is invalid, keep due to lack of data
|
||||
return true;
|
||||
}
|
||||
const today = new Date();
|
||||
|
||||
const daysSinceRelease = Math.floor(
|
||||
(today.getTime() - releaseDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (daysSinceRelease < 0) {
|
||||
// a movie released in the future is not digitaly released yet.
|
||||
logger.debug(
|
||||
`Filtering out all results as movie is not digitally released yet`,
|
||||
{
|
||||
title: requestedMetadata?.title,
|
||||
daysSinceRelease,
|
||||
}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (daysSinceRelease > 365) {
|
||||
// a movie released more than a year ago is likely to have a digital release.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!releaseDates || releaseDates.length === 0) {
|
||||
// if release dates couldn't be fetched for a recent movie, keep due to lack of data
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if at least one of the release dates is in the past and return true if so
|
||||
const hasDigitalRelease = releaseDates.some(
|
||||
(releaseDate) =>
|
||||
releaseDate.type >= 4 &&
|
||||
releaseDate.type <= 6 &&
|
||||
new Date(releaseDate.release_date) <= today
|
||||
);
|
||||
|
||||
if (hasDigitalRelease) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Filtering out all results as no digital release was found`,
|
||||
{
|
||||
title: requestedMetadata?.title,
|
||||
daysSinceRelease,
|
||||
}
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
const performTitleMatch = (stream: ParsedStream) => {
|
||||
const titleMatchingOptions = {
|
||||
mode: 'exact',
|
||||
@@ -564,6 +695,19 @@ class StreamFilterer {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Early digital release filter check - if it returns false, filter out all streams
|
||||
if (!applyDigitalReleaseFilter()) {
|
||||
// Set the count to total streams since ALL streams are filtered out
|
||||
this.filterStatistics.removed.noDigitalRelease.total = streams.length;
|
||||
this.filterStatistics.removed.noDigitalRelease.details[
|
||||
'No digital release available'
|
||||
] = streams.length;
|
||||
|
||||
const finalStreams: ParsedStream[] = [];
|
||||
this.generateFilterSummary(streams, finalStreams, start);
|
||||
return finalStreams;
|
||||
}
|
||||
|
||||
const excludedRegexPatterns =
|
||||
isRegexAllowed &&
|
||||
this.userData.excludedRegexPatterns &&
|
||||
@@ -1367,33 +1511,8 @@ class StreamFilterer {
|
||||
...filteredStreams,
|
||||
]);
|
||||
|
||||
// L// filter summary
|
||||
const totalFiltered = streams.length - finalStreams.length;
|
||||
|
||||
const summary = [
|
||||
'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
||||
` 🔍 Filter Summary`,
|
||||
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
||||
` 📊 Total Streams : ${streams.length}`,
|
||||
` ✔️ Kept : ${finalStreams.length}`,
|
||||
` ❌ Filtered : ${totalFiltered}`,
|
||||
];
|
||||
|
||||
// Add filter details if any streams were filtered
|
||||
const { filterDetails, includedDetails } = this.getFormattedFilterDetails();
|
||||
|
||||
if (filterDetails.length > 0) {
|
||||
summary.push('\n 🔎 Filter Details:');
|
||||
summary.push(...filterDetails);
|
||||
}
|
||||
if (includedDetails.length > 0) {
|
||||
summary.push('\n 🔎 Included Details:');
|
||||
summary.push(...includedDetails);
|
||||
}
|
||||
summary.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
logger.info(summary.join('\n'));
|
||||
|
||||
logger.info(`Applied filters in ${getTimeTakenSincePoint(start)}`);
|
||||
// Generate filter summary
|
||||
this.generateFilterSummary(streams, finalStreams, start);
|
||||
return finalStreams;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
MdSurroundSound,
|
||||
MdTextFields,
|
||||
MdVideoLibrary,
|
||||
MdMiscellaneousServices,
|
||||
} from 'react-icons/md';
|
||||
import { BiSolidCameraMovie } from 'react-icons/bi';
|
||||
import { SiDolby } from 'react-icons/si';
|
||||
@@ -341,6 +342,10 @@ function Content() {
|
||||
<MdCleaningServices className="text-lg mr-3" />
|
||||
Deduplicator
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="miscellaneous">
|
||||
<MdMiscellaneousServices className="text-lg mr-3" />
|
||||
Miscellaneous
|
||||
</TabsTrigger>
|
||||
</div>
|
||||
</SettingsNavCard>
|
||||
</TabsList>
|
||||
@@ -2444,6 +2449,59 @@ function Content() {
|
||||
</div>
|
||||
</>
|
||||
</TabsContent>
|
||||
<TabsContent value="miscellaneous" className="space-y-4">
|
||||
<>
|
||||
<HeadingWithPageControls heading="Miscellaneous" />
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-[--muted]">
|
||||
Additional miscellaneous filters.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{/* <SettingsCard>
|
||||
<Switch
|
||||
label="Enable"
|
||||
side="right"
|
||||
value={userData.miscellaneous?.enabled ?? false}
|
||||
/>
|
||||
</SettingsCard> */}
|
||||
<SettingsCard
|
||||
title="Digital Release Filter"
|
||||
description="This will filter out all results for movies that are determined to not have a digital release."
|
||||
>
|
||||
<Switch
|
||||
label="Enable"
|
||||
side="right"
|
||||
value={userData.digitalReleaseFilter}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
digitalReleaseFilter: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</SettingsCard>
|
||||
{mode === 'pro' && (
|
||||
<SettingsCard
|
||||
title="Exclude Season Packs"
|
||||
description="Whether to filter out results that contain entire seasons."
|
||||
>
|
||||
<Switch
|
||||
label="Enable"
|
||||
side="right"
|
||||
value={userData.excludeSeasonPacks}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
excludeSeasonPacks: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</SettingsCard>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -362,24 +362,6 @@ function Content() {
|
||||
/>
|
||||
</SettingsCard>
|
||||
)}
|
||||
{mode === 'pro' && (
|
||||
<SettingsCard
|
||||
title="Exclude Season Packs"
|
||||
description="Whether to filter out results that contain entire seasons."
|
||||
>
|
||||
<Switch
|
||||
label="Enable"
|
||||
side="right"
|
||||
value={userData.excludeSeasonPacks}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
excludeSeasonPacks: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</SettingsCard>
|
||||
)}
|
||||
{mode === 'pro' && (
|
||||
<SettingsCard title="Hide Errors">
|
||||
<Switch
|
||||
|
||||
Reference in New Issue
Block a user