Skip to content

feat(plex): Retrieve MusicBrainz IDs#426

Merged
FoxxMD merged 4 commits intoFoxxMD:masterfrom
owendaprile:push-ursmqpqmrwns
Jan 5, 2026
Merged

feat(plex): Retrieve MusicBrainz IDs#426
FoxxMD merged 4 commits intoFoxxMD:masterfrom
owendaprile:push-ursmqpqmrwns

Conversation

@owendaprile
Copy link
Copy Markdown
Contributor

Checklist before requesting a review

Type of change

  • New feature (non-breaking change which adds functionality)

Describe your changes

This adds support for retrieving MusicBrainz IDs for tracks, albums, and album artists in the Plex source for libraries that use the Plex Music agent and where the albums have been matched to a MusicBrainz result (in Plex).

Plex does not seem to store MusicBrainz IDs for track artists, so this uses the album artist for both album artists and track artists. This way, sources that don't support album artists still get an artist MBID.

I'm running this with no issues through teal.fm: https://pdsls.dev/at://did:plc:zhxv5pxpmojhnvaqy4mwailv/fm.teal.alpha.feed.play/3maos62xhyb2y

@netlify
Copy link
Copy Markdown

netlify bot commented Dec 23, 2025

Deploy Preview for multi-scrobbler canceled.

Name Link
🔨 Latest commit a7a99ee
🔍 Latest deploy log https://app.netlify.com/projects/multi-scrobbler/deploys/695bffc7e25a300008af1663

@FoxxMD
Copy link
Copy Markdown
Owner

FoxxMD commented Dec 30, 2025

Thanks for the PR! Is there a reason you couldn't use plexApi (from the @lukehagar/plexjs library) to get metadata, rather than using a new httpclient?

@owendaprile
Copy link
Copy Markdown
Contributor Author

owendaprile commented Dec 30, 2025

I originally tried to use the PlexAPI.library.getMediaMetaData method which is available in plexjs@0.39.0 but it doesn't correctly parse responses from my Plex server. It's expecting some fields that my server doesn't return. I couldn't find a way to relax the parsing requirements so I could just get the GUID fields.

The same goes for PlexAPI.content.getMetadataItem from plexjs@0.43.0.

There's also been major changes to the plexjs library since 0.39.0 which would require replacing most of the method calls from what I've seen.

@FoxxMD
Copy link
Copy Markdown
Owner

FoxxMD commented Dec 31, 2025

That's fair. I wish the library was better and had those relaxed methods, like you mentioned. It's been a pain point for me. Maybe the newer version will work better. I'll try it with the new version and if it's not easy i'll go with your PR as-is. Thanks for doing the legwork 👍

@owendaprile
Copy link
Copy Markdown
Contributor Author

Hopefully you'll be able to get it to work! I tried pretty much the exact example from the library docs for the new version (0.43.0) and it just seems to be completely mismatched with the actual Plex API. The MBIDs are in the response but it seems like it's expecting them to be numbers.

index.ts:

import { PlexAPI } from "@lukehagar/plexjs";

const plexApi = new PlexAPI({
  token: "...",
  serverURL: "...",
})

async function run() {
  const result = await plexApi.content.getMetadataItem({
    ids: ["16900"],
  })
  
  console.log(result);
}

