[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Add step and flow result to lifecycle

FoxxMD (Mar 23, 2026, 6:32 PM UTC) 4b27671a 5f4943e0

+62 -44
+57 -44
src/backend/common/AbstractComponent.ts
··· 4 4 } from "@foxxmd/regex-buddy-core"; 5 5 import deepEqual from 'fast-deep-equal'; 6 6 import { Simulate } from "react-dom/test-utils"; 7 - import { PlayData, PlayObject, TransformResult } from "../../core/Atomic.js"; 7 + import { LifecycleStep, PlayData, PlayObject, TransformResult } from "../../core/Atomic.js"; 8 8 import { buildPlayHumanDiffable, buildTrackString } from "../../core/StringUtils.js"; 9 9 import { CommonClientConfig } from "./infrastructure/config/client/index.js"; 10 10 import { CommonSourceConfig } from "./infrastructure/config/source/index.js"; ··· 29 29 import { MSCache } from "./Cache.js"; 30 30 import { jdiff } from "../../core/DataUtils.js"; 31 31 import ConsoleFormatter from "jsondiffpatch/formatters/console"; 32 + import clone from "clone"; 32 33 33 34 const console = new ConsoleFormatter(); 34 35 ··· 211 212 logger.debug(`Transform start for => ${buildTrackString(play)}`); 212 213 let transformedPlay: PlayObject = play; 213 214 let transformHistory: TransformResult[] = []; 215 + let shouldBreak = false; 214 216 for(const hookItem of hook) { 215 217 216 218 const { ··· 219 221 onSkip = 'continue', 220 222 failureReturnPartial = false 221 223 } = hookItem; 224 + 225 + const existingStepIndex = transformedPlay.meta.lifecycle.steps.findIndex(x => x.name === 'hookType' && x.source === this.getIdentifier()); 226 + const step: LifecycleStep = existingStepIndex !== -1 ? transformedPlay.meta.lifecycle.steps[existingStepIndex] : { 227 + name: hookType, 228 + source: this.getIdentifier(), 229 + } 222 230 223 231 let newTransformedPlay: PlayObject, 224 232 stageName: string = 'Unnamed', ··· 235 243 236 244 if(err !== undefined) { 237 245 const merged = mergeSimpleError(err); 246 + step.error = merged; 247 + 238 248 const skipError = findCauseByReference(err, SkipTransformStageError); 239 249 if(skipError !== undefined) { 240 250 let skipMsg = `Stage '${stageName}' was skipped`; 251 + step.flowResult = onSkip; 252 + step.flowKnownState = 'skip'; 253 + 241 254 if(onSkip === 'stop') { 242 255 skipMsg += ' and will stop transform due to onSkip: stop'; 243 256 } 257 + step.flowReason = skipMsg; 258 + 244 259 logger.debug(merged, skipMsg); 245 260 if(onSkip === 'stop') { 246 - break; 261 + shouldBreak = true; 247 262 } 248 263 } else if(onFailure === 'continue') { 249 - logger.warn(merged, 'A transform encounte[red an error but continuing due to onFailure: continue'); 264 + logger.warn(merged, 'Transform encountered an error but continuing due to onFailure: continue'); 265 + step.flowReason = 'Transform encountered an error but continuing due to onFailure: continue'; 266 + step.flowResult = onFailure; 250 267 } else { 268 + step.flowResult = onFailure; 251 269 const reqError = findCauseByReference(err, StagePrerequisiteError); 252 270 if(reqError !== undefined) { 253 - logger.warn(merged, 'Transform could not be completed due to prerequisite failure'); 271 + const prereqMsg = 'Transform could not be completed due to prerequisite failure'; 272 + logger.warn(merged, prereqMsg); 273 + step.flowKnownState = 'prereq'; 274 + step.flowReason = prereqMsg; 254 275 } else { 255 276 logger.error(merged, 'Transform encountered an error'); 256 277 cacheOk = false; 278 + step.flowReason = 'Transform encountered an error'; 257 279 } 258 280 259 281 if(!failureReturnPartial) { 260 282 // rewind to original play so we don't return partial transform 261 283 transformedPlay = play; 262 284 transformHistory = []; 285 + } else if(transformHistory.length > 0) { 286 + step.flowReason += ' | Preserving play transformations up to this point due to failureReturnPartial=true' 263 287 } 264 - break; 288 + shouldBreak = true; 265 289 } 266 290 } else { 291 + step.flowResult = onSuccess; 292 + 293 + if(!deepEqual(play, newTransformedPlay)) { 294 + const o = JSON.parse(JSON.stringify(play)); 295 + const t = JSON.parse(JSON.stringify(newTransformedPlay)); 296 + const patch = jdiff.diff(o,t); 297 + step.patch = patch; 298 + } 299 + 300 + if(newTransformedPlay.meta.lifecycleInputs?.length > 0) { 301 + step.inputs = clone(newTransformedPlay.meta.lifecycleInputs) 302 + } else if(transformedPlay.meta.lifecycleInputs?.length > 0) { 303 + step.inputs = clone(transformedPlay.meta.lifecycleInputs) 304 + } 305 + 306 + delete transformedPlay.meta.lifecycleInputs; 307 + 308 + // for logging only 267 309 transformHistory.push({ 268 310 type: hookItem.type, 269 311 name: stageName, ··· 274 316 275 317 if(err === undefined && onSuccess === 'stop') { 276 318 logger.debug(`Stopping transform due to onSuccess: stop`); 277 - break; 319 + shouldBreak = true; 278 320 } 279 - } 280 321 281 - const orig = play.data; 282 - 283 - // if(transformedPlay.meta.transforms === undefined) { 284 - // transformedPlay.meta.transforms = { 285 - // original: play.data 286 - // }; 287 - // } 322 + if(existingStepIndex !== -1) { 323 + transformedPlay.meta.lifecycle.steps[existingStepIndex] = step; 324 + } else { 325 + transformedPlay.meta.lifecycle.steps.push(step); 326 + } 288 327 289 - const o = JSON.parse(JSON.stringify(play)); 290 - const t = JSON.parse(JSON.stringify(transformedPlay)); 291 - const patch = jdiff.diff(o,t); 328 + if(shouldBreak) { 329 + break; 330 + } 331 + } 292 332 293 333 if(shouldLog !== false) { 294 334 if(transformHistory.length === 0) { ··· 324 364 } 325 365 } 326 366 327 - const existingStepIndex = transformedPlay.meta.lifecycle.steps.findIndex(x => x.name === 'hookType' && x.source === this.getIdentifier()); 328 - if(existingStepIndex !== -1) { 329 - transformedPlay.meta.lifecycle.steps[existingStepIndex].patch = patch; 330 - transformedPlay.meta.lifecycle.steps[existingStepIndex].inputs = transformedPlay.meta.lifecycleInputs; 331 - } else { 332 - transformedPlay.meta.lifecycle.steps.push({ 333 - name: hookType, 334 - source: this.getIdentifier(), 335 - patch: patch, 336 - inputs: transformedPlay.meta.lifecycleInputs 337 - }); 338 - } 339 - delete transformedPlay.meta.lifecycleInputs; 340 - 341 - // transformedPlay.meta.lifecycle = { 342 - // ...(transformedPlay.meta.lifecycle ?? { 343 - // original: play 344 - // }), 345 - // steps: [ 346 - // ...(transformedPlay.meta.lifecycle?.steps ?? []), 347 - // { 348 - // name: hookType, 349 - // source: this.getIdentifier(), 350 - // patch: patch 351 - // } 352 - // ] 353 - // } 354 367 await this.cache.cacheTransform.set<PlayObject>(transformHash, transformedPlay, '10m'); 355 368 return transformedPlay; 356 369 } catch (e) {
+5
src/core/Atomic.ts
··· 6 6 import { MarkOptional } from "ts-essentials"; 7 7 import { ErrorObject } from "serialize-error"; 8 8 import { PlayPlatformIdStr } from "../backend/common/infrastructure/Atomic.js"; 9 + import { FlowControlTerm } from "../backend/common/infrastructure/Transform.js"; 9 10 10 11 export interface SourceStatusData { 11 12 status: string; ··· 307 308 export interface LifecycleStep { 308 309 name: string 309 310 source: string 311 + flowResult?: FlowControlTerm 312 + flowReason?: string 313 + flowKnownState?: 'skip' | 'prereq' 314 + error?: ErrorLike 310 315 patch?: Delta 311 316 inputs?: LifecycleInput[] 312 317 }