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

refactor(teal): Generate and use real lexicon definitions for tealfm implementation

Removes hand-rolled interfaces and generates interfaces+validation for official lexicon definitions

FoxxMD (Jun 3, 2026, 6:14 PM UTC) 95d7d276 18c520ce

+1069 -81
+2 -1
.dockerignore
··· 31 31 .env 32 32 *.env 33 33 .storybook 34 - .stories 34 + .stories 35 + **/lexicons/**/*.json
+3 -1
.gitignore
··· 153 153 storybook-static 154 154 155 155 *.db 156 - *.db* 156 + *.db* 157 + 158 + **/lexicons/**/*.json
+246 -14
package-lock.json
··· 11 11 "license": "MIT", 12 12 "dependencies": { 13 13 "@astronautlabs/mdns": "^1.0.7", 14 + "@atcute/bluesky": "^4.0.5", 14 15 "@atcute/identity-resolver-node": "^1.0.3", 16 + "@atcute/lexicons": "^2.0.0", 15 17 "@atcute/repo": "^1.0.0", 16 18 "@atproto/api": "^0.18.0", 17 19 "@atproto/oauth-client-node": "^0.3.10", ··· 103 105 "youtubei.js": "^15.0.1" 104 106 }, 105 107 "devDependencies": { 108 + "@atcute/lex-cli": "^3.1.0", 106 109 "@chakra-ui/react": "^3.34.0", 107 110 "@chromatic-com/storybook": "^5.1.2", 108 111 "@dbus-types/notifications": "^0.0.5", ··· 277 280 "integrity": "sha512-9bVdQ15Tbzpor1z/bteaemC9NvRDZgMD4Cfbu1/r/6Zv6fCBXvIJJoJ6Dh/BMvTlAiT2Xewm36i0VWRnZ4yw6A==", 278 281 "license": "MIT" 279 282 }, 283 + "node_modules/@atcute/atproto": { 284 + "version": "4.0.2", 285 + "resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-4.0.2.tgz", 286 + "integrity": "sha512-hLnvjiIOStpdUm0cEN+R5YydvbV0d6ap17Iv+t7i/nhSCN3TGMya7M0ftCWtCo+xoQ1EU6HK74R8jqXWlyrM0w==", 287 + "license": "0BSD", 288 + "dependencies": { 289 + "@atcute/lexicons": "^2.0.0" 290 + }, 291 + "peerDependencies": { 292 + "@atcute/lexicons": "^2.0.0" 293 + } 294 + }, 295 + "node_modules/@atcute/bluesky": { 296 + "version": "4.0.5", 297 + "resolved": "https://registry.npmjs.org/@atcute/bluesky/-/bluesky-4.0.5.tgz", 298 + "integrity": "sha512-kw+kc41cNXBFUyFU9oPDKhltB9JTEKNDTiNoSbtOvonwg2Wej9Zg8vt0UhLvb6958rSwLvG0xX+SuQbZ0O8k2Q==", 299 + "license": "0BSD", 300 + "dependencies": { 301 + "@atcute/atproto": "^4.0.2", 302 + "@atcute/lexicons": "^2.0.0" 303 + }, 304 + "peerDependencies": { 305 + "@atcute/lexicons": "^2.0.0" 306 + } 307 + }, 280 308 "node_modules/@atcute/car": { 281 309 "version": "6.0.0", 282 310 "resolved": "https://registry.npmjs.org/@atcute/car/-/car-6.0.0.tgz", ··· 372 400 "@atcute/lexicons": "^1.0.0" 373 401 } 374 402 }, 375 - "node_modules/@atcute/lexicons": { 403 + "node_modules/@atcute/identity-resolver-node/node_modules/@atcute/lexicons": { 404 + "version": "1.3.1", 405 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.3.1.tgz", 406 + "integrity": "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw==", 407 + "license": "0BSD", 408 + "dependencies": { 409 + "@atcute/uint8array": "^1.1.1", 410 + "@atcute/util-text": "^1.3.1", 411 + "@standard-schema/spec": "^1.1.0", 412 + "esm-env": "^1.2.2" 413 + } 414 + }, 415 + "node_modules/@atcute/identity-resolver/node_modules/@atcute/lexicons": { 416 + "version": "1.3.1", 417 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.3.1.tgz", 418 + "integrity": "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw==", 419 + "license": "0BSD", 420 + "peer": true, 421 + "dependencies": { 422 + "@atcute/uint8array": "^1.1.1", 423 + "@atcute/util-text": "^1.3.1", 424 + "@standard-schema/spec": "^1.1.0", 425 + "esm-env": "^1.2.2" 426 + } 427 + }, 428 + "node_modules/@atcute/identity/node_modules/@atcute/lexicons": { 376 429 "version": "1.3.1", 377 430 "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.3.1.tgz", 378 431 "integrity": "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw==", 379 432 "license": "0BSD", 433 + "peer": true, 434 + "dependencies": { 435 + "@atcute/uint8array": "^1.1.1", 436 + "@atcute/util-text": "^1.3.1", 437 + "@standard-schema/spec": "^1.1.0", 438 + "esm-env": "^1.2.2" 439 + } 440 + }, 441 + "node_modules/@atcute/lex-cli": { 442 + "version": "3.1.0", 443 + "resolved": "https://registry.npmjs.org/@atcute/lex-cli/-/lex-cli-3.1.0.tgz", 444 + "integrity": "sha512-Mba7y12ENIbdj+cr+gt5mJTalpuz/UbS4ykm/wT7t9ipoJtidXMSeuQFfeoyBvvidW77Vg4G6zxZkoDxtlr6Mg==", 445 + "dev": true, 446 + "license": "0BSD", 447 + "dependencies": { 448 + "@atcute/identity": "^2.0.0", 449 + "@atcute/identity-resolver": "^2.0.0", 450 + "@atcute/lexicon-doc": "^3.0.0", 451 + "@atcute/lexicon-resolver": "^1.0.0", 452 + "@atcute/lexicons": "^2.0.0", 453 + "@optique/core": "^1.0.2", 454 + "@optique/run": "^1.0.2", 455 + "picocolors": "^1.1.1", 456 + "prettier": "^3.8.3", 457 + "valibot": "^1.4.0" 458 + }, 459 + "bin": { 460 + "lex-cli": "cli.mjs" 461 + } 462 + }, 463 + "node_modules/@atcute/lex-cli/node_modules/@atcute/identity": { 464 + "version": "2.0.0", 465 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-2.0.0.tgz", 466 + "integrity": "sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==", 467 + "dev": true, 468 + "license": "0BSD", 469 + "dependencies": { 470 + "@atcute/lexicons": "^2.0.0", 471 + "valibot": "^1.4.0" 472 + }, 473 + "peerDependencies": { 474 + "@atcute/lexicons": "^2.0.0" 475 + } 476 + }, 477 + "node_modules/@atcute/lex-cli/node_modules/@atcute/identity-resolver": { 478 + "version": "2.0.0", 479 + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-2.0.0.tgz", 480 + "integrity": "sha512-IKg1BDQAF2bIdN10DL6KAXmTjK+3enTU2IRbuani9TsFahBwGZ7O5FiVmTiL6QlGfauGNW5S0xNCOxWXWMoR2Q==", 481 + "dev": true, 482 + "license": "0BSD", 483 + "dependencies": { 484 + "@atcute/lexicons": "^2.0.0", 485 + "@atcute/util-fetch": "^2.0.0", 486 + "valibot": "^1.4.0" 487 + }, 488 + "peerDependencies": { 489 + "@atcute/identity": "^2.0.0", 490 + "@atcute/lexicons": "^2.0.0" 491 + } 492 + }, 493 + "node_modules/@atcute/lex-cli/node_modules/@atcute/lexicon-resolver": { 494 + "version": "1.0.0", 495 + "resolved": "https://registry.npmjs.org/@atcute/lexicon-resolver/-/lexicon-resolver-1.0.0.tgz", 496 + "integrity": "sha512-4v3oSijgd3w9JS2BMsV1dfD+GD1ClQeuEJatM9b7rNffi1kMTSn/3nOQbJ1aNeXfqZbCkn3ATDRdvgki1xIAZw==", 497 + "dev": true, 498 + "license": "0BSD", 499 + "dependencies": { 500 + "@atcute/crypto": "^2.4.1", 501 + "@atcute/lexicon-doc": "^3.0.0", 502 + "@atcute/lexicons": "^2.0.0", 503 + "@atcute/repo": "^1.0.0", 504 + "@atcute/util-fetch": "^2.0.0", 505 + "valibot": "^1.4.0" 506 + }, 507 + "peerDependencies": { 508 + "@atcute/identity": "^2.0.0", 509 + "@atcute/identity-resolver": "^2.0.0", 510 + "@atcute/lexicon-doc": "^3.0.0", 511 + "@atcute/lexicons": "^2.0.0" 512 + } 513 + }, 514 + "node_modules/@atcute/lex-cli/node_modules/@atcute/util-fetch": { 515 + "version": "2.0.0", 516 + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-2.0.0.tgz", 517 + "integrity": "sha512-v+4aFQ/tuBqTV+URDJaFgm3mASWdglKXiPaGutJ1bs7QtQKmPZeesPY5MzW/a+MtI8GWCEJk8X9wOfalPOFSlg==", 518 + "dev": true, 519 + "license": "0BSD", 520 + "dependencies": { 521 + "valibot": "^1.4.0" 522 + } 523 + }, 524 + "node_modules/@atcute/lexicon-doc": { 525 + "version": "3.0.0", 526 + "resolved": "https://registry.npmjs.org/@atcute/lexicon-doc/-/lexicon-doc-3.0.0.tgz", 527 + "integrity": "sha512-hxBTEvO78X4j/HsGgcH8GKgMA9hP9FM6jtwQL8Yd6MIHtGXdB8QygvBmOAJEqm1oE9ckU1SSU3cwa2t4tbZZNw==", 528 + "dev": true, 529 + "license": "0BSD", 530 + "dependencies": { 531 + "@atcute/identity": "^2.0.0", 532 + "@atcute/lexicons": "^2.0.0", 533 + "@atcute/uint8array": "^1.1.1", 534 + "@atcute/util-text": "^1.3.1", 535 + "valibot": "^1.4.0" 536 + }, 537 + "peerDependencies": { 538 + "@atcute/lexicons": "^2.0.0" 539 + } 540 + }, 541 + "node_modules/@atcute/lexicon-doc/node_modules/@atcute/identity": { 542 + "version": "2.0.0", 543 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-2.0.0.tgz", 544 + "integrity": "sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==", 545 + "dev": true, 546 + "license": "0BSD", 547 + "dependencies": { 548 + "@atcute/lexicons": "^2.0.0", 549 + "valibot": "^1.4.0" 550 + }, 551 + "peerDependencies": { 552 + "@atcute/lexicons": "^2.0.0" 553 + } 554 + }, 555 + "node_modules/@atcute/lexicons": { 556 + "version": "2.0.0", 557 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-2.0.0.tgz", 558 + "integrity": "sha512-fIlwP+TPEAGoF5aU5s+f8N5sOjOu8Mww/sQL1B57Dp2hj3G/EWG9XwOHPokzycBCgXx+UxIIrzZCGy8whsVDZw==", 559 + "license": "0BSD", 380 560 "dependencies": { 381 561 "@atcute/uint8array": "^1.1.1", 382 562 "@atcute/util-text": "^1.3.1", ··· 426 606 "@atcute/cbor": "^2.0.0", 427 607 "@atcute/cid": "^2.0.0", 428 608 "@atcute/lexicons": "^2.0.0" 429 - } 430 - }, 431 - "node_modules/@atcute/repo/node_modules/@atcute/lexicons": { 432 - "version": "2.0.0", 433 - "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-2.0.0.tgz", 434 - "integrity": "sha512-fIlwP+TPEAGoF5aU5s+f8N5sOjOu8Mww/sQL1B57Dp2hj3G/EWG9XwOHPokzycBCgXx+UxIIrzZCGy8whsVDZw==", 435 - "license": "0BSD", 436 - "dependencies": { 437 - "@atcute/uint8array": "^1.1.1", 438 - "@atcute/util-text": "^1.3.1", 439 - "@standard-schema/spec": "^1.1.0", 440 - "esm-env": "^1.2.2" 441 609 } 442 610 }, 443 611 "node_modules/@atcute/uint8array": { ··· 2710 2878 "license": "Apache-2.0", 2711 2879 "engines": { 2712 2880 "node": ">=8.0.0" 2881 + } 2882 + }, 2883 + "node_modules/@optique/core": { 2884 + "version": "1.0.2", 2885 + "resolved": "https://registry.npmjs.org/@optique/core/-/core-1.0.2.tgz", 2886 + "integrity": "sha512-znsqMmjAdeOgSJzdJlpZpgAscojwQmeQYXzYnuEKllz5VCj6WyEkdzU4QuvJQtWQY3ve2taXwudEBRur0VHBOQ==", 2887 + "dev": true, 2888 + "funding": [ 2889 + "https://github.com/sponsors/dahlia" 2890 + ], 2891 + "license": "MIT", 2892 + "engines": { 2893 + "bun": ">=1.2.0", 2894 + "deno": ">=2.3.0", 2895 + "node": ">=20.0.0" 2896 + } 2897 + }, 2898 + "node_modules/@optique/run": { 2899 + "version": "1.0.2", 2900 + "resolved": "https://registry.npmjs.org/@optique/run/-/run-1.0.2.tgz", 2901 + "integrity": "sha512-0Wc+zC8SLGV8zXQX+pk+o0c6wE/ddx/36CHZ0toTh5lApsjruUuGhqbxvljerAAG5un1xQbOLxzksBVC6UPgSg==", 2902 + "dev": true, 2903 + "funding": [ 2904 + "https://github.com/sponsors/dahlia" 2905 + ], 2906 + "license": "MIT", 2907 + "dependencies": { 2908 + "@optique/core": "1.0.2" 2909 + }, 2910 + "engines": { 2911 + "bun": ">=1.2.0", 2912 + "deno": ">=2.3.0", 2913 + "node": ">=20.0.0" 2713 2914 } 2714 2915 }, 2715 2916 "node_modules/@oxc-parser/binding-android-arm-eabi": { ··· 14751 14952 "node": ">= 0.8.0" 14752 14953 } 14753 14954 }, 14955 + "node_modules/prettier": { 14956 + "version": "3.8.3", 14957 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", 14958 + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", 14959 + "dev": true, 14960 + "license": "MIT", 14961 + "bin": { 14962 + "prettier": "bin/prettier.cjs" 14963 + }, 14964 + "engines": { 14965 + "node": ">=14" 14966 + }, 14967 + "funding": { 14968 + "url": "https://github.com/prettier/prettier?sponsor=1" 14969 + } 14970 + }, 14754 14971 "node_modules/pretty-format": { 14755 14972 "version": "27.5.1", 14756 14973 "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", ··· 17631 17848 "version": "6.0.3", 17632 17849 "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", 17633 17850 "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", 17634 - "dev": true, 17851 + "devOptional": true, 17635 17852 "license": "Apache-2.0", 17636 17853 "bin": { 17637 17854 "tsc": "bin/tsc", ··· 18022 18239 "license": "MIT", 18023 18240 "bin": { 18024 18241 "uuid": "dist/bin/uuid" 18242 + } 18243 + }, 18244 + "node_modules/valibot": { 18245 + "version": "1.4.1", 18246 + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.1.tgz", 18247 + "integrity": "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==", 18248 + "devOptional": true, 18249 + "license": "MIT", 18250 + "peerDependencies": { 18251 + "typescript": ">=5" 18252 + }, 18253 + "peerDependenciesMeta": { 18254 + "typescript": { 18255 + "optional": true 18256 + } 18025 18257 } 18026 18258 }, 18027 18259 "node_modules/vary": {
+5 -1
package.json
··· 24 24 "install:parallel": "concurrently --kill-others-on-fail --names app,docs \"npm ci --no-audit\" \"npm run docs:install\"", 25 25 "cliff": "git-cliff --unreleased", 26 26 "storybook": "storybook dev -p 6006", 27 - "build-storybook": "storybook build" 27 + "build-storybook": "storybook build", 28 + "atproto:lex:tealfm": "npm exec -- lex-cli pull -c src/backend/common/vendor/teal/lex.config.ts && npm exec -- lex-cli generate -c src/backend/common/vendor/teal/lex.config.ts" 28 29 }, 29 30 "exports": { 30 31 ".": { ··· 49 50 "homepage": ".", 50 51 "dependencies": { 51 52 "@astronautlabs/mdns": "^1.0.7", 53 + "@atcute/bluesky": "^4.0.5", 52 54 "@atcute/identity-resolver-node": "^1.0.3", 55 + "@atcute/lexicons": "^2.0.0", 53 56 "@atcute/repo": "^1.0.0", 54 57 "@atproto/api": "^0.18.0", 55 58 "@atproto/oauth-client-node": "^0.3.10", ··· 141 144 "youtubei.js": "^15.0.1" 142 145 }, 143 146 "devDependencies": { 147 + "@atcute/lex-cli": "^3.1.0", 144 148 "@chakra-ui/react": "^3.34.0", 145 149 "@chromatic-com/storybook": "^5.1.2", 146 150 "@dbus-types/notifications": "^0.0.5",
-44
src/backend/common/infrastructure/config/client/tealfm.ts
··· 51 51 export interface TealClientAIOConfig extends TealClientConfig { 52 52 type: 'tealfm' 53 53 } 54 - export interface TealArtistCredit { 55 - artistName?: string, 56 - /** The MusicBrainz artist ID URI, formatted as mbid:<uuid> */ 57 - artistMbId?: string 58 - } 59 - /** 60 - * https://github.com/teal-fm/teal/blob/main/lexicons/fm.teal.alpha/feed/play.json 61 - * https://github.com/teal-fm/teal/blob/main/lexicons/fm.teal.alpha/feed/defs.json 62 - * */ 63 - export interface ScrobbleRecord { 64 - $type: "fm.teal.alpha.feed.play", 65 - trackName: string, 66 - playedTime: string, 67 - duration?: number 68 - artists?: TealArtistCredit[] 69 - /** Album name */ 70 - releaseName?: string 71 - /** A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)` */ 72 - submissionClientAgent: string, 73 - musicServiceBaseDomain?: string 74 - /** The MusicBrainz ID URI of the track, formatted as mbid:<uuid> */ 75 - trackMbId?: string 76 - /** The MusicBrainz recording ID URI of the track, formatted as mbid:<uuid> */ 77 - recordingMbId?: string 78 - /** The MusicBrainz release ID URI, formatted as mbid:<uuid> */ 79 - releaseMbId?: string 80 - isrc?: string, 81 - /** The URL associated with this track */ 82 - originUrl?: string 83 - /** Distinguishing information for track variants (e.g. 'Acoustic Version', 'Live at Wembley', 'Radio Edit', 'Demo'). Used to differentiate between different versions of the same base track while maintaining grouping capabilities. */ 84 - trackDiscriminant?: string; 85 - /** Distinguishing information for release variants (e.g. 'Deluxe Edition', 'Remastered', '2023 Remaster', 'Special Edition'). Used to differentiate between different versions of the same base release while maintaining grouping capabilities. */ 86 - releaseDiscriminant?: string; 87 - [x: string]: unknown 88 - } 89 - 90 - export interface StatusRecord { 91 - $type: "fm.teal.alpha.actor.status", 92 - /** item is just ScrobbleRecord, but without $type */ 93 - item: Omit<ScrobbleRecord, '$type'>, 94 - time: string, 95 - expiry: string, 96 - [x: string]: unknown 97 - } 98 54 99 55 export interface ListRecord<T> { 100 56 uri: string;
+15 -11
src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts
··· 1 1 import { getRoot } from "../../../ioc.js"; 2 2 import { AbstractApiOptions, PagelessListensTimeRangeOptions, PagelessTimeRangeListens, PagelessTimeRangeListensResult } from "../../infrastructure/Atomic.js"; 3 - import { ListRecord, ScrobbleRecord, StatusRecord, TealClientData } from "../../infrastructure/config/client/tealfm.js"; 3 + import { ListRecord, TealClientData } from "../../infrastructure/config/client/tealfm.js"; 4 4 import AbstractApiClient from "../AbstractApiClient.js"; 5 5 import { Agent, ComAtprotoRepoCreateRecord, ComAtprotoRepoListRecords, ComAtprotoRepoPutRecord } from "@atproto/api"; 6 6 import { MSCache } from "../../Cache.js"; 7 - import { BrainzMeta, PlayObject, PlayObjectLifecycleless, ScrobbleActionResult, UnixTimestamp, MBID, NowPlayingUpdateThreshold, SourcePlayerObj, Second } from "../../../../core/Atomic.js"; 7 + import { BrainzMeta, PlayObject, PlayObjectLifecycleless, ScrobbleActionResult, UnixTimestamp, MBID, SourcePlayerObj } from "../../../../core/Atomic.js"; 8 8 import { musicServiceToCononical } from '../listenbrainz/lzUtils.js'; 9 9 import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; 10 10 import { RecordOptions } from "../../infrastructure/config/client/tealfm.js"; ··· 17 17 import { decodeTid, generateTID } from '@ewanc26/tid'; 18 18 import { Duration } from "dayjs/plugin/duration.js"; 19 19 import { streamBodyProgress } from "../../../utils/NetworkUtils.js"; 20 + import { FmTealAlphaActorStatus, FmTealAlphaFeedPlay } from "../teal/lexicons/index.js"; 20 21 21 22 export abstract class AbstractBlueSkyApiClient extends AbstractApiClient implements PagelessTimeRangeListens { 22 23 ··· 36 37 37 38 abstract restoreSession(): Promise<boolean>; 38 39 39 - async createScrobbleRecord(record: ScrobbleRecord): Promise<ScrobbleActionResult> { 40 + async createScrobbleRecord(record: FmTealAlphaFeedPlay.Main): Promise<ScrobbleActionResult> { 40 41 const input: ComAtprotoRepoCreateRecord.InputSchema = { 41 42 repo: this.agent.sessionManager.did, 42 43 collection: "fm.teal.alpha.feed.play", ··· 50 51 } 51 52 } 52 53 53 - async updateStatusRecord(record: StatusRecord): Promise<ScrobbleActionResult> { 54 + async updateStatusRecord(record: FmTealAlphaActorStatus.Main): Promise<ScrobbleActionResult> { 54 55 const input: ComAtprotoRepoPutRecord.InputSchema = { 55 56 repo: this.agent.sessionManager.did, 56 57 collection: "fm.teal.alpha.actor.status", ··· 118 119 fromTS = usecToUnix(timestampUs); 119 120 } 120 121 121 - const plays = (resp.data.records as unknown as ListRecord<ScrobbleRecord>[]).map(x => listRecordToPlay(x)); 122 + const plays = (resp.data.records as unknown as ListRecord<FmTealAlphaFeedPlay.Main>[]).map(x => listRecordToPlay(x)); 122 123 123 124 return {data: plays, meta: {to, from: fromTS, limit}}; 124 125 } ··· 128 129 } 129 130 } 130 131 131 - export const playToRecord = (play: PlayObject): ScrobbleRecord => { 132 + export const playToRecord = (play: PlayObject): FmTealAlphaFeedPlay.Main => { 132 133 133 - const record: ScrobbleRecord = { 134 + const record: FmTealAlphaFeedPlay.Main = { 134 135 $type: "fm.teal.alpha.feed.play", 135 136 trackName: play.data.track, 136 137 artists: play.data.artists.map(x => removeUndefinedKeys({ artistName: x.name, artistMbId: mbidUriOrUndefined(x.mbid as MBID) })), ··· 160 161 return `mbid:${mbid}`; 161 162 } 162 163 163 - export const playToStatusRecord = (play: PlayObject, notPlaying: boolean, position?: number): StatusRecord => { 164 + export const playToStatusRecord = (play: PlayObject, notPlaying: boolean, position?: number): FmTealAlphaActorStatus.Main => { 164 165 const { $type, ...item } = notPlaying 165 166 ? { trackName: "", artists: [] } 166 167 : playToRecord(play); ··· 177 178 $type: "fm.teal.alpha.actor.status", 178 179 time: dayjs().toISOString(), 179 180 expiry: expiry.toISOString(), 180 - item 181 + item: { 182 + artists: [], 183 + ...item 184 + } 181 185 }; 182 186 } 183 187 ··· 201 205 return dayjs.duration(expiry.diff(dayjs(), 'ms')); 202 206 } 203 207 204 - export const listRecordToPlay = (listRecord: ListRecord<ScrobbleRecord>): PlayObject => { 208 + export const listRecordToPlay = (listRecord: ListRecord<FmTealAlphaFeedPlay.Main>): PlayObject => { 205 209 const opts: RecordOptions = {}; 206 210 const uriRes = parseRegexSingle(ATPROTO_URI_REGEX, listRecord.uri); 207 211 if (uriRes !== undefined) { ··· 212 216 return recordToPlay(listRecord.value, opts); 213 217 } 214 218 215 - export const recordToPlay = (record: ScrobbleRecord, options: RecordOptions = {}): PlayObject => { 219 + export const recordToPlay = (record: FmTealAlphaFeedPlay.Main, options: RecordOptions = {}): PlayObject => { 216 220 217 221 const play: PlayObjectLifecycleless = { 218 222 data: {
+26
src/backend/common/vendor/teal/lex.config.ts
··· 1 + import { defineLexiconConfig } from '@atcute/lex-cli'; 2 + import path from 'path'; 3 + import { fileURLToPath } from 'url'; 4 + import { dirname } from 'path'; 5 + 6 + const __filename = fileURLToPath(import.meta.url); 7 + const __dirname = dirname(__filename); 8 + 9 + export default defineLexiconConfig({ 10 + generate: { 11 + files: ['lexicons/**/*.json'], 12 + outdir: 'lexicons/', 13 + imports: ['@atcute/bluesky'], 14 + }, 15 + pull: { 16 + outdir: path.resolve(__dirname, 'lexicons/'), 17 + sources: [ 18 + { 19 + type: 'git', 20 + remote: 'https://github.com/teal-fm/teal.git', 21 + ref: 'main', 22 + pattern: ['lexicons/fm.teal.alpha/feed/*.json','lexicons/fm.teal.alpha/actor/*.json'], 23 + }, 24 + ], 25 + }, 26 + });
+6
src/backend/common/vendor/teal/lexicons/README.md
··· 1 + # lexicon sources 2 + 3 + this directory contains lexicon documents pulled from the following sources: 4 + 5 + - https://github.com/teal-fm/teal.git (ref: main) 6 + - commit: f661868af2a56199f1b3cae1ad20cd5831ec4212
+11
src/backend/common/vendor/teal/lexicons/index.ts
··· 1 + export * as FmTealAlphaActorDefs from "./types/fm/teal/alpha/actor/defs.js"; 2 + export * as FmTealAlphaActorGetProfile from "./types/fm/teal/alpha/actor/getProfile.js"; 3 + export * as FmTealAlphaActorGetProfiles from "./types/fm/teal/alpha/actor/getProfiles.js"; 4 + export * as FmTealAlphaActorProfile from "./types/fm/teal/alpha/actor/profile.js"; 5 + export * as FmTealAlphaActorProfileStatus from "./types/fm/teal/alpha/actor/profileStatus.js"; 6 + export * as FmTealAlphaActorSearchActors from "./types/fm/teal/alpha/actor/searchActors.js"; 7 + export * as FmTealAlphaActorStatus from "./types/fm/teal/alpha/actor/status.js"; 8 + export * as FmTealAlphaFeedDefs from "./types/fm/teal/alpha/feed/defs.js"; 9 + export * as FmTealAlphaFeedGetActorFeed from "./types/fm/teal/alpha/feed/getActorFeed.js"; 10 + export * as FmTealAlphaFeedGetPlay from "./types/fm/teal/alpha/feed/getPlay.js"; 11 + export * as FmTealAlphaFeedPlay from "./types/fm/teal/alpha/feed/play.js";
+96
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/defs.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import * as AppBskyRichtextFacet from "@atcute/bluesky/types/app/richtext/facet"; 4 + import * as FmTealAlphaActorProfile from "./profile.js"; 5 + import * as FmTealAlphaFeedDefs from "../feed/defs.js"; 6 + 7 + const _miniProfileViewSchema = /*#__PURE__*/ v.object({ 8 + $type: /*#__PURE__*/ v.optional( 9 + /*#__PURE__*/ v.literal("fm.teal.alpha.actor.defs#miniProfileView"), 10 + ), 11 + /** 12 + * IPLD of the avatar 13 + */ 14 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 15 + /** 16 + * The decentralized identifier of the actor 17 + */ 18 + did: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 19 + displayName: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 20 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 21 + }); 22 + const _profileViewSchema = /*#__PURE__*/ v.object({ 23 + $type: /*#__PURE__*/ v.optional( 24 + /*#__PURE__*/ v.literal("fm.teal.alpha.actor.defs#profileView"), 25 + ), 26 + /** 27 + * IPLD of the avatar 28 + */ 29 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 30 + /** 31 + * IPLD of the banner image 32 + */ 33 + banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 34 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 35 + /** 36 + * Free-form profile description text. 37 + */ 38 + description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 39 + /** 40 + * Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon. 41 + */ 42 + get descriptionFacets() { 43 + return /*#__PURE__*/ v.optional( 44 + /*#__PURE__*/ v.array(AppBskyRichtextFacet.mainSchema), 45 + ); 46 + }, 47 + /** 48 + * The decentralized identifier of the actor 49 + */ 50 + did: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 51 + displayName: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 52 + /** 53 + * The user's most recent item featured on their profile. 54 + */ 55 + get featuredItem() { 56 + return /*#__PURE__*/ v.optional(FmTealAlphaActorProfile.featuredItemSchema); 57 + }, 58 + get status() { 59 + return /*#__PURE__*/ v.optional(statusViewSchema); 60 + }, 61 + }); 62 + const _statusViewSchema = /*#__PURE__*/ v.object({ 63 + $type: /*#__PURE__*/ v.optional( 64 + /*#__PURE__*/ v.literal("fm.teal.alpha.actor.defs#statusView"), 65 + ), 66 + /** 67 + * The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time. 68 + */ 69 + expiry: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 70 + get item() { 71 + return /*#__PURE__*/ v.optional(FmTealAlphaFeedDefs.playViewSchema); 72 + }, 73 + /** 74 + * The unix timestamp of when the item was recorded 75 + */ 76 + time: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 77 + }); 78 + 79 + type miniProfileView$schematype = typeof _miniProfileViewSchema; 80 + type profileView$schematype = typeof _profileViewSchema; 81 + type statusView$schematype = typeof _statusViewSchema; 82 + 83 + export interface miniProfileViewSchema extends miniProfileView$schematype {} 84 + export interface profileViewSchema extends profileView$schematype {} 85 + export interface statusViewSchema extends statusView$schematype {} 86 + 87 + export const miniProfileViewSchema = 88 + _miniProfileViewSchema as miniProfileViewSchema; 89 + export const profileViewSchema = _profileViewSchema as profileViewSchema; 90 + export const statusViewSchema = _statusViewSchema as statusViewSchema; 91 + 92 + export interface MiniProfileView extends v.InferInput< 93 + typeof miniProfileViewSchema 94 + > {} 95 + export interface ProfileView extends v.InferInput<typeof profileViewSchema> {} 96 + export interface StatusView extends v.InferInput<typeof statusViewSchema> {}
+36
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/getProfile.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaActorDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("fm.teal.alpha.actor.getProfile", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * The author's DID 10 + */ 11 + actor: /*#__PURE__*/ v.actorIdentifierString(), 12 + }), 13 + output: { 14 + type: "lex", 15 + schema: /*#__PURE__*/ v.object({ 16 + get actor() { 17 + return FmTealAlphaActorDefs.profileViewSchema; 18 + }, 19 + }), 20 + }, 21 + }); 22 + 23 + type main$schematype = typeof _mainSchema; 24 + 25 + export interface mainSchema extends main$schematype {} 26 + 27 + export const mainSchema = _mainSchema as mainSchema; 28 + 29 + export interface $params extends v.InferInput<mainSchema["params"]> {} 30 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 31 + 32 + declare module "@atcute/lexicons/ambient" { 33 + interface XRPCQueries { 34 + "fm.teal.alpha.actor.getProfile": mainSchema; 35 + } 36 + }
+42
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/getProfiles.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaActorDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("fm.teal.alpha.actor.getProfiles", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Array of actor DIDs 10 + * @minLength 1 11 + */ 12 + actors: /*#__PURE__*/ v.constrain( 13 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.actorIdentifierString()), 14 + [/*#__PURE__*/ v.arrayLength(1)], 15 + ), 16 + }), 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + get actors() { 21 + return /*#__PURE__*/ v.array( 22 + FmTealAlphaActorDefs.miniProfileViewSchema, 23 + ); 24 + }, 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "fm.teal.alpha.actor.getProfiles": mainSchema; 41 + } 42 + }
+100
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/profile.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as AppBskyRichtextFacet from "@atcute/bluesky/types/app/richtext/facet"; 5 + 6 + const _featuredItemSchema = /*#__PURE__*/ v.object({ 7 + $type: /*#__PURE__*/ v.optional( 8 + /*#__PURE__*/ v.literal("fm.teal.alpha.actor.profile#featuredItem"), 9 + ), 10 + /** 11 + * The MusicBrainz ID URI of the item, formatted as mbid:<uuid> 12 + */ 13 + mbid: /*#__PURE__*/ v.genericUriString(), 14 + /** 15 + * The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc. 16 + */ 17 + type: /*#__PURE__*/ v.string(), 18 + }); 19 + const _mainSchema = /*#__PURE__*/ v.record( 20 + /*#__PURE__*/ v.literal("self"), 21 + /*#__PURE__*/ v.object({ 22 + $type: /*#__PURE__*/ v.literal("fm.teal.alpha.actor.profile"), 23 + /** 24 + * Small image to be displayed next to posts from account. AKA, 'profile picture' 25 + * @accept image/jpeg, image/png 26 + * @maxSize 1000000 27 + */ 28 + avatar: /*#__PURE__*/ v.optional( 29 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.blob(), [ 30 + /*#__PURE__*/ v.blobSize(1000000), 31 + /*#__PURE__*/ v.blobAccept(["image/jpeg", "image/png"]), 32 + ]), 33 + ), 34 + /** 35 + * Larger horizontal image to display behind profile view. 36 + * @accept image/jpeg, image/png 37 + * @maxSize 1000000 38 + */ 39 + banner: /*#__PURE__*/ v.optional( 40 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.blob(), [ 41 + /*#__PURE__*/ v.blobSize(1000000), 42 + /*#__PURE__*/ v.blobAccept(["image/jpeg", "image/png"]), 43 + ]), 44 + ), 45 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 46 + /** 47 + * Free-form profile description text. 48 + * @maxLength 2560 49 + * @maxGraphemes 256 50 + */ 51 + description: /*#__PURE__*/ v.optional( 52 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 53 + /*#__PURE__*/ v.stringLength(0, 2560), 54 + /*#__PURE__*/ v.stringGraphemes(0, 256), 55 + ]), 56 + ), 57 + /** 58 + * Annotations of text in the profile description (mentions, URLs, hashtags, etc). 59 + */ 60 + get descriptionFacets() { 61 + return /*#__PURE__*/ v.optional( 62 + /*#__PURE__*/ v.array(AppBskyRichtextFacet.mainSchema), 63 + ); 64 + }, 65 + /** 66 + * @maxLength 640 67 + * @maxGraphemes 64 68 + */ 69 + displayName: /*#__PURE__*/ v.optional( 70 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 71 + /*#__PURE__*/ v.stringLength(0, 640), 72 + /*#__PURE__*/ v.stringGraphemes(0, 64), 73 + ]), 74 + ), 75 + /** 76 + * The user's most recent item featured on their profile. 77 + */ 78 + get featuredItem() { 79 + return /*#__PURE__*/ v.optional(featuredItemSchema); 80 + }, 81 + }), 82 + ); 83 + 84 + type featuredItem$schematype = typeof _featuredItemSchema; 85 + type main$schematype = typeof _mainSchema; 86 + 87 + export interface featuredItemSchema extends featuredItem$schematype {} 88 + export interface mainSchema extends main$schematype {} 89 + 90 + export const featuredItemSchema = _featuredItemSchema as featuredItemSchema; 91 + export const mainSchema = _mainSchema as mainSchema; 92 + 93 + export interface FeaturedItem extends v.InferInput<typeof featuredItemSchema> {} 94 + export interface Main extends v.InferInput<typeof mainSchema> {} 95 + 96 + declare module "@atcute/lexicons/ambient" { 97 + interface Records { 98 + "fm.teal.alpha.actor.profile": mainSchema; 99 + } 100 + }
+42
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/profileStatus.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.record( 6 + /*#__PURE__*/ v.literal("self"), 7 + /*#__PURE__*/ v.object({ 8 + $type: /*#__PURE__*/ v.literal("fm.teal.alpha.actor.profileStatus"), 9 + /** 10 + * The onboarding completion status 11 + */ 12 + completedOnboarding: /*#__PURE__*/ v.string< 13 + | "complete" 14 + | "none" 15 + | "playOnboarding" 16 + | "profileOnboarding" 17 + | (string & {}) 18 + >(), 19 + /** 20 + * The timestamp when this status was created 21 + */ 22 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 23 + /** 24 + * The timestamp when this status was last updated 25 + */ 26 + updatedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 27 + }), 28 + ); 29 + 30 + type main$schematype = typeof _mainSchema; 31 + 32 + export interface mainSchema extends main$schematype {} 33 + 34 + export const mainSchema = _mainSchema as mainSchema; 35 + 36 + export interface Main extends v.InferInput<typeof mainSchema> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface Records { 40 + "fm.teal.alpha.actor.profileStatus": mainSchema; 41 + } 42 + }
+61
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/searchActors.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaActorDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("fm.teal.alpha.actor.searchActors", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Cursor for pagination 10 + */ 11 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 12 + /** 13 + * The maximum number of actors to return 14 + * @minimum 1 15 + * @maximum 25 16 + */ 17 + limit: /*#__PURE__*/ v.optional( 18 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 19 + /*#__PURE__*/ v.integerRange(1, 25), 20 + ]), 21 + ), 22 + /** 23 + * The search query 24 + * @maxLength 640 25 + * @maxGraphemes 128 26 + */ 27 + q: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 28 + /*#__PURE__*/ v.stringLength(0, 640), 29 + /*#__PURE__*/ v.stringGraphemes(0, 128), 30 + ]), 31 + }), 32 + output: { 33 + type: "lex", 34 + schema: /*#__PURE__*/ v.object({ 35 + get actors() { 36 + return /*#__PURE__*/ v.array( 37 + FmTealAlphaActorDefs.miniProfileViewSchema, 38 + ); 39 + }, 40 + /** 41 + * Cursor for pagination 42 + */ 43 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 44 + }), 45 + }, 46 + }); 47 + 48 + type main$schematype = typeof _mainSchema; 49 + 50 + export interface mainSchema extends main$schematype {} 51 + 52 + export const mainSchema = _mainSchema as mainSchema; 53 + 54 + export interface $params extends v.InferInput<mainSchema["params"]> {} 55 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 56 + 57 + declare module "@atcute/lexicons/ambient" { 58 + interface XRPCQueries { 59 + "fm.teal.alpha.actor.searchActors": mainSchema; 60 + } 61 + }
+36
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/actor/status.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaFeedDefs from "../feed/defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.record( 7 + /*#__PURE__*/ v.literal("self"), 8 + /*#__PURE__*/ v.object({ 9 + $type: /*#__PURE__*/ v.literal("fm.teal.alpha.actor.status"), 10 + /** 11 + * The RFC 3339 formatted time of the expiry time of the item. If unavailable, default to 10 minutes past the start time. 12 + */ 13 + expiry: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 14 + get item() { 15 + return FmTealAlphaFeedDefs.playViewSchema; 16 + }, 17 + /** 18 + * The RFC 3339 formatted time of when the item was recorded 19 + */ 20 + time: /*#__PURE__*/ v.datetimeString(), 21 + }), 22 + ); 23 + 24 + type main$schematype = typeof _mainSchema; 25 + 26 + export interface mainSchema extends main$schematype {} 27 + 28 + export const mainSchema = _mainSchema as mainSchema; 29 + 30 + export interface Main extends v.InferInput<typeof mainSchema> {} 31 + 32 + declare module "@atcute/lexicons/ambient" { 33 + interface Records { 34 + "fm.teal.alpha.actor.status": mainSchema; 35 + } 36 + }
+109
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/feed/defs.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + 4 + const _artistSchema = /*#__PURE__*/ v.object({ 5 + $type: /*#__PURE__*/ v.optional( 6 + /*#__PURE__*/ v.literal("fm.teal.alpha.feed.defs#artist"), 7 + ), 8 + /** 9 + * The MusicBrainz artist ID URI, formatted as mbid:<uuid> 10 + */ 11 + artistMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 12 + /** 13 + * The name of the artist 14 + * @minLength 1 15 + * @maxLength 256 16 + * @maxGraphemes 2560 17 + */ 18 + artistName: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 19 + /*#__PURE__*/ v.stringLength(1, 256), 20 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 21 + ]), 22 + }); 23 + const _playViewSchema = /*#__PURE__*/ v.object({ 24 + $type: /*#__PURE__*/ v.optional( 25 + /*#__PURE__*/ v.literal("fm.teal.alpha.feed.defs#playView"), 26 + ), 27 + /** 28 + * Array of artists in order of original appearance. 29 + */ 30 + get artists() { 31 + return /*#__PURE__*/ v.array(artistSchema); 32 + }, 33 + /** 34 + * The length of the track in seconds 35 + */ 36 + duration: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 37 + /** 38 + * The ISRC code associated with the recording 39 + */ 40 + isrc: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 41 + /** 42 + * The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided. 43 + */ 44 + musicServiceBaseDomain: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 45 + /** 46 + * The URL associated with this track 47 + */ 48 + originUrl: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 49 + /** 50 + * The unix timestamp of when the track was played 51 + */ 52 + playedTime: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 53 + /** 54 + * The MusicBrainz recording ID URI of the track, formatted as mbid:<uuid> 55 + */ 56 + recordingMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 57 + /** 58 + * The MusicBrainz release ID URI, formatted as mbid:<uuid> 59 + */ 60 + releaseMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 61 + /** 62 + * The name of the release/album 63 + * @maxLength 256 64 + * @maxGraphemes 2560 65 + */ 66 + releaseName: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 68 + /*#__PURE__*/ v.stringLength(0, 256), 69 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 70 + ]), 71 + ), 72 + /** 73 + * A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided. 74 + * @maxLength 256 75 + * @maxGraphemes 2560 76 + */ 77 + submissionClientAgent: /*#__PURE__*/ v.optional( 78 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 79 + /*#__PURE__*/ v.stringLength(0, 256), 80 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 81 + ]), 82 + ), 83 + /** 84 + * The MusicBrainz ID URI of the track, formatted as mbid:<uuid> 85 + */ 86 + trackMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 87 + /** 88 + * The name of the track 89 + * @minLength 1 90 + * @maxLength 256 91 + * @maxGraphemes 2560 92 + */ 93 + trackName: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 94 + /*#__PURE__*/ v.stringLength(1, 256), 95 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 96 + ]), 97 + }); 98 + 99 + type artist$schematype = typeof _artistSchema; 100 + type playView$schematype = typeof _playViewSchema; 101 + 102 + export interface artistSchema extends artist$schematype {} 103 + export interface playViewSchema extends playView$schematype {} 104 + 105 + export const artistSchema = _artistSchema as artistSchema; 106 + export const playViewSchema = _playViewSchema as playViewSchema; 107 + 108 + export interface Artist extends v.InferInput<typeof artistSchema> {} 109 + export interface PlayView extends v.InferInput<typeof playViewSchema> {}
+44
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/feed/getActorFeed.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaFeedDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("fm.teal.alpha.feed.getActorFeed", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * The author's DID for the play 10 + */ 11 + authorDID: /*#__PURE__*/ v.actorIdentifierString(), 12 + /** 13 + * The cursor to start the query from 14 + */ 15 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 16 + /** 17 + * The upper limit of tracks to get per request. Default is 20, max is 50. 18 + */ 19 + limit: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 20 + }), 21 + output: { 22 + type: "lex", 23 + schema: /*#__PURE__*/ v.object({ 24 + get plays() { 25 + return /*#__PURE__*/ v.array(FmTealAlphaFeedDefs.playViewSchema); 26 + }, 27 + }), 28 + }, 29 + }); 30 + 31 + type main$schematype = typeof _mainSchema; 32 + 33 + export interface mainSchema extends main$schematype {} 34 + 35 + export const mainSchema = _mainSchema as mainSchema; 36 + 37 + export interface $params extends v.InferInput<mainSchema["params"]> {} 38 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 39 + 40 + declare module "@atcute/lexicons/ambient" { 41 + interface XRPCQueries { 42 + "fm.teal.alpha.feed.getActorFeed": mainSchema; 43 + } 44 + }
+40
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/feed/getPlay.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaFeedDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("fm.teal.alpha.feed.getPlay", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * The author's DID for the play 10 + */ 11 + authorDID: /*#__PURE__*/ v.actorIdentifierString(), 12 + /** 13 + * The record key of the play 14 + */ 15 + rkey: /*#__PURE__*/ v.string(), 16 + }), 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + get play() { 21 + return FmTealAlphaFeedDefs.playViewSchema; 22 + }, 23 + }), 24 + }, 25 + }); 26 + 27 + type main$schematype = typeof _mainSchema; 28 + 29 + export interface mainSchema extends main$schematype {} 30 + 31 + export const mainSchema = _mainSchema as mainSchema; 32 + 33 + export interface $params extends v.InferInput<mainSchema["params"]> {} 34 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 35 + 36 + declare module "@atcute/lexicons/ambient" { 37 + interface XRPCQueries { 38 + "fm.teal.alpha.feed.getPlay": mainSchema; 39 + } 40 + }
+138
src/backend/common/vendor/teal/lexicons/types/fm/teal/alpha/feed/play.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as FmTealAlphaFeedDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.record( 7 + /*#__PURE__*/ v.tidString(), 8 + /*#__PURE__*/ v.object({ 9 + $type: /*#__PURE__*/ v.literal("fm.teal.alpha.feed.play"), 10 + /** 11 + * DEPRECATED: USE 'artists' INSTEAD. Array of Musicbrainz artist IDs. 12 + * @deprecated 13 + */ 14 + artistMbIds: /*#__PURE__*/ v.optional( 15 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.string()), 16 + ), 17 + /** 18 + * DEPRECATED: USE 'artists' INSTEAD. Array of artist names in order of original appearance. 19 + * @deprecated 20 + */ 21 + artistNames: /*#__PURE__*/ v.optional( 22 + /*#__PURE__*/ v.array( 23 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 24 + /*#__PURE__*/ v.stringLength(1, 256), 25 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 26 + ]), 27 + ), 28 + ), 29 + /** 30 + * Array of artists in order of original appearance. 31 + */ 32 + get artists() { 33 + return /*#__PURE__*/ v.optional( 34 + /*#__PURE__*/ v.array(FmTealAlphaFeedDefs.artistSchema), 35 + ); 36 + }, 37 + /** 38 + * The length of the track in seconds 39 + */ 40 + duration: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 41 + /** 42 + * The ISRC code associated with the recording 43 + */ 44 + isrc: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 45 + /** 46 + * The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided. 47 + */ 48 + musicServiceBaseDomain: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 49 + /** 50 + * The URL associated with this track 51 + */ 52 + originUrl: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 53 + /** 54 + * The unix timestamp of when the track was played 55 + */ 56 + playedTime: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 57 + /** 58 + * The MusicBrainz recording ID URI of the track, formatted as mbid:<uuid> 59 + */ 60 + recordingMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 61 + /** 62 + * Distinguishing information for release variants (e.g. 'Deluxe Edition', 'Remastered', '2023 Remaster', 'Special Edition'). Used to differentiate between different versions of the same base release while maintaining grouping capabilities. 63 + * @maxLength 128 64 + * @maxGraphemes 1280 65 + */ 66 + releaseDiscriminant: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 68 + /*#__PURE__*/ v.stringLength(0, 128), 69 + /*#__PURE__*/ v.stringGraphemes(0, 1280), 70 + ]), 71 + ), 72 + /** 73 + * The MusicBrainz release ID URI, formatted as mbid:<uuid> 74 + */ 75 + releaseMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 76 + /** 77 + * The name of the release/album 78 + * @maxLength 256 79 + * @maxGraphemes 2560 80 + */ 81 + releaseName: /*#__PURE__*/ v.optional( 82 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 83 + /*#__PURE__*/ v.stringLength(0, 256), 84 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 85 + ]), 86 + ), 87 + /** 88 + * A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided. 89 + * @maxLength 256 90 + * @maxGraphemes 2560 91 + */ 92 + submissionClientAgent: /*#__PURE__*/ v.optional( 93 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 94 + /*#__PURE__*/ v.stringLength(0, 256), 95 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 96 + ]), 97 + ), 98 + /** 99 + * Distinguishing information for track variants (e.g. 'Acoustic Version', 'Live at Wembley', 'Radio Edit', 'Demo'). Used to differentiate between different versions of the same base track while maintaining grouping capabilities. 100 + * @maxLength 128 101 + * @maxGraphemes 1280 102 + */ 103 + trackDiscriminant: /*#__PURE__*/ v.optional( 104 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 105 + /*#__PURE__*/ v.stringLength(0, 128), 106 + /*#__PURE__*/ v.stringGraphemes(0, 1280), 107 + ]), 108 + ), 109 + /** 110 + * The MusicBrainz ID URI of the track, formatted as mbid:<uuid> 111 + */ 112 + trackMbId: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 113 + /** 114 + * The name of the track 115 + * @minLength 1 116 + * @maxLength 256 117 + * @maxGraphemes 2560 118 + */ 119 + trackName: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 120 + /*#__PURE__*/ v.stringLength(1, 256), 121 + /*#__PURE__*/ v.stringGraphemes(0, 2560), 122 + ]), 123 + }), 124 + ); 125 + 126 + type main$schematype = typeof _mainSchema; 127 + 128 + export interface mainSchema extends main$schematype {} 129 + 130 + export const mainSchema = _mainSchema as mainSchema; 131 + 132 + export interface Main extends v.InferInput<typeof mainSchema> {} 133 + 134 + declare module "@atcute/lexicons/ambient" { 135 + interface Records { 136 + "fm.teal.alpha.feed.play": mainSchema; 137 + } 138 + }
+4 -3
src/backend/scrobblers/TealfmScrobbler.ts
··· 12 12 import { playToListenPayload } from '../common/vendor/listenbrainz/lzUtils.js'; 13 13 import { Notifiers } from "../notifier/Notifiers.js"; 14 14 15 - import AbstractScrobbleClient, { nowPlayingUpdateByPlayDuration, shouldClearNPStatus } from "./AbstractScrobbleClient.js"; 16 - import { ScrobbleRecord, TealClientConfig } from "../common/infrastructure/config/client/tealfm.js"; 15 + import { nowPlayingUpdateByPlayDuration, shouldClearNPStatus } from "./AbstractScrobbleClient.js"; 16 + import { TealClientConfig } from "../common/infrastructure/config/client/tealfm.js"; 17 17 import { BlueSkyAppApiClient } from "../common/vendor/bluesky/BlueSkyAppApiClient.js"; 18 18 import { BlueSkyOauthApiClient } from "../common/vendor/bluesky/BlueSkyOauthApiClient.js"; 19 19 import { AbstractBlueSkyApiClient, nowPlayingExpirationDuration, playToRecord, playToStatusRecord, recordToPlay } from "../common/vendor/bluesky/AbstractBlueSkyApiClient.js"; ··· 23 23 import { fromStream } from '@atcute/repo'; 24 24 import { playToRepositoryCreatePlayHistoricalOpts, RepositoryCreatePlayHistoricalOpts } from "../common/database/drizzle/repositories/PlayHistoricalRepository.js"; 25 25 import { isAbortError } from "abort-controller-x"; 26 + import { FmTealAlphaFeedPlay } from "../common/vendor/teal/lexicons/index.js"; 26 27 27 28 export default class TealScrobbler extends AbstractHistoricalScrobbleClient { 28 29 ··· 234 235 if(entry.collection === 'fm.teal.alpha.feed.play') { 235 236 let play: PlayObject; 236 237 try { 237 - play = recordToPlay(entry.record as ScrobbleRecord, { 238 + play = recordToPlay(entry.record as FmTealAlphaFeedPlay.Main, { 238 239 web: did !== undefined ? `at://did:plc:${did}/fm.teal.alpha.feed.play/${entry.rkey}` : undefined, 239 240 playId: entry.rkey, 240 241 user: did
+7 -6
src/core/PlayTestUtils.ts
··· 11 11 import { CALCULATED_PLAYER_STATUSES, NO_DEVICE, NO_USER, PlayerStateDataMaybePlay, PlayPlatformId, REPORTED_PLAYER_STATUSES, SINGLE_USER_PLATFORM_ID } from '../backend/common/infrastructure/Atomic.js'; 12 12 import { arrayListAnd, artistNamesToCredits } from './StringUtils.js'; 13 13 import { findDelimiters } from "./StringUtils.js"; 14 - import { ListRecord, ScrobbleRecord } from '../backend/common/infrastructure/config/client/tealfm.js'; 14 + import { ListRecord } from '../backend/common/infrastructure/config/client/tealfm.js'; 15 15 import { nanoid } from 'nanoid'; 16 16 import { LastFMTrackObject } from '../backend/common/vendor/LastfmApiClient.js'; 17 17 import { MarkOptional } from 'ts-essentials'; 18 18 import { defaultLifecycle } from '../backend/utils/PlayTransformUtils.js'; 19 19 import clone from 'clone'; 20 20 import { removeUndefinedKeys } from '../backend/utils.js'; 21 + import { FmTealAlphaFeedPlay } from '../backend/common/vendor/teal/lexicons/index.js'; 21 22 22 23 dayjs.extend(utc) 23 24 dayjs.extend(isBetween); ··· 476 477 export const generateTealPlayRecord = (opts: { 477 478 withMbids?: boolean, 478 479 withIsrc?: boolean 479 - } = {}): [ListRecord<ScrobbleRecord>, { did: string, tid: string }] => { 480 + } = {}): [ListRecord<FmTealAlphaFeedPlay.Main>, { did: string, tid: string }] => { 480 481 const { 481 482 withMbids = true, 482 483 withIsrc = true, ··· 488 489 const did = nanoid(12); 489 490 const tid = nanoid(10); 490 491 491 - const rec: ListRecord<ScrobbleRecord> = { 492 + const rec: ListRecord<FmTealAlphaFeedPlay.Main> = { 492 493 uri: `at://did:plc:${did}/fm.teal.alpha.feed.play/${tid}`, 493 494 cid: nanoid(12), 494 495 value: { 495 496 '$type': 'fm.teal.alpha.feed.play', 496 - artists: artists.map(x => withMbids ? { artistName: x, artistMbId: generateMbid() } : { artistName: x }), 497 + artists: artists.map(x => withMbids ? { artistName: x, artistMbId: `mbid:${generateMbid()}` } : { artistName: x }), 497 498 releaseName: faker.music.album(), 498 499 trackName: faker.music.songName(), 499 500 playedTime: now.toISOString(), ··· 503 504 } 504 505 505 506 if (withMbids) { 506 - rec.value.releaseMbId = generateMbid(); 507 - rec.value.recordingMbId = generateMbid(); 507 + rec.value.releaseMbId = `mbid:${generateMbid()}`; 508 + rec.value.recordingMbId = `mbid:${generateMbid()}`; 508 509 } 509 510 510 511 if (withIsrc) {