mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
feat(downloader): New option to download on finish (#1964)
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -412,7 +412,22 @@
|
|||||||
"choose-download-folder": "Downloadordner wählen",
|
"choose-download-folder": "Downloadordner wählen",
|
||||||
"download-playlist": "Wiedergabeliste herunterladen",
|
"download-playlist": "Wiedergabeliste herunterladen",
|
||||||
"presets": "Voreinstellungen",
|
"presets": "Voreinstellungen",
|
||||||
"skip-existing": "Vorhandene Dateien überspringen"
|
"skip-existing": "Vorhandene Dateien überspringen",
|
||||||
|
"download-finish-settings": {
|
||||||
|
"label": "Song am Ende runterladen",
|
||||||
|
"submenu": {
|
||||||
|
"enabled": "Aktiviert",
|
||||||
|
"mode": "Zeitmodus",
|
||||||
|
"seconds": "Sekunden",
|
||||||
|
"percent": "Prozent",
|
||||||
|
"advanced": "Erweitert"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"title": "Konfiguriere wann runtergeladen werden soll",
|
||||||
|
"last-seconds": "Letzten x Sekunden",
|
||||||
|
"last-percent": "Nach x Prozent"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Downloader",
|
"name": "Downloader",
|
||||||
"renderer": {
|
"renderer": {
|
||||||
|
|||||||
@ -412,7 +412,22 @@
|
|||||||
"choose-download-folder": "Choose download folder",
|
"choose-download-folder": "Choose download folder",
|
||||||
"download-playlist": "Download playlist",
|
"download-playlist": "Download playlist",
|
||||||
"presets": "Presets",
|
"presets": "Presets",
|
||||||
"skip-existing": "Skip existing files"
|
"skip-existing": "Skip existing files",
|
||||||
|
"download-finish-settings": {
|
||||||
|
"label": "Download on finish",
|
||||||
|
"submenu": {
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"mode": "Time mode",
|
||||||
|
"seconds": "Seconds",
|
||||||
|
"percent": "Percent",
|
||||||
|
"advanced": "Advanced"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"title": "Configure when to download",
|
||||||
|
"last-seconds": "Last x seconds",
|
||||||
|
"last-percent": "After x percent"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Downloader",
|
"name": "Downloader",
|
||||||
"renderer": {
|
"renderer": {
|
||||||
|
|||||||
@ -11,6 +11,13 @@ import { t } from '@/i18n';
|
|||||||
export type DownloaderPluginConfig = {
|
export type DownloaderPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
downloadFolder?: string;
|
downloadFolder?: string;
|
||||||
|
downloadOnFinish?: {
|
||||||
|
enabled: boolean;
|
||||||
|
seconds: number;
|
||||||
|
percent: number;
|
||||||
|
mode: 'percent' | 'seconds';
|
||||||
|
folder?: string;
|
||||||
|
};
|
||||||
selectedPreset: string;
|
selectedPreset: string;
|
||||||
customPresetSetting: Preset;
|
customPresetSetting: Preset;
|
||||||
skipExisting: boolean;
|
skipExisting: boolean;
|
||||||
@ -20,6 +27,13 @@ export type DownloaderPluginConfig = {
|
|||||||
export const defaultConfig: DownloaderPluginConfig = {
|
export const defaultConfig: DownloaderPluginConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
downloadFolder: undefined,
|
downloadFolder: undefined,
|
||||||
|
downloadOnFinish: {
|
||||||
|
enabled: false,
|
||||||
|
seconds: 20,
|
||||||
|
percent: 10,
|
||||||
|
mode: 'seconds',
|
||||||
|
folder: undefined,
|
||||||
|
},
|
||||||
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
||||||
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
||||||
skipExisting: false,
|
skipExisting: false,
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
import {
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from 'node:fs';
|
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import { app, BrowserWindow, dialog } from 'electron';
|
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||||
import {
|
import {
|
||||||
ClientType,
|
ClientType,
|
||||||
Innertube,
|
Innertube,
|
||||||
@ -29,7 +25,12 @@ import {
|
|||||||
|
|
||||||
import { fetchFromGenius } from '@/plugins/lyrics-genius/main';
|
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 registerCallback, {
|
||||||
|
cleanupName,
|
||||||
|
getImage,
|
||||||
|
MediaType,
|
||||||
|
type SongInfo,
|
||||||
|
} from '@/providers/song-info';
|
||||||
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
@ -114,6 +115,8 @@ export const onMainLoad = async ({
|
|||||||
ipc.handle('download-playlist-request', async (url: string) =>
|
ipc.handle('download-playlist-request', async (url: string) =>
|
||||||
downloadPlaylist(url),
|
downloadPlaylist(url),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
downloadSongOnFinishSetup({ ipc, getConfig });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onConfigChange = (newConfig: DownloaderPluginConfig) => {
|
export const onConfigChange = (newConfig: DownloaderPluginConfig) => {
|
||||||
@ -162,6 +165,48 @@ export async function downloadSongFromId(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadSongOnFinishSetup({
|
||||||
|
ipc,
|
||||||
|
}: Pick<BackendContext<DownloaderPluginConfig>, 'ipc' | 'getConfig'>) {
|
||||||
|
let currentUrl: string | undefined;
|
||||||
|
let duration: number | undefined;
|
||||||
|
let time = 0;
|
||||||
|
|
||||||
|
registerCallback((songInfo: SongInfo) => {
|
||||||
|
if (
|
||||||
|
!songInfo.isPaused &&
|
||||||
|
songInfo.url !== currentUrl &&
|
||||||
|
config.downloadOnFinish?.enabled
|
||||||
|
) {
|
||||||
|
if (typeof currentUrl === 'string' && duration && duration > 0) {
|
||||||
|
if (
|
||||||
|
config.downloadOnFinish.mode === 'seconds' &&
|
||||||
|
duration - time <= config.downloadOnFinish.seconds
|
||||||
|
) {
|
||||||
|
downloadSong(currentUrl, config.downloadOnFinish.folder ?? config.downloadFolder);
|
||||||
|
} else if (
|
||||||
|
config.downloadOnFinish.mode === 'percent' &&
|
||||||
|
time >= duration * (config.downloadOnFinish.percent / 100)
|
||||||
|
) {
|
||||||
|
downloadSong(currentUrl, config.downloadOnFinish.folder ?? config.downloadFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUrl = songInfo.url;
|
||||||
|
duration = songInfo.songDuration;
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('ytmd:player-api-loaded', () => {
|
||||||
|
ipc.send('ytmd:setup-time-changed-listener');
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('ytmd:time-changed', (_, t: number) => {
|
||||||
|
if (t > time) time = t;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadSongUnsafe(
|
async function downloadSongUnsafe(
|
||||||
isId: boolean,
|
isId: boolean,
|
||||||
idOrUrl: string,
|
idOrUrl: string,
|
||||||
@ -375,7 +420,12 @@ async function iterableStreamToProcessedUint8Array(
|
|||||||
'writeFile',
|
'writeFile',
|
||||||
safeVideoName,
|
safeVideoName,
|
||||||
Buffer.concat(
|
Buffer.concat(
|
||||||
await downloadChunks(stream, contentLength, sendFeedback, increasePlaylistProgress),
|
await downloadChunks(
|
||||||
|
stream,
|
||||||
|
contentLength,
|
||||||
|
sendFeedback,
|
||||||
|
increasePlaylistProgress,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -516,10 +566,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playlist || !playlist.items || playlist.items.length === 0) {
|
if (!playlist || !playlist.items || playlist.items.length === 0 || !playlist.header || !('title' in playlist.header)) {
|
||||||
sendError(
|
sendError(
|
||||||
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
|
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalPlaylistTitle = playlist.header?.title?.text;
|
const normalPlaylistTitle = playlist.header?.title?.text;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
export const getFolder = (customFolder: string) =>
|
export const getFolder = (customFolder?: string) =>
|
||||||
customFolder || app.getPath('downloads');
|
customFolder ?? app.getPath('downloads');
|
||||||
|
|
||||||
export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
|
export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
|
||||||
win.webContents.send('downloader-feedback', message);
|
win.webContents.send('downloader-feedback', message);
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
import prompt from 'custom-electron-prompt';
|
||||||
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
|
|
||||||
import { downloadPlaylist } from './main';
|
import { downloadPlaylist } from './main';
|
||||||
import { getFolder } from './main/utils';
|
import { getFolder } from './main/utils';
|
||||||
@ -6,11 +8,13 @@ import { DefaultPresetList } from './types';
|
|||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
|
import { type DownloaderPluginConfig, defaultConfig } from './index';
|
||||||
|
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { MenuTemplate } from '@/menu';
|
import type { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
import type { DownloaderPluginConfig } from './index';
|
|
||||||
|
|
||||||
export const onMenu = async ({
|
export const onMenu = async ({
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
@ -18,6 +22,142 @@ export const onMenu = async ({
|
|||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
label: t('plugins.downloader.menu.download-finish-settings.label'),
|
||||||
|
type: 'submenu',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.submenu.enabled',
|
||||||
|
),
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: config.downloadOnFinish?.enabled ?? false,
|
||||||
|
click(item) {
|
||||||
|
setConfig({
|
||||||
|
downloadOnFinish: {
|
||||||
|
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
||||||
|
enabled: item.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.downloader.menu.choose-download-folder'),
|
||||||
|
click() {
|
||||||
|
const result = dialog.showOpenDialogSync({
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
defaultPath: getFolder(config.downloadOnFinish?.folder ?? config.downloadFolder),
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
setConfig({
|
||||||
|
downloadOnFinish: {
|
||||||
|
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
||||||
|
folder: result[0],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.submenu.mode',
|
||||||
|
),
|
||||||
|
type: 'submenu',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.submenu.seconds',
|
||||||
|
),
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.downloadOnFinish?.mode === 'seconds',
|
||||||
|
click() {
|
||||||
|
setConfig({
|
||||||
|
downloadOnFinish: {
|
||||||
|
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
||||||
|
mode: 'seconds',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.submenu.percent',
|
||||||
|
),
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.downloadOnFinish?.mode === 'percent',
|
||||||
|
click() {
|
||||||
|
setConfig({
|
||||||
|
downloadOnFinish: {
|
||||||
|
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
||||||
|
mode: 'percent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.submenu.advanced',
|
||||||
|
),
|
||||||
|
async click() {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.prompt.title',
|
||||||
|
),
|
||||||
|
type: 'multiInput',
|
||||||
|
multiInputOptions: [
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.prompt.last-seconds',
|
||||||
|
),
|
||||||
|
inputAttrs: {
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
min: '0',
|
||||||
|
step: '1',
|
||||||
|
},
|
||||||
|
value: config.downloadOnFinish?.seconds ?? defaultConfig.downloadOnFinish!.seconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.downloader.menu.download-finish-settings.prompt.last-percent',
|
||||||
|
),
|
||||||
|
inputAttrs: {
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
min: '1',
|
||||||
|
max: '100',
|
||||||
|
step: '1',
|
||||||
|
},
|
||||||
|
value: config.downloadOnFinish?.percent ?? defaultConfig.downloadOnFinish!.percent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...promptOptions(),
|
||||||
|
height: 240,
|
||||||
|
resizable: true,
|
||||||
|
}).catch(console.error);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig({
|
||||||
|
downloadOnFinish: {
|
||||||
|
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
||||||
|
seconds: Number(res[0]),
|
||||||
|
percent: Number(res[1]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: t('plugins.downloader.menu.download-playlist'),
|
label: t('plugins.downloader.menu.download-playlist'),
|
||||||
click: () => downloadPlaylist(),
|
click: () => downloadPlaylist(),
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "solid-js",
|
"jsxImportSource": "solid-js",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|||||||
Reference in New Issue
Block a user