mirror of
https://github.com/agregarr/agregarr.git
synced 2026-05-04 17:09:23 -05:00
fix(smart collections): adds recently released filtered hubs
replacement for default hubs minus placeholders re #128, re #115
This commit is contained in:
+8
-8
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ export type CollectionSource =
|
||||
| 'radarrtag'
|
||||
| 'sonarrtag'
|
||||
| 'comingsoon'
|
||||
| 'recently_added';
|
||||
| 'filtered_hub';
|
||||
|
||||
/**
|
||||
* Configuration for creating/updating collections in Plex
|
||||
|
||||
+55
-36
@@ -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
@@ -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 || '';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user