Skip to content

Commit 17722ef

Browse files
committed
WIP testing and improvements for artist astring parsing
* Implement faker data generation for artist string * Add test suite for parsing different styles of artist strings
1 parent c1e1c69 commit 17722ef

File tree

5 files changed

+204
-12
lines changed

5 files changed

+204
-12
lines changed

src/backend/tests/listenbrainz/listenbrainz.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ import { UpstreamError } from "../../common/errors/UpstreamError.js";
99
import { ListenbrainzApiClient, ListenResponse } from "../../common/vendor/ListenbrainzApiClient.js";
1010
import { ExpectedResults } from "../utils/interfaces.js";
1111
import { withRequestInterception } from "../utils/networking.js";
12-
import artistWithProperJoiner from './correctlyMapped/artistProperHasJoinerInName.json';
12+
import artistWithProperJoiner from './correctlyMapped/artistProperHasJoinerInName.json' with { type: "json" };
1313
// correct mappings
14-
import multiArtistInArtistName from './correctlyMapped/multiArtistInArtistName.json';
15-
import multiArtistsInTrackName from './correctlyMapped/multiArtistInTrackName.json';
16-
import multiMappedArtistsWithSingleUserArtist from './correctlyMapped/multiArtistMappingWithSingleRecordedArtist.json';
17-
import noArtistMapping from './correctlyMapped/noArtistMapping.json';
18-
import normalizedValues from './correctlyMapped/normalizedName.json';
19-
import slightlyDifferentNames from './correctlyMapped/trackNameSlightlyDifferent.json';
14+
import multiArtistInArtistName from './correctlyMapped/multiArtistInArtistName.json' with { type: "json" };
15+
import multiArtistsInTrackName from './correctlyMapped/multiArtistInTrackName.json' with { type: "json" };
16+
import multiMappedArtistsWithSingleUserArtist from './correctlyMapped/multiArtistMappingWithSingleRecordedArtist.json' with { type: "json" };
17+
import noArtistMapping from './correctlyMapped/noArtistMapping.json' with { type: "json" };
18+
import normalizedValues from './correctlyMapped/normalizedName.json' with { type: "json" };
19+
import slightlyDifferentNames from './correctlyMapped/trackNameSlightlyDifferent.json' with { type: "json" };
2020

2121
// incorrect mappings
22-
import incorrectMultiArtistsTrackName from './incorrectlyMapped/multiArtistsInTrackName.json';
23-
import veryWrong from './incorrectlyMapped/veryWrong.json';
22+
import incorrectMultiArtistsTrackName from './incorrectlyMapped/multiArtistsInTrackName.json' with { type: "json" };
23+
import veryWrong from './incorrectlyMapped/veryWrong.json' with { type: "json" };
2424

