diff --git a/src/plugins/downloader/main/index.ts b/src/plugins/downloader/main/index.ts index d15dcea7..09343214 100644 --- a/src/plugins/downloader/main/index.ts +++ b/src/plugins/downloader/main/index.ts @@ -31,7 +31,6 @@ import { fetchFromGenius } from '@/plugins/lyrics-genius/main'; import { isEnabled } from '@/config/plugins'; import { cleanupName, getImage, MediaType, type SongInfo } from '@/providers/song-info'; import { getNetFetchAsFetch } from '@/plugins/utils/main'; -import { cache } from '@/providers/decorators'; import { t } from '@/i18n'; @@ -296,7 +295,7 @@ async function downloadSongUnsafe( mkdirSync(dir); } - let fileBuffer = await iterableStreamToTargetFile( + let fileBuffer = await iterableStreamToProcessedUint8Array( iterableStream, targetFileExtension, metadata, @@ -326,15 +325,12 @@ async function downloadSongUnsafe( ); } -async function iterableStreamToTargetFile( +async function downloadChunks( stream: AsyncGenerator, - extension: string, - metadata: CustomSongInfo, - presetFfmpegArgs: string[], contentLength: number, sendFeedback: (str: string, value?: number) => void, increasePlaylistProgress: (value: number) => void = () => {}, -): Promise { +) { const chunks = []; let downloaded = 0; for await (const chunk of stream) { @@ -352,65 +348,80 @@ async function iterableStreamToTargetFile( // This is a very rough estimate, trying to make the progress bar look nice increasePlaylistProgress(ratio * 0.15); } - - sendFeedback(t('plugins.downloader.backend.feedback.loading'), 2); // Indefinite progress bar after download - - const buffer = Buffer.concat(chunks); - const safeVideoName = randomBytes(32).toString('hex'); - const releaseFFmpegMutex = await ffmpegMutex.acquire(); - - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } - - sendFeedback(t('plugins.downloader.backend.feedback.preparing-file')); - ffmpeg.FS('writeFile', safeVideoName, buffer); - - sendFeedback(t('plugins.downloader.backend.feedback.converting')); - - ffmpeg.setProgress(({ ratio }) => { - sendFeedback( - t('plugins.downloader.backend.feedback.conversion-progress', { - percent: Math.floor(ratio * 100), - }), - ratio, - ); - increasePlaylistProgress(0.15 + (ratio * 0.85)); - }); - - const safeVideoNameWithExtension = `${safeVideoName}.${extension}`; - try { - await ffmpeg.run( - '-i', - safeVideoName, - ...presetFfmpegArgs, - ...getFFmpegMetadataArgs(metadata), - safeVideoNameWithExtension, - ); - } finally { - ffmpeg.FS('unlink', safeVideoName); - } - - sendFeedback(t('plugins.downloader.backend.feedback.saving')); - - try { - return ffmpeg.FS('readFile', safeVideoNameWithExtension); - } finally { - ffmpeg.FS('unlink', safeVideoNameWithExtension); - } - } catch (error: unknown) { - sendError(error as Error, safeVideoName); - } finally { - releaseFFmpegMutex(); - } - return null; + return chunks; } -const getCoverBuffer = cache(async (url: string) => { +async function iterableStreamToProcessedUint8Array( + stream: AsyncGenerator, + extension: string, + metadata: CustomSongInfo, + presetFfmpegArgs: string[], + contentLength: number, + sendFeedback: (str: string, value?: number) => void, + increasePlaylistProgress: (value: number) => void = () => {}, +): Promise { + sendFeedback(t('plugins.downloader.backend.feedback.loading'), 2); // Indefinite progress bar after download + + const safeVideoName = randomBytes(32).toString('hex'); + + return await ffmpegMutex.runExclusive(async () => { + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } + + sendFeedback(t('plugins.downloader.backend.feedback.preparing-file')); + ffmpeg.FS( + 'writeFile', + safeVideoName, + Buffer.concat( + await downloadChunks(stream, contentLength, sendFeedback, increasePlaylistProgress), + ), + ); + + sendFeedback(t('plugins.downloader.backend.feedback.converting')); + + ffmpeg.setProgress(({ ratio }) => { + sendFeedback( + t('plugins.downloader.backend.feedback.conversion-progress', { + percent: Math.floor(ratio * 100), + }), + ratio, + ); + increasePlaylistProgress(0.15 + (ratio * 0.85)); + }); + + const safeVideoNameWithExtension = `${safeVideoName}.${extension}`; + try { + await ffmpeg.run( + '-i', + safeVideoName, + ...presetFfmpegArgs, + ...getFFmpegMetadataArgs(metadata), + safeVideoNameWithExtension, + ); + } finally { + ffmpeg.FS('unlink', safeVideoName); + } + + sendFeedback(t('plugins.downloader.backend.feedback.saving')); + + try { + return ffmpeg.FS('readFile', safeVideoNameWithExtension); + } finally { + ffmpeg.FS('unlink', safeVideoNameWithExtension); + } + } catch (error: unknown) { + sendError(error as Error, safeVideoName); + } + return null; + }); +} + +const getCoverBuffer = async (url: string) => { const nativeImage = cropMaxWidth(await getImage(url)); return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null; -}); +}; async function writeID3( buffer: Buffer, diff --git a/src/plugins/notifications/utils.ts b/src/plugins/notifications/utils.ts index 2d3f8876..2db71197 100644 --- a/src/plugins/notifications/utils.ts +++ b/src/plugins/notifications/utils.ts @@ -5,7 +5,6 @@ import { app, NativeImage } from 'electron'; import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack'; -import { cache } from '@/providers/decorators'; import { SongInfo } from '@/providers/song-info'; import type { NotificationsPluginConfig } from './index'; @@ -30,7 +29,7 @@ export const urgencyLevels = [ { name: 'High', value: 'critical' } as const, ]; -const nativeImageToLogo = cache((nativeImage: NativeImage) => { +const nativeImageToLogo = (nativeImage: NativeImage) => { const temporaryImage = nativeImage.resize({ height: 256 }); const margin = Math.max(temporaryImage.getSize().width - 256, 0); @@ -40,7 +39,7 @@ const nativeImageToLogo = cache((nativeImage: NativeImage) => { width: 256, height: 256, }); -}); +}; export const notificationImage = ( songInfo: SongInfo, @@ -66,7 +65,7 @@ export const notificationImage = ( } }; -export const saveImage = cache((img: NativeImage, savePath: string) => { +export const saveImage = (img: NativeImage, savePath: string) => { try { fs.writeFileSync(savePath, img.toPNG()); } catch (error: unknown) { @@ -76,7 +75,7 @@ export const saveImage = cache((img: NativeImage, savePath: string) => { } return savePath; -}); +}; export const snakeToCamel = (string_: string) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) => diff --git a/src/providers/song-info.ts b/src/providers/song-info.ts index 5280a2e8..8216a534 100644 --- a/src/providers/song-info.ts +++ b/src/providers/song-info.ts @@ -2,7 +2,6 @@ import { BrowserWindow, ipcMain, nativeImage, net } from 'electron'; import { Mutex } from 'async-mutex'; -import { cache } from './decorators'; import config from '@/config'; import type { GetPlayerResponse } from '@/types/get-player-response'; @@ -45,19 +44,20 @@ export interface SongInfo { } // Grab the native image using the src -export const getImage = cache( - async (src: string): Promise => { - const result = await net.fetch(src); - const buffer = await result.arrayBuffer(); - const output = nativeImage.createFromBuffer(Buffer.from(buffer)); - if (output.isEmpty() && !src.endsWith('.jpg') && src.includes('.jpg')) { - // Fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315) - return getImage(src.slice(0, src.lastIndexOf('.jpg') + 4)); - } +export const getImage = async (src: string): Promise => { + const result = await net.fetch(src); + const output = nativeImage.createFromBuffer( + Buffer.from( + await result.arrayBuffer(), + ), + ); + if (output.isEmpty() && !src.endsWith('.jpg') && src.includes('.jpg')) { + // Fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315) + return getImage(src.slice(0, src.lastIndexOf('.jpg') + 4)); + } - return output; - }, -); + return output; +}; const handleData = async ( data: GetPlayerResponse,