mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 19:31:46 +00:00
feat: migrate to new plugin api
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -1,4 +0,0 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('downloader');
|
||||
export default config;
|
||||
36
src/plugins/downloader/index.ts
Normal file
36
src/plugins/downloader/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { DefaultPresetList, Preset } from './types';
|
||||
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
|
||||
export type DownloaderPluginConfig = {
|
||||
enabled: boolean;
|
||||
downloadFolder?: string;
|
||||
selectedPreset: string;
|
||||
customPresetSetting: Preset;
|
||||
skipExisting: boolean;
|
||||
playlistMaxItems?: number;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('downloader', {
|
||||
name: 'Downloader',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
downloadFolder: undefined,
|
||||
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
||||
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined,
|
||||
} as DownloaderPluginConfig,
|
||||
styles: [style],
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ import {
|
||||
import { join } from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
|
||||
import { app, BrowserWindow, dialog } from 'electron';
|
||||
import {
|
||||
ClientType,
|
||||
Innertube,
|
||||
@ -27,16 +27,16 @@ import {
|
||||
sendFeedback as sendFeedback_,
|
||||
setBadge,
|
||||
} from './utils';
|
||||
import config from './config';
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
|
||||
|
||||
import style from './style.css';
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
|
||||
|
||||
import { fetchFromGenius } from '../lyrics-genius/main';
|
||||
import { isEnabled } from '../../config/plugins';
|
||||
import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
|
||||
import { injectCSS } from '../utils/main';
|
||||
import { cache } from '../../providers/decorators';
|
||||
import builder, { DownloaderPluginConfig } from '../index';
|
||||
|
||||
import { fetchFromGenius } from '../../lyrics-genius/main';
|
||||
import { isEnabled } from '../../../config/plugins';
|
||||
import { cleanupName, getImage, SongInfo } from '../../../providers/song-info';
|
||||
import { getNetFetchAsFetch } from '../../utils/main';
|
||||
import { cache } from '../../../providers/decorators';
|
||||
|
||||
import type { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
|
||||
import type PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
|
||||
@ -44,7 +44,7 @@ import type { Playlist } from 'youtubei.js/dist/src/parser/ytmusic';
|
||||
import type { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
|
||||
import type TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
|
||||
|
||||
import type { GetPlayerResponse } from '../../types/get-player-response';
|
||||
import type { GetPlayerResponse } from '../../../types/get-player-response';
|
||||
|
||||
type CustomSongInfo = SongInfo & { trackId?: string };
|
||||
|
||||
@ -89,42 +89,30 @@ export const getCookieFromWindow = async (win: BrowserWindow) => {
|
||||
.join(';');
|
||||
};
|
||||
|
||||
export default async (win_: BrowserWindow) => {
|
||||
win = win_;
|
||||
injectCSS(win.webContents, style);
|
||||
let config: DownloaderPluginConfig = builder.config;
|
||||
|
||||
yt = await Innertube.create({
|
||||
cache: new UniversalCache(false),
|
||||
cookie: await getCookieFromWindow(win),
|
||||
generate_session_locally: true,
|
||||
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url =
|
||||
typeof input === 'string'
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL(input.url);
|
||||
export default builder.createMain(({ handle, getConfig, on }) => {
|
||||
return {
|
||||
async onLoad(win) {
|
||||
config = await getConfig();
|
||||
|
||||
if (init?.body && !init.method) {
|
||||
init.method = 'POST';
|
||||
}
|
||||
|
||||
const request = new Request(
|
||||
url,
|
||||
input instanceof Request ? input : undefined,
|
||||
);
|
||||
|
||||
return net.fetch(request, init);
|
||||
}) as typeof fetch,
|
||||
});
|
||||
ipcMain.on('download-song', (_, url: string) => downloadSong(url));
|
||||
ipcMain.on('video-src-changed', (_, data: GetPlayerResponse) => {
|
||||
playingUrl = data.microformat.microformatDataRenderer.urlCanonical;
|
||||
});
|
||||
ipcMain.on('download-playlist-request', async (_event, url: string) =>
|
||||
downloadPlaylist(url),
|
||||
);
|
||||
};
|
||||
yt = await Innertube.create({
|
||||
cache: new UniversalCache(false),
|
||||
cookie: await getCookieFromWindow(win),
|
||||
generate_session_locally: true,
|
||||
fetch: getNetFetchAsFetch(),
|
||||
});
|
||||
handle('download-song', (_, url: string) => downloadSong(url));
|
||||
on('video-src-changed', (_, data: GetPlayerResponse) => {
|
||||
playingUrl = data.microformat.microformatDataRenderer.urlCanonical;
|
||||
});
|
||||
handle('download-playlist-request', async (_event, url: string) => downloadPlaylist(url));
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
config = newConfig;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export async function downloadSong(
|
||||
url: string,
|
||||
@ -209,7 +197,7 @@ async function downloadSongUnsafe(
|
||||
metadata.trackId = trackId;
|
||||
|
||||
const dir =
|
||||
playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
|
||||
playlistFolder || config.downloadFolder || app.getPath('downloads');
|
||||
const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${
|
||||
metadata.title
|
||||
}`;
|
||||
@ -239,11 +227,11 @@ async function downloadSongUnsafe(
|
||||
);
|
||||
}
|
||||
|
||||
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
|
||||
const selectedPreset = config.selectedPreset ?? 'mp3 (256kbps)';
|
||||
let presetSetting: Preset;
|
||||
if (selectedPreset === 'Custom') {
|
||||
presetSetting =
|
||||
config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
||||
config.customPresetSetting ?? DefaultPresetList['Custom'];
|
||||
} else if (selectedPreset === 'Source') {
|
||||
presetSetting = DefaultPresetList['Source'];
|
||||
} else {
|
||||
@ -276,7 +264,7 @@ async function downloadSongUnsafe(
|
||||
}
|
||||
const filePath = join(dir, filename);
|
||||
|
||||
if (config.get('skipExisting') && existsSync(filePath)) {
|
||||
if (config.skipExisting && existsSync(filePath)) {
|
||||
sendFeedback(null, -1);
|
||||
return;
|
||||
}
|
||||
@ -517,10 +505,10 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
|
||||
}
|
||||
|
||||
const folder = getFolder(config.get('downloadFolder') ?? '');
|
||||
const folder = getFolder(config.downloadFolder ?? '');
|
||||
const playlistFolder = join(folder, safePlaylistTitle);
|
||||
if (existsSync(playlistFolder)) {
|
||||
if (!config.get('skipExisting')) {
|
||||
if (!config.skipExisting) {
|
||||
sendError(new Error(`The folder ${playlistFolder} already exists`));
|
||||
return;
|
||||
}
|
||||
@ -637,6 +625,7 @@ const getAndroidTvInfo = async (id: string): Promise<VideoInfo> => {
|
||||
client_type: ClientType.TV_EMBEDDED,
|
||||
generate_session_locally: true,
|
||||
retrieve_player: true,
|
||||
fetch: getNetFetchAsFetch(),
|
||||
});
|
||||
// GetInfo 404s with the bypass, so we use getBasicInfo instead
|
||||
// that's fine as we only need the streaming data
|
||||
@ -1,46 +1,49 @@
|
||||
import { dialog } from 'electron';
|
||||
|
||||
import { downloadPlaylist } from './main';
|
||||
import { defaultMenuDownloadLabel, getFolder } from './utils';
|
||||
import { defaultMenuDownloadLabel, getFolder } from './main/utils';
|
||||
import { DefaultPresetList } from './types';
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
import builder from './index';
|
||||
|
||||
export default (): MenuTemplate => [
|
||||
{
|
||||
label: defaultMenuDownloadLabel,
|
||||
click: () => downloadPlaylist(),
|
||||
},
|
||||
{
|
||||
label: 'Choose download folder',
|
||||
click() {
|
||||
const result = dialog.showOpenDialogSync({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
defaultPath: getFolder(config.get('downloadFolder') ?? ''),
|
||||
});
|
||||
if (result) {
|
||||
config.set('downloadFolder', result[0]);
|
||||
} // Else = user pressed cancel
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: defaultMenuDownloadLabel,
|
||||
click: () => downloadPlaylist(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Presets',
|
||||
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||
label: preset,
|
||||
type: 'radio',
|
||||
checked: config.get('selectedPreset') === preset,
|
||||
{
|
||||
label: 'Choose download folder',
|
||||
click() {
|
||||
config.set('selectedPreset', preset);
|
||||
const result = dialog.showOpenDialogSync({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
defaultPath: getFolder(config.downloadFolder ?? ''),
|
||||
});
|
||||
if (result) {
|
||||
setConfig({ downloadFolder: result[0] });
|
||||
} // Else = user pressed cancel
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Skip existing files',
|
||||
type: 'checkbox',
|
||||
checked: config.get('skipExisting'),
|
||||
click(item) {
|
||||
config.set('skipExisting', item.checked);
|
||||
},
|
||||
},
|
||||
];
|
||||
{
|
||||
label: 'Presets',
|
||||
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||
label: preset,
|
||||
type: 'radio',
|
||||
checked: config.selectedPreset === preset,
|
||||
click() {
|
||||
setConfig({ selectedPreset: preset });
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Skip existing files',
|
||||
type: 'checkbox',
|
||||
checked: config.skipExisting,
|
||||
click(item) {
|
||||
setConfig({ skipExisting: item.checked });
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import downloadHTML from './templates/download.html?raw';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import defaultConfig from '../../config/defaults';
|
||||
import { getSongMenu } from '../../providers/dom-elements';
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
@ -11,67 +13,71 @@ const downloadButton = ElementFromHtml(downloadHTML);
|
||||
|
||||
let doneFirstLoad = false;
|
||||
|
||||
export default () => {
|
||||
const menuObserver = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
export default builder.createRenderer(() => {
|
||||
return {
|
||||
onLoad() {
|
||||
const menuObserver = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.contains(downloadButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuUrl = document.querySelector<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href;
|
||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.prepend(downloadButton);
|
||||
progress = document.querySelector('#ytmcustom-download');
|
||||
|
||||
if (doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => doneFirstLoad ||= true, 500);
|
||||
});
|
||||
|
||||
window.download = () => {
|
||||
let videoUrl = getSongMenu()
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')
|
||||
?.getAttribute('href');
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
videoUrl = defaultConfig.url + '/' + videoUrl;
|
||||
}
|
||||
|
||||
if (videoUrl.includes('?playlist=')) {
|
||||
window.ipcRenderer.send('download-playlist-request', videoUrl);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
videoUrl = getSongInfo().url || window.location.href;
|
||||
}
|
||||
|
||||
window.ipcRenderer.send('download-song', videoUrl);
|
||||
};
|
||||
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, { once: true, passive: true });
|
||||
|
||||
window.ipcRenderer.on('downloader-feedback', (_, feedback: string) => {
|
||||
if (progress) {
|
||||
progress.innerHTML = feedback || 'Download';
|
||||
} else {
|
||||
console.warn('Cannot update progress');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (menu.contains(downloadButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuUrl = document.querySelector<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href;
|
||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.prepend(downloadButton);
|
||||
progress = document.querySelector('#ytmcustom-download');
|
||||
|
||||
if (doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => doneFirstLoad ||= true, 500);
|
||||
});
|
||||
|
||||
window.download = () => {
|
||||
let videoUrl = getSongMenu()
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')
|
||||
?.getAttribute('href');
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
videoUrl = defaultConfig.url + '/' + videoUrl;
|
||||
}
|
||||
|
||||
if (videoUrl.includes('?playlist=')) {
|
||||
window.ipcRenderer.send('download-playlist-request', videoUrl);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
videoUrl = getSongInfo().url || window.location.href;
|
||||
}
|
||||
|
||||
window.ipcRenderer.send('download-song', videoUrl);
|
||||
};
|
||||
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, { once: true, passive: true });
|
||||
|
||||
window.ipcRenderer.on('downloader-feedback', (_, feedback: string) => {
|
||||
if (progress) {
|
||||
progress.innerHTML = feedback || 'Download';
|
||||
} else {
|
||||
console.warn('Cannot update progress');
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user