Something went wrong. Try again.
[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
Something went wrong. Try again.
31 kB · 617 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617import { loggerTest } from "@foxxmd/logging";import chai, { expect } from 'chai';import asPromised from 'chai-as-promised';import { describe, it } from 'mocha';import pEvent from "p-event";import clone from 'clone';import type {PlayObject} from "../../../core/Atomic.ts";import { generatePlay, generatePlayerStateData, generatePlays, normalizePlays } from "../../../core/tests/utils/PlayTestUtils.ts";import { TestMemoryPositionalSource, TestMemorySource, TestSource } from "./TestSource.ts";import spotifyPayload from '../plays/spotifyCurrentPlaybackState.json' with { type: "json" };import SpotifySource from "../../sources/SpotifySource.ts";import MockDate from 'mockdate';import dayjs from "dayjs";import { REPORTED_PLAYER_STATUSES } from '../../../core/Atomic.ts';import type {SourceConfig} from "../../common/infrastructure/config/source/sources.ts";import type MemorySource from "../../sources/MemorySource.ts";import { RT_TICK_DEFAULT, setRtTick } from "../../sources/PlayerState/RealtimePlayer.ts";import { sleep } from "../../utils.ts";import DeezerInternalSource from "../../sources/DeezerInternalSource.ts";import type {DeezerInternalSourceOptions} from "../../common/infrastructure/config/source/deezer.ts";import { artistCreditsToNames } from "../../../core/StringUtils.ts";import type { MarkOptional } from "ts-essentials";import { WildcardEmitter } from "../../common/WildcardEmitter.ts";import type { MSBackendEventMap } from "../../common/infrastructure/MSBackendEventMap.ts";
chai.use(asPromised);
const emitter = new WildcardEmitter<MSBackendEventMap>();const generateSource = async () => { const source = new TestSource('spotify', 'test-basic', {id: `test-${Date.now()}`}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await source.initialize(); await source.initTasks(); return source;}const generateMemorySource = async (config: MarkOptional<SourceConfig, 'id'> = {}) => { const s = new TestMemorySource('spotify', 'test-memory', {id: `test-${Date.now()}`, ...config}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await s.initialize(); // s.buildTransformRules(); s.scheduler.stop(); return s;}
const generateMemoryPositionalSource = async (config: MarkOptional<SourceConfig, 'id'> = {}) => { const s = new TestMemoryPositionalSource('spotify', 'test-positional', {id: `test-${Date.now()}`, ...config}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await s.initialize(); //s.buildTransformRules(); s.scheduler.stop(); return s;}
describe('Sources use transform plays correctly', function () {
it('Transforms play on preCompare', async function() { await using source = await generateSource(); source.config.options = { playTransform: { preCompare: { type: 'user', title: [ { search: 'cool', replace: 'fun' } ] } } }; await source.buildTransformRules(); const newScrobble = generatePlay({ track: 'my cool track' }); await source.queuePlay([newScrobble]); await sleep(3); const discovered = await source.getRecentlyDiscoveredPlays(); expect(discovered.length).eq(1); expect(discovered[0].data.track).is.eq('my fun track'); });
it('Transforms play on postCompare', async function() { await using source = await generateSource(); source.config.options = { playTransform: { postCompare: { type: 'user', title: [ { search: 'cool', replace: 'fun' } ] } } }; await source.buildTransformRules(); const newScrobble = generatePlay({ track: 'my cool track' });
const pAwaiter = pEvent(source.emitter, 'discoveredToScrobble') as Promise<MSBackendEventMap['discoveredToScrobble'][0]>;
const promiseRes = await Promise.all([ source.queuePlay([newScrobble]), sleep(3), pAwaiter ]); //await source.queuePlay([newScrobble]); //await sleep(3); const discovered = await source.getRecentlyDiscoveredPlays(); expect(discovered.length).eq(1); expect(discovered[0].data.track).is.eq('my cool track');
//const pAwaiter = pEvent(source.emitter, 'discoveredToScrobble') as Promise<MSBackendEventMap['discoveredToScrobble'][0]>; //source.handle(discovered); const e = promiseRes[2]; expect(e).is.not.undefined; const res: PlayObject[] = !Array.isArray(e.data.data) ? [e.data.data] : e.data.data; expect(res.length).is.eq(1); expect(res[0].data.track).is.eq('my fun track'); });
// TODO need to rework these
// it('Transforms play existing comparison', async function() { // await using source = await generateSource(); // source.config.options = { // playTransform: { // compare: { // existing: { // type: 'user', // title: [ // { // search: 'hugely cool and very different track', // replace: 'fun' // } // ] // } // } // } // }; // source.buildTransformRules(); // const newScrobble = generatePlay({ // track: 'my hugely cool and very different track title', // }); // const discovered = await source.discover([newScrobble]) // expect(discovered.length).eq(1); // expect(discovered[0].data.track).is.eq('my hugely cool and very different track title');
// expect((await source.discover([newScrobble])).length).is.eq(1); // });
// it('Transforms play candidate comparison', async function() { // await using source = await generateSource(); // source.config.options = { // playTransform: { // compare: { // candidate: { // type: 'user', // title: [ // { // search: 'hugely cool and very different track', // replace: 'fun' // } // ] // } // } // } // }; // source.buildTransformRules(); // const newScrobble = generatePlay({ // track: 'my hugely cool and very different track title', // }); // const discovered = await source.discover([newScrobble]) // expect(discovered.length).eq(1); // expect(discovered[0].data.track).is.eq('my hugely cool and very different track title');
// expect((await source.discover([newScrobble])).length).is.eq(1); // });})
describe('Sources correctly parse incoming payloads', function () {
it('Spotify parses payload with no album artists correctly', function() { const noAAPayload = clone(spotifyPayload) noAAPayload.item.album.artists = []; const play = SpotifySource.formatPlayObj(noAAPayload as SpotifyApi.CurrentPlaybackResponse); expect(play.data.track).eq('The Sandpits Of Zonhoven'); expect(play.data.album).eq('Bloodbags And Downtube Shifters'); expect(artistCreditsToNames(play.data.artists)).eql(['Dubmood', 'MASTER BOOT RECORD']); expect(play.data.albumArtists).to.be.empty; });
it('Spotify parses payload with different album artists correctly', function() { const play = SpotifySource.formatPlayObj(spotifyPayload as SpotifyApi.CurrentPlaybackResponse); expect(play.data.track).eq('The Sandpits Of Zonhoven'); expect(play.data.album).eq('Bloodbags And Downtube Shifters'); expect(artistCreditsToNames(play.data.artists)).eql(['Dubmood', 'MASTER BOOT RECORD']); expect(artistCreditsToNames(play.data.albumArtists)).eql(['Dubmood']); });
it('Spotify parses payload with identical album artists correctly', function() { const identicalArtistsPayload = clone(spotifyPayload) identicalArtistsPayload.item.album.artists = identicalArtistsPayload.item.artists; const identicalArtistsPlay = SpotifySource.formatPlayObj(identicalArtistsPayload as SpotifyApi.CurrentPlaybackResponse); expect(identicalArtistsPlay.data.track).eq('The Sandpits Of Zonhoven'); expect(identicalArtistsPlay.data.album).eq('Bloodbags And Downtube Shifters'); expect(artistCreditsToNames(identicalArtistsPlay.data.artists)).eql(['Dubmood', 'MASTER BOOT RECORD']); expect(artistCreditsToNames(identicalArtistsPlay.data.albumArtists)).to.be.empty; });});
describe('Player Cleanup', function () {
this.afterEach(() => { MockDate.reset(); setRtTick(RT_TICK_DEFAULT); }); this.beforeEach(() => { setRtTick(1); });
const cleanedUpDuration = async (generateSource: (config: MarkOptional<SourceConfig, 'id'>) => Promise<MemorySource>) => { await using source = await generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0);
let position = 0; let timeSince = 0;
// simulate polling playing source for 30 seconds, 10 second interval for(let i = 0; i < 3; i++) { position += 10; timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); }
// simulate polling another 20 seconds without any updates from the Source for(let i = 0; i < 2; i++) { timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); expect((await source.processRecentPlays([])).length).to.be.eq(0); }
MockDate.set(initialDate.add(timeSince + 2, 'seconds').toDate()); await sleep(1); const discoveredPlays = await source.processRecentPlays([]); // cleanup should discover stale play expect(discoveredPlays.length).to.be.eq(1); expect(discoveredPlays[0].data.listenedFor).closeTo(30, 2); }
it('Discovers cleaned up Play with correct duration (Non Positional Source)', async function () { await cleanedUpDuration(generateMemorySource); });
it('Discovers cleaned up Play with correct duration (Positional Source)', async function () { await cleanedUpDuration(generateMemoryPositionalSource); });
const noScrobbleRediscoveryOnActive = async (generateSource: (config: MarkOptional<SourceConfig, 'id'>) => Promise<MemorySource>) => {
await using source = await generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0);
let position = 0; let timeSince = 0;
// simulate polling playing source for 30 seconds, 10 second interval for(let i = 0; i < 3; i++) { position += 10; timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); }
// simulate polling another 20 seconds without any updates from the Source for(let i = 0; i < 2; i++) { timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); expect((await source.processRecentPlays([])).length).to.be.eq(0); }
timeSince += 2;
MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); const discoveredPlays = await source.processRecentPlays([]); // cleanup should discover stale play expect(discoveredPlays.length).to.be.eq(1); expect(discoveredPlays[0].data.listenedFor).closeTo(30, 2);
timeSince += 10;
position -= 9; // simulate polling another 20 seconds with active source again for(let i = 0; i < 2; i++) { timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); }
timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); // new Play const advancedState = generatePlayerStateData({stateUpdatedAt: dayjs(), position: 0, status: REPORTED_PLAYER_STATUSES.playing}); // should not return play because it has only been played for ~20 seconds, less than 50% of duration const plays = await source.processRecentPlays([advancedState]) expect(plays.length).to.be.eq(0); }
it('Does not discover same Play after becoming active again (Non Positional Source)', async function () { await noScrobbleRediscoveryOnActive(generateMemorySource); });
it('Does not discover same Play after becoming active again (Positional Source)', async function () { await noScrobbleRediscoveryOnActive(generateMemoryPositionalSource); });
const noScrobbleStale = async (generateSource: (config: MarkOptional<SourceConfig,'id'>) => Promise<MemorySource>) => {
await using source = await 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 const initialState = generatePlayerStateData({position: 0, playData: {duration: 90}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0);
let position = 0; let timeSince = 0;
// simulate polling playing source for 30 seconds, 10 second interval for(let i = 0; i < 3; i++) { position += 10; timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: initialDate, position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); }
// simulate polling another 20 seconds without any updates from the Source for(let i = 0; i < 2; i++) { timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); expect((await source.processRecentPlays([])).length).to.be.eq(0); }
MockDate.set(initialDate.add(timeSince + 2, 'seconds').toDate()); const discoveredPlays = await source.processRecentPlays([]); // cleanup should not discover stale play expect(discoveredPlays.length).to.be.eq(0);
}
it('Does not discover cleaned up Play that did not meet threshold (Non Positional Source)', async function () { await noScrobbleStale(generateMemorySource); });
it('Does not discover cleaned up Play that did not meet threshold (Positional Source)', async function () { await noScrobbleStale(generateMemoryPositionalSource); });
const scrobbleRediscoveryOnActive = async (generateSource: (config: MarkOptional<SourceConfig, 'id'>) => Promise<MemorySource>) => {
await using source = await 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 const initialState = generatePlayerStateData({position: 0, playData: {duration: 90}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0);
let position = 0; let timeSince = 0;
// simulate polling playing source for 30 seconds, 10 second interval for(let i = 0; i < 3; i++) { position += 10; timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); }
// simulate polling another 20 seconds without any updates from the Source for(let i = 0; i < 2; i++) { timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); expect((await source.processRecentPlays([])).length).to.be.eq(0); }
timeSince += 2;
MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); const discoveredPlays = await source.processRecentPlays([]); // cleanup should not discover stale play expect(discoveredPlays.length).to.be.eq(0);
// so that loop starts 1 second after "paused" position position -= 9;
// simulate ~50 seconds of listening (enough for scrobble) MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); for(let i = 0; i < 5; i++) { position += 10; timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); }
timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); // new Play const advancedState = generatePlayerStateData({stateUpdatedAt: dayjs(), position: 0, status: REPORTED_PLAYER_STATUSES.playing}); // should return discovered play with ~90 seconds of duration const plays = await source.processRecentPlays([advancedState]) expect(plays.length).to.be.eq(1); expect(plays[0].data.duration).to.be.closeTo(90, 2);
}
it('Does discover Play after becoming active again (Non Positional Source)', async function () { await scrobbleRediscoveryOnActive(generateMemorySource); });
it('Does discover Play after becoming active again (Positional Source)', async function () { await scrobbleRediscoveryOnActive(generateMemoryPositionalSource); });});
class DeezerTestSource extends DeezerInternalSource { protected async doCheckConnection(): Promise<true | string | undefined> { return; } doAuthentication = async () => { return true; }}
const generateDeezerSource = async (options: DeezerInternalSourceOptions = {}) => { const source = new DeezerTestSource('test', {id: `test-${Date.now()}`,data: {arl: 'test'}, options}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); source.queueIdleMs = 2; source.queueConcurrency = 1; source.stopPollingWaitInterval = 10; await source.initialize(); await source.startDiscoveryQueue(); return source;}const firstPlayDate = dayjs().subtract(1, 'hour');const normalizedPlays = normalizePlays(generatePlays(6), {initialDate: firstPlayDate});const lastPlay = normalizedPlays[normalizePlays.length - 1];
describe('Deezer Internal Source', function() {
describe('When fuzzyDiscoveryIgnore is not defined or false', function () {
it('discovers fuzzy play', async function() { const interimPlay = generatePlay({playDate: lastPlay.data.playDate.add(15, 's'), duration: 80}); const targetPlay = normalizedPlays[normalizedPlays.length - 2] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's');
await using source = await generateDeezerSource(); const queued = await source.queuePlay([...normalizedPlays, interimPlay]); expect(queued).length(normalizedPlays.length + 1); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 1); await source.queuePlay([fuzzyPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 2); }); });
describe('When fuzzyDiscoveryIgnore is true', function () {
it('does not discover fuzzy play with interim plays', async function() { const interimPlay = generatePlay({playDate: lastPlay.data.playDate.add(15, 's'), duration: 80}); const targetPlay = normalizedPlays[normalizedPlays.length - 2] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's');
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: true}); const queued = await source.queuePlay([...normalizedPlays, interimPlay]); expect(queued).length(normalizedPlays.length + 1); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); await source.queuePlay([fuzzyPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 1); });
it('discovers fuzzy play when it is the last play ', async function() { const targetPlay = normalizedPlays[normalizedPlays.length - 1] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's');
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: true}); const queued = await source.queuePlay(normalizedPlays); expect(queued).length(normalizedPlays.length);
await source.queuePlay([fuzzyPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 1); });
it('discovers fuzzy play when it is played consecutively', async function() { const targetPlay = normalizedPlays[normalizedPlays.length - 1] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); const morePlays = normalizePlays([...normalizedPlays, fuzzyPlay, ...generatePlays(2)], {initialDate: firstPlayDate});
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: false}); const queued = await source.queuePlay(morePlays); expect(queued).length(morePlays.length); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(morePlays.length); }); });
describe('When fuzzyDiscoveryIgnore is aggressive', function () {
it('does not discover fuzzy play with interim plays', async function() { const interimPlay = generatePlay({playDate: lastPlay.data.playDate.add(15, 's'), duration: 80}); const targetPlay = normalizedPlays[normalizedPlays.length - 2] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's');
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); const queued = await source.queuePlay([...normalizedPlays, interimPlay]); expect(queued).length(normalizedPlays.length + 1); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]);
await source.queuePlay([fuzzyPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]);
expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 1); });
it('does not discover play found during duration of previous', async function() { const interimPlay = generatePlay({playDate: lastPlay.data.playDate.add(15, 's'), duration: 80}); const targetPlay = normalizedPlays[normalizedPlays.length - 2] const duringPlay = clone(targetPlay); duringPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration * 0.5, 's');
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); //await source.discover([...normalizedPlays, interimPlay]); await source.queuePlay([...normalizedPlays, interimPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); await source.queuePlay([duringPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]);
expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 1) });
it('does not discover fuzzy play with delay of up to 40 seconds', async function() { const interimPlay = generatePlay({playDate: lastPlay.data.playDate.add(15, 's'), duration: 80}); const targetPlay = normalizedPlays[normalizedPlays.length - 2] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration + 39, 's');
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.queuePlay([...normalizedPlays, interimPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); await source.queuePlay([fuzzyPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]);
expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length + 1) });
it('it does not discover fuzzy play when it is the last play ', async function() { const targetPlay = normalizedPlays[normalizedPlays.length - 1] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's');
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.queuePlay(normalizedPlays); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); await source.queuePlay([fuzzyPlay]); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(normalizedPlays.length); });
it('does not discover fuzzy play when it is played consecutively', async function() { const targetPlay = normalizedPlays[normalizedPlays.length - 1] const fuzzyPlay = clone(targetPlay); fuzzyPlay.data.playDate = targetPlay.data.playDate.add(targetPlay.data.duration, 's'); const morePlays = normalizePlays([...normalizedPlays, fuzzyPlay, ...generatePlays(2)], {initialDate: firstPlayDate});
await using source = await generateDeezerSource({fuzzyDiscoveryIgnore: 'aggressive'}); await source.queuePlay(morePlays); await Promise.race([pEvent(source.emitter, 'queueEmptied'), pEvent(source.emitter, 'discoveryQueueError')]); expect(await source.getRecentlyDiscoveredPlays()).length(morePlays.length - 1); });
});});