[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.

POC for sqlite-based queue

FoxxMD (Apr 1, 2026, 9:05 PM UTC) 22fe9f24 de67879a

+224 -10
+3 -1
.dockerignore
··· 30 30 .env 31 31 *.env 32 32 .storybook 33 - .stories 33 + .stories 34 + *.db 35 + *.db-*
+3
.gitignore
··· 151 151 152 152 *storybook.log 153 153 storybook-static 154 + 155 + *.db 156 + *.db-*
+116 -8
package-lock.json
··· 78 78 "jsondiffpatch-react": "^1.0.8", 79 79 "keyv": "^5.5.0", 80 80 "kodi-api": "^0.2.1", 81 + "kysely-node-native-sqlite": "^1.1.0", 81 82 "lastfm-ts-api": "^2.6.0", 82 83 "merge-error-cause": "^5.0.2", 83 84 "mopidy": "^1.3.0", ··· 118 119 "undici": "7.24.6", 119 120 "vite-express": "^0.16.0", 120 121 "vlc-client": "^1.1.1", 122 + "workmatic": "github:FoxxMD/workmatic#testDist", 121 123 "xml2js": "0.6.2", 122 124 "youtubei.js": "^15.0.1" 123 125 }, ··· 143 145 "@types/jest": "^27.5.2", 144 146 "@types/jscodeshift": "^0.11.6", 145 147 "@types/mocha": "^9.1.0", 146 - "@types/node": "^20.19.2", 148 + "@types/node": "^24.1.0", 147 149 "@types/object-hash": "^3.0.0", 148 150 "@types/passport": "^1.0.12", 149 151 "@types/react": "^18.2.18", ··· 175 177 "with-local-tmp-dir": "^6.0.0" 176 178 }, 177 179 "engines": { 178 - "node": ">=20.19.2", 180 + "node": ">=24.14.0", 179 181 "npm": ">=11.12.1" 180 182 } 181 183 }, ··· 1014 1016 }, 1015 1017 "funding": { 1016 1018 "url": "https://github.com/chalk/strip-ansi?sponsor=1" 1019 + } 1020 + }, 1021 + "node_modules/@colors/colors": { 1022 + "version": "1.5.0", 1023 + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", 1024 + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", 1025 + "license": "MIT", 1026 + "optional": true, 1027 + "engines": { 1028 + "node": ">=0.1.90" 1017 1029 } 1018 1030 }, 1019 1031 "node_modules/@curvenote/ansi-to-react": { ··· 4274 4286 "license": "MIT" 4275 4287 }, 4276 4288 "node_modules/@types/node": { 4277 - "version": "20.19.37", 4289 + "version": "24.12.0", 4290 + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", 4291 + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", 4278 4292 "license": "MIT", 4279 4293 "dependencies": { 4280 - "undici-types": "~6.21.0" 4294 + "undici-types": "~7.16.0" 4281 4295 } 4282 4296 }, 4283 4297 "node_modules/@types/object-hash": { ··· 6495 6509 "node": ">=8" 6496 6510 } 6497 6511 }, 6512 + "node_modules/cli-table3": { 6513 + "version": "0.6.5", 6514 + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", 6515 + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", 6516 + "license": "MIT", 6517 + "dependencies": { 6518 + "string-width": "^4.2.0" 6519 + }, 6520 + "engines": { 6521 + "node": "10.* || >= 12.*" 6522 + }, 6523 + "optionalDependencies": { 6524 + "@colors/colors": "1.5.0" 6525 + } 6526 + }, 6527 + "node_modules/cli-table3/node_modules/emoji-regex": { 6528 + "version": "8.0.0", 6529 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 6530 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 6531 + "license": "MIT" 6532 + }, 6533 + "node_modules/cli-table3/node_modules/string-width": { 6534 + "version": "4.2.3", 6535 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 6536 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 6537 + "license": "MIT", 6538 + "dependencies": { 6539 + "emoji-regex": "^8.0.0", 6540 + "is-fullwidth-code-point": "^3.0.0", 6541 + "strip-ansi": "^6.0.1" 6542 + }, 6543 + "engines": { 6544 + "node": ">=8" 6545 + } 6546 + }, 6498 6547 "node_modules/cli-width": { 6499 6548 "version": "4.1.0", 6500 6549 "dev": true, ··· 7978 8027 } 7979 8028 }, 7980 8029 "node_modules/fastq": { 7981 - "version": "1.17.1", 7982 - "dev": true, 8030 + "version": "1.20.1", 8031 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", 8032 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", 7983 8033 "license": "ISC", 7984 8034 "dependencies": { 7985 8035 "reusify": "^1.0.4" ··· 9657 9707 "uuid": "^8.3.2" 9658 9708 } 9659 9709 }, 9710 + "node_modules/kysely": { 9711 + "version": "0.27.6", 9712 + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz", 9713 + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", 9714 + "license": "MIT", 9715 + "engines": { 9716 + "node": ">=14.0.0" 9717 + } 9718 + }, 9719 + "node_modules/kysely-node-native-sqlite": { 9720 + "version": "1.1.0", 9721 + "resolved": "https://registry.npmjs.org/kysely-node-native-sqlite/-/kysely-node-native-sqlite-1.1.0.tgz", 9722 + "integrity": "sha512-RUEslfInufxA0XN3bLBWwgBNUYC20Ac8RC4aijnYTqQq22ABAH5bVoDtLMr5Re/m94PA+d+XRcJBYB+sM+scxQ==", 9723 + "license": "ISC", 9724 + "engines": { 9725 + "node": "^22.5.0" 9726 + }, 9727 + "peerDependencies": { 9728 + "kysely": ">=0.27.0" 9729 + } 9730 + }, 9660 9731 "node_modules/lastfm-ts-api": { 9661 9732 "version": "2.6.0", 9662 9733 "license": "MIT", ··· 12118 12189 }, 12119 12190 "node_modules/reusify": { 12120 12191 "version": "1.0.4", 12121 - "dev": true, 12122 12192 "license": "MIT", 12123 12193 "engines": { 12124 12194 "iojs": ">=1.0.0", ··· 13648 13718 } 13649 13719 }, 13650 13720 "node_modules/undici-types": { 13651 - "version": "6.21.0", 13721 + "version": "7.16.0", 13722 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", 13723 + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", 13652 13724 "license": "MIT" 13653 13725 }, 13654 13726 "node_modules/unicorn-magic": { ··· 14427 14499 "version": "6.5.1", 14428 14500 "dev": true, 14429 14501 "license": "Apache-2.0" 14502 + }, 14503 + "node_modules/workmatic": { 14504 + "version": "1.0.5", 14505 + "resolved": "git+ssh://git@github.com/FoxxMD/workmatic.git#e8b5cecd3b925bf8b4434c7ca554773dfeeeca06", 14506 + "license": "MIT", 14507 + "dependencies": { 14508 + "cli-table3": "^0.6.5", 14509 + "fastq": "^1.18.0", 14510 + "kysely": "^0.27.5", 14511 + "kysely-node-native-sqlite": "^1.1.0", 14512 + "nanoid": "^5.0.9" 14513 + }, 14514 + "bin": { 14515 + "workmatic": "dist/cli.js" 14516 + }, 14517 + "engines": { 14518 + "node": ">=22.10.7" 14519 + } 14520 + }, 14521 + "node_modules/workmatic/node_modules/nanoid": { 14522 + "version": "5.1.7", 14523 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", 14524 + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", 14525 + "funding": [ 14526 + { 14527 + "type": "github", 14528 + "url": "https://github.com/sponsors/ai" 14529 + } 14530 + ], 14531 + "license": "MIT", 14532 + "bin": { 14533 + "nanoid": "bin/nanoid.js" 14534 + }, 14535 + "engines": { 14536 + "node": "^18 || >=20" 14537 + } 14430 14538 }, 14431 14539 "node_modules/wrap-ansi": { 14432 14540 "version": "8.1.0",
+3 -1
package.json
··· 116 116 "jsondiffpatch-react": "^1.0.8", 117 117 "keyv": "^5.5.0", 118 118 "kodi-api": "^0.2.1", 119 + "kysely-node-native-sqlite": "^1.1.0", 119 120 "lastfm-ts-api": "^2.6.0", 120 121 "merge-error-cause": "^5.0.2", 121 122 "mopidy": "^1.3.0", ··· 156 157 "undici": "7.24.6", 157 158 "vite-express": "^0.16.0", 158 159 "vlc-client": "^1.1.1", 160 + "workmatic": "github:FoxxMD/workmatic#testDist", 159 161 "xml2js": "0.6.2", 160 162 "youtubei.js": "^15.0.1" 161 163 }, ··· 181 183 "@types/jest": "^27.5.2", 182 184 "@types/jscodeshift": "^0.11.6", 183 185 "@types/mocha": "^9.1.0", 184 - "@types/node": "^20.19.2", 186 + "@types/node": "^24.1.0", 185 187 "@types/object-hash": "^3.0.0", 186 188 "@types/passport": "^1.0.12", 187 189 "@types/react": "^18.2.18",
+60
patches/kysely-node-native-sqlite+1.1.0.patch
··· 1 + diff --git a/node_modules/kysely-node-native-sqlite/dist/index.cjs b/node_modules/kysely-node-native-sqlite/dist/index.cjs 2 + index 1015da7..0679a95 100644 3 + --- a/node_modules/kysely-node-native-sqlite/dist/index.cjs 4 + +++ b/node_modules/kysely-node-native-sqlite/dist/index.cjs 5 + @@ -55,7 +55,11 @@ var import_node_sqlite = require("node:sqlite"); 6 + var NodeNativeSqliteConnection = class { 7 + #db; 8 + constructor(...args) { 9 + - this.#db = new import_node_sqlite.DatabaseSync(...args); 10 + + if (args[0] instanceof import_node_sqlite.DatabaseSync) { 11 + + this.#db = args[0]; 12 + + } else { 13 + + this.#db = new import_node_sqlite.DatabaseSync(...args); 14 + + } 15 + } 16 + [Symbol.dispose]() { 17 + this.#db.close(); 18 + diff --git a/node_modules/kysely-node-native-sqlite/dist/index.d.cts b/node_modules/kysely-node-native-sqlite/dist/index.d.cts 19 + index 72366bb..32e8464 100644 20 + --- a/node_modules/kysely-node-native-sqlite/dist/index.d.cts 21 + +++ b/node_modules/kysely-node-native-sqlite/dist/index.d.cts 22 + @@ -3,7 +3,7 @@ import { Dialect, SqliteAdapter, Driver, Kysely, DatabaseIntrospector, SqliteQue 23 + 24 + declare class NodeNativeSqliteDialect implements Dialect { 25 + #private; 26 + - constructor(...args: ConstructorParameters<typeof DatabaseSync>); 27 + + constructor(...args: ConstructorParameters<typeof DatabaseSync> | [DatabaseSync]); 28 + createAdapter(): SqliteAdapter; 29 + createDriver(): Driver; 30 + createIntrospector(db: Kysely<unknown>): DatabaseIntrospector; 31 + diff --git a/node_modules/kysely-node-native-sqlite/dist/index.d.ts b/node_modules/kysely-node-native-sqlite/dist/index.d.ts 32 + index 72366bb..32e8464 100644 33 + --- a/node_modules/kysely-node-native-sqlite/dist/index.d.ts 34 + +++ b/node_modules/kysely-node-native-sqlite/dist/index.d.ts 35 + @@ -3,7 +3,7 @@ import { Dialect, SqliteAdapter, Driver, Kysely, DatabaseIntrospector, SqliteQue 36 + 37 + declare class NodeNativeSqliteDialect implements Dialect { 38 + #private; 39 + - constructor(...args: ConstructorParameters<typeof DatabaseSync>); 40 + + constructor(...args: ConstructorParameters<typeof DatabaseSync> | [DatabaseSync]); 41 + createAdapter(): SqliteAdapter; 42 + createDriver(): Driver; 43 + createIntrospector(db: Kysely<unknown>): DatabaseIntrospector; 44 + diff --git a/node_modules/kysely-node-native-sqlite/dist/index.js b/node_modules/kysely-node-native-sqlite/dist/index.js 45 + index 8c2b8cf..17b7eb1 100644 46 + --- a/node_modules/kysely-node-native-sqlite/dist/index.js 47 + +++ b/node_modules/kysely-node-native-sqlite/dist/index.js 48 + @@ -32,7 +32,11 @@ import { DatabaseSync } from "node:sqlite"; 49 + var NodeNativeSqliteConnection = class { 50 + #db; 51 + constructor(...args) { 52 + - this.#db = new DatabaseSync(...args); 53 + + if (args[0] instanceof DatabaseSync) { 54 + + this.#db = args[0]; 55 + + } else { 56 + + this.#db = new DatabaseSync(...args); 57 + + } 58 + } 59 + [Symbol.dispose]() { 60 + this.#db.close();
+20
src/backend/common/database/JobDb.ts
··· 1 + import { createDatabase, WorkmaticDb } from 'workmatic'; 2 + import { DatabaseSync } from 'node:sqlite'; 3 + import { configDir } from '../index.js'; 4 + import * as path from 'path'; 5 + 6 + export const getJobsDb = (dbName: string = ':memory:'): [WorkmaticDb, DatabaseSync] => { 7 + let dbPath: string; 8 + if(dbName === ':memory') { 9 + dbPath = dbName; 10 + } else { 11 + dbPath = path.resolve(configDir, `${dbName}.db`); 12 + } 13 + 14 + const database = new DatabaseSync(dbPath); 15 + const db = createDatabase({ db: database }); 16 + database.exec(`CREATE INDEX IF NOT EXISTS playdate ON workmatic_jobs((payload->>'$.data.playDate'))`); 17 + database.exec(`CREATE INDEX IF NOT EXISTS seenAt ON workmatic_jobs((payload->>'$.meta.seenAt'))`); 18 + 19 + return [db, database]; 20 + }
+19
src/backend/tests/queue/queue.test.ts
··· 1 + import chai, { assert, expect } from 'chai'; 2 + import asPromised from 'chai-as-promised'; 3 + import { after, before, describe, it } from 'mocha'; 4 + import { createDatabase, createClient, createWorker } from 'workmatic'; 5 + import { DatabaseSync } from 'node:sqlite'; 6 + import { getJobsDb } from '../../common/database/JobDb.js'; 7 + import { generatePlay } from '../../../core/PlayTestUtils.js'; 8 + 9 + chai.use(asPromised); 10 + 11 + it('does stuff', async function () { 12 + 13 + const [db, nodeDb] = getJobsDb(':memory:'); 14 + const client = createClient({ db, queue: 'scrobble' }); 15 + 16 + const { id } = await client.add(generatePlay()); 17 + console.log(`Job created: ${id}`); 18 + expect(true).to.eq(true); 19 + });