Skip to content

Commit

Permalink
RealMAX - Enable & Update to new api
Browse files Browse the repository at this point in the history
  • Loading branch information
Inrixia committed Jan 30, 2025
1 parent ea94499 commit 685901c
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 185 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,15 @@ Scrobbles and sets currently playing for [**ListenBrainz**](https://listenbrainz

## RealMAX

Tidal has quietly removed their api endpoint for looking up tracks by ISRC. This plugin no longer works due to this change, please disable it if you have it installed.

~~Install Url:~~
Install Url:

```
https://inrixia.github.io/neptune-plugins/RealMAX
```

~~Tidal oftern has multiple versions of the same song at different qualities.
Tidal oftern has multiple versions of the same song at different qualities.
With RealMAX when playing a song if there is a version available at a higher quality it will automatically be added as the next song in the queue and skipped to.
This ensures you are **always listening to the best quality of a song**~~
This ensures you are **always listening to the best quality of a song**

## Volume Scroll

Expand Down
160 changes: 79 additions & 81 deletions plugins/RealMAX/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import { settings } from "./Settings";

export { Settings } from "./Settings";

alert("RealMAX - Tidal has quietly removed their api endpoint for looking up tracks by ISRC. This plugin no longer works due to this change, please disable it.");

const unloadIntercept = intercept(
"playbackControls/MEDIA_PRODUCT_TRANSITION",
debounce(async () => {
Expand All @@ -38,85 +36,85 @@ const unloadIntercept = intercept(
}, 125)
);

// ContextMenu.onOpen(async (contextSource, contextMenu, trackItems) => {
// document.getElementById("realMax-button")?.remove();
// if (trackItems.length === 0 || !settings.displayMaxContextButton) return;
ContextMenu.onOpen(async (contextSource, contextMenu, trackItems) => {
document.getElementById("realMax-button")?.remove();
if (trackItems.length === 0 || !settings.displayMaxContextButton) return;

// let sourceName = trackItems[0].title;
// if (contextSource.type === "PLAYLIST") sourceName = store.getState().content.playlists.find((playlist) => playlist.uuid === contextSource.playlistId)?.title ?? sourceName;
// else if (contextSource.type === "ALBUM") sourceName = (await AlbumCache.get(+contextSource.albumId))?.title ?? sourceName;
// sourceName = `${sourceName} - RealMAX`;
let sourceName = trackItems[0].title;
if (contextSource.type === "PLAYLIST") sourceName = store.getState().content.playlists.find((playlist) => playlist.uuid === contextSource.playlistId)?.title ?? sourceName;
else if (contextSource.type === "ALBUM") sourceName = (await AlbumCache.get(+contextSource.albumId))?.title ?? sourceName;
sourceName = `${sourceName} - RealMAX`;

// const maxButton = document.createElement("button");
// maxButton.type = "button";
// maxButton.role = "menuitem";
// maxButton.textContent = trackItems.length > 1 ? `RealMAX - Process ${trackItems.length} tracks` : "RealMAX - Process track";
// maxButton.id = "realMax-button";
// maxButton.className = "context-button"; // Set class name for styling
// contextMenu.appendChild(maxButton);
// maxButton.addEventListener("click", async () => {
// maxButton.remove();
// const trackIds: number[] = [];
// let maxIdsFound = 0;
// for (const index in trackItems) {
// const trackItem = trackItems[index];
// const trackId = trackItem.id!;
// if (trackId === undefined) continue;
// const maxItem = await MaxTrack[settings.considerNewestRelease ? "getLatestMaxTrack" : "getMaxTrack"](trackId).catch(
// trace.msg.err.withContext(`Skipping adding ${trackItem.title} to ${sourceName}`)
// );
// if (maxItem === false || maxItem === undefined) continue;
// if (maxItem?.id !== undefined) {
// if ((await MediaItemCache.ensure(trackId)) !== undefined) {
// trace.msg.log(`Found Max quality for ${maxItem.title} in ${sourceName}! ${index}/${trackItems.length - 1} done.`);
// trackIds.push(+maxItem.id);
// maxIdsFound++;
// continue;
// }
// trace.msg.log(`Found Max quality for ${maxItem.title} in ${sourceName}, but track is unavailable... Skipping! ${index}/${trackItems.length - 1} done.`);
// trackIds.push(trackId);
// continue;
// }
// trace.msg.log(`${sourceName} - ${index}/${trackItems.length - 1} done. `);
// trackIds.push(trackId);
// }
// const [{ playlist }] = await interceptPromise(
// () =>
// actions.folders.createPlaylist({
// description: "Automatically generated by RealMAX",
// folderId: "root",
// fromPlaylist: undefined,
// isPublic: false,
// title: sourceName,
// // @ts-expect-error This works lol
// ids: trackIds.length > 450 ? undefined : trackIds,
// }),
// ["content/LOAD_PLAYLIST_SUCCESS"],
// ["content/LOAD_PLAYLIST_FAIL"]
// );
// if (trackIds.length > 500) {
// for (const trackIdsChunk of chunkArray(trackIds, 450)) {
// await interceptPromise(
// () =>
// actions.content.addMediaItemsToPlaylist({
// addToIndex: -1,
// mediaItemIdsToAdd: trackIdsChunk,
// onDupes: "ADD",
// playlistUUID: playlist.uuid!,
// }),
// ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"],
// ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"]
// );
// }
// }
// if (playlist?.uuid === undefined) {
// return trace.msg.err(`Failed to create playlist "${sourceName}"`);
// }
// trace.msg.err(`Successfully created RealMAX playlist "${sourceName}" - Found ${maxIdsFound} RealMAX replacements!`);
// });
// });
const maxButton = document.createElement("button");
maxButton.type = "button";
maxButton.role = "menuitem";
maxButton.textContent = trackItems.length > 1 ? `RealMAX - Process ${trackItems.length} tracks` : "RealMAX - Process track";
maxButton.id = "realMax-button";
maxButton.className = "context-button"; // Set class name for styling
contextMenu.appendChild(maxButton);
maxButton.addEventListener("click", async () => {
maxButton.remove();
const trackIds: number[] = [];
let maxIdsFound = 0;
for (const index in trackItems) {
const trackItem = trackItems[index];
const trackId = trackItem.id!;
if (trackId === undefined) continue;
const maxItem = await MaxTrack[settings.considerNewestRelease ? "getLatestMaxTrack" : "getMaxTrack"](trackId).catch(
trace.msg.err.withContext(`Skipping adding ${trackItem.title} to ${sourceName}`)
);
if (maxItem === false || maxItem === undefined) continue;
if (maxItem?.id !== undefined) {
if ((await MediaItemCache.ensure(trackId)) !== undefined) {
trace.msg.log(`Found Max quality for ${maxItem.title} in ${sourceName}! ${index}/${trackItems.length - 1} done.`);
trackIds.push(+maxItem.id);
maxIdsFound++;
continue;
}
trace.msg.log(`Found Max quality for ${maxItem.title} in ${sourceName}, but track is unavailable... Skipping! ${index}/${trackItems.length - 1} done.`);
trackIds.push(trackId);
continue;
}
trace.msg.log(`${sourceName} - ${index}/${trackItems.length - 1} done. `);
trackIds.push(trackId);
}
const [{ playlist }] = await interceptPromise(
() =>
actions.folders.createPlaylist({
description: "Automatically generated by RealMAX",
folderId: "root",
fromPlaylist: undefined,
isPublic: false,
title: sourceName,
// @ts-expect-error This works lol
ids: trackIds.length > 450 ? undefined : trackIds,
}),
["content/LOAD_PLAYLIST_SUCCESS"],
["content/LOAD_PLAYLIST_FAIL"]
);
if (trackIds.length > 500) {
for (const trackIdsChunk of chunkArray(trackIds, 450)) {
await interceptPromise(
() =>
actions.content.addMediaItemsToPlaylist({
addToIndex: -1,
mediaItemIdsToAdd: trackIdsChunk,
onDupes: "ADD",
playlistUUID: playlist.uuid!,
}),
["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"],
["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"]
);
}
}
if (playlist?.uuid === undefined) {
return trace.msg.err(`Failed to create playlist "${sourceName}"`);
}
trace.msg.err(`Successfully created RealMAX playlist "${sourceName}" - Found ${maxIdsFound} RealMAX replacements!`);
});
});

// export const onUnload = () => {
// unloadIntercept();
// safeUnload();
// };
export const onUnload = () => {
unloadIntercept();
safeUnload();
};
10 changes: 4 additions & 6 deletions plugins/Shazam/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { actions, store } from "@neptune";
import { interceptPromise } from "@inrixia/lib/intercept/interceptPromise";

import { fetchIsrc } from "@inrixia/lib/api/tidal";

import { Tracer } from "@inrixia/lib/trace";
const trace = Tracer("[Shazam]");

Expand Down Expand Up @@ -54,10 +52,10 @@ const handleDrop = async (event: DragEvent) => {
continue;
}
let trackToAdd;
// for await (trackToAdd of MaxTrack.getTracksFromISRC(isrc)) {
// // Break on first HiRes track. Otherwise trackToAdd will just be the final track found.
// if (MaxTrack.hasHiRes(trackToAdd)) break;
// }
for await (trackToAdd of MaxTrack.getTracksFromISRC(isrc)) {
// Break on first HiRes track. Otherwise trackToAdd will just be the final track found.
if (MaxTrack.hasHiRes(trackToAdd)) break;
}
if (trackToAdd !== undefined) {
trace.msg.log(`Adding ${prefix}...`);
return await addToPlaylist(playlistUUID, [trackToAdd.id!.toString()]);
Expand Down
10 changes: 5 additions & 5 deletions plugins/SongDownloader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ ContextMenu.onOpen(async (contextSource, contextMenu, trackItems) => {

const downloadTrack = async (trackItem: TrackItem, updateMethods: ButtonMethods, folderPath?: string) => {
let trackId = trackItem.id!;
// if (settings.useRealMAX && settings.desiredDownloadQuality === AudioQuality.HiRes) {
// updateMethods.set("Checking RealMAX for better quality...");
// const maxTrack = await MaxTrack.getMaxTrack(trackId);
// if (maxTrack !== false) trackId = +maxTrack.id!;
// }
if (settings.useRealMAX && settings.desiredDownloadQuality === AudioQuality.HiRes) {
updateMethods.set("Checking RealMAX for better quality...");
const maxTrack = await MaxTrack.getMaxTrack(trackId);
if (maxTrack !== false) trackId = +maxTrack.id!;
}

updateMethods.set("Fetching playback info & tags...");
const playbackInfo = PlaybackInfoCache.ensure(trackId, settings.desiredDownloadQuality);
Expand Down
24 changes: 15 additions & 9 deletions plugins/_lib/MaxTrack.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { fetchIsrcIterable, Resource } from "./api/tidal";
import { fetchIsrcIterable, TApiTrack } from "./api/tidal";
import { AsyncCachable } from "./Caches/AsyncCachable";
import { ExtendedMediaItem } from "./Caches/ExtendedTrackItem";
import { MediaItemCache } from "./Caches/MediaItemCache";
import { ItemId, MediaItem, TrackItem } from "neptune-types/tidal";

export type TrackFilter = (trackItem: Resource) => boolean;
export type TrackFilter = (trackItem: TApiTrack) => boolean;
export class MaxTrack {
public static getMaxTrack = AsyncCachable(async (itemId: ItemId): Promise<TrackItem | false> => {
const extTrackItem = await ExtendedMediaItem.get(itemId);
if (extTrackItem === undefined) return false;
if (extTrackItem.tidalTrack.contentType === "track" && this.hasHiRes(extTrackItem.tidalTrack)) return false;

for await (const trackItem of this.getTracksFromMediaItem(extTrackItem, this.hasHiRes)) {
for await (const trackItem of this.getTracksFromMediaItem(extTrackItem, this.hasHiResApi)) {
return trackItem;
}
return false;
Expand Down Expand Up @@ -55,17 +55,23 @@ export class MaxTrack {
}
}
public static async *getTracksFromISRC(isrc: string, filter?: TrackFilter): AsyncGenerator<TrackItem> {
for await (const { resource } of fetchIsrcIterable(isrc)) {
if (resource?.id === undefined) continue;
if (resource.artifactType !== "track") continue;
if (filter && !filter(resource)) continue;
const trackItem = await MediaItemCache.ensureTrack(resource?.id);
for await (const track of fetchIsrcIterable(isrc)) {
if (track.id === undefined) continue;
if (track.type !== "tracks") continue;
if (filter && !filter(track)) continue;
const trackItem = await MediaItemCache.ensureTrack(track.id);
if (trackItem !== undefined) yield trackItem;
}
}
public static hasHiRes(trackItem: Resource | TrackItem): boolean {
public static hasHiRes(trackItem: TrackItem): boolean {
const tags = trackItem.mediaMetadata?.tags;
if (tags === undefined) return false;
return tags.includes("HIRES_LOSSLESS");
}
public static hasHiResApi(apiTrack: TApiTrack): boolean {
const tags = apiTrack.attributes.mediaTags;
if (tags === undefined) return false;

return tags.includes("HIRES_LOSSLESS");
}
}
28 changes: 12 additions & 16 deletions plugins/_lib/api/tidal/isrc.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { Datum, ISRCResponse } from "./types";
import { TApiTrack, TApiTracks } from "./types";
import { getToken } from "./auth";
import { requestJsonCached } from "../../native/request/requestJsonCached";

type ISRCOptions = {
offset: number;
limit: number;
};
export const fetchIsrc = async (isrc: string, options?: ISRCOptions) => {
const { limit, offset } = options ?? { limit: 100, offset: 0 };
return requestJsonCached<ISRCResponse>(`https://openapi.tidal.com/tracks/byIsrc?isrc=${isrc}&countryCode=US&limit=${limit}&offset=${offset}`, {
const fetchTidal = async <T>(url: string) =>
requestJsonCached<T>(url, {
headers: {
Authorization: `Bearer ${await getToken()}`,
"Content-Type": "application/vnd.tidal.v1+json",
},
});
};

export async function* fetchIsrcIterable(isrc: string): AsyncIterable<Datum> {
let offset = 0;
const limit = 100;
const baseURL = "https://openapi.tidal.com/v2";

export async function* fetchIsrcIterable(isrc: string): AsyncIterable<TApiTrack> {
let next: string | undefined = `${baseURL}/tracks?countryCode=US&filter[isrc]=${isrc}`;
while (true) {
const response = await fetchIsrc(isrc, { limit, offset });
if (response?.data !== undefined) yield* response.data;
if (response.data.length < limit) break;
offset += limit;
if (next === undefined) break;
const resp: TApiTracks = await fetchTidal<TApiTracks>(next);
if (resp?.data === undefined || resp.data.length === 0) break;
yield* resp.data;
next = `${baseURL}${resp.links.next}`;
}
}
61 changes: 0 additions & 61 deletions plugins/_lib/api/tidal/types/ISRC.ts

This file was deleted.

Loading

0 comments on commit 685901c

Please sign in to comment.