refactor(listenbrainz): Consolidate lz endpoint submit parsing into lz response parsing

Consolidtes logic for parsing lz listen data structure
This commit is contained in:
FoxxMD
2025-11-13 16:25:06 +00:00
parent fa4bf5ab10
commit ce7ad36db4
3 changed files with 110 additions and 99 deletions
+72 -97
View File
@@ -270,82 +270,27 @@ export class ListenbrainzApiClient extends AbstractApiClient {
export const listenPayloadToPlay = (payload: ListenPayload, nowPlaying: boolean = false): PlayObject => {
// const listened = payload.listened_at ?? dayjs().unix();
// const listenedAt = typeof listened === 'number' ? dayjs.unix(listened) : dayjs(listened);
// const {
// track_metadata: {
// additional_info = {}
// } = {},
// } = payload;
// const play = ListenbrainzApiClient.listenResponseToPlay({
// ...payload,
// track_metadata: {
// ...payload.track_metadata,
// additional_info
// },
// listened_at: listenedAt.unix()
// });
// play.meta.nowPlaying = nowPlaying;
const listened = payload.listened_at ?? dayjs().unix();
const listenedAt = typeof listened === 'number' ? dayjs.unix(listened) : dayjs(listened);
const {
listened_at = dayjs().unix(),
track_metadata: {
artist_name,
track_name,
release_name,
additional_info: {
duration,
duration_ms,
track_mbid,
artist_mbids,
artist_names = [],
release_mbid,
release_group_mbid,
release_artist_name,
release_artist_names = []
} = {}
additional_info = {}
} = {},
} = payload;
let albumArtists: string[];
if(release_artist_name !== undefined) {
albumArtists = [release_artist_name];
}
if(release_artist_names.length > 0) {
albumArtists = unique([...(albumArtists ?? []), ...release_artist_names])
}
const play = listenResponseToPlay({
...payload,
track_metadata: {
...payload.track_metadata,
additional_info,
},
listened_at: listenedAt.unix()
});
let dur: number = duration;
if(dur === undefined && duration_ms !== undefined) {
dur = duration_ms/1000;
}
play.meta.nowPlaying = nowPlaying;
const oldPlay = {
data: {
playDate: typeof listened_at === 'number' ? dayjs.unix(listened_at) : dayjs(listened_at),
track: track_name,
artists: unique([artist_name, ...artist_names]),
albumArtists,
album: release_name,
duration: dur,
meta: {
brainz: {
artist: artist_mbids !== undefined ? artist_mbids : undefined,
album: release_mbid,
albumArtist: release_group_mbid,
track: track_mbid
}
}
},
meta: {
nowPlaying,
}
}
return oldPlay;
return play;
}
export const listenResponseToPlay = (listen: ListenResponse): PlayObject => {
@@ -367,7 +312,7 @@ export const listenResponseToPlay = (listen: ListenResponse): PlayObject => {
} = {}
} = listen;
const naivePlay = listenResponseToNaivePlay(listen);
const naivePlay = listenToNaivePlay(listen);
if(artistMappings.length === 0) {
// if there are no artist mappings its likely MB doesn't have info on this track so just use our internally derived attempt
@@ -616,7 +561,7 @@ export const listenResponseToPlay = (listen: ListenResponse): PlayObject => {
/**
* Try to parse true artists and track name without using MB information
* */
export const listenResponseToNaivePlay = (listen: ListenResponse): PlayObject => {
export const listenToNaivePlay = (listen: ListenResponse): PlayObject => {
const {
listened_at,
recording_msid,
@@ -627,12 +572,18 @@ export const listenResponseToNaivePlay = (listen: ListenResponse): PlayObject =>
additional_info: {
recording_msid: aRecordingMsid,
recording_mbid: aRecordingMbid,
release_artist_name,
release_artist_names = [],
release_group_mbid,
release_mbid,
artist_mbids = [],
duration: aDuration,
duration_ms: aDurationMs,
music_service_name,
music_service,
submission_client,
submission_client_version
submission_client_version,
artist_names = [],
} = {},
mbid_mapping: {
recording_mbid: mRecordingMbid
@@ -649,24 +600,39 @@ export const listenResponseToNaivePlay = (listen: ListenResponse): PlayObject =>
}
let normalTrackName = track_name;
let artists: string[] = [artist_name];
let artists: string[] = [];
// since we aren't using MB mappings we should be conservative and assume artist string with & are proper names (not joiner)
const parsedArtists = parseCredits(artist_name, [',', '/', '\\']);
if (parsedArtists !== undefined) {
if (parsedArtists.primary !== undefined) {
artists.push(parsedArtists.primary);
if(artist_names.length > 0) {
artists = artist_names;
} else {
artists = [artist_name];
// since we aren't using MB mappings we should be conservative and assume artist string with & are proper names (not joiner)
const parsedArtists = parseCredits(artist_name, [',', '/', '\\']);
if (parsedArtists !== undefined) {
if (parsedArtists.primary !== undefined) {
artists.push(parsedArtists.primary);
}
artists = artists.concat(parsedArtists.secondary);
}
artists = artists.concat(parsedArtists.secondary);
// use all delimiters when trying to find artists in track name
const parsedTrackArtists = parseCredits(track_name);
if (parsedTrackArtists !== undefined) {
// if we found "ft. something" in track string then we now have a "real" track name and more artists
normalTrackName = parsedTrackArtists.primary;
artists = artists.concat(parsedTrackArtists.secondary)
}
artists = uniqueNormalizedStrArr(artists);
}
// use all delimiters when trying to find artists in track name
const parsedTrackArtists = parseCredits(track_name);
if (parsedTrackArtists !== undefined) {
// if we found "ft. something" in track string then we now have a "real" track name and more artists
normalTrackName = parsedTrackArtists.primary;
artists = artists.concat(parsedTrackArtists.secondary)
let albumArtists: string[];
if(release_artist_name !== undefined) {
albumArtists = [release_artist_name];
}
artists = uniqueNormalizedStrArr(artists);
if(release_artist_names.length > 0) {
albumArtists = unique([...(albumArtists ?? []), ...release_artist_names])
}
const play: PlayObject = {
data: {
@@ -674,24 +640,35 @@ export const listenResponseToNaivePlay = (listen: ListenResponse): PlayObject =>
track: normalTrackName,
artists: artists,
album: release_name,
duration: dur
albumArtists,
duration: dur,
meta: {
}
},
meta: {
source: 'listenbrainz',
source: submission_client ?? 'listenbrainz',
playId,
deviceId: combinePartsToString([music_service_name ?? music_service, submission_client, submission_client_version])
}
}
const brainzMeta: BrainzMeta = {};
if(Object.keys(additional_info).length > 0) {
brainzMeta.additionalInfo = additional_info;
if(trackId !== undefined) {
play.meta.trackid = trackId;
}
// we shouldn't include more metdata here because we don't know if the MB mapped data is actually correct
if(trackId !== undefined) {
brainzMeta.track = trackId;
play.meta.trackid = trackId;
const brainzMeta: BrainzMeta = removeUndefinedKeys({
album: release_mbid,
releaseGroup: release_group_mbid,
track: trackId
}) ?? {};
if(Object.keys(additional_info).length > 0) {
brainzMeta.additionalInfo = additional_info;
}
if(artist_mbids.filter(x => x.trim() !== "").length > 0) {
brainzMeta.artist = artist_mbids.filter(x => x.trim() !== "");
brainzMeta.additionalInfo.artist_mbids = brainzMeta.artist;
}
if(Object.keys(brainzMeta).length > 0) {
@@ -809,6 +786,4 @@ export const musicServiceToCononical = (str: string): string | undefined => {
}
}
return undefined;
}
const FEAT_REGEX = new RegExp(/(.+) feat\. (.+)/);
}
+3 -1
View File
@@ -97,6 +97,8 @@ export interface AdditionalTrackInfo {
spotify_artist_ids?: string[];
artist_names?: string[];
albumartist?: string;
tracknumber?: number
}
export interface Track {
artist_name: string;
@@ -139,7 +141,7 @@ export interface TrackPayload extends MinimumTrack {
mbid_mapping?: MbidMapping
}
export interface ListenPayload {
listened_at: Date | number;
listened_at?: Date | number;
track_metadata: TrackPayload;
}
@@ -7,7 +7,7 @@ import { PlayObject } from "../../../core/Atomic.js";
import { UpstreamError } from "../../common/errors/UpstreamError.js";
import { ListenbrainzApiClient, playToListenPayload, listenResponseToPlay, listenPayloadToPlay } from "../../common/vendor/ListenbrainzApiClient.js";
import { ListenResponse } from '../../common/vendor/listenbrainz/interfaces.js';
import { ListenPayload, ListenResponse, SubmitPayload } from '../../common/vendor/listenbrainz/interfaces.js';
import { ExpectedResults } from "../utils/interfaces.js";
import { withRequestInterception } from "../utils/networking.js";
import artistWithProperJoiner from './correctlyMapped/artistProperHasJoinerInName.json' with { type: "json" };
@@ -194,4 +194,38 @@ describe('Listenbrainz Endpoint Behavior', function() {
});
it('Should use artist_names if provided, rather than parse artist from string', function () {
const playFromPayload = listenPayloadToPlay(submit);
expect(playFromPayload.data.artists).to.be.eql(submit.track_metadata.additional_info.artist_names);
});
});
const submit: ListenPayload = {
track_metadata: {
artist_name: "Télépopmusik feat. Mau",
track_name: "15 Minutes",
release_name: "Angel Milk",
additional_info: {
submission_client: "navidrome",
submission_client_version: "0.58.5 (131c0c56)",
tracknumber: 15,
artist_names: [
"Télépopmusik",
"Mau",
],
artist_mbids: [
"265f242e-cf4e-4fbe-a3fe-43112387172f",
"",
],
recording_mbid: "69864bde-4958-484e-bbeb-f9d8f06eb932",
release_mbid: "90e011e2-1a3b-483c-9684-355601689c0f",
release_group_mbid: "d1456679-3901-30a6-929c-39d6d84f49a0",
duration_ms: 939020,
},
},
};