feat(downloader): New option to download on finish (#1964)

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Johannes7k75
2024-07-14 15:00:40 +02:00
committed by GitHub
parent 83023c19a6
commit 30848b7c4a
7 changed files with 251 additions and 16 deletions

View File

@ -412,7 +412,22 @@
"choose-download-folder": "Downloadordner wählen",
"download-playlist": "Wiedergabeliste herunterladen",
"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",
"renderer": {

View File

@ -412,7 +412,22 @@
"choose-download-folder": "Choose download folder",
"download-playlist": "Download playlist",
"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",
"renderer": {

View File

@ -11,6 +11,13 @@ import { t } from '@/i18n';
export type DownloaderPluginConfig = {
enabled: boolean;
downloadFolder?: string;
downloadOnFinish?: {
enabled: boolean;
seconds: number;
percent: number;
mode: 'percent' | 'seconds';
folder?: string;
};
selectedPreset: string;
customPresetSetting: Preset;
skipExisting: boolean;
@ -20,6 +27,13 @@ export type DownloaderPluginConfig = {
export const defaultConfig: DownloaderPluginConfig = {
enabled: false,
downloadFolder: undefined,
downloadOnFinish: {
enabled: false,
seconds: 20,
percent: 10,
mode: 'seconds',
folder: undefined,
},
selectedPreset: 'mp3 (256kbps)', // Selected preset
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
skipExisting: false,

View File

@ -1,12 +1,8 @@
import {
existsSync,
mkdirSync,
writeFileSync,
} from 'node:fs';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { app, BrowserWindow, dialog } from 'electron';
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
import {
ClientType,
Innertube,
@ -29,7 +25,12 @@ import {
import { fetchFromGenius } from '@/plugins/lyrics-genius/main';
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 { t } from '@/i18n';
@ -114,6 +115,8 @@ export const onMainLoad = async ({
ipc.handle('download-playlist-request', async (url: string) =>
downloadPlaylist(url),
);
downloadSongOnFinishSetup({ ipc, getConfig });
};
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(
isId: boolean,
idOrUrl: string,
@ -375,7 +420,12 @@ async function iterableStreamToProcessedUint8Array(
'writeFile',
safeVideoName,
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;
}
if (!playlist || !playlist.items || playlist.items.length === 0) {
if (!playlist || !playlist.items || playlist.items.length === 0 || !playlist.header || !('title' in playlist.header)) {
sendError(
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
);
return;
}
const normalPlaylistTitle = playlist.header?.title?.text;

View File

@ -1,8 +1,8 @@
import { app, BrowserWindow } from 'electron';
import is from 'electron-is';
export const getFolder = (customFolder: string) =>
customFolder || app.getPath('downloads');
export const getFolder = (customFolder?: string) =>
customFolder ?? app.getPath('downloads');
export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
win.webContents.send('downloader-feedback', message);

View File

@ -1,4 +1,6 @@
import { dialog } from 'electron';
import prompt from 'custom-electron-prompt';
import { deepmerge } from 'deepmerge-ts';
import { downloadPlaylist } from './main';
import { getFolder } from './main/utils';
@ -6,11 +8,13 @@ import { DefaultPresetList } from './types';
import { t } from '@/i18n';
import promptOptions from '@/providers/prompt-options';
import { type DownloaderPluginConfig, defaultConfig } from './index';
import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu';
import type { DownloaderPluginConfig } from './index';
export const onMenu = async ({
getConfig,
setConfig,
@ -18,6 +22,142 @@ export const onMenu = async ({
const config = await getConfig();
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'),
click: () => downloadPlaylist(),

View File

@ -8,7 +8,7 @@
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"moduleResolution": "node",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"baseUrl": ".",