2525
interface LZTestFixture {
2626
data: ListenResponse
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { loggerTest, loggerDebug, childLogger } from "@foxxmd/logging";
2+
import chai, { assert, expect } from 'chai';
3+
import asPromised from 'chai-as-promised';
4+
import { after, before, describe, it } from 'mocha';
5+
6+
import { asPlays, generateArtistsStr, generatePlay, normalizePlays } from "../utils/PlayTestUtils.js";
7+
import { parseArtistCredits, parseCredits } from "../../utils/StringUtils.js";
8+
9+
describe('Parsing Artists from String', function() {
10+
11+
it('Parses Artists from an Artist-like string', function () {
12+
for(const i of Array(20)) {
13+
const [str, primaries, secondaries] = generateArtistsStr();
14+
const credits = parseArtistCredits(str);
15+
const allArtists = primaries.concat(secondaries);
16+
const parsed = [credits.primary].concat(credits.secondary ?? [])
17+
expect(primaries.concat(secondaries),`
18+
'${str}'
19+
Expected => ${allArtists.join(' || ')}
20+
Found => ${parsed.join(' || ')}`)
21+
.eql(parsed)
22+
}
23+
});
24+
25+
it('Parses singlar Artist with wrapped vs multiple', function () {
26+
const [str, primaries, secondaries] = generateArtistsStr({primary: 1, secondary: {num: 2, ft: 'vs', joiner: '/', ftWrap: true}});
27+
const credits = parseArtistCredits(str);
28+
const moreCredits = parseCredits(str);
29+
expect(true).eq(true);
30+
});
31+
32+
describe('When joiner is known', function () {
33+
34+
it('Parses many primary artists', function () {
35+
for(const i of Array(10)) {
36+
const [str, primaries, secondaries] = generateArtistsStr({primary: {max: 3, joiner: '/'}, secondary: 0});
37+
const credits = parseArtistCredits(str, ['/']);
38+
const allArtists = primaries.concat(secondaries);
39+
const parsed = [credits.primary].concat(credits.secondary ?? [])
40+
expect(primaries.concat(secondaries),`
41+
'${str}'
42+
Expected => ${allArtists.join(' || ')}
43+
Found => ${parsed.join(' || ')}`)
44+
.eql(parsed)
45+
}
46+
});
47+
48+
it('Parses many secondary artists', function () {
49+
// fails on -- Peso Pluma / Lil Baby / R. Kelly (featuring TOMORROW X TOGETHER / AC/DC / DaVido)
50+
for(const i of Array(10)) {
51+
const [str, primaries, secondaries] = generateArtistsStr({primary: {max: 3, joiner: '/'}, secondary: {joiner: '/', finalJoiner: false}});
52+
const credits = parseArtistCredits(str, ['/']);
53+
const allArtists = primaries.concat(secondaries);
54+
const parsed = [credits.primary].concat(credits.secondary ?? [])
55+
expect(primaries.concat(secondaries),`
56+
'${str}'
57+
Expected => ${allArtists.join(' || ')}
58+
Found => ${parsed.join(' || ')}`)
59+
.eql(parsed)
60+
}
61+
});
62+
63+
64+
});
65+
66+
67+
});

src/backend/tests/utils/PlayTestUtils.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import isBetween from "dayjs/plugin/isBetween.js";
55
import relativeTime from "dayjs/plugin/relativeTime.js";
66
import timezone from "dayjs/plugin/timezone.js";
77
import utc from "dayjs/plugin/utc.js";
8-
import { JsonPlayObject, ObjectPlayData, PlayMeta, PlayObject } from "../../../core/Atomic.js";
8+
import { FEAT, JOINERS, JOINERS_FINAL, JsonPlayObject, ObjectPlayData, PlayMeta, PlayObject } from "../../../core/Atomic.js";
99
import { sortByNewestPlayDate } from "../../utils.js";
10+
import { arrayListAnd } from '../../../core/StringUtils.js';
1011

1112
dayjs.extend(utc)
1213
dayjs.extend(isBetween);
@@ -154,3 +155,92 @@ export const generatePlay = (data: ObjectPlayData = {}, meta: PlayMeta = {}): Pl
154155
export const generatePlays = (numberOfPlays: number, data: ObjectPlayData = {}, meta: PlayMeta = {}): PlayObject[] => {
155156
return Array.from(Array(numberOfPlays), () => generatePlay(data, meta));
156157
}
158+
159+
export const generateArtist = () => faker.music.artist;
160+
161+
export const generateArtists = (num?: number, max: number = 3) => {
162+
// if(num !== undefined) {
163+
// return Array(num).map(x => faker.music.artist);
164+
// }
165+
if(num === 0 || max === 0) {
166+
return [];
167+
}
168+
return faker.helpers.multiple(faker.music.artist, {count: {min: num ?? 1, max: num ?? max}});
169+
}
170+
171+
export interface ArtistGenerateOptions {
172+
num?: number
173+
max?: number
174+
joiner?: string
175+
finalJoiner?: false | string
176+
spacedJoiners?: boolean
177+
}
178+
179+
export interface SecondaryArtistGenerateOptions extends ArtistGenerateOptions {
180+
ft?: string
181+
ftWrap?: boolean
182+
}
183+
184+
export interface CompoundArtistGenerateOptions {
185+
primary?: number | ArtistGenerateOptions
186+
secondary?: number | SecondaryArtistGenerateOptions
187+
}
188+
189+
export const generateArtistsStr = (options: CompoundArtistGenerateOptions = {}): [string, string[], string[]] => {
190+
191+
const {primary = {}, secondary = {}} = options;
192+
193+
const primaryOpts: ArtistGenerateOptions = typeof primary === 'number' ? {num: primary} : primary;
194+
const secondaryOpts: SecondaryArtistGenerateOptions = typeof secondary === 'number' ? {num: secondary} : secondary;
195+
196+
const primaryArt = generateArtists(primaryOpts.num, primaryOpts.max)
197+
const secondaryArt = generateArtists(secondaryOpts.num, secondaryOpts.max);
198+
199+
200+
const joinerPrimary: string = primaryOpts.joiner ?? faker.helpers.arrayElement(JOINERS);
201+
let finalJoinerPrimary: string = joinerPrimary;
202+
if(primaryOpts.finalJoiner !== false) {
203+
if(primaryOpts.finalJoiner === undefined) {
204+
if(joinerPrimary === ',') {
205+
finalJoinerPrimary = faker.helpers.arrayElement(JOINERS_FINAL);
206+
}
207+
208+
} else {
209+
finalJoinerPrimary = primaryOpts.finalJoiner;
210+
}
211+
}
212+
213+
const primaryStr = arrayListAnd(primaryArt, joinerPrimary, finalJoinerPrimary, primaryOpts.spacedJoiners);
214+
215+
if(secondaryArt.length === 0) {
216+
return [primaryStr, primaryArt, []];
217+
}
218+
219+
const joinerSecondary: string = secondaryOpts.joiner ?? faker.helpers.arrayElement(JOINERS);
220+
let finalJoinerSecondary: string = joinerSecondary;
221+
if(secondaryOpts.finalJoiner !== false) {
222+
if(secondaryOpts.finalJoiner === undefined) {
223+
if(joinerSecondary === ',') {
224+
finalJoinerSecondary = faker.helpers.arrayElement(JOINERS_FINAL);
225+
}
226+
} else {
227+
finalJoinerSecondary = secondaryOpts.finalJoiner;
228+
}
229+
}
230+
231+
const secondaryStr = arrayListAnd(secondaryArt, joinerSecondary, finalJoinerSecondary, secondaryOpts.spacedJoiners);
232+
const ft = secondaryOpts.ft ?? faker.helpers.arrayElement(FEAT);
233+
let sec = `${ft} ${secondaryStr}`;
234+
let wrap: boolean;
235+
if(secondaryOpts.ftWrap !== undefined) {
236+
wrap = secondaryOpts.ftWrap;
237+
} else {
238+
wrap = faker.datatype.boolean();
239+
}
240+
if(wrap) {
241+
sec = `(${sec})`;
242+
}
243+
const artistStr = `${primaryStr} ${sec}`;
244+
245+
return [artistStr, primaryArt, secondaryArt];
246+
}

src/core/Atomic.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export interface SourcePlayerObj {
207207
play: PlayObject,
208208
playFirstSeenAt?: string,
209209
playLastUpdatedAt?: string,
210-
playerLastUpdatedAt: string
210+
playerLastUpdatedAt: strin
211211
position?: Second
212212
listenedDuration: Second
213213
status: {
@@ -273,4 +273,13 @@ export interface URLData {
273273
url: URL
274274
normal: string
275275
port: number
276-
}
276+
}
277+
278+
export type Joiner = ',' | '&' | '/' | '\\' | string;
279+
export const JOINERS: Joiner[] = [',','&','/','\\'];
280+
281+
export type FinalJoiners = '&';
282+
export const JOINERS_FINAL: FinalJoiners[] = ['&'];
283+
284+
export type Feat = 'ft' | 'feat' | 'vs' | 'ft.' | 'feat.' | 'vs.' | 'featuring'
285+
export const FEAT: Feat[] = ['ft','feat','vs','ft.','feat.','vs.','featuring'];

src/core/StringUtils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,29 @@ export const combinePartsToString = (parts: any[], glue: string = '-'): string |
196196
}
197197
return undefined;
198198
}
199+
200+
export const arrayListOxfordAnd = (list: string[], joiner: string, finalJoiner: string, spaced: boolean = true): string => {
201+
if(list.length === 1) {
202+
return list[0];
203+
}
204+
const start = list.slice(0, list.length - 1);
205+
const end = list.slice(list.length - 1);
206+
207+
const joinerProper = joiner === ',' ? ', ' : (spaced ? ` ${joiner} ` : joiner);
208+
const finalProper = spaced ? ` ${finalJoiner} ` : finalJoiner;
209+
210+
return [start.join(joinerProper), end].join(joiner === ',' && spaced ? `,${finalProper}` : finalProper);
211+
}
212+
213+
export const arrayListAnd = (list: string[], joiner: string, finalJoiner: string, spaced: boolean = true): string => {
214+
if(list.length === 1) {
215+
return list[0];
216+
}
217+
const start = list.slice(0, list.length - 1);
218+
const end = list.slice(list.length - 1);
219+
220+
const joinerProper = joiner === ',' ? ', ' : (spaced ? ` ${joiner} ` : joiner);
221+
const finalProper = spaced ? ` ${finalJoiner} ` : finalJoiner;
222+
223+
return [start.join(joinerProper), end].join(finalProper);
224+
}

0 commit comments

Comments
 (0)