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.
14 kB · 325 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325import { expect } from 'chai';import { components, plays } from '../../common/database/drizzle/schema/schema.ts';import dayjs from 'dayjs';import { generatePlay } from '../../../core/tests/utils/PlayTestUtils.ts';import { fixtureCreateComponent, fixtureCreatePlay } from '../utils/databaseFixtures.ts';import { DrizzlePlayRepository, type RepositoryCreatePlayOpts } from '../../common/database/drizzle/repositories/PlayRepository.ts';import { generateRandomObj } from '../../../core/tests/utils/fixtures.ts';import { generateArray } from '../../../core/DataUtils.ts';import { objectsEqual } from '../../utils/DataUtils.ts';import { sql } from 'drizzle-orm';import clone from 'clone';import { transientDb } from '../utils/TransientTestUtils.ts';
describe('Repository Operations', function () {
it('creates Plays and inputs', async function () {
const db = await transientDb();
const component = await db.insert(components).values(fixtureCreateComponent()).returning();
const repo = new DrizzlePlayRepository(db, { componentId: component[0].id });
const numPlays = 3;
const playData = generateArray<RepositoryCreatePlayOpts>(numPlays, () => ({ ...fixtureCreatePlay(), state: 'queued', input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }))
const rows = await repo.createPlays(playData); expect(rows).length(numPlays); const fullPlays = await db.query.plays.findMany({ with: { input: true } }); fullPlays.forEach((play, index) => { const ref = playData[index];
expect(play.play.data.track).eq(ref.play.data.track); expect(play.input).to.not.undefined; expect(objectsEqual(play.input.data, ref.input.data)).is.true; })
});
it('finds Plays by state', async function () {
const db = await transientDb(); const component = await db.insert(components).values(fixtureCreateComponent()).returning();
const repo = new DrizzlePlayRepository(db, { componentId: component[0].id });
const numPlays = 3;
const playData = generateArray<RepositoryCreatePlayOpts>(numPlays, () => ({ ...fixtureCreatePlay(), state: 'queued', input: { data: generateRandomObj(undefined, { allowUndefined: false }) } })); const discovered = { ...fixtureCreatePlay(), state: 'discovered' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }; playData.push(discovered)
await repo.createPlays(playData);
const plays = await repo.findPlays({ state: ['discovered'] }); expect(plays).length(1); expect(plays[0].play.data.track).eq(discovered.play.data.track); });
it('finds Plays by date range', async function () {
const db = await transientDb();
const component = await db.insert(components).values(fixtureCreateComponent()).returning();
const repo = new DrizzlePlayRepository(db, { componentId: component[0].id });
const playData: RepositoryCreatePlayOpts[] = [ { ...fixtureCreatePlay({ play: generatePlay({ playDate: dayjs().subtract(2, 'm') }) }), state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay({ play: generatePlay({ playDate: dayjs().subtract(6, 'm') }) }), state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay({ play: generatePlay({ playDate: dayjs().subtract(8, 'm') }) }), state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay({ play: generatePlay({ playDate: dayjs().subtract(10, 'm') }) }), state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, ]
await repo.createPlays(playData);
const newerPlays = await repo.findPlays({ playedAt: { type: 'gt', date: dayjs().subtract(3, 'm') } }); expect(newerPlays).length(1); expect(newerPlays[0].play.data.track).eq(playData[0].play.data.track);
const olderPlays = await repo.findPlays({ playedAt: { type: 'lt', date: dayjs().subtract(6, 'm').subtract(5, 's') } }); expect(olderPlays).length(2); expect(olderPlays[0].play.data.track).eq(playData[2].play.data.track); expect(olderPlays[1].play.data.track).eq(playData[3].play.data.track);
const bwPlays = await repo.findPlays({ playedAt: { type: 'between', range: [dayjs().subtract(9, 'm'), dayjs().subtract(3, 'm')] } }); expect(bwPlays).length(2); expect(bwPlays[0].play.data.track).eq(playData[1].play.data.track); expect(bwPlays[1].play.data.track).eq(playData[2].play.data.track); });
it('finds Plays by component', async function () {
const db = await transientDb();
const component1 = await db.insert(components).values(fixtureCreateComponent()).returning(); const component2 = await db.insert(components).values(fixtureCreateComponent({ uid: 'test2', name: 'jelly2' })).returning(); const component3 = await db.insert(components).values(fixtureCreateComponent({ uid: 'test3', name: 'jelly3' })).returning();
const repo = new DrizzlePlayRepository(db);
const playData: RepositoryCreatePlayOpts[] = [ { ...fixtureCreatePlay(), componentId: component1[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay(), componentId: component3[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay(), componentId: component3[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } } ]
await repo.createPlays(playData);
const plays = await repo.findPlays({ componentId: component3[0].id }); expect(plays).length(2); expect(plays[0].play.data.track).eq(playData[1].play.data.track); expect(plays[1].play.data.track).eq(playData[2].play.data.track);
const plays1 = await repo.findPlays({ componentId: component1[0].id }); expect(plays1).length(1); expect(plays1[0].play.data.track).eq(playData[0].play.data.track);
const noPlays = await repo.findPlays({ componentId: component2[0].id }); expect(noPlays).length(0); });
it('finds purgable Plays', async function () {
const db = await transientDb();
const component1 = await db.insert(components).values(fixtureCreateComponent()).returning(); const component2 = await db.insert(components).values(fixtureCreateComponent({ uid: 'test2', name: 'jelly2' })).returning();
const repo = new DrizzlePlayRepository(db);
const playData: RepositoryCreatePlayOpts[] = [ { ...fixtureCreatePlay({ play: generatePlay({}, { seenAt: dayjs().subtract(25, 'h') }) }), componentId: component1[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay({ play: generatePlay({}, { seenAt: dayjs().subtract(26, 'h') }) }), componentId: component1[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay({ play: generatePlay({}, { seenAt: dayjs().subtract(26, 'h') }) }), componentId: component2[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, { ...fixtureCreatePlay({ play: generatePlay({}, { seenAt: dayjs().subtract(25, 'h').subtract(1, 'm') }) }), componentId: component1[0].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, ]
const initialPlays = await repo.createPlays(playData);
const childPlays = await repo.createPlays([ { ...fixtureCreatePlay({ play: generatePlay({}, { seenAt: dayjs().subtract(25, 'h') }) }), componentId: component2[0].id, parentId: initialPlays[1].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }, ])
// does not return newer plays expect((await repo.findPurgablePlayIds(dayjs().subtract(27, 'h'), { componentId: 1 }))).length(0);
const pPlays = await repo.findPurgablePlayIds(dayjs().subtract(24, 'h'), { componentId: 1 }); // only returns plays that do not have children expect(pPlays).length(2); expect(pPlays[0]).to.eq(initialPlays[0].id); expect(pPlays[1]).to.eq(initialPlays[3].id);
// only finds plays by component // and allows purging if they have parent id const p2Plays = await repo.findPurgablePlayIds(dayjs().subtract(23, 'h'), { componentId: 2 }); expect(p2Plays).length(2); expect(p2Plays[0]).to.eq(initialPlays[2].id); expect(p2Plays[1]).to.eq(childPlays[0].id); });
describe('checkExisting', function () {
it('finds a Play by parent id and matching parent hash', async function () {
const db = await transientDb();
// Component A - "source" plays const componentA = await db.insert(components).values(fixtureCreateComponent()).returning(); const repoA = new DrizzlePlayRepository(db, { componentId: componentA[0].id });
const playDataA = generateArray<RepositoryCreatePlayOpts>(3, () => ({ ...fixtureCreatePlay(), state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }));
const rowsA = await repoA.createPlays(playDataA);
// a clone of one of the previously created plays (PlayObject only, not the db row) const clonedPlay = clone(playDataA[1].play);
// Component B - a "client" play that is a child of the Component A play the clone is based off of. // Its own content (and therefore its own playHash) differs from the source play - it can only be // matched back to the incoming clone through the parent relationship + the parent's frozen hash. const componentB = await db.insert(components).values(fixtureCreateComponent({ uid: 'componentB', name: 'componentB' })).returning(); const repoB = new DrizzlePlayRepository(db, { componentId: componentB[0].id });
const childPlay = clone(playDataA[1].play); childPlay.data.track = `${childPlay.data.track} (transformed)`;
const rowsB = await repoB.createPlays([{ ...fixtureCreatePlay({ play: childPlay }), parentId: rowsA[1].id, state: 'queued' as const, input: { data: generateRandomObj(undefined, { allowUndefined: false }) } }]);
// sanity: without a parentId the transformed client play cannot be matched from the source clone expect(await repoB.checkExisting(clonedPlay)).to.be.undefined; // sanity: passing the id of a different parent play does not match expect(await repoB.checkExisting(clonedPlay, { parentId: rowsA[0].id })).to.be.undefined;
const existing = await repoB.checkExisting(clonedPlay, { parentId: rowsA[1].id });
expect(existing, 'checkExisting should return the client play whose parent the clone is based off of').to.not.be.undefined; expect(existing.id).eq(rowsB[0].id); expect(existing.play.data.track).eq(childPlay.data.track); });
});
it('Get json property from play', async function () {
const db = await transientDb();
try {
const component = await db.insert(components).values(fixtureCreateComponent()).returning();
const playRows = await db.insert(plays).values([ fixtureCreatePlay({ componentId: component[0].id, play: generatePlay({}, { source: 'test1' }) }), fixtureCreatePlay({ componentId: component[0].id, play: generatePlay({}, { source: 'test2' }) }) ]).returning();
// https://github.com/drizzle-team/drizzle-orm/discussions/938#discussioncomment-6542336 const result = await db.query.plays.findMany({ where: { AND: [ { OR: [ { RAW: (p) => sql`lower(json_extract(${p.play}, '$.data.track')) LIKE '%'|| ${playRows[0].play.data.track.substring(0, 5).toLocaleLowerCase()} || '%'` } ] } ] } });
// await db.select().from(plays).where( // sql`json_extract(${plays.play}, '$.meta.source') = 'test1'` // );
expect(result).length(1); expect(result[0].play.meta.source).eq('test1');
} catch (e) { throw e; } db.$client.close(); });
});