refactor(rocksky): Transformer cleanup and undefined guards

This commit is contained in:
FoxxMD 2026-09-25 10:20:26 -04:00
parent 47e737929d
commit 5eb229d9e0
4 changed files with 172 additions and 156 deletions

View file

@ -260,7 +260,7 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
protected async doBuildInitData(): Promise<true | string | undefined> {
this.defaults = parseStageConfig(this.config.defaults, childLogger(this.logger, 'Defaults'));
this.api = new MusicbrainzApiClientPool(this.config.name, {apis: this.config.data!.apis}, {
this.api = new MusicbrainzApiClientPool(this.config.name, {apis: this.config.data?.apis ?? [{enable: true}]}, {
logger: this.logger,
cache: this.clientCache,
logUrl: this.config.options?.logUrl
@ -340,25 +340,25 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
try {
switch(searchType) {
case 'isrc':
results = (await this.searchByIsrc(play, stageConfig, opts))!;
results = (await this.searchByIsrc(play, stageConfig, opts));
break;
case 'album':
results = (await this.searchByAlbum(play, stageConfig, opts))!;
results = (await this.searchByAlbum(play, stageConfig, opts));
break;
case 'artist':
results = (await this.searchByArtist(play, stageConfig, opts))!;
results = (await this.searchByArtist(play, stageConfig, opts));
break;
case 'basic':
results = (await this.searchByBasicFields(play, stageConfig, opts))!;
results = (await this.searchByBasicFields(play, stageConfig, opts));
break;
case 'freetext':
results = (await this.searchByFreeText(play, stageConfig, opts))!;
results = (await this.searchByFreeText(play, stageConfig, opts));
break;
case 'basicorids':
results = (await this.searchByBasicFieldsOrMBIDs(play, stageConfig, opts))!;
results = (await this.searchByBasicFieldsOrMBIDs(play, stageConfig, opts));
break;
case 'mbidrecording':
results = (await this.searchByRecordingMbid(play, stageConfig, opts))!;
results = (await this.searchByRecordingMbid(play, stageConfig, opts));
break;
}
queries.push({type: `mbQuery-${searchType}${results.recordings.length === 0 ? '-empty' : ''}`, input: results.requestQuery});
@ -383,12 +383,12 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
return {...results, requestQueries: queries};
}
public async searchByBasicFields(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByBasicFields(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
this.logger.debug({labels: ['Basic Search']}, 'Searching by artist/album/track');
return await this.api.searchByRecording(play, opts);
}
public async searchByBasicFieldsOrMBIDs(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByBasicFieldsOrMBIDs(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
const using: UsingTypes[] = [];
const {
data: {
@ -412,7 +412,7 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
return await this.api.searchByRecording(play, {using, ...opts});
}
public async searchByIsrc(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByIsrc(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
if(play.data.isrc !== undefined) {
this.logger.debug({labels: ['ISRC Search']},'Searching with ISRC');
return await this.api.searchByRecording(play, {using: ['isrc'], ...opts});
@ -420,7 +420,7 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
throw new SearchPrerequisiteError('Play does not have ISRC');
}
public async searchByRecordingMbid(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByRecordingMbid(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
if(play.data.meta?.brainz?.recording !== undefined) {
this.logger.debug({labels: ['MBID Search']},'Searching with Recording MBID');
return await this.api.searchByRecording(play, {using: ['mbidrecording'], ...opts});
@ -428,7 +428,7 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
throw new SearchPrerequisiteError('Play does not have recording MBID');
}
public async searchByAlbum(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByAlbum(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
// possibly the artist is incorrect (may be combined as one string)
// if we have an album we can likely still get a decent hit w/o using artist
if(play.data.album !== undefined && play.data.artists !== undefined && play.data.artists.length > 0) {
@ -441,57 +441,56 @@ export default class MusicbrainzTransformer extends AtomicPartsTransformer<Exter
if(play.data.artists === undefined) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
if(play.data.artists.length === 1) {
throw new SearchPrerequisiteError('Play only has one artist');
}
// only other possibility is artists is empty
throw new SearchPrerequisiteError('Play does not have any artists');
}
public async searchByArtist(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByArtist(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
const {
searchArtistMethod = this.defaults.searchArtistMethod,
} = stageConfig;
// if only one artist
// then its likely artist string is combined
if(play.data.artists !== undefined && play.data.artists.length === 1) {
if(searchArtistMethod === 'naive') {
// try a naive split using any common delimiter found and use the first value as artist
// -- this will likely result in a less accurate match but at least it might find something
// -- usually the "primary artist" is listed first in a combined artist string so cross your fingers this works
const naiveSplit = splitByFirstRegexFound(play.data.artists[0].name, [play.data.artists[0].name]).map(x => x.trim());
if(naiveSplit.length > 1) {
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with track + first value from artist string split');
return await this.api.searchByRecording({...play, data: {
...play.data,
artists: [{name: naiveSplit[0]}]
}}, {using: ['title','artist']});
} else {
throw new SearchPrerequisiteError('Naive parsing did not produce multiple artists');
}
} else if(searchArtistMethod === 'native') {
// use MS native parsing to extract artists from artist string and title
// -- cleaning title is aggressive but at this point MB has not found anything which means its likely not "proper"
// IE "My Track (Cool Remix)" is a proper recording name but "My Track (feat. Someone)" is not bc MB would have removed the joiner from the name
// so we do the same to hopefully get a match
//
// ...additionally, since MB hasn't found anything with single artist string its likely the artist name does not have a common delimiter as part of their proper name
// IE "Crosby, Stills, Nash & Young" is a proper artist name with delimiters included but "My Artist & My Feat Artists" is not
// so we split out all artists by all found delimiters
const nativePlay = nativeParse(play, {titleClean: true, delimiters: DELIMITERS});
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with aggressive native parsing');
return await this.api.searchByRecording(nativePlay, {using: ['title','artist'], ...opts});
}
}
if(play.data.artists === undefined) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
if(play.data.artists.length > 1) {
throw new SearchPrerequisiteError('Play has more than one artist already');
if(play.data.artists === undefined) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
if(play.data.artists.length > 1) {
throw new SearchPrerequisiteError('Play has more than one artist already');
}
if(play.data.artists.length === 0) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
// if only one artist
// then its likely artist string is combined
if(searchArtistMethod === 'naive') {
// try a naive split using any common delimiter found and use the first value as artist
// -- this will likely result in a less accurate match but at least it might find something
// -- usually the "primary artist" is listed first in a combined artist string so cross your fingers this works
const naiveSplit = splitByFirstRegexFound(play.data.artists[0].name, [play.data.artists[0].name]).map(x => x.trim());
if(naiveSplit.length > 1) {
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with track + first value from artist string split');
return await this.api.searchByRecording({...play, data: {
...play.data,
artists: [{name: naiveSplit[0]}]
}}, {using: ['title','artist']});
}
throw new SearchPrerequisiteError('Naive parsing did not produce multiple artists');
}
if(searchArtistMethod === 'native') {
// use MS native parsing to extract artists from artist string and title
// -- cleaning title is aggressive but at this point MB has not found anything which means its likely not "proper"
// IE "My Track (Cool Remix)" is a proper recording name but "My Track (feat. Someone)" is not bc MB would have removed the joiner from the name
// so we do the same to hopefully get a match
//
// ...additionally, since MB hasn't found anything with single artist string its likely the artist name does not have a common delimiter as part of their proper name
// IE "Crosby, Stills, Nash & Young" is a proper artist name with delimiters included but "My Artist & My Feat Artists" is not
// so we split out all artists by all found delimiters
const nativePlay = nativeParse(play, {titleClean: true, delimiters: DELIMITERS});
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with aggressive native parsing');
return await this.api.searchByRecording(nativePlay, {using: ['title','artist'], ...opts});
}
throw new SearchPrerequisiteError(`Unknown searchArtistMethod '${searchArtistMethod}'`);
}
public async searchByFreeText(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList | undefined> {
public async searchByFreeText(play: PlayObject, stageConfig: MusicbrainzTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<IRecordingMSList> {
this.logger.debug({labels: ['Freetext Search']},'Trying freetext search');
const results = await this.api.searchByRecording(play, {freetext: true, ...opts}) as IRecordingMSList;
results.freeText = true;
@ -667,7 +666,7 @@ export const filterByValidReleaseStatus = <T extends IRecordingMatch[]>(list: T,
})
}));
return releaseFiltered.filter(x => (
(list.find(y => y.id === x.id)!.releases ?? []).length === 0
(list.find(y => y.id === x.id)?.releases ?? []).length === 0
&& releaseAllowEmpty
)
|| x.releases.length > 0);
@ -692,7 +691,7 @@ export const filterByValidReleaseGroupPrimary = <T extends IRecordingMatch[]>(li
})
}));
return releaseFiltered.filter(x => (
(list.find(y => y.id === x.id)!.releases ?? []).length === 0
(list.find(y => y.id === x.id)?.releases ?? []).length === 0
&& releaseAllowEmpty
)
|| x.releases.length > 0);
@ -717,7 +716,7 @@ export const filterByValidReleaseGroupSecondary = (list: IRecordingMatch[], stag
})
}));
return releaseFiltered.filter(x => (
(list.find(y => y.id === x.id)!.releases ?? []).length === 0
(list.find(y => y.id === x.id)?.releases ?? []).length === 0
&& releaseAllowEmpty
)
|| x.releases.length > 0);
@ -742,14 +741,15 @@ export const filterByValidReleaseCountry = (list: IRecordingMatch[], stageConfig
})
}));
return releaseFiltered.filter(x => (
(list.find(y => y.id === x.id)!.releases ?? []).length === 0
(list.find(y => y.id === x.id)?.releases ?? []).length === 0
&& releaseAllowEmpty
)
|| x.releases.length > 0);
}
export const filterByExplicitTrackMbid = (list: IRecordingMatch[], play: PlayObject): [IRecordingMatch[], boolean] => {
if (play.data.meta?.brainz?.track === undefined) {
const trackMbid = play.data.meta?.brainz?.track;
if (trackMbid === undefined) {
return [list, false];
}
let recMatch: IRecordingMatch | undefined,
@ -758,13 +758,13 @@ export const filterByExplicitTrackMbid = (list: IRecordingMatch[], play: PlayObj
if (recMatch !== undefined) {
break;
}
for (const rel of rec.releases!) {
for (const rel of rec.releases ?? []) {
if (rel.media.some(x => {
if(x.tracks !== undefined) {
return x.tracks.some(y => y.id === play.data.meta!.brainz!.track);
return x.tracks.some(y => y.id === trackMbid);
}
if(x.track !== undefined) {
return x.track.some(y => y.id === play.data.meta!.brainz!.track);
return x.track.some(y => y.id === trackMbid);
}
return false;
})) {
@ -776,14 +776,15 @@ export const filterByExplicitTrackMbid = (list: IRecordingMatch[], play: PlayObj
}
if (recMatch !== undefined) {
const r = structuredClone(recMatch);
r.releases = r.releases!.filter(x => x.id === releaseMatchId);
r.releases = (r.releases ?? []).filter(x => x.id === releaseMatchId);
return [[r], true];
}
return [list, false];
}
export const filterByExplicitReleaseMbid = (list: IRecordingMatch[], play: PlayObject): [IRecordingMatch[], boolean] => {
if (play.data.meta?.brainz?.album === undefined) {
const albumMbid = play.data.meta?.brainz?.album;
if (albumMbid === undefined) {
return [list, false];
}
let recMatch: IRecordingMatch | undefined,
@ -792,8 +793,8 @@ export const filterByExplicitReleaseMbid = (list: IRecordingMatch[], play: PlayO
if (recMatch !== undefined) {
break;
}
for (const rel of rec.releases!) {
if(rel.id === play.data.meta!.brainz!.album) {
for (const rel of rec.releases ?? []) {
if(rel.id === albumMbid) {
releaseMatchId = rel.id;
recMatch = rec;
}
@ -801,7 +802,7 @@ export const filterByExplicitReleaseMbid = (list: IRecordingMatch[], play: PlayO
}
if (recMatch !== undefined) {
const r = structuredClone(recMatch);
r.releases = r.releases!.filter(x => x.id === releaseMatchId);
r.releases = (r.releases ?? []).filter(x => x.id === releaseMatchId);
return [[r], true];
}
return [list, false];
@ -836,7 +837,7 @@ export const rankReleasesByPriority = (list: IRecordingMatch[], stageConfig: Mus
const grpPAScore = groupPrimaryPriority.findIndex(x => x === a["release-group"]?.["primary-type"]?.toLocaleLowerCase()) + 1;
const grpSAScore = (a["release-group"]?.["secondary-types"] ?? []).reduce((acc: number, curr: string) => acc + groupSecPriority.findIndex(x => x === (curr as MBReleaseGroupSecondaryType).toLocaleLowerCase()) + 1,0);
const countryAScore = countryPriority.findIndex(x => a.country === undefined ? false : x === a.country.toLocaleLowerCase()) + 1;
const compareScore = scoreNormalizedStringsWeighted(play.data.album!, a.title, albumWeight, albumWeight !== 0 ? 0.05 : 0);
const compareScore = scoreNormalizedStringsWeighted(play.data.album, a.title, albumWeight, albumWeight !== 0 ? 0.05 : 0);
return {
...a,
albumScore: statAScore + grpPAScore + grpSAScore + countryAScore + compareScore,
@ -848,7 +849,7 @@ export const rankReleasesByPriority = (list: IRecordingMatch[], stageConfig: Mus
if(releases.length > 0) {
albumScore = releases[0].albumCompareScore;
}
const titleScore = titleWeight === 0 ? 0 : scoreTrackWeightedAndNormalized(play.data.track!, x.title, titleWeight, {exact: 0.05, naive: 0.03})[0];
const titleScore = titleWeight === 0 ? 0 : scoreTrackWeightedAndNormalized(play.data.track ?? '', x.title, titleWeight, {exact: 0.05, naive: 0.03})[0];
return {
...x,

View file

@ -182,7 +182,7 @@ export const configFromEnv = (logger: MaybeLogger = new MaybeLogger()) => {
}
if (soSet.size > 0) {
mbConfig.defaults!.searchOrder = Array.from(soSet);
mbConfig.defaults = {...mbConfig.defaults, searchOrder: Array.from(soSet)};
}
logger.debug(`Using presets: ${presets.join(',')}`);
}

View file

@ -183,29 +183,36 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
for(const searchType of searchOrder) {
try {
let searchResult: SongViewDetailedMS | undefined;
switch(searchType) {
case 'isrc':
results = await this.searchByIsrc(play, stageConfig, opts);
searchResult = await this.searchByIsrc(play, stageConfig, opts);
break;
case 'artist':
results = await this.searchByArtist(play, stageConfig, opts);
searchResult = await this.searchByArtist(play, stageConfig, opts);
break;
case 'basic':
results = await this.searchByBasicFields(play, stageConfig, opts);
searchResult = await this.searchByBasicFields(play, stageConfig, opts);
break;
case 'basicorids':
results = await this.searchByBasicFieldsOrIDs(play, stageConfig, opts);
searchResult = await this.searchByBasicFieldsOrIDs(play, stageConfig, opts);
break;
case 'mbid':
results = await this.searchByRecordingMbid(play, stageConfig, opts);
searchResult = await this.searchByRecordingMbid(play, stageConfig, opts);
break;
}
queries.push({type: `rsQuery-${searchType}${(results!.matches ?? []).length === 0 ? '-resultButNoMatch' : ''}`, input: results!.requestQuery});
if((results!.matches ?? []).length === 0 && !allowNoMatch) {
if(searchResult === undefined) {
this.logger.warn(`Unknown search type '${searchType}'`);
continue;
}
results = searchResult;
const matches = results.matches ?? [];
queries.push({type: `rsQuery-${searchType}${matches.length === 0 ? '-resultButNoMatch' : ''}`, input: results.requestQuery});
if(matches.length === 0 && !allowNoMatch) {
this.logger.debug(`'${searchType}' search type returned result but no matches`);
continue;
}
if((results!.matches ?? []).length > 0 && !results!.matches!.some(x => x.score! >= score)) {
if(matches.length > 0 && !matches.some(x => x.score !== undefined && x.score >= score)) {
this.logger.debug(`'${searchType}' search type returned no matches with score >= ${score}`);
continue;
}
@ -235,11 +242,11 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
this.logger.debug({ labels: ['Basic Search'] }, 'Searching by artist/album/track');
const requestQuery = JSON.stringify({
title: play.data.track,
artitst: artistCreditsToNames(play.data.artists!).join(', '),
artitst: artistCreditsToNames(play.data.artists).join(', '),
album: play.data.album
});
try {
const res = await this.api.rsProxy.matchSong(play.data.track!, artistCreditsToNames(play.data.artists!).join(', '), undefined, undefined, play.data.album);
const res = await this.api.rsProxy.matchSong(requireTrack(play), artistCreditsToNames(play.data.artists).join(', '), undefined, undefined, play.data.album);
return {
requestQuery,
...res
@ -274,12 +281,12 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
this.logger.debug({labels: ['Basic Or MBID Search']}, `Searching using ${using.join(', ')}}`);
const requestQuery = JSON.stringify({
title: play.data.track,
artitst: artistCreditsToNames(play.data.artists!).join(', '),
artitst: artistCreditsToNames(play.data.artists).join(', '),
album: play.data.album,
mbid: brainz.recording
});
try {
const res = await this.api.rsProxy.matchSong(play.data.track!, artistCreditsToNames(play.data.artists!).join(', '), brainz.recording, play.data.isrc, play.data.album);
const res = await this.api.rsProxy.matchSong(requireTrack(play), artistCreditsToNames(play.data.artists).join(', '), brainz.recording, play.data.isrc, play.data.album);
return {
requestQuery,
...res
@ -295,11 +302,11 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
this.logger.debug({labels: ['ISRC Search']},'Searching with ISRC');
const requestQuery =JSON.stringify({
title: play.data.track,
artitst: artistCreditsToNames(play.data.artists!).join(', '),
artitst: artistCreditsToNames(play.data.artists).join(', '),
isrc: play.data.isrc
});
try{
const res = await this.api.rsProxy.matchSong(play.data.track!, artistCreditsToNames(play.data.artists!).join(', '), undefined, play.data.isrc);
const res = await this.api.rsProxy.matchSong(requireTrack(play), artistCreditsToNames(play.data.artists).join(', '), undefined, play.data.isrc);
return {
requestQuery,
...res
@ -317,11 +324,11 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
this.logger.debug({labels: ['MBID Search']},'Searching with Recording MBID');
const requestQuery = JSON.stringify({
title: play.data.track,
artitst: artistCreditsToNames(play.data.artists!).join(', '),
artitst: artistCreditsToNames(play.data.artists).join(', '),
mbid: play.data.meta?.brainz?.recording
});
try {
const res = await this.api.rsProxy.matchSong(play.data.track!, artistCreditsToNames(play.data.artists!).join(', '), play.data.meta?.brainz?.recording, undefined, undefined);
const res = await this.api.rsProxy.matchSong(requireTrack(play), artistCreditsToNames(play.data.artists).join(', '), play.data.meta?.brainz?.recording, undefined, undefined);
return {
requestQuery,
...res
@ -334,72 +341,72 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
throw new SearchPrerequisiteError('Play does not have recording MBID');
}
public async searchByArtist(play: PlayObject, stageConfig: RockskyTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<SongViewDetailedMS | undefined> {
public async searchByArtist(play: PlayObject, stageConfig: RockskyTransformerDataStage, opts: OptionalCacheUsage = {}): Promise<SongViewDetailedMS> {
const {
searchArtistMethod = this.defaults.searchArtistMethod,
} = stageConfig;
// if only one artist
// then its likely artist string is combined
if(play.data.artists !== undefined && play.data.artists.length === 1) {
if(searchArtistMethod === 'naive') {
// try a naive split using any common delimiter found and use the first value as artist
// -- this will likely result in a less accurate match but at least it might find something
// -- usually the "primary artist" is listed first in a combined artist string so cross your fingers this works
const naiveSplit = splitByFirstRegexFound(play.data.artists[0].name, [play.data.artists[0].name]).map(x => x.trim());
if(naiveSplit.length > 1) {
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with track + first value from artist string split');
const requestQuery = JSON.stringify({
title: play.data.track,
artitst: naiveSplit[0],
});
try {
const res = await this.api.rsProxy.matchSong(play.data.track!, naiveSplit[0]);
return {
requestQuery,
...res
};
} catch (e: any) {
e.requestQuery = requestQuery;
throw e;
}
} else {
throw new SearchPrerequisiteError('Naive parsing did not produce multiple artists');
}
} else if(searchArtistMethod === 'native') {
// use MS native parsing to extract artists from artist string and title
// -- cleaning title is aggressive but at this point MB has not found anything which means its likely not "proper"
// IE "My Track (Cool Remix)" is a proper recording name but "My Track (feat. Someone)" is not bc MB would have removed the joiner from the name
// so we do the same to hopefully get a match
//
// ...additionally, since MB hasn't found anything with single artist string its likely the artist name does not have a common delimiter as part of their proper name
// IE "Crosby, Stills, Nash & Young" is a proper artist name with delimiters included but "My Artist & My Feat Artists" is not
// so we split out all artists by all found delimiters
const nativePlay = nativeParse(play, {titleClean: true, delimiters: DELIMITERS});
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with aggressive native parsing');
const requestQuery = JSON.stringify({
title: nativePlay.data.track,
artitst: artistCreditsToNames(nativePlay.data.artists!).join(', '),
});
try {
const res = await this.api.rsProxy.matchSong(nativePlay.data.track!, artistCreditsToNames(nativePlay.data.artists!).join(', '));
return {
requestQuery,
...res
};
} catch (e: any) {
e.requestQuery = requestQuery;
throw e;
}
if(play.data.artists === undefined) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
if(play.data.artists.length > 1) {
throw new SearchPrerequisiteError('Play has more than one artist already');
}
if(play.data.artists.length === 0) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
// if only one artist
// then its likely artist string is combined
if(searchArtistMethod === 'naive') {
// try a naive split using any common delimiter found and use the first value as artist
// -- this will likely result in a less accurate match but at least it might find something
// -- usually the "primary artist" is listed first in a combined artist string so cross your fingers this works
const naiveSplit = splitByFirstRegexFound(play.data.artists[0].name, [play.data.artists[0].name]).map(x => x.trim());
if(naiveSplit.length > 1) {
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with track + first value from artist string split');
const requestQuery = JSON.stringify({
title: play.data.track,
artitst: naiveSplit[0],
});
try {
const res = await this.api.rsProxy.matchSong(requireTrack(play), naiveSplit[0]);
return {
requestQuery,
...res
};
} catch (e: any) {
e.requestQuery = requestQuery;
throw e;
}
}
if(play.data.artists === undefined) {
throw new SearchPrerequisiteError('Play does not have any artists');
}
if(play.data.artists.length > 1) {
throw new SearchPrerequisiteError('Play has more than one artist already');
throw new SearchPrerequisiteError('Naive parsing did not produce multiple artists');
}
if(searchArtistMethod === 'native') {
// use MS native parsing to extract artists from artist string and title
// -- cleaning title is aggressive but at this point MB has not found anything which means its likely not "proper"
// IE "My Track (Cool Remix)" is a proper recording name but "My Track (feat. Someone)" is not bc MB would have removed the joiner from the name
// so we do the same to hopefully get a match
//
// ...additionally, since MB hasn't found anything with single artist string its likely the artist name does not have a common delimiter as part of their proper name
// IE "Crosby, Stills, Nash & Young" is a proper artist name with delimiters included but "My Artist & My Feat Artists" is not
// so we split out all artists by all found delimiters
const nativePlay = nativeParse(play, {titleClean: true, delimiters: DELIMITERS});
this.logger.debug({labels: ['Parsed Artist Search']},'Searching with aggressive native parsing');
const requestQuery = JSON.stringify({
title: nativePlay.data.track,
artitst: artistCreditsToNames(nativePlay.data.artists).join(', '),
});
try {
const res = await this.api.rsProxy.matchSong(requireTrack(nativePlay), artistCreditsToNames(nativePlay.data.artists).join(', '));
return {
requestQuery,
...res
};
} catch (e: any) {
e.requestQuery = requestQuery;
throw e;
}
}
throw new SearchPrerequisiteError(`Unknown searchArtistMethod '${searchArtistMethod}'`);
}
public async handlePostFetch(play: PlayObject, transformData: SongViewDetailedMS, stageConfig: RockskyTransformerDataStage): Promise<PlayObject> {
@ -412,19 +419,20 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
throw new StagePrerequisiteError('All search prerequisites failed, Rocksky API could not be searched with the given searchOrder options', {shortStack: true});
}
let mergedSongView: SongViewDetailedMS = transformData;
if((transformData.matches ?? []).length === 0) {
const matches = transformData.matches ?? [];
if(matches.length === 0) {
if(!allowNoMatch) {
throw new StagePrerequisiteError('No matches returned from Rocksky API', {shortStack: true, inputs: transformData.requestQueries});
}
} else {
const filteredList: SongMatchView[] = transformData.matches!.filter(x => x.score! >= score);
const filteredList: SongMatchView[] = matches.filter(x => x.score !== undefined && x.score >= score);
if(filteredList.length === 0) {
throw new StagePrerequisiteError(`All ${transformData.matches!.length} candidate matches associated with this match had a score < ${score}, best match was ${transformData.matches![0].score}`, {shortStack: true});
throw new StagePrerequisiteError(`All ${matches.length} candidate matches associated with this match had a score < ${score}, best match was ${matches[0].score}`, {shortStack: true});
}
//const mergedConfig = Object.assign({}, removeUndefinedKeys({...this.defaults}), removeUndefinedKeys({...stageConfig}));
//filteredList = rankSongMatchesByPriority(filteredList, mergedConfig, play);
this.logger.debug(`${filteredList.length} of ${transformData.matches!.length} were valid, filtered matches. Using match with best score of ${filteredList[0].score}`);
this.logger.debug(`${filteredList.length} of ${matches.length} were valid, filtered matches. Using match with best score of ${filteredList[0].score}`);
mergedSongView = {
...transformData,
title: filteredList[0].title ?? transformData.title,
@ -488,7 +496,7 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
return transformData.data.artists;
}
// source provided only one artist but rocksky has real, separated artists
if((play.data.artists ?? []).length === 1 && transformData.data.artists!.length > 1) {
if((play.data.artists ?? []).length === 1 && (transformData.data.artists ?? []).length > 1) {
return transformData.data.artists;
}
@ -592,7 +600,7 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
if(play.meta.source === 'lastfm') {
return transformData.data.meta;
}
if(Object.keys(play.data.meta!.brainz ?? {}).length <= 1) {
if(Object.keys(play.data.meta?.brainz ?? {}).length <= 1) {
// only one (or none) mbids from original so likely no loss of fidelity by only using
// recording mbid from rocksky
return transformData.data.meta;
@ -665,4 +673,11 @@ export default class RockskyTransformer extends AtomicPartsTransformer<ExternalM
export class SearchPrerequisiteError extends SimpleError {
name = 'Search Prerequistie Failure';
}
}
const requireTrack = (play: PlayObject): string => {
if(play.data.track === undefined) {
throw new SearchPrerequisiteError('Play does not have a track title');
}
return play.data.track;
}

View file

@ -37,7 +37,7 @@ export const configFromEnv = (logger: MaybeLogger = new MaybeLogger()) => {
}
if (soSet.size > 0) {
rsConfig.defaults!.searchOrder = Array.from(soSet);
rsConfig.defaults = {...rsConfig.defaults, searchOrder: Array.from(soSet)};
}
logger.debug(`Using presets: ${presets.join(',')}`);
}