From 10c6e0f7a08631b45eb6b2aa7bfc82639e203b52 Mon Sep 17 00:00:00 2001 From: Inrixia Date: Wed, 26 Jun 2024 06:53:25 +1200 Subject: [PATCH] SongDownloader - Working native metadata tagging --- plugins/SongDownloader/package-lock.json | 20 +----- plugins/SongDownloader/package.json | 3 +- plugins/SongDownloader/src/addMetadata.ts | 69 ------------------- plugins/SongDownloader/src/index.ts | 12 ++-- plugins/SongDownloader/src/makeTags.ts | 45 ++++++++++++ plugins/_lib/fetch.ts | 10 --- .../native/downloadTrack.native.ts | 43 ++++++++++-- plugins/_lib/package-lock.json | 18 +++++ plugins/_lib/package.json | 1 + 9 files changed, 112 insertions(+), 109 deletions(-) delete mode 100644 plugins/SongDownloader/src/addMetadata.ts create mode 100644 plugins/SongDownloader/src/makeTags.ts delete mode 100644 plugins/_lib/fetch.ts diff --git a/plugins/SongDownloader/package-lock.json b/plugins/SongDownloader/package-lock.json index 6735336..4f04e53 100644 --- a/plugins/SongDownloader/package-lock.json +++ b/plugins/SongDownloader/package-lock.json @@ -5,8 +5,7 @@ "packages": { "": { "dependencies": { - "@inrixia/lib": "../_lib", - "flac-stream-tagger": "^1.0.8" + "@inrixia/lib": "../_lib" } }, "../_lib": { @@ -19,23 +18,6 @@ "node_modules/@inrixia/lib": { "resolved": "../_lib", "link": true - }, - "node_modules/flac-stream-tagger": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/flac-stream-tagger/-/flac-stream-tagger-1.0.8.tgz", - "integrity": "sha512-kvlCUcdGcRppnVFZX0eZN94aWVZuSpMJqCHLNjciJSHbpi/I0PBrj7aVzOomiwkYHmHUcJ1h2j9ok7gjsy5pAA==", - "license": "SEE LICENSE.md", - "dependencies": { - "imageinfo": "^1.0.4" - } - }, - "node_modules/imageinfo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/imageinfo/-/imageinfo-1.0.4.tgz", - "integrity": "sha512-BJml4q/QCO2187F4UcO/b6hTYIhbq4nnd1XNs65jyCED9em4m6XmeGWDxjewjfJoC7VJABhOdmqb64KA24rLZw==", - "engines": { - "node": ">=0.4" - } } } } diff --git a/plugins/SongDownloader/package.json b/plugins/SongDownloader/package.json index 5368b58..b082805 100644 --- a/plugins/SongDownloader/package.json +++ b/plugins/SongDownloader/package.json @@ -1,6 +1,5 @@ { "dependencies": { - "@inrixia/lib": "../_lib", - "flac-stream-tagger": "^1.0.8" + "@inrixia/lib": "../_lib" } } diff --git a/plugins/SongDownloader/src/addMetadata.ts b/plugins/SongDownloader/src/addMetadata.ts deleted file mode 100644 index ed395b0..0000000 --- a/plugins/SongDownloader/src/addMetadata.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { utils } from "@neptune"; -import { TrackItem } from "neptune-types/tidal"; -import { fullTitle } from "@inrixia/lib/fullTitle"; -import type { ExtendedPlaybackInfoWithBytes } from "@inrixia/lib/trackBytes/download"; -import { ManifestMimeType } from "@inrixia/lib/Caches/PlaybackInfoTypes"; -import { actions } from "@neptune"; -import { interceptPromise } from "@inrixia/lib/intercept/interceptPromise"; - -import { FlacStreamTagger } from "flac-stream-tagger"; - -import { FlacTagMap, PictureType } from "flac-stream-tagger"; -import { AlbumCache } from "@inrixia/lib/Caches/AlbumCache"; - -export async function addMetadata(trackInfo: ExtendedPlaybackInfoWithBytes, track: TrackItem) { - if (trackInfo.manifestMimeType === ManifestMimeType.Tidal) { - switch (trackInfo.manifest.codecs) { - case "flac": { - return trackInfo.stream.pipe(new FlacStreamTagger(await makeTags(track))); - } - } - } -} -async function makeTags(track: TrackItem) { - const tagMap: FlacTagMap = {}; - if (track.title) tagMap.title = fullTitle(track); - if (track.album?.title) tagMap.album = track.album.title; - if (track.trackNumber !== undefined) tagMap.trackNumber = track.trackNumber.toString(); - if (track.releaseDate !== undefined) tagMap.date = track.releaseDate; - if (track.copyright) tagMap.copyright = track.copyright; - if (track.isrc) tagMap.isrc = track.isrc; - if (track.replayGain) tagMap.REPLAYGAIN_TRACK_GAIN = track.replayGain.toString(); - if (track.peak) tagMap.REPLAYGAIN_TRACK_PEAK = track.peak.toString(); - if (track.url) tagMap.comment = track.url; - if (track.artist?.name) tagMap.artist = track.artist.name; - tagMap.performer = (track.artists ?? []).map(({ name }) => name).filter((name) => name !== undefined); - - 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; - } - - const albumId = track.album?.id; - let cover = track.album?.cover; - if (albumId !== undefined) { - 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; - if (album.recordLabel) tagMap.organization = album.recordLabel; - if (album.numberOfTracks) tagMap.totalTracks = album.numberOfTracks.toString(); - if (!tagMap.date && album.releaseDate) tagMap.date = album.releaseDate; - if (!tagMap.date && album.releaseYear) tagMap.date = album.releaseYear.toString(); - cover ??= album.cover; - } - } - - let picture; - // if (cover !== undefined) { - // try { - // picture = { - // pictureType: PictureType.FrontCover, - // buffer: await requestStream(utils.getMediaURLFromID(cover)).then(rejectNotOk).then(toBuffer), - // }; - // } catch {} - // } - return { tagMap, picture }; -} diff --git a/plugins/SongDownloader/src/index.ts b/plugins/SongDownloader/src/index.ts index b9309cd..5452ce7 100644 --- a/plugins/SongDownloader/src/index.ts +++ b/plugins/SongDownloader/src/index.ts @@ -12,6 +12,7 @@ import { settings } from "./Settings"; import { ContextMenu } from "@inrixia/lib/ContextMenu"; import { PlaybackInfoCache } from "@inrixia/lib/Caches/PlaybackInfoCache"; import { startTrackDownload, openDialog, saveDialog, getDownloadProgress } from "@inrixia/lib/nativeBridge"; +import { makeTags } from "./makeTags"; export { Settings } from "./Settings"; type DownloadButtoms = Record; @@ -92,10 +93,11 @@ ContextMenu.onOpen(async (contextSource, contextMenu, trackItems) => { }); }); -const downloadTrack = async (track: TrackItem, updateMethods: ButtonMethods, filePath?: string) => { +const downloadTrack = async (trackItem: TrackItem, updateMethods: ButtonMethods, filePath?: string) => { + const metaTags = makeTags(trackItem); updateMethods.set("Fetching playback info..."); - const playbackInfo = await PlaybackInfoCache.ensure(track.id!, settings.desiredDownloadQuality); - const fileName = parseFileName(track, playbackInfo); + const playbackInfo = await PlaybackInfoCache.ensure(trackItem.id!, settings.desiredDownloadQuality); + const fileName = parseFileName(trackItem, playbackInfo); if (filePath !== undefined) { filePath = `${filePath}\\${fileName}`; } else { @@ -104,9 +106,11 @@ const downloadTrack = async (track: TrackItem, updateMethods: ButtonMethods, fil const dialogResult = await saveDialog({ defaultPath, filters: [{ name: "", extensions: [parseExtension(fileName) ?? "*"] }] }); filePath = dialogResult?.filePath; } + updateMethods.set("Building metadata..."); + await metaTags; updateMethods.set("Downloading..."); let downloadEnded = false; - const downloadComplete = startTrackDownload(playbackInfo, filePath).finally(() => (downloadEnded = true)); + const downloadComplete = startTrackDownload(playbackInfo, filePath, await metaTags).finally(() => (downloadEnded = true)); const updateDownloadProgress = async () => { const downloadProgress = await getDownloadProgress(filePath); if (downloadProgress !== undefined) updateMethods.onProgress(downloadProgress); diff --git a/plugins/SongDownloader/src/makeTags.ts b/plugins/SongDownloader/src/makeTags.ts new file mode 100644 index 0000000..ffa43e0 --- /dev/null +++ b/plugins/SongDownloader/src/makeTags.ts @@ -0,0 +1,45 @@ +import { utils, actions } from "@neptune"; +import { TrackItem } from "neptune-types/tidal"; +import { fullTitle } from "@inrixia/lib/fullTitle"; +import { interceptPromise } from "@inrixia/lib/intercept/interceptPromise"; + +import { AlbumCache } from "@inrixia/lib/Caches/AlbumCache"; +import { FlacTagMap } from "@inrixia/lib/nativeBridge"; + +export const makeTags = async (track: TrackItem) => { + const tags: FlacTagMap = {}; + if (track.title) tags.title = fullTitle(track); + if (track.album?.title) tags.album = track.album.title; + if (track.trackNumber !== undefined) tags.trackNumber = track.trackNumber.toString(); + if (track.releaseDate !== undefined) tags.date = track.releaseDate; + if (track.copyright) tags.copyright = track.copyright; + if (track.isrc) tags.isrc = track.isrc; + if (track.replayGain) tags.REPLAYGAIN_TRACK_GAIN = track.replayGain.toString(); + if (track.peak) tags.REPLAYGAIN_TRACK_PEAK = track.peak.toString(); + if (track.url) tags.comment = track.url; + if (track.artist?.name) tags.artist = track.artist.name; + tags.performer = (track.artists ?? []).map(({ name }) => name).filter((name) => name !== undefined); + + 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) tags.lyrics = lyrics.lyrics; + } + + const albumId = track.album?.id; + let cover = track.album?.cover; + if (albumId !== undefined) { + const album = await AlbumCache.get(albumId); + if (album !== undefined) { + tags.albumArtist = (album.artists ?? []).map(({ name }) => name).filter((name) => name !== undefined); + if (album.genre) tags.genres = album.genre; + if (album.recordLabel) tags.organization = album.recordLabel; + if (album.numberOfTracks) tags.totalTracks = album.numberOfTracks.toString(); + if (!tags.date && album.releaseDate) tags.date = album.releaseDate; + if (!tags.date && album.releaseYear) tags.date = album.releaseYear.toString(); + cover ??= album.cover; + } + } + return { tags, coverUrl: utils.getMediaURLFromID(cover) }; +}; diff --git a/plugins/_lib/fetch.ts b/plugins/_lib/fetch.ts deleted file mode 100644 index 4c77966..0000000 --- a/plugins/_lib/fetch.ts +++ /dev/null @@ -1,10 +0,0 @@ -// export type TrackOptions = { -// trackId: number; -// desiredQuality: AudioQuality; -// }; -// export interface DownloadTrackOptions extends FetchyOptions { -// playbackInfo?: ExtendedPlayackInfo; -// } -// console.log("Ensuring..."); -// const { playbackInfo, manifest, manifestMimeType } = options?.playbackInfo ?? (await PlaybackInfoCache.ensure(trackId, desiredQuality)); -// console.log("Ensured", playbackInfo, manifest, manifestMimeType); diff --git a/plugins/_lib/nativeBridge/native/downloadTrack.native.ts b/plugins/_lib/nativeBridge/native/downloadTrack.native.ts index 3a5dd6e..d0b27da 100644 --- a/plugins/_lib/nativeBridge/native/downloadTrack.native.ts +++ b/plugins/_lib/nativeBridge/native/downloadTrack.native.ts @@ -1,18 +1,51 @@ -import type { ExtendedPlayackInfo } from "../../Caches/PlaybackInfoTypes"; -import type { DownloadProgress } from "./request/helpers.native"; +import { type Readable } from "stream"; +import { ManifestMimeType, type ExtendedPlayackInfo } from "../../Caches/PlaybackInfoTypes"; +import { rejectNotOk, toBuffer, type DownloadProgress } from "./request/helpers.native"; import { requestTrackStream } from "./request/requestTrack.native"; import { createWriteStream } from "fs"; +import { FlacStreamTagger, PictureType, type FlacTagMap } from "flac-stream-tagger"; +import { requestStream } from "./request/requestStream.native"; + export type { DownloadProgress } from "./request/helpers.native"; +export type { FlacTagMap } from "flac-stream-tagger"; -const downloadStatus: Record = {}; +export type MetaTags = { tags: FlacTagMap; coverUrl?: string }; +const addTags = async (extPlaybackInfo: ExtendedPlayackInfo, stream: Readable, metaTags?: MetaTags) => { + if (metaTags === undefined) return stream; + const { tags, coverUrl } = metaTags; + if (extPlaybackInfo.manifestMimeType === ManifestMimeType.Tidal) { + switch (extPlaybackInfo.manifest.codecs) { + case "flac": { + let picture; + if (coverUrl !== undefined) { + try { + picture = { + pictureType: PictureType.FrontCover, + buffer: await requestStream(coverUrl).then(rejectNotOk).then(toBuffer), + }; + } catch {} + } + return stream.pipe( + new FlacStreamTagger({ + tagMap: tags, + picture, + }) + ); + } + } + } + return stream; +}; -export const startTrackDownload = async (extPlaybackInfo: ExtendedPlayackInfo, filePath: string): Promise => { +const downloadStatus: Record = {}; +export const startTrackDownload = async (extPlaybackInfo: ExtendedPlayackInfo, filePath: string, metaTags?: MetaTags): Promise => { if (downloadStatus[filePath] !== undefined) throw new Error(`Something is already downloading to ${filePath}`); try { const stream = await requestTrackStream(extPlaybackInfo, { onProgress: (progress) => (downloadStatus[filePath] = progress) }); + const metaStream = await addTags(extPlaybackInfo, stream, metaTags); return new Promise((res) => - stream.pipe(createWriteStream(filePath)).on("finish", () => { + metaStream.pipe(createWriteStream(filePath)).on("finish", () => { delete downloadStatus[filePath]; res(); }) diff --git a/plugins/_lib/package-lock.json b/plugins/_lib/package-lock.json index c9d3e29..b827228 100644 --- a/plugins/_lib/package-lock.json +++ b/plugins/_lib/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@inrixia/lib", "dependencies": { + "flac-stream-tagger": "^1.0.9", "idb": "^8.0.0", "music-metadata": "^7.14.0" } @@ -59,6 +60,15 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, + "node_modules/flac-stream-tagger": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/flac-stream-tagger/-/flac-stream-tagger-1.0.9.tgz", + "integrity": "sha512-4WCrh9SuoA82kIe/NBKsGb3L2kKl8o4t0ydZDoSK2h8WuW51/Xeb6K3wyNjaWWiYHXEE4xXVt58zQlytRfgq+A==", + "license": "SEE LICENSE.md", + "dependencies": { + "imageinfo": "^1.0.4" + } + }, "node_modules/idb": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", @@ -85,6 +95,14 @@ ], "license": "BSD-3-Clause" }, + "node_modules/imageinfo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/imageinfo/-/imageinfo-1.0.4.tgz", + "integrity": "sha512-BJml4q/QCO2187F4UcO/b6hTYIhbq4nnd1XNs65jyCED9em4m6XmeGWDxjewjfJoC7VJABhOdmqb64KA24rLZw==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", diff --git a/plugins/_lib/package.json b/plugins/_lib/package.json index b56d379..92f3009 100644 --- a/plugins/_lib/package.json +++ b/plugins/_lib/package.json @@ -1,6 +1,7 @@ { "name": "@inrixia/lib", "dependencies": { + "flac-stream-tagger": "^1.0.9", "idb": "^8.0.0", "music-metadata": "^7.14.0" }