From 9d0cf8f8456844e46036d6932071cdfa33569b46 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 1 Apr 2026 16:47:32 +0000 Subject: [PATCH] feat: Implement dispose methods for classes with schedulers and long running async tasks --- .../scrobblers/AbstractScrobbleClient.ts | 12 ++ src/backend/sources/AbstractSource.ts | 6 + src/backend/sources/MemorySource.ts | 21 +++ .../PlayerState/AbstractPlayerState.ts | 9 ++ .../sources/PlayerState/ListenRange.ts | 9 ++ .../PlayerState/PositionalPlayerState.ts | 8 +- .../sources/PlayerState/RealtimePlayer.ts | 7 + src/backend/tests/cache/cache.test.ts | 9 +- .../tests/scrobbler/scrobblers.test.ts | 144 +++++++++--------- src/backend/tests/source/source.test.ts | 34 ++--- 10 files changed, 160 insertions(+), 99 deletions(-) diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 1eee98d1..3382c14e 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -198,6 +198,18 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i this.existingScrobble = (playObjPre: PlayObject, existingScrobbles: PlayObject[], log?: boolean) => existingScrobble(playObjPre, existingScrobbles, existingScrobbleOpts, log); } + [Symbol.dispose]() { + this.scheduler.stop(); + for(const job of this.scheduler.getAllJobs()) { + this.scheduler.removeById(job.id); + } + + } + async [Symbol.asyncDispose]() { + this[Symbol.dispose](); + await this.tryStopScrobbling(); + } + protected async postCache(): Promise { await super.postCache(); this.generateStaggerMappers(); diff --git a/src/backend/sources/AbstractSource.ts b/src/backend/sources/AbstractSource.ts index a4407abc..c5861ac1 100644 --- a/src/backend/sources/AbstractSource.ts +++ b/src/backend/sources/AbstractSource.ts @@ -117,6 +117,12 @@ export default abstract class AbstractSource extends AbstractComponent implement this.discoveredCounter = getRoot().items.sourceMetics.discovered; } + async [Symbol.asyncDispose]() { + if(this.canPoll) { + await this.tryStopPolling(); + } + } + protected async postCache(): Promise { await super.postCache(); this.generateStaggerMappers(); diff --git a/src/backend/sources/MemorySource.ts b/src/backend/sources/MemorySource.ts index 386ba9c8..f6e0eac2 100644 --- a/src/backend/sources/MemorySource.ts +++ b/src/backend/sources/MemorySource.ts @@ -71,6 +71,25 @@ export default class MemorySource extends AbstractSource { }))); } + [Symbol.dispose]() { + this.scheduler.stop(); + for(const job of this.scheduler.getAllJobs()) { + this.scheduler.removeById(job.id); + } + for(const p of this.players.keys()) { + this.deletePlayer(p); + } + } + + async [Symbol.asyncDispose]() { + await super[Symbol.asyncDispose](); + this.scheduler.stop(); + for(const job of this.scheduler.getAllJobs()) { + this.scheduler.removeById(job.id); + } + this[Symbol.dispose](); + } + cleanupPlayers = () => { for (const key of this.players.keys()) { this.cleanupPlayer(key); @@ -188,6 +207,8 @@ export default class MemorySource extends AbstractSource { if(reason !== undefined) { this.players.get(id)?.logger.debug(reason); } + using player = this.players.get(id); + player[Symbol.dispose](); this.players.delete(id); this.playerState.delete(id); this.emitEvent('playerDelete', {platformId: id}); diff --git a/src/backend/sources/PlayerState/AbstractPlayerState.ts b/src/backend/sources/PlayerState/AbstractPlayerState.ts index db1faee4..a5dc045f 100644 --- a/src/backend/sources/PlayerState/AbstractPlayerState.ts +++ b/src/backend/sources/PlayerState/AbstractPlayerState.ts @@ -99,6 +99,15 @@ export abstract class AbstractPlayerState { this.stateIntervalOptions = {staleInterval, orphanedInterval: orphanedInterval}; } + [Symbol.dispose]() { + if(this.currentListenRange !== undefined) { + this.currentListenRange[Symbol.dispose](); + } + for(const lr of this.listenRanges) { + lr[Symbol.dispose](); + } + } + protected abstract newListenProgress(data?: Partial): ListenProgress; protected abstract newListenRange(start?: ListenProgress, end?: ListenProgress, options?: object): ListenRange; diff --git a/src/backend/sources/PlayerState/ListenRange.ts b/src/backend/sources/PlayerState/ListenRange.ts index 44c692ef..cf6b511f 100644 --- a/src/backend/sources/PlayerState/ListenRange.ts +++ b/src/backend/sources/PlayerState/ListenRange.ts @@ -15,6 +15,10 @@ export abstract class ListenRange { this.end = e; } + [Symbol.dispose]() { + + } + public abstract isPositional(): boolean; public abstract isInitial(): boolean; public abstract seeked(position?: number, reportedTS?: Dayjs): [boolean, Second?]; @@ -100,6 +104,11 @@ export class ListenRangePositional extends ListenRange { this.finalized = false; } + [Symbol.dispose]() { + super[Symbol.dispose]() + this.rtPlayer[Symbol.dispose](); + } + isPositional(): boolean { return true; } diff --git a/src/backend/sources/PlayerState/PositionalPlayerState.ts b/src/backend/sources/PlayerState/PositionalPlayerState.ts index 25159df1..7452197e 100644 --- a/src/backend/sources/PlayerState/PositionalPlayerState.ts +++ b/src/backend/sources/PlayerState/PositionalPlayerState.ts @@ -1,12 +1,10 @@ import { Logger } from "@foxxmd/logging"; import { CALCULATED_PLAYER_STATUSES, PlayPlatformId, REPORTED_PLAYER_STATUSES } from "../../common/infrastructure/Atomic.js"; import { AbstractPlayerState, PlayerStateOptions } from "./AbstractPlayerState.js"; -import { GenericPlayerState } from "./GenericPlayerState.js"; -import { GenericRealtimePlayer, RealtimePlayer } from "./RealtimePlayer.js"; -import { PlayProgress, PlayProgressPositional, Second } from "../../../core/Atomic.js"; +import { PlayProgressPositional, Second } from "../../../core/Atomic.js"; import { Dayjs } from "dayjs"; -import { ListenProgress, ListenProgressPositional } from "./ListenProgress.js"; -import { ListenRange, ListenRangePositional } from "./ListenRange.js"; +import { ListenProgressPositional } from "./ListenProgress.js"; +import { ListenRangePositional } from "./ListenRange.js"; export class PositionalPlayerState extends AbstractPlayerState { diff --git a/src/backend/sources/PlayerState/RealtimePlayer.ts b/src/backend/sources/PlayerState/RealtimePlayer.ts index 1a6ec9af..ff7722c5 100644 --- a/src/backend/sources/PlayerState/RealtimePlayer.ts +++ b/src/backend/sources/PlayerState/RealtimePlayer.ts @@ -33,6 +33,13 @@ export abstract class RealtimePlayer { this.scheduler.stop(); } + [Symbol.dispose]() { + this.scheduler.stop(); + for(const job of this.scheduler.getAllJobs()) { + this.scheduler.removeById(job.id); + } + } + public play(position?: number) { if (position !== undefined) { this.position = position; diff --git a/src/backend/tests/cache/cache.test.ts b/src/backend/tests/cache/cache.test.ts index 4b005251..f481c4ec 100644 --- a/src/backend/tests/cache/cache.test.ts +++ b/src/backend/tests/cache/cache.test.ts @@ -4,9 +4,8 @@ import asPromised from 'chai-as-promised'; import { after, before, describe, it } from 'mocha'; import dayjs from "dayjs"; import withLocalTmpDir from 'with-local-tmp-dir'; -import path from 'path'; import { initFileCache, initMemoryCache, initValkeyCache, MSCache } from "../../common/Cache.js"; -import { generatePlay, generatePlayerStateData, generatePlays, normalizePlays } from "../../../core/PlayTestUtils.js"; +import { generatePlays } from "../../../core/PlayTestUtils.js"; import { ListenProgressPositional, ListenProgressTS } from "../../sources/PlayerState/ListenProgress.js"; import { isPortReachableConnect } from "../../utils/NetworkUtils.js"; import { getRoot } from "../../ioc.js"; @@ -165,7 +164,7 @@ describe('#Caching', function () { const root = getRoot(); root.upsert({ cache: () => () => new MSCache(loggerTest, { scrobble: { provider: 'file', connection: process.cwd(), persistInterval: 100 } }) }); - const test = new TestScrobbler(); + await using test = new TestScrobbler(); await test.initialize(); const plays = generatePlays(100); await test.queueScrobble(plays, 'testSource'); @@ -174,13 +173,11 @@ describe('#Caching', function () { const dirContents = await promises.readdir('.'); const hasCache = dirContents.some(x => x === 'ms-scrobble.cache'); expect(hasCache).is.true; - test.scheduler.stop(); - const newTest = new TestScrobbler(); + await using newTest = new TestScrobbler(); await newTest.initialize(); expect(newTest.queuedScrobbles.length).to.eq(plays.length); expect(newTest.queuedScrobbles[0].play.data.track).to.eq(queued[0].data.track); - newTest.scheduler.stop(); }, { unsafeCleanup: true }); diff --git a/src/backend/tests/scrobbler/scrobblers.test.ts b/src/backend/tests/scrobbler/scrobblers.test.ts index 3420259a..1dc8e0cf 100644 --- a/src/backend/tests/scrobbler/scrobblers.test.ts +++ b/src/backend/tests/scrobbler/scrobblers.test.ts @@ -50,8 +50,6 @@ const generateTestScrobbler = () => { return testScrobbler; } -let testScrobbler: TestScrobbler = generateTestScrobbler() - describe('Networking', function () { describe('Authentication', function () { @@ -118,12 +116,9 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu describe('When scrobble is unique', function () { - beforeEach(function() { - testScrobbler = generateTestScrobbler(); - }); - it('It is not detected as duplicate when play date is newer than most recent', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const newScrobble = generatePlay({ @@ -136,6 +131,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu it('It is not detected as duplicate when play date is close to an existing scrobble', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const newScrobble = generatePlay({ @@ -147,6 +143,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu it('It handles unique detection when no existing scrobble matches above a score of 0', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const uniquePlay = generatePlay({ @@ -165,12 +162,9 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu describe('When scrobble track/artist/album matches existing but is a new scrobble', function () { - beforeEach(function() { - testScrobbler = generateTestScrobbler(); - }); - it('Is not detected as duplicate when artist is same, time is similar, but track is different', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const diffPlay = clone(normalizedWithMixedDur[1]); @@ -182,6 +176,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu it('Is not detected as duplicate when track is same, time is similar, but artist is different', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const diffPlay = clone(normalizedWithMixedDur[1]); @@ -194,6 +189,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu it('Is not detected as duplicate when play date is different by more than 10 seconds (high granularity source)', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const timeOffPos = clone(normalizedWithMixedDur[normalizedWithMixedDur.length - 1]); @@ -212,6 +208,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu initialDate: firstPlayDate, defaultMeta: {source: 'subsonic'} }); + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = recent; const timeOffPos = clone(recent[recent.length - 1]); @@ -226,12 +223,9 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu describe('When existing has duration', function () { - beforeEach(function() { - testScrobbler = generateTestScrobbler(); - }); - it('A track with continuity to the previous track is not detected as a duplicate', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithDur; const brickPt1 = normalizedWithDur.find(x => x.data.track.includes('Another Brick')); @@ -261,6 +255,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu initialDate: firstPlayDate, defaultMeta: {source: 'jellyfin'} }); + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = recent; const repeatPlay = clone(recent[recent.length - 1]); @@ -281,6 +276,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu playDate: dayjs().subtract(179, 's'), duration: 179 }); + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = [play]; const newPlay = clone(play); @@ -296,16 +292,14 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu describe('When scrobble is a duplicate (title/artists/album)', function () { - beforeEach(function() { - testScrobbler = generateTestScrobbler(); - }); - it('Is detected as duplicate when an exact match', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; assert.isTrue((await testScrobbler.alreadyScrobbled(normalizedWithMixedDur[normalizedWithMixedDur.length - 1]))[0]); }); it('Is detected as duplicate when artist/title differences are whitespace or case', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const ref = normalizedWithMixedDur[3]; @@ -330,6 +324,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu }); it('Is detected as duplicate when artist/title differences are from unicode normalization', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const ref = normalizedWithMixedDur.find(x => x.data.track === 'Jimbó'); @@ -341,6 +336,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu it('Is detected as duplicate when play date is off by 10 seconds or less (high granularity source)', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const timeOffPos = clone(normalizedWithMixedDur[normalizedWithMixedDur.length - 1]); @@ -370,6 +366,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu initialDate: firstPlayDate, defaultMeta: {source: 'subsonic'} }); + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = recent; const timeOffPos = clone(recent[recent.length - 1]); @@ -383,6 +380,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu }); it('Is detected as duplicate when title is exact, artist is similar, and time is similar', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const ref = normalizedWithMixedDur[3]; @@ -427,7 +425,7 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu lifecycle: defaultLifecycle() } } - + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithMixedDurOlder.concat(ref); assert.isTrue((await testScrobbler.alreadyScrobbled(spotifyPlay))[0]); @@ -435,12 +433,9 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu describe('When at least one play has duration', function () { - beforeEach(function() { - testScrobbler = generateTestScrobbler(); - }); - it('Is detected as duplicate when play date is close to the end of an existing scrobble', async function () { + await using testScrobbler = generateTestScrobbler(); testScrobbler.testRecentScrobbles = normalizedWithDur; const timeEnd = clone(normalizedWithDur[normalizedWithMixedDur.length - 2]); @@ -465,16 +460,13 @@ describe('Detects duplicate and unique scrobbles from client recent history', fu describe('Detects duplicate and unique scrobbles using actively tracked scrobbles', function() { - beforeEach(function() { - testScrobbler = generateTestScrobbler(); - testScrobbler.testRecentScrobbles = normalizedWithMixedDur; - }); - it('Detects a unique play', async function() { const newScrobble = generatePlay({ playDate: normalizedWithMixedDur[normalizedWithMixedDur.length - 3].data.playDate.add(3, 'seconds') }); + await using testScrobbler = generateTestScrobbler(); + testScrobbler.testRecentScrobbles = normalizedWithMixedDur; const [matchedPlay, matchedData] = await testScrobbler.findExistingSubmittedPlayObj(newScrobble); assert.isUndefined(matchedPlay); @@ -485,6 +477,8 @@ describe('Detects duplicate and unique scrobbles using actively tracked scrobble const newScrobble = generatePlay({ playDate: normalizedWithMixedDur[normalizedWithMixedDur.length - 3].data.playDate.add(3, 'seconds') }); + await using testScrobbler = generateTestScrobbler(); + testScrobbler.testRecentScrobbles = normalizedWithMixedDur; testScrobbler.addScrobbledTrack(newScrobble, newScrobble); const [matchedPlay, matchedData] = await testScrobbler.findExistingSubmittedPlayObj(newScrobble); @@ -497,6 +491,8 @@ describe('Detects duplicate and unique scrobbles using actively tracked scrobble const newScrobble = generatePlay({ playDate: normalizedWithMixedDur[normalizedWithMixedDur.length - 3].data.playDate.add(3, 'seconds') }); + await using testScrobbler = generateTestScrobbler(); + testScrobbler.testRecentScrobbles = normalizedWithMixedDur; testScrobbler.addScrobbledTrack(newScrobble, newScrobble); const dupScrobble = clone(newScrobble); @@ -519,7 +515,7 @@ describe('Upstream Scrobbles', function() { it('Calls timerange func to get SOT scrobbles when none exists', async function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); - const scrobbler = generateTestScrobbler(); + await using scrobbler = generateTestScrobbler(); scrobbler.testRecentScrobbles = []; await scrobbler.tryInitialize(); scrobbler.testRecentScrobbles = existingPlays; @@ -533,11 +529,12 @@ describe('Upstream Scrobbles', function() { await emptied; scrobbler.tryStopScrobbling().then(() => null); expect(sp.called).is.true; + return; }); it('Uses cached timerange for closely grouped scrobbles', async function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); - const scrobbler = generateTestScrobbler(); + await using scrobbler = generateTestScrobbler(); scrobbler.testRecentScrobbles = []; await scrobbler.tryInitialize(); scrobbler.testRecentScrobbles = existingPlays; @@ -552,11 +549,12 @@ describe('Upstream Scrobbles', function() { await emptied; scrobbler.tryStopScrobbling().then(() => null); expect(sp.callCount).to.eq(1); + return; }); it('Uses separate timerange calls when scrobbles are not closely grouped', async function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); - const scrobbler = generateTestScrobbler(); + await using scrobbler = generateTestScrobbler(); scrobbler.testRecentScrobbles = []; await scrobbler.tryInitialize(); scrobbler.testRecentScrobbles = existingPlays; @@ -572,11 +570,12 @@ describe('Upstream Scrobbles', function() { await emptied; scrobbler.tryStopScrobbling().then(() => null); expect(sp.callCount).to.eq(2); + return; }); it('Gets fresh timerange if TTL of staleAfter has passed', async function() { const existingPlays = normalizePlays(generatePlays(3), {initialDate: dayjs().subtract(1, 'hour')}); - const scrobbler = generateTestScrobbler(); + await using scrobbler = generateTestScrobbler(); scrobbler.testRecentScrobbles = []; await scrobbler.tryInitialize(); scrobbler.testRecentScrobbles = existingPlays; @@ -597,6 +596,7 @@ describe('Upstream Scrobbles', function() { await emptied2; scrobbler.tryStopScrobbling().then(() => null); expect(sp.calledTwice).is.true; + return; }); }); @@ -604,7 +604,7 @@ describe('Upstream Scrobbles', function() { describe('Dead Scrobbles', function() { it('Processes all dead scrobbles', async function () { - testScrobbler = generateTestScrobbler(); + await using testScrobbler = generateTestScrobbler(); await testScrobbler.initialize(); testScrobbler.testRecentScrobbles = []; @@ -619,21 +619,19 @@ describe('Dead Scrobbles', function() { }); - +const normalizedScrobbler = async () => { + await using testScrobbler = generateTestScrobbler(); + await testScrobbler.initialize(); + testScrobbler.testRecentScrobbles = normalizedWithMixedDur; + testScrobbler.scrobbleSleep = 500; + testScrobbler.scrobbleDelay = 0; + //testScrobbler.lastScrobbleCheck = dayjs().subtract(60, 'seconds'); + testScrobbler.config.options = {}; + return testScrobbler; +} describe('Scrobble client uses transform plays correctly', function() { - beforeEach(async function() { - testScrobbler = generateTestScrobbler(); - await testScrobbler.initialize(); - testScrobbler.testRecentScrobbles = normalizedWithMixedDur; - testScrobbler.scrobbleSleep = 500; - testScrobbler.scrobbleDelay = 0; - //testScrobbler.lastScrobbleCheck = dayjs().subtract(60, 'seconds'); - testScrobbler.config.options = {}; - //testScrobbler.initScrobbleMonitoring().catch(console.error); - }); - // TODO need to find a better way to detect this // since we are now doing it in the processing loop instead of before queue @@ -656,6 +654,7 @@ describe('Scrobble client uses transform plays correctly', function() { // }); it('Transforms play on scrobble when postCompare is present', async function() { + await using testScrobbler = await normalizedScrobbler(); testScrobbler.config.options = { playTransform: { postCompare: { @@ -679,6 +678,7 @@ describe('Scrobble client uses transform plays correctly', function() { }); it('Transforms candidate play on comparison', async function() { + await using testScrobbler = await normalizedScrobbler(); testScrobbler.config.options = { playTransform: { compare: { @@ -701,6 +701,7 @@ describe('Scrobble client uses transform plays correctly', function() { }); it('Transforms existing play on comparison', async function() { + await using testScrobbler = await normalizedScrobbler(); testScrobbler.config.options = { playTransform: { compare: { @@ -724,24 +725,25 @@ describe('Scrobble client uses transform plays correctly', function() { afterEach(async function () { this.timeout(3500); - await testScrobbler.tryStopScrobbling() + //await testScrobbler.tryStopScrobbling() }); }); -describe('Manages scrobble queue', function() { +const normalizedMonitoringScrobbler = async () => { + const testScrobbler = generateTestScrobbler(); + await testScrobbler.initialize(); + testScrobbler.testRecentScrobbles = normalizedWithMixedDur; + testScrobbler.scrobbleSleep = 100; + testScrobbler.scrobbleDelay = 0; + testScrobbler.initScrobbleMonitoring().catch(console.error); + return testScrobbler; +} - beforeEach(async function() { - testScrobbler = generateTestScrobbler(); - await testScrobbler.initialize(); - testScrobbler.testRecentScrobbles = normalizedWithMixedDur; - testScrobbler.testRecentScrobbles = normalizedWithMixedDur; - testScrobbler.scrobbleSleep = 500; - testScrobbler.scrobbleDelay = 0; - testScrobbler.initScrobbleMonitoring().catch(console.error); - }); +describe('Manages scrobble queue', function() { it('Scrobbles a uniquely queued play', async function() { + await using testScrobbler = await normalizedMonitoringScrobbler(); const newScrobble = generatePlay({ playDate: normalizedWithMixedDur[normalizedWithMixedDur.length - 3].data.playDate.add(3, 'seconds') }); @@ -753,6 +755,7 @@ describe('Manages scrobble queue', function() { }); it('Does not Scrobble a duplicate play queued after original is scrobbled', async function() { + await using testScrobbler = await normalizedMonitoringScrobbler(); this.timeout(20000); const newScrobble = generatePlay({ @@ -774,6 +777,7 @@ describe('Manages scrobble queue', function() { }); it('Does not Scrobble a duplicate play queued before original is scrobbled', async function() { + await using testScrobbler = await normalizedMonitoringScrobbler(); this.timeout(3500); const newScrobble = generatePlay({ @@ -795,6 +799,7 @@ describe('Manages scrobble queue', function() { }); it('Delays scrobbles when many are queued', async function () { + await using testScrobbler = await normalizedMonitoringScrobbler(); this.timeout(3500); const newScrobble1 = generatePlay({ @@ -823,7 +828,6 @@ describe('Manages scrobble queue', function() { after(async function () { this.timeout(3500); - await testScrobbler.tryStopScrobbling() }); }); @@ -833,7 +837,7 @@ describe('Now Playing', function() { it('When no Now Playing exists, chooses play based on sorted platform id', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const firstPlatform: PlayPlatformId = ['aaa', 'NO_USER']; @@ -852,7 +856,7 @@ describe('Now Playing', function() { it('When Now Playing platform does not exist in queued plays, chooses play based on sorted platform id', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const firstPlatform: PlayPlatformId = ['aaa', 'NO_USER']; @@ -873,7 +877,7 @@ describe('Now Playing', function() { it('Chooses play based on existing Now Playing', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const firstPlatform: PlayPlatformId = ['aaa', 'NO_USER']; @@ -897,7 +901,7 @@ describe('Now Playing', function() { it('Sorts Sources alphabetically when using default Source sorting', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const a = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -914,7 +918,7 @@ describe('Now Playing', function() { it('Sorts Sources based on user config', async function () { - const npScrobbler = new NowPlayingScrobbler({name: 'test', options: {nowPlaying: ['btest', 'atest']}}); + await using npScrobbler = new NowPlayingScrobbler({name: 'test', options: {nowPlaying: ['btest', 'atest']}}); await npScrobbler.initialize(); const a = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -931,7 +935,7 @@ describe('Now Playing', function() { it('Does not report if source is not in user config', async function () { - const npScrobbler = new NowPlayingScrobbler({name: 'test', options: {nowPlaying: ['btest', 'atest']}}); + await using npScrobbler = new NowPlayingScrobbler({name: 'test', options: {nowPlaying: ['btest', 'atest']}}); await npScrobbler.initialize(); const c = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -949,7 +953,7 @@ describe('Now Playing', function() { it('Should update if no existing Now Playing', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const res = npScrobbler.shouldUpdatePlayingNow(generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})})); @@ -958,7 +962,7 @@ describe('Now Playing', function() { it('Should update if previous Now Playing matches updated and last updated diff is greater than upper limit', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const lastUpdate = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -971,7 +975,7 @@ describe('Now Playing', function() { it('Should NOT update if previous Now Playing matches updated and last updated diff is less than upper limit', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const lastUpdate = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -984,7 +988,7 @@ describe('Now Playing', function() { it('Should update if previous Now Playing does NOT match updated and last updated diff is greater than lower limit', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const lastUpdate = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -997,7 +1001,7 @@ describe('Now Playing', function() { it('Should NOT update if previous Now Playing does NOT match updated and last updated diff is less than than lower limit', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); await npScrobbler.initialize(); const lastUpdate = generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}); @@ -1018,7 +1022,7 @@ describe('Now Playing', function() { it('Should update when no existing Now Playing', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); npScrobbler.nowPlayingTaskInterval = 10; await npScrobbler.initialize(); npScrobbler.scheduler.startById('pn_task'); @@ -1032,7 +1036,7 @@ describe('Now Playing', function() { it('Should update when updated does not match Now Playing', async function () { - const npScrobbler = new NowPlayingScrobbler(); + await using npScrobbler = new NowPlayingScrobbler(); npScrobbler.nowPlayingTaskInterval = 10; await npScrobbler.initialize(); npScrobbler.scheduler.startById('pn_task'); diff --git a/src/backend/tests/source/source.test.ts b/src/backend/tests/source/source.test.ts index ac72d1a5..97c9f0a1 100644 --- a/src/backend/tests/source/source.test.ts +++ b/src/backend/tests/source/source.test.ts @@ -27,8 +27,6 @@ const emitter = new EventEmitter(); const generateSource = () => { return new TestSource('spotify', 'test', {}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); } -let source: TestSource = generateSource(); - const generateMemorySource = (config: SourceConfig = {}) => { const s = new TestMemorySource('spotify', 'test', config, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); s.buildTransformRules(); @@ -45,11 +43,8 @@ const generateMemoryPositionalSource = (config: SourceConfig = {}) => { describe('Sources use transform plays correctly', function () { - beforeEach(function() { - source = generateSource(); - }); - it('Transforms play on preCompare', async function() { + await using source = generateSource(); source.config.options = { playTransform: { preCompare: { @@ -72,6 +67,7 @@ describe('Sources use transform plays correctly', function () { }); it('Transforms play on postCompare', async function() { + await using source = generateSource(); source.config.options = { playTransform: { postCompare: { @@ -100,6 +96,7 @@ describe('Sources use transform plays correctly', function () { }); it('Transforms play existing comparison', async function() { + await using source = generateSource(); source.config.options = { playTransform: { compare: { @@ -126,6 +123,7 @@ describe('Sources use transform plays correctly', function () { }); it('Transforms play candidate comparison', async function() { + await using source = generateSource(); source.config.options = { playTransform: { compare: { @@ -195,7 +193,7 @@ describe('Player Cleanup', function () { }); const cleanedUpDuration = async (generateSource: (config: SourceConfig) => MemorySource) => { - const source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); + await using source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, timestamp: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0); @@ -239,7 +237,7 @@ describe('Player Cleanup', function () { const noScrobbleRediscoveryOnActive = async (generateSource: (config: SourceConfig) => MemorySource) => { - const source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); + await using source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, timestamp: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0); @@ -307,7 +305,7 @@ describe('Player Cleanup', function () { const noScrobbleStale = async (generateSource: (config: SourceConfig) => MemorySource) => { - const source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); + await using source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); // if player incorrectly counted stale time then 30s of actual play + 20s of stale time > scrobble threshold of 50% of 90s @@ -352,7 +350,7 @@ describe('Player Cleanup', function () { const scrobbleRediscoveryOnActive = async (generateSource: (config: SourceConfig) => MemorySource) => { - const source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); + await using source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); // if player incorrectly counted stale time then 30s of actual play + 20s of stale time > scrobble threshold of 50% of 90s @@ -457,7 +455,7 @@ describe('Deezer Internal Source', function() { const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: true}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: true}); await source.discover([...normalizedPlays, interimPlay]); const discovered = await source.discover([fuzzyPlay]); @@ -470,7 +468,7 @@ describe('Deezer Internal Source', function() { const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: true}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: true}); await source.discover(normalizedPlays); const discovered = await source.discover([fuzzyPlay]); @@ -484,7 +482,7 @@ describe('Deezer Internal Source', function() { fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); const morePlays = normalizePlays([...normalizedPlays, fuzzyPlay, ...generatePlays(2)], {initialDate: firstPlayDate}); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: true}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: true}); const discovered = await source.discover(morePlays); expect(discovered.length).to.eq(morePlays.length); @@ -499,7 +497,7 @@ describe('Deezer Internal Source', function() { const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.discover([...normalizedPlays, interimPlay]); const discovered = await source.discover([fuzzyPlay]); @@ -513,7 +511,7 @@ describe('Deezer Internal Source', function() { const duringPlay = clone(targetPlay); duringPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration * 0.5, 's'); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.discover([...normalizedPlays, interimPlay]); const discovered = await source.discover([duringPlay]); @@ -527,7 +525,7 @@ describe('Deezer Internal Source', function() { const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration + 39, 's'); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.discover([...normalizedPlays, interimPlay]); const discovered = await source.discover([fuzzyPlay]); @@ -540,7 +538,7 @@ describe('Deezer Internal Source', function() { const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.discover(normalizedPlays); const discovered = await source.discover([fuzzyPlay]); @@ -554,7 +552,7 @@ describe('Deezer Internal Source', function() { fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); const morePlays = normalizePlays([...normalizedPlays, fuzzyPlay, ...generatePlays(2)], {initialDate: firstPlayDate}); - const source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); + await using source = generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); const discovered = await source.discover(morePlays); expect(discovered.length).to.eq(morePlays.length - 1); -- 2.51.2