run();
Error:
ResponseValidationError: Response validation failed
    at safeParseResponse (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/lib/matchers.ts:334:7)
    at matchFunc (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/lib/matchers.ts:297:9)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async $do (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/funcs/contentGetMetadataItem.ts:221:20) {
  statusCode: 200,
  body: '{"MediaContainer":{"size":1,"allowSync":true,"identifier":"com.plexapp.plugins.library","librarySectionID":1,"librarySectionTitle":"Music","librarySectionUUID":"2bc6b969-87bf-4edb-a2a1-7020a3c6ef28","mediaTagPrefix":"/system/bundle/media/flags/","mediaTagVersion":1758205129,"Metadata":[{"ratingKey":"16831","key":"/library/metadata/16831","parentRatingKey":"16829","grandparentRatingKey":"5061","guid":"plex://track/6931c1e0844185ecc6c4918e","parentGuid":"plex://album/6931c1df844185ecc6c48dd8","grandparentGuid":"plex://artist/602458a7adc8af002c821e4f","parentStudio":"deadAir","type":"track","title":"So What?","grandparentKey":"/library/metadata/5061","parentKey":"/library/metadata/16829","librarySectionTitle":"Music","librarySectionID":1,"librarySectionKey":"/library/sections/1","grandparentTitle":"Jane Remover","parentTitle":"♡","summary":"","index":2,"parentIndex":1,"userRating":10.0,"viewCount":48,"skipCount":1,"lastViewedAt":1766530271,"lastRatedAt":1766414868,"parentYear":2025,"thumb":"/library/metadata/16829/thumb/1764963474","parentThumb":"/library/metadata/16829/thumb/1764963474","grandparentThumb":"/library/metadata/5061/thumb/1766462550","duration":246442,"addedAt":1764963472,"updatedAt":1764963474,"musicAnalysisVersion":"1","Media":[{"id":23774,"duration":246442,"bitrate":1740,"audioChannels":2,"audioCodec":"flac","container":"flac","hasVoiceActivity":false,"Part":[{"id":30113,"key":"/library/parts/30113/1764946844/file.flac","duration":246442,"file":"/data/Music/Jane Remover/♡/02 - Jane Remover - So What?.flac","size":53854602,"container":"flac","hasThumbnail":"1","Stream":[{"id":57136,"streamType":2,"selected":true,"codec":"flac","index":0,"channels":2,"bitrate":1740,"albumGain":"-11.06","albumPeak":"1.017471","albumRange":"7.813473","audioChannelLayout":"stereo","bitDepth":24,"gain":"-11.06","loudness":"-7.74","lra":"7.62","peak":"0.975000","samplingRate":44100,"displayTitle":"FLAC (Stereo)","extendedDisplayTitle":"FLAC (Stereo)"}]}]}],"Image":[{"alt":"So What?","type":"coverPoster","url":"/library/metadata/5061/thumb/1766462550"}],"Guid":[{"id":"mbid://9dc87868-7086-4a24-bdfb-9b739e0e7b54"}],"Field":[{"locked":true,"name":"title"},{"locked":true,"name":"originalTitle"},{"locked":true,"name":"index"},{"locked":true,"name":"parentIndex"}]}]}}',
  headers: Headers {},
  contentType: 'application/json',
  rawResponse: Response {
    [Symbol(state)]: {
      aborted: false,
      rangeRequested: false,
      timingAllowPassed: true,
      requestIncludesCredentials: true,
      type: 'default',
      status: 200,
      timingInfo: {
        startTime: 543.779728,
        redirectStartTime: 0,
        redirectEndTime: 0,
        postRedirectStartTime: 543.779728,
        finalServiceWorkerStartTime: 0,
        finalNetworkResponseStartTime: 597.821572,
        finalNetworkRequestStartTime: 583.281709,
        endTime: 614.34544,
        encodedBodySize: 1016,
        decodedBodySize: 2296,
        finalConnectionTimingInfo: {
          domainLookupStartTime: 543.779728,
          domainLookupEndTime: 543.779728,
          connectionStartTime: 543.779728,
          connectionEndTime: 543.779728,
          secureConnectionStartTime: 543.779728,
          ALPNNegotiatedProtocol: undefined
        }
      },
      cacheState: '',
      statusText: 'OK',
      headersList: HeadersList {
        cookies: null,
        [Symbol(headers map)]: Map(10) {
          'alt-svc' => { name: 'alt-svc', value: 'h3=":11354"; ma=2592000' },
          'cache-control' => { name: 'cache-control', value: 'no-cache' },
          'content-encoding' => { name: 'content-encoding', value: 'gzip' },
          'content-length' => { name: 'content-length', value: '1016' },
          'content-type' => { name: 'content-type', value: 'application/json' },
          'date' => { name: 'date', value: 'Thu, 01 Jan 2026 17:30:52 GMT' },
          'via' => { name: 'via', value: '1.1 Caddy' },
          'x-plex-content-compressed-length' => { name: 'x-plex-content-compressed-length', value: '1016' },
          'x-plex-content-original-length' => { name: 'x-plex-content-original-length', value: '2296' },
          'x-plex-protocol' => { name: 'x-plex-protocol', value: '1.0' }
        },
        [Symbol(headers map sorted)]: [
          [ 'alt-svc', 'h3=":11354"; ma=2592000' ],
          [ 'cache-control', 'no-cache' ],
          [ 'content-encoding', 'gzip' ],
          [ 'content-length', '1016' ],
          [ 'content-type', 'application/json' ],
          [ 'date', 'Thu, 01 Jan 2026 17:30:52 GMT' ],
          [ 'via', '1.1 Caddy' ],
          [ 'x-plex-content-compressed-length', '1016' ],
          [ 'x-plex-content-original-length', '2296' ],
          [ 'x-plex-protocol', '1.0' ]
        ]
      },
      urlList: [ URL {} ],
      body: {
        stream: ReadableStream {
          [Symbol(kType)]: 'ReadableStream',
          [Symbol(kState)]: [Object: null prototype] {
            disturbed: true,
            reader: [ReadableStreamDefaultReader],
            state: 'closed',
            storedError: undefined,
            transfer: [Object: null prototype],
            controller: [ReadableByteStreamController]
          },
          [Symbol(nodejs.webstream.isClosedPromise)]: {
            promise: [Promise],
            resolve: [Function (anonymous)],
            reject: [Function (anonymous)]
          },
          [Symbol(nodejs.webstream.controllerErrorFunction)]: [Function (anonymous)]
        },
        source: null,
        length: null
      }
    },
    [Symbol(headers)]: Headers {}
  },
  cause: ZodError: [
    {
      "code": "invalid_type",
      "expected": "number",
      "received": "string",
      "path": [
        "MediaContainerWithMetadata",
        "MediaContainer",
        "Metadata",
        0,
        "Guid",
        0,
        "id"
      ],
      "message": "Expected number, received string"
    }
  ]
      at get error [as error] (/var/home/owen/Projects/plexjs-test/node_modules/zod/v3/types.cjs:45:31)
      at ZodEffects.parse (/var/home/owen/Projects/plexjs-test/node_modules/zod/v3/types.cjs:120:22)
      at safeParseResponse.request.request (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/lib/matchers.ts:299:42)
      at safeParseResponse (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/lib/matchers.ts:331:15)
      at matchFunc (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/lib/matchers.ts:297:9)
      at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
      at async $do (/var/home/owen/Projects/plexjs-test/node_modules/@lukehagar/plexjs/src/funcs/contentGetMetadataItem.ts:221:20) {
    issues: [
      {
        code: 'invalid_type',
        expected: 'number',
        received: 'string',
        path: [
          'MediaContainerWithMetadata',
          'MediaContainer',
          'Metadata',
          0,
          'Guid',
          0,
          'id'
        ],
        message: 'Expected number, received string'
      }
    ],
    addIssue: [Function (anonymous)],
    addIssues: [Function (anonymous)],
    errors: [
      {
        code: 'invalid_type',
        expected: 'number',
        received: 'string',
        path: [
          'MediaContainerWithMetadata',
          'MediaContainer',
          'Metadata',
          0,
          'Guid',
          0,
          'id'
        ],
        message: 'Expected number, received string'
      }
    ]
  },
  rawValue: {
    ContentType: 'application/json',
    StatusCode: 200,
    RawResponse: Response {
      [Symbol(state)]: {
        aborted: false,
        rangeRequested: false,
        timingAllowPassed: true,
        requestIncludesCredentials: true,
        type: 'default',
        status: 200,
        timingInfo: {
          startTime: 543.779728,
          redirectStartTime: 0,
          redirectEndTime: 0,
          postRedirectStartTime: 543.779728,
          finalServiceWorkerStartTime: 0,
          finalNetworkResponseStartTime: 597.821572,
          finalNetworkRequestStartTime: 583.281709,
          endTime: 614.34544,
          encodedBodySize: 1016,
          decodedBodySize: 2296,
          finalConnectionTimingInfo: {
            domainLookupStartTime: 543.779728,
            domainLookupEndTime: 543.779728,
            connectionStartTime: 543.779728,
            connectionEndTime: 543.779728,
            secureConnectionStartTime: 543.779728,
            ALPNNegotiatedProtocol: undefined
          }
        },
        cacheState: '',
        statusText: 'OK',
        headersList: HeadersList {
          cookies: null,
          [Symbol(headers map)]: Map(10) {
            'alt-svc' => [Object],
            'cache-control' => [Object],
            'content-encoding' => [Object],
            'content-length' => [Object],
            'content-type' => [Object],
            'date' => [Object],
            'via' => [Object],
            'x-plex-content-compressed-length' => [Object],
            'x-plex-content-original-length' => [Object],
            'x-plex-protocol' => [Object]
          },
          [Symbol(headers map sorted)]: [
            [Array], [Array],
            [Array], [Array],
            [Array], [Array],
            [Array], [Array],
            [Array], [Array]
          ]
        },
        urlList: [ URL {} ],
        body: {
          stream: ReadableStream {
            [Symbol(kType)]: 'ReadableStream',
            [Symbol(kState)]: [Object: null prototype],
            [Symbol(nodejs.webstream.isClosedPromise)]: [Object],
            [Symbol(nodejs.webstream.controllerErrorFunction)]: [Function (anonymous)]
          },
          source: null,
          length: null
        }
      },
      [Symbol(headers)]: Headers {}
    },
    Headers: {
      'alt-svc': [ 'h3=":11354"; ma=2592000' ],
      'cache-control': [ 'no-cache' ],
      'content-encoding': [ 'gzip' ],
      'content-length': [ '1016' ],
      'content-type': [ 'application/json' ],
      date: [ 'Thu', '01 Jan 2026 17:30:52 GMT' ],
      via: [ '1.1 Caddy' ],
      'x-plex-content-compressed-length': [ '1016' ],
      'x-plex-content-original-length': [ '2296' ],
      'x-plex-protocol': [ '1.0' ]
    },
    MediaContainerWithMetadata: {
      MediaContainer: {
        size: 1,
        allowSync: true,
        identifier: 'com.plexapp.plugins.library',
        librarySectionID: 1,
        librarySectionTitle: 'Music',
        librarySectionUUID: '2bc6b969-87bf-4edb-a2a1-7020a3c6ef28',
        mediaTagPrefix: '/system/bundle/media/flags/',
        mediaTagVersion: 1758205129,
        Metadata: [
          {
            ratingKey: '16831',
            key: '/library/metadata/16831',
            parentRatingKey: '16829',
            grandparentRatingKey: '5061',
            guid: 'plex://track/6931c1e0844185ecc6c4918e',
            parentGuid: 'plex://album/6931c1df844185ecc6c48dd8',
            grandparentGuid: 'plex://artist/602458a7adc8af002c821e4f',
            parentStudio: 'deadAir',
            type: 'track',
            title: 'So What?',
            grandparentKey: '/library/metadata/5061',
            parentKey: '/library/metadata/16829',
            librarySectionTitle: 'Music',
            librarySectionID: 1,
            librarySectionKey: '/library/sections/1',
            grandparentTitle: 'Jane Remover',
            parentTitle: '♡',
            summary: '',
            index: 2,
            parentIndex: 1,
            userRating: 10,
            viewCount: 48,
            skipCount: 1,
            lastViewedAt: 1766530271,
            lastRatedAt: 1766414868,
            parentYear: 2025,
            thumb: '/library/metadata/16829/thumb/1764963474',
            parentThumb: '/library/metadata/16829/thumb/1764963474',
            grandparentThumb: '/library/metadata/5061/thumb/1766462550',
            duration: 246442,
            addedAt: 1764963472,
            updatedAt: 1764963474,
            musicAnalysisVersion: '1',
            Media: [Array],
            Image: [Array],
            Guid: [Array],
            Field: [Array]
          }
        ]
      }
    }
  },
  rawMessage: 'Response validation failed'
}

