fix(smart collections): adds recently released filtered hubs

replacement for default hubs minus placeholders

re #128, re #115
This commit is contained in:
Tom Wheeler
2025-12-01 00:46:42 +13:00
parent 6083a79f06
commit c238da9d87
15 changed files with 292 additions and 124 deletions
+8 -8
View File
@@ -254,7 +254,7 @@ components:
'radarrtag',
'sonarrtag',
'comingsoon',
'recently_added',
'filtered_hub',
]
example: 'overseerr'
subtype:
@@ -702,7 +702,7 @@ components:
'radarrtag',
'sonarrtag',
'comingsoon',
'recently_added',
'filtered_hub',
]
example: 'overseerr'
subtype:
@@ -1185,7 +1185,7 @@ components:
'radarrtag',
'sonarrtag',
'comingsoon',
'recently_added',
'filtered_hub',
]
example: 'overseerr'
subtype:
@@ -1699,7 +1699,7 @@ components:
'radarrtag',
'sonarrtag',
'comingsoon',
'recently_added',
'filtered_hub',
]
description: 'Type of collection source'
example: 'trakt'
@@ -3754,7 +3754,7 @@ components:
radarrtag,
sonarrtag,
comingsoon,
recently_added,
filtered_hub,
multi-source,
]
example: 'trakt'
@@ -5832,7 +5832,7 @@ paths:
radarrtag,
sonarrtag,
comingsoon,
recently_added,
filtered_hub,
multi-source,
]
example: imdb
@@ -5914,7 +5914,7 @@ paths:
radarrtag,
sonarrtag,
comingsoon,
recently_added,
filtered_hub,
]
example: 'imdb'
subtype:
@@ -9478,7 +9478,7 @@ paths:
radarrtag,
sonarrtag,
comingsoon,
recently_added,
filtered_hub,
multi-source,
]
description: Filter by collection source
+3
View File
@@ -64,6 +64,9 @@ app
// Migrate comingsoon/recently_added to standalone recently_added type
settings.migrateComingSoonRecentlyAddedToStandalone();
// Migrate recently_added to filtered_hub type
settings.migrateRecentlyAddedToFilteredHub();
// Migrate poster templates to unified layering system for v1.3.2
await settings.migratePosterTemplatesV132();
+1 -1
View File
@@ -152,7 +152,7 @@ export type CollectionSource =
| 'radarrtag'
| 'sonarrtag'
| 'comingsoon'
| 'recently_added';
| 'filtered_hub';
/**
* Configuration for creating/updating collections in Plex
+55 -36
View File
@@ -1,11 +1,15 @@
/**
* Recently Added Collection Sync
* Filtered Hub Collection Sync
*
* Creates a smart collection that replicates Plex's "Recently Added" hub
* but excludes placeholder items created by the placeholder feature.
* Creates smart collections that replicate Plex's default hubs
* but exclude placeholder items created by the placeholder feature.
*
* Supports:
* - recently_added: Replicates "Recently Added" hub (sorted by addedAt)
* - recently_released: Replicates "Recently Released" hub (sorted by originallyAvailableAt)
*
* This is useful when users enable `createPlaceholdersForMissing` on their
* collections and want a clean "Recently Added" view without placeholders.
* collections and want clean hub views without placeholders.
*/
import type PlexAPI from '@server/api/plexapi';
@@ -30,13 +34,13 @@ import { CollectionSyncErrorType } from '@server/lib/collections/core/types';
import type { CollectionConfig } from '@server/lib/settings';
import logger from '@server/logger';
export class RecentlyAddedCollectionSync extends BaseCollectionSync {
export class FilteredHubCollectionSync extends BaseCollectionSync {
constructor() {
super('recently_added');
super('filtered_hub');
}
/**
* Validate that configuration is valid for recently_added collections
* Validate that configuration is valid for filtered_hub collections
*/
protected async validateConfiguration(): Promise<void> {
// No external API dependencies - just needs Plex
@@ -155,17 +159,30 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
templateContext
);
logger.info('Syncing Recently Added (filtered) collection', {
label: 'Recently Added Collections',
// Validate subtype
const subtype = config.subtype as 'recently_added' | 'recently_released';
if (
!subtype ||
!['recently_added', 'recently_released'].includes(subtype)
) {
throw this.createSyncError(
CollectionSyncErrorType.CONFIGURATION_ERROR,
`Invalid filtered_hub subtype: ${subtype}. Must be 'recently_added' or 'recently_released'`
);
}
logger.info('Syncing filtered hub collection', {
label: 'Filtered Hub Collections',
configName: config.name,
libraryId: config.libraryId,
mediaType,
subtype,
generatedName: collectionName,
});
// Check if smart collection already exists
// Define custom label for this collection
const customLabel = `Agregarr-recently_added-${config.id}`;
const customLabel = `Agregarr-filtered_hub-${config.id}`;
// Filter collections to only those in the target library
const libraryCollections = allCollections.filter(
@@ -198,9 +215,10 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
let collectionRatingKey: string;
if (existingCollection) {
logger.info('Recently Added (filtered) smart collection already exists', {
label: 'Recently Added Collections',
logger.info('Filtered hub smart collection already exists', {
label: 'Filtered Hub Collections',
collectionName,
subtype,
ratingKey: existingCollection.ratingKey,
});
@@ -219,23 +237,24 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
).default;
const smartCollectionManager = new PlexSmartCollectionManager(plexClient);
const smartCollectionKey =
await smartCollectionManager.createFilteredRecentlyAdded(
collectionName,
config.libraryId,
mediaType
);
const smartCollectionKey = await smartCollectionManager.createFilteredHub(
collectionName,
config.libraryId,
mediaType,
subtype
);
if (!smartCollectionKey) {
throw this.createSyncError(
CollectionSyncErrorType.COLLECTION_ERROR,
'Failed to create Recently Added (filtered) smart collection'
`Failed to create filtered hub smart collection (subtype: ${subtype})`
);
}
logger.info('Created Recently Added (filtered) smart collection', {
label: 'Recently Added Collections',
logger.info('Created filtered hub smart collection', {
label: 'Filtered Hub Collections',
collectionName,
subtype,
smartCollectionKey,
});
@@ -278,7 +297,7 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
if (shouldGeneratePoster) {
try {
logger.debug('Fetching items from collection for poster generation', {
label: 'Recently Added Collections',
label: 'Filtered Hub Collections',
collectionRatingKey,
collectionName,
});
@@ -289,7 +308,7 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
);
logger.debug('Fetched items from collection', {
label: 'Recently Added Collections',
label: 'Filtered Hub Collections',
collectionRatingKey,
itemCount: children.length,
});
@@ -332,17 +351,14 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
items
);
} catch (posterError) {
logger.warn(
'Failed to generate poster for Recently Added (filtered) collection',
{
label: 'Recently Added Collections',
collectionName,
error:
posterError instanceof Error
? posterError.message
: String(posterError),
}
);
logger.warn('Failed to generate poster for filtered hub collection', {
label: 'Filtered Hub Collections',
collectionName,
error:
posterError instanceof Error
? posterError.message
: String(posterError),
});
// Don't fail the sync if poster generation fails
}
}
@@ -352,5 +368,8 @@ export class RecentlyAddedCollectionSync extends BaseCollectionSync {
}
// Export singleton instance
export const recentlyAddedCollectionSync = new RecentlyAddedCollectionSync();
export default recentlyAddedCollectionSync;
export const filteredHubCollectionSync = new FilteredHubCollectionSync();
export default filteredHubCollectionSync;
// Legacy export for backwards compatibility
export const recentlyAddedCollectionSync = filteredHubCollectionSync;
@@ -263,45 +263,69 @@ class PlexSmartCollectionManager {
}
/**
* Create a filtered Recently Added smart collection that excludes coming soon placeholders
* @param title - Title for the smart collection (usually "Recently Added")
* Create a filtered hub replacement smart collection that excludes coming soon placeholders
* Supports: recently_added, recently_released
* @param title - Title for the smart collection
* @param libraryKey - Library section key (e.g., "1" for movies)
* @param mediaType - 'movie' or 'tv'
* @param subtype - Hub subtype ('recently_added' or 'recently_released')
* @returns The rating key of the created smart collection or null if failed
*/
public async createFilteredRecentlyAdded(
public async createFilteredHub(
title: string,
libraryKey: string,
mediaType: 'movie' | 'tv'
mediaType: 'movie' | 'tv',
subtype: 'recently_added' | 'recently_released'
): Promise<string | null> {
try {
logger.debug(
`Creating filtered Recently Added smart collection "${title}" for library ${libraryKey}`,
`Creating filtered hub smart collection "${title}" for library ${libraryKey}`,
{
label: 'Plex API',
title,
libraryKey,
mediaType,
subtype,
}
);
const type = mediaType === 'movie' ? 1 : 2;
// Build filter URI based on media type
// Build filter URI based on media type and subtype
let filterUri: string;
if (mediaType === 'tv') {
// TV Shows: Sort by Last Episode Date Added (lastViewedAt), filter out "Trailer (Placeholder)"
// Note: Plex uses title!= for "is not" filter
const sortParam = 'lastViewedAt:desc';
const titleFilter = encodeURIComponent('Trailer (Placeholder)');
filterUri = `/library/sections/${libraryKey}/all?type=${type}&sort=${sortParam}&episode.title!=${titleFilter}`;
if (subtype === 'recently_added') {
// Recently Added: Sort by Date Added (addedAt), exclude placeholders
if (mediaType === 'tv') {
// TV Shows: Filter out "Trailer (Placeholder)" episode titles
const sortParam = 'addedAt:desc';
const titleFilter = encodeURIComponent('Trailer (Placeholder)');
filterUri = `/library/sections/${libraryKey}/all?type=${type}&sort=${sortParam}&episode.title!=${titleFilter}`;
} else {
// Movies: Filter out "trailer-placeholder" label
const sortParam = 'addedAt:desc';
const labelFilter = 'trailer-placeholder';
filterUri = `/library/sections/${libraryKey}/all?type=${type}&sort=${sortParam}&label!=${encodeURIComponent(
labelFilter
)}`;
}
} else if (subtype === 'recently_released') {
// Recently Released: Sort by Release Date (originallyAvailableAt), exclude placeholders
if (mediaType === 'tv') {
// TV Shows (Episodes): Sort by air date, filter out "Trailer (Placeholder)"
const sortParam = 'originallyAvailableAt:desc';
const titleFilter = encodeURIComponent('Trailer (Placeholder)');
filterUri = `/library/sections/${libraryKey}/all?type=${type}&sort=${sortParam}&episode.title!=${titleFilter}`;
} else {
// Movies: Sort by release date, filter out "trailer-placeholder" label
const sortParam = 'originallyAvailableAt:desc';
const labelFilter = 'trailer-placeholder';
filterUri = `/library/sections/${libraryKey}/all?type=${type}&sort=${sortParam}&label!=${encodeURIComponent(
labelFilter
)}`;
}
} else {
// Movies: Sort by Date Added (addedAt), filter out "trailer-placeholder" label
const sortParam = 'addedAt:desc';
const labelFilter = 'trailer-placeholder';
filterUri = `/library/sections/${libraryKey}/all?type=${type}&sort=${sortParam}&label!=${encodeURIComponent(
labelFilter
)}`;
throw new Error(`Unsupported filtered hub subtype: ${subtype}`);
}
const uri = `server://${
@@ -320,7 +344,7 @@ class PlexSmartCollectionManager {
!('MediaContainer' in createResponse)
) {
logger.error(
'Invalid response when creating filtered Recently Added smart collection',
'Invalid response when creating filtered hub smart collection',
{
label: 'Plex API',
response: createResponse,
@@ -335,7 +359,7 @@ class PlexSmartCollectionManager {
if (!mediaContainer.Metadata || mediaContainer.Metadata.length === 0) {
logger.error(
'No metadata returned when creating filtered Recently Added smart collection',
'No metadata returned when creating filtered hub smart collection',
{
label: 'Plex API',
response: createResponse,
@@ -352,30 +376,46 @@ class PlexSmartCollectionManager {
// Note: Labels, titles, and visibility are handled by updateCollectionMetadata in the sync flow
logger.info(
`Successfully created filtered Recently Added smart collection "${title}" with rating key ${smartCollectionRatingKey}`,
`Successfully created filtered hub smart collection "${title}" with rating key ${smartCollectionRatingKey}`,
{
label: 'Plex API',
title,
smartCollectionRatingKey,
mediaType,
subtype,
}
);
return smartCollectionRatingKey;
} catch (error) {
logger.error(
`Error creating filtered Recently Added smart collection "${title}"`,
{
label: 'Plex API',
title,
libraryKey,
mediaType,
error: error instanceof Error ? error.message : String(error),
}
);
logger.error(`Error creating filtered hub smart collection "${title}"`, {
label: 'Plex API',
title,
libraryKey,
mediaType,
subtype,
error: error instanceof Error ? error.message : String(error),
});
return null;
}
}
/**
* @deprecated Use createFilteredHub instead
* Legacy method for backwards compatibility
*/
public async createFilteredRecentlyAdded(
title: string,
libraryKey: string,
mediaType: 'movie' | 'tv'
): Promise<string | null> {
return this.createFilteredHub(
title,
libraryKey,
mediaType,
'recently_added'
);
}
}
export default PlexSmartCollectionManager;
@@ -558,11 +558,11 @@ export class CollectionSyncService {
);
return new ComingSoonCollectionSync();
}
case 'recently_added': {
const { RecentlyAddedCollectionSync } = await import(
case 'filtered_hub': {
const { FilteredHubCollectionSync } = await import(
'../external/recentlyadded'
);
return new RecentlyAddedCollectionSync();
return new FilteredHubCollectionSync();
}
case 'multi-source':
throw new Error(
+68 -5
View File
@@ -51,7 +51,7 @@ export interface CollectionConfig {
| 'radarrtag'
| 'sonarrtag'
| 'comingsoon'
| 'recently_added';
| 'filtered_hub';
readonly subtype?: string; // Specific option like 'users', 'most_popular_plays', 'most_popular_duration', etc. Optional for types like recently_added
readonly template: string; // Collection template
readonly customMovieTemplate?: string; // Custom template for movie collections when mediaType is 'both'
@@ -1266,7 +1266,7 @@ class Settings {
) {
migratedCount++;
logger.info(
`Migrating comingsoon/recently_added config "${config.name}" to standalone recently_added type`,
`Migrating comingsoon/recently_added config "${config.name}" to filtered_hub type with subtype recently_added`,
{
label: 'Settings Migration',
configId: config.id,
@@ -1275,8 +1275,8 @@ class Settings {
return {
...config,
type: 'recently_added' as const,
subtype: undefined, // recently_added doesn't have subtypes
type: 'filtered_hub' as const,
subtype: 'recently_added', // filtered_hub requires a subtype
};
}
@@ -1286,7 +1286,70 @@ class Settings {
if (migratedCount > 0) {
logger.info(
`Migrated ${migratedCount} comingsoon/recently_added config(s) to standalone recently_added type`,
`Migrated ${migratedCount} comingsoon/recently_added config(s) to filtered_hub type`,
{
label: 'Settings Migration',
}
);
}
this.data.completedMigrations.push(migrationId);
this.save();
}
/**
* Migrate recently_added type to filtered_hub with subtype recently_added
* This is a one-time migration for the filtered hub refactoring
*/
public migrateRecentlyAddedToFilteredHub(): void {
const migrationId = 'recently-added-to-filtered-hub';
// Initialize completedMigrations if it doesn't exist
if (!this.data.completedMigrations) {
this.data.completedMigrations = [];
}
// Skip if already completed
if (this.data.completedMigrations.includes(migrationId)) {
return;
}
if (!this.data.plex.collectionConfigs) {
this.data.completedMigrations.push(migrationId);
this.save();
return;
}
let migratedCount = 0;
this.data.plex.collectionConfigs = this.data.plex.collectionConfigs.map(
(config) => {
// Check if this is a recently_added config that needs migration
// Type assertion needed because 'recently_added' is a legacy type
if ((config.type as string) === 'recently_added') {
migratedCount++;
logger.info(
`Migrating recently_added config "${config.name}" to filtered_hub type with subtype recently_added`,
{
label: 'Settings Migration',
configId: config.id,
}
);
return {
...config,
type: 'filtered_hub' as const,
subtype: 'recently_added', // Set subtype to recently_added
};
}
return config;
}
);
if (migratedCount > 0) {
logger.info(
`Migrated ${migratedCount} recently_added config(s) to filtered_hub type`,
{
label: 'Settings Migration',
}
@@ -116,7 +116,7 @@ const CollectionTypeSection = ({
{ value: 'radarrtag', label: 'Radarr Tag' },
{ value: 'sonarrtag', label: 'Sonarr Tag' },
{ value: 'comingsoon', label: 'Coming Soon' },
{ value: 'recently_added', label: 'Recently Added (filtered)' },
{ value: 'filtered_hub', label: 'Filtered Plex Hub' },
{ value: 'multi-source', label: 'Multiple Sources' },
];
@@ -345,8 +345,20 @@ const CollectionTypeSection = ({
case 'radarrtag':
case 'sonarrtag':
return []; // These use custom tag selectors instead of subtypes
case 'recently_added':
return []; // No subtypes - standalone type that creates filtered smart collection
case 'filtered_hub':
return [
{
value: 'recently_added',
label: 'Recently Added',
description: 'Replaces Recently Added hub (sorted by date added)',
},
{
value: 'recently_released',
label: 'Recently Released',
description:
'Replaces Recently Released hub (sorted by release date)',
},
];
default:
return [];
}
@@ -350,10 +350,10 @@ const TimeRestrictionsSection = ({
</div>
)}
{/* Custom Sync Schedule Section - Only for Agregarr collections (not hubs, pre-existing, or recently_added) */}
{/* Custom Sync Schedule Section - Only for Agregarr collections (not hubs, pre-existing, or filtered_hub) */}
{!isDefaultPlexHub &&
!isPreExisting &&
values.type !== 'recently_added' && (
values.type !== 'filtered_hub' && (
<div className="mt-6">
<label className="mb-4 block text-sm font-medium text-gray-200">
{intl.formatMessage(messages.customSyncSchedule)}
@@ -251,7 +251,7 @@ const CollectionFormConfigForm = ({
type !== 'multi-source' &&
type !== 'radarrtag' &&
type !== 'sonarrtag' &&
type !== 'recently_added', // Only required if not a hub, pre-existing, multi-source, tag-based, or recently_added
type !== 'filtered_hub', // Only required if not a hub, pre-existing, multi-source, tag-based, or recently_added
then: (schema) => schema.required('Collection sub-type is required'),
otherwise: (schema) => schema.notRequired(),
}),
@@ -1686,7 +1686,7 @@ const CollectionFormConfigForm = ({
? hasSelectedRadarrTag
: values.type === 'sonarrtag'
? hasSelectedSonarrTag
: values.type === 'recently_added'
: values.type === 'filtered_hub'
? true // recently_added doesn't require a subtype
: values.subtype) && // Radarr/Sonarr tag collections require a tag instead of subtype
// For Trakt time-period subtypes, also require timePeriod to be selected
@@ -1767,7 +1767,7 @@ const CollectionFormConfigForm = ({
? hasSelectedRadarrTag
: values.type === 'sonarrtag'
? hasSelectedSonarrTag
: values.type === 'recently_added'
: values.type === 'filtered_hub'
? true // recently_added doesn't require a subtype
: values.subtype) &&
(values.libraryIds?.length > 0 || values.libraryId) &&
@@ -1812,7 +1812,7 @@ const CollectionFormConfigForm = ({
isCollection &&
values.type &&
(values.type === 'multi-source' ||
values.type === 'recently_added' ||
values.type === 'filtered_hub' ||
(values.type === 'radarrtag'
? hasSelectedRadarrTag
: values.type === 'sonarrtag'
@@ -1827,7 +1827,7 @@ const CollectionFormConfigForm = ({
{/* Item Order - available for all collection types except multi-source and recently_added */}
{values.type !== 'multi-source' &&
values.type !== 'recently_added' && (
values.type !== 'filtered_hub' && (
<div className="form-row">
<label
htmlFor="itemOrder"
@@ -1951,7 +1951,7 @@ const CollectionFormConfigForm = ({
</div>
{/* Max Items - not applicable for recently_added */}
{values.type !== 'recently_added' && (
{values.type !== 'filtered_hub' && (
<div className="form-row">
<label
htmlFor="collectionMaxItems"
@@ -1983,7 +1983,7 @@ const CollectionFormConfigForm = ({
{/* Smart Collection - Show Unwatched Only */}
{/* Hide for: recently_added (already smart), and tmdb auto_franchise (multi-collection) */}
{values.type !== 'recently_added' &&
{values.type !== 'filtered_hub' &&
!(
values.type === 'tmdb' &&
values.subtype === 'auto_franchise'
@@ -2137,7 +2137,7 @@ const CollectionFormConfigForm = ({
{typedValues.type &&
typedValues.type !== 'overseerr' &&
typedValues.type !== 'tautulli' &&
typedValues.type !== 'recently_added' &&
typedValues.type !== 'filtered_hub' &&
!(
typedValues.type === 'tmdb' &&
typedValues.subtype === 'auto_franchise'
@@ -2243,7 +2243,7 @@ const CollectionFormConfigForm = ({
{typedValues.type &&
typedValues.type !== 'overseerr' &&
typedValues.type !== 'tautulli' &&
typedValues.type !== 'recently_added' &&
typedValues.type !== 'filtered_hub' &&
!(
typedValues.type === 'tmdb' &&
typedValues.subtype === 'auto_franchise'
@@ -1559,23 +1559,55 @@ export const getTemplatePresets = (
}
}
// Recently Added (filtered) collection presets
if (values.type === 'recently_added') {
return [
{
label: 'Recently Added',
value: 'Recently Added',
},
{
label: 'Recently Added (Filtered)',
value: 'Recently Added (Filtered)',
},
{
label: 'New Arrivals',
value: 'New Arrivals',
},
{ label: 'Custom', value: 'custom' },
];
// Filtered Hub collection presets - different presets per subtype
if (values.type === 'filtered_hub') {
switch (values.subtype) {
case 'recently_added':
return [
{
label: 'Recently Added',
value: 'Recently Added',
},
{
label: 'New Arrivals',
value: 'New Arrivals',
},
{
label: 'Latest Additions',
value: 'Latest Additions',
},
{ label: 'Custom', value: 'custom' },
];
case 'recently_released':
return [
{
label: 'Recently Released',
value: 'Recently Released',
},
{
label: 'New Releases',
value: 'New Releases',
},
{
label: 'Latest Releases',
value: 'Latest Releases',
},
{
label: 'Just Released',
value: 'Just Released',
},
{ label: 'Custom', value: 'custom' },
];
default:
// Fallback if no subtype selected yet
return [
{
label: 'Filtered Hub',
value: 'Filtered Hub',
},
{ label: 'Custom', value: 'custom' },
];
}
}
// Fallback for unknown types
@@ -1088,7 +1088,7 @@ const AllCollectionsView: React.FC = () => {
? 'Multi-Source'
: config.type === 'comingsoon'
? 'Coming Soon'
: config.type === 'recently_added'
: config.type === 'filtered_hub'
? 'Recently Added'
: config.type || '';
@@ -708,7 +708,7 @@ const SortableItem = ({
? 'Multi-Source'
: collection.type === 'comingsoon'
? 'Coming Soon'
: collection.type === 'recently_added'
: collection.type === 'filtered_hub'
? 'Recently Added'
: collection.type || '';
+4 -4
View File
@@ -144,7 +144,7 @@ export interface CollectionFormConfig {
| 'radarrtag'
| 'sonarrtag'
| 'comingsoon'
| 'recently_added';
| 'filtered_hub';
readonly subtype?: string; // Specific option like 'users', 'most_popular_plays', etc. - optional for hubs/pre-existing
readonly timePeriod?: 'daily' | 'weekly' | 'monthly' | 'all'; // Time period for Trakt time-based subtypes
readonly configType?: FormConfigType; // Metadata for form behavior identification
@@ -336,7 +336,7 @@ export interface CollectionConfigCreateRequest {
| 'radarrtag'
| 'sonarrtag'
| 'comingsoon'
| 'recently_added';
| 'filtered_hub';
readonly subtype?: string;
readonly template?: string;
readonly customMovieTemplate?: string;
@@ -640,7 +640,7 @@ export type CollectionSourceType =
| 'radarrtag'
| 'sonarrtag'
| 'comingsoon'
| 'recently_added';
| 'filtered_hub';
export type MediaType = 'movie' | 'tv';
/**
@@ -888,7 +888,7 @@ export type MultiSourceType =
| 'anilist'
| 'myanimelist'
| 'comingsoon'
| 'recently_added';
| 'filtered_hub';
/**
* Source definition for multi-source collections
+2 -3
View File
@@ -237,13 +237,12 @@ export function validateCollectionFormConfig(
errors.push('Collection type is required');
}
// Subtype not required for multi-source, tag-based, or recently_added collections
// Subtype not required for multi-source or tag-based collections (but required for filtered_hub)
if (
!config.subtype &&
config.type !== 'multi-source' &&
config.type !== 'radarrtag' &&
config.type !== 'sonarrtag' &&
config.type !== 'recently_added'
config.type !== 'sonarrtag'
) {
errors.push('Collection subtype is required');
}