[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.

fix(ui): Fix total stats count

FoxxMD (Jun 24, 2026, 4:12 PM UTC) a603be6e 7ad23a76

+26 -10
+1 -2
src/backend/common/AbstractComponent.ts
··· 515 515 516 516 public abstract getRunningState(): ComponentState 517 517 518 - public getApiData(): Omit<ComponentMinimalSelect, 'type'> & Pick<ComponentCommonApi, 'state'> { 518 + public getApiData(): Omit<ComponentMinimalSelect, 'type' | 'countLive'> & Pick<ComponentCommonApi, 'state'> { 519 519 let state: ComponentState; 520 520 if(!this.initializedOnce || this.initializing) { 521 521 state = COMPONENT_STATE.INITIALIZING; ··· 530 530 name: this.dbComponent.name, 531 531 state, 532 532 mode: this.dbComponent.mode, 533 - countLive: this.dbComponent.countLive, 534 533 countNonLive: this.dbComponent.countNonLive, 535 534 createdAt: this.dbComponent.createdAt, 536 535 lastReadyAt: this.dbComponent.lastReadyAt,
+3 -3
src/backend/common/database/drizzle/repositories/PlayRepository.ts
··· 3 3 import clone from 'clone'; 4 4 import { Traverse, TraverseContext } from 'neotraverse/modern'; 5 5 import { loggerNoop } from "../../../MaybeLogger.js"; 6 - import { DateLike, DeepReplaceValue, ErrorLike, PlayObject, REGEX_ISO8601_LOOSE, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js"; 6 + import { DateLike, DeepReplaceValue, ErrorLike, PlayObject, PlayState, REGEX_ISO8601_LOOSE, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js"; 7 7 import { generateInputEntity, generatePlayEntity, PlayEntityOpts, hydratePlaySelect, PlayHydateOptions } from "../entityUtils.js"; 8 8 import { playInputs, plays, queueStates, relations } from "../schema/schema.js"; 9 9 import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, QueueStateSelect, FindWith, PlaySelectWithQueueStates, WhereClause, PlayWith } from "../drizzleTypes.js";; ··· 704 704 })) as PlayWith<'queueStates'>[]).map(x => ({...x, play: hydratePlaySelect(x)})); 705 705 } 706 706 707 - public getComponentPlayCountByState = async (componentId?: string) => { 707 + public getComponentPlayCountByState = async (componentId?: number): Promise<{state: PlayState, 'count(*)': number}[]> => { 708 708 709 709 const res = await this.db.all(sql`select state, count(*) from plays p 710 710 where componentId = ${componentId ?? this.componentId} 711 711 group by state;`); 712 - return res; 712 + return res as {state: PlayState, 'count(*)': number}[]; 713 713 } 714 714 715 715 public getPlayCountByState = async () => {
+9 -1
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 104 104 preloadScrobbles: boolean = true; 105 105 scrobbleSOTRanges: PaginatedTimeRangeOptions[] = []; 106 106 tracksScrobbled: number = 0; 107 + tracksScrobbledTotal: number = 0; 107 108 108 109 lastScrobbleAttempt: Dayjs = dayjs(0) 109 110 upstreamRefresh: MarkOptional<Required<UpstreamRefreshOptions>, 'refreshInitialCount'>; ··· 341 342 this.migrationRepo = new GenericRepository<'componentMigrations'>(this.db, 'componentMigrations', 'Component Migrations', {logger: this.logger}); 342 343 this.playRepo.componentId = this.dbComponent.id; 343 344 this.queueRepo.componentId = this.dbComponent.id; 344 - this.tracksScrobbled = this.dbComponent.countLive + this.dbComponent.countNonLive; 345 + const counts = await this.playRepo.getComponentPlayCountByState(); 346 + const scrobbledCount = counts.find(x => x.state === 'scrobbled'); 347 + if(scrobbledCount !== undefined) { 348 + this.tracksScrobbledTotal = scrobbledCount['count(*)']; 349 + } 345 350 await this.updateQueueStats([CLIENT_INGRESS_QUEUE, CLIENT_DEAD_QUEUE]); 346 351 } 347 352 ··· 425 430 type: this.type, 426 431 status: this.status, 427 432 queued: this.queuedLength, 433 + tracksScrobbled: this.tracksScrobbled, 434 + countLive: this.tracksScrobbledTotal, 428 435 deadLetterScrobbles: this.deadLetterQueued, 429 436 deadLetterScrobblesTotal: this.deadLetterLength, 430 437 } ··· 851 858 this.scrobbledCounter.labels(this.getPrometheusLabels()).inc(); 852 859 //this.lastScrobbledPlayDate = playObj.data.playDate; 853 860 this.tracksScrobbled++; 861 + this.tracksScrobbledTotal++; 854 862 } 855 863 856 864 findExistingSubmittedPlayObj = async (playObjPre: PlayObject): Promise<([undefined, undefined] | [ScrobbledPlayObject, ScrobbledPlayObject[]])> => {
+9 -2
src/backend/sources/AbstractSource.ts
··· 83 83 stopPollingWaitInterval: number = 200; 84 84 pollRetries: number = 0; 85 85 tracksDiscovered: number = 0; 86 + tracksDiscoveredTotal: number = 0; 86 87 87 88 protected isSleeping: boolean = false; 88 89 protected wakeAt: Dayjs = dayjs(); ··· 214 215 215 216 protected async postDatabase(): Promise<void> { 216 217 this.playRepo = new DrizzlePlayRepository(this.db, {logger: this.logger}); 217 - this.tracksDiscovered = this.dbComponent.countLive; 218 218 this.playRepo.componentId = this.dbComponent.id; 219 + const counts = await this.playRepo.getComponentPlayCountByState(); 220 + const discoveredCount = counts.find(x => x.state === 'discovered'); 221 + if(discoveredCount !== undefined) { 222 + this.tracksDiscoveredTotal = discoveredCount['count(*)']; 223 + } 219 224 } 220 225 221 226 protected generateStaggerMappers() { ··· 295 300 manualListening: this.manualListening, 296 301 systemListeningBehavior: this.getSystemListeningBehavior(), 297 302 sleeping: this.getIsSleeping(), 298 - wakeAt: this.wakeAt !== undefined ? this.wakeAt.toISOString() : undefined 303 + wakeAt: this.wakeAt !== undefined ? this.wakeAt.toISOString() : undefined, 304 + countLive: this.tracksDiscoveredTotal 299 305 } 300 306 } 301 307 ··· 332 338 this.cache.cacheDb.set(this.recentDiscoveredCacheKey(), recentPlays, '2m'); 333 339 } 334 340 this.tracksDiscovered++; 341 + this.tracksDiscoveredTotal++ 335 342 this.logger.info(`Discovered => ${buildTrackString(play)}`); 336 343 this.emitEvent('discovered', {play}); 337 344 this.emitPlayInsert({...playRow[0], queueStates: []} as unknown as PlayApiCommonDetailed);
+2 -2
src/client/components/msComponent/Stats.tsx
··· 22 22 import { shortTodayAwareFormat } from "../../../core/TimeUtils.js"; 23 23 24 24 export const CountLiveIndicator = (props: { 25 - data: Pick<ComponentCommonApiJson, 'countLive' | 'mode' | 'id'> & { tracksDiscovered?: number, scrobbled?: number }, 25 + data: Pick<ComponentCommonApiJson, 'countLive' | 'mode' | 'id'> & { tracksDiscovered?: number, tracksScrobbled?: number }, 26 26 recent?: number 27 27 recentTimeout?: number 28 28 streamable?: boolean ··· 38 38 ...rest 39 39 } = props; 40 40 41 - const sessionCount = props.data.mode === 'source' ? props.data.tracksDiscovered : props.data.countLive; 41 + const sessionCount = props.data.mode === 'source' ? props.data.tracksDiscovered : props.data.tracksScrobbled; 42 42 43 43 const [total, setTotal] = useState(props.data.countLive); 44 44 const [current, setCurrent] = useState(sessionCount);
+1
src/core/Api.ts
··· 91 91 92 92 export type ComponentCientApiBase = { 93 93 queued: number 94 + tracksScrobbled: number 94 95 deadLetterScrobbles: number 95 96 deadLetterScrobblesTotal: number 96 97 }
+1
src/core/tests/utils/apiFixtures.ts
··· 196 196 return { 197 197 ...common, 198 198 queued, 199 + tracksScrobbled: common.countLive, 199 200 deadLetterScrobbles, 200 201 deadLetterScrobblesTotal, 201 202 }