From ce7ad36db48f612e2e10fd711175dff485df031c Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Thu, 13 Nov 2025 16:25:06 +0000 Subject: [PATCH] refactor(listenbrainz): Consolidate lz endpoint submit parsing into lz response parsing Consolidtes logic for parsing lz listen data structure --- .../common/vendor/ListenbrainzApiClient.ts | 167 ++++++++---------- .../common/vendor/listenbrainz/interfaces.ts | 4 +- .../tests/listenbrainz/listenbrainz.test.ts | 36 +++- 3 files changed, 109 insertions(+), 98 deletions(-) diff --git a/src/backend/common/vendor/ListenbrainzApiClient.ts b/src/backend/common/vendor/ListenbrainzApiClient.ts index 1c3a4dba..14e13fa9 100644 --- a/src/backend/common/vendor/ListenbrainzApiClient.ts +++ b/src/backend/common/vendor/ListenbrainzApiClient.ts @@ -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]) - } - - let dur: number = duration; - if(dur === undefined && duration_ms !== undefined) { - dur = duration_ms/1000; - } + const play = listenResponseToPlay({ + ...payload, + track_metadata: { + ...payload.track_metadata, + additional_info, + }, + listened_at: listenedAt.unix() + }); - 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, - } - } + play.meta.nowPlaying = 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); + } + + let albumArtists: string[]; + if(release_artist_name !== undefined) { + albumArtists = [release_artist_name]; } - // 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) + if(release_artist_names.length > 0) { + albumArtists = unique([...(albumArtists ?? []), ...release_artist_names]) } - artists = uniqueNormalizedStrArr(artists); + 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(trackId !== undefined) { + 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; + } - - // 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; + 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\. (.+)/); \ No newline at end of file +} \ No newline at end of file diff --git a/src/backend/common/vendor/listenbrainz/interfaces.ts b/src/backend/common/vendor/listenbrainz/interfaces.ts index 5dc66b82..ca1ddd6d 100644 --- a/src/backend/common/vendor/listenbrainz/interfaces.ts +++ b/src/backend/common/vendor/listenbrainz/interfaces.ts @@ -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; } diff --git a/src/backend/tests/listenbrainz/listenbrainz.test.ts b/src/backend/tests/listenbrainz/listenbrainz.test.ts index 7948ddf8..a7e92693 100644 --- a/src/backend/tests/listenbrainz/listenbrainz.test.ts +++ b/src/backend/tests/listenbrainz/listenbrainz.test.ts @@ -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, + }, + }, +}; \ No newline at end of file -- 2.51.2