Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "\u0079\u006f\u0075\u0074\u0075\u0062\u0065\u002d\u006d\u0075\u0073\u0069\u0063",
"desktopName": "com.github.th_ch.\u0079\u006f\u0075\u0074\u0075\u0062\u0065\u005f\u006d\u0075\u0073\u0069\u0063",
"productName": "\u0059\u006f\u0075\u0054\u0075\u0062\u0065\u0020\u004d\u0075\u0073\u0069\u0063",
"name": "youtube-music",
"desktopName": "com.github.th_ch.youtube_music",
"productName": "YouTube Music",
"version": "3.11.0",
"description": "\u0059\u006f\u0075\u0054\u0075\u0062\u0065\u0020\u004d\u0075\u0073\u0069\u0063 Desktop App - including custom plugins",
"description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js",
"type": "module",
"license": "MIT",
Expand Down Expand Up @@ -133,6 +133,7 @@
"tinyld": "1.3.4",
"virtua": "0.48.5",
"vudio": "2.1.1",
"which": "^6.0.0",
"x11": "2.3.0",
"youtubei.js": "^16.0.1",
"zod": "4.2.1"
Expand Down
22 changes: 20 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/plugins/downloader/index.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { t } from '@/i18n';
export type DownloaderPluginConfig = {
enabled: boolean;
downloadFolder?: string;
// Engine to use for downloading: either the built-in youtube.js or external yt-dlp
engine?: 'youtube.js' | 'yt-dlp';
// Path to the yt-dlp executable (when using yt-dlp). Defaults to common system path.
ytdlpPath?: string;
// Optional path to ffmpeg/ffprobe executable or folder for yt-dlp
ytdlpFfmpegPath?: string;
downloadOnFinish?: {
enabled: boolean;
seconds: number;
Expand All @@ -26,6 +32,9 @@ export type DownloaderPluginConfig = {

export const defaultConfig: DownloaderPluginConfig = {
enabled: false,
engine: 'youtube.js',
ytdlpPath: undefined,
ytdlpFfmpegPath: undefined,
downloadFolder: undefined,
downloadOnFinish: {
enabled: false,
Expand Down
169 changes: 168 additions & 1 deletion src/plugins/downloader/main/index.ts
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'readdirSync' is defined but never used.

import { join } from 'node:path';
import { randomBytes } from 'node:crypto';

import { app, type BrowserWindow, dialog, ipcMain } from 'electron';
import { spawn, spawnSync } from 'node:child_process';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <importPlugin/order> reported by reviewdog 🐶
There should be at least one empty line between import groups

Suggested change
import { spawn, spawnSync } from 'node:child_process';
import { spawn, spawnSync } from 'node:child_process';

Comment on lines 5 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <importPlugin/order> reported by reviewdog 🐶
node:child_process import should occur before import of electron

Suggested change
import { app, type BrowserWindow, dialog, ipcMain } from 'electron';
import { spawn, spawnSync } from 'node:child_process';
import { spawn, spawnSync } from 'node:child_process';
import { app, type BrowserWindow, dialog, ipcMain } from 'electron';

import { which } from 'which';
import {
Innertube,
UniversalCache,
Expand Down Expand Up @@ -441,6 +443,171 @@ async function downloadSongUnsafe(
return;
}

// If configured to use yt-dlp, delegate downloading to the external binary
if ((config.engine ?? 'youtube.js') === 'yt-dlp') {
const findYtdlpExecutable = async (preferred?: string) => {
const candidates: (string | undefined)[] = [];
if (preferred) candidates.push(preferred);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Delete ······

Suggested change

// Try to find yt-dlp using the which command
try {
const foundPath = await which('yt-dlp', { nothrow: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-assignment> reported by reviewdog 🐶
Unsafe assignment of an error typed value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-call> reported by reviewdog 🐶
Unsafe call of a(n) error type typed value.

if (foundPath) return foundPath;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-return> reported by reviewdog 🐶
Unsafe return of a value of type error.

} catch (_) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'_' is defined but never used.

// ignore if which fails
}

for (const c of candidates) {
if (!c) continue;
try {
// Try running `--version` to check it's executable
const res = spawnSync(c, ['--version'], { encoding: 'utf8', stdio: 'ignore' });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ·encoding:·'utf8',·stdio:·'ignore' with ⏎············encoding:·'utf8',⏎············stdio:·'ignore',⏎·········

Suggested change
const res = spawnSync(c, ['--version'], { encoding: 'utf8', stdio: 'ignore' });
const res = spawnSync(c, ['--version'], {
encoding: 'utf8',
stdio: 'ignore',
});

if (res && res.status === 0) return c;
} catch (_) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'_' is defined but never used.

// ignore and try next
}
}
return null;
};

const findFfmpegExecutable = async (preferred?: string) => {
const candidates: (string | undefined)[] = [];
if (preferred) candidates.push(preferred);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Delete ······

Suggested change

// Try to find ffmpeg using the which command
try {
const foundPath = await which('ffmpeg', { nothrow: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-assignment> reported by reviewdog 🐶
Unsafe assignment of an error typed value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-call> reported by reviewdog 🐶
Unsafe call of a(n) error type typed value.

if (foundPath) return foundPath;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-return> reported by reviewdog 🐶
Unsafe return of a value of type error.

} catch (_) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'_' is defined but never used.

// ignore if which fails
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Delete ······

Suggested change

return null;
};

const ytdlpExecutable = await findYtdlpExecutable(config.ytdlpPath ?? undefined);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-assignment> reported by reviewdog 🐶
Unsafe assignment of an error typed value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace config.ytdlpPath·??·undefined with ⏎······config.ytdlpPath·??·undefined,⏎····

Suggested change
const ytdlpExecutable = await findYtdlpExecutable(config.ytdlpPath ?? undefined);
const ytdlpExecutable = await findYtdlpExecutable(
config.ytdlpPath ?? undefined,
);

const ffmpegPath = await findFfmpegExecutable(config.ytdlpFfmpegPath ?? undefined);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-assignment> reported by reviewdog 🐶
Unsafe assignment of an error typed value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace config.ytdlpFfmpegPath·??·undefined); with ⏎······config.ytdlpFfmpegPath·??·undefined,

Suggested change
const ffmpegPath = await findFfmpegExecutable(config.ytdlpFfmpegPath ?? undefined);
const ffmpegPath = await findFfmpegExecutable(
config.ytdlpFfmpegPath ?? undefined,


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Insert );⏎

Suggested change
);

// Check if both yt-dlp and ffmpeg are available
if (!ytdlpExecutable) {
dialog.showMessageBox(win, {
type: 'error',
buttons: ['OK'],
title: t('plugins.downloader.backend.dialog.ytdlp-not-found.title') || 'yt-dlp introuvable',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ·t('plugins.downloader.backend.dialog.ytdlp-not-found.title')·|| with ⏎··········t('plugins.downloader.backend.dialog.ytdlp-not-found.title')·||⏎·········

Suggested change
title: t('plugins.downloader.backend.dialog.ytdlp-not-found.title') || 'yt-dlp introuvable',
title:
t('plugins.downloader.backend.dialog.ytdlp-not-found.title') ||
'yt-dlp introuvable',

message:
t('plugins.downloader.backend.dialog.ytdlp-not-found.message') ||
"yt-dlp n'a pas été trouvé. Veuillez installer yt-dlp ou indiquer le chemin complet dans le menu du plugin.",
});
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Delete ····

Suggested change

if (!ffmpegPath) {
dialog.showMessageBox(win, {
type: 'error',
buttons: ['OK'],
title: t('plugins.downloader.backend.dialog.ffmpeg-not-found.title') || 'ffmpeg introuvable',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ·t('plugins.downloader.backend.dialog.ffmpeg-not-found.title')·|| with ⏎··········t('plugins.downloader.backend.dialog.ffmpeg-not-found.title')·||⏎·········

Suggested change
title: t('plugins.downloader.backend.dialog.ffmpeg-not-found.title') || 'ffmpeg introuvable',
title:
t('plugins.downloader.backend.dialog.ffmpeg-not-found.title') ||
'ffmpeg introuvable',

message:
t('plugins.downloader.backend.dialog.ffmpeg-not-found.message') ||
"ffmpeg n'a pas été trouvé. Veuillez installer ffmpeg ou indiquer le chemin complet dans le menu du plugin.",
});
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Delete ····

Suggested change

const baseName = filename.replace(new RegExp(`\\.${targetFileExtension}$`), '');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace new·RegExp(\.${targetFileExtension}$),·'' with ⏎······new·RegExp(\.${targetFileExtension}$),⏎······'',⏎····

Suggested change
const baseName = filename.replace(new RegExp(`\\.${targetFileExtension}$`), '');
const baseName = filename.replace(
new RegExp(`\\.${targetFileExtension}$`),
'',
);

const outputTemplate =
targetFileExtension === 'mp3'
? join(dir, `${baseName}.mp3`)
: join(dir, `${baseName}.%(ext)s`);

const urlToDownload = isId ? `https://www.youtube.com/watch?v=${id}` : idOrUrl;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ·?·https://www.youtube.com/watch?v=${id}`` with ⏎······?·https://www.youtube.com/watch?v=${id}`⏎·····`

Suggested change
const urlToDownload = isId ? `https://www.youtube.com/watch?v=${id}` : idOrUrl;
const urlToDownload = isId
? `https://www.youtube.com/watch?v=${id}`
: idOrUrl;


const args: string[] = ['--no-playlist', '--add-metadata', '--embed-thumbnail', '--output', outputTemplate];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace '--no-playlist',·'--add-metadata',·'--embed-thumbnail',·'--output',·outputTemplate with ⏎······'--no-playlist',⏎······'--add-metadata',⏎······'--embed-thumbnail',⏎······'--output',⏎······outputTemplate,⏎····

Suggested change
const args: string[] = ['--no-playlist', '--add-metadata', '--embed-thumbnail', '--output', outputTemplate];
const args: string[] = [
'--no-playlist',
'--add-metadata',
'--embed-thumbnail',
'--output',
outputTemplate,
];


if (targetFileExtension === 'mp3') {
args.unshift('--extract-audio', '--audio-format', 'mp3', '--audio-quality', '0');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace '--extract-audio',·'--audio-format',·'mp3',·'--audio-quality',·'0' with ⏎········'--extract-audio',⏎········'--audio-format',⏎········'mp3',⏎········'--audio-quality',⏎········'0',⏎······

Suggested change
args.unshift('--extract-audio', '--audio-format', 'mp3', '--audio-quality', '0');
args.unshift(
'--extract-audio',
'--audio-format',
'mp3',
'--audio-quality',
'0',
);

} else {
args.unshift('-f', 'best');
}

// Pass ffmpeg location to yt-dlp
args.push('--ffmpeg-location', ffmpegPath);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-argument> reported by reviewdog 🐶
Unsafe argument of type error typed assigned to a parameter of type string.


sendFeedback(t('plugins.downloader.backend.feedback.downloading'), 2);

await new Promise<void>((resolve, reject) => {
try {
const proc = spawn(ytdlpExecutable, [...args, urlToDownload], {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unsafe-argument> reported by reviewdog 🐶
Unsafe argument of type error typed assigned to a parameter of type string.

stdio: ['ignore', 'pipe', 'pipe'],
});

const outChunks: string[] = [];
const errChunks: string[] = [];

proc.stdout.on('data', (d) => {
const s = String(d);
outChunks.push(s);
sendFeedback(s);
});
proc.stderr.on('data', (d) => {
const s = String(d);
errChunks.push(s);
sendFeedback(s);
});

proc.on('close', (code) => {
const out = outChunks.join('');
const err = errChunks.join('');
if (code === 0) {
sendFeedback(null, -1);
resolve();
return;
}

const tail = (str: string, max = 2000) =>
str.length > max ? '...\n' + str.slice(-max) : str;

dialog.showMessageBox(win, {
type: 'error',
buttons: ['OK'],
defaultId: 0,
title:
t('plugins.downloader.backend.dialog.ytdlp-error.title') ||
`yt-dlp erreur`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/quotes> reported by reviewdog 🐶
Strings must use singlequote.

Suggested change
`yt-dlp erreur`,
'yt-dlp erreur',

message:
t('plugins.downloader.backend.dialog.ytdlp-error.message') ||
`yt-dlp a échoué avec le code ${code}`,
detail:
t('plugins.downloader.backend.dialog.ytdlp-error.detail') ||
`Derniers messages d'erreur:\n${tail(err)}\n\nSortie:\n${tail(out)}`,
});

reject(new Error(`yt-dlp exited with code ${code}: ${tail(err, 500)}`));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace new·Error(yt-dlp·exited·with·code·${code}:·${tail(err,·500)}) with ⏎············new·Error(yt-dlp·exited·with·code·${code}:·${tail(err,·500)}),⏎··········

Suggested change
reject(new Error(`yt-dlp exited with code ${code}: ${tail(err, 500)}`));
reject(
new Error(`yt-dlp exited with code ${code}: ${tail(err, 500)}`),
);

});

proc.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EACCES') {
dialog.showMessageBox(win, {
type: 'error',
buttons: ['OK'],
title:
t('plugins.downloader.backend.dialog.ytdlp-permission.title') ||
'Permission refusée',
message:
t('plugins.downloader.backend.dialog.ytdlp-permission.message') ||
`Impossible d'exécuter ${ytdlpExecutable} (EACCES). Rendre le fichier exécutable : chmod +x ${ytdlpExecutable} ou choisissez un autre chemin dans le menu du plugin.`,
});
}
reject(err);
});
} catch (err) {
reject(err);
}
});

// ytdlp already wrote the file to disk using the output template. We're done.
return;
}

const stream = await info.download(downloadOptions);

console.info(
Expand Down
Empty file modified src/plugins/downloader/main/utils.ts
100644 → 100755
Empty file.
Loading
Loading