From 57f7712f61d360a5327b10742f397b6de16a463f Mon Sep 17 00:00:00 2001 From: jorenn92 Date: Fri, 26 Jan 2024 12:41:43 +0100 Subject: [PATCH 1/2] fix(rules): Addressed an issue where certain collection-related rules exhibited unexpected behavior when media was added to other groups in the same run --- server/src/modules/api/lib/cache.ts | 6 ++ .../collections/collections.service.ts | 91 ++++++++++--------- .../rules/constants/constants.service.ts | 12 ++- .../rules/constants/rules.constants.ts | 3 + server/src/modules/rules/rules.service.ts | 55 +++++++++++ .../rules/tasks/rule-executor.service.ts | 13 ++- 6 files changed, 128 insertions(+), 52 deletions(-) diff --git a/server/src/modules/api/lib/cache.ts b/server/src/modules/api/lib/cache.ts index 66da6eb4..9396f5ae 100644 --- a/server/src/modules/api/lib/cache.ts +++ b/server/src/modules/api/lib/cache.ts @@ -58,6 +58,12 @@ class CacheManager { public getAllCaches(): Record { return this.availableCaches; } + + public flushAll(): void { + for (const [key, value] of Object.entries(this.getAllCaches())) { + value.flush(); + } + } } const cacheManager = new CacheManager(); diff --git a/server/src/modules/collections/collections.service.ts b/server/src/modules/collections/collections.service.ts index 2c0c32fe..c6eedfea 100644 --- a/server/src/modules/collections/collections.service.ts +++ b/server/src/modules/collections/collections.service.ts @@ -98,21 +98,23 @@ export class CollectionsService { const itemCount = await queryBuilder.getCount(); let { entities } = await queryBuilder.getRawAndEntities(); - entities = await Promise.all( - entities.map(async (el) => { - el.plexData = await this.plexApi.getMetadata(el.plexId.toString()); - if (el.plexData?.grandparentRatingKey) { - el.plexData.parentData = await this.plexApi.getMetadata( - el.plexData.grandparentRatingKey.toString(), - ); - } else if (el.plexData?.parentRatingKey) { - el.plexData.parentData = await this.plexApi.getMetadata( - el.plexData.parentRatingKey.toString(), - ); - } - return el; - }), - ); + entities = ( + await Promise.all( + entities.map(async (el) => { + el.plexData = await this.plexApi.getMetadata(el.plexId.toString()); + if (el.plexData?.grandparentRatingKey) { + el.plexData.parentData = await this.plexApi.getMetadata( + el.plexData.grandparentRatingKey.toString(), + ); + } else if (el.plexData?.parentRatingKey) { + el.plexData.parentData = await this.plexApi.getMetadata( + el.plexData.parentRatingKey.toString(), + ); + } + return el; + }), + ) + ).filter((el) => el.plexData !== undefined); return { totalSize: itemCount, @@ -152,21 +154,23 @@ export class CollectionsService { const itemCount = await queryBuilder.getCount(); let { entities } = await queryBuilder.getRawAndEntities(); - entities = await Promise.all( - entities.map(async (el) => { - el.plexData = await this.plexApi.getMetadata(el.plexId.toString()); - if (el.plexData?.grandparentRatingKey) { - el.plexData.parentData = await this.plexApi.getMetadata( - el.plexData.grandparentRatingKey.toString(), - ); - } else if (el.plexData?.parentRatingKey) { - el.plexData.parentData = await this.plexApi.getMetadata( - el.plexData.parentRatingKey.toString(), - ); - } - return el; - }), - ); + entities = ( + await Promise.all( + entities.map(async (el) => { + el.plexData = await this.plexApi.getMetadata(el.plexId.toString()); + if (el.plexData?.grandparentRatingKey) { + el.plexData.parentData = await this.plexApi.getMetadata( + el.plexData.grandparentRatingKey.toString(), + ); + } else if (el.plexData?.parentRatingKey) { + el.plexData.parentData = await this.plexApi.getMetadata( + el.plexData.parentRatingKey.toString(), + ); + } + return el; + }), + ) + ).filter((el) => el.plexData !== undefined); return { totalSize: itemCount, @@ -832,18 +836,21 @@ export class CollectionsService { ) { // log record const plexData = await this.plexApi.getMetadata(plexId.toString()); // fetch data from cache - - const subject = - plexData.type === 'episode' - ? `${plexData.grandparentTitle} - season ${plexData.parentIndex} - episode ${plexData.index}` - : plexData.type === 'season' - ? `${plexData.parentTitle} - season ${plexData.index}` - : plexData.title; - this.addLogRecord( - { id: collectionId } as Collection, - `${type === 'add' ? 'Added' : type === 'handle' ? 'Successfully handled' : type === 'exclude' ? 'Added a specific exclusion for' : type === 'include' ? 'Removed specific exclusion of' : 'Removed'} "${subject}"`, - ECollectionLogType.MEDIA, - ); + // if there's no data.. skip logging + + if (plexData) { + const subject = + plexData.type === 'episode' + ? `${plexData.grandparentTitle} - season ${plexData.parentIndex} - episode ${plexData.index}` + : plexData.type === 'season' + ? `${plexData.parentTitle} - season ${plexData.index}` + : plexData.title; + this.addLogRecord( + { id: collectionId } as Collection, + `${type === 'add' ? 'Added' : type === 'handle' ? 'Successfully handled' : type === 'exclude' ? 'Added a specific exclusion for' : type === 'include' ? 'Removed specific exclusion of' : 'Removed'} "${subject}"`, + ECollectionLogType.MEDIA, + ); + } } private async removeChildFromCollection( diff --git a/server/src/modules/rules/constants/constants.service.ts b/server/src/modules/rules/constants/constants.service.ts index 74f466a0..e8fdc1d7 100644 --- a/server/src/modules/rules/constants/constants.service.ts +++ b/server/src/modules/rules/constants/constants.service.ts @@ -31,11 +31,13 @@ export class RuleConstanstService { } public getValueHumanName(location: [number, number]) { - return `${this.ruleConstants.applications.find( - (el) => el.id === location[0], - )?.name} - ${this.ruleConstants.applications - .find((el) => el.id === location[0]) - ?.props.find((el) => el.id === location[1])?.humanName}`; + return `${ + this.ruleConstants.applications.find((el) => el.id === location[0])?.name + } - ${ + this.ruleConstants.applications + .find((el) => el.id === location[0]) + ?.props.find((el) => el.id === location[1])?.humanName + }`; } public getValueFromIdentifier(identifier: string): [number, number] { diff --git a/server/src/modules/rules/constants/rules.constants.ts b/server/src/modules/rules/constants/rules.constants.ts index ccd1b3da..b3e5942f 100644 --- a/server/src/modules/rules/constants/rules.constants.ts +++ b/server/src/modules/rules/constants/rules.constants.ts @@ -98,6 +98,7 @@ export interface Property { type: RuleType; mediaType: MediaType; humanName: string; + cacheReset?: boolean; // for properties that require a cache reset between group executions } export interface ApplicationProperties { @@ -163,6 +164,7 @@ export class RuleConstants { humanName: 'Present in amount of other collections', mediaType: MediaType.BOTH, type: RuleType.NUMBER, + cacheReset: true, } as Property, { id: 7, @@ -264,6 +266,7 @@ export class RuleConstants { humanName: '[list] Collections media is present in (titles)', mediaType: MediaType.BOTH, type: RuleType.TEXT, + cacheReset: true, } as Property, { id: 20, diff --git a/server/src/modules/rules/rules.service.ts b/server/src/modules/rules/rules.service.ts index 21078703..e94fe53b 100644 --- a/server/src/modules/rules/rules.service.ts +++ b/server/src/modules/rules/rules.service.ts @@ -29,6 +29,7 @@ import { RuleYamlService } from './helpers/yaml.service'; import { RuleComparatorService } from './helpers/rule.comparator.service'; import { PlexLibraryItem } from '../api/plex-api/interfaces/library.interfaces'; import { ECollectionLogType } from 'src/modules/collections/entities/collection_log.entities'; +import cacheManager from 'src/modules/api/lib/cache'; export interface ReturnStatus { code: 0 | 1; @@ -951,4 +952,58 @@ export class RulesService { } return { code: 0, result: 'Invalid input' }; } + + /** + * Reset the Plex cache if any rule in the rule group requires it. + * + * @param {RulesDto} rulegroup - The rule group to check for cache reset requirement. + * @return {Promise} Whether the Plex cache was reset. + */ + public async resetPlexCacheIfgroupUsesRuleThatRequiresIt( + rulegroup: RulesDto, + ): Promise { + try { + let result = false; + const constant = await this.getRuleConstants(); + + // for all rules in group + for (const rule of rulegroup.rules) { + const parsedRule = JSON.parse((rule as RuleDbDto).ruleJson) as RuleDto; + + //test first value + const first = + constant.applications[parsedRule.firstVal[0]].props[ + parsedRule.firstVal[1] + ]; + + result = first?.cacheReset ? true : result; + + // test second value + const second = parsedRule.lastVal + ? constant.applications[parsedRule.lastVal[0]].props[ + parsedRule.lastVal[1] + ] + : undefined; + + result = second?.cacheReset ? true : result; + } + + // if any rule requires a cache reset + if (result) { + cacheManager.getCache('plextv').flush(); + cacheManager.getCache('plexguid').flush(); + this.logger.log( + `Flushed Plex cache because a rule in the group required it`, + ); + } + + return result; + } catch (e) { + this.logger.warn( + `Couldn't determine if rulegroup with id ${rulegroup.id} requires a cache reset`, + ); + this.logger.debug(e); + return false; + } + } } diff --git a/server/src/modules/rules/tasks/rule-executor.service.ts b/server/src/modules/rules/tasks/rule-executor.service.ts index 0721f329..09e253ab 100644 --- a/server/src/modules/rules/tasks/rule-executor.service.ts +++ b/server/src/modules/rules/tasks/rule-executor.service.ts @@ -63,7 +63,7 @@ export class RuleExecutorService implements OnApplicationBootstrap { this.onApplicationBootstrap(); }, 10000); } else { - this.logger.error(`Creation of job Rule Handler failed.`); + this.logger.error(`Creation of job Rule Handler failed`); } } } @@ -77,13 +77,11 @@ export class RuleExecutorService implements OnApplicationBootstrap { } public async executeAllRules() { - this.logger.log('Starting Execution of all active rules.'); + this.logger.log('Starting Execution of all active rules'); const appStatus = await this.settings.testConnections(); // reset API caches, make sure latest data is used - for (const [key, value] of Object.entries(cacheManager.getAllCaches())) { - value.flush(); - } + cacheManager.flushAll(); if (appStatus) { const ruleGroups = await this.getAllActiveRuleGroups(); @@ -93,6 +91,11 @@ export class RuleExecutorService implements OnApplicationBootstrap { this.logger.log(`Executing rules for '${rulegroup.name}'`); this.startTime = new Date(); + // reset Plex cache if group uses a rule that requires it (collection rules for example) + this.rulesService.resetPlexCacheIfgroupUsesRuleThatRequiresIt( + rulegroup, + ); + // prepare this.workerData = []; this.resultData = []; From fdefee8b0336cf8f0a321681d0bf2011ea34c088 Mon Sep 17 00:00:00 2001 From: jorenn92 Date: Fri, 26 Jan 2024 12:46:26 +0100 Subject: [PATCH 2/2] refactor: Wait for potential cache reset --- server/src/modules/rules/tasks/rule-executor.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/modules/rules/tasks/rule-executor.service.ts b/server/src/modules/rules/tasks/rule-executor.service.ts index 09e253ab..371ff281 100644 --- a/server/src/modules/rules/tasks/rule-executor.service.ts +++ b/server/src/modules/rules/tasks/rule-executor.service.ts @@ -92,7 +92,7 @@ export class RuleExecutorService implements OnApplicationBootstrap { this.startTime = new Date(); // reset Plex cache if group uses a rule that requires it (collection rules for example) - this.rulesService.resetPlexCacheIfgroupUsesRuleThatRequiresIt( + await this.rulesService.resetPlexCacheIfgroupUsesRuleThatRequiresIt( rulegroup, );