From 9b808c88146f5f995c9646891ecaf565049119f2 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 10 Sep 2024 10:28:50 -0400 Subject: [PATCH] feat(modifications): Refactor hooks to accept an array of parts * Hook can be single object part or array of parts * All arrays are processed #185 --- package-lock.json | 1 + package.json | 1 + src/backend/common/AbstractComponent.ts | 47 ++++-- src/backend/common/infrastructure/Atomic.ts | 32 ++-- src/backend/common/schema/aio-client.json | 137 +++++++++++++----- src/backend/common/schema/aio-source.json | 137 +++++++++++++----- src/backend/common/schema/aio.json | 137 +++++++++++++----- src/backend/common/schema/client.json | 137 +++++++++++++----- src/backend/common/schema/source.json | 137 +++++++++++++----- src/backend/tests/component/component.test.ts | 108 ++++++++++++-- src/backend/utils/PlayTransformUtils.ts | 111 +++++++------- 11 files changed, 700 insertions(+), 285 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6580369..ab97371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "express-session": "^1.17.2", + "fast-deep-equal": "^3.1.3", "fixed-size-list": "^0.3.0", "formidable": "^3.5", "gotify": "^1.1.0", diff --git a/package.json b/package.json index a5844af..f73740c 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "express-session": "^1.17.2", + "fast-deep-equal": "^3.1.3", "fixed-size-list": "^0.3.0", "formidable": "^3.5", "gotify": "^1.1.0", diff --git a/src/backend/common/AbstractComponent.ts b/src/backend/common/AbstractComponent.ts index 321ad31..3ae51e5 100644 --- a/src/backend/common/AbstractComponent.ts +++ b/src/backend/common/AbstractComponent.ts @@ -2,7 +2,10 @@ import { childLogger, Logger } from "@foxxmd/logging"; import { cacheFunctions, } from "@foxxmd/regex-buddy-core"; +import deepEqual from 'fast-deep-equal'; +import { Simulate } from "react-dom/test-utils"; import { PlayObject } from "../../core/Atomic.js"; +import { buildTrackString } from "../../core/StringUtils.js"; import { configPartsToStrongParts, countRegexes, @@ -12,13 +15,14 @@ import { hasNodeNetworkException } from "./errors/NodeErrors.js"; import { hasUpstreamError } from "./errors/UpstreamError.js"; import { ConditionalSearchAndReplaceRegExp, - PlayTransformParts, + PlayTransformParts, PlayTransformPartsArray, PlayTransformRules, TRANSFORM_HOOK, TransformHook } from "./infrastructure/Atomic.js"; import { CommonClientConfig } from "./infrastructure/config/client/index.js"; import { CommonSourceConfig } from "./infrastructure/config/source/index.js"; +import play = Simulate.play; export default abstract class AbstractComponent { requiresAuth: boolean = false; @@ -34,8 +38,6 @@ export default abstract class AbstractComponent { config: CommonClientConfig | CommonSourceConfig; transformRules!: PlayTransformRules; - // TODO set this based on number of rules? - // we will know how many rules there are at component build time... regexCache!: ReturnType; logger: Logger; @@ -261,7 +263,7 @@ export default abstract class AbstractComponent { const getLogger = () => logger !== undefined ? logger : childLogger(this.logger, labels); try { - let hook: PlayTransformParts | undefined; + let hook: PlayTransformPartsArray | undefined; switch (hookType) { case TRANSFORM_HOOK.preCompare: @@ -282,20 +284,35 @@ export default abstract class AbstractComponent { return play; } - const [transformedPlay, transformDetails] = transformPlayUsingParts(play, hook, { - logger: getLogger, - regex: { - searchAndReplace: this.regexCache.searchAndReplace, - testMaybeRegex: this.regexCache.testMaybeRegex, + let transformedPlay: PlayObject = play; + const transformDetails: string[] = []; + for(const hookItem of hook) { + const newTransformedPlay = transformPlayUsingParts(transformedPlay, hookItem, { + logger: getLogger, + regex: { + searchAndReplace: this.regexCache.searchAndReplace, + testMaybeRegex: this.regexCache.testMaybeRegex, + } + }); + if(!deepEqual(newTransformedPlay, transformedPlay)) { + transformDetails.push(buildTrackString(transformedPlay, {include: ['artist', 'track', 'album']})); } - }); + transformedPlay = newTransformedPlay; + } - const shouldLog = log ?? this.config.options?.playTransform?.log ?? true; - if(shouldLog) { - this.logger.debug({labels: [...labels, hookType]}, transformDetails); + if(transformDetails.length > 0) { + let transformStatements = [`Original: ${buildTrackString(play, {include: ['artist', 'track', 'album']})}`]; + const shouldLog = log ?? this.config.options?.playTransform?.log ?? false; + if (shouldLog === true || shouldLog === 'all') { + if(shouldLog === 'all') { + transformStatements = transformStatements.concat(transformDetails.map(x => `=> ${x}`)); + } else { + transformStatements.push(`=> ${transformDetails[transformDetails.length - 1]}`); + } + this.logger.debug({labels: [...labels, hookType]}, `Transform Pipeline:\n${transformStatements.join('\n')}`); } - - return transformedPlay; + } + return transformedPlay; } catch (e) { getLogger().warn(new Error(`Unexpected error occurred, returning original play.`, {cause: e})); return play; diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts index 20e644c..109b8fb 100644 --- a/src/backend/common/infrastructure/Atomic.ts +++ b/src/backend/common/infrastructure/Atomic.ts @@ -245,28 +245,42 @@ export type AbstractApiOptions = Record & { logger: Logger } export type keyOmit = T & { [P in U]?: never } -export interface ConditionalSearchAndReplaceRegExp extends SearchAndReplaceRegExp { +export interface ConditionalSearchAndReplaceRegExp extends SearchAndReplaceRegExp{ when?: WhenConditionsConfig } -export type SearchAndReplaceTerm = string | ConditionalSearchAndReplaceRegExp; +export type ConditionalSearchAndReplaceTerm = Omit +export type SearchAndReplaceTerm = string | ConditionalSearchAndReplaceTerm; export type PlayTransformParts = PlayTransformPartsAtomic & { when?: WhenConditionsConfig }; +export type PlayTransformPartsArray = PlayTransformParts[]; + +export type PlayTransformPartsConfig = PlayTransformPartsArray | PlayTransformParts; + export interface PlayTransformPartsAtomic { title?: T artists?: T album?: T } -export interface PlayTransformHooks { - preCompare?: PlayTransformParts +export interface PlayTransformHooksConfig { + preCompare?: PlayTransformPartsConfig + compare?: { + candidate?: PlayTransformPartsConfig + existing?: PlayTransformPartsConfig + } + postCompare?: PlayTransformPartsConfig +} + +export interface PlayTransformHooks extends PlayTransformHooksConfig { + preCompare?: PlayTransformPartsArray compare?: { - candidate?: PlayTransformParts - existing?: PlayTransformParts + candidate?: PlayTransformPartsArray + existing?: PlayTransformPartsArray } - postCompare?: PlayTransformParts + postCompare?: PlayTransformPartsArray } export type PlayTransformRules = PlayTransformHooks @@ -278,8 +292,8 @@ export const TRANSFORM_HOOK = { existing: 'existing' as TransformHook, postCompare: 'postCompare' as TransformHook, } -export type PlayTransformConfig = PlayTransformHooks; -export type PlayTransformOptions = PlayTransformConfig & { log?: boolean } +export type PlayTransformConfig = PlayTransformHooksConfig; +export type PlayTransformOptions = PlayTransformConfig & { log?: boolean | 'all' } export type WhenParts = PlayTransformPartsAtomic; diff --git a/src/backend/common/schema/aio-client.json b/src/backend/common/schema/aio-client.json index 540f004..5353582 100644 --- a/src/backend/common/schema/aio-client.json +++ b/src/backend/common/schema/aio-client.json @@ -98,6 +98,40 @@ "title": "CommonClientOptions", "type": "object" }, + "ConditionalSearchAndReplaceTerm": { + "properties": { + "replace": { + "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", + "title": "replace", + "type": "string" + }, + "search": { + "anyOf": [ + { + "$ref": "#/definitions/RegExp" + }, + { + "type": "string" + } + ], + "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object", + "title": "search" + }, + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "required": [ + "replace", + "search" + ], + "title": "ConditionalSearchAndReplaceTerm", + "type": "object" + }, "LastfmClientAIOConfig": { "properties": { "configureAs": { @@ -454,11 +488,11 @@ "compare": { "properties": { "candidate": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "candidate" }, "existing": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "existing" } }, @@ -466,11 +500,11 @@ "type": "object" }, "postCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "postCompare" }, "preCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "preCompare" } }, @@ -485,8 +519,12 @@ { "properties": { "log": { - "title": "log", - "type": "boolean" + "enum": [ + "all", + false, + true + ], + "title": "log" } }, "type": "object" @@ -495,6 +533,26 @@ "title": "PlayTransformOptions" }, "PlayTransformParts": { + "allOf": [ + { + "$ref": "#/definitions/PlayTransformPartsAtomic" + }, + { + "properties": { + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "type": "object" + } + ], + "title": "PlayTransformParts" + }, + "PlayTransformPartsAtomic": { "properties": { "album": { "items": { @@ -518,9 +576,23 @@ "type": "array" } }, - "title": "PlayTransformParts", + "title": "PlayTransformPartsAtomic", "type": "object" }, + "PlayTransformPartsConfig": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/PlayTransformParts" + }, + "type": "array" + }, + { + "$ref": "#/definitions/PlayTransformParts" + } + ], + "title": "PlayTransformPartsConfig" + }, "RegExp": { "properties": { "dotAll": { @@ -603,47 +675,34 @@ "title": "RequestRetryOptions", "type": "object" }, - "SearchAndReplaceRegExp": { - "properties": { - "replace": { - "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", - "title": "replace", - "type": "string" - }, - "search": { - "anyOf": [ - { - "$ref": "#/definitions/RegExp" - }, - { - "type": "string" - } - ], - "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object\n\nEX `[\"find this string\", \"/some string*\\/ig\"]`", - "examples": [ - "find this string", - "/some string*/ig" - ], - "title": "search" - } - }, - "required": [ - "replace", - "search" - ], - "title": "SearchAndReplaceRegExp", - "type": "object" - }, "SearchAndReplaceTerm": { "anyOf": [ { - "$ref": "#/definitions/SearchAndReplaceRegExp" + "$ref": "#/definitions/ConditionalSearchAndReplaceTerm" }, { "type": "string" } ], "title": "SearchAndReplaceTerm" + }, + "WhenParts": { + "properties": { + "album": { + "title": "album", + "type": "string" + }, + "artists": { + "title": "artists", + "type": "string" + }, + "title": { + "title": "title", + "type": "string" + } + }, + "title": "WhenParts", + "type": "object" } }, "properties": { diff --git a/src/backend/common/schema/aio-source.json b/src/backend/common/schema/aio-source.json index bc71001..455b447 100644 --- a/src/backend/common/schema/aio-source.json +++ b/src/backend/common/schema/aio-source.json @@ -301,6 +301,40 @@ "title": "CommonSourceOptions", "type": "object" }, + "ConditionalSearchAndReplaceTerm": { + "properties": { + "replace": { + "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", + "title": "replace", + "type": "string" + }, + "search": { + "anyOf": [ + { + "$ref": "#/definitions/RegExp" + }, + { + "type": "string" + } + ], + "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object", + "title": "search" + }, + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "required": [ + "replace", + "search" + ], + "title": "ConditionalSearchAndReplaceTerm", + "type": "object" + }, "DeezerData": { "properties": { "clientId": { @@ -1681,11 +1715,11 @@ "compare": { "properties": { "candidate": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "candidate" }, "existing": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "existing" } }, @@ -1693,11 +1727,11 @@ "type": "object" }, "postCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "postCompare" }, "preCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "preCompare" } }, @@ -1712,8 +1746,12 @@ { "properties": { "log": { - "title": "log", - "type": "boolean" + "enum": [ + "all", + false, + true + ], + "title": "log" } }, "type": "object" @@ -1722,6 +1760,26 @@ "title": "PlayTransformOptions" }, "PlayTransformParts": { + "allOf": [ + { + "$ref": "#/definitions/PlayTransformPartsAtomic" + }, + { + "properties": { + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "type": "object" + } + ], + "title": "PlayTransformParts" + }, + "PlayTransformPartsAtomic": { "properties": { "album": { "items": { @@ -1745,9 +1803,23 @@ "type": "array" } }, - "title": "PlayTransformParts", + "title": "PlayTransformPartsAtomic", "type": "object" }, + "PlayTransformPartsConfig": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/PlayTransformParts" + }, + "type": "array" + }, + { + "$ref": "#/definitions/PlayTransformParts" + } + ], + "title": "PlayTransformPartsConfig" + }, "PlexSourceAIOConfig": { "properties": { "clients": { @@ -1951,41 +2023,10 @@ "title": "ScrobbleThresholds", "type": "object" }, - "SearchAndReplaceRegExp": { - "properties": { - "replace": { - "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", - "title": "replace", - "type": "string" - }, - "search": { - "anyOf": [ - { - "$ref": "#/definitions/RegExp" - }, - { - "type": "string" - } - ], - "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object\n\nEX `[\"find this string\", \"/some string*\\/ig\"]`", - "examples": [ - "find this string", - "/some string*/ig" - ], - "title": "search" - } - }, - "required": [ - "replace", - "search" - ], - "title": "SearchAndReplaceRegExp", - "type": "object" - }, "SearchAndReplaceTerm": { "anyOf": [ { - "$ref": "#/definitions/SearchAndReplaceRegExp" + "$ref": "#/definitions/ConditionalSearchAndReplaceTerm" }, { "type": "string" @@ -2670,6 +2711,24 @@ "title": "WebScrobblerSourceAIOConfig", "type": "object" }, + "WhenParts": { + "properties": { + "album": { + "title": "album", + "type": "string" + }, + "artists": { + "title": "artists", + "type": "string" + }, + "title": { + "title": "title", + "type": "string" + } + }, + "title": "WhenParts", + "type": "object" + }, "YTMusicData": { "properties": { "authUser": { diff --git a/src/backend/common/schema/aio.json b/src/backend/common/schema/aio.json index 698db57..d5685a7 100644 --- a/src/backend/common/schema/aio.json +++ b/src/backend/common/schema/aio.json @@ -558,6 +558,40 @@ "title": "CommonSourceOptions", "type": "object" }, + "ConditionalSearchAndReplaceTerm": { + "properties": { + "replace": { + "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", + "title": "replace", + "type": "string" + }, + "search": { + "anyOf": [ + { + "$ref": "#/definitions/RegExp" + }, + { + "type": "string" + } + ], + "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object", + "title": "search" + }, + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "required": [ + "replace", + "search" + ], + "title": "ConditionalSearchAndReplaceTerm", + "type": "object" + }, "DeezerData": { "properties": { "clientId": { @@ -2518,11 +2552,11 @@ "compare": { "properties": { "candidate": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "candidate" }, "existing": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "existing" } }, @@ -2530,11 +2564,11 @@ "type": "object" }, "postCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "postCompare" }, "preCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "preCompare" } }, @@ -2549,8 +2583,12 @@ { "properties": { "log": { - "title": "log", - "type": "boolean" + "enum": [ + "all", + false, + true + ], + "title": "log" } }, "type": "object" @@ -2559,6 +2597,26 @@ "title": "PlayTransformOptions" }, "PlayTransformParts": { + "allOf": [ + { + "$ref": "#/definitions/PlayTransformPartsAtomic" + }, + { + "properties": { + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "type": "object" + } + ], + "title": "PlayTransformParts" + }, + "PlayTransformPartsAtomic": { "properties": { "album": { "items": { @@ -2582,9 +2640,23 @@ "type": "array" } }, - "title": "PlayTransformParts", + "title": "PlayTransformPartsAtomic", "type": "object" }, + "PlayTransformPartsConfig": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/PlayTransformParts" + }, + "type": "array" + }, + { + "$ref": "#/definitions/PlayTransformParts" + } + ], + "title": "PlayTransformPartsConfig" + }, "PlexSourceAIOConfig": { "properties": { "clients": { @@ -2820,41 +2892,10 @@ "title": "ScrobbleThresholds", "type": "object" }, - "SearchAndReplaceRegExp": { - "properties": { - "replace": { - "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", - "title": "replace", - "type": "string" - }, - "search": { - "anyOf": [ - { - "$ref": "#/definitions/RegExp" - }, - { - "type": "string" - } - ], - "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object\n\nEX `[\"find this string\", \"/some string*\\/ig\"]`", - "examples": [ - "find this string", - "/some string*/ig" - ], - "title": "search" - } - }, - "required": [ - "replace", - "search" - ], - "title": "SearchAndReplaceRegExp", - "type": "object" - }, "SearchAndReplaceTerm": { "anyOf": [ { - "$ref": "#/definitions/SearchAndReplaceRegExp" + "$ref": "#/definitions/ConditionalSearchAndReplaceTerm" }, { "type": "string" @@ -3608,6 +3649,24 @@ ], "title": "WebhookConfig" }, + "WhenParts": { + "properties": { + "album": { + "title": "album", + "type": "string" + }, + "artists": { + "title": "artists", + "type": "string" + }, + "title": { + "title": "title", + "type": "string" + } + }, + "title": "WhenParts", + "type": "object" + }, "YTMusicData": { "properties": { "authUser": { diff --git a/src/backend/common/schema/client.json b/src/backend/common/schema/client.json index a7de9b7..5929e6d 100644 --- a/src/backend/common/schema/client.json +++ b/src/backend/common/schema/client.json @@ -95,6 +95,40 @@ "title": "CommonClientOptions", "type": "object" }, + "ConditionalSearchAndReplaceTerm": { + "properties": { + "replace": { + "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", + "title": "replace", + "type": "string" + }, + "search": { + "anyOf": [ + { + "$ref": "#/definitions/RegExp" + }, + { + "type": "string" + } + ], + "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object", + "title": "search" + }, + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "required": [ + "replace", + "search" + ], + "title": "ConditionalSearchAndReplaceTerm", + "type": "object" + }, "LastfmClientConfig": { "properties": { "configureAs": { @@ -427,11 +461,11 @@ "compare": { "properties": { "candidate": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "candidate" }, "existing": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "existing" } }, @@ -439,11 +473,11 @@ "type": "object" }, "postCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "postCompare" }, "preCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "preCompare" } }, @@ -458,8 +492,12 @@ { "properties": { "log": { - "title": "log", - "type": "boolean" + "enum": [ + "all", + false, + true + ], + "title": "log" } }, "type": "object" @@ -468,6 +506,26 @@ "title": "PlayTransformOptions" }, "PlayTransformParts": { + "allOf": [ + { + "$ref": "#/definitions/PlayTransformPartsAtomic" + }, + { + "properties": { + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "type": "object" + } + ], + "title": "PlayTransformParts" + }, + "PlayTransformPartsAtomic": { "properties": { "album": { "items": { @@ -491,9 +549,23 @@ "type": "array" } }, - "title": "PlayTransformParts", + "title": "PlayTransformPartsAtomic", "type": "object" }, + "PlayTransformPartsConfig": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/PlayTransformParts" + }, + "type": "array" + }, + { + "$ref": "#/definitions/PlayTransformParts" + } + ], + "title": "PlayTransformPartsConfig" + }, "RegExp": { "properties": { "dotAll": { @@ -552,47 +624,34 @@ "title": "RegExp", "type": "object" }, - "SearchAndReplaceRegExp": { - "properties": { - "replace": { - "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", - "title": "replace", - "type": "string" - }, - "search": { - "anyOf": [ - { - "$ref": "#/definitions/RegExp" - }, - { - "type": "string" - } - ], - "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object\n\nEX `[\"find this string\", \"/some string*\\/ig\"]`", - "examples": [ - "find this string", - "/some string*/ig" - ], - "title": "search" - } - }, - "required": [ - "replace", - "search" - ], - "title": "SearchAndReplaceRegExp", - "type": "object" - }, "SearchAndReplaceTerm": { "anyOf": [ { - "$ref": "#/definitions/SearchAndReplaceRegExp" + "$ref": "#/definitions/ConditionalSearchAndReplaceTerm" }, { "type": "string" } ], "title": "SearchAndReplaceTerm" + }, + "WhenParts": { + "properties": { + "album": { + "title": "album", + "type": "string" + }, + "artists": { + "title": "artists", + "type": "string" + }, + "title": { + "title": "title", + "type": "string" + } + }, + "title": "WhenParts", + "type": "object" } } } diff --git a/src/backend/common/schema/source.json b/src/backend/common/schema/source.json index c9b8b85..3c14998 100644 --- a/src/backend/common/schema/source.json +++ b/src/backend/common/schema/source.json @@ -352,6 +352,40 @@ "title": "CommonSourceOptions", "type": "object" }, + "ConditionalSearchAndReplaceTerm": { + "properties": { + "replace": { + "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", + "title": "replace", + "type": "string" + }, + "search": { + "anyOf": [ + { + "$ref": "#/definitions/RegExp" + }, + { + "type": "string" + } + ], + "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object", + "title": "search" + }, + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "required": [ + "replace", + "search" + ], + "title": "ConditionalSearchAndReplaceTerm", + "type": "object" + }, "DeezerData": { "properties": { "clientId": { @@ -1644,11 +1678,11 @@ "compare": { "properties": { "candidate": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "candidate" }, "existing": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "existing" } }, @@ -1656,11 +1690,11 @@ "type": "object" }, "postCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "postCompare" }, "preCompare": { - "$ref": "#/definitions/PlayTransformParts", + "$ref": "#/definitions/PlayTransformPartsConfig", "title": "preCompare" } }, @@ -1675,8 +1709,12 @@ { "properties": { "log": { - "title": "log", - "type": "boolean" + "enum": [ + "all", + false, + true + ], + "title": "log" } }, "type": "object" @@ -1685,6 +1723,26 @@ "title": "PlayTransformOptions" }, "PlayTransformParts": { + "allOf": [ + { + "$ref": "#/definitions/PlayTransformPartsAtomic" + }, + { + "properties": { + "when": { + "items": { + "$ref": "#/definitions/WhenParts" + }, + "title": "when", + "type": "array" + } + }, + "type": "object" + } + ], + "title": "PlayTransformParts" + }, + "PlayTransformPartsAtomic": { "properties": { "album": { "items": { @@ -1708,9 +1766,23 @@ "type": "array" } }, - "title": "PlayTransformParts", + "title": "PlayTransformPartsAtomic", "type": "object" }, + "PlayTransformPartsConfig": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/PlayTransformParts" + }, + "type": "array" + }, + { + "$ref": "#/definitions/PlayTransformParts" + } + ], + "title": "PlayTransformPartsConfig" + }, "PlexSourceConfig": { "properties": { "clients": { @@ -1906,41 +1978,10 @@ "title": "ScrobbleThresholds", "type": "object" }, - "SearchAndReplaceRegExp": { - "properties": { - "replace": { - "description": "The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", - "title": "replace", - "type": "string" - }, - "search": { - "anyOf": [ - { - "$ref": "#/definitions/RegExp" - }, - { - "type": "string" - } - ], - "description": "The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object\n\nEX `[\"find this string\", \"/some string*\\/ig\"]`", - "examples": [ - "find this string", - "/some string*/ig" - ], - "title": "search" - } - }, - "required": [ - "replace", - "search" - ], - "title": "SearchAndReplaceRegExp", - "type": "object" - }, "SearchAndReplaceTerm": { "anyOf": [ { - "$ref": "#/definitions/SearchAndReplaceRegExp" + "$ref": "#/definitions/ConditionalSearchAndReplaceTerm" }, { "type": "string" @@ -2488,6 +2529,24 @@ "title": "WebScrobblerSourceConfig", "type": "object" }, + "WhenParts": { + "properties": { + "album": { + "title": "album", + "type": "string" + }, + "artists": { + "title": "artists", + "type": "string" + }, + "title": { + "title": "title", + "type": "string" + } + }, + "title": "WhenParts", + "type": "object" + }, "YTMusicData": { "properties": { "authUser": { diff --git a/src/backend/tests/component/component.test.ts b/src/backend/tests/component/component.test.ts index 21a0321..13e286c 100644 --- a/src/backend/tests/component/component.test.ts +++ b/src/backend/tests/component/component.test.ts @@ -34,6 +34,48 @@ describe('Play Transforms', function () { expect(Object.keys(component.transformRules).length).eq(0); }); + it('Converts single object hook into hook array', function() { + component.config = { + options: { + playTransform: { + preCompare: { + title: ['something'] + } + } + } + } + + component.buildTransformRules(); + + expect(component.transformRules.preCompare).to.be.an('array'); + expect(component.transformRules.preCompare).to.be.length(1); + expect(component.transformRules.preCompare).to.have.nested.property('0.title'); + }); + + it('Accepts hook array', function() { + component.config = { + options: { + playTransform: { + preCompare: [ + { + title: ['something'] + }, + { + title: ['something else'] + } + ] + } + } + } + + component.buildTransformRules(); + + expect(component.transformRules.preCompare).to.be.an('array'); + expect(component.transformRules.preCompare).to.be.length(2); + expect(component.transformRules.preCompare).to.have.nested.property('0.title'); + expect(component.transformRules.preCompare).to.have.nested.property('1.title') + }); + it('Converts transform config into real S&P data', function() { component.config = { options: { @@ -47,10 +89,10 @@ describe('Play Transforms', function () { component.buildTransformRules(); - expect(component.transformRules.preCompare).to.exist; - expect(component.transformRules.preCompare.title).to.exist; - expect(Array.isArray(component.transformRules.preCompare.title)).is.true; - expect( isConditionalSearchAndReplace(component.transformRules.preCompare.title[0])).is.true + expect(component.transformRules.preCompare[0]).to.exist; + expect(component.transformRules.preCompare[0].title).to.exist; + expect(Array.isArray(component.transformRules.preCompare[0].title)).is.true; + expect( isConditionalSearchAndReplace(component.transformRules.preCompare[0].title[0])).is.true }); it('Converts transform config into real S&P data with default being empty string', function() { @@ -66,12 +108,12 @@ describe('Play Transforms', function () { component.buildTransformRules(); - expect(component.transformRules.preCompare).to.exist; - expect(component.transformRules.preCompare.title).to.exist; - expect(Array.isArray(component.transformRules.preCompare.title)).is.true; - expect( isConditionalSearchAndReplace(component.transformRules.preCompare.title[0])).is.true - expect( component.transformRules.preCompare.title[0].search).is.eq('something'); - expect( component.transformRules.preCompare.title[0].replace).is.eq(''); + expect(component.transformRules.preCompare[0]).to.exist; + expect(component.transformRules.preCompare[0].title).to.exist; + expect(Array.isArray(component.transformRules.preCompare[0].title)).is.true; + expect( isConditionalSearchAndReplace(component.transformRules.preCompare[0].title[0])).is.true + expect( component.transformRules.preCompare[0].title[0].search).is.eq('something'); + expect( component.transformRules.preCompare[0].title[0].replace).is.eq(''); }); it('Respects transform config when it is already S&P data', function() { @@ -92,12 +134,12 @@ describe('Play Transforms', function () { component.buildTransformRules(); - expect(component.transformRules.preCompare).to.exist; - expect(component.transformRules.preCompare.title).to.exist; - expect(Array.isArray(component.transformRules.preCompare.title)).is.true; - expect( isConditionalSearchAndReplace(component.transformRules.preCompare.title[0])).is.true - expect( component.transformRules.preCompare.title[0].search).is.eq('nothing'); - expect( component.transformRules.preCompare.title[0].replace).is.eq('anything'); + expect(component.transformRules.preCompare[0]).to.exist; + expect(component.transformRules.preCompare[0].title).to.exist; + expect(Array.isArray(component.transformRules.preCompare[0].title)).is.true; + expect( isConditionalSearchAndReplace(component.transformRules.preCompare[0].title[0])).is.true + expect( component.transformRules.preCompare[0].title[0].search).is.eq('nothing'); + expect( component.transformRules.preCompare[0].title[0].replace).is.eq('anything'); }); }); @@ -330,5 +372,39 @@ describe('Play Transforms', function () { }); + describe('Multiple hook transforms', function() { + it('Accumulates transforms', function() { + component.config = { + options: { + playTransform: { + preCompare: [ + { + title: [ + { + search: "something", + replace: "another else" + } + ] + }, + { + title: [ + { + search: "another else", + replace: "final thing" + } + ] + } + ] + } + } + } + + component.buildTransformRules(); + const play = generatePlay({track: 'My cool something track'}); + const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare); + expect(transformed.data.track).equal('My cool final thing track'); + }); + + }); }) diff --git a/src/backend/utils/PlayTransformUtils.ts b/src/backend/utils/PlayTransformUtils.ts index 1b9df11..be507e8 100644 --- a/src/backend/utils/PlayTransformUtils.ts +++ b/src/backend/utils/PlayTransformUtils.ts @@ -4,7 +4,7 @@ import { ObjectPlayData, PlayObject } from "../../core/Atomic.js"; import { buildTrackString } from "../../core/StringUtils.js"; import { ConditionalSearchAndReplaceRegExp, - PlayTransformParts, PlayTransformRules, + PlayTransformParts, PlayTransformPartsArray, PlayTransformPartsConfig, PlayTransformRules, SearchAndReplaceTerm, WhenConditionsConfig, WhenParts @@ -49,52 +49,57 @@ export const isConditionalSearchAndReplace = (val: unknown): val is ConditionalS && ('replace' in val && typeof val.replace === 'string') && (!('when' in val) || isWhenConditionConfig(val.when)); } -export const configPartsToStrongParts = (val: PlayTransformParts | undefined): PlayTransformParts => { +export const configPartsToStrongParts = (val: PlayTransformPartsConfig | undefined): PlayTransformPartsArray => { if (val === undefined) { - return {} + return [] } - const { - title: titleConfig, - artists: artistConfig, - album: albumConfig, - when: whenConfig - } = val; - let title, - artists, - album, - when; + const arr = Array.isArray(val) ? val : [val]; + + return arr.map((x) => { + const { + title: titleConfig, + artists: artistConfig, + album: albumConfig, + when: whenConfig + } = x; + let title, + artists, + album, + when; - if (titleConfig !== undefined) { - if (!Array.isArray(titleConfig)) { - throw new Error('title must be an array'); + if (titleConfig !== undefined) { + if (!Array.isArray(titleConfig)) { + throw new Error('title must be an array'); + } + title = titleConfig.map(configValToSearchReplace); } - title = titleConfig.map(configValToSearchReplace); - } - if (artistConfig !== undefined) { - if (!Array.isArray(artistConfig)) { - throw new Error('arist must be an array'); + if (artistConfig !== undefined) { + if (!Array.isArray(artistConfig)) { + throw new Error('arist must be an array'); + } + artists = artistConfig.map(configValToSearchReplace); } - artists = artistConfig.map(configValToSearchReplace); - } - if (albumConfig !== undefined) { - if (!Array.isArray(albumConfig)) { - throw new Error('album must be an array'); + if (albumConfig !== undefined) { + if (!Array.isArray(albumConfig)) { + throw new Error('album must be an array'); + } + album = albumConfig.map(configValToSearchReplace); } - album = albumConfig.map(configValToSearchReplace); - } - if (whenConfig !== undefined) { - if (!isWhenConditionConfig(whenConfig)) { - throw new Error('when must be an array of artist/title/album objects and each object\'s property must be a string'); + if (whenConfig !== undefined) { + if (!isWhenConditionConfig(whenConfig)) { + throw new Error('when must be an array of artist/title/album objects and each object\'s property must be a string'); + } + when = whenConfig; } - when = whenConfig; - } - return { - title, - artists, - album, - when - } + return { + title, + artists, + album, + when + } + }); + } export const testWhen = (parts: WhenParts, play: PlayObject, options?: SuppliedRegex): boolean => { @@ -134,7 +139,7 @@ export interface TransformPlayPartsOptions { regex?: SuppliedRegex } -export const transformPlayUsingParts = (play: PlayObject, parts: PlayTransformParts, options?: TransformPlayPartsOptions): [PlayObject, string?] => { +export const transformPlayUsingParts = (play: PlayObject, parts: PlayTransformParts, options?: TransformPlayPartsOptions): PlayObject => { const { data: { track, @@ -158,7 +163,7 @@ export const transformPlayUsingParts = (play: PlayObject, parts: PlayTransformPa if(parts.when !== undefined) { if(!testWhenConditions(parts.when, play, {testMaybeRegex})) { - return [play]; + return play; } } @@ -244,29 +249,35 @@ export const transformPlayUsingParts = (play: PlayObject, parts: PlayTransformPa } } - return [transformedPlay, `Play transformed: -Original : ${buildTrackString(play, {include: ['artist', 'track', 'album']})} -Transformed : ${buildTrackString(transformedPlay, {include: ['artist', 'track', 'album']})} -`]; + return transformedPlay; } - return [play]; + return play; } export const countRegexes = (rules: PlayTransformRules): number => { let rulesCount = 0; if(rules.preCompare !== undefined) { - rulesCount = countRulesInParts(rules.preCompare) + countWhens(rules.preCompare.when); + for(const hookItem of rules.preCompare) { + rulesCount = countRulesInParts(hookItem) + countWhens(hookItem.when); + } + } if(rules.postCompare !== undefined) { - rulesCount = countRulesInParts(rules.postCompare) + countWhens(rules.postCompare.when); + for(const hookItem of rules.postCompare) { + rulesCount = countRulesInParts(hookItem) + countWhens(hookItem.when); + } } if(rules.compare !== undefined) { if(rules.compare.existing !== undefined) { - rulesCount = countRulesInParts(rules.compare.existing) + countWhens(rules.compare.existing.when); + for(const hookItem of rules.compare.existing) { + rulesCount = countRulesInParts(hookItem) + countWhens(hookItem.when); + } } if(rules.compare.candidate !== undefined) { - rulesCount = countRulesInParts(rules.compare.candidate) + countWhens(rules.compare.candidate.when); + for(const hookItem of rules.compare.candidate) { + rulesCount = countRulesInParts(hookItem) + countWhens(hookItem.when); + } } } return rulesCount;