mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 12:21: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 { isEnabled } from '@/config/plugins';
|
||||||
import { cleanupName, getImage, MediaType, type SongInfo } from '@/providers/song-info';
|
import { cleanupName, getImage, MediaType, type SongInfo } from '@/providers/song-info';
|
||||||
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
||||||
import { cache } from '@/providers/decorators';
|
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
@ -296,7 +295,7 @@ async function downloadSongUnsafe(
|
|||||||
mkdirSync(dir);
|
mkdirSync(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileBuffer = await iterableStreamToTargetFile(
|
let fileBuffer = await iterableStreamToProcessedUint8Array(
|
||||||
iterableStream,
|
iterableStream,
|
||||||
targetFileExtension,
|
targetFileExtension,
|
||||||
metadata,
|
metadata,
|
||||||
@ -326,15 +325,12 @@ async function downloadSongUnsafe(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function iterableStreamToTargetFile(
|
async function downloadChunks(
|
||||||
stream: AsyncGenerator<Uint8Array, void>,
|
stream: AsyncGenerator<Uint8Array, void>,
|
||||||
extension: string,
|
|
||||||
metadata: CustomSongInfo,
|
|
||||||
presetFfmpegArgs: string[],
|
|
||||||
contentLength: number,
|
contentLength: number,
|
||||||
sendFeedback: (str: string, value?: number) => void,
|
sendFeedback: (str: string, value?: number) => void,
|
||||||
increasePlaylistProgress: (value: number) => void = () => {},
|
increasePlaylistProgress: (value: number) => void = () => {},
|
||||||
): Promise<Uint8Array | null> {
|
) {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
for await (const chunk of stream) {
|
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
|
// This is a very rough estimate, trying to make the progress bar look nice
|
||||||
increasePlaylistProgress(ratio * 0.15);
|
increasePlaylistProgress(ratio * 0.15);
|
||||||
}
|
}
|
||||||
|
return chunks;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
const nativeImage = cropMaxWidth(await getImage(url));
|
||||||
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
|
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
|
||||||
});
|
};
|
||||||
|
|
||||||
async function writeID3(
|
async function writeID3(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { app, NativeImage } from 'electron';
|
|||||||
|
|
||||||
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
||||||
|
|
||||||
import { cache } from '@/providers/decorators';
|
|
||||||
import { SongInfo } from '@/providers/song-info';
|
import { SongInfo } from '@/providers/song-info';
|
||||||
|
|
||||||
import type { NotificationsPluginConfig } from './index';
|
import type { NotificationsPluginConfig } from './index';
|
||||||
@ -30,7 +29,7 @@ export const urgencyLevels = [
|
|||||||
{ name: 'High', value: 'critical' } as const,
|
{ name: 'High', value: 'critical' } as const,
|
||||||
];
|
];
|
||||||
|
|
||||||
const nativeImageToLogo = cache((nativeImage: NativeImage) => {
|
const nativeImageToLogo = (nativeImage: NativeImage) => {
|
||||||
const temporaryImage = nativeImage.resize({ height: 256 });
|
const temporaryImage = nativeImage.resize({ height: 256 });
|
||||||
const margin = Math.max(temporaryImage.getSize().width - 256, 0);
|
const margin = Math.max(temporaryImage.getSize().width - 256, 0);
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ const nativeImageToLogo = cache((nativeImage: NativeImage) => {
|
|||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
export const notificationImage = (
|
export const notificationImage = (
|
||||||
songInfo: SongInfo,
|
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 {
|
try {
|
||||||
fs.writeFileSync(savePath, img.toPNG());
|
fs.writeFileSync(savePath, img.toPNG());
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@ -76,7 +75,7 @@ export const saveImage = cache((img: NativeImage, savePath: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return savePath;
|
return savePath;
|
||||||
});
|
};
|
||||||
|
|
||||||
export const snakeToCamel = (string_: string) =>
|
export const snakeToCamel = (string_: string) =>
|
||||||
string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
|
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 { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
import { cache } from './decorators';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
import type { GetPlayerResponse } from '@/types/get-player-response';
|
import type { GetPlayerResponse } from '@/types/get-player-response';
|
||||||
@ -45,19 +44,20 @@ export interface SongInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grab the native image using the src
|
// Grab the native image using the src
|
||||||
export const getImage = cache(
|
export const getImage = async (src: string): Promise<Electron.NativeImage> => {
|
||||||
async (src: string): Promise<Electron.NativeImage> => {
|
const result = await net.fetch(src);
|
||||||
const result = await net.fetch(src);
|
const output = nativeImage.createFromBuffer(
|
||||||
const buffer = await result.arrayBuffer();
|
Buffer.from(
|
||||||
const output = nativeImage.createFromBuffer(Buffer.from(buffer));
|
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));
|
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 (
|
const handleData = async (
|
||||||
data: GetPlayerResponse,
|
data: GetPlayerResponse,
|
||||||
|
|||||||
Reference in New Issue
Block a user