fix: remove possible memory leak

This commit is contained in:
JellyBrick
2024-02-27 02:42:42 +09:00
parent c7715115ee
commit 55c934ac7c
3 changed files with 90 additions and 80 deletions

View File

@ -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<Uint8Array, void>,
extension: string,
metadata: CustomSongInfo,
presetFfmpegArgs: string[],
contentLength: number,
sendFeedback: (str: string, value?: number) => void,
increasePlaylistProgress: (value: number) => void = () => {},
): Promise<Uint8Array | null> {
) {
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<Uint8Array, void>,
extension: string,
metadata: CustomSongInfo,
presetFfmpegArgs: string[],
contentLength: number,
sendFeedback: (str: string, value?: number) => void,
increasePlaylistProgress: (value: number) => void = () => {},
): Promise<Uint8Array | null> {
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,

View File

@ -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) =>

View File

@ -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<Electron.NativeImage> => {
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<Electron.NativeImage> => {
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,