FoxxMD added 2 commits January 5, 2026 18:10
* Use Keyv memory cache
* Return undefined instead of null to simplify assignment
* Throw error with cause for clearer logging
@FoxxMD FoxxMD added the safe to test trusted to build image label Jan 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 5, 2026

📦 A new release has been made for this pull request.

To play around with this PR, pull an image:

  • foxxmd/multi-scrobbler:pr-426

Images are available for x86_64 and ARM64.

Latest commit: a7a99ee

@FoxxMD
Copy link
Copy Markdown
Owner

FoxxMD commented Jan 5, 2026

@owendaprile I'm going to use your implementation after all. It's simpler than trying to make plexjs cooperate 😒

I've updated the branch with some commits:

  • Replaced your cache implementation to use the existing Keyv memory cache
  • Using undefined instead of null for cache results to simplify assignment
  • Added a request timeout for cache calls

If everything looks good to you (can try testing the published docker image foxxmd/multi-scrobbler:pr-426) I'll go ahead and merge.

@owendaprile
Copy link
Copy Markdown
Contributor Author

Seems to be working the same. I'm scrobbling to teal.fm so without #425 I don't see artist MBIDs, but both release and recording MBIDs are coming through: https://pdsls.dev/at://did:plc:zhxv5pxpmojhnvaqy4mwailv/fm.teal.alpha.feed.play/3mbp2vwukcv2k

@FoxxMD FoxxMD merged commit 2485267 into FoxxMD:master Jan 5, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test trusted to build image

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants