This commit is contained in:
JellyBrick
2023-11-27 04:59:20 +09:00
parent e0a3489640
commit 11d06c50a5
26 changed files with 817 additions and 836 deletions

View File

@ -2,7 +2,9 @@ import { DefaultPresetList, Preset } from './types';
import style from './style.css?inline';
import { createPluginBuilder } from '../utils/builder';
import { createPlugin } from '@/utils';
import { onConfigChange, onMainLoad } from '@/plugins/downloader/main';
import { onPlayerApiReady, onRendererLoad } from '@/plugins/downloader/renderer';
export type DownloaderPluginConfig = {
enabled: boolean;
@ -13,7 +15,7 @@ export type DownloaderPluginConfig = {
playlistMaxItems?: number;
}
const builder = createPluginBuilder('downloader', {
export default createPlugin({
name: 'Downloader',
restartNeeded: true,
config: {
@ -24,13 +26,14 @@ const builder = createPluginBuilder('downloader', {
skipExisting: false,
playlistMaxItems: undefined,
} as DownloaderPluginConfig,
styles: [style],
stylesheets: [style],
backend: {
start: onMainLoad,
onConfigChange,
},
renderer: {
start: onRendererLoad,
onPlayerApiReady,
}
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -28,15 +28,16 @@ import {
setBadge,
} from './utils';
import { fetchFromGenius } from '@/plugins/lyrics-genius/main';
import { isEnabled } from '@/config/plugins';
import { cleanupName, getImage, SongInfo } from '@/providers/song-info';
import { getNetFetchAsFetch } from '@/plugins/utils/main';
import { cache } from '@/providers/decorators';
import { BackendContext } from '@/types/contexts';
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
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 { DownloaderPluginConfig } from '../index';
import type { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
import type PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
@ -44,7 +45,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,31 +90,28 @@ export const getCookieFromWindow = async (win: BrowserWindow) => {
.join(';');
};
let config: DownloaderPluginConfig = builder.config;
let config: DownloaderPluginConfig;
export default builder.createMain(({ handle, getConfig, on }) => {
return {
async onLoad(_win) {
win = _win;
config = await getConfig();
export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContext<DownloaderPluginConfig>) => {
win = _win;
config = await getConfig();
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;
}
};
});
yt = await Innertube.create({
cache: new UniversalCache(false),
cookie: await getCookieFromWindow(win),
generate_session_locally: true,
fetch: getNetFetchAsFetch(),
});
ipc.handle('download-song', (url: string) => downloadSong(url));
ipc.on('video-src-changed', (data: GetPlayerResponse) => {
playingUrl = data.microformat.microformatDataRenderer.urlCanonical;
});
ipc.handle('download-playlist-request', async (url: string) => downloadPlaylist(url));
};
export const onConfigChange = (newConfig: DownloaderPluginConfig) => {
config = newConfig;
};
export async function downloadSong(
url: string,
@ -319,7 +317,7 @@ async function iterableStreamToTargetFile(
contentLength: number,
sendFeedback: (str: string, value?: number) => void,
increasePlaylistProgress: (value: number) => void = () => {},
) {
): Promise<Uint8Array | null> {
const chunks = [];
let downloaded = 0;
for await (const chunk of stream) {
@ -379,6 +377,7 @@ async function iterableStreamToTargetFile(
} finally {
releaseFFmpegMutex();
}
return null;
}
const getCoverBuffer = cache(async (url: string) => {

View File

@ -4,9 +4,12 @@ import { downloadPlaylist } from './main';
import { defaultMenuDownloadLabel, getFolder } from './main/utils';
import { DefaultPresetList } from './types';
import builder from './index';
import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu';
export default builder.createMenu(async ({ getConfig, setConfig }) => {
import type { DownloaderPluginConfig } from './index';
export const onMenu = async ({ getConfig, setConfig }: MenuContext<DownloaderPluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig();
return [
@ -46,4 +49,4 @@ export default builder.createMenu(async ({ getConfig, setConfig }) => {
},
},
];
});
};

View File

@ -1,11 +1,14 @@
import downloadHTML from './templates/download.html?raw';
import builder from './index';
import defaultConfig from '@/config/defaults';
import { getSongMenu } from '@/providers/dom-elements';
import { getSongInfo } from '@/providers/song-info-front';
import defaultConfig from '../../config/defaults';
import { getSongMenu } from '../../providers/dom-elements';
import { ElementFromHtml } from '../utils/renderer';
import { getSongInfo } from '../../providers/song-info-front';
import type { RendererContext } from '@/types/contexts';
import type { DownloaderPluginConfig } from './index';
let menu: Element | null = null;
let progress: Element | null = null;
@ -13,70 +16,67 @@ const downloadButton = ElementFromHtml(downloadHTML);
let doneFirstLoad = false;
export default builder.createRenderer(({ invoke, on }) => {
const menuObserver = new MutationObserver(() => {
const menuObserver = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
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);
});
export const onRendererLoad = ({ ipc }: RendererContext<DownloaderPluginConfig>) => {
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=')) {
ipc.invoke('download-playlist-request', videoUrl);
return;
}
} else {
videoUrl = getSongInfo().url || window.location.href;
}
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);
});
return {
onLoad() {
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=')) {
invoke('download-playlist-request', videoUrl);
return;
}
} else {
videoUrl = getSongInfo().url || window.location.href;
}
invoke('download-song', videoUrl);
};
on('downloader-feedback', (feedback: string) => {
if (progress) {
progress.innerHTML = feedback || 'Download';
} else {
console.warn('Cannot update progress');
}
});
},
onPlayerApiReady() {
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
childList: true,
subtree: true,
});
},
ipc.invoke('download-song', videoUrl);
};
});
ipc.on('downloader-feedback', (feedback: string) => {
if (progress) {
progress.innerHTML = feedback || 'Download';
} else {
console.warn('Cannot update progress');
}
});
};
export const onPlayerApiReady = () => {
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
childList: true,
subtree: true,
});
};