From c017dd734bc2f814a933bb4762eff97ed5a48dae Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 4 Nov 2025 16:31:36 +0000 Subject: [PATCH] feat(subsonic): Add basic repeat detection based on listened duration #375 --- .../PlayerState/AbstractPlayerState.ts | 2 +- .../PlayerState/SubsonicPlayerState.ts | 19 +++++++++++++++++++ src/backend/sources/SubsonicSource.ts | 7 ++++++- 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/backend/sources/PlayerState/SubsonicPlayerState.ts diff --git a/src/backend/sources/PlayerState/AbstractPlayerState.ts b/src/backend/sources/PlayerState/AbstractPlayerState.ts index bfcb4e25..9c5ddf13 100644 --- a/src/backend/sources/PlayerState/AbstractPlayerState.ts +++ b/src/backend/sources/PlayerState/AbstractPlayerState.ts @@ -318,7 +318,7 @@ export abstract class AbstractPlayerState { * * Listened duraton is more than 2 minutes/50% of Play OR... * * Previous Position was close to end of Play */ - protected isSessionRepeat(position?: number, reportedTS?: Dayjs) { + protected isSessionRepeat(position?: number, reportedTS?: Dayjs): boolean { if(this.currentListenRange === undefined) { return false; } diff --git a/src/backend/sources/PlayerState/SubsonicPlayerState.ts b/src/backend/sources/PlayerState/SubsonicPlayerState.ts new file mode 100644 index 00000000..e5f078a2 --- /dev/null +++ b/src/backend/sources/PlayerState/SubsonicPlayerState.ts @@ -0,0 +1,19 @@ +import { SourcePlayerObj } from "../../../core/Atomic.js"; +import { GenericPlayerState } from "./GenericPlayerState.js"; +import { Dayjs } from "dayjs"; + +export class SubsonicPlayerState extends GenericPlayerState { + + protected isSessionRepeat(position?: number, reportedTS?: Dayjs) { + if(super.isSessionRepeat()) { + return true; + } + // if track has a duration and the listened duration for this session is greater than 100% + 5% (for buffer) + // then assume track is on repeat + if(this.currentPlay.data.duration !== undefined && this.getListenDuration() > (this.currentPlay.data.duration + (0.05 * this.currentPlay.data.duration))) { + this.logger.debug('Listened duration for this session is over 105%, triggering as a repeat'); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/backend/sources/SubsonicSource.ts b/src/backend/sources/SubsonicSource.ts index 167a3dac..1e83b661 100644 --- a/src/backend/sources/SubsonicSource.ts +++ b/src/backend/sources/SubsonicSource.ts @@ -6,13 +6,16 @@ import request, { Request } from 'superagent'; import { PlayObject } from "../../core/Atomic.js"; import { isNodeNetworkException } from "../common/errors/NodeErrors.js"; import { UpstreamError } from "../common/errors/UpstreamError.js"; -import { DEFAULT_RETRY_MULTIPLIER, FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js"; +import { DEFAULT_RETRY_MULTIPLIER, FormatPlayObjectOptions, InternalConfig, PlayPlatformId } from "../common/infrastructure/Atomic.js"; import { SubSonicSourceConfig } from "../common/infrastructure/config/source/subsonic.js"; import { getSubsonicResponse, SubsonicResponse, SubsonicResponseCommon } from "../common/vendor/subsonic/interfaces.js"; import { parseRetryAfterSecsFromObj, removeDuplicates, sleep } from "../utils.js"; import { findCauseByFunc } from "../utils/ErrorUtils.js"; import { RecentlyPlayedOptions } from "./AbstractSource.js"; import MemorySource from "./MemorySource.js"; +import { SubsonicPlayerState } from './PlayerState/SubsonicPlayerState.js'; +import { PlayerStateOptions } from './PlayerState/AbstractPlayerState.js'; +import { Logger } from '@foxxmd/logging'; dayjs.extend(isSameOrAfter); @@ -291,6 +294,8 @@ export class SubsonicSource extends MemorySource { const userFiltered = this.usersAllow.length == 0 ? deduped : deduped.filter(x => x.meta.user === undefined || this.usersAllow.map(x => x.toLocaleLowerCase()).includes(x.meta.user.toLocaleLowerCase())); return this.processRecentPlays(userFiltered); } + + getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new SubsonicPlayerState(logger, id, opts); } export const getSubsonicResponseFromError = (error: unknown): UpstreamError => findCauseByFunc(error, (err) => { -- 2.51.2