diff --git a/src/backend/common/transforms/AbstractTransformer.ts b/src/backend/common/transforms/AbstractTransformer.ts index 9f2f953b..d7a65d0c 100644 --- a/src/backend/common/transforms/AbstractTransformer.ts +++ b/src/backend/common/transforms/AbstractTransformer.ts @@ -21,7 +21,7 @@ export interface RegexObject { searchAndReplace: typeof searchAndReplace } -export default abstract class AbstractTransformer extends AbstractInitializable { +export default abstract class AbstractTransformer extends AbstractInitializable { declare config: TransformerCommonConfig; configHash: string; @@ -40,16 +40,16 @@ export default abstract class AbstractTransformer extends AbstractIniti this.configHash = hashObject(this.config); } - public parseConfig(data: any) { + public parseConfig(data: any): Y { if (!isStageTyped(data)) { throw new Error(`Must be an object with a 'type' property.`); } return this.doParseConfig(data); } - protected abstract doParseConfig(data: StageConfig): StageConfig; + protected abstract doParseConfig(data: StageConfig): Y; - public async handle(data: StageConfig, play: PlayObject): Promise { + public async handle(data: Y, play: PlayObject): Promise { const cacheKey = `${this.configHash}-${hashObject(data)}-${hashObject(playContentInvariantTransform(play))}` const cachedTransform = await this.cache.get(cacheKey); @@ -68,13 +68,13 @@ export default abstract class AbstractTransformer extends AbstractIniti let transformData: T; try { - transformData = await this.getTransformerData(play); + transformData = await this.getTransformerData(play, data); } catch (e) { throw new Error(`Could not fetch transformer data`, { cause: e }); } try { - await this.checkShouldTransform(play, transformData); + await this.checkShouldTransform(play, transformData, data); } catch (e) { this.logger.debug(new Error('checkShouldTransform did not pass, returning original Play', { cause: e })); return play; @@ -87,11 +87,11 @@ export default abstract class AbstractTransformer extends AbstractIniti protected abstract doHandle(data: StageConfig, play: PlayObject, transformData: T): Promise; - public async getTransformerData(play: PlayObject): Promise { + public async getTransformerData(play: PlayObject, stageConfig: Y): Promise { return undefined; } - public async checkShouldTransform(play: PlayObject, transformData: T): Promise { + public async checkShouldTransform(play: PlayObject, transformData: T, stageConfig: Y): Promise { return; } } \ No newline at end of file diff --git a/src/backend/common/transforms/AtomicPartsTransformer.ts b/src/backend/common/transforms/AtomicPartsTransformer.ts index 46a749c9..a7280af5 100644 --- a/src/backend/common/transforms/AtomicPartsTransformer.ts +++ b/src/backend/common/transforms/AtomicPartsTransformer.ts @@ -1,17 +1,19 @@ import { ObjectPlayData, PlayObject, TrackMeta } from "../../../core/Atomic.js"; -import { AtomicStageConfig } from "../infrastructure/Transform.js"; +import { AtomicStageConfig, StageConfig } from "../infrastructure/Transform.js"; import AbstractTransformer from "./AbstractTransformer.js"; -export default abstract class AtomicPartsTransformer extends AbstractTransformer { +//export type GenericAtomicStageConfig = - protected async doHandle(parts: AtomicStageConfig, play: PlayObject, transformData: T): Promise { +export default abstract class AtomicPartsTransformer = StageConfig> extends AbstractTransformer { + + protected async doHandle(parts: Z, play: PlayObject, transformData: T): Promise { const { throwOnFailure = false, } = this.config.options || {}; try { - await this.checkShouldTransform(play, transformData); + await this.checkShouldTransform(play, transformData, parts); } catch (e) { this.logger.debug(new Error('checkShouldTransform did not pass, returning original Play', { cause: e })); return play; @@ -84,11 +86,11 @@ export default abstract class AtomicPartsTransformer extends Abstrac return transformedPlay; } - public async getTransformerData(play: PlayObject): Promise { + public async getTransformerData(play: PlayObject, stageConfig: Z): Promise { return undefined; } - public async checkShouldTransform(play: PlayObject, transformData: T): Promise { + public async checkShouldTransform(play: PlayObject, transformData: T, stageConfig: Z): Promise { return; } diff --git a/src/backend/common/transforms/NativeTransformer.ts b/src/backend/common/transforms/NativeTransformer.ts index 60695e5c..7b86fafd 100644 --- a/src/backend/common/transforms/NativeTransformer.ts +++ b/src/backend/common/transforms/NativeTransformer.ts @@ -8,6 +8,8 @@ import { parseRegexSingle, parseToRegexOrLiteralSearch } from "@foxxmd/regex-bud import { TransformerOptions } from "./AbstractTransformer.js"; import { DELIMITERS_NO_AMP } from "../infrastructure/Atomic.js"; import { asArray } from "../../utils/DataUtils.js"; +import { MaybeLogger } from "../logging.js"; +import { childLogger } from "@foxxmd/logging"; export type ArtistParseSource = 'artists' | 'title' @@ -32,12 +34,74 @@ export interface NativeTransformerData { artistsParseMonolithicOnly?: boolean } +export interface NativeTransformerDataStrong { + artistsParseFrom?: ArtistParseSource[] + artistsParseMonolithicOnly?: boolean + ignoreArtistsRegex?: RegExp[] + delimiters?: string[] +} + +export interface NativeTransformerDataStage extends NativeTransformerDataStrong,PlayTransformNativeStage { +} + export type NativeTransformerConfig = TransformerCommon; -export default class NativeTransformer extends AtomicPartsTransformer { +export const parseStageConfig = (data: NativeTransformerData | undefined, logger: MaybeLogger = new MaybeLogger()): NativeTransformerDataStrong => { + + if (data === undefined) { + return {}; + } + + const config: NativeTransformerDataStrong = { + }; + + if (data === null || typeof data !== 'object') { + throw new Error('Native Transformer data should be an object or not defined.'); + } + if (data.artistsIgnore !== undefined) { + data.artistsIgnore = asArray(data.artistsIgnore); + const nonStr = data.artistsIgnore.filter(x => typeof x !== 'string'); + if (nonStr.length > 0) { + throw new Error(`ignoreArtists must be an array of strings but non-strings found: ${nonStr.map(x => (x as unknown).toString()).join(' | ')}`) + } + config.ignoreArtistsRegex = []; + for (const i of data.artistsIgnore) { + try { + config.ignoreArtistsRegex.push(parseToRegexOrLiteralSearch(i)); + } catch (e) { + throw new Error(`Could not convert ignoreArtist string to regex (or literal): ${i}`); + } + } + logger.debug(`Defaults - Ignoring artists using ${data.artistsIgnore.length} rules`); + } + + if (data.delimiters !== undefined) { + config.delimiters = asArray(data.delimiters); + logger.debug(`Defaults - Using user-defined delimiters '${data.delimitersExtra.join(' ')}' instead of built-ins`); + } else if (data.delimitersExtra !== undefined) { + config.delimiters = [...DELIMITERS_NO_AMP, ...(asArray(data.delimitersExtra))]; + logger.debug(`Defaults - Using extra delimiters '${data.delimitersExtra.join(' ')}' with built-in delimiters '${DELIMITERS_NO_AMP.join(' ')}'`); + } + + if (config.delimiters !== undefined) { + config.delimiters.map(x => x.trim()); + } + + if (data.artistsParseFrom !== undefined) { + const arr = asArray(data.artistsParseFrom); + config.artistsParseFrom = arr.map(asArtistParseSource); + logger.debug(`Defaults - Will try to parse artists from ${config.artistsParseFrom.join(' and ')} string`); + } + + return config; +} + +export default class NativeTransformer extends AtomicPartsTransformer { declare config: NativeTransformerConfig; + protected defaults: NativeTransformerDataStrong = {}; + ignoreArtistsRegex: RegExp[] = []; delimiters?: string[] parseArtistsFrom: ArtistParseSource[] @@ -47,54 +111,18 @@ export default class NativeTransformer extends AtomicPartsTransformer { - if(this.config.data === undefined) { - return true; - } - if(this.config.data === null || typeof this.config.data !== 'object') { - throw new Error('Native Transformer data should be an object or not defined.'); - } - if(this.config.data.artistsIgnore !== undefined) { - this.config.data.artistsIgnore = asArray(this.config.data.artistsIgnore); - const nonStr = this.config.data.artistsIgnore.filter(x => typeof x !== 'string'); - if(nonStr.length > 0) { - throw new Error(`ignoreArtists must be an array of strings but non-strings found: ${nonStr.map(x => (x as unknown).toString()).join(' | ')}`) - } - for(const i of this.config.data.artistsIgnore) { - try { - this.ignoreArtistsRegex.push(parseToRegexOrLiteralSearch(i)); - } catch (e) { - throw new Error(`Could not convert ignoreArtist string to regex (or literal): ${i}`); - } - } - this.logger.debug(`Ignoring artists using ${this.config.data.artistsIgnore.length} rules`); - } - - if(this.config.data.delimiters !== undefined) { - this.delimiters = asArray(this.config.data.delimiters); - this.logger.debug(`Using user-defined delimiters '${this.config.data.delimitersExtra.join(' ')}' instead of built-ins`); - } else if(this.config.data.delimitersExtra !== undefined) { - this.delimiters = [...DELIMITERS_NO_AMP, ...(asArray(this.config.data.delimitersExtra))]; - this.logger.debug(`Using extra delimiters '${this.config.data.delimitersExtra.join(' ')}' with built-in delimiters '${DELIMITERS_NO_AMP.join(' ')}'`); - } - - if(this.delimiters !== undefined) { - this.delimiters.map(x => x.trim()); - } - - if(this.config.data.artistsParseFrom !== undefined) { - const arr = asArray(this.config.data.artistsParseFrom); - this.parseArtistsFrom = arr.map(asArtistParseSource); - this.logger.debug(`Will try to parse artists from ${this.parseArtistsFrom.join(' and ')} string`); - } + this.defaults = parseStageConfig(this.config.defaults, childLogger(this.logger, 'Defaults')); + return true; } - protected doParseConfig(data: StageConfig) { + protected doParseConfig(data: NativeTransformerDataStage) { if (data.type !== 'native') { throw new Error(`NativeTransformer is only usable with 'native' type stages`); } - const stage: PlayTransformNativeStage = { + const stage: NativeTransformerDataStage = { ...data, + ...parseStageConfig(data), type: 'native' } @@ -116,12 +144,14 @@ export default class NativeTransformer extends AtomicPartsTransformer { + public async getTransformerData(play: PlayObject, stageConfig: NativeTransformerDataStage): Promise { let artists = []; const { - artistsParseFrom: parseArtistsFrom = ['artists', 'title'], - artistsParseMonolithicOnly = true - } = this.config.data || {}; + artistsParseFrom: parseArtistsFrom = this.defaults.artistsParseFrom ?? ['artists', 'title'], + artistsParseMonolithicOnly = this.defaults.artistsParseMonolithicOnly ?? true, + ignoreArtistsRegex = this.defaults.ignoreArtistsRegex ?? [], + delimiters = this.defaults.delimiters + } = stageConfig || {}; if(parseArtistsFrom.includes('artists')) { @@ -129,12 +159,12 @@ export default class NativeTransformer extends AtomicPartsTransformer ({reg: x.toString(), res: parseRegexSingle(x, artist)})).filter(x => x !== undefined); + const matchedIgnoreArtists = ignoreArtistsRegex.map(x => ({reg: x.toString(), res: parseRegexSingle(x, artist)})).filter(x => x !== undefined); if(matchedIgnoreArtists.length > 0) { this.logger.debug(`Will not parse artist because it matched an ignore regex:\n${matchedIgnoreArtists.map(x => `Reg: ${x.reg} => ${x.res.match}`).join('\n')}`); artists.push(artist); } else { - const artistCredits = parseArtistCredits(artist, this.delimiters); + const artistCredits = parseArtistCredits(artist, delimiters); if (artistCredits !== undefined) { if (artistCredits.primary !== undefined) { artists.push(artistCredits.primary); @@ -159,7 +189,7 @@ export default class NativeTransformer extends AtomicPartsTransformer> { - data?: T; +export interface TransformerCommonConfig, Y = Record> { + defaults?: T; + data?: Y type: string; name?: string; options?: {