mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
fix: remove possible memory leak
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user