diff --git a/lib/Caches/AlbumCache.ts b/lib/Caches/AlbumCache.ts new file mode 100644 index 0000000..3b26f05 --- /dev/null +++ b/lib/Caches/AlbumCache.ts @@ -0,0 +1,26 @@ +import { actions, store } from "@neptune"; +import type { Album } from "neptune-types/tidal"; +import { interceptPromise } from "../intercept/interceptPromise"; +import { undefinedWarn } from "../undefinedError"; + +export class AlbumCache { + private static readonly _cache: Record = {}; + public static async get(albumId?: number) { + if (albumId === undefined) return undefined; + + let mediaItem = this._cache[albumId]; + if (mediaItem !== undefined) return mediaItem; + + const mediaItems: Record = store.getState().content.albums; + for (const itemId in mediaItems) this._cache[itemId] = mediaItems[itemId]; + + if (this._cache[albumId] === undefined) { + const album = await interceptPromise(() => actions.content.loadAlbum({ albumId }), ["content/LOAD_ALBUM_SUCCESS"], []) + .then((res) => res?.[0].album) + .catch(undefinedWarn("AlbumCache.get")); + if (album !== undefined) this._cache[albumId] = album; + } + + return this._cache[albumId]; + } +} diff --git a/lib/TrackCache/ExtendedTrackItem.ts b/lib/Caches/ExtendedTrackItem.ts similarity index 91% rename from lib/TrackCache/ExtendedTrackItem.ts rename to lib/Caches/ExtendedTrackItem.ts index 25bf6fe..e281f21 100644 --- a/lib/TrackCache/ExtendedTrackItem.ts +++ b/lib/Caches/ExtendedTrackItem.ts @@ -1,11 +1,12 @@ import { actions } from "@neptune"; import { ItemId, TrackItem, Album } from "neptune-types/tidal"; -import { interceptPromise } from "../interceptPromise"; +import { interceptPromise } from "../intercept/interceptPromise"; import { MusicBrainz } from "../musicbrainzApi"; import { Recording } from "../musicbrainzApi/types/Recording"; import { Release } from "../musicbrainzApi/types/UPCData"; import { TrackItemCache } from "./TrackItemCache"; import { undefinedWarn } from "../undefinedError"; +import { AlbumCache } from "./AlbumCache"; export class ExtendedTrackItem { public readonly trackId: ItemId; @@ -38,9 +39,7 @@ export class ExtendedTrackItem { } public async album(): Promise { if (this._album !== undefined) return this._album; - - actions.content.loadAlbum({ albumId: this.trackItem()?.album?.id! }); - return (this._album = await interceptPromise(["content/LOAD_ALBUM_SUCCESS"], []).then((res) => res?.[0].album)); + return (this._album = await AlbumCache.get(this.trackItem()?.album?.id)); } public async recording(): Promise { if (this._recording !== undefined) return this._recording; diff --git a/lib/TrackCache/TrackItemCache.ts b/lib/Caches/TrackItemCache.ts similarity index 73% rename from lib/TrackCache/TrackItemCache.ts rename to lib/Caches/TrackItemCache.ts index 5134743..e84552e 100644 --- a/lib/TrackCache/TrackItemCache.ts +++ b/lib/Caches/TrackItemCache.ts @@ -1,6 +1,6 @@ import { store } from "@neptune"; -import { TrackItem, MediaItem, ItemId } from "neptune-types/tidal"; -import { undefinedWarn } from "../undefinedError"; +import type { TrackItem, MediaItem, ItemId } from "neptune-types/tidal"; + export class TrackItemCache { private static readonly _cache: Record = {}; public static get(trackId?: ItemId) { @@ -16,7 +16,6 @@ export class TrackItemCache { this._cache[itemId] = item; } - mediaItem = this._cache[trackId]; - if (mediaItem !== undefined) return mediaItem; + return this._cache[trackId]; } } diff --git a/lib/interceptActions.ts b/lib/intercept/interceptActions.ts similarity index 100% rename from lib/interceptActions.ts rename to lib/intercept/interceptActions.ts diff --git a/lib/interceptPromise.ts b/lib/intercept/interceptPromise.ts similarity index 62% rename from lib/interceptPromise.ts rename to lib/intercept/interceptPromise.ts index e156501..7e0d4b4 100644 --- a/lib/interceptPromise.ts +++ b/lib/intercept/interceptPromise.ts @@ -1,16 +1,31 @@ import { intercept } from "@neptune"; import { ActionType, CallbackFunction, PayloadActionTypeTuple } from "neptune-types/api/intercept"; -export const interceptPromise = (resActionType: RESAT[], rejActionType: REJAT[], timeoutMs = 5000): Promise> => { +export const interceptPromise = ( + trigger: Function, + resActionType: RESAT[], + rejActionType: REJAT[], + { timeoutMs, cancel }: { timeoutMs?: number; cancel?: boolean } = {} +): Promise> => { + timeoutMs ??= 5000; + cancel ??= false; let res: CallbackFunction; let rej: (err: PayloadActionTypeTuple | string) => void; const p = new Promise>((_res, _rej) => { res = _res; rej = _rej; }); - const unloadRes = intercept(resActionType, res!, true); + const unloadRes = intercept( + resActionType, + (payload) => { + res(payload); + if (cancel) return true; + }, + true + ); const unloadRej = intercept(rejActionType, rej!, true); const timeout = setTimeout(() => rej(`${rejActionType}_TIMEOUT`), timeoutMs); + trigger(); return p.finally(() => { clearTimeout(timeout); unloadRes(); diff --git a/lib/tidalDevApi/isrc.ts b/lib/tidalDevApi/isrc.ts index 92a36a4..949416b 100644 --- a/lib/tidalDevApi/isrc.ts +++ b/lib/tidalDevApi/isrc.ts @@ -1,4 +1,4 @@ -import { ISRCResponse, TrackData } from "./types/ISRC"; +import { Datum, ISRCResponse, Resource } from "./types/ISRC"; import { requestStream, rejectNotOk, toJson } from "../fetch"; import { getToken } from "./auth"; @@ -18,7 +18,7 @@ export const fetchIsrc = async (isrc: string, options?: ISRCOptions) => { .then(toJson); }; -export async function* fetchIsrcIterable(isrc: string): AsyncIterable { +export async function* fetchIsrcIterable(isrc: string): AsyncIterable { let offset = 0; const limit = 100; while (true) { diff --git a/lib/tidalDevApi/types/ISRC.ts b/lib/tidalDevApi/types/ISRC.ts index c7bf0e1..710c16b 100644 --- a/lib/tidalDevApi/types/ISRC.ts +++ b/lib/tidalDevApi/types/ISRC.ts @@ -1,14 +1,58 @@ -import type { TrackItem } from "neptune-types/tidal"; +export interface Datum { + resource?: Resource; + id?: string; + status?: number; + message?: string; +} -export type TrackData = { - resource: TrackItem; - id: string; - status: number; - message: string; -}; +export interface Resource { + artifactType?: string; + id?: string; + title?: string; + artists?: Artist[]; + album?: Album; + duration?: number; + trackNumber?: number; + volumeNumber?: number; + isrc?: string; + copyright?: string; + mediaMetadata?: MediaMetadata; + properties?: Record; + tidalUrl?: string; +} + +interface Album { + id?: string; + title?: string; + imageCover?: ImageCover[]; + videoCover?: any[]; +} + +interface ImageCover { + url?: string; + width?: number; + height?: number; +} + +interface Artist { + id?: string; + name?: string; + picture?: ImageCover[]; + main?: boolean; +} + +interface MediaMetadata { + tags?: string[]; +} + +export interface Metadata { + requested?: number; + success?: number; + failure?: number; +} export type ISRCResponse = { - data: TrackData[]; + data: Datum[]; metadata: { requested: number; success: number; diff --git a/plugins/LastFM/src/index.ts b/plugins/LastFM/src/index.ts index dc1c1b6..d2ecc4b 100644 --- a/plugins/LastFM/src/index.ts +++ b/plugins/LastFM/src/index.ts @@ -13,7 +13,7 @@ export { Settings } from "./Settings"; // @ts-expect-error Remove this when types are available import { storage } from "@plugin"; import { undefinedWarn } from "../../../lib/undefinedError"; -import { ExtendedTrackItem } from "../../../lib/TrackCache/ExtendedTrackItem"; +import { ExtendedTrackItem } from "../../../lib/Caches/ExtendedTrackItem"; let totalPlayTime = 0; let lastPlayStart: number | null = null; diff --git a/plugins/RealMAX/src/index.ts b/plugins/RealMAX/src/index.ts index d9c6d48..96ab5ed 100644 --- a/plugins/RealMAX/src/index.ts +++ b/plugins/RealMAX/src/index.ts @@ -1,9 +1,12 @@ import { ItemId, TrackItem } from "neptune-types/tidal"; -import { TrackItemCache } from "../../../lib/TrackCache/TrackItemCache"; +import { TrackItemCache } from "../../../lib/Caches/TrackItemCache"; import { fetchIsrcIterable } from "../../../lib/tidalDevApi/isrc"; import { actions, intercept, store } from "@neptune"; import { PlaybackContext } from "../../../lib/AudioQualityTypes"; -import { ExtendedTrackItem } from "../../../lib/TrackCache/ExtendedTrackItem"; +import { ExtendedTrackItem } from "../../../lib/Caches/ExtendedTrackItem"; +import { Resource } from "../../../lib/tidalDevApi/types/ISRC"; +import { interceptPromise } from "../../../lib/intercept/interceptPromise"; +import { debounce } from "../../../lib/debounce"; const hasHiRes = (trackItem: TrackItem) => { const tags = trackItem.mediaMetadata?.tags; @@ -12,48 +15,54 @@ const hasHiRes = (trackItem: TrackItem) => { }; class MaxTrack { - private static readonly _idMap: Record> = {}; - public static async getMaxId(itemId: ItemId | undefined): Promise { - if (itemId === undefined) return false; + private static readonly _idMap: Record> = {}; + public static async fastCacheMaxId(itemId: ItemId): Promise { + if (itemId === undefined) return undefined; + return MaxTrack._idMap[itemId]; + } + public static async getMaxId(itemId: ItemId | undefined): Promise { + if (itemId === undefined) return undefined; const idMapping = MaxTrack._idMap[itemId]; if (idMapping !== undefined) return idMapping; const extTrackItem = await ExtendedTrackItem.get(itemId); const trackItem = extTrackItem?.trackItem(); - if (trackItem !== undefined && hasHiRes(trackItem)) return false; + if (trackItem !== undefined && hasHiRes(trackItem)) return undefined; const isrcs = await extTrackItem?.isrcs(); - if (isrcs === undefined) return (this._idMap[itemId] = Promise.resolve(false)); + if (isrcs === undefined) return (this._idMap[itemId] = Promise.resolve(undefined)); return (this._idMap[itemId] = (async () => { for (const isrc of isrcs) { for await (const { resource } of fetchIsrcIterable(isrc)) { - if (resource?.id !== undefined && hasHiRes(resource)) return resource.id; + if (resource?.id !== undefined && hasHiRes(resource)) { + if (resource.artifactType !== "track") continue; + const maxTrackItem = TrackItemCache.get(resource?.id); + if (maxTrackItem !== undefined && !hasHiRes(maxTrackItem)) continue; + else return resource; + } } } - return false; + return undefined; })()); } } -// @ts-ignore intercept doesnt like async functions -const unloadPlay = intercept("playbackControls/MEDIA_PRODUCT_TRANSITION", async ([{ playbackContext }]: [{ playbackContext: PlaybackContext }]) => { - const { elements, currentIndex } = store.getState().playQueue; - for (let index = currentIndex; index < Math.min(elements.length - 1, currentIndex + 5); index++) { - const mediaItemId = elements[index]?.mediaItemId; - if (mediaItemId === undefined) return; - const maxId = await MaxTrack.getMaxId(mediaItemId); - const maxInjected = elements[index + 1]?.mediaItemId === maxId; - if (maxInjected) { - actions.playQueue.removeAtIndex({ index: index + 1 }); - actions.playQueue.movePrevious(); - } else if (index === currentIndex && maxId !== false) { - actions.playQueue.addNext({ mediaItemIds: [maxId] }); - actions.playQueue.moveNext(); - } - } -}); -export const onUnload = () => { - unloadPlay(); -}; +// export const onUnload = intercept( +// "playbackControls/TIME_UPDATE", +// debounce(async () => { +// const { elements, currentIndex } = store.getState().playQueue; +// const queueId = elements[currentIndex]?.mediaItemId; + +// const maxItem = await MaxTrack.getMaxId(queueId); +// if (maxItem !== undefined) { +// actions.playQueue.clearActiveItems(); +// await interceptPromise(() => actions.content.fetchAndPlayMediaItem({ itemId: maxItem?.id!, itemType: "track", sourceContext: { type: "user" } }), ["playbackControls/MEDIA_PRODUCT_TRANSITION"], []); +// const mediaItemIds = elements.slice(currentIndex + 1).map(({ mediaItemId }) => mediaItemId); +// actions.playQueue.addMediaItemsToQueue({ mediaItemIds, position: "next", options: { overwritePlayQueue: true }, sourceContext: { type: "user" } }); +// } +// // Preload next +// await MaxTrack.getMaxId(elements[currentIndex + 1]?.mediaItemId); +// }, 125) +// ); diff --git a/plugins/Shazam/src/index.ts b/plugins/Shazam/src/index.ts index b5c1329..c9b4a68 100644 --- a/plugins/Shazam/src/index.ts +++ b/plugins/Shazam/src/index.ts @@ -4,7 +4,7 @@ init(); import { actions, store } from "@neptune"; import { DecodedSignature } from "shazamio-core"; -import { interceptPromise } from "../../../lib/interceptPromise"; +import { interceptPromise } from "../../../lib/intercept/interceptPromise"; import { messageError, messageWarn, messageInfo } from "../../../lib/messageLogging"; import { fetchShazamData } from "./shazamApi/fetch"; @@ -15,8 +15,11 @@ import { fetchIsrc } from "../../../lib/tidalDevApi/isrc"; export { Settings } from "./Settings"; const addToPlaylist = async (playlistUUID: string, mediaItemIdsToAdd: string[]) => { - actions.content.addMediaItemsToPlaylist({ mediaItemIdsToAdd, onDupes: "SKIP", playlistUUID }); - await interceptPromise(["etag/SET_PLAYLIST_ETAG", "content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"], ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"]); + await interceptPromise( + () => actions.content.addMediaItemsToPlaylist({ mediaItemIdsToAdd, onDupes: "SKIP", playlistUUID }), + ["etag/SET_PLAYLIST_ETAG", "content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"], + ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"] + ); actions.content.loadListItemsPage({ listName: `playlists/${playlistUUID}`, listType: "mediaItems", reset: false }); setTimeout(() => actions.content.loadListItemsPage({ listName: `playlists/${playlistUUID}`, listType: "mediaItems", reset: true }), 1000); }; @@ -58,7 +61,10 @@ const handleDrop = async (event: DragEvent) => { const ids = (isrcData?.data ?? []).map((track) => track.id); if (ids.length > 0) { messageInfo(`Adding ${trackName} to playlist`); - await addToPlaylist(playlistUUID, ids); + await addToPlaylist( + playlistUUID, + ids.filter((id) => id !== undefined) + ); } else { console.log("[SHAZAM!]", shazamData); messageWarn(`Track ${trackName} is not avalible in Tidal`); diff --git a/plugins/SongDownloader/src/addMetadata.ts b/plugins/SongDownloader/src/addMetadata.ts index 58fe39f..8ac434f 100644 --- a/plugins/SongDownloader/src/addMetadata.ts +++ b/plugins/SongDownloader/src/addMetadata.ts @@ -5,9 +5,10 @@ import { ExtendedPlaybackInfoWithBytes } from "../../../lib/trackBytes/download" import { rejectNotOk, requestStream, toBuffer } from "../../../lib/fetch"; import { ManifestMimeType } from "../../../lib/trackBytes/getPlaybackInfo"; import { actions } from "@neptune"; -import { interceptPromise } from "../../../lib/interceptPromise"; +import { interceptPromise } from "../../../lib/intercept/interceptPromise"; import { type FlacTagMap, PictureType, createFlacTagsBuffer } from "./flac-tagger"; +import { AlbumCache } from "../../../lib/Caches/AlbumCache"; export async function addMetadata(trackInfo: ExtendedPlaybackInfoWithBytes, track: TrackItem) { if (trackInfo.manifestMimeType === ManifestMimeType.Tidal) { @@ -32,9 +33,8 @@ async function makeTags(track: TrackItem) { if (track.artist?.name) tagMap.artist = track.artist.name; tagMap.performer = (track.artists ?? []).map(({ name }) => name).filter((name) => name !== undefined); - if (track.id) { - actions.content.loadItemLyrics({ itemId: track.id, itemType: "track" }); - const lyrics = await interceptPromise(["content/LOAD_ITEM_LYRICS_SUCCESS"], ["content/LOAD_ITEM_LYRICS_FAIL"]) + if (track.id !== undefined) { + const lyrics = await interceptPromise(() => actions.content.loadItemLyrics({ itemId: track.id!, itemType: "track" }), ["content/LOAD_ITEM_LYRICS_SUCCESS"], ["content/LOAD_ITEM_LYRICS_FAIL"]) .catch(() => undefined) .then((res) => res?.[0]); if (lyrics?.lyrics !== undefined) tagMap.lyrics = lyrics.lyrics; @@ -43,10 +43,7 @@ async function makeTags(track: TrackItem) { const albumId = track.album?.id; let cover = track.album?.cover; if (albumId !== undefined) { - actions.content.loadAlbum({ albumId }); - const album = await interceptPromise(["content/LOAD_ALBUM_SUCCESS"], []) - .catch(() => undefined) - .then((res) => res?.[0].album); + const album = await AlbumCache.get(albumId); if (album !== undefined) { tagMap.albumArtist = (album.artists ?? []).map(({ name }) => name).filter((name) => name !== undefined); if (album.genre) tagMap.genres = album.genre; diff --git a/plugins/SongDownloader/src/index.ts b/plugins/SongDownloader/src/index.ts index 1214bfb..5eaf01c 100644 --- a/plugins/SongDownloader/src/index.ts +++ b/plugins/SongDownloader/src/index.ts @@ -10,13 +10,13 @@ import { fetchTrack, DownloadTrackOptions, TrackOptions } from "../../../lib/tra import { ItemId, MediaItem, TrackItem, VideoItem } from "neptune-types/tidal"; import { saveFile } from "./lib/saveFile"; -import { interceptPromise } from "../../../lib/interceptPromise"; +import { interceptPromise } from "../../../lib/intercept/interceptPromise"; import { messageError } from "../../../lib/messageLogging"; import { addMetadata } from "./addMetadata"; import { fileNameFromInfo } from "./lib/fileName"; import { toBuffer } from "../../../lib/fetch"; -import { TrackItemCache } from "../../../lib/TrackCache/TrackItemCache"; +import { TrackItemCache } from "../../../lib/Caches/TrackItemCache"; type DownloadButtoms = Record; const downloadButtons: DownloadButtoms = {}; @@ -69,13 +69,19 @@ const intercepts = [ export const onUnload = () => intercepts.forEach((unload) => unload()); const onAlbum = async (albumId: ItemId) => { - await actions.content.loadAllAlbumMediaItems({ albumId }); - const [{ mediaItems }] = await interceptPromise(["content/LOAD_ALL_ALBUM_MEDIA_ITEMS_SUCCESS"], ["content/LOAD_ALL_ALBUM_MEDIA_ITEMS_FAIL"]); + const [{ mediaItems }] = await interceptPromise( + () => actions.content.loadAllAlbumMediaItems({ albumId }), + ["content/LOAD_ALL_ALBUM_MEDIA_ITEMS_SUCCESS"], + ["content/LOAD_ALL_ALBUM_MEDIA_ITEMS_FAIL"] + ); downloadItems(Object.values(mediaItems).map((mediaItem) => mediaItem.item)); }; const onPlaylist = async (playlistUUID: ItemId) => { - await actions.content.loadListItemsPage({ loadAll: true, listName: `playlists/${playlistUUID}`, listType: "mediaItems" }); - const [{ items }] = await interceptPromise(["content/LOAD_LIST_ITEMS_PAGE_SUCCESS"], ["content/LOAD_LIST_ITEMS_PAGE_FAIL"]); + const [{ items }] = await interceptPromise( + () => actions.content.loadListItemsPage({ loadAll: true, listName: `playlists/${playlistUUID}`, listType: "mediaItems" }), + ["content/LOAD_LIST_ITEMS_PAGE_SUCCESS"], + ["content/LOAD_LIST_ITEMS_PAGE_FAIL"] + ); downloadItems(Object.values(items).map((mediaItem) => mediaItem?.item)); }; diff --git a/plugins/Testing/src/index.ts b/plugins/Testing/src/index.ts index 5a9cd1b..ed45ee7 100644 --- a/plugins/Testing/src/index.ts +++ b/plugins/Testing/src/index.ts @@ -1,3 +1,3 @@ -import { interceptActions } from "../../../lib/interceptActions"; +import { interceptActions } from "../../../lib/intercept/interceptActions"; export const onUnload = interceptActions(/.*/, console.log); diff --git a/plugins/TidalTags/src/index.ts b/plugins/TidalTags/src/index.ts index 0de3ae2..b52c82e 100644 --- a/plugins/TidalTags/src/index.ts +++ b/plugins/TidalTags/src/index.ts @@ -11,7 +11,7 @@ export { Settings } from "./Settings"; import { storage } from "@plugin"; import { isElement } from "./lib/isElement"; import { setInfoColumnHeaders, setInfoColumns } from "./setInfoColumns"; -import { TrackItemCache } from "../../../lib/TrackCache/TrackItemCache"; +import { TrackItemCache } from "../../../lib/Caches/TrackItemCache"; import { PlaybackContext } from "../../../lib/AudioQualityTypes"; import { getHeaders } from "../../../lib/fetch"; diff --git a/plugins/TidalTags/src/setFLACInfo.ts b/plugins/TidalTags/src/setFLACInfo.ts index 4b94366..31f02e3 100644 --- a/plugins/TidalTags/src/setFLACInfo.ts +++ b/plugins/TidalTags/src/setFLACInfo.ts @@ -20,7 +20,7 @@ flacInfoElem.textContent = ""; flacInfoElem.style.border = ""; const retryPromise = (getValue: () => T | Promise, options: { interval?: number; maxRetries?: number } = {}) => { - options.maxRetries ??= 40; + options.maxRetries ??= 200; options.interval ??= 250; let selectorInterval: NodeJS.Timeout; let retries = 0;