Skip to content

Commit 2485267

Browse files
authored
Merge pull request #426 from owendaprile/push-ursmqpqmrwns
feat(plex): Retrieve MusicBrainz IDs
2 parents 46f4fbe + a7a99ee commit 2485267

File tree

2 files changed

+93
-5
lines changed

2 files changed

+93
-5
lines changed

src/backend/common/Cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export class MSCache {
181181
}
182182

183183

184-
export const initMemoryCache = (opts: Parameters<typeof createKeyv>[0] = {}): Keyv | KeyvStoreAdapter => {
184+
export const initMemoryCache = <T = any>(opts: Parameters<typeof createKeyv>[0] = {}): Keyv<T> | KeyvStoreAdapter => {
185185
const {
186186
ttl = '1h',
187187
lruSize = 200,

src/backend/sources/PlexApiSource.ts

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { Logger } from '@foxxmd/logging';
2626
import { MemoryPositionalSource } from './MemoryPositionalSource.js';
2727
import { FixedSizeList } from 'fixed-size-list';
2828
import { SDKValidationError } from '@lukehagar/plexjs/sdk/models/errors/sdkvalidationerror.js';
29+
import { Keyv } from 'cacheable';
30+
import { initMemoryCache } from "../common/Cache.js";
2931

3032
const 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

490578
async function streamToString(stream: any) {

0 commit comments

Comments
 (0)