[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(source): Optional position for non-positional sources

* Allow non-positional Sources to still report position (for display only)
* Refactor Azuracast to be non-positional due to reported position not always being accurate (azuracast approximates track length)

FoxxMD (Mar 20, 2025, 3:40 PM UTC) 242bacc1 cbe28f97

+20 -30
+2 -6
src/backend/sources/AzuracastSource.ts
··· 1 - import { MemoryPositionalSource } from "./MemoryPositionalSource.js"; 2 - import { sleep } from "../utils.js"; 1 + import MemorySource from "./MemorySource.js"; 3 2 import { RecentlyPlayedOptions } from "./AbstractSource.js"; 4 3 import { childLogger, Logger } from "@foxxmd/logging"; 5 4 import { EventEmitter } from "events"; ··· 17 16 } from "../common/infrastructure/Atomic.js"; 18 17 import { AzuracastSourceConfig, AzuraNowPlayingResponse, AzuraStationResponse } from "../common/infrastructure/config/source/azuracast.js"; 19 18 import { isPortReachable, normalizeWSAddress } from "../utils/NetworkUtils.js"; 20 - import { PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js"; 21 - import { AzuracastPlayerState } from "./PlayerState/AzuracastPlayerState.js"; 22 19 23 20 24 - export class AzuracastSource extends MemoryPositionalSource { 21 + export class AzuracastSource extends MemorySource { 25 22 26 23 declare config: AzuracastSourceConfig; 27 24 ··· 230 227 return this.processRecentPlays([playerState]); 231 228 } 232 229 233 - getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new AzuracastPlayerState(logger, id, opts); 234 230 } 235 231 236 232 const formatPlayObj = (obj: AzuraNowPlayingResponse, options: FormatPlayObjectOptions = {}): PlayObject => {
+7 -1
src/backend/sources/MemoryPositionalSource.ts
··· 1 1 import { Logger } from "@foxxmd/logging"; 2 - import { PlayerStateDataMaybePlay, PlayPlatformId } from "../common/infrastructure/Atomic.js"; 2 + import { InternalConfig, PlayerStateDataMaybePlay, PlayPlatformId, SourceType } from "../common/infrastructure/Atomic.js"; 3 3 import MemorySource from "./MemorySource.js"; 4 4 import { PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js"; 5 5 import { PositionalPlayerState } from "./PlayerState/PositionalPlayerState.js"; 6 6 import { PlayObject } from "../../core/Atomic.js"; 7 + import { SourceConfig } from "../common/infrastructure/config/source/sources.js"; 8 + import EventEmitter from "events"; 7 9 8 10 export class MemoryPositionalSource extends MemorySource { 11 + constructor(type: SourceType, name: string, config: SourceConfig, internal: InternalConfig, emitter: EventEmitter) { 12 + super(type, name, config, internal, emitter); 13 + this.isPositional = true; 14 + } 9 15 getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new PositionalPlayerState(logger, id, opts) 10 16 }
+3 -1
src/backend/sources/MemorySource.ts
··· 52 52 53 53 scheduler: ToadScheduler = new ToadScheduler(); 54 54 55 + protected isPositional: boolean = false; 56 + 55 57 constructor(type: SourceType, name: string, config: SourceConfig, internal: InternalConfig, emitter: EventEmitter) { 56 58 super(type, name, config, internal, emitter); 57 59 ··· 331 333 for(const player of this.players.values()) { 332 334 if(player.calculatedStatus === CALCULATED_PLAYER_STATUSES.playing) { 333 335 const pos = player.getPosition(); 334 - if(pos !== undefined && player.currentPlay !== undefined && player.currentPlay.data?.duration !== undefined) { 336 + if(pos !== undefined && this.isPositional && player.currentPlay !== undefined && player.currentPlay.data?.duration !== undefined) { 335 337 const { 336 338 data: { 337 339 duration
-16
src/backend/sources/PlayerState/AzuracastPlayerState.ts
··· 1 - import { Logger } from "@foxxmd/logging"; 2 - import { PlayPlatformId, REPORTED_PLAYER_STATUSES } from "../../common/infrastructure/Atomic.js"; 3 - import { AbstractPlayerState, PlayerStateOptions } from "./AbstractPlayerState.js"; 4 - import { GenericPlayerState } from "./GenericPlayerState.js"; 5 - import { PositionalPlayerState } from "./PositionalPlayerState.js"; 6 - 7 - export class AzuracastPlayerState extends PositionalPlayerState { 8 - constructor(logger: Logger, platformId: PlayPlatformId, opts?: PlayerStateOptions) { 9 - super(logger, platformId, {allowedDrift: 17000, rtTruth: true, ...(opts || {})}); 10 - this.gracefulEndBuffer = this.allowedDrift / 1000; 11 - } 12 - 13 - protected isSessionStillPlaying(position: number): boolean { 14 - return this.reportedStatus === REPORTED_PLAYER_STATUSES.playing; 15 - } 16 - }
+3 -3
src/backend/sources/PlayerState/GenericPlayerState.ts
··· 19 19 super(logger, platformId, opts); 20 20 } 21 21 22 - protected currentListenSessionContinue(position: number = 0, timestamp?: Dayjs) { 22 + protected currentListenSessionContinue(position?: number, timestamp?: Dayjs) { 23 23 if (this.currentListenRange === undefined) { 24 24 this.logger.debug('Started new Player listen range.'); 25 - this.currentListenRange = this.newListenRange(this.newListenProgress({timestamp})); 25 + this.currentListenRange = this.newListenRange(this.newListenProgress({position, timestamp})); 26 26 } else { 27 27 this.calculatedStatus = CALCULATED_PLAYER_STATUSES.playing; 28 - this.currentListenRange.setRangeEnd(this.newListenProgress({timestamp})); 28 + this.currentListenRange.setRangeEnd(this.newListenProgress({position, timestamp})); 29 29 } 30 30 } 31 31
+4 -2
src/backend/sources/PlayerState/ListenProgress.ts
··· 6 6 7 7 public timestamp: Dayjs; 8 8 public positionPercent?: number; 9 + public position?: Second; 9 10 10 11 constructor(data: Partial<PlayProgress> = {}) { 11 - const {timestamp, positionPercent} = data; 12 + const {timestamp, positionPercent, position} = data; 12 13 this.timestamp = timestamp ?? dayjs(); 13 14 this.positionPercent = positionPercent; 15 + this.position = position; 14 16 } 15 17 16 18 getDuration(end: ListenProgressTS): Second { ··· 27 29 } 28 30 29 31 export class ListenProgressPositional extends ListenProgressTS implements PlayProgressPositional { 30 - public position: Second; 32 + declare public position: Second; 31 33 32 34 constructor(data: PlayProgressPositional) { 33 35 super(data);
+1 -1
src/backend/sources/PlayerState/ListenRange.ts
··· 66 66 } 67 67 68 68 public getPosition(): Second { 69 - return undefined; 69 + return this.end.position; 70 70 } 71 71 72 72 public finalize(position?: number) {