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

feat(database): Implement database structures needed for historical plays

FoxxMD (May 15, 2026, 5:03 PM UTC) 0d6d803d 396e4b92

+1144 -60
+1 -54
src/backend/common/database/drizzle/drizzleTypes.ts
··· 17 17 export type PlayInputSelect = typeof playInputs.$inferSelect; 18 18 19 19 export type PlaySelect = typeof plays.$inferSelect; 20 - export type PlaySelectRel = ModelWithRelations<typeof plays>; 21 20 export type PlaySelectWithQueueStates = GenericRelationResult<'plays', 'queueStates'>; 21 + export type PlayWith<K extends keyof TSchema['plays']["relations"]> = GenericRelationResult<'plays', K>; 22 22 export type PlayNew = typeof plays.$inferInsert; 23 23 24 24 ··· 42 42 43 43 export type CompareOp<T> = Pick<RelationFieldsFilterInternals<T>, 'gt' | 'gte' | 'eq' | 'lt' | 'lte' | 'ne'> 44 44 export type CompareOpKey<T> = keyof CompareOp<T>; 45 - 46 - 47 - 48 - /** 49 - * Based on https://github.com/drizzle-team/drizzle-orm/issues/695#issuecomment-3133969178 50 - */ 51 - 52 - // Helper type to find the tsName corresponding to a given dbName in TSchema 53 - type FindTsNameByDbName<TDbNameToFind extends string> = { 54 - [K in keyof TSchema]: TSchema[K] extends { 55 - // updated dbName -> name 56 - name: TDbNameToFind; 57 - } 58 - ? K 59 - : TDbNameToFind; 60 - }[keyof TSchema]; 61 - 62 - // Helper type to find the dbName corresponding to a given tsName in TSchema 63 - type FindDbNameByTsName<TTable extends Schema[keyof Schema]> = { 64 - [K in keyof Schema]: Schema[K] extends TTable ? K : never; 65 - }[keyof Schema]; 66 - 67 - /** 68 - * Utility type to infer the model type for a given table name from the schema. 69 - * Handles nested relations recursively. 70 - * Uses referencedTableName (dbName) and FindTsNameByDbName helper. 71 - */ 72 - export type ModelWithRelationsFromName< 73 - TTableName extends keyof TSchema, 74 - > = InferSelectModel<Schema[TTableName]> & { 75 - [K in keyof TSchema[TTableName]['relations']]?: TSchema[TTableName]['relations'][K] extends infer TRelation 76 - // updated referencedTableName -> targetTableName 77 - ? TRelation extends { targetTableName: infer TRefDbName extends string } 78 - ? FindTsNameByDbName<TRefDbName> extends infer TRefTsName extends 79 - keyof TSchema 80 - ? TRelation extends Many<any> 81 - ? ModelWithRelationsFromName<TRefTsName>[] 82 - : ModelWithRelationsFromName<TRefTsName> | null 83 - : never 84 - : never 85 - : never; 86 - }; 87 - 88 - /** 89 - * Utility type to infer the model type for a given table from the schema. 90 - * Handles nested relations recursively. 91 - * Uses referencedTableName (dbName) and FindDbNameByTsName helper. 92 - */ 93 - export type ModelWithRelations<TTable extends Schema[keyof Schema]> = 94 - FindDbNameByTsName<TTable> extends infer TTableName extends keyof TSchema 95 - ? ModelWithRelationsFromName<TTableName> 96 - : never; 97 - 98 45 99 46 // all relations are are now fully typed and optional 100 47 //type FullPlay = ModelWithRelations<typeof plays>;
+17
src/backend/common/database/drizzle/migrations/20260515170311_nostalgic_venom/migration.sql
··· 1 + CREATE TABLE `plays_historical` ( 2 + `id` integer PRIMARY KEY, 3 + `uid` text(200), 4 + `componentId` integer, 5 + `playedAt` number, 6 + `seenAt` number, 7 + `play` text NOT NULL, 8 + `playHash` text, 9 + `mbidIdentifier` text, 10 + CONSTRAINT `fk_plays_historical_componentId_components_id_fk` FOREIGN KEY (`componentId`) REFERENCES `components`(`id`) ON UPDATE CASCADE ON DELETE CASCADE 11 + ); 12 + --> statement-breakpoint 13 + ALTER TABLE `components` ADD `lastReadyAt` number;--> statement-breakpoint 14 + ALTER TABLE `components` ADD `lastActiveAt` number;--> statement-breakpoint 15 + CREATE INDEX `play_historical_component_id_idx` ON `plays_historical` (`componentId`);--> statement-breakpoint 16 + CREATE UNIQUE INDEX `play_historical_uid_idx` ON `plays_historical` (`uid`);--> statement-breakpoint 17 + CREATE INDEX `play_historical_playedAt_idx` ON `plays_historical` (`playedAt`);
+1094
src/backend/common/database/drizzle/migrations/20260515170311_nostalgic_venom/snapshot.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "sqlite", 4 + "id": "195d210b-d216-4980-8d74-fa3ac3abfcd2", 5 + "prevIds": [ 6 + "68ef2e2f-c7b2-41bb-9487-2d49bd04d70d" 7 + ], 8 + "ddl": [ 9 + { 10 + "name": "component_migrations", 11 + "entityType": "tables" 12 + }, 13 + { 14 + "name": "components", 15 + "entityType": "tables" 16 + }, 17 + { 18 + "name": "jobs", 19 + "entityType": "tables" 20 + }, 21 + { 22 + "name": "play_inputs", 23 + "entityType": "tables" 24 + }, 25 + { 26 + "name": "plays", 27 + "entityType": "tables" 28 + }, 29 + { 30 + "name": "plays_historical", 31 + "entityType": "tables" 32 + }, 33 + { 34 + "name": "play_queue_states", 35 + "entityType": "tables" 36 + }, 37 + { 38 + "type": "integer", 39 + "notNull": false, 40 + "autoincrement": false, 41 + "default": null, 42 + "generated": null, 43 + "name": "id", 44 + "entityType": "columns", 45 + "table": "component_migrations" 46 + }, 47 + { 48 + "type": "integer", 49 + "notNull": true, 50 + "autoincrement": false, 51 + "default": null, 52 + "generated": null, 53 + "name": "componentId", 54 + "entityType": "columns", 55 + "table": "component_migrations" 56 + }, 57 + { 58 + "type": "text", 59 + "notNull": true, 60 + "autoincrement": false, 61 + "default": null, 62 + "generated": null, 63 + "name": "name", 64 + "entityType": "columns", 65 + "table": "component_migrations" 66 + }, 67 + { 68 + "type": "integer", 69 + "notNull": false, 70 + "autoincrement": false, 71 + "default": null, 72 + "generated": null, 73 + "name": "success", 74 + "entityType": "columns", 75 + "table": "component_migrations" 76 + }, 77 + { 78 + "type": "text", 79 + "notNull": false, 80 + "autoincrement": false, 81 + "default": null, 82 + "generated": null, 83 + "name": "error", 84 + "entityType": "columns", 85 + "table": "component_migrations" 86 + }, 87 + { 88 + "type": "number", 89 + "notNull": true, 90 + "autoincrement": false, 91 + "default": null, 92 + "generated": null, 93 + "name": "attemptedAt", 94 + "entityType": "columns", 95 + "table": "component_migrations" 96 + }, 97 + { 98 + "type": "integer", 99 + "notNull": false, 100 + "autoincrement": false, 101 + "default": null, 102 + "generated": null, 103 + "name": "id", 104 + "entityType": "columns", 105 + "table": "components" 106 + }, 107 + { 108 + "type": "text(200)", 109 + "notNull": true, 110 + "autoincrement": false, 111 + "default": null, 112 + "generated": null, 113 + "name": "uid", 114 + "entityType": "columns", 115 + "table": "components" 116 + }, 117 + { 118 + "type": "text", 119 + "notNull": true, 120 + "autoincrement": false, 121 + "default": null, 122 + "generated": null, 123 + "name": "mode", 124 + "entityType": "columns", 125 + "table": "components" 126 + }, 127 + { 128 + "type": "text(50)", 129 + "notNull": true, 130 + "autoincrement": false, 131 + "default": null, 132 + "generated": null, 133 + "name": "type", 134 + "entityType": "columns", 135 + "table": "components" 136 + }, 137 + { 138 + "type": "text", 139 + "notNull": true, 140 + "autoincrement": false, 141 + "default": null, 142 + "generated": null, 143 + "name": "name", 144 + "entityType": "columns", 145 + "table": "components" 146 + }, 147 + { 148 + "type": "integer", 149 + "notNull": true, 150 + "autoincrement": false, 151 + "default": "0", 152 + "generated": null, 153 + "name": "countLive", 154 + "entityType": "columns", 155 + "table": "components" 156 + }, 157 + { 158 + "type": "integer", 159 + "notNull": true, 160 + "autoincrement": false, 161 + "default": "0", 162 + "generated": null, 163 + "name": "countNonLive", 164 + "entityType": "columns", 165 + "table": "components" 166 + }, 167 + { 168 + "type": "number", 169 + "notNull": false, 170 + "autoincrement": false, 171 + "default": null, 172 + "generated": null, 173 + "name": "createdAt", 174 + "entityType": "columns", 175 + "table": "components" 176 + }, 177 + { 178 + "type": "number", 179 + "notNull": false, 180 + "autoincrement": false, 181 + "default": null, 182 + "generated": null, 183 + "name": "lastReadyAt", 184 + "entityType": "columns", 185 + "table": "components" 186 + }, 187 + { 188 + "type": "number", 189 + "notNull": false, 190 + "autoincrement": false, 191 + "default": null, 192 + "generated": null, 193 + "name": "lastActiveAt", 194 + "entityType": "columns", 195 + "table": "components" 196 + }, 197 + { 198 + "type": "integer", 199 + "notNull": false, 200 + "autoincrement": false, 201 + "default": null, 202 + "generated": null, 203 + "name": "id", 204 + "entityType": "columns", 205 + "table": "jobs" 206 + }, 207 + { 208 + "type": "integer", 209 + "notNull": true, 210 + "autoincrement": false, 211 + "default": null, 212 + "generated": null, 213 + "name": "componentFromId", 214 + "entityType": "columns", 215 + "table": "jobs" 216 + }, 217 + { 218 + "type": "integer", 219 + "notNull": true, 220 + "autoincrement": false, 221 + "default": null, 222 + "generated": null, 223 + "name": "componentToId", 224 + "entityType": "columns", 225 + "table": "jobs" 226 + }, 227 + { 228 + "type": "text(50)", 229 + "notNull": true, 230 + "autoincrement": false, 231 + "default": null, 232 + "generated": null, 233 + "name": "name", 234 + "entityType": "columns", 235 + "table": "jobs" 236 + }, 237 + { 238 + "type": "text", 239 + "notNull": true, 240 + "autoincrement": false, 241 + "default": "'idle'", 242 + "generated": null, 243 + "name": "status", 244 + "entityType": "columns", 245 + "table": "jobs" 246 + }, 247 + { 248 + "type": "integer", 249 + "notNull": true, 250 + "autoincrement": false, 251 + "default": "0", 252 + "generated": null, 253 + "name": "retries", 254 + "entityType": "columns", 255 + "table": "jobs" 256 + }, 257 + { 258 + "type": "text", 259 + "notNull": false, 260 + "autoincrement": false, 261 + "default": null, 262 + "generated": null, 263 + "name": "error", 264 + "entityType": "columns", 265 + "table": "jobs" 266 + }, 267 + { 268 + "type": "text", 269 + "notNull": false, 270 + "autoincrement": false, 271 + "default": null, 272 + "generated": null, 273 + "name": "transformOptions", 274 + "entityType": "columns", 275 + "table": "jobs" 276 + }, 277 + { 278 + "type": "text", 279 + "notNull": false, 280 + "autoincrement": false, 281 + "default": null, 282 + "generated": null, 283 + "name": "initialParameters", 284 + "entityType": "columns", 285 + "table": "jobs" 286 + }, 287 + { 288 + "type": "text", 289 + "notNull": false, 290 + "autoincrement": false, 291 + "default": null, 292 + "generated": null, 293 + "name": "cursor", 294 + "entityType": "columns", 295 + "table": "jobs" 296 + }, 297 + { 298 + "type": "integer", 299 + "notNull": false, 300 + "autoincrement": false, 301 + "default": null, 302 + "generated": null, 303 + "name": "total", 304 + "entityType": "columns", 305 + "table": "jobs" 306 + }, 307 + { 308 + "type": "integer", 309 + "notNull": true, 310 + "autoincrement": false, 311 + "default": "0", 312 + "generated": null, 313 + "name": "imported", 314 + "entityType": "columns", 315 + "table": "jobs" 316 + }, 317 + { 318 + "type": "integer", 319 + "notNull": true, 320 + "autoincrement": false, 321 + "default": "0", 322 + "generated": null, 323 + "name": "scrobbled", 324 + "entityType": "columns", 325 + "table": "jobs" 326 + }, 327 + { 328 + "type": "number", 329 + "notNull": true, 330 + "autoincrement": false, 331 + "default": null, 332 + "generated": null, 333 + "name": "createdAt", 334 + "entityType": "columns", 335 + "table": "jobs" 336 + }, 337 + { 338 + "type": "number", 339 + "notNull": true, 340 + "autoincrement": false, 341 + "default": null, 342 + "generated": null, 343 + "name": "updatedAt", 344 + "entityType": "columns", 345 + "table": "jobs" 346 + }, 347 + { 348 + "type": "number", 349 + "notNull": false, 350 + "autoincrement": false, 351 + "default": null, 352 + "generated": null, 353 + "name": "completedAt", 354 + "entityType": "columns", 355 + "table": "jobs" 356 + }, 357 + { 358 + "type": "integer", 359 + "notNull": false, 360 + "autoincrement": false, 361 + "default": null, 362 + "generated": null, 363 + "name": "id", 364 + "entityType": "columns", 365 + "table": "play_inputs" 366 + }, 367 + { 368 + "type": "integer", 369 + "notNull": true, 370 + "autoincrement": false, 371 + "default": null, 372 + "generated": null, 373 + "name": "playId", 374 + "entityType": "columns", 375 + "table": "play_inputs" 376 + }, 377 + { 378 + "type": "text", 379 + "notNull": false, 380 + "autoincrement": false, 381 + "default": null, 382 + "generated": null, 383 + "name": "data", 384 + "entityType": "columns", 385 + "table": "play_inputs" 386 + }, 387 + { 388 + "type": "text", 389 + "notNull": true, 390 + "autoincrement": false, 391 + "default": null, 392 + "generated": null, 393 + "name": "play", 394 + "entityType": "columns", 395 + "table": "play_inputs" 396 + }, 397 + { 398 + "type": "number", 399 + "notNull": false, 400 + "autoincrement": false, 401 + "default": null, 402 + "generated": null, 403 + "name": "createdAt", 404 + "entityType": "columns", 405 + "table": "play_inputs" 406 + }, 407 + { 408 + "type": "integer", 409 + "notNull": false, 410 + "autoincrement": false, 411 + "default": null, 412 + "generated": null, 413 + "name": "id", 414 + "entityType": "columns", 415 + "table": "plays" 416 + }, 417 + { 418 + "type": "text(30)", 419 + "notNull": true, 420 + "autoincrement": false, 421 + "default": null, 422 + "generated": null, 423 + "name": "uid", 424 + "entityType": "columns", 425 + "table": "plays" 426 + }, 427 + { 428 + "type": "integer", 429 + "notNull": false, 430 + "autoincrement": false, 431 + "default": null, 432 + "generated": null, 433 + "name": "componentId", 434 + "entityType": "columns", 435 + "table": "plays" 436 + }, 437 + { 438 + "type": "text", 439 + "notNull": false, 440 + "autoincrement": false, 441 + "default": null, 442 + "generated": null, 443 + "name": "error", 444 + "entityType": "columns", 445 + "table": "plays" 446 + }, 447 + { 448 + "type": "number", 449 + "notNull": false, 450 + "autoincrement": false, 451 + "default": null, 452 + "generated": null, 453 + "name": "playedAt", 454 + "entityType": "columns", 455 + "table": "plays" 456 + }, 457 + { 458 + "type": "number", 459 + "notNull": false, 460 + "autoincrement": false, 461 + "default": null, 462 + "generated": null, 463 + "name": "seenAt", 464 + "entityType": "columns", 465 + "table": "plays" 466 + }, 467 + { 468 + "type": "number", 469 + "notNull": true, 470 + "autoincrement": false, 471 + "default": null, 472 + "generated": null, 473 + "name": "updatedAt", 474 + "entityType": "columns", 475 + "table": "plays" 476 + }, 477 + { 478 + "type": "text", 479 + "notNull": true, 480 + "autoincrement": false, 481 + "default": null, 482 + "generated": null, 483 + "name": "play", 484 + "entityType": "columns", 485 + "table": "plays" 486 + }, 487 + { 488 + "type": "text", 489 + "notNull": true, 490 + "autoincrement": false, 491 + "default": null, 492 + "generated": null, 493 + "name": "state", 494 + "entityType": "columns", 495 + "table": "plays" 496 + }, 497 + { 498 + "type": "integer", 499 + "notNull": false, 500 + "autoincrement": false, 501 + "default": null, 502 + "generated": null, 503 + "name": "parentId", 504 + "entityType": "columns", 505 + "table": "plays" 506 + }, 507 + { 508 + "type": "integer", 509 + "notNull": false, 510 + "autoincrement": false, 511 + "default": null, 512 + "generated": null, 513 + "name": "jobId", 514 + "entityType": "columns", 515 + "table": "plays" 516 + }, 517 + { 518 + "type": "text", 519 + "notNull": false, 520 + "autoincrement": false, 521 + "default": null, 522 + "generated": null, 523 + "name": "playHash", 524 + "entityType": "columns", 525 + "table": "plays" 526 + }, 527 + { 528 + "type": "text", 529 + "notNull": false, 530 + "autoincrement": false, 531 + "default": null, 532 + "generated": null, 533 + "name": "mbidIdentifier", 534 + "entityType": "columns", 535 + "table": "plays" 536 + }, 537 + { 538 + "type": "text", 539 + "notNull": false, 540 + "autoincrement": false, 541 + "default": null, 542 + "generated": null, 543 + "name": "compacted", 544 + "entityType": "columns", 545 + "table": "plays" 546 + }, 547 + { 548 + "type": "integer", 549 + "notNull": false, 550 + "autoincrement": false, 551 + "default": null, 552 + "generated": null, 553 + "name": "id", 554 + "entityType": "columns", 555 + "table": "plays_historical" 556 + }, 557 + { 558 + "type": "text(200)", 559 + "notNull": false, 560 + "autoincrement": false, 561 + "default": null, 562 + "generated": null, 563 + "name": "uid", 564 + "entityType": "columns", 565 + "table": "plays_historical" 566 + }, 567 + { 568 + "type": "integer", 569 + "notNull": false, 570 + "autoincrement": false, 571 + "default": null, 572 + "generated": null, 573 + "name": "componentId", 574 + "entityType": "columns", 575 + "table": "plays_historical" 576 + }, 577 + { 578 + "type": "number", 579 + "notNull": false, 580 + "autoincrement": false, 581 + "default": null, 582 + "generated": null, 583 + "name": "playedAt", 584 + "entityType": "columns", 585 + "table": "plays_historical" 586 + }, 587 + { 588 + "type": "number", 589 + "notNull": false, 590 + "autoincrement": false, 591 + "default": null, 592 + "generated": null, 593 + "name": "seenAt", 594 + "entityType": "columns", 595 + "table": "plays_historical" 596 + }, 597 + { 598 + "type": "text", 599 + "notNull": true, 600 + "autoincrement": false, 601 + "default": null, 602 + "generated": null, 603 + "name": "play", 604 + "entityType": "columns", 605 + "table": "plays_historical" 606 + }, 607 + { 608 + "type": "text", 609 + "notNull": false, 610 + "autoincrement": false, 611 + "default": null, 612 + "generated": null, 613 + "name": "playHash", 614 + "entityType": "columns", 615 + "table": "plays_historical" 616 + }, 617 + { 618 + "type": "text", 619 + "notNull": false, 620 + "autoincrement": false, 621 + "default": null, 622 + "generated": null, 623 + "name": "mbidIdentifier", 624 + "entityType": "columns", 625 + "table": "plays_historical" 626 + }, 627 + { 628 + "type": "integer", 629 + "notNull": false, 630 + "autoincrement": false, 631 + "default": null, 632 + "generated": null, 633 + "name": "id", 634 + "entityType": "columns", 635 + "table": "play_queue_states" 636 + }, 637 + { 638 + "type": "integer", 639 + "notNull": true, 640 + "autoincrement": false, 641 + "default": null, 642 + "generated": null, 643 + "name": "playId", 644 + "entityType": "columns", 645 + "table": "play_queue_states" 646 + }, 647 + { 648 + "type": "integer", 649 + "notNull": true, 650 + "autoincrement": false, 651 + "default": null, 652 + "generated": null, 653 + "name": "componentId", 654 + "entityType": "columns", 655 + "table": "play_queue_states" 656 + }, 657 + { 658 + "type": "text(50)", 659 + "notNull": true, 660 + "autoincrement": false, 661 + "default": null, 662 + "generated": null, 663 + "name": "queueName", 664 + "entityType": "columns", 665 + "table": "play_queue_states" 666 + }, 667 + { 668 + "type": "text", 669 + "notNull": true, 670 + "autoincrement": false, 671 + "default": "'queued'", 672 + "generated": null, 673 + "name": "queueStatus", 674 + "entityType": "columns", 675 + "table": "play_queue_states" 676 + }, 677 + { 678 + "type": "integer", 679 + "notNull": true, 680 + "autoincrement": false, 681 + "default": "0", 682 + "generated": null, 683 + "name": "retries", 684 + "entityType": "columns", 685 + "table": "play_queue_states" 686 + }, 687 + { 688 + "type": "text", 689 + "notNull": false, 690 + "autoincrement": false, 691 + "default": null, 692 + "generated": null, 693 + "name": "error", 694 + "entityType": "columns", 695 + "table": "play_queue_states" 696 + }, 697 + { 698 + "type": "number", 699 + "notNull": true, 700 + "autoincrement": false, 701 + "default": null, 702 + "generated": null, 703 + "name": "createdAt", 704 + "entityType": "columns", 705 + "table": "play_queue_states" 706 + }, 707 + { 708 + "type": "number", 709 + "notNull": true, 710 + "autoincrement": false, 711 + "default": null, 712 + "generated": null, 713 + "name": "updatedAt", 714 + "entityType": "columns", 715 + "table": "play_queue_states" 716 + }, 717 + { 718 + "columns": [ 719 + "componentId" 720 + ], 721 + "tableTo": "components", 722 + "columnsTo": [ 723 + "id" 724 + ], 725 + "onUpdate": "CASCADE", 726 + "onDelete": "CASCADE", 727 + "nameExplicit": false, 728 + "name": "fk_component_migrations_componentId_components_id_fk", 729 + "entityType": "fks", 730 + "table": "component_migrations" 731 + }, 732 + { 733 + "columns": [ 734 + "componentFromId" 735 + ], 736 + "tableTo": "components", 737 + "columnsTo": [ 738 + "id" 739 + ], 740 + "onUpdate": "CASCADE", 741 + "onDelete": "CASCADE", 742 + "nameExplicit": false, 743 + "name": "fk_jobs_componentFromId_components_id_fk", 744 + "entityType": "fks", 745 + "table": "jobs" 746 + }, 747 + { 748 + "columns": [ 749 + "componentToId" 750 + ], 751 + "tableTo": "components", 752 + "columnsTo": [ 753 + "id" 754 + ], 755 + "onUpdate": "CASCADE", 756 + "onDelete": "CASCADE", 757 + "nameExplicit": false, 758 + "name": "fk_jobs_componentToId_components_id_fk", 759 + "entityType": "fks", 760 + "table": "jobs" 761 + }, 762 + { 763 + "columns": [ 764 + "playId" 765 + ], 766 + "tableTo": "plays", 767 + "columnsTo": [ 768 + "id" 769 + ], 770 + "onUpdate": "CASCADE", 771 + "onDelete": "CASCADE", 772 + "nameExplicit": false, 773 + "name": "fk_play_inputs_playId_plays_id_fk", 774 + "entityType": "fks", 775 + "table": "play_inputs" 776 + }, 777 + { 778 + "columns": [ 779 + "componentId" 780 + ], 781 + "tableTo": "components", 782 + "columnsTo": [ 783 + "id" 784 + ], 785 + "onUpdate": "CASCADE", 786 + "onDelete": "CASCADE", 787 + "nameExplicit": false, 788 + "name": "fk_plays_componentId_components_id_fk", 789 + "entityType": "fks", 790 + "table": "plays" 791 + }, 792 + { 793 + "columns": [ 794 + "parentId" 795 + ], 796 + "tableTo": "plays", 797 + "columnsTo": [ 798 + "id" 799 + ], 800 + "onUpdate": "CASCADE", 801 + "onDelete": "SET NULL", 802 + "nameExplicit": false, 803 + "name": "fk_plays_parentId_plays_id_fk", 804 + "entityType": "fks", 805 + "table": "plays" 806 + }, 807 + { 808 + "columns": [ 809 + "jobId" 810 + ], 811 + "tableTo": "jobs", 812 + "columnsTo": [ 813 + "id" 814 + ], 815 + "onUpdate": "CASCADE", 816 + "onDelete": "CASCADE", 817 + "nameExplicit": false, 818 + "name": "fk_plays_jobId_jobs_id_fk", 819 + "entityType": "fks", 820 + "table": "plays" 821 + }, 822 + { 823 + "columns": [ 824 + "componentId" 825 + ], 826 + "tableTo": "components", 827 + "columnsTo": [ 828 + "id" 829 + ], 830 + "onUpdate": "CASCADE", 831 + "onDelete": "CASCADE", 832 + "nameExplicit": false, 833 + "name": "fk_plays_historical_componentId_components_id_fk", 834 + "entityType": "fks", 835 + "table": "plays_historical" 836 + }, 837 + { 838 + "columns": [ 839 + "playId" 840 + ], 841 + "tableTo": "plays", 842 + "columnsTo": [ 843 + "id" 844 + ], 845 + "onUpdate": "CASCADE", 846 + "onDelete": "CASCADE", 847 + "nameExplicit": false, 848 + "name": "fk_play_queue_states_playId_plays_id_fk", 849 + "entityType": "fks", 850 + "table": "play_queue_states" 851 + }, 852 + { 853 + "columns": [ 854 + "componentId" 855 + ], 856 + "tableTo": "components", 857 + "columnsTo": [ 858 + "id" 859 + ], 860 + "onUpdate": "CASCADE", 861 + "onDelete": "CASCADE", 862 + "nameExplicit": false, 863 + "name": "fk_play_queue_states_componentId_components_id_fk", 864 + "entityType": "fks", 865 + "table": "play_queue_states" 866 + }, 867 + { 868 + "columns": [ 869 + "id" 870 + ], 871 + "nameExplicit": false, 872 + "name": "component_migrations_pk", 873 + "table": "component_migrations", 874 + "entityType": "pks" 875 + }, 876 + { 877 + "columns": [ 878 + "id" 879 + ], 880 + "nameExplicit": false, 881 + "name": "components_pk", 882 + "table": "components", 883 + "entityType": "pks" 884 + }, 885 + { 886 + "columns": [ 887 + "id" 888 + ], 889 + "nameExplicit": false, 890 + "name": "jobs_pk", 891 + "table": "jobs", 892 + "entityType": "pks" 893 + }, 894 + { 895 + "columns": [ 896 + "id" 897 + ], 898 + "nameExplicit": false, 899 + "name": "play_inputs_pk", 900 + "table": "play_inputs", 901 + "entityType": "pks" 902 + }, 903 + { 904 + "columns": [ 905 + "id" 906 + ], 907 + "nameExplicit": false, 908 + "name": "plays_pk", 909 + "table": "plays", 910 + "entityType": "pks" 911 + }, 912 + { 913 + "columns": [ 914 + "id" 915 + ], 916 + "nameExplicit": false, 917 + "name": "plays_historical_pk", 918 + "table": "plays_historical", 919 + "entityType": "pks" 920 + }, 921 + { 922 + "columns": [ 923 + "id" 924 + ], 925 + "nameExplicit": false, 926 + "name": "play_queue_states_pk", 927 + "table": "play_queue_states", 928 + "entityType": "pks" 929 + }, 930 + { 931 + "columns": [ 932 + { 933 + "value": "uid", 934 + "isExpression": false 935 + }, 936 + { 937 + "value": "mode", 938 + "isExpression": false 939 + }, 940 + { 941 + "value": "type", 942 + "isExpression": false 943 + } 944 + ], 945 + "isUnique": true, 946 + "where": null, 947 + "origin": "manual", 948 + "name": "uid_mode_type_idx", 949 + "entityType": "indexes", 950 + "table": "components" 951 + }, 952 + { 953 + "columns": [ 954 + { 955 + "value": "playId", 956 + "isExpression": false 957 + } 958 + ], 959 + "isUnique": true, 960 + "where": null, 961 + "origin": "manual", 962 + "name": "play_input_id_idx", 963 + "entityType": "indexes", 964 + "table": "play_inputs" 965 + }, 966 + { 967 + "columns": [ 968 + { 969 + "value": "parentId", 970 + "isExpression": false 971 + } 972 + ], 973 + "isUnique": false, 974 + "where": null, 975 + "origin": "manual", 976 + "name": "play_parent_id_idx", 977 + "entityType": "indexes", 978 + "table": "plays" 979 + }, 980 + { 981 + "columns": [ 982 + { 983 + "value": "componentId", 984 + "isExpression": false 985 + } 986 + ], 987 + "isUnique": false, 988 + "where": null, 989 + "origin": "manual", 990 + "name": "play_component_id_idx", 991 + "entityType": "indexes", 992 + "table": "plays" 993 + }, 994 + { 995 + "columns": [ 996 + { 997 + "value": "uid", 998 + "isExpression": false 999 + } 1000 + ], 1001 + "isUnique": true, 1002 + "where": null, 1003 + "origin": "manual", 1004 + "name": "play_uid_idx", 1005 + "entityType": "indexes", 1006 + "table": "plays" 1007 + }, 1008 + { 1009 + "columns": [ 1010 + { 1011 + "value": "playedAt", 1012 + "isExpression": false 1013 + } 1014 + ], 1015 + "isUnique": false, 1016 + "where": null, 1017 + "origin": "manual", 1018 + "name": "play_playedAt_idx", 1019 + "entityType": "indexes", 1020 + "table": "plays" 1021 + }, 1022 + { 1023 + "columns": [ 1024 + { 1025 + "value": "seenAt", 1026 + "isExpression": false 1027 + } 1028 + ], 1029 + "isUnique": false, 1030 + "where": null, 1031 + "origin": "manual", 1032 + "name": "play_seenAt_idx", 1033 + "entityType": "indexes", 1034 + "table": "plays" 1035 + }, 1036 + { 1037 + "columns": [ 1038 + { 1039 + "value": "componentId", 1040 + "isExpression": false 1041 + } 1042 + ], 1043 + "isUnique": false, 1044 + "where": null, 1045 + "origin": "manual", 1046 + "name": "play_historical_component_id_idx", 1047 + "entityType": "indexes", 1048 + "table": "plays_historical" 1049 + }, 1050 + { 1051 + "columns": [ 1052 + { 1053 + "value": "uid", 1054 + "isExpression": false 1055 + } 1056 + ], 1057 + "isUnique": true, 1058 + "where": null, 1059 + "origin": "manual", 1060 + "name": "play_historical_uid_idx", 1061 + "entityType": "indexes", 1062 + "table": "plays_historical" 1063 + }, 1064 + { 1065 + "columns": [ 1066 + { 1067 + "value": "playedAt", 1068 + "isExpression": false 1069 + } 1070 + ], 1071 + "isUnique": false, 1072 + "where": null, 1073 + "origin": "manual", 1074 + "name": "play_historical_playedAt_idx", 1075 + "entityType": "indexes", 1076 + "table": "plays_historical" 1077 + }, 1078 + { 1079 + "columns": [ 1080 + { 1081 + "value": "playId", 1082 + "isExpression": false 1083 + } 1084 + ], 1085 + "isUnique": false, 1086 + "where": null, 1087 + "origin": "manual", 1088 + "name": "play_queue_state_id_idx", 1089 + "entityType": "indexes", 1090 + "table": "play_queue_states" 1091 + } 1092 + ], 1093 + "renames": [] 1094 + }
+4 -4
src/backend/common/database/drizzle/repositories/PlayRepository.ts
··· 4 4 import { ErrorLike, PlayObject, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js"; 5 5 import { generateInputEntity, generatePlayEntity, PlayEntityOpts, hydratePlaySelect, PlayHydateOptions } from "../entityUtils.js"; 6 6 import { playInputs, plays, queueStates, relations } from "../schema/schema.js"; 7 - import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, CompareOpKey, QueueStateSelect, PlayInputSelect, PlaySelectRel, FindWith, PlaySelectWithQueueStates, WhereClause } from "../drizzleTypes.js";; 7 + import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, QueueStateSelect, FindWith, PlaySelectWithQueueStates, WhereClause, PlayWith } from "../drizzleTypes.js";; 8 8 import { MarkOptional, MarkRequired, PathValue } from "ts-essentials"; 9 9 import { genGroupIdStrFromPlay, removeEmptyArrays, removeUndefinedKeys } from "../../../../utils.js"; 10 10 import dayjs, { Dayjs } from "dayjs"; ··· 236 236 return results.map(identifierExtractor[identifier]); 237 237 } 238 238 239 - findPlaysPaginated = async <T = PlaySelectRel>(args: QueryPlaysOpts, opts: HydrateOpts & ComponentConstrainedRepoOpts = {}): Promise<PaginatedResponse<T>> => { 239 + findPlaysPaginated = async <T = PlayWith<'queueStates' | 'input' | 'parent'>>(args: QueryPlaysOpts, opts: HydrateOpts & ComponentConstrainedRepoOpts = {}): Promise<PaginatedResponse<T>> => { 240 240 const { 241 241 limit = 100, 242 242 offset = 0, ··· 664 664 }) 665 665 } 666 666 667 - public getTemporallyClosePlays = async (play: PlayObject, opts: {states?: PlaySelect['state'][], bufferTime?: number} & { with?: WithPlayRelation[] } & ComponentConstrainedRepoOpts = {}): Promise<PlaySelectRel[]> => { 667 + public getTemporallyClosePlays = async (play: PlayObject, opts: {states?: PlaySelect['state'][], bufferTime?: number} & { with?: WithPlayRelation[] } & ComponentConstrainedRepoOpts = {}): Promise<PlayWith<'queueStates'>[]> => { 668 668 const { 669 669 componentId = this.componentId, 670 670 bufferTime, ··· 688 688 return ((await this.db.query.plays.findMany({ 689 689 where, 690 690 with: buildPlayWith(qWith) 691 - })) as PlaySelectRel[]).map(x => ({...x, play: hydratePlaySelect(x)})); 691 + })) as PlayWith<'queueStates'>[]).map(x => ({...x, play: hydratePlaySelect(x)})); 692 692 } 693 693 } 694 694
+28 -2
src/backend/common/database/drizzle/schema/schema.ts
··· 99 99 uniqueIndex('play_input_id_idx').on(table.playId) 100 100 ]); 101 101 102 + export const playsHistorical = sqliteTable("plays_historical", { 103 + id: integer().primaryKey(), 104 + uid: text({ length: 200 }).unique(), 105 + componentId: integer().references(() => components.id, {onDelete: 'cascade', onUpdate: 'cascade'}), 106 + playedAt: DayjsTimestamp('playedAt'), 107 + seenAt: DayjsTimestamp('seenAt'), 108 + play: PlayJson('play').notNull(), 109 + playHash: text(), 110 + mbidIdentifier: text() 111 + }, (table) => [ 112 + index("play_historical_component_id_idx").on(table.componentId), 113 + uniqueIndex("play_historical_uid_idx").on(table.uid), 114 + index("play_historical_playedAt_idx").on(table.playedAt) 115 + ]); 116 + 102 117 // export const playParentRelations = defineRelations({plays}, (r) => ({ 103 118 // plays: { 104 119 // parent: r.one.plays({ ··· 160 175 countLive: integer().notNull().default(0), 161 176 // number of discovered/scrobbled plays from backlog/jobs 162 177 countNonLive: integer().notNull().default(0), 163 - createdAt: DayjsTimestamp('createdAt').$defaultFn(() => dayjs()) 178 + createdAt: DayjsTimestamp('createdAt').$defaultFn(() => dayjs()), 179 + lastReadyAt: DayjsTimestamp('lastReadyAt'), 180 + lastActiveAt: DayjsTimestamp('lastActiveAt') 164 181 }, 165 182 (table) => [ 166 183 uniqueIndex('uid_mode_type_idx').on(table.uid,table.mode,table.type) ··· 194 211 completedAt: DayjsTimestamp('completedAt') 195 212 }); 196 213 197 - const playRelations = defineRelations({ plays, queueStates, playInputs, components, jobs, componentMigrations }, (r) => ({ 214 + const playRelations = defineRelations({ plays, queueStates, playInputs, components, jobs, componentMigrations,playsHistorical }, (r) => ({ 198 215 plays: { 199 216 queueStates: r.many.queueStates(), 200 217 input: r.one.playInputs({ ··· 218 235 optional: true 219 236 }) 220 237 }, 238 + playsHistorical: { 239 + component: r.one.components({ 240 + from: r.playsHistorical.componentId, 241 + to: r.components.id, 242 + }), 243 + }, 221 244 queueStates: { 222 245 play: r.one.plays({ 223 246 from: r.queueStates.playId, ··· 230 253 }, 231 254 components: { 232 255 plays: r.many.plays(), 256 + playsHistorical: r.many.plays(), 233 257 queueStates: r.many.queueStates(), 234 258 migrations: r.many.componentMigrations(), 235 259 }, ··· 250 274 switch(name) { 251 275 case 'plays': 252 276 return plays; 277 + case 'playsHistorical': 278 + return playsHistorical; 253 279 case 'components': 254 280 return components; 255 281 case 'playInputs':