diff --git a/src/backend/common/Cache.ts b/src/backend/common/Cache.ts index c29c1c69..f69b5231 100644 --- a/src/backend/common/Cache.ts +++ b/src/backend/common/Cache.ts @@ -17,7 +17,7 @@ import { fileOrDirectoryIsWriteable } from '../utils/FSUtils.js'; import { asCacheAuthProvider, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js'; import { Typeson } from 'typeson'; import { builtin } from 'typeson-registry'; -import { loggerNoop } from './logging.js'; +import { loggerNoop } from './MaybeLogger.js'; import { ListenProgressPositional, ListenProgressTS } from '../sources/PlayerState/ListenProgress.js'; const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); diff --git a/src/backend/common/MaybeLogger.ts b/src/backend/common/MaybeLogger.ts new file mode 100644 index 00000000..8d8443f3 --- /dev/null +++ b/src/backend/common/MaybeLogger.ts @@ -0,0 +1,67 @@ +import { Logger } from "@foxxmd/logging"; + + +export class MaybeLogger { + logger?: Logger; + + constructor(logger?: Logger, label?: string) { + if (logger !== undefined && label !== undefined) { + this.logger = logger;//this.logger = childLogger(logger, label); + } else { + this.logger = logger; + } + } + + public info(first: any, ...rest: any) { + if (this.logger) { + this.logger.info(first, ...rest); + } + } + + public debug(first: any, ...rest: any) { + if (this.logger) { + this.logger.debug(first, ...rest); + } + } + + public trace(first: any, ...rest: any) { + if (this.logger) { + this.logger.trace(first, ...rest); + } + } + + public warn(first: any, ...rest: any) { + if (this.logger) { + this.logger.warn(first, ...rest); + } + } + + public verbose(first: any, ...rest: any) { + if (this.logger) { + this.logger.verbose(first, ...rest); + } + } + + public error(first: any, ...rest: any) { + if (this.logger) { + this.logger.error(first, ...rest); + } + } +} +export const noopLog = (_: any, ...rest: any) => undefined; + +export const loggerNoop: Logger = { + trace: noopLog, + debug: noopLog, + log: noopLog, + info: noopLog, + verbose: noopLog, + warn: noopLog, + error: noopLog, + fatal: noopLog, + silent: noopLog, + level: 'silent', + child: (_: any, ...rest: any) => loggerNoop as Logger +} as unknown as Logger; + + diff --git a/src/backend/common/errors/UpstreamError.ts b/src/backend/common/errors/UpstreamError.ts index 94e76b20..c0f1b922 100644 --- a/src/backend/common/errors/UpstreamError.ts +++ b/src/backend/common/errors/UpstreamError.ts @@ -12,7 +12,7 @@ export class UpstreamError extends Error { constructor(message: string, options?: UpstreamErrorOptions | undefined) { super(message, options); - const {showStopper = false, response, responseBody} = options; + const {showStopper = false, response, responseBody} = options ?? {}; this.showStopper = showStopper; this.response = response; this.responseBody = responseBody; diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts index d51c5899..e4c4f13f 100644 --- a/src/backend/common/infrastructure/Atomic.ts +++ b/src/backend/common/infrastructure/Atomic.ts @@ -1,10 +1,9 @@ import { Logger } from '@foxxmd/logging'; -import { SearchAndReplaceRegExp } from "@foxxmd/regex-buddy-core"; import { Dayjs, ManipulateType } from "dayjs"; import { Request, Response } from "express"; import { NextFunction, ParamsDictionary, Query } from "express-serve-static-core"; import { FixedSizeList } from 'fixed-size-list'; -import { isPlayObject, PlayMeta, PlayMetaLifecycleless, PlayObject, PlayObjectLifecycleless, UnixTimestamp } from "../../../core/Atomic.js"; +import { isPlayObject, PlayMetaLifecycleless, PlayObject, PlayObjectLifecycleless, UnixTimestamp } from "../../../core/Atomic.js"; import TupleMap from "../TupleMap.js"; import { MusicBrainzApi } from 'musicbrainz-api'; import { SourceType } from './config/source/sources.js'; diff --git a/src/backend/common/logging.ts b/src/backend/common/logging.ts index 8053e389..68bcfcea 100644 --- a/src/backend/common/logging.ts +++ b/src/backend/common/logging.ts @@ -1,4 +1,4 @@ -import { childLogger, FileLogOptions, Logger, loggerAppRolling, LogLevel, LogLevelStreamEntry, LogOptions, parseLogOptions } from '@foxxmd/logging'; +import { FileLogOptions, Logger, loggerAppRolling, LogLevel, LogLevelStreamEntry, LogOptions, parseLogOptions } from '@foxxmd/logging'; import { buildDestinationJsonPrettyStream, buildDestinationRollingFile, buildDestinationStdout, buildLogger } from "@foxxmd/logging/factory"; import { PassThrough, Transform } from "node:stream"; import path from "path"; @@ -74,60 +74,4 @@ export const componentFileLogger = async (type: string, name: string, fileConfig } } -export class MaybeLogger { - logger?: Logger - constructor(logger?: Logger, label?: string) { - if (logger !== undefined && label !== undefined) { - this.logger = childLogger(logger, label); - } else { - this.logger = logger; - } - } - - public info(first: any, ...rest: any) { - if (this.logger) { - this.logger.info(first, ...rest); - } - } - - public debug(first: any, ...rest: any) { - if (this.logger) { - this.logger.debug(first, ...rest); - } - } - - public warn(first: any, ...rest: any) { - if (this.logger) { - this.logger.warn(first, ...rest); - } - } - - public verbose(first: any, ...rest: any) { - if (this.logger) { - this.logger.verbose(first, ...rest); - } - } - - public error(first: any, ...rest: any) { - if (this.logger) { - this.logger.error(first, ...rest); - } - } -} - -const noopLog = (_: any, ...rest: any) => undefined; - -export const loggerNoop: Logger = { - trace: noopLog, - debug: noopLog, - log: noopLog, - info: noopLog, - verbose: noopLog, - warn: noopLog, - error: noopLog, - fatal: noopLog, - silent: noopLog, - level: 'silent', - child: (_: any, ...rest: any) => loggerNoop as Logger -} as unknown as Logger; \ No newline at end of file diff --git a/src/backend/common/transforms/MusicbrainzTransformer.ts b/src/backend/common/transforms/MusicbrainzTransformer.ts index 88bdc1b3..92e8f288 100644 --- a/src/backend/common/transforms/MusicbrainzTransformer.ts +++ b/src/backend/common/transforms/MusicbrainzTransformer.ts @@ -5,7 +5,7 @@ import { ExternalMetadataTerm, PlayTransformMetadataStage } from "../infrastruct import AtomicPartsTransformer from "./AtomicPartsTransformer.js"; import { TransformerOptions } from "./AbstractTransformer.js"; import { ARTIST_WEIGHT, DELIMITERS, MUSICBRAINZ_URL, MusicbrainzApiConfigData, TITLE_WEIGHT } from "../infrastructure/Atomic.js"; -import { MaybeLogger } from "../logging.js"; +import { MaybeLogger } from '../MaybeLogger.js'; import { childLogger, Logger } from "@foxxmd/logging"; import { MusicbrainzApiClient, MusicbrainzApiConfig, recordingToPlay, UsingTypes } from "../vendor/musicbrainz/MusicbrainzApiClient.js"; import { IRecordingList, IRecordingMatch, IRelease, MusicBrainzApi } from "musicbrainz-api"; diff --git a/src/backend/common/transforms/NativeTransformer.ts b/src/backend/common/transforms/NativeTransformer.ts index f1386b41..be10880b 100644 --- a/src/backend/common/transforms/NativeTransformer.ts +++ b/src/backend/common/transforms/NativeTransformer.ts @@ -8,7 +8,7 @@ import { parseRegexSingle, parseToRegexOrLiteralSearch } from "@foxxmd/regex-bud import { TransformerOptions } from "./AbstractTransformer.js"; import { DELIMITERS_NO_AMP } from "../infrastructure/Atomic.js"; import { asArray } from "../../utils/DataUtils.js"; -import { MaybeLogger } from "../logging.js"; +import { MaybeLogger } from '../MaybeLogger.js'; import { childLogger } from "@foxxmd/logging"; export type ArtistParseSource = 'artists' | 'title' diff --git a/src/backend/common/vendor/ListenbrainzApiClient.ts b/src/backend/common/vendor/ListenbrainzApiClient.ts index 5866d359..1ba6327a 100644 --- a/src/backend/common/vendor/ListenbrainzApiClient.ts +++ b/src/backend/common/vendor/ListenbrainzApiClient.ts @@ -12,23 +12,22 @@ import { uniqueNormalizedStrArr, } from "../../utils/StringUtils.js"; import { findDelimiters } from "../../../core/StringUtils.js"; -import { getScrobbleTsSOCDate } from "../../utils/TimeUtils.js"; import { UpstreamError } from "../errors/UpstreamError.js"; import { AbstractApiOptions, DEFAULT_RETRY_MULTIPLIER, DELIMITERS, FormatPlayObjectOptions, PagelessListensTimeRangeOptions, PagelessTimeRangeListens, PagelessTimeRangeListensResult } from "../infrastructure/Atomic.js"; import { DEFAULT_ITEMS_PER_GET_LZ, ListenBrainzClientData, MAX_ITEMS_PER_GET_LZ } from "../infrastructure/config/client/listenbrainz.js"; import AbstractApiClient from "./AbstractApiClient.js"; import { getBaseFromUrl, isPortReachableConnect, joinedUrl, normalizeWebAddress } from '../../utils/NetworkUtils.js'; -import { isEmptyArrayOrUndefined, removeUndefinedKeys, unique } from '../../utils.js'; -import { version } from '../../ioc.js'; -import { ListenPayload, ListenResponse, ListenType, MinimumTrack, SubmitListenAdditionalTrackInfo, SubmitPayload } from './listenbrainz/interfaces.js'; +import { removeUndefinedKeys, unique } from '../../utils.js'; +import { ListenPayload, ListenResponse, ListenType, SubmitPayload } from './listenbrainz/interfaces.js'; import { baseFormatPlayObj } from '../../utils/PlayTransformUtils.js'; import { ScrobbleSubmitError, SimpleError } from '../errors/MSErrors.js'; import pRetry from 'p-retry'; import { findCauseByFunc } from '../../utils/ErrorUtils.js'; import { isSuperAgentResponseError } from '../errors/ErrorUtils.js'; +import { playToSubmitPayload } from './listenbrainz/lzUtils.js'; -interface SubmitOptions { +export interface SubmitOptions { log?: boolean listenType?: ListenType } @@ -721,163 +720,3 @@ export const listenToNaivePlay = (listen: ListenResponse): PlayObject => { } -export const playToListenPayload = (play: PlayObject): ListenPayload => { - const { - data: { - playDate, - artists = [], - // MB doesn't use this during submission AFAIK - // instead it relies on (assumes??) you will submit album/release group/etc where album artist gets credit on an individual release - albumArtists = [], - album, - track, - isrc, - duration, - meta: { - brainz = {}, - spotify = {}, - } = {} - }, - meta: { - mediaPlayerName, - mediaPlayerVersion, - musicService, - source - } - } = play; - // using submit-listens exmaple from openapi https://rain0r.github.io/listenbrainz-openapi/index.html#/lbCore/submitListens - // which is documented in official docs https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#openapi-specification - // and based on this LZ developer comment https://github.com/lyarenei/jellyfin-plugin-listenbrainz/issues/10#issuecomment-1253867941 - - const msAdditionalInfo = brainz.additionalInfo ?? {}; - - let addInfo: SubmitListenAdditionalTrackInfo = { - // primary artists - artist_names: Array.from(new Set([...artists])), - // primary artist - release_artist_name: albumArtists.length === 1 ? albumArtists[0] : undefined, - release_artist_names: albumArtists.length > 0 ? albumArtists : undefined, - // use data from LZ response, if this Play was originally from LZ Source - media_player: mediaPlayerName ?? msAdditionalInfo.media_player, - media_player_version: mediaPlayerVersion ?? msAdditionalInfo.media_player_version, - music_service: musicService !== undefined ? musicServiceToCononical(musicService) : msAdditionalInfo.music_service, - music_service_name: musicService ?? source ?? msAdditionalInfo.music_service_name, - spotify_id: msAdditionalInfo.spotify_id, - spotify_album_id: msAdditionalInfo.spotify_album_id, - spotify_artist_ids: msAdditionalInfo.spotify_artist_ids, - origin_url: msAdditionalInfo.origin_url, - isrc: isrc ?? msAdditionalInfo.isrc, - tracknumber: brainz.trackNumber ?? msAdditionalInfo.tracknumber - }; - - if(Object.keys(spotify).length > 0) { - if(spotify.track !== undefined) { - const trackUrl = `https://open.spotify.com/track/${spotify.track}`; - if(addInfo.origin_url === undefined) { - addInfo.origin_url = trackUrl; - } - if(addInfo.spotify_id === undefined) { - addInfo.spotify_id = trackUrl; - } - } - if(isEmptyArrayOrUndefined(addInfo.spotify_artist_ids) && !isEmptyArrayOrUndefined(spotify.artist)) { - addInfo.spotify_artist_ids = spotify.artist.map(x => `https://open.spotify.com/artist/${x}`) - } - if(isEmptyArrayOrUndefined(addInfo.spotify_album_artist_ids) && !isEmptyArrayOrUndefined(spotify.albumArtist)) { - addInfo.spotify_album_artist_ids = spotify.albumArtist.map(x => `https://open.spotify.com/artist/${x}`) - } - if(addInfo.spotify_album_id === undefined && spotify.album !== undefined) { - addInfo.spotify_album_id = `https://open.spotify.com/album/${spotify.album}` - } - } - - addInfo = removeUndefinedKeys(addInfo) - - // possible lastfm provides an empty album field when no album data is found - let al = album; - if(al !== undefined && al !== null) { - if(al.trim() === '') { - al = undefined; - } - } - - const minTrackData = removeUndefinedKeys({ - artist_name: Array.from(new Set([...artists])).join(', '), - track_name: track, - release_name: al, - }); - - return { - listened_at: getScrobbleTsSOCDate(play).unix(), - track_metadata: { - ...minTrackData, - additional_info: { - duration: play.data.duration !== undefined ? Math.round(duration) : undefined, - track_mbid: brainz.track, - recording_mbid: brainz.recording, - artist_mbids: brainz.artist, - release_mbid: brainz.album, - release_group_mbid: brainz.releaseGroup, - submission_client: 'multi-scrobbler', - submission_client_version: version, - ...addInfo - } - } - } - } - -const musicServices = { - spotify: 'spotify.com', - bandcamp: 'bandcamp.com', - ['youtube music']: 'music.youtube.com', - youtube: 'youtube.com', - deezer: 'deezer.com', - tidal: 'tidal.com', - apple: 'music.apple.com', - archive: 'archive.org', - soundcloud: 'soundcloud.com', - jamendo: 'jamendo.com', - play: 'play.google.com' -} -/** - * Converts MS musicService to LZ cononical Music Service Name, if one exists - * @see https://listenbrainz.readthedocs.io/en/latest/users/json.html#payload-json-details - * */ -export const musicServiceToCononical = (str?: string): string | undefined => { - if(str === undefined) { - return undefined; - } - const lower = str.trim().toLocaleLowerCase(); - for(const [k, v] of Object.entries(musicServices)) { - if(lower.includes(k)) { - return v; - } - } - return undefined; -} - -/** - * Returns a known music service based on the given URL - * @see https://listenbrainz.readthedocs.io/en/latest/users/json.html#payload-json-details - * */ -export const urlToMusicService = (url?: string): string | undefined => { - if(url === undefined) { - return undefined; - } - const lower = url.trim().toLocaleLowerCase(); - for(const [k, v] of Object.entries(musicServices)) { - if(url.includes(v)) { - return k; - } - } - return undefined; -} - -export const playToSubmitPayload = (play: PlayObject, options: SubmitOptions = {}): SubmitPayload => { - const { listenType = 'single'} = options; - const listenPayload: SubmitPayload = {listen_type: listenType, payload: [playToListenPayload(play)]}; - if(listenType === 'playing_now') { - delete listenPayload.payload[0].listened_at; - } - return listenPayload; -} \ No newline at end of file diff --git a/src/backend/common/vendor/RockSkyApiClient.ts b/src/backend/common/vendor/RockSkyApiClient.ts index cc277bfd..4a816b2c 100644 --- a/src/backend/common/vendor/RockSkyApiClient.ts +++ b/src/backend/common/vendor/RockSkyApiClient.ts @@ -9,7 +9,7 @@ import AbstractApiClient from "./AbstractApiClient.js"; import { isPortReachableConnect, joinedUrl, normalizeWebAddress } from '../../utils/NetworkUtils.js'; import { unique } from '../../utils.js'; import { ListenPayload, ListenResponse, ListenType, SubmitPayload } from './listenbrainz/interfaces.js'; -import { playToListenPayload } from './ListenbrainzApiClient.js'; +import { playToListenPayload } from './listenbrainz/lzUtils.js'; import { RockskyScrobble } from './rocksky/interfaces.js'; import { Handle } from "@atcute/lexicons"; import { identifierToAtProtoHandle } from './bluesky/bsUtils.js'; diff --git a/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts b/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts index 2c992ee1..90ae04a1 100644 --- a/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts +++ b/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts @@ -5,7 +5,7 @@ import AbstractApiClient from "../AbstractApiClient.js"; import { Agent, ComAtprotoRepoCreateRecord, ComAtprotoRepoListRecords } from "@atproto/api"; import { MSCache } from "../../Cache.js"; import { BrainzMeta, PlayObject, PlayObjectLifecycleless, ScrobbleActionResult, UnixTimestamp } from "../../../../core/Atomic.js"; -import { musicServiceToCononical } from "../ListenbrainzApiClient.js"; +import { musicServiceToCononical } from '../listenbrainz/lzUtils.js'; import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; import { RecordOptions } from "../../infrastructure/config/client/tealfm.js"; import dayjs, { ManipulateType } from "dayjs"; diff --git a/src/backend/common/vendor/bluesky/bsUtils.ts b/src/backend/common/vendor/bluesky/bsUtils.ts index 30cc279c..69d77bb7 100644 --- a/src/backend/common/vendor/bluesky/bsUtils.ts +++ b/src/backend/common/vendor/bluesky/bsUtils.ts @@ -2,7 +2,7 @@ import { Handle } from "@atcute/lexicons"; import { isHandle } from "@atcute/lexicons/syntax"; import { Logger } from "@foxxmd/logging"; import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; -import { MaybeLogger } from "../../logging.js"; +import { MaybeLogger } from '../../MaybeLogger.js'; import { AtprotoDid } from "@atproto/oauth-client-node"; export const HANDLE_REGEX = new RegExp(/.+\..+/); diff --git a/src/backend/common/vendor/discord/DiscordUtils.ts b/src/backend/common/vendor/discord/DiscordUtils.ts index 9b18e353..410c68ea 100644 --- a/src/backend/common/vendor/discord/DiscordUtils.ts +++ b/src/backend/common/vendor/discord/DiscordUtils.ts @@ -3,7 +3,7 @@ import { isPlayObject, PlayObject, SourcePlayerObj } from "../../../../core/Atom import { asPlayerStateData, SourceData } from "../../infrastructure/Atomic.js"; import { GatewayActivity, GatewayOpcodes, PresenceUpdateStatus } from "discord.js"; import { capitalize } from "../../../../core/StringUtils.js"; -import { urlToMusicService } from "../ListenbrainzApiClient.js"; +import { urlToMusicService } from '../listenbrainz/lzUtils.js'; import { ACTIVITY_TYPE, ActivityData, ActivityTypes, DiscordData, DiscordStrongData, ActivityTypeString as MSActivityType, StatusType } from "../../infrastructure/config/client/discord.js"; import { parseBool, removeUndefinedKeys } from "../../../utils.js"; import { parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from "../../../utils/StringUtils.js"; diff --git a/src/backend/common/vendor/koito/KoitoApiClient.ts b/src/backend/common/vendor/koito/KoitoApiClient.ts index 21a515d2..45ba59cb 100644 --- a/src/backend/common/vendor/koito/KoitoApiClient.ts +++ b/src/backend/common/vendor/koito/KoitoApiClient.ts @@ -6,7 +6,7 @@ import AbstractApiClient from "../AbstractApiClient.js"; import { getBaseFromUrl, isPortReachableConnect, joinedUrl, normalizeWebAddress } from "../../../utils/NetworkUtils.js"; import request, { Request, Response } from 'superagent'; import { UpstreamError } from "../../errors/UpstreamError.js"; -import { playToListenPayload } from "../ListenbrainzApiClient.js"; +import { playToListenPayload } from '../listenbrainz/lzUtils.js'; import { SubmitPayload } from '../listenbrainz/interfaces.js'; import { ListenType } from '../listenbrainz/interfaces.js'; import { parseRegexSingleOrFail } from "../../../utils.js"; diff --git a/src/backend/common/vendor/listenbrainz/lzUtils.ts b/src/backend/common/vendor/listenbrainz/lzUtils.ts new file mode 100644 index 00000000..4226be1a --- /dev/null +++ b/src/backend/common/vendor/listenbrainz/lzUtils.ts @@ -0,0 +1,157 @@ +import { PlayObject } from "../../../../core/Atomic.js"; +import { isEmptyArrayOrUndefined, removeUndefinedKeys } from "../../../utils.js"; +import { getScrobbleTsSOCDate } from "../../../utils/TimeUtils.js"; +import { SubmitOptions } from "../ListenbrainzApiClient.js"; +import { ListenPayload, MinimumTrack, SubmitListenAdditionalTrackInfo, SubmitPayload } from "./interfaces.js"; +import {version as appVersion } from '../../../version.js'; + +export const playToListenPayload = (play: PlayObject, version?: string): ListenPayload => { + const { + data: { + playDate, artists = [], + // MB doesn't use this during submission AFAIK + // instead it relies on (assumes??) you will submit album/release group/etc where album artist gets credit on an individual release + albumArtists = [], album, track, isrc, duration, meta: { + brainz = {}, spotify = {}, + } = {} + }, meta: { + mediaPlayerName, mediaPlayerVersion, musicService, source + } + } = play; + // using submit-listens exmaple from openapi https://rain0r.github.io/listenbrainz-openapi/index.html#/lbCore/submitListens + // which is documented in official docs https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#openapi-specification + // and based on this LZ developer comment https://github.com/lyarenei/jellyfin-plugin-listenbrainz/issues/10#issuecomment-1253867941 + const msAdditionalInfo = brainz.additionalInfo ?? {}; + + let addInfo: SubmitListenAdditionalTrackInfo = { + // primary artists + artist_names: Array.from(new Set([...artists])), + // primary artist + release_artist_name: albumArtists.length === 1 ? albumArtists[0] : undefined, + release_artist_names: albumArtists.length > 0 ? albumArtists : undefined, + // use data from LZ response, if this Play was originally from LZ Source + media_player: mediaPlayerName ?? msAdditionalInfo.media_player, + media_player_version: mediaPlayerVersion ?? msAdditionalInfo.media_player_version, + music_service: musicService !== undefined ? musicServiceToCononical(musicService) : msAdditionalInfo.music_service, + music_service_name: musicService ?? source ?? msAdditionalInfo.music_service_name, + spotify_id: msAdditionalInfo.spotify_id, + spotify_album_id: msAdditionalInfo.spotify_album_id, + spotify_artist_ids: msAdditionalInfo.spotify_artist_ids, + origin_url: msAdditionalInfo.origin_url, + isrc: isrc ?? msAdditionalInfo.isrc, + tracknumber: brainz.trackNumber ?? msAdditionalInfo.tracknumber + }; + + if (Object.keys(spotify).length > 0) { + if (spotify.track !== undefined) { + const trackUrl = `https://open.spotify.com/track/${spotify.track}`; + if (addInfo.origin_url === undefined) { + addInfo.origin_url = trackUrl; + } + if (addInfo.spotify_id === undefined) { + addInfo.spotify_id = trackUrl; + } + } + if (isEmptyArrayOrUndefined(addInfo.spotify_artist_ids) && !isEmptyArrayOrUndefined(spotify.artist)) { + addInfo.spotify_artist_ids = spotify.artist.map(x => `https://open.spotify.com/artist/${x}`); + } + if (isEmptyArrayOrUndefined(addInfo.spotify_album_artist_ids) && !isEmptyArrayOrUndefined(spotify.albumArtist)) { + addInfo.spotify_album_artist_ids = spotify.albumArtist.map(x => `https://open.spotify.com/artist/${x}`); + } + if (addInfo.spotify_album_id === undefined && spotify.album !== undefined) { + addInfo.spotify_album_id = `https://open.spotify.com/album/${spotify.album}`; + } + } + + addInfo = removeUndefinedKeys(addInfo); + + // possible lastfm provides an empty album field when no album data is found + let al = album; + if (al !== undefined && al !== null) { + if (al.trim() === '') { + al = undefined; + } + } + + const minTrackData = removeUndefinedKeys({ + artist_name: Array.from(new Set([...artists])).join(', '), + track_name: track, + release_name: al, + }); + + return { + listened_at: getScrobbleTsSOCDate(play).unix(), + track_metadata: { + ...minTrackData, + additional_info: { + duration: play.data.duration !== undefined ? Math.round(duration) : undefined, + track_mbid: brainz.track, + recording_mbid: brainz.recording, + artist_mbids: brainz.artist, + release_mbid: brainz.album, + release_group_mbid: brainz.releaseGroup, + submission_client: 'multi-scrobbler', + submission_client_version: version ?? appVersion, + ...addInfo + } + } + }; +}; +export const musicServices = { + spotify: 'spotify.com', + bandcamp: 'bandcamp.com', + ['youtube music']: 'music.youtube.com', + youtube: 'youtube.com', + deezer: 'deezer.com', + tidal: 'tidal.com', + apple: 'music.apple.com', + archive: 'archive.org', + soundcloud: 'soundcloud.com', + jamendo: 'jamendo.com', + play: 'play.google.com' +}; +/** + * Converts MS musicService to LZ cononical Music Service Name, if one exists + * @see https://listenbrainz.readthedocs.io/en/latest/users/json.html#payload-json-details + * */ + + +export const musicServiceToCononical = (str?: string): string | undefined => { + if (str === undefined) { + return undefined; + } + const lower = str.trim().toLocaleLowerCase(); + for (const [k, v] of Object.entries(musicServices)) { + if (lower.includes(k)) { + return v; + } + } + return undefined; +}; +/** + * Returns a known music service based on the given URL + * @see https://listenbrainz.readthedocs.io/en/latest/users/json.html#payload-json-details + * */ + + +export const urlToMusicService = (url?: string): string | undefined => { + if (url === undefined) { + return undefined; + } + const lower = url.trim().toLocaleLowerCase(); + for (const [k, v] of Object.entries(musicServices)) { + if (url.includes(v)) { + return k; + } + } + return undefined; +}; +export const playToSubmitPayload = (play: PlayObject, options: SubmitOptions = {}): SubmitPayload => { + const { listenType = 'single' } = options; + const listenPayload: SubmitPayload = { listen_type: listenType, payload: [playToListenPayload(play)] }; + if (listenType === 'playing_now') { + delete listenPayload.payload[0].listened_at; + } + return listenPayload; +}; + diff --git a/src/backend/common/vendor/musicbrainz/MusicbrainzApiClient.ts b/src/backend/common/vendor/musicbrainz/MusicbrainzApiClient.ts index 133ef999..e7bbde71 100644 --- a/src/backend/common/vendor/musicbrainz/MusicbrainzApiClient.ts +++ b/src/backend/common/vendor/musicbrainz/MusicbrainzApiClient.ts @@ -8,7 +8,8 @@ import { MusicBrainzApi, IRecording, IRecordingList, IRelease } from 'musicbrain import { difference, isDebugMode, isEmptyArrayOrUndefined, sleep } from "../../../utils.js"; import {SequentialRoundRobin} from 'round-robin-js'; import { Cacheable } from "cacheable"; -import { getRoot, version } from "../../../ioc.js"; +import { getRoot } from "../../../ioc.js"; +import { version } from "../../../version.js"; import { hashObject } from "../../../utils/StringUtils.js"; import { playContentInvariantTransform } from "../../../utils/PlayComparisonUtils.js"; import { childLogger } from "@foxxmd/logging";; diff --git a/src/backend/index.ts b/src/backend/index.ts index 443f1738..f3759c31 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -13,7 +13,8 @@ import { SimpleIntervalJob, ToadScheduler } from "toad-scheduler"; import { projectDir } from "./common/index.js"; import { AIOConfig } from "./common/infrastructure/config/aioConfig.js"; import { appLogger, initLogger as getInitLogger } from "./common/logging.js"; -import { getRoot, parseVersion } from "./ioc.js"; +import { getRoot } from "./ioc.js"; +import { parseVersion } from "./version.js"; import { initServer } from "./server/index.js"; import { createHeartbeatClientsTask } from "./tasks/heartbeatClients.js"; import { createHeartbeatSourcesTask } from "./tasks/heartbeatSources.js"; diff --git a/src/backend/ioc.ts b/src/backend/ioc.ts index efaa5616..1cd037c5 100644 --- a/src/backend/ioc.ts +++ b/src/backend/ioc.ts @@ -1,4 +1,3 @@ -import { getVersion } from "@foxxmd/get-version"; import { Logger, loggerDebug, LogOptions } from "@foxxmd/logging"; import { EventEmitter } from "events"; import { createContainer } from "iti"; @@ -14,16 +13,9 @@ import TransformerManager from "./common/transforms/TransformerManager.js"; import { TransformerCommonConfig } from "../core/Atomic.js"; import prom, { Counter, Gauge } from 'prom-client'; import { CoverArtApiClient } from "./common/vendor/musicbrainz/CoverArtApiClient.js"; - -export let version: string = 'unknown'; - -export const parseVersion = async () => { - version = await getVersion({priority: ['env', 'git', 'file']}); - return version; -} +import { version } from "./version.js"; let root: ReturnType; - export interface RootOptions { baseUrl?: string, port?: number diff --git a/src/backend/scrobblers/KoitoScrobbler.ts b/src/backend/scrobblers/KoitoScrobbler.ts index c055b191..1e889154 100644 --- a/src/backend/scrobblers/KoitoScrobbler.ts +++ b/src/backend/scrobblers/KoitoScrobbler.ts @@ -3,9 +3,8 @@ import EventEmitter from "events"; import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js"; import { buildTrackString, capitalize } from "../../core/StringUtils.js"; import { isNodeNetworkException } from "../common/errors/NodeErrors.js"; -import { UpstreamError } from "../common/errors/UpstreamError.js"; import { FormatPlayObjectOptions, TimeRangeListensFetcher } from "../common/infrastructure/Atomic.js"; -import { playToListenPayload } from "../common/vendor/ListenbrainzApiClient.js"; +import { playToListenPayload } from '../common/vendor/listenbrainz/lzUtils.js'; import { Notifiers } from "../notifier/Notifiers.js"; import AbstractScrobbleClient from "./AbstractScrobbleClient.js"; @@ -13,7 +12,6 @@ import { isDebugMode } from "../utils.js"; import { KoitoClientConfig } from "../common/infrastructure/config/client/koito.js"; import { KoitoApiClient, listenObjectResponseToPlay } from "../common/vendor/koito/KoitoApiClient.js"; import { createGetScrobblesForTimeRangeFunc } from "../utils/ListenFetchUtils.js"; -import dayjs from "dayjs"; export default class KoitoScrobbler extends AbstractScrobbleClient { diff --git a/src/backend/scrobblers/ListenbrainzScrobbler.ts b/src/backend/scrobblers/ListenbrainzScrobbler.ts index 011cc192..98e2502e 100644 --- a/src/backend/scrobblers/ListenbrainzScrobbler.ts +++ b/src/backend/scrobblers/ListenbrainzScrobbler.ts @@ -7,7 +7,8 @@ import { isNodeNetworkException } from "../common/errors/NodeErrors.js"; import { hasUpstreamError, UpstreamError } from "../common/errors/UpstreamError.js"; import { FormatPlayObjectOptions, TimeRangeListensFetcher } from "../common/infrastructure/Atomic.js"; import { DEFAULT_MS_ITEMS_PER_GET_LZ, ListenBrainzClientConfig } from "../common/infrastructure/config/client/listenbrainz.js"; -import { ListenbrainzApiClient, playToListenPayload, playToSubmitPayload } from "../common/vendor/ListenbrainzApiClient.js"; +import { ListenbrainzApiClient } from "../common/vendor/ListenbrainzApiClient.js"; +import { playToListenPayload } from '../common/vendor/listenbrainz/lzUtils.js'; import { ListenPayload } from '../common/vendor/listenbrainz/interfaces.js'; import { Notifiers } from "../notifier/Notifiers.js"; diff --git a/src/backend/scrobblers/RockskyScrobbler.ts b/src/backend/scrobblers/RockskyScrobbler.ts index 26b2ff82..a8c2df23 100644 --- a/src/backend/scrobblers/RockskyScrobbler.ts +++ b/src/backend/scrobblers/RockskyScrobbler.ts @@ -3,10 +3,9 @@ import EventEmitter from "events"; import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js"; import { buildTrackString, capitalize } from "../../core/StringUtils.js"; import { isNodeNetworkException } from "../common/errors/NodeErrors.js"; -import { hasUpstreamError, UpstreamError } from "../common/errors/UpstreamError.js"; import { FormatPlayObjectOptions } from "../common/infrastructure/Atomic.js"; -import { ListenBrainzClientConfig } from "../common/infrastructure/config/client/listenbrainz.js"; -import { ListenbrainzApiClient, playToListenPayload } from "../common/vendor/ListenbrainzApiClient.js"; +import { ListenbrainzApiClient } from "../common/vendor/ListenbrainzApiClient.js"; +import { playToListenPayload } from '../common/vendor/listenbrainz/lzUtils.js'; import { ListenPayload } from '../common/vendor/listenbrainz/interfaces.js'; import { Notifiers } from "../notifier/Notifiers.js"; diff --git a/src/backend/scrobblers/TealfmScrobbler.ts b/src/backend/scrobblers/TealfmScrobbler.ts index 72b0d60f..c456f9d3 100644 --- a/src/backend/scrobblers/TealfmScrobbler.ts +++ b/src/backend/scrobblers/TealfmScrobbler.ts @@ -4,7 +4,7 @@ import { PlayObject } from "../../core/Atomic.js"; import { buildTrackString, capitalize } from "../../core/StringUtils.js"; import { isNodeNetworkException } from "../common/errors/NodeErrors.js"; import { FormatPlayObjectOptions } from "../common/infrastructure/Atomic.js"; -import { playToListenPayload } from "../common/vendor/ListenbrainzApiClient.js"; +import { playToListenPayload } from '../common/vendor/listenbrainz/lzUtils.js'; import { Notifiers } from "../notifier/Notifiers.js"; import AbstractScrobbleClient from "./AbstractScrobbleClient.js"; diff --git a/src/backend/sources/ChromecastSource.ts b/src/backend/sources/ChromecastSource.ts index 6486774a..d608af37 100644 --- a/src/backend/sources/ChromecastSource.ts +++ b/src/backend/sources/ChromecastSource.ts @@ -17,7 +17,7 @@ import { SourceData, } from "../common/infrastructure/Atomic.js"; import { ChromecastSourceConfig } from "../common/infrastructure/config/source/chromecast.js"; -import { MaybeLogger } from "../common/logging.js"; +import { MaybeLogger } from '../common/MaybeLogger.js'; import { chromePlayerStateToReported, genDeviceId, diff --git a/src/backend/sources/PlayerState/RealtimePlayer.ts b/src/backend/sources/PlayerState/RealtimePlayer.ts index 61eebd07..1a6ec9af 100644 --- a/src/backend/sources/PlayerState/RealtimePlayer.ts +++ b/src/backend/sources/PlayerState/RealtimePlayer.ts @@ -1,4 +1,3 @@ -import { childLogger, Logger } from "@foxxmd/logging"; import dayjs, { Dayjs } from "dayjs"; import { SimpleIntervalJob, Task, ToadScheduler } from "toad-scheduler"; diff --git a/src/backend/tests/listenbrainz/listenbrainz.test.ts b/src/backend/tests/listenbrainz/listenbrainz.test.ts index 2d9a7f69..88155d7c 100644 --- a/src/backend/tests/listenbrainz/listenbrainz.test.ts +++ b/src/backend/tests/listenbrainz/listenbrainz.test.ts @@ -6,7 +6,8 @@ import { http, HttpResponse } from "msw"; import { PlayObject } from "../../../core/Atomic.js"; import { UpstreamError } from "../../common/errors/UpstreamError.js"; -import { ListenbrainzApiClient, playToListenPayload, listenResponseToPlay, listenPayloadToPlay } from "../../common/vendor/ListenbrainzApiClient.js"; +import { ListenbrainzApiClient, listenResponseToPlay, listenPayloadToPlay } from "../../common/vendor/ListenbrainzApiClient.js"; +import { playToListenPayload } from '../../common/vendor/listenbrainz/lzUtils.js'; import { ListenPayload, ListenResponse, SubmitPayload } from '../../common/vendor/listenbrainz/interfaces.js'; import { ExpectedResults } from "../utils/interfaces.js"; import { withRequestInterception } from "../utils/networking.js"; diff --git a/src/backend/tests/utils/playComparisons.test.ts b/src/backend/tests/utils/playComparisons.test.ts index a23a8de0..57a4dd0c 100644 --- a/src/backend/tests/utils/playComparisons.test.ts +++ b/src/backend/tests/utils/playComparisons.test.ts @@ -1,4 +1,3 @@ -import { loggerTest } from "@foxxmd/logging"; import { assert, expect } from 'chai'; import clone from "clone"; import { describe, it } from 'mocha'; @@ -6,8 +5,6 @@ import { genericSourcePlayMatch, playsAreAddedOnly, playsAreBumpedOnly, playsAre import { generatePlay, generatePlays } from "../../../core/PlayTestUtils.js"; import { PlayObject } from "../../../core/Atomic.js"; -const logger = loggerTest; - const newPlay = generatePlay(); const existingList = generatePlays(10); diff --git a/src/backend/utils/AsyncUtils.ts b/src/backend/utils/AsyncUtils.ts index 1ac79c6b..2455d037 100644 --- a/src/backend/utils/AsyncUtils.ts +++ b/src/backend/utils/AsyncUtils.ts @@ -1,4 +1,3 @@ -import { randomInt } from "crypto"; import pMap, { Mapper, Options, pMapIterable } from "p-map"; import { sleep } from "../utils.js"; @@ -61,7 +60,7 @@ export function staggerMapper(options: { maxRandomStagger?: sleep(initialStagger); initialStagger += initialInterval; } else { - const s = randomInt(maxRandomStagger) + const s = Math.max((Math.random() * 1000), maxRandomStagger) await sleep(s); } return await mapper(x, index); diff --git a/src/backend/utils/DataUtils.ts b/src/backend/utils/DataUtils.ts index 0e8cc66c..57f2a9eb 100644 --- a/src/backend/utils/DataUtils.ts +++ b/src/backend/utils/DataUtils.ts @@ -1,6 +1,6 @@ import JSON5 from "json5"; import { constants, promises } from "fs"; -import { MaybeLogger } from "../common/logging.js"; +import { MaybeLogger } from '../common/MaybeLogger.js'; export const asArray = (data: T | T[]): T[] => { if (Array.isArray(data)) { diff --git a/src/backend/utils/ListenFetchUtils.ts b/src/backend/utils/ListenFetchUtils.ts index 844a6c2d..435c3f3f 100644 --- a/src/backend/utils/ListenFetchUtils.ts +++ b/src/backend/utils/ListenFetchUtils.ts @@ -1,9 +1,10 @@ -import { childLogger, Logger, loggerTest } from "@foxxmd/logging"; +import { childLogger, Logger } from "@foxxmd/logging"; import dayjs, { Dayjs } from "dayjs"; import { Duration } from "dayjs/plugin/duration.js"; import { PlayObject, UnixTimestamp } from "../../core/Atomic.js"; import { CursorType, hasPagelessTimeRangeListens, hasPaginatedTimeRangeListens, PagelessListensTimeRangeOptions, PagelessTimeRangeListens, PagelessTimeRangeListensResult, PaginatedListensTimeRangeOptions, PaginatedTimeRangeCommonOptions, PaginatedTimeRangeListens, PaginatedTimeRangeListensResult, PaginatedTimeRangeOptions, PaginatedTimeRangeSource, REFRESH_STALE_DEFAULT, TimeRangeListensFetcher } from "../common/infrastructure/Atomic.js"; -import { loggerNoop, MaybeLogger } from "../common/logging.js"; +import { loggerNoop } from '../common/MaybeLogger.js'; +import { MaybeLogger } from '../common/MaybeLogger.js'; import { sortByOldestPlayDate } from "../utils.js"; import { sortByNewestPlayDate } from '../../core/PlayUtils.js'; import { todayAwareFormat } from "../../core/TimeUtils.js"; diff --git a/src/backend/utils/MDNSUtils.ts b/src/backend/utils/MDNSUtils.ts index a7d76e0e..ae2c0141 100644 --- a/src/backend/utils/MDNSUtils.ts +++ b/src/backend/utils/MDNSUtils.ts @@ -2,7 +2,7 @@ import { Browser, Service, ServiceType } from "@astronautlabs/mdns"; import { Logger } from "@foxxmd/logging"; import AvahiBrowser from 'avahi-browse'; import { MdnsDeviceInfo } from "../common/infrastructure/Atomic.js"; -import { MaybeLogger } from "../common/logging.js"; +import { MaybeLogger } from '../common/MaybeLogger.js'; import { sleep } from "../utils.js"; import { debounce, DebouncedFunction } from "./debounce.js"; diff --git a/src/backend/utils/PlayComparisonUtils.ts b/src/backend/utils/PlayComparisonUtils.ts index a2c283c7..e2d29b34 100644 --- a/src/backend/utils/PlayComparisonUtils.ts +++ b/src/backend/utils/PlayComparisonUtils.ts @@ -9,7 +9,7 @@ import { StringSamenessResult } from "@foxxmd/string-sameness"; import { Duration } from "dayjs/plugin/duration.js"; import { PlayTransformRules, TRANSFORM_HOOK, TransformHook } from "../common/infrastructure/Transform.js"; import { Logger } from "@foxxmd/logging"; -import { loggerNoop } from "../common/logging.js"; +import { loggerNoop } from '../common/MaybeLogger.js'; import { lifecyclelessInvariantTransform } from "../../core/PlayUtils.js"; import { findAsyncSequential } from "./AsyncUtils.js"; diff --git a/src/backend/utils/PlayTransformUtils.ts b/src/backend/utils/PlayTransformUtils.ts index 0a48a6a7..709a6cfb 100644 --- a/src/backend/utils/PlayTransformUtils.ts +++ b/src/backend/utils/PlayTransformUtils.ts @@ -1,4 +1,4 @@ -import { Logger, loggerTest } from "@foxxmd/logging"; +import { Logger } from "@foxxmd/logging"; import { searchAndReplace as searchAndReplaceFunc, testMaybeRegex as testMaybeRegexFunc } from "@foxxmd/regex-buddy-core"; import { PlayLifecycle, PlayObject, PlayObjectLifecycleless } from "../../core/Atomic.js"; diff --git a/src/backend/utils/RequestUtils.ts b/src/backend/utils/RequestUtils.ts index 0dbe5bab..0bb05147 100644 --- a/src/backend/utils/RequestUtils.ts +++ b/src/backend/utils/RequestUtils.ts @@ -7,7 +7,7 @@ import request, { Request, Response } from 'superagent'; import pRetry, { RetryContext, Options } from 'p-retry'; import { DEFAULT_RETRY_MULTIPLIER } from "../common/infrastructure/Atomic.js"; import { SimpleError } from "../common/errors/MSErrors.js"; -import { loggerNoop } from "../common/logging.js"; +import { loggerNoop } from '../common/MaybeLogger.js'; import { findCauseByFunc } from "./ErrorUtils.js"; import { isSuperAgentResponseError } from "../common/errors/ErrorUtils.js"; import { isNodeNetworkException, NodeNetworkException } from "../common/errors/NodeErrors.js"; diff --git a/src/backend/utils/SchemaCompiledUtils.ts b/src/backend/utils/SchemaCompiledUtils.ts index a9548507..3fac6a69 100644 --- a/src/backend/utils/SchemaCompiledUtils.ts +++ b/src/backend/utils/SchemaCompiledUtils.ts @@ -2,7 +2,7 @@ import { readFileSync, accessSync } from "node:fs"; import { constants, promises } from "fs"; import { resolve } from "path"; import { projectDir } from "../common/index.js"; -import { MaybeLogger } from "../common/logging.js"; +import { MaybeLogger } from '../common/MaybeLogger.js'; export const getSchemaForType = (type: string, logger: MaybeLogger = new MaybeLogger()): any => { const path = resolve(projectDir, `src/backend/common/schema/${type}.json`); diff --git a/src/backend/utils/SchemaUtils.ts b/src/backend/utils/SchemaUtils.ts index 51be42eb..a10905d2 100644 --- a/src/backend/utils/SchemaUtils.ts +++ b/src/backend/utils/SchemaUtils.ts @@ -9,7 +9,7 @@ import { createParser, SchemaGenerator, LiteralType, LiteralTypeFormatter } from "ts-json-schema-generator"; -import { MaybeLogger } from "../common/logging.js"; +import { MaybeLogger } from '../common/MaybeLogger.js'; // https://github.com/vega/ts-json-schema-generator/issues/1899#issuecomment-2407674526 // https://github.com/vega/ts-json-schema-generator?tab=readme-ov-file#custom-formatting diff --git a/src/backend/version.ts b/src/backend/version.ts new file mode 100644 index 00000000..aec441e9 --- /dev/null +++ b/src/backend/version.ts @@ -0,0 +1,9 @@ +import { getVersion } from "@foxxmd/get-version"; + +export let version: string = 'unknown'; + +export const parseVersion = async () => { + version = await getVersion({ priority: ['env', 'git', 'file'] }); + return version; +}; + diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx index 920b016b..ab626a66 100644 --- a/src/client/components/ActivityDetail.tsx +++ b/src/client/components/ActivityDetail.tsx @@ -28,6 +28,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => { } } = props; + debugger; const [collapsibleOpen, setCollapsibleOpen] = useState(undefined); return ( diff --git a/src/client/components/PlayData.tsx b/src/client/components/PlayData.tsx index 6b6d765c..f016be9e 100644 --- a/src/client/components/PlayData.tsx +++ b/src/client/components/PlayData.tsx @@ -1,7 +1,7 @@ import React, { Fragment, useMemo, useState } from 'react'; import { EmptyState, DataList, HStack, Tag, Tabs, Wrap, Box, Flex, SegmentGroup, Stack, Text, Separator, IconButton, Container, SimpleGrid, Float } from "@chakra-ui/react" import { LuCode, LuText } from "react-icons/lu" -import { JsonPlayObject } from '../../core/Atomic.js'; +import { JsonPlayObject, PlayObjectLifecycleless } from '../../core/Atomic.js'; import { shortTodayAwareFormat } from '../../core/TimeUtils.js'; import dayjs from 'dayjs'; import { ChakraCodeBlock } from './CodeBlock.js'; @@ -23,7 +23,7 @@ const EmptyPlayData = () => { export type DisplayDates = false | 'all' | 'played' | 'seen'; export interface PlayInfoProps { - play?: JsonPlayObject + play?: JsonPlayObject | PlayObjectLifecycleless final?: JsonPlayObject showCodeToggle?: boolean showCompare?: boolean diff --git a/src/client/components/ScrobbleMatchResult.tsx b/src/client/components/ScrobbleMatchResult.tsx index fb403255..29a8fcc8 100644 --- a/src/client/components/ScrobbleMatchResult.tsx +++ b/src/client/components/ScrobbleMatchResult.tsx @@ -11,7 +11,7 @@ import { JsonDiffPatch } from "./JsonDiff"; import { formatNumber, jdiff } from "../../core/DataUtils"; export interface ScrobbleMatchResultProps { - match: PlayMatchResult + match: PlayMatchResult } export const ScrobbleMatchResult = (props: ScrobbleMatchResultProps) => { diff --git a/src/client/components/TransformSteps.tsx b/src/client/components/TransformSteps.tsx index c34d4402..03c6cb32 100644 --- a/src/client/components/TransformSteps.tsx +++ b/src/client/components/TransformSteps.tsx @@ -23,6 +23,11 @@ export const TransformSteps = (props: LifeycleStepsTimelineProps) => { } = props; let currentPlay: JsonPlayObject | false = JSON.parse(JSON.stringify(original)); + if(currentPlay !== false) { + currentPlay.data.meta = { + ...(currentPlay.data.meta ?? {}), + } + } return ( diff --git a/src/core/tests/utils/fixtures.ts b/src/core/tests/utils/fixtures.ts index e6b73409..4f781b2e 100644 --- a/src/core/tests/utils/fixtures.ts +++ b/src/core/tests/utils/fixtures.ts @@ -10,8 +10,8 @@ import { lifecyclelessInvariantTransform } from '../../PlayUtils.js'; import clone from 'clone'; import { jdiff } from '../../DataUtils.js'; import { existingScrobble } from '../../../backend/utils/PlayComparisonUtils.js'; -import { playToListenPayload } from '../../../backend/common/vendor/ListenbrainzApiClient.js'; import { UpstreamError } from '../../../backend/common/errors/UpstreamError.js'; +import { playToListenPayload } from '../../../backend/common/vendor/listenbrainz/lzUtils.js'; interface BlockPath { key: string, parent: string }; type BlockPaths = BlockPath[]; @@ -204,7 +204,7 @@ export const generateLifecycleStep = (play: PlayObject, opts: GenerateLifecycleO const { name = ['preCompare', 'postCompare'][faker.number.int({ min: 0, max: 1 })], - source = `${play.meta.source}-${faker.word.noun()}`, + source = `${play.meta?.source}-${faker.word.noun()}`, equal = faker.datatype.boolean(0.1), inputCount } = opts; @@ -235,6 +235,9 @@ export const generateLifecycleStep = (play: PlayObject, opts: GenerateLifecycleO new Traverse(modifiedPlay).forEach((ctx, x) => { if (modifiableKeys.includes(ctx.key)) { if (faker.datatype.boolean(0.3)) { + if(ctx.key === 'meta' && (ctx.parent === undefined || ctx.parent.key !== 'data')) { + return; + } somethingModified = true; if(ctx.key === 'brainz' && Object.keys(x).length === 0) { ctx.update(generateBrainz(play, {include: ['album', 'artist', 'track']}), true); diff --git a/src/stories/List.stories.tsx b/src/stories/List.stories.tsx index 15447798..1440a66f 100644 --- a/src/stories/List.stories.tsx +++ b/src/stories/List.stories.tsx @@ -8,6 +8,7 @@ import {Provider} from "../client/components/Provider"; import { generateJsonPlays } from "../core/PlayTestUtils.js"; import { ErrorLike, JsonPlayObject } from "../core/Atomic.js"; import {examplePlay, lastfmErrorExample} from './storyUtils.js'; +import {playWithLifecycleScrobble, generatePlayWithLifecycle} from '../core/tests/utils/fixtures' const stack = "Scrobble Submit Error: Failed to submit to Listenbrainz (listen_type single)\n at ListenbrainzApiClient.submitListen (/app/src/backend/common/vendor/ListenbrainzApiClient.ts:246:19)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async ListenbrainzScrobbler.doScrobble (/app/src/backend/scrobblers/ListenbrainzScrobbler.ts:87:28)\n at async ListenbrainzScrobbler.scrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:679:28)\n at async ListenbrainzScrobbler.processDeadLetterScrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:920:39)\n at async ListenbrainzScrobbler.processDeadLetterQueue (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:894:43)\n at async PromisePoolExecutor.handler (/app/src/backend/tasks/heartbeatClients.ts:35:21)\n at async PromisePoolExecutor.waitForActiveTaskToFinish (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:375:9)\n at async PromisePoolExecutor.waitForProcessingSlot (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:368:13)\n at async PromisePoolExecutor.process (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:354:13)"; @@ -39,11 +40,12 @@ const meta = preview.meta({ // More on argTypes: https://storybook.js.org/docs/api/argtypes args: { data:[ - ...generateJsonPlays(2).map((x) => ({play: x, status: 'queued'})), - {play: examplePlay(), status: 'scrobbled'}, - {play: lastfmErrorExample(), status: 'error'} - ] , + // ...generateJsonPlays(2).map((x) => ({play: x, status: 'queued'})), + // {play: examplePlay(), status: 'scrobbled'}, + // {play: lastfmErrorExample(), status: 'error'} + ] , }, + render: function Render(args, { loaded: { data } }) { return () }, decorators: [ (Story) => (), ] @@ -52,5 +54,17 @@ decorators: [ // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const List = meta.story({ + loaders: [ + async () => { + const queued = await generatePlayWithLifecycle(); + const scrobbled = await playWithLifecycleScrobble(generatePlayWithLifecycle()); + const scrobbleError = await playWithLifecycleScrobble(generatePlayWithLifecycle(), {error: true}); + return {data: [ + {play: queued, status: 'queued'}, + {play: scrobbled, status: 'scrobbled'}, + {play: scrobbleError, status: 'error'} + ]}; + } + ], //render: function Render(args) { return () } }); \ No newline at end of file