Merge pull request #426 from owendaprile/push-ursmqpqmrwns

feat(plex): Retrieve MusicBrainz IDs
This commit is contained in:
Matt Foxx
2026-01-05 15:47:17 -05:00
committed by GitHub
2 changed files with 93 additions and 5 deletions
+1 -1
View File
@@ -181,7 +181,7 @@ export class MSCache {
}
export const initMemoryCache = (opts: Parameters<typeof createKeyv>[0] = {}): Keyv | KeyvStoreAdapter => {
export const initMemoryCache = <T = any>(opts: Parameters<typeof createKeyv>[0] = {}): Keyv<T> | KeyvStoreAdapter => {
const {
ttl = '1h',
lruSize = 200,
+92 -4
View File
@@ -26,6 +26,8 @@ import { Logger } from '@foxxmd/logging';
import { MemoryPositionalSource } from './MemoryPositionalSource.js';
import { FixedSizeList } from 'fixed-size-list';
import { SDKValidationError } from '@lukehagar/plexjs/sdk/models/errors/sdkvalidationerror.js';
import { Keyv } from 'cacheable';
import { initMemoryCache } from "../common/Cache.js";
const shortDeviceId = truncateStringToLength(10, '');
@@ -39,6 +41,8 @@ export default class PlexApiSource extends MemoryPositionalSource {
plexApi: PlexAPI;
plexUser: string;
httpClient: HTTPClient;
deviceId: string;
address: URLData;
@@ -56,6 +60,8 @@ export default class PlexApiSource extends MemoryPositionalSource {
uniqueDropReasons: FixedSizeList<string>;
libraries: {name: string, collectionType: string, uuid: string}[] = [];
private mbIdCache: Keyv<string>;
declare config: PlexApiSourceConfig;
@@ -68,9 +74,11 @@ export default class PlexApiSource extends MemoryPositionalSource {
this.deviceId = `${name}-ms${internal.version}-${truncateStringToLength(10, '')(hashObject(config))}`;
this.uniqueDropReasons = new FixedSizeList<string>(100);
this.mediaIdsSeen = new FixedSizeList<string>(100);
this.mbIdCache = initMemoryCache<string | null>({lruSize: 1000, ttl: '1m'}) as Keyv<string | null>;
}
protected async doBuildInitData(): Promise<true | string | undefined> {
this.regexCache
const {
data: {
token,
@@ -123,8 +131,6 @@ export default class PlexApiSource extends MemoryPositionalSource {
this.address = normalizeWebAddress(this.config.data.url);
this.logger.debug(`Config URL: ${this.config.data.url} | Normalized: ${this.address.toString()}`);
let httpClient: HTTPClient | undefined;
if(ignoreInvalidCert) {
this.logger.debug('Using http client that ignores self-signed certs');
@@ -145,13 +151,15 @@ export default class PlexApiSource extends MemoryPositionalSource {
return fetch(input, {...init, dispatcher: bypassAgent});
}
};
httpClient = new HTTPClient({ fetcher: bypassFetcher });
this.httpClient = new HTTPClient({ fetcher: bypassFetcher });
} else {
this.httpClient = new HTTPClient();
}
this.plexApi = new PlexAPI({
serverURL: this.address.url.toString(),
accessToken: this.config.data.token,
httpClient
httpClient: this.httpClient
});
return true;
@@ -396,6 +404,32 @@ export default class PlexApiSource extends MemoryPositionalSource {
for(const sessionData of allSessions) {
const validPlay = this.isActivityValid(sessionData[0], sessionData[1]);
if(validPlay === true) {
// Pull MBIDs for track, album, and artist.
const [trackMbId, albumMbId, albumArtistMbId] = await Promise.all([
this.getMusicBrainzId(sessionData[1].ratingKey),
this.getMusicBrainzId(sessionData[1].parentRatingKey),
this.getMusicBrainzId(sessionData[1].grandparentRatingKey),
]);
if (!sessionData[0].play.data.meta) {
sessionData[0].play.data.meta = {};
}
const prevBrainzMeta = sessionData[0].play.data.meta.brainz ?? {};
sessionData[0].play.data.meta.brainz = {
...prevBrainzMeta,
track: trackMbId,
album: albumMbId,
// Plex doesn't track MBIDs for track artists, so we use the
// album artist MBID instead.
artist: albumArtistMbId !== undefined
? [...new Set([...(prevBrainzMeta.artist ?? []), albumArtistMbId])]
: prevBrainzMeta.artist,
albumArtist: albumArtistMbId !== undefined
? [...new Set([...(prevBrainzMeta.albumArtist ?? []), albumArtistMbId])]
: prevBrainzMeta.albumArtist,
};
validSessions.push(sessionData[0]);
} else if(this.logFilterFailure !== false) {
const stateIdentifyingInfo = buildStatePlayerPlayIdententifyingInfo(sessionData[0]);
@@ -485,6 +519,60 @@ ${JSON.stringify(obj)}`);
}
getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new PlexPlayerState(logger, id, opts);
getMusicBrainzId = async (ratingKey: string | undefined): Promise<string | undefined> => {
if (!ratingKey) {
return null;
}
const cachedMbId = await this.mbIdCache.get(ratingKey);
if (cachedMbId !== undefined && cachedMbId !== null) {
return cachedMbId;
}
if(cachedMbId === null) {
return undefined;
}
try {
const signal = AbortSignal.timeout(5000); // reasonable 5s timeout
// The current version of plexjs (0.39.0) does not return the GUID
// fields, so we make the call manually.
const request = await this.httpClient.request(
new Request(
new URL(`/library/metadata/${ratingKey}`, this.address.url),
{
method: "GET",
headers: {
"X-Plex-Token": this.config.data.token,
"Accept": "application/json",
},
signal
}
)
);
const result = await request.json();
// There shouldn't be multiple metadata or GUID objects, but we return
// the first MBID to be safe.
for (const metadata of result.MediaContainer.Metadata ?? []) {
for (const guid of metadata.Guid ?? []) {
if (typeof guid.id === "string" && guid.id.startsWith("mbid://")) {
const mbid = guid.id.replace("mbid://", "");
await this.mbIdCache.set(ratingKey, mbid);
return mbid;
}
}
}
} catch (e) {
this.logger.warn(new Error(`Failed to get MusicBrainz IDs from Plex for item ${ratingKey}`, {cause: e}));
}
this.mbIdCache.set(ratingKey, null);
return undefined;
}
}
async function streamToString(stream: any) {