diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index cdfa17c3a3..866b1cd83e 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -134,8 +134,8 @@ export class MediaRequest {
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
- status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
- status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
+ status: MediaStatus.UNKNOWN,
+ status4k: MediaStatus.UNKNOWN,
mediaType: requestBody.mediaType,
});
} else {
@@ -148,14 +148,6 @@ export class MediaRequest {
throw new BlacklistedMediaError('This media is blacklisted.');
}
-
- if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
- media.status = MediaStatus.PENDING;
- }
-
- if (media.status4k === MediaStatus.UNKNOWN && requestBody.is4k) {
- media.status4k = MediaStatus.PENDING;
- }
}
const existing = await requestRepository
@@ -211,20 +203,29 @@ export class MediaRequest {
let tags = requestBody.tags;
if (useOverrides) {
- const defaultRadarrId = requestBody.is4k
- ? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
- : settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
- const defaultSonarrId = requestBody.is4k
- ? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
- : settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
-
const overrideRuleRepository = getRepository(OverrideRule);
- const overrideRules = await overrideRuleRepository.find({
- where:
- requestBody.mediaType === MediaType.MOVIE
- ? { radarrServiceId: defaultRadarrId }
- : { sonarrServiceId: defaultSonarrId },
- });
+
+ // Get override rules for all services (both 4K and non-4K) to allow service type switching
+ let overrideRules: OverrideRule[] = [];
+ if (requestBody.mediaType === MediaType.MOVIE) {
+ // Get rules for all Radarr services
+ const radarrServiceIds = settings.radarr.map((_, index) => index);
+ overrideRules = await overrideRuleRepository
+ .createQueryBuilder('rule')
+ .where('rule.radarrServiceId IN (:...serviceIds)', {
+ serviceIds: radarrServiceIds,
+ })
+ .getMany();
+ } else {
+ // Get rules for all Sonarr services
+ const sonarrServiceIds = settings.sonarr.map((_, index) => index);
+ overrideRules = await overrideRuleRepository
+ .createQueryBuilder('rule')
+ .where('rule.sonarrServiceId IN (:...serviceIds)', {
+ serviceIds: sonarrServiceIds,
+ })
+ .getMany();
+ }
const appliedOverrideRules = overrideRules.filter((rule) => {
const hasAnimeKeyword =
@@ -291,13 +292,47 @@ export class MediaRequest {
) {
return false;
}
+ if (rule.years) {
+ let releaseYear: number;
+ if (requestBody.mediaType === MediaType.MOVIE) {
+ releaseYear = new Date(
+ (tmdbMedia as any).release_date
+ ).getFullYear();
+ } else {
+ releaseYear = new Date(
+ (tmdbMedia as any).first_air_date
+ ).getFullYear();
+ }
+
+ const yearMatches = rule.years.split(',').some((yearRange) => {
+ const trimmedRange = yearRange.trim();
+ if (trimmedRange.includes('-')) {
+ // Handle ranges like "2000-2010"
+ const [startYear, endYear] = trimmedRange.split('-').map(Number);
+ return releaseYear >= startYear && releaseYear <= endYear;
+ } else {
+ // Handle individual years
+ return Number(trimmedRange) === releaseYear;
+ }
+ });
+
+ if (!yearMatches) {
+ return false;
+ }
+ }
return true;
});
// hacky way to prioritize rules
// TODO: make this better
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
- const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
+ const keys: (keyof OverrideRule)[] = [
+ 'genre',
+ 'language',
+ 'keywords',
+ 'years',
+ 'serviceSwitch',
+ ];
const aSpecificity = keys.filter((key) => a[key] !== null).length;
const bSpecificity = keys.filter((key) => b[key] !== null).length;
@@ -307,11 +342,82 @@ export class MediaRequest {
})[0];
if (prioritizedRule) {
- if (prioritizedRule.rootFolder) {
- rootFolder = prioritizedRule.rootFolder;
+ let serviceSwitched = false;
+
+ // Handle intelligent service switching based on serviceSwitch field
+ if (prioritizedRule.serviceSwitch) {
+ const originalIs4k = requestBody.is4k;
+ let targetIs4k = originalIs4k;
+
+ // Determine target service type based on serviceSwitch setting
+ switch (prioritizedRule.serviceSwitch) {
+ case 'force4k':
+ targetIs4k = true;
+ break;
+ case 'forceStandard':
+ targetIs4k = false;
+ break;
+ case 'auto':
+ // Keep original request type
+ targetIs4k = originalIs4k;
+ break;
+ }
+
+ // Apply service switching if type changed
+ if (targetIs4k !== originalIs4k) {
+ serviceSwitched = true;
+ requestBody.is4k = targetIs4k;
+
+ // Find appropriate default service for the target type and use its defaults
+ if (requestBody.mediaType === MediaType.MOVIE) {
+ const targetService = targetIs4k
+ ? settings.radarr.find((r) => r.is4k && r.isDefault)
+ : settings.radarr.find((r) => !r.is4k && r.isDefault);
+
+ if (targetService) {
+ requestBody.serverId = targetService.id;
+ // Reset to service defaults when switching to prevent conflicts
+ profileId = targetService.activeProfileId;
+ rootFolder = targetService.activeDirectory;
+ }
+ } else {
+ const targetService = targetIs4k
+ ? settings.sonarr.find((s) => s.is4k && s.isDefault)
+ : settings.sonarr.find((s) => !s.is4k && s.isDefault);
+
+ if (targetService) {
+ requestBody.serverId = targetService.id;
+ // Reset to service defaults when switching to prevent conflicts
+ profileId = targetService.activeProfileId;
+ rootFolder = targetService.activeDirectory;
+ }
+ }
+ }
+
+ // When service switching is enabled, ignore specific service IDs to prevent conflicts
+ } else {
+ // No service switching - apply specific service IDs from override rule
+ if (
+ prioritizedRule.radarrServiceId !== null &&
+ prioritizedRule.radarrServiceId !== undefined
+ ) {
+ requestBody.serverId = prioritizedRule.radarrServiceId;
+ } else if (
+ prioritizedRule.sonarrServiceId !== null &&
+ prioritizedRule.sonarrServiceId !== undefined
+ ) {
+ requestBody.serverId = prioritizedRule.sonarrServiceId;
+ }
}
- if (prioritizedRule.profileId) {
- profileId = prioritizedRule.profileId;
+
+ // Only apply override rule's profile and root folder if no service switching occurred
+ if (!serviceSwitched) {
+ if (prioritizedRule.rootFolder) {
+ rootFolder = prioritizedRule.rootFolder;
+ }
+ if (prioritizedRule.profileId) {
+ profileId = prioritizedRule.profileId;
+ }
}
if (prioritizedRule.tags) {
tags = [
@@ -329,6 +435,17 @@ export class MediaRequest {
}
}
+ // Update media status based on final request type (after potential service switching)
+ if (media.status !== MediaStatus.BLACKLISTED) {
+ if (!requestBody.is4k && media.status === MediaStatus.UNKNOWN) {
+ media.status = MediaStatus.PENDING;
+ }
+
+ if (requestBody.is4k && media.status4k === MediaStatus.UNKNOWN) {
+ media.status4k = MediaStatus.PENDING;
+ }
+ }
+
if (requestBody.mediaType === MediaType.MOVIE) {
await mediaRepository.save(media);
diff --git a/server/entity/OverrideRule.ts b/server/entity/OverrideRule.ts
index aab72006f4..0505be66bc 100644
--- a/server/entity/OverrideRule.ts
+++ b/server/entity/OverrideRule.ts
@@ -24,6 +24,12 @@ class OverrideRule {
@Column({ nullable: true })
public keywords?: string;
+ @Column({ nullable: true })
+ public years?: string;
+
+ @Column({ nullable: true })
+ public serviceSwitch?: string;
+
@Column({ type: 'int', nullable: true })
public profileId?: number;
diff --git a/server/migration/postgres/1755781927232-AddYearsToOverrideRule.ts b/server/migration/postgres/1755781927232-AddYearsToOverrideRule.ts
new file mode 100644
index 0000000000..819e9c3817
--- /dev/null
+++ b/server/migration/postgres/1755781927232-AddYearsToOverrideRule.ts
@@ -0,0 +1,15 @@
+import type { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddYearsToOverrideRule1755781927232 implements MigrationInterface {
+ name = 'AddYearsToOverrideRule1755781927232';
+
+ public async up(queryRunner: QueryRunner): Promise
{intl.formatMessage(messages.settingsDescription)}
+
+ Recommendations: 1980-{new Date().getFullYear()} (all),
+ 2000-{new Date().getFullYear()} (modern), 1980-1999
+ (classic)
+
+
{intl.formatMessage(messages.settings)}
+ + {intl.formatMessage(messages.years)} + + {rule.years} +
+ )} {intl.formatMessage(messages.settings)} + {rule.serviceSwitch && ( ++ + {intl.formatMessage(messages.serviceSwitch)} + + {rule.serviceSwitch === 'force4k' + ? intl.formatMessage(messages.serviceSwitchForce4k) + : rule.serviceSwitch === 'forceStandard' + ? intl.formatMessage(messages.serviceSwitchForceStandard) + : intl.formatMessage(messages.serviceSwitchAuto)} +
+ )} {rule.profileId && (diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index c088143a50..d9a567be57 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -790,6 +790,13 @@ "components.Settings.OverrideRuleModal.settingsDescription": "Specifies which settings will be changed when the above conditions are met.", "components.Settings.OverrideRuleModal.tags": "Tags", "components.Settings.OverrideRuleModal.users": "Users", + "components.Settings.OverrideRuleModal.years": "Years", + "components.Settings.OverrideRuleModal.yearsDescription": "Filter by release/air date year. Use comma-separated values (e.g., 1980,1995) or ranges (e.g., 2000-2010)", + "components.Settings.OverrideRuleModal.serviceSwitch": "Service Switch", + "components.Settings.OverrideRuleModal.serviceSwitchDescription": "Automatically switch between 4K and standard quality based on the rule conditions", + "components.Settings.OverrideRuleModal.serviceSwitchAuto": "Keep Original Request Type", + "components.Settings.OverrideRuleModal.serviceSwitchForce4k": "Force 4K Quality", + "components.Settings.OverrideRuleModal.serviceSwitchForceStandard": "Force Standard Quality", "components.Settings.OverrideRuleTile.conditions": "Conditions", "components.Settings.OverrideRuleTile.genre": "Genre", "components.Settings.OverrideRuleTile.keywords": "Keywords", @@ -799,6 +806,11 @@ "components.Settings.OverrideRuleTile.settings": "Settings", "components.Settings.OverrideRuleTile.tags": "Tags", "components.Settings.OverrideRuleTile.users": "Users", + "components.Settings.OverrideRuleTile.years": "Years", + "components.Settings.OverrideRuleTile.serviceSwitch": "Service Switch", + "components.Settings.OverrideRuleTile.serviceSwitchForce4k": "Force 4K", + "components.Settings.OverrideRuleTile.serviceSwitchForceStandard": "Force Standard", + "components.Settings.OverrideRuleTile.serviceSwitchAuto": "Keep Original", "components.Settings.RadarrModal.add": "Add Server", "components.Settings.RadarrModal.announced": "Announced", "components.Settings.RadarrModal.apiKey": "API Key",