@@ -26,6 +26,8 @@ import { Logger } from '@foxxmd/logging';
2626import { MemoryPositionalSource } from './MemoryPositionalSource.js' ;
2727import { FixedSizeList } from 'fixed-size-list' ;
2828import { SDKValidationError } from '@lukehagar/plexjs/sdk/models/errors/sdkvalidationerror.js' ;
29+ import { Keyv } from 'cacheable' ;
30+ import { initMemoryCache } from "../common/Cache.js" ;
2931
3032const shortDeviceId = truncateStringToLength ( 10 , '' ) ;
3133
@@ -39,6 +41,8 @@ export default class PlexApiSource extends MemoryPositionalSource {
3941 plexApi : PlexAPI ;
4042 plexUser : string ;
4143
44+ httpClient : HTTPClient ;
45+
4246 deviceId : string ;
4347
4448 address : URLData ;
@@ -56,6 +60,8 @@ export default class PlexApiSource extends MemoryPositionalSource {
5660 uniqueDropReasons : FixedSizeList < string > ;
5761
5862 libraries : { name : string , collectionType : string , uuid : string } [ ] = [ ] ;
63+
64+ private mbIdCache : Keyv < string > ;
5965
6066 declare config : PlexApiSourceConfig ;
6167
@@ -68,9 +74,11 @@ export default class PlexApiSource extends MemoryPositionalSource {
6874 this . deviceId = `${ name } -ms${ internal . version } -${ truncateStringToLength ( 10 , '' ) ( hashObject ( config ) ) } ` ;
6975 this . uniqueDropReasons = new FixedSizeList < string > ( 100 ) ;
7076 this . mediaIdsSeen = new FixedSizeList < string > ( 100 ) ;
77+ this . mbIdCache = initMemoryCache < string | null > ( { lruSize : 1000 , ttl : '1m' } ) as Keyv < string | null > ;
7178 }
7279
7380 protected async doBuildInitData ( ) : Promise < true | string | undefined > {
81+ this . regexCache
7482 const {
7583 data : {
7684 token,
@@ -123,8 +131,6 @@ export default class PlexApiSource extends MemoryPositionalSource {
123131 this . address = normalizeWebAddress ( this . config . data . url ) ;
124132 this . logger . debug ( `Config URL: ${ this . config . data . url } | Normalized: ${ this . address . toString ( ) } ` ) ;
125133
126- let httpClient : HTTPClient | undefined ;
127-
128134 if ( ignoreInvalidCert ) {
129135 this . logger . debug ( 'Using http client that ignores self-signed certs' ) ;
130136
@@ -145,13 +151,15 @@ export default class PlexApiSource extends MemoryPositionalSource {
145151 return fetch ( input , { ...init , dispatcher : bypassAgent } ) ;
146152 }
147153 } ;
148- httpClient = new HTTPClient ( { fetcher : bypassFetcher } ) ;
154+ this . httpClient = new HTTPClient ( { fetcher : bypassFetcher } ) ;
155+ } else {
156+ this . httpClient = new HTTPClient ( ) ;
149157 }
150158
151159 this . plexApi = new PlexAPI ( {
152160 serverURL : this . address . url . toString ( ) ,
153161 accessToken : this . config . data . token ,
154- httpClient
162+ httpClient : this . httpClient
155163 } ) ;
156164
157165 return true ;
@@ -396,6 +404,32 @@ export default class PlexApiSource extends MemoryPositionalSource {
396404 for ( const sessionData of allSessions ) {
397405 const validPlay = this . isActivityValid ( sessionData [ 0 ] , sessionData [ 1 ] ) ;
398406 if ( validPlay === true ) {
407+ // Pull MBIDs for track, album, and artist.
408+ const [ trackMbId , albumMbId , albumArtistMbId ] = await Promise . all ( [
409+ this . getMusicBrainzId ( sessionData [ 1 ] . ratingKey ) ,
410+ this . getMusicBrainzId ( sessionData [ 1 ] . parentRatingKey ) ,
411+ this . getMusicBrainzId ( sessionData [ 1 ] . grandparentRatingKey ) ,
412+ ] ) ;
413+
414+ if ( ! sessionData [ 0 ] . play . data . meta ) {
415+ sessionData [ 0 ] . play . data . meta = { } ;
416+ }
417+
418+ const prevBrainzMeta = sessionData [ 0 ] . play . data . meta . brainz ?? { } ;
419+ sessionData [ 0 ] . play . data . meta . brainz = {
420+ ...prevBrainzMeta ,
421+ track : trackMbId ,
422+ album : albumMbId ,
423+ // Plex doesn't track MBIDs for track artists, so we use the
424+ // album artist MBID instead.
425+ artist : albumArtistMbId !== undefined
426+ ? [ ...new Set ( [ ...( prevBrainzMeta . artist ?? [ ] ) , albumArtistMbId ] ) ]
427+ : prevBrainzMeta . artist ,
428+ albumArtist : albumArtistMbId !== undefined
429+ ? [ ...new Set ( [ ...( prevBrainzMeta . albumArtist ?? [ ] ) , albumArtistMbId ] ) ]
430+ : prevBrainzMeta . albumArtist ,
431+ } ;
432+
399433 validSessions . push ( sessionData [ 0 ] ) ;
400434 } else if ( this . logFilterFailure !== false ) {
401435 const stateIdentifyingInfo = buildStatePlayerPlayIdententifyingInfo ( sessionData [ 0 ] ) ;
@@ -485,6 +519,60 @@ ${JSON.stringify(obj)}`);
485519 }
486520
487521 getNewPlayer = ( logger : Logger , id : PlayPlatformId , opts : PlayerStateOptions ) => new PlexPlayerState ( logger , id , opts ) ;
522+
523+ getMusicBrainzId = async ( ratingKey : string | undefined ) : Promise < string | undefined > => {
524+ if ( ! ratingKey ) {
525+ return null ;
526+ }
527+
528+ const cachedMbId = await this . mbIdCache . get ( ratingKey ) ;
529+ if ( cachedMbId !== undefined && cachedMbId !== null ) {
530+ return cachedMbId ;
531+ }
532+ if ( cachedMbId === null ) {
533+ return undefined ;
534+ }
535+
536+ try {
537+ const signal = AbortSignal . timeout ( 5000 ) ; // reasonable 5s timeout
538+
539+ // The current version of plexjs (0.39.0) does not return the GUID
540+ // fields, so we make the call manually.
541+ const request = await this . httpClient . request (
542+ new Request (
543+ new URL ( `/library/metadata/${ ratingKey } ` , this . address . url ) ,
544+ {
545+ method : "GET" ,
546+ headers : {
547+ "X-Plex-Token" : this . config . data . token ,
548+ "Accept" : "application/json" ,
549+ } ,
550+ signal
551+ }
552+ )
553+ ) ;
554+
555+ const result = await request . json ( ) ;
556+
557+ // There shouldn't be multiple metadata or GUID objects, but we return
558+ // the first MBID to be safe.
559+ for ( const metadata of result . MediaContainer . Metadata ?? [ ] ) {
560+ for ( const guid of metadata . Guid ?? [ ] ) {
561+ if ( typeof guid . id === "string" && guid . id . startsWith ( "mbid://" ) ) {
562+ const mbid = guid . id . replace ( "mbid://" , "" ) ;
563+
564+ await this . mbIdCache . set ( ratingKey , mbid ) ;
565+ return mbid ;
566+ }
567+ }
568+ }
569+ } catch ( e ) {
570+ this . logger . warn ( new Error ( `Failed to get MusicBrainz IDs from Plex for item ${ ratingKey } ` , { cause : e } ) ) ;
571+ }
572+
573+ this . mbIdCache . set ( ratingKey , null ) ;
574+ return undefined ;
575+ }
488576}
489577
490578async function streamToString ( stream : any ) {
0 commit comments