Refactor metadata stage so atomic parts are objects or booleans

This commit is contained in:
FoxxMD
2025-11-20 18:34:23 +00:00
parent 128c48947e
commit dc5fab6f9a
3 changed files with 161 additions and 124 deletions
+11 -7
View File
@@ -6,7 +6,7 @@ export interface ConditionalSearchAndReplaceRegExp extends SearchAndReplaceRegEx
export type ConditionalSearchAndReplaceTerm = Omit<ConditionalSearchAndReplaceRegExp, 'test'>
export type SearchAndReplaceTerm = string | ConditionalSearchAndReplaceTerm;
export type ExternalMetadataTerm = true | undefined | { when: WhenConditionsConfig };
export type ExternalMetadataTerm = boolean | undefined | { when: WhenConditionsConfig };
export type PlayTransformParts<T, Y = MaybeStageTyped> = Extract<PlayTransformStage<T[]>, Y> & { when?: WhenConditionsConfig };
export type PlayTransformUserParts<T> = PlayTransformUserStage<T[]> & { when?: WhenConditionsConfig };
@@ -21,8 +21,12 @@ export interface PlayTransformPartsAtomic<T> {
album?: T
}
export type StageType = 'spotify' | 'listenbrainz' | 'native' | 'user';
export const STAGE_TYPES: StageType[] = ['spotify','listenbrainz','native','user']
export type StageTypeMetadata = 'spotify' | 'listenbrainz' | 'native';
export type StageTypeUser = 'user';
export type StageType = StageTypeMetadata | StageTypeUser;
export const STAGE_TYPES_USER: StageTypeUser[] = ['user'];
export const STAGE_TYPES_METADATA: StageTypeMetadata[] = ['spotify','listenbrainz','native'];
export const STAGE_TYPES: StageType[] = [...STAGE_TYPES_METADATA, ...STAGE_TYPES_USER];
export interface StageTyped {
type: StageType
@@ -38,14 +42,14 @@ export interface PlayTransformStageTyped<T> extends PlayTransformPartsAtomic<T>
type: StageType
}
export interface PlayTransformMetadataStage extends PlayTransformStageTyped<ExternalMetadataTerm[]> {
export interface PlayTransformMetadataStage extends PlayTransformStageTyped<ExternalMetadataTerm> {
score?: number
all?: ExternalMetadataTerm
type: 'spotify' | 'listenbrainz' | 'native'
// all?: ExternalMetadataTerm
type: StageTypeMetadata
}
export interface PlayTransformUserStage<T> extends PlayTransformStageTyped<T> {
type: 'user'
type: StageTypeUser
}
export type UntypedPlayTransformUserStage<T> = Omit<PlayTransformUserStage<T>, 'type'> & {type?: never};
+131 -96
View File
@@ -4,12 +4,13 @@ import asPromised from 'chai-as-promised';
import { after, before, describe, it } from 'mocha';
import AbstractComponent from "../../common/AbstractComponent.js";
import { ConditionalSearchAndReplaceRegExp, TRANSFORM_HOOK } from "../../common/infrastructure/Transform.js";
import { ConditionalSearchAndReplaceRegExp, STAGE_TYPES, STAGE_TYPES_METADATA, TRANSFORM_HOOK } from "../../common/infrastructure/Transform.js";
import { isConditionalSearchAndReplace } from "../../utils/PlayTransformUtils.js";
import { asPlays, generatePlay, normalizePlays } from "../utils/PlayTestUtils.js";
import { WebhookPayload } from "../../common/infrastructure/config/health/webhooks.js";
import { findCauseByMessage } from "../../utils/ErrorUtils.js";
import { ComAtprotoServerDescribeServer } from "@atproto/api";
chai.use(asPromised);
@@ -220,6 +221,37 @@ describe('Play Transforms', function () {
});
});
describe('Non-User Stage Parsing', function () {
describe('Non-User Stage Types', function () {
for(const t of STAGE_TYPES_METADATA) {
it(`Allows non-user Stage Type ${t}`, function () {
component.config = {
options: {
playTransform: {
preCompare: {
type: t,
title: true
}
}
}
}
expect(() => component.buildTransformRules()).to.not.throw();
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.type');
expect(component.transformRules.preCompare[0].type).eq(t);
});
}
});
});
describe('Play Transforming', function () {
it('Returns original play if no hooks are defined', function () {
@@ -230,136 +262,139 @@ describe('Play Transforms', function () {
expect(JSON.stringify(play)).equal(JSON.stringify(transformed));
});
it('Transforms when hook is present', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: ['something']
describe('User Play Transforming', function () {
it('Transforms when hook is present', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: ['something']
}
}
}
}
}
component.buildTransformRules();
component.buildTransformRules();
const play = generatePlay({ track: 'My coolsomething track' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).equal('My cool track');
});
const play = generatePlay({ track: 'My coolsomething track' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).equal('My cool track');
});
it('Transforms consecutively when hook is present with multiple values', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: ['something', 'cool']
it('Transforms consecutively when hook is present with multiple values', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: ['something', 'cool']
}
}
}
}
}
component.buildTransformRules();
component.buildTransformRules();
const play = generatePlay({ track: 'My coolsomething track' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).equal('My track');
});
const play = generatePlay({ track: 'My coolsomething track' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).equal('My track');
});
it('Transforms using parsed regex', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: [
{
search: '/(cool )(some)(thing)/i',
replace: '$1$3'
}
]
it('Transforms using parsed regex', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: [
{
search: '/(cool )(some)(thing)/i',
replace: '$1$3'
}
]
}
}
}
}
}
component.buildTransformRules();
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 thing track');
});
const play = generatePlay({ track: 'My cool something track' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).equal('My cool thing track');
});
it('Transforms using parsed regex to get primary artist from delimited artist string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
artists: [
{
search: '/(.*?)(\\s*\\/\\s*)(.*$)/i',
replace: '$1'
}
]
it('Transforms using parsed regex to get primary artist from delimited artist string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
artists: [
{
search: '/(.*?)(\\s*\\/\\s*)(.*$)/i',
replace: '$1'
}
]
}
}
}
}
}
component.buildTransformRules();
component.buildTransformRules();
const play = generatePlay({ artists: ['My Artist One / My Artist Two / Another Guy'] });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.artists).length(1)
expect(transformed.data.artists[0]).equal('My Artist One');
});
const play = generatePlay({ artists: ['My Artist One / My Artist Two / Another Guy'] });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.artists).length(1)
expect(transformed.data.artists[0]).equal('My Artist One');
});
it('Removes title when transform replaces with empty string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: ['something']
it('Removes title when transform replaces with empty string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
title: ['something']
}
}
}
}
}
component.buildTransformRules();
component.buildTransformRules();
const play = generatePlay({ track: 'something' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).is.undefined;
});
const play = generatePlay({ track: 'something' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.track).is.undefined;
});
it('Removes album when transform replaces with empty string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
album: ['something']
it('Removes album when transform replaces with empty string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
album: ['something']
}
}
}
}
}
component.buildTransformRules();
component.buildTransformRules();
const play = generatePlay({ album: 'something' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.album).is.undefined;
});
const play = generatePlay({ album: 'something' });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.album).is.undefined;
});
it('Removes an artist when transform replaces with empty string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
artists: ['something']
it('Removes an artist when transform replaces with empty string', function () {
component.config = {
options: {
playTransform: {
preCompare: {
artists: ['something']
}
}
}
}
}
component.buildTransformRules();
component.buildTransformRules();
const play = generatePlay({ artists: ['something', 'big'] });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.artists!.length).is.eq(1)
expect(transformed.data.artists![0]).is.eq('big')
});
const play = generatePlay({ artists: ['something', 'big'] });
const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare);
expect(transformed.data.artists!.length).is.eq(1)
expect(transformed.data.artists![0]).is.eq('big')
});
});
+19 -21
View File
@@ -16,7 +16,6 @@ import {
WhenConditionsConfig,
WhenParts
} from "../common/infrastructure/Transform.js";
import e from "express";
export const isWhenCondition = (val: unknown): val is WhenParts<string> => {
if (val !== null && typeof val === 'object') {
@@ -78,10 +77,7 @@ export const isExternalMetadataTerm = (val: unknown): val is ExternalMetadataTer
}
const tf = typeof val;
if(tf === 'boolean') {
if(val === true) {
return true;
}
throw new Error(`Value must be one of: true, undefined, or object with 'when'`);
return true;
}
if(tf === null) {
throw new Error(`Value is null but must be one of: true, undefined, or object with 'when'`);
@@ -92,34 +88,36 @@ export const isExternalMetadataTerm = (val: unknown): val is ExternalMetadataTer
}
throw new Error(`Value is not a proper 'when' object`);
}
throw new Error(`Value is type of ${tf} but must be one of: true, undefined, or object with 'when'`);
throw new Error(`Value is type of ${tf} but must be one of: boolean, undefined, or object with 'when'`);
}
export const isPlayTransformStage = (val: object | Partial<PlayTransformStage<SearchAndReplaceTerm[]>>): val is PlayTransformStage<SearchAndReplaceTerm[]> => {
if(!('type' in val)) {
if (!('type' in val)) {
throw new Error(`Stage is missing 'type'. Must be one of: ${STAGE_TYPES.join(', ')}`);
}
if(!STAGE_TYPES.includes(val.type)) {
if (!STAGE_TYPES.includes(val.type)) {
throw new Error(`Stage has invalid 'type'. Must be one of: ${STAGE_TYPES.join(', ')}`);
}
for(const k of ['artist', 'title', 'album']) {
if(!(k in val)) {
for (const k of ['artist', 'title', 'album']) {
if (!(k in val)) {
continue;
}
if(!Array.isArray(val[k])) {
throw new Error(`${k} must be an array`);
}
for(const term of val[k]) {
if (val.type === 'user') {
if (!Array.isArray(val[k])) {
throw new Error(`${k} must be an array`);
}
try {
if(val.type === 'user') {
isSearchAndReplaceTerm(val[k]);
} else {
isExternalMetadataTerm(val[k]);
}
isSearchAndReplaceTerm(val[k]);
} catch (e) {
throw new Error(`Property '${k}' was not a valid type`, {cause: e});
throw new Error(`Property '${k}' was not a valid type`, { cause: e });
}
} else {
try {
isExternalMetadataTerm(val[k]);
} catch (e) {
throw new Error(`Property '${k}' was not a valid type`, { cause: e });
}
}
}