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

fix(jellyfin): Handle string or object data for album props when parsing to play

#598#issuecomment-4564522030

FoxxMD (May 28, 2026, 3:08 PM UTC) b6cae4a5 1eed4845

+97 -8
+10 -7
src/backend/sources/JellyfinApiSource.ts
··· 43 43 import dayjs from "dayjs"; 44 44 import EventEmitter from "events"; 45 45 import { ArtistCredit, BrainzMeta, PlayObject, PlayObjectLifecycleless } from "../../core/Atomic.js"; 46 - import { artistNamesToCredits, buildTrackString, combinePartsToString, truncateStringToLength } from "../../core/StringUtils.js"; 46 + import { artistNamesToCredits, artistNameToCredit, buildTrackString, combinePartsToString, truncateStringToLength } from "../../core/StringUtils.js"; 47 47 import { 48 48 FormatPlayObjectOptions, 49 49 InternalConfig, ··· 59 59 import { MemoryPositionalSource } from "./MemoryPositionalSource.js"; 60 60 import { FixedSizeList } from "fixed-size-list"; 61 61 import { baseFormatPlayObj } from "../utils/PlayTransformUtils.js"; 62 + import { noCasePropObj } from "../utils/DataUtils.js"; 62 63 63 64 const shortDeviceId = truncateStringToLength(10, ''); 64 65 ··· 466 467 meta.albumArtist = [ProviderIds.MusicBrainzAlbumArtist]; 467 468 } 468 469 470 + const normalizedArtists = Artists.map(x => typeof x === 'string' ? artistNameToCredit(x) : artistNameToCredit(noCasePropObj(x))); 469 471 let playArtists: ArtistCredit[] = []; 470 - if(Artists.length === 1 && meta.artist !== undefined) { 471 - playArtists.push({name: Artists[0], mbid: meta.artist[0]}); 472 + if(normalizedArtists.length === 1 && meta.artist !== undefined) { 473 + playArtists.push({...normalizedArtists[0], mbid: meta.artist[0]}); 472 474 } else { 473 - playArtists = artistNamesToCredits(Artists); 475 + playArtists = normalizedArtists; 474 476 } 477 + const normalizedAlbumArtists = AlbumArtists.map(x => typeof x === 'string' ? artistNameToCredit(x) : artistNameToCredit(noCasePropObj(x))); 475 478 let playAlbumArtists: ArtistCredit[] = []; 476 - if(AlbumArtists.length === 1 && meta.albumArtist.length === 1) { 477 - playAlbumArtists.push({name: AlbumArtists[0], mbid: meta.albumArtist[0]}); 479 + if(normalizedAlbumArtists.length === 1 && meta.albumArtist !== undefined) { 480 + playAlbumArtists.push({...normalizedAlbumArtists[0], mbid: meta.albumArtist[0]}); 478 481 } else { 479 - playAlbumArtists = artistNamesToCredits(AlbumArtists); 482 + playAlbumArtists = normalizedAlbumArtists; 480 483 } 481 484 482 485 const play: PlayObjectLifecycleless = {
+87 -1
src/backend/tests/jellyfin/jellyfin.test.ts
··· 8 8 import validSession from './validSession.json' with { type: "json" }; 9 9 import { JellyApiData } from "../../common/infrastructure/config/source/jellyfin.js"; 10 10 import { generatePlay } from "../../../core/PlayTestUtils.js"; 11 - import { fakerJA } from "@faker-js/faker"; 11 + import { faker, fakerJA } from "@faker-js/faker"; 12 12 import { 13 13 // @ts-expect-error weird typings? 14 14 SessionInfo, 15 + // @ts-expect-error weird typings? 16 + BaseItemDto, 15 17 } from "@jellyfin/sdk/lib/generated-client/index.js"; 16 18 // @ts-expect-error weird typings? 17 19 import { getImageApi } from "@jellyfin/sdk/lib/utils/api/index.js"; ··· 371 373 }); 372 374 373 375 }); 376 + }); 377 + 378 + describe('Play Data Formatting', function() { 379 + 380 + it('Should handle artists as strings or objects', async function () { 381 + const itemStr: BaseItemDto = { 382 + Name: faker.word.words({count: {min: 1, max: 3}}), 383 + Artists: [faker.word.words({count: {min: 1, max: 3}})] 384 + }; 385 + 386 + const playStr = JellyfinApiSource.formatPlayObj(itemStr); 387 + 388 + expect(playStr.data.artists).length(1); 389 + expect(playStr.data.artists[0].name).eq(itemStr.Artists[0]); 390 + 391 + const itemObj: BaseItemDto = { 392 + Name: faker.word.words({count: {min: 1, max: 3}}), 393 + Artists: [{Name: faker.word.words({count: {min: 1, max: 3}})}] 394 + }; 395 + 396 + const playObj = JellyfinApiSource.formatPlayObj(itemObj); 397 + 398 + expect(playObj.data.artists).length(1); 399 + expect(playObj.data.artists[0].name).eq(itemObj.Artists[0].Name); 400 + }); 401 + 402 + it('Should handle album artists as strings or objects', async function () { 403 + const itemStr: BaseItemDto = { 404 + Name: faker.word.words({count: {min: 1, max: 3}}), 405 + Artists: [faker.word.words({count: {min: 1, max: 3}})], 406 + AlbumArtists: [faker.word.words({count: {min: 1, max: 3}})] 407 + }; 408 + 409 + const playStr = JellyfinApiSource.formatPlayObj(itemStr); 410 + 411 + expect(playStr.data.albumArtists).length(1); 412 + expect(playStr.data.albumArtists[0].name).eq(itemStr.AlbumArtists[0]); 413 + 414 + const itemObj: BaseItemDto = { 415 + Name: faker.word.words({count: {min: 1, max: 3}}), 416 + Artists: [{Name: faker.word.words({count: {min: 1, max: 3}})}], 417 + AlbumArtists: [{Name: faker.word.words({count: {min: 1, max: 3}})}] 418 + }; 419 + 420 + const playObj = JellyfinApiSource.formatPlayObj(itemObj); 421 + 422 + expect(playObj.data.albumArtists).length(1); 423 + expect(playObj.data.albumArtists[0].name).eq(itemObj.AlbumArtists[0].Name); 424 + }); 425 + 426 + it('Should add mbids to artist props', async function () { 427 + const itemStr: BaseItemDto = { 428 + Name: faker.word.words({count: {min: 1, max: 3}}), 429 + Artists: [faker.word.words({count: {min: 1, max: 3}})], 430 + AlbumArtists: [faker.word.words({count: {min: 1, max: 3}})], 431 + ProviderIds: { 432 + MusicBrainzArtist: 'foo', 433 + MusicBrainzAlbumArtist: 'bar' 434 + } 435 + }; 436 + 437 + const playStr = JellyfinApiSource.formatPlayObj(itemStr); 438 + 439 + expect(playStr.data.artists[0].mbid).eq(itemStr.ProviderIds.MusicBrainzArtist); 440 + expect(playStr.data.albumArtists[0].mbid).eq(itemStr.ProviderIds.MusicBrainzAlbumArtist); 441 + }); 442 + 443 + it('Does not add mbids to artist props if there are multiple artists', async function () { 444 + const itemStr: BaseItemDto = { 445 + Name: faker.word.words({count: {min: 1, max: 3}}), 446 + Artists: [faker.word.words({count: {min: 1, max: 3}}), faker.word.words({count: {min: 1, max: 3}})], 447 + AlbumArtists: [faker.word.words({count: {min: 1, max: 3}}), faker.word.words({count: {min: 1, max: 3}})], 448 + ProviderIds: { 449 + MusicBrainzArtist: 'foo', 450 + MusicBrainzAlbumArtist: 'bar' 451 + } 452 + }; 453 + 454 + const playStr = JellyfinApiSource.formatPlayObj(itemStr); 455 + 456 + expect(playStr.data.artists[0].mbid).is.undefined; 457 + expect(playStr.data.albumArtists[0].mbid).is.undefined; 458 + }); 459 + 374 460 }); 375 461 });