mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Merge pull request #426 from owendaprile/push-ursmqpqmrwns
feat(plex): Retrieve MusicBrainz IDs
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user