mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
New GUI
This commit is contained in:
parent
6707b7fdd4
commit
cd62595518
47 changed files with 997 additions and 2283 deletions
4
@types/messageHandler.d.ts
vendored
4
@types/messageHandler.d.ts
vendored
|
|
@ -13,9 +13,11 @@ export interface MessageHandler {
|
||||||
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
|
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
|
||||||
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
|
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
|
||||||
downloadItem: (data) => void,
|
downloadItem: (data) => void,
|
||||||
isDownloading: () => boolean,
|
isDownloading: () => Promise<boolean>,
|
||||||
writeToClipboard: (text: string) => void,
|
writeToClipboard: (text: string) => void,
|
||||||
openFolder: (path: FolderTypes) => void,
|
openFolder: (path: FolderTypes) => void,
|
||||||
|
openFile: (data: [FolderTypes, string]) => void,
|
||||||
|
openURL: (data: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FolderTypes = 'content' | 'config';
|
export type FolderTypes = 'content' | 'config';
|
||||||
|
|
|
||||||
36
@types/ws.d.ts
vendored
Normal file
36
@types/ws.d.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from "./messageHandler"
|
||||||
|
|
||||||
|
export type WSMessage<T extends keyof MessageTypes, P extends 0|1 = 0> = {
|
||||||
|
name: T,
|
||||||
|
data: MessageTypes[T][P]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WSMessageWithID<T extends keyof MessageTypes, P extends 0|1 = 0> = WSMessage<T, P> & {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UnknownWSMessage = {
|
||||||
|
name: keyof MessageTypes,
|
||||||
|
data: MessageTypes[keyof MessageTypes][0],
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageTypes = {
|
||||||
|
'auth': [AuthData, AuthResponse],
|
||||||
|
'checkToken': [undefined, CheckTokenResponse],
|
||||||
|
'search': [SearchData, SearchResponse],
|
||||||
|
'default': [string, unknown],
|
||||||
|
'availableDubCodes': [undefined, string[]],
|
||||||
|
'availableSubCodes': [undefined, string[]],
|
||||||
|
'resolveItems': [ResolveItemsData, ResponseBase<QueueItem[]>],
|
||||||
|
'listEpisodes': [string, EpisodeListResponse],
|
||||||
|
'downloadItem': [unknown, undefined],
|
||||||
|
'isDownloading': [undefined, boolean],
|
||||||
|
'writeToClipboard': [string, undefined],
|
||||||
|
'openFolder': [FolderTypes, undefined],
|
||||||
|
'changeProvider': [undefined, boolean],
|
||||||
|
'type': [undefined, 'funi'|'crunchy'|undefined],
|
||||||
|
'setup': ['funi'|'crunchy'|undefined, undefined],
|
||||||
|
'openFile': [[FolderTypes, string], undefined],
|
||||||
|
'openURL': [string, undefined]
|
||||||
|
}
|
||||||
BIN
build/Icon.ico
BIN
build/Icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
BIN
build/Icon.png
BIN
build/Icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -1 +0,0 @@
|
||||||
All credits go to KX-DAREKON#0420
|
|
||||||
2
config/gui.yml
Normal file
2
config/gui.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
port: 3000
|
||||||
|
password: "default"
|
||||||
12
crunchy.ts
12
crunchy.ts
|
|
@ -455,13 +455,13 @@ export default class Crunchy implements ServiceClass {
|
||||||
// check title
|
// check title
|
||||||
item.title = item.title != '' ? item.title : 'NO_TITLE';
|
item.title = item.title != '' ? item.title : 'NO_TITLE';
|
||||||
// static data
|
// static data
|
||||||
const oMetadata = [],
|
const oMetadata: string[] = [],
|
||||||
oBooleans = [],
|
oBooleans: string[] = [],
|
||||||
tMetadata = item.type + '_metadata',
|
tMetadata = item.type + '_metadata',
|
||||||
iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record<string, any>,
|
iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record<string, any>,
|
||||||
iTitle = [ item.title ];
|
iTitle = [ item.title ];
|
||||||
|
|
||||||
const audio_languages = [];
|
const audio_languages: string[] = [];
|
||||||
|
|
||||||
// set object booleans
|
// set object booleans
|
||||||
if(iMetadata.duration_ms){
|
if(iMetadata.duration_ms){
|
||||||
|
|
@ -523,7 +523,7 @@ export default class Crunchy implements ServiceClass {
|
||||||
const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata ? true : false;
|
const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||||
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false;
|
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||||
// make obj ids
|
// make obj ids
|
||||||
const objects_ids = [];
|
const objects_ids: string[] = [];
|
||||||
objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id);
|
objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id);
|
||||||
if(item.seq_id){
|
if(item.seq_id){
|
||||||
objects_ids.unshift(item.seq_id);
|
objects_ids.unshift(item.seq_id);
|
||||||
|
|
@ -870,7 +870,7 @@ export default class Crunchy implements ServiceClass {
|
||||||
return objectInfo;
|
return objectInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedMedia = [];
|
const selectedMedia: Partial<CrunchyEpMeta>[] = [];
|
||||||
|
|
||||||
for(const item of objectInfo.data){
|
for(const item of objectInfo.data){
|
||||||
if(item.type != 'episode' && item.type != 'movie'){
|
if(item.type != 'episode' && item.type != 'movie'){
|
||||||
|
|
@ -998,7 +998,7 @@ export default class Crunchy implements ServiceClass {
|
||||||
} as Variable;
|
} as Variable;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let streams = [];
|
let streams: any[] = [];
|
||||||
let hsLangs: string[] = [];
|
let hsLangs: string[] = [];
|
||||||
const pbStreams = pbData.data[0];
|
const pbStreams = pbData.data[0];
|
||||||
|
|
||||||
|
|
|
||||||
8
funi.ts
8
funi.ts
|
|
@ -19,7 +19,7 @@ import * as yamlCfg from './modules/module.cfg-loader';
|
||||||
import vttConvert from './modules/module.vttconvert';
|
import vttConvert from './modules/module.vttconvert';
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { Item } from './@types/items';
|
import type { Item } from './@types/items.js';
|
||||||
|
|
||||||
// params
|
// params
|
||||||
|
|
||||||
|
|
@ -266,7 +266,7 @@ export default class Funi implements ServiceClass {
|
||||||
return showList;
|
return showList;
|
||||||
const eps = showList.value;
|
const eps = showList.value;
|
||||||
const epSelList = parseSelect(data.e as string, data.but);
|
const epSelList = parseSelect(data.e as string, data.but);
|
||||||
const fnSlug: FuniEpisodeData[] = [], epSelEpsTxt = []; let is_selected = false;
|
const fnSlug: FuniEpisodeData[] = [], epSelEpsTxt: string[] = []; let is_selected = false;
|
||||||
|
|
||||||
|
|
||||||
for(const e in eps){
|
for(const e in eps){
|
||||||
|
|
@ -331,7 +331,7 @@ export default class Funi implements ServiceClass {
|
||||||
debug: this.debug,
|
debug: this.debug,
|
||||||
});
|
});
|
||||||
if(!episodeData.ok || !episodeData.res){return { isOk: false, reason: new Error('Unable to get episodeData') }; }
|
if(!episodeData.ok || !episodeData.res){return { isOk: false, reason: new Error('Unable to get episodeData') }; }
|
||||||
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds = [];
|
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds: { id: number, lang: langsData.LanguageItem }[] = [];
|
||||||
// build fn
|
// build fn
|
||||||
season = parseInt(ep.parent.seasonNumber);
|
season = parseInt(ep.parent.seasonNumber);
|
||||||
if(ep.mediaCategory != 'Episode'){
|
if(ep.mediaCategory != 'Episode'){
|
||||||
|
|
@ -508,7 +508,7 @@ export default class Funi implements ServiceClass {
|
||||||
plStreams: Record<string|number, {
|
plStreams: Record<string|number, {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}> = {},
|
}> = {},
|
||||||
plLayersStr = [],
|
plLayersStr: string[] = [],
|
||||||
plLayersRes: Record<string|number, {
|
plLayersRes: Record<string|number, {
|
||||||
width: number,
|
width: number,
|
||||||
height: number
|
height: number
|
||||||
|
|
|
||||||
1
gui.ts
Normal file
1
gui.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import './gui/server/index'
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
USE_BROWSER=true
|
|
||||||
TEST=true
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
|
|
@ -1,143 +0,0 @@
|
||||||
import { app, BrowserWindow, dialog, screen } from 'electron';
|
|
||||||
import path from 'path/posix';
|
|
||||||
import fs from 'fs';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import express from 'express';
|
|
||||||
import { Console } from 'console';
|
|
||||||
import json from '../../../package.json';
|
|
||||||
|
|
||||||
process.on('uncaughtException', (er, or) => {
|
|
||||||
console.error(er, or);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (er, pr) => {
|
|
||||||
console.log(er, pr);
|
|
||||||
});
|
|
||||||
|
|
||||||
const getDataDirectory = () => {
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'darwin': {
|
|
||||||
if (!process.env.HOME) {
|
|
||||||
console.error('Unknown home directory');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return path.join(process.env.HOME, 'Library', 'Application Support', json.name);
|
|
||||||
}
|
|
||||||
case 'win32': {
|
|
||||||
if (!process.env.APPDATA) {
|
|
||||||
console.error('Unknown home directory');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log('Appdata', process.env.APPDATA);
|
|
||||||
return path.join(process.env.APPDATA, json.name);
|
|
||||||
}
|
|
||||||
case 'linux': {
|
|
||||||
if (!process.env.HOME) {
|
|
||||||
console.error('Unknown home directory');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return path.join(process.env.HOME, `.${json.name}`);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.error('Unsupported platform!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (!fs.existsSync(getDataDirectory()))
|
|
||||||
fs.mkdirSync(getDataDirectory());
|
|
||||||
|
|
||||||
export { getDataDirectory };
|
|
||||||
process.env.contentDirectory = getDataDirectory();
|
|
||||||
|
|
||||||
import './menu';
|
|
||||||
|
|
||||||
|
|
||||||
if (fs.existsSync(path.join(__dirname, '.env')))
|
|
||||||
dotenv.config({ path: path.join(__dirname, '.env'), debug: true });
|
|
||||||
|
|
||||||
if (require('electron-squirrel-startup')) {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isWindows = process.platform === 'win32';
|
|
||||||
|
|
||||||
let mainWindow: BrowserWindow|undefined = undefined;
|
|
||||||
export { mainWindow };
|
|
||||||
|
|
||||||
const icon = path.join(__dirname, 'images', `Logo_Inverted.${isWindows ? 'ico' : 'png'}`);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-global-assign
|
|
||||||
console = (() => {
|
|
||||||
const logFolder = path.join(getDataDirectory(), 'logs');
|
|
||||||
if (!fs.existsSync(logFolder))
|
|
||||||
fs.mkdirSync(logFolder);
|
|
||||||
if (fs.existsSync(path.join(logFolder, 'latest.log')))
|
|
||||||
fs.renameSync(path.join(logFolder, 'latest.log'), path.join(logFolder, `${Date.now()}.log`));
|
|
||||||
return new Console(fs.createWriteStream(path.join(logFolder, 'latest.log')));
|
|
||||||
})();
|
|
||||||
|
|
||||||
const createWindow = async () => {
|
|
||||||
(await import('../../../modules/module.cfg-loader')).ensureConfig();
|
|
||||||
// Create the browser window.
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: screen.getPrimaryDisplay().bounds.width,
|
|
||||||
height: screen.getPrimaryDisplay().bounds.height,
|
|
||||||
title: `AniDL GUI v${json.version}`,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
preload: path.join(__dirname, 'preload.js')
|
|
||||||
},
|
|
||||||
icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.on('crashed', (e) => console.log(e));
|
|
||||||
|
|
||||||
(await import('./messageHandler')).default(mainWindow);
|
|
||||||
|
|
||||||
if (!process.env.USE_BROWSER) {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Path.sep seems to return / on windows with electron
|
|
||||||
// \\ in Filename on Linux is possible but I don't see another way rn
|
|
||||||
const sep = isWindows ? '\\' : '/';
|
|
||||||
|
|
||||||
const p = __dirname.split(sep);
|
|
||||||
p.pop();
|
|
||||||
p.push('build');
|
|
||||||
|
|
||||||
console.log(p.join(sep));
|
|
||||||
|
|
||||||
app.use(express.static(p.join(sep)));
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log('Express started');
|
|
||||||
resolve(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.loadURL('http://localhost:3000');
|
|
||||||
if (process.env.TEST)
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
};
|
|
||||||
|
|
||||||
app.on('ready', createWindow);
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('quit', () => {
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import { Menu, MenuItem, MenuItemConstructorOptions, shell } from 'electron';
|
|
||||||
import path from 'path';
|
|
||||||
import { getDataDirectory } from '.';
|
|
||||||
import json from '../../../package.json';
|
|
||||||
|
|
||||||
const template: (MenuItemConstructorOptions | MenuItem)[] = [
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
role: 'undo'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'redo'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'cut'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'copy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'paste'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Debug',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
role: 'toggleDevTools'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open log folder',
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(path.join(getDataDirectory(), 'logs'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'forceReload'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Settings',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Open settings folder',
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(path.join(getDataDirectory(), 'config'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open settings file...',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'FFmpeg/Mkvmerge path',
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(path.join(getDataDirectory(), 'config', 'bin-path.yml'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Advanced options',
|
|
||||||
sublabel: 'See the documention for the options you may enter here',
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(path.join(getDataDirectory(), 'config', 'cli-defaults.yml'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Output path',
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(path.join(getDataDirectory(), 'config', 'dir-path.yml'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Help',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Version',
|
|
||||||
sublabel: json.version
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'GitHub',
|
|
||||||
click: () => {
|
|
||||||
shell.openExternal('https://github.com/anidl/multi-downloader-nx');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Report a Bug',
|
|
||||||
click: () => {
|
|
||||||
shell.openExternal(`https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG&version=${json.version}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contributors',
|
|
||||||
click: () => {
|
|
||||||
shell.openExternal('https://github.com/anidl/multi-downloader-nx/graphs/contributors');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Discord',
|
|
||||||
click: () => {
|
|
||||||
shell.openExternal('https://discord.gg/qEpbWen5vq');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { BrowserWindow, ipcMain } from 'electron';
|
|
||||||
import { MessageHandler } from '../../../@types/messageHandler';
|
|
||||||
import Crunchy from './serviceHandler/crunchyroll';
|
|
||||||
import Funimation from './serviceHandler/funimation';
|
|
||||||
|
|
||||||
export default (window: BrowserWindow) => {
|
|
||||||
let handler: MessageHandler|undefined;
|
|
||||||
ipcMain.handle('setup', (_, data) => {
|
|
||||||
if (data === 'funi') {
|
|
||||||
handler = new Funimation(window);
|
|
||||||
} else if (data === 'crunchy') {
|
|
||||||
handler = new Crunchy(window);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('changeProvider', (ev) => {
|
|
||||||
if (handler?.isDownloading())
|
|
||||||
return ev.returnValue = false;
|
|
||||||
handler = undefined;
|
|
||||||
ev.returnValue = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('type', async () => handler === undefined ? undefined : handler instanceof Funimation ? 'funi' : 'crunchy');
|
|
||||||
ipcMain.handle('auth', async (_, data) => handler?.auth(data));
|
|
||||||
ipcMain.handle('checkToken', async () => handler?.checkToken());
|
|
||||||
ipcMain.handle('search', async (_, data) => handler?.search(data));
|
|
||||||
ipcMain.handle('default', async (_, data) => handler?.handleDefault(data));
|
|
||||||
ipcMain.handle('availableDubCodes', async () => handler?.availableDubCodes());
|
|
||||||
ipcMain.handle('availableSubCodes', async () => handler?.availableSubCodes());
|
|
||||||
ipcMain.handle('resolveItems', async (_, data) => handler?.resolveItems(data));
|
|
||||||
ipcMain.handle('listEpisodes', async (_, data) => handler?.listEpisodes(data));
|
|
||||||
ipcMain.handle('downloadItem', async (_, data) => handler?.downloadItem(data));
|
|
||||||
ipcMain.handle('writeToClipboard', async (_, data) => handler?.writeToClipboard(data));
|
|
||||||
ipcMain.handle('openFolder', async (_, data) => handler?.openFolder(data));
|
|
||||||
ipcMain.on('isDownloading', (ev) => ev.returnValue = handler?.isDownloading());
|
|
||||||
};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { contextBridge, ipcRenderer } from 'electron';
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('Electron', {
|
|
||||||
ipcRenderer: {
|
|
||||||
...ipcRenderer,
|
|
||||||
on: (name: string, handler: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
|
||||||
ipcRenderer.on(name, handler);
|
|
||||||
return ipcRenderer;
|
|
||||||
},
|
|
||||||
removeListener: (name: string, handler: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
|
||||||
ipcRenderer.removeListener(name, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import { BrowserWindow, clipboard, dialog, shell } from 'electron';
|
|
||||||
import { DownloadInfo, FolderTypes, ProgressData } from '../../../../@types/messageHandler';
|
|
||||||
import { RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
|
|
||||||
import { loadCfg } from '../../../../modules/module.cfg-loader';
|
|
||||||
|
|
||||||
export default class Base {
|
|
||||||
|
|
||||||
constructor(private window: BrowserWindow) {}
|
|
||||||
|
|
||||||
private downloading = false;
|
|
||||||
|
|
||||||
setDownloading(downloading: boolean) {
|
|
||||||
this.downloading = downloading;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDownloading() {
|
|
||||||
return this.downloading;
|
|
||||||
}
|
|
||||||
|
|
||||||
alertError(error: Error) {
|
|
||||||
dialog.showMessageBoxSync(this.window, {
|
|
||||||
message: `${error.name ?? 'An error occured'}\n${error.message}`,
|
|
||||||
detail: error.stack,
|
|
||||||
title: 'Error',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
makeProgressHandler(videoInfo: DownloadInfo) {
|
|
||||||
return ((data: ProgressData) => {
|
|
||||||
const progress = (typeof data.percent === 'string' ?
|
|
||||||
parseFloat(data.percent) : data.percent) / 100;
|
|
||||||
this.window.setProgressBar(progress === 1 ? -1 : progress);
|
|
||||||
this.sendMessage({
|
|
||||||
name: 'progress',
|
|
||||||
data: {
|
|
||||||
downloadInfo: videoInfo,
|
|
||||||
progress: data
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
getWindow() {
|
|
||||||
return this.window;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
|
||||||
this.window.webContents.send('randomEvent', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
isDownloading() {
|
|
||||||
return this.downloading;
|
|
||||||
}
|
|
||||||
|
|
||||||
async writeToClipboard(text: string) {
|
|
||||||
clipboard.writeText(text, 'clipboard');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async openFolder(folderType: FolderTypes) {
|
|
||||||
const conf = loadCfg();
|
|
||||||
switch (folderType) {
|
|
||||||
case 'content':
|
|
||||||
shell.openPath(conf.dir.content);
|
|
||||||
break;
|
|
||||||
case 'config':
|
|
||||||
shell.openPath(conf.dir.config);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
1
gui/react/.env
Normal file
1
gui/react/.env
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
PORT=3002
|
||||||
|
|
@ -18,8 +18,11 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
|
"proxy": "http://localhost:3000",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
|
|
@ -43,5 +46,8 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,14 @@ specifiers:
|
||||||
'@types/node': ^18.14.0
|
'@types/node': ^18.14.0
|
||||||
'@types/react': ^18.0.25
|
'@types/react': ^18.0.25
|
||||||
'@types/react-dom': ^18.0.11
|
'@types/react-dom': ^18.0.11
|
||||||
|
'@types/uuid': ^9.0.1
|
||||||
notistack: ^2.0.8
|
notistack: ^2.0.8
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
react-scripts: 5.0.1
|
react-scripts: 5.0.1
|
||||||
typescript: ^4.9.5
|
typescript: ^4.9.5
|
||||||
|
uuid: ^9.0.0
|
||||||
|
ws: ^8.12.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.20.12
|
'@babel/core': 7.20.12
|
||||||
|
|
@ -35,6 +38,11 @@ dependencies:
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
react-scripts: 5.0.1_pegpel5nwbugtuutvxsiaw5kjq
|
react-scripts: 5.0.1_pegpel5nwbugtuutvxsiaw5kjq
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
|
uuid: 9.0.0
|
||||||
|
ws: 8.12.1
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@types/uuid': 9.0.1
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|
@ -2753,6 +2761,10 @@ packages:
|
||||||
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
|
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/uuid/9.0.1:
|
||||||
|
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/ws/8.5.4:
|
/@types/ws/8.5.4:
|
||||||
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
|
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -9805,6 +9817,11 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/uuid/9.0.0:
|
||||||
|
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/v8-to-istanbul/8.1.1:
|
/v8-to-istanbul/8.1.1:
|
||||||
resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==}
|
resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==}
|
||||||
engines: {node: '>=10.12.0'}
|
engines: {node: '>=10.12.0'}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@ import { messageChannelContext } from './provider/MessageChannel';
|
||||||
import { ClearAll, Folder } from "@mui/icons-material";
|
import { ClearAll, Folder } from "@mui/icons-material";
|
||||||
import useStore from "./hooks/useStore";
|
import useStore from "./hooks/useStore";
|
||||||
import StartQueueButton from "./components/StartQueue";
|
import StartQueueButton from "./components/StartQueue";
|
||||||
|
import MenuBar from "./components/MenuBar/MenuBar";
|
||||||
|
|
||||||
const Layout: React.FC = () => {
|
const Layout: React.FC = () => {
|
||||||
|
|
||||||
const messageHandler = React.useContext(messageChannelContext);
|
const messageHandler = React.useContext(messageChannelContext);
|
||||||
const [, dispatch] = useStore();
|
const [, dispatch] = useStore();
|
||||||
|
|
||||||
return <Box>
|
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box sx={{ height: 50, mb: 4, display: 'flex', gap: 1 }}>
|
<MenuBar />
|
||||||
|
<Box sx={{ height: 50, mb: 4, display: 'flex', gap: 1, mt: 3 }}>
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
<AuthButton />
|
<AuthButton />
|
||||||
<Box sx={{ display: 'flex', gap: 1, height: 36 }}>
|
<Box sx={{ display: 'flex', gap: 1, height: 36 }}>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
|
||||||
|
|
||||||
const Style: FCWithChildren = ({children}) => {
|
const Style: FCWithChildren = ({children}) => {
|
||||||
return <ThemeProvider theme={makeTheme('dark')}>
|
return <ThemeProvider theme={makeTheme('dark')}>
|
||||||
<Container sx={{ mt: 3 }} maxWidth='xl'>
|
<Container maxWidth='xl'>
|
||||||
<Box sx={{ position: 'fixed', height: '100%', width: '100%', zIndex: -500, backgroundColor: 'rgb(0, 30, 60)', top: 0, left: 0 }}/>
|
<Box sx={{ position: 'fixed', height: '100%', width: '100%', zIndex: -500, backgroundColor: 'rgb(0, 30, 60)', top: 0, left: 0 }}/>
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ const LogoutButton: React.FC = () => {
|
||||||
const messageChannel = React.useContext(messageChannelContext);
|
const messageChannel = React.useContext(messageChannelContext);
|
||||||
const [, dispatch] = useStore();
|
const [, dispatch] = useStore();
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
if (messageChannel?.isDownloading())
|
if (await messageChannel?.isDownloading())
|
||||||
return alert('You are currently downloading. Please finish the download first.');
|
return alert('You are currently downloading. Please finish the download first.');
|
||||||
if (messageChannel?.logout())
|
if (await messageChannel?.logout())
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'service',
|
type: 'service',
|
||||||
payload: undefined
|
payload: undefined
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,14 @@ const useDownloadManager = () => {
|
||||||
}, [messageHandler, dispatch]);
|
}, [messageHandler, dispatch]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentDownload)
|
(async () => {
|
||||||
return;
|
if (!currentDownload)
|
||||||
if (messageHandler?.isDownloading())
|
return;
|
||||||
return;
|
if (await messageHandler?.isDownloading())
|
||||||
console.log('start download');
|
return;
|
||||||
messageHandler?.downloadItem(currentDownload);
|
console.log('start download');
|
||||||
|
messageHandler?.downloadItem(currentDownload);
|
||||||
|
})();
|
||||||
}, [currentDownload, messageHandler]);
|
}, [currentDownload, messageHandler]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
85
gui/react/src/components/MenuBar/MenuBar.tsx
Normal file
85
gui/react/src/components/MenuBar/MenuBar.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Box, Button, Menu, MenuItem } from "@mui/material";
|
||||||
|
import React from "react"
|
||||||
|
import { messageChannelContext } from "../../provider/MessageChannel";
|
||||||
|
|
||||||
|
const MenuBar: React.FC = () => {
|
||||||
|
const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>();
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
const msg = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLElement>, n: 'settings'|'help') => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
setMenuOpen(n);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setMenuOpen(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!msg)
|
||||||
|
return <></>
|
||||||
|
|
||||||
|
return <Box sx={{ width: '100%', display: 'flex' }}>
|
||||||
|
<Button onClick={(e) => handleClick(e, 'settings')}>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
<Button onClick={(e) => handleClick(e, 'help')}>
|
||||||
|
Help
|
||||||
|
</Button>
|
||||||
|
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFolder('config');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open settings folder
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFile(['config', 'bin-path.yml']);
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open FFmpeg/Mkvmerge file
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFile(['config', 'cli-defaults.yml']);
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open advanced options
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFolder('content');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open output path
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Menu open={openMenu === 'help'} anchorEl={anchorEl} onClose={handleClose}>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://github.com/anidl/multi-downloader-nx');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
GitHub
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Report a bug
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Contributors
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://discord.gg/qEpbWen5vq');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Discord
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuBar;
|
||||||
|
|
@ -9,8 +9,8 @@ const StartQueueButton: React.FC = () => {
|
||||||
const messageChannel = React.useContext(messageChannelContext);
|
const messageChannel = React.useContext(messageChannelContext);
|
||||||
const [store, dispatch] = useStore();
|
const [store, dispatch] = useStore();
|
||||||
|
|
||||||
const change = () => {
|
const change = async () => {
|
||||||
if (messageChannel?.isDownloading() && store.downloadQueue)
|
if (await messageChannel?.isDownloading() && store.downloadQueue)
|
||||||
alert("The current download will be finished before the queue stops")
|
alert("The current download will be finished before the queue stops")
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'downloadQueue',
|
type: 'downloadQueue',
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,24 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => {
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container!);
|
const root = createRoot(container!);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<ErrorHandler>
|
||||||
<ErrorHandler>
|
<Store>
|
||||||
<Store>
|
<SnackbarProvider
|
||||||
<SnackbarProvider
|
ref={notistackRef}
|
||||||
ref={notistackRef}
|
action={(key) => (
|
||||||
action={(key) => (
|
<IconButton onClick={onClickDismiss(key)} color="inherit">
|
||||||
<IconButton onClick={onClickDismiss(key)} color="inherit">
|
<CloseOutlined />
|
||||||
<CloseOutlined />
|
</IconButton>
|
||||||
</IconButton>
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<Style>
|
||||||
<Style>
|
<MessageChannel>
|
||||||
<MessageChannel>
|
<ServiceProvider>
|
||||||
<ServiceProvider>
|
<App />
|
||||||
<App />
|
</ServiceProvider>
|
||||||
</ServiceProvider>
|
</MessageChannel>
|
||||||
</MessageChannel>
|
</Style>
|
||||||
</Style>
|
</SnackbarProvider>
|
||||||
</SnackbarProvider>
|
</Store>
|
||||||
</Store>
|
</ErrorHandler>
|
||||||
</ErrorHandler>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { MessageHandler } from '../../../../@types/messageHandler';
|
import type { AuthData, AuthResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
|
||||||
import type { IpcRenderer, IpcRendererEvent } from "electron";
|
|
||||||
import useStore from '../hooks/useStore';
|
import useStore from '../hooks/useStore';
|
||||||
|
import type { MessageTypes, WSMessage, WSMessageWithID } from '../../../../@types/ws';
|
||||||
import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
|
import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
|
||||||
import { Backdrop, Typography } from '@mui/material';
|
import { Backdrop, Typography } from '@mui/material';
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
|
|
||||||
export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => boolean });
|
export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> });
|
||||||
|
|
||||||
export class RandomEventHandler {
|
export class RandomEventHandler {
|
||||||
private handler: {
|
private handler: {
|
||||||
|
|
@ -36,53 +36,93 @@ export class RandomEventHandler {
|
||||||
|
|
||||||
export const messageChannelContext = React.createContext<FrontEndMessanges|undefined>(undefined);
|
export const messageChannelContext = React.createContext<FrontEndMessanges|undefined>(undefined);
|
||||||
|
|
||||||
|
async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocket, msg: WSMessage<T>): Promise<WSMessage<T, 1>> {
|
||||||
|
const id = v4();
|
||||||
|
const ret = new Promise<WSMessage<T, 1>>((resolve) => {
|
||||||
|
const handler = function({ data }: MessageEvent) {
|
||||||
|
const parsed = JSON.parse(data.toString()) as WSMessageWithID<T, 1>;
|
||||||
|
if (parsed.id === id) {
|
||||||
|
socket.removeEventListener('message', handler);
|
||||||
|
resolve(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket.addEventListener('message', handler);
|
||||||
|
});
|
||||||
|
const toSend = msg as WSMessageWithID<T>;
|
||||||
|
toSend.id = id;
|
||||||
|
|
||||||
|
socket.send(JSON.stringify(toSend));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
||||||
|
|
||||||
const [store, dispatch] = useStore();
|
const [store, dispatch] = useStore();
|
||||||
|
const [socket, setSocket] = React.useState<undefined|WebSocket|null>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const wws = new WebSocket(`ws://localhost:3000/ws?${new URLSearchParams({
|
||||||
|
password: prompt('This website requires a password') ?? ''
|
||||||
|
})}`, );
|
||||||
|
wws.addEventListener('open', () => {
|
||||||
|
console.log('[INFO] [WS] Connected');
|
||||||
|
setSocket(wws);
|
||||||
|
});
|
||||||
|
wws.addEventListener('error', (er) => {
|
||||||
|
console.error(`[ERROR] [WS]`, er);
|
||||||
|
setSocket(null);
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const { ipcRenderer } = (window as any).Electron as { ipcRenderer: IpcRenderer };
|
|
||||||
const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []);
|
const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const currentService = await ipcRenderer.invoke('type');
|
if (!socket)
|
||||||
if (currentService !== undefined)
|
return;
|
||||||
return dispatch({ type: 'service', payload: currentService });
|
const currentService = await messageAndResponse(socket, { name: 'type', data: undefined });
|
||||||
if (store.service !== currentService)
|
if (currentService.data !== undefined)
|
||||||
ipcRenderer.invoke('setup', store.service)
|
return dispatch({ type: 'service', payload: currentService.data });
|
||||||
|
if (store.service !== currentService.data)
|
||||||
|
messageAndResponse(socket, { name: 'setup', data: store.service });
|
||||||
})();
|
})();
|
||||||
}, [store.service, dispatch, ipcRenderer])
|
}, [store.service, dispatch, socket])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (!socket)
|
||||||
|
return;
|
||||||
/* finish is a placeholder */
|
/* finish is a placeholder */
|
||||||
const listener = (_: IpcRendererEvent, initalData: RandomEvent<'finish'>) => {
|
const listener = (initalData: MessageEvent<string>) => {
|
||||||
const eventName = initalData.name as keyof RandomEvents;
|
const data = JSON.parse(initalData.data) as RandomEvent<'finish'>;
|
||||||
const data = initalData as unknown as RandomEvent<typeof eventName>;
|
|
||||||
|
|
||||||
randomEventHandler.emit(data.name, data);
|
randomEventHandler.emit(data.name, data);
|
||||||
}
|
}
|
||||||
ipcRenderer.on('randomEvent', listener);
|
socket.addEventListener('message', listener);
|
||||||
return () => {
|
return () => {
|
||||||
ipcRenderer.removeListener('randomEvent', listener);
|
socket.removeEventListener('message', listener);
|
||||||
};
|
};
|
||||||
}, [ ipcRenderer ]);
|
}, [ socket ]);
|
||||||
|
|
||||||
|
if (socket === undefined || socket === null)
|
||||||
|
return <Typography color='primary'>{socket === undefined ? 'Loading...' : 'WebSocket Error. Please try to reload and make sure the password ist correct.'}</Typography>;
|
||||||
|
|
||||||
const messageHandler: FrontEndMessanges = {
|
const messageHandler: FrontEndMessanges = {
|
||||||
auth: async (data) => await ipcRenderer.invoke('auth', data),
|
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
|
||||||
checkToken: async () => await ipcRenderer.invoke('checkToken'),
|
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
|
||||||
search: async (data) => await ipcRenderer.invoke('search', data),
|
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
|
||||||
handleDefault: async (data) => await ipcRenderer.invoke('default', data),
|
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
|
||||||
availableDubCodes: async () => await ipcRenderer.invoke('availableDubCodes'),
|
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data,
|
||||||
availableSubCodes: async () => await ipcRenderer.invoke('availableSubCodes'),
|
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
|
||||||
resolveItems: async (data) => await ipcRenderer.invoke('resolveItems', data),
|
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
|
||||||
listEpisodes: async (data) => await ipcRenderer.invoke('listEpisodes', data),
|
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
|
||||||
randomEvents: randomEventHandler,
|
randomEvents: randomEventHandler,
|
||||||
downloadItem: (data) => ipcRenderer.invoke('downloadItem', data),
|
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
|
||||||
isDownloading: () => ipcRenderer.sendSync('isDownloading'),
|
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
|
||||||
writeToClipboard: async (data) => await ipcRenderer.invoke('writeToClipboard', data),
|
writeToClipboard: async (data) => messageAndResponse(socket, { name: 'writeToClipboard', data }),
|
||||||
openFolder: async (data) => await ipcRenderer.invoke('openFolder', data),
|
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
|
||||||
logout: () => ipcRenderer.sendSync('changeProvider')
|
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
|
||||||
|
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
|
||||||
|
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
return <messageChannelContext.Provider value={messageHandler}>
|
return <messageChannelContext.Provider value={messageHandler}>
|
||||||
|
|
|
||||||
29
gui/server/index.ts
Normal file
29
gui/server/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { ensureConfig, loadCfg, workingDir } from '../../modules/module.cfg-loader';
|
||||||
|
import cors from 'cors';
|
||||||
|
import ServiceHandler from './serviceHandler';
|
||||||
|
import open from 'open';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
process.title = 'AniDL';
|
||||||
|
|
||||||
|
ensureConfig();
|
||||||
|
|
||||||
|
const cfg = loadCfg();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
export { app, cfg };
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge: 1000 * 60 * 20 }))
|
||||||
|
|
||||||
|
const server = app.listen(cfg.gui.port, () => {
|
||||||
|
console.log(`[INFO] GUI server started on port ${cfg.gui.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
new ServiceHandler(server);
|
||||||
|
|
||||||
|
open(`http://localhost:${cfg.gui.port}`);
|
||||||
100
gui/server/serviceHandler.ts
Normal file
100
gui/server/serviceHandler.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { ServerResponse } from 'http';
|
||||||
|
import { Server } from 'http';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
|
import { MessageHandler } from '../../@types/messageHandler';
|
||||||
|
import Funi from '../../funi';
|
||||||
|
import CrunchyHandler from './services/crunchyroll';
|
||||||
|
import FunimationHandler from './services/funimation';
|
||||||
|
import WebSocketHandler from './websocket';
|
||||||
|
|
||||||
|
export default class ServiceHandler {
|
||||||
|
|
||||||
|
private service: MessageHandler|undefined = undefined;
|
||||||
|
private ws: WebSocketHandler;
|
||||||
|
|
||||||
|
constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) {
|
||||||
|
this.ws = new WebSocketHandler(server);
|
||||||
|
this.handleMessanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessanges() {
|
||||||
|
this.ws.events.on('setup', ({ data }) => {
|
||||||
|
if (data === 'funi') {
|
||||||
|
this.service = new FunimationHandler(this.ws);
|
||||||
|
} else if (data === 'crunchy') {
|
||||||
|
this.service = new CrunchyHandler(this.ws);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.events.on('changeProvider', async (_, respond) => {
|
||||||
|
if (await this.service?.isDownloading())
|
||||||
|
return respond(false);
|
||||||
|
this.service = undefined;
|
||||||
|
respond(true);
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.events.on('auth', async ({ data }, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
respond(await this.service.auth(data));
|
||||||
|
});
|
||||||
|
this.ws.events.on('type', async ({}, respond) => respond(this.service === undefined ? undefined : this.service instanceof Funi ? 'funi' : 'crunchy'));
|
||||||
|
this.ws.events.on('checkToken', async ({}, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
respond(await this.service.checkToken());
|
||||||
|
});
|
||||||
|
this.ws.events.on('search', async ({ data }, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
respond(await this.service.search(data));
|
||||||
|
});
|
||||||
|
this.ws.events.on('default', async ({ data }, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
respond(await this.service.handleDefault(data));
|
||||||
|
});
|
||||||
|
this.ws.events.on('availableDubCodes', async ({}, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond([]);
|
||||||
|
respond(await this.service.availableDubCodes());
|
||||||
|
});
|
||||||
|
this.ws.events.on('availableSubCodes', async ({}, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond([]);
|
||||||
|
respond(await this.service.availableSubCodes());
|
||||||
|
});
|
||||||
|
this.ws.events.on('resolveItems', async ({ data }, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
respond(await this.service.resolveItems(data));
|
||||||
|
});
|
||||||
|
this.ws.events.on('listEpisodes', async ({ data }, respond) => {
|
||||||
|
if (this.service === undefined)
|
||||||
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
respond(await this.service.listEpisodes(data));
|
||||||
|
});
|
||||||
|
this.ws.events.on('downloadItem', async ({ data }, respond) => {
|
||||||
|
this.service!.downloadItem(data);
|
||||||
|
respond(undefined);
|
||||||
|
});
|
||||||
|
this.ws.events.on('writeToClipboard', async ({ data }, respond) => {
|
||||||
|
this.service!.writeToClipboard(data);
|
||||||
|
respond(undefined);
|
||||||
|
});
|
||||||
|
this.ws.events.on('openFolder', async ({ data }, respond) => {
|
||||||
|
this.service!.openFolder(data);
|
||||||
|
respond(undefined);
|
||||||
|
});
|
||||||
|
this.ws.events.on('openFile', async ({ data }, respond) => {
|
||||||
|
this.service!.openFile(data);
|
||||||
|
respond(undefined);
|
||||||
|
});
|
||||||
|
this.ws.events.on('openURL', async ({ data }, respond) => {
|
||||||
|
this.service!.openURL(data);
|
||||||
|
respond(undefined);
|
||||||
|
});
|
||||||
|
this.ws.events.on('isDownloading', async ({}, respond) => respond(await this.service!.isDownloading()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
77
gui/server/services/base.ts
Normal file
77
gui/server/services/base.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { DownloadInfo, FolderTypes, ProgressData } from '../../../@types/messageHandler';
|
||||||
|
import { RandomEvent, RandomEvents } from '../../../@types/randomEvents';
|
||||||
|
import WebSocketHandler from '../websocket';
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import open from 'open';
|
||||||
|
import { cfg } from '..';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default class Base {
|
||||||
|
|
||||||
|
constructor(private ws: WebSocketHandler) {}
|
||||||
|
|
||||||
|
private downloading = false;
|
||||||
|
|
||||||
|
setDownloading(downloading: boolean) {
|
||||||
|
this.downloading = downloading;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDownloading() {
|
||||||
|
return this.downloading;
|
||||||
|
}
|
||||||
|
|
||||||
|
alertError(error: Error) {
|
||||||
|
console.log(`[ERROR] ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeProgressHandler(videoInfo: DownloadInfo) {
|
||||||
|
return ((data: ProgressData) => {
|
||||||
|
this.sendMessage({
|
||||||
|
name: 'progress',
|
||||||
|
data: {
|
||||||
|
downloadInfo: videoInfo,
|
||||||
|
progress: data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
||||||
|
this.ws.sendMessage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isDownloading() {
|
||||||
|
return this.downloading;
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeToClipboard(text: string) {
|
||||||
|
copy(text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openFolder(folderType: FolderTypes) {
|
||||||
|
switch (folderType) {
|
||||||
|
case 'content':
|
||||||
|
open(cfg.dir.content);
|
||||||
|
break;
|
||||||
|
case 'config':
|
||||||
|
open(cfg.dir.config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openFile(data: [FolderTypes, string]) {
|
||||||
|
switch (data[0]) {
|
||||||
|
case 'config':
|
||||||
|
open(path.join(cfg.dir.config, data[1]));
|
||||||
|
break;
|
||||||
|
case 'content':
|
||||||
|
throw new Error('No subfolders');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openURL(data: string) {
|
||||||
|
open(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,110 +1,110 @@
|
||||||
import { BrowserWindow } from 'electron';
|
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||||
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
|
import Crunchy from '../../../crunchy';
|
||||||
import Crunchy from '../../../../crunchy';
|
import { ArgvType } from '../../../modules/module.app-args';
|
||||||
import { ArgvType } from '../../../../modules/module.app-args';
|
import { buildDefault, getDefault } from '../../../modules/module.args';
|
||||||
import { buildDefault, getDefault } from '../../../../modules/module.args';
|
import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData';
|
||||||
import { languages, subtitleLanguagesFilter } from '../../../../modules/module.langsData';
|
import WebSocketHandler from '../websocket';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
class CrunchyHandler extends Base implements MessageHandler {
|
class CrunchyHandler extends Base implements MessageHandler {
|
||||||
private crunchy: Crunchy;
|
private crunchy: Crunchy;
|
||||||
constructor(window: BrowserWindow) {
|
constructor(ws: WebSocketHandler) {
|
||||||
super(window);
|
super(ws);
|
||||||
this.crunchy = new Crunchy();
|
this.crunchy = new Crunchy();
|
||||||
this.crunchy.refreshToken();
|
this.crunchy.refreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
|
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
|
||||||
await this.crunchy.refreshToken(true);
|
await this.crunchy.refreshToken(true);
|
||||||
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
|
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleDefault(name: string) {
|
public async handleDefault(name: string) {
|
||||||
return getDefault(name, this.crunchy.cfg.cli);
|
return getDefault(name, this.crunchy.cfg.cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async availableDubCodes(): Promise<string[]> {
|
public async availableDubCodes(): Promise<string[]> {
|
||||||
const dubLanguageCodesArray = [];
|
const dubLanguageCodesArray: string[] = [];
|
||||||
for(const language of languages){
|
for(const language of languages){
|
||||||
if (language.cr_locale)
|
if (language.cr_locale)
|
||||||
dubLanguageCodesArray.push(language.code);
|
dubLanguageCodesArray.push(language.code);
|
||||||
}
|
}
|
||||||
return [...new Set(dubLanguageCodesArray)];
|
return [...new Set(dubLanguageCodesArray)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async availableSubCodes(): Promise<string[]> {
|
public async availableSubCodes(): Promise<string[]> {
|
||||||
return subtitleLanguagesFilter;
|
return subtitleLanguagesFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
||||||
await this.crunchy.refreshToken(true);
|
await this.crunchy.refreshToken(true);
|
||||||
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
||||||
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
||||||
if (!res.isOk)
|
if (!res.isOk)
|
||||||
return res;
|
return res;
|
||||||
return { isOk: true, value: res.value.map(a => {
|
return { isOk: true, value: res.value.map(a => {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
ids: a.data.map(a => a.mediaId),
|
ids: a.data.map(a => a.mediaId),
|
||||||
title: a.episodeTitle,
|
title: a.episodeTitle,
|
||||||
parent: {
|
parent: {
|
||||||
title: a.seasonTitle,
|
title: a.seasonTitle,
|
||||||
season: a.season.toString()
|
season: a.season.toString()
|
||||||
},
|
},
|
||||||
e: a.e,
|
e: a.e,
|
||||||
image: a.image,
|
image: a.image,
|
||||||
episode: a.episodeNumber
|
episode: a.episodeNumber
|
||||||
};
|
};
|
||||||
}) };
|
}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(data: SearchData): Promise<SearchResponse> {
|
public async search(data: SearchData): Promise<SearchResponse> {
|
||||||
await this.crunchy.refreshToken(true);
|
await this.crunchy.refreshToken(true);
|
||||||
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
||||||
const crunchySearch = await this.crunchy.doSearch(data);
|
const crunchySearch = await this.crunchy.doSearch(data);
|
||||||
if (!crunchySearch.isOk) {
|
if (!crunchySearch.isOk) {
|
||||||
this.crunchy.refreshToken();
|
this.crunchy.refreshToken();
|
||||||
return crunchySearch;
|
return crunchySearch;
|
||||||
}
|
}
|
||||||
return { isOk: true, value: crunchySearch.value };
|
return { isOk: true, value: crunchySearch.value };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkToken(): Promise<CheckTokenResponse> {
|
public async checkToken(): Promise<CheckTokenResponse> {
|
||||||
if (await this.crunchy.getProfile()) {
|
if (await this.crunchy.getProfile()) {
|
||||||
return { isOk: true, value: undefined };
|
return { isOk: true, value: undefined };
|
||||||
} else {
|
} else {
|
||||||
return { isOk: false, reason: new Error('') };
|
return { isOk: false, reason: new Error('') };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public auth(data: AuthData) {
|
public auth(data: AuthData) {
|
||||||
return this.crunchy.doAuth(data);
|
return this.crunchy.doAuth(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadItem(data: DownloadData) {
|
public async downloadItem(data: DownloadData) {
|
||||||
await this.crunchy.refreshToken(true);
|
await this.crunchy.refreshToken(true);
|
||||||
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
|
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
|
||||||
this.setDownloading(true);
|
this.setDownloading(true);
|
||||||
const _default = buildDefault() as ArgvType;
|
const _default = buildDefault() as ArgvType;
|
||||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||||
dubLang: data.dubLang,
|
dubLang: data.dubLang,
|
||||||
e: data.e
|
e: data.e
|
||||||
});
|
});
|
||||||
if (res.isOk) {
|
if (res.isOk) {
|
||||||
for (const select of res.value) {
|
for (const select of res.value) {
|
||||||
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||||
novids: data.novids }))) {
|
novids: data.novids }))) {
|
||||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||||
er.name = 'Download error';
|
er.name = 'Download error';
|
||||||
this.alertError(er);
|
this.alertError(er);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.alertError(res.reason);
|
this.alertError(res.reason);
|
||||||
}
|
}
|
||||||
this.sendMessage({ name: 'finish', data: undefined });
|
this.sendMessage({ name: 'finish', data: undefined });
|
||||||
this.setDownloading(false);
|
this.setDownloading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CrunchyHandler;
|
export default CrunchyHandler;
|
||||||
|
|
@ -1,115 +1,115 @@
|
||||||
import { BrowserWindow } from 'electron';
|
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||||
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
|
import Funimation from '../../../funi';
|
||||||
import Funimation from '../../../../funi';
|
import { ArgvType } from '../../../modules/module.app-args';
|
||||||
import { ArgvType } from '../../../../modules/module.app-args';
|
import { buildDefault, getDefault } from '../../../modules/module.args';
|
||||||
import { buildDefault, getDefault } from '../../../../modules/module.args';
|
import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData';
|
||||||
import { languages, subtitleLanguagesFilter } from '../../../../modules/module.langsData';
|
import WebSocketHandler from '../websocket';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
class FunimationHandler extends Base implements MessageHandler {
|
class FunimationHandler extends Base implements MessageHandler {
|
||||||
private funi: Funimation;
|
private funi: Funimation;
|
||||||
constructor(window: BrowserWindow) {
|
constructor(ws: WebSocketHandler) {
|
||||||
super(window);
|
super(ws);
|
||||||
this.funi = new Funimation();
|
this.funi = new Funimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
|
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
|
||||||
const parse = parseInt(id);
|
const parse = parseInt(id);
|
||||||
if (isNaN(parse) || parse <= 0)
|
if (isNaN(parse) || parse <= 0)
|
||||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||||
const request = await this.funi.listShowItems(parse);
|
const request = await this.funi.listShowItems(parse);
|
||||||
if (!request.isOk)
|
if (!request.isOk)
|
||||||
return request;
|
return request;
|
||||||
return { isOk: true, value: request.value.map(item => ({
|
return { isOk: true, value: request.value.map(item => ({
|
||||||
e: item.id_split.join(''),
|
e: item.id_split.join(''),
|
||||||
lang: item.audio ?? [],
|
lang: item.audio ?? [],
|
||||||
name: item.title,
|
name: item.title,
|
||||||
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
|
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
|
||||||
seasonTitle: item.seasonTitle,
|
seasonTitle: item.seasonTitle,
|
||||||
episode: item.episodeNum,
|
episode: item.episodeNum,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
img: item.thumb,
|
img: item.thumb,
|
||||||
description: item.synopsis,
|
description: item.synopsis,
|
||||||
time: item.runtime ?? item.item.runtime
|
time: item.runtime ?? item.item.runtime
|
||||||
})) };
|
})) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleDefault(name: string) {
|
public async handleDefault(name: string) {
|
||||||
return getDefault(name, this.funi.cfg.cli);
|
return getDefault(name, this.funi.cfg.cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async availableDubCodes(): Promise<string[]> {
|
public async availableDubCodes(): Promise<string[]> {
|
||||||
const dubLanguageCodesArray = [];
|
const dubLanguageCodesArray: string[] = [];
|
||||||
for(const language of languages){
|
for(const language of languages){
|
||||||
if (language.funi_locale)
|
if (language.funi_locale)
|
||||||
dubLanguageCodesArray.push(language.code);
|
dubLanguageCodesArray.push(language.code);
|
||||||
}
|
}
|
||||||
return [...new Set(dubLanguageCodesArray)];
|
return [...new Set(dubLanguageCodesArray)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async availableSubCodes(): Promise<string[]> {
|
public async availableSubCodes(): Promise<string[]> {
|
||||||
return subtitleLanguagesFilter;
|
return subtitleLanguagesFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
||||||
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
||||||
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
|
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
|
||||||
if (!res.isOk)
|
if (!res.isOk)
|
||||||
return res;
|
return res;
|
||||||
return { isOk: true, value: res.value.map(a => {
|
return { isOk: true, value: res.value.map(a => {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
ids: [a.episodeID],
|
ids: [a.episodeID],
|
||||||
title: a.title,
|
title: a.title,
|
||||||
parent: {
|
parent: {
|
||||||
title: a.seasonTitle,
|
title: a.seasonTitle,
|
||||||
season: a.seasonNumber
|
season: a.seasonNumber
|
||||||
},
|
},
|
||||||
image: a.image,
|
image: a.image,
|
||||||
e: a.episodeID,
|
e: a.episodeID,
|
||||||
episode: a.epsiodeNumber,
|
episode: a.epsiodeNumber,
|
||||||
};
|
};
|
||||||
}) };
|
}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(data: SearchData): Promise<SearchResponse> {
|
public async search(data: SearchData): Promise<SearchResponse> {
|
||||||
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
||||||
const funiSearch = await this.funi.searchShow(false, data);
|
const funiSearch = await this.funi.searchShow(false, data);
|
||||||
if (!funiSearch.isOk)
|
if (!funiSearch.isOk)
|
||||||
return funiSearch;
|
return funiSearch;
|
||||||
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
|
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
|
||||||
image: a.image.showThumbnail,
|
image: a.image.showThumbnail,
|
||||||
name: a.title,
|
name: a.title,
|
||||||
desc: a.description,
|
desc: a.description,
|
||||||
id: a.id,
|
id: a.id,
|
||||||
lang: a.languages,
|
lang: a.languages,
|
||||||
rating: a.starRating
|
rating: a.starRating
|
||||||
})) };
|
})) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkToken(): Promise<CheckTokenResponse> {
|
public async checkToken(): Promise<CheckTokenResponse> {
|
||||||
return this.funi.checkToken();
|
return this.funi.checkToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public auth(data: AuthData) {
|
public auth(data: AuthData) {
|
||||||
return this.funi.auth(data);
|
return this.funi.auth(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadItem(data: DownloadData) {
|
public async downloadItem(data: DownloadData) {
|
||||||
this.setDownloading(true);
|
this.setDownloading(true);
|
||||||
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
|
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
|
||||||
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
|
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
|
||||||
const _default = buildDefault() as ArgvType;
|
const _default = buildDefault() as ArgvType;
|
||||||
if (!res.isOk)
|
if (!res.isOk)
|
||||||
return this.alertError(res.reason);
|
return this.alertError(res.reason);
|
||||||
|
|
||||||
for (const ep of res.value) {
|
for (const ep of res.value) {
|
||||||
await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: data.dlsubs, sub: false, ccTag: _default.ccTag } }, { ..._default, callbackMaker: this.makeProgressHandler.bind(this), ass: true, fileName: data.fileName, q: data.q, force: 'y',
|
await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: data.dlsubs, sub: false, ccTag: _default.ccTag } }, { ..._default, callbackMaker: this.makeProgressHandler.bind(this), ass: true, fileName: data.fileName, q: data.q, force: 'y',
|
||||||
noaudio: data.noaudio, novids: data.novids });
|
noaudio: data.noaudio, novids: data.novids });
|
||||||
}
|
}
|
||||||
this.sendMessage({ name: 'finish', data: undefined });
|
this.sendMessage({ name: 'finish', data: undefined });
|
||||||
this.setDownloading(false);
|
this.setDownloading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FunimationHandler;
|
export default FunimationHandler;
|
||||||
74
gui/server/websocket.ts
Normal file
74
gui/server/websocket.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { IncomingMessage, Server } from "http";
|
||||||
|
import ws, { WebSocket } from 'ws';
|
||||||
|
import { RandomEvent, RandomEvents } from "../../@types/randomEvents";
|
||||||
|
import { MessageTypes, UnknownWSMessage, WSMessage } from "../../@types/ws";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { cfg } from ".";
|
||||||
|
|
||||||
|
declare interface ExternalEvent {
|
||||||
|
on<T extends keyof MessageTypes>(event: T, listener: (msg: WSMessage<T>, respond: (data: MessageTypes[T][1]) => void) => void): this;
|
||||||
|
emit<T extends keyof MessageTypes>(event: T, msg: WSMessage<T>, respond: (data: MessageTypes[T][1]) => void): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExternalEvent extends EventEmitter {}
|
||||||
|
|
||||||
|
export default class WebSocketHandler {
|
||||||
|
|
||||||
|
private wsServer: ws.Server;
|
||||||
|
|
||||||
|
public events: ExternalEvent = new ExternalEvent();
|
||||||
|
|
||||||
|
constructor(server: Server) {
|
||||||
|
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/ws' });
|
||||||
|
|
||||||
|
this.wsServer.on('connection', (socket, req) => {
|
||||||
|
console.log(`[INFO] [WS] Connection from '${req.socket.remoteAddress}'`);
|
||||||
|
socket.on('error', (er) => console.log(`[ERROR] [WS] ${er}`));
|
||||||
|
socket.on('message', (data) => {
|
||||||
|
const json = JSON.parse(data.toString()) as UnknownWSMessage;
|
||||||
|
this.events.emit(json.name, json as any, (data) => {
|
||||||
|
this.wsServer.clients.forEach(client => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN)
|
||||||
|
return;
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
data,
|
||||||
|
id: json.id,
|
||||||
|
name: json.name
|
||||||
|
}), (er) => {
|
||||||
|
if (er)
|
||||||
|
console.log(`[ERROR] [WS] ${er}`)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('upgrade', (request, socket, head) => {
|
||||||
|
if (!this.authenticate(request)) {
|
||||||
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.wsServer.handleUpgrade(request, socket, head, socket => {
|
||||||
|
this.wsServer.emit('connection', socket, request);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
||||||
|
this.wsServer.clients.forEach(client => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN)
|
||||||
|
return;
|
||||||
|
client.send(JSON.stringify(data), (er) => {
|
||||||
|
if (er)
|
||||||
|
console.log(`[ERROR] [WS] ${er}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private authenticate(request: IncomingMessage): boolean {
|
||||||
|
return cfg.gui.password === new URL(`http://${request.headers.host}${request.url}`).searchParams.get('password');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ Object.entries(groups).forEach(([key, value]) => {
|
||||||
typeof argument.default === 'object'
|
typeof argument.default === 'object'
|
||||||
? Array.isArray(argument.default)
|
? Array.isArray(argument.default)
|
||||||
? JSON.stringify(argument.default)
|
? JSON.stringify(argument.default)
|
||||||
: argument.default.default
|
: (argument.default as any).default
|
||||||
: argument.default
|
: argument.default
|
||||||
}\`|` : ''}`
|
}\`|` : ''}`
|
||||||
+ ` ${typeof argument.default === 'object' && !Array.isArray(argument.default)
|
+ ` ${typeof argument.default === 'object' && !Array.isArray(argument.default)
|
||||||
|
|
|
||||||
|
|
@ -15,61 +15,11 @@ type BuildTypes = `${'ubuntu'|'windows'|'macos'|'arm'}64`
|
||||||
const buildType = process.argv[2] as BuildTypes;
|
const buildType = process.argv[2] as BuildTypes;
|
||||||
const isGUI = process.argv[3] === 'true';
|
const isGUI = process.argv[3] === 'true';
|
||||||
|
|
||||||
if (isGUI) {
|
buildBinary(buildType, isGUI);
|
||||||
buildGUI(buildType);
|
|
||||||
} else {
|
|
||||||
buildBinary(buildType);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function buildGUI(buildType: BuildTypes) {
|
|
||||||
execSync(`npx electron-builder build --publish=never ${getCommand(buildType)}`, { stdio: [0,1,2] });
|
|
||||||
execSync(`7z a -t7z "../${buildsDir}/multi-downloader-nx-${buildType}-gui.7z" ${getOutputFileName(buildType).map(a => `"${a}"`).join(' ')}`,{
|
|
||||||
stdio:[0,1,2],
|
|
||||||
cwd: path.join('dist')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommand(buildType: BuildTypes) {
|
|
||||||
switch (buildType) {
|
|
||||||
case 'arm64':
|
|
||||||
return '--linux --arm64';
|
|
||||||
case 'ubuntu64':
|
|
||||||
return '--linux --x64';
|
|
||||||
case 'windows64':
|
|
||||||
return '--win';
|
|
||||||
case 'macos64':
|
|
||||||
return '--mac dmg';
|
|
||||||
default:
|
|
||||||
return '--error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOutputFileName(buildType: BuildTypes): string[] {
|
|
||||||
switch (buildType) {
|
|
||||||
case 'arm64':
|
|
||||||
return [
|
|
||||||
`${pkg.name}_${pkg.version}_arm64.deb`
|
|
||||||
];
|
|
||||||
case 'ubuntu64':
|
|
||||||
return [
|
|
||||||
`${pkg.name}_${pkg.version}_amd64.deb`
|
|
||||||
];
|
|
||||||
case 'windows64':
|
|
||||||
return [
|
|
||||||
`${pkg.name} Setup ${pkg.version}.exe`
|
|
||||||
];
|
|
||||||
case 'macos64':
|
|
||||||
return [
|
|
||||||
`${pkg.name}-${pkg.version}.dmg`
|
|
||||||
];
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown build type ${buildType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// main
|
// main
|
||||||
async function buildBinary(buildType: BuildTypes) {
|
async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||||
const buildStr = 'multi-downloader-nx';
|
const buildStr = 'multi-downloader-nx';
|
||||||
const acceptableBuilds = ['windows64','ubuntu64','macos64'];
|
const acceptableBuilds = ['windows64','ubuntu64','macos64'];
|
||||||
if(!acceptableBuilds.includes(buildType)){
|
if(!acceptableBuilds.includes(buildType)){
|
||||||
|
|
@ -87,7 +37,7 @@ async function buildBinary(buildType: BuildTypes) {
|
||||||
}
|
}
|
||||||
fs.mkdirSync(buildDir);
|
fs.mkdirSync(buildDir);
|
||||||
const buildConfig = [
|
const buildConfig = [
|
||||||
pkg.main,
|
gui ? 'gui.js' : 'index.js',
|
||||||
'--target', nodeVer + getTarget(buildType),
|
'--target', nodeVer + getTarget(buildType),
|
||||||
'--output', `${buildDir}/${pkg.short_name}`,
|
'--output', `${buildDir}/${pkg.short_name}`,
|
||||||
];
|
];
|
||||||
|
|
@ -109,6 +59,10 @@ async function buildBinary(buildType: BuildTypes) {
|
||||||
fs.copySync('./package.json', `${buildDir}/package.json`);
|
fs.copySync('./package.json', `${buildDir}/package.json`);
|
||||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
fs.copySync('./docs/', `${buildDir}/docs/`);
|
||||||
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
||||||
|
if (gui) {
|
||||||
|
fs.copySync('./gui', `${buildDir}/gui`)
|
||||||
|
fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`)
|
||||||
|
}
|
||||||
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
||||||
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import yargs, { Choices } from 'yargs';
|
||||||
import { args, AvailableMuxer, groups } from './module.args';
|
import { args, AvailableMuxer, groups } from './module.args';
|
||||||
import { LanguageItem } from './module.langsData';
|
import { LanguageItem } from './module.langsData';
|
||||||
|
|
||||||
let argvC: { [x: string]: unknown; ccTag: string, defaultAudio: LanguageItem, defaultSub: LanguageItem, ffmpegOptions: string[], mkvmergeOptions: string[], force: 'Y'|'y'|'N'|'n'|'C'|'c', skipUpdate: boolean, videoTitle: string, override: string[], fsRetryTime: number, forceMuxer: AvailableMuxer|undefined; username: string|undefined, password: string|undefined, silentAuth: boolean, skipSubMux: boolean, downloadArchive: boolean, addArchive: boolean, but: boolean, auth: boolean | undefined; dlFonts: boolean | undefined; search: string | undefined; 'search-type': string; page: number | undefined; 'search-locale': string; new: boolean | undefined; 'movie-listing': string | undefined; series: string | undefined; s: string | undefined; e: string | undefined; q: number; x: number; kstream: number; partsize: number; hslang: string; dlsubs: string[]; novids: boolean | undefined; noaudio: boolean | undefined; nosubs: boolean | undefined; dubLang: string[]; all: boolean; fontSize: number; allDubs: boolean; timeout: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; fileName: string; numbers: number; nosess: string; debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; service: 'funi' | 'crunchy'; update: boolean; fontName: string | undefined; _: (string | number)[]; $0: string; dlVideoOnce: boolean; };
|
let argvC: { [x: string]: unknown; port: number, ccTag: string, defaultAudio: LanguageItem, defaultSub: LanguageItem, ffmpegOptions: string[], mkvmergeOptions: string[], force: 'Y'|'y'|'N'|'n'|'C'|'c', skipUpdate: boolean, videoTitle: string, override: string[], fsRetryTime: number, forceMuxer: AvailableMuxer|undefined; username: string|undefined, password: string|undefined, silentAuth: boolean, skipSubMux: boolean, downloadArchive: boolean, addArchive: boolean, but: boolean, auth: boolean | undefined; dlFonts: boolean | undefined; search: string | undefined; 'search-type': string; page: number | undefined; 'search-locale': string; new: boolean | undefined; 'movie-listing': string | undefined; series: string | undefined; s: string | undefined; e: string | undefined; q: number; x: number; kstream: number; partsize: number; hslang: string; dlsubs: string[]; novids: boolean | undefined; noaudio: boolean | undefined; nosubs: boolean | undefined; dubLang: string[]; all: boolean; fontSize: number; allDubs: boolean; timeout: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; fileName: string; numbers: number; nosess: string; debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; service: 'funi' | 'crunchy'; update: boolean; fontName: string | undefined; _: (string | number)[]; $0: string; dlVideoOnce: boolean; };
|
||||||
|
|
||||||
export type ArgvType = typeof argvC;
|
export type ArgvType = typeof argvC;
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ const getArgv = (cfg: { [key:string]: unknown }) => {
|
||||||
...a,
|
...a,
|
||||||
group: groups[a.group],
|
group: groups[a.group],
|
||||||
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
|
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
|
||||||
parseDefault(a.default.name || a.name, a.default.default) : a.default
|
parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
for (const item of data)
|
for (const item of data)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ const groups = {
|
||||||
'fileName': 'Filename Template:',
|
'fileName': 'Filename Template:',
|
||||||
'debug': 'Debug:',
|
'debug': 'Debug:',
|
||||||
'util': 'Utilities:',
|
'util': 'Utilities:',
|
||||||
'help': 'Help:'
|
'help': 'Help:',
|
||||||
|
'gui': 'GUI:'
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'season' | 'width' | 'height' | 'service'
|
export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'season' | 'width' | 'height' | 'service'
|
||||||
|
|
@ -703,6 +704,16 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
default: {
|
default: {
|
||||||
default: 'cc'
|
default: 'cc'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'port',
|
||||||
|
describe: 'Set the port for the GUI server',
|
||||||
|
docDescribe: true,
|
||||||
|
group: 'gui',
|
||||||
|
service: 'both',
|
||||||
|
default: 3000,
|
||||||
|
type: 'number',
|
||||||
|
usage: '${port}'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -715,10 +726,10 @@ const getDefault = <T extends boolean|string|number|unknown[]>(name: string, cfg
|
||||||
if (typeof option.default === 'object') {
|
if (typeof option.default === 'object') {
|
||||||
if (Array.isArray(option.default))
|
if (Array.isArray(option.default))
|
||||||
return option.default as T;
|
return option.default as T;
|
||||||
if (Object.prototype.hasOwnProperty.call(cfg, option.default.name ?? option.name)) {
|
if (Object.prototype.hasOwnProperty.call(cfg, (option.default as any).name ?? option.name)) {
|
||||||
return cfg[option.default.name ?? option.name];
|
return cfg[(option.default as any).name ?? option.name];
|
||||||
} else {
|
} else {
|
||||||
return option.default.default as T;
|
return (option.default as any).default as T;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return option.default as T;
|
return option.default as T;
|
||||||
|
|
@ -733,7 +744,7 @@ const buildDefault = () => {
|
||||||
if (Array.isArray(item.default)) {
|
if (Array.isArray(item.default)) {
|
||||||
data[item.name] = item.default;
|
data[item.name] = item.default;
|
||||||
} else {
|
} else {
|
||||||
data[item.default.name ?? item.name] = item.default.default;
|
data[(item.default as any).name ?? item.name] = (item.default as any).default;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data[item.name] = item.default;
|
data[item.name] = item.default;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export { workingDir };
|
||||||
|
|
||||||
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
|
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
|
||||||
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
|
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
|
||||||
|
const guiCfgFile = path.join(workingDir, 'config', 'gui')
|
||||||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||||
const sessCfgFile = path.join(workingDir, 'config', 'session');
|
const sessCfgFile = path.join(workingDir, 'config', 'session');
|
||||||
const tokenFile = {
|
const tokenFile = {
|
||||||
|
|
@ -23,7 +24,7 @@ export const ensureConfig = () => {
|
||||||
if (!fs.existsSync(path.join(workingDir, 'config')))
|
if (!fs.existsSync(path.join(workingDir, 'config')))
|
||||||
fs.mkdirSync(path.join(workingDir, 'config'));
|
fs.mkdirSync(path.join(workingDir, 'config'));
|
||||||
if (process.env.contentDirectory)
|
if (process.env.contentDirectory)
|
||||||
[binCfgFile, dirCfgFile, cliCfgFile].forEach(a => {
|
[binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => {
|
||||||
if (!fs.existsSync(`${a}.yml`))
|
if (!fs.existsSync(`${a}.yml`))
|
||||||
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
|
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
|
||||||
});
|
});
|
||||||
|
|
@ -59,6 +60,10 @@ export type ConfigObject = {
|
||||||
},
|
},
|
||||||
cli: {
|
cli: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
},
|
||||||
|
gui: {
|
||||||
|
port: number,
|
||||||
|
password: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +80,10 @@ const loadCfg = () : ConfigObject => {
|
||||||
cli: loadYamlCfgFile<{
|
cli: loadYamlCfgFile<{
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}>(cliCfgFile),
|
}>(cliCfgFile),
|
||||||
|
gui: loadYamlCfgFile<{
|
||||||
|
port: number,
|
||||||
|
password: string
|
||||||
|
}>(guiCfgFile)
|
||||||
};
|
};
|
||||||
const defaultDirs = {
|
const defaultDirs = {
|
||||||
fonts: '${wdir}/fonts/',
|
fonts: '${wdir}/fonts/',
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const fontFamilies = {
|
||||||
// collect styles from ass string
|
// collect styles from ass string
|
||||||
function assFonts(ass: string){
|
function assFonts(ass: string){
|
||||||
const strings = ass.replace(/\r/g,'').split('\n');
|
const strings = ass.replace(/\r/g,'').split('\n');
|
||||||
const styles = [];
|
const styles: string[] = [];
|
||||||
for(const s of strings){
|
for(const s of strings){
|
||||||
if(s.match(/^Style: /)){
|
if(s.match(/^Style: /)){
|
||||||
const addStyle = s.split(',');
|
const addStyle = s.split(',');
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const languages: LanguageItem[] = [
|
||||||
|
|
||||||
// construct dub language codes
|
// construct dub language codes
|
||||||
const dubLanguageCodes = (() => {
|
const dubLanguageCodes = (() => {
|
||||||
const dubLanguageCodesArray = [];
|
const dubLanguageCodesArray: string[] = [];
|
||||||
for(const language of languages){
|
for(const language of languages){
|
||||||
dubLanguageCodesArray.push(language.code);
|
dubLanguageCodesArray.push(language.code);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ class Merger {
|
||||||
}
|
}
|
||||||
|
|
||||||
public FFmpeg() : string {
|
public FFmpeg() : string {
|
||||||
const args = [];
|
const args: string[] = [];
|
||||||
const metaData = [];
|
const metaData: string[] = [];
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let audioIndex = 0;
|
let audioIndex = 0;
|
||||||
|
|
@ -137,7 +137,7 @@ class Merger {
|
||||||
};
|
};
|
||||||
|
|
||||||
public MkvMerge = () => {
|
public MkvMerge = () => {
|
||||||
const args = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
let hasVideo = false;
|
let hasVideo = false;
|
||||||
|
|
||||||
|
|
@ -272,7 +272,7 @@ class Merger {
|
||||||
language: LanguageItem,
|
language: LanguageItem,
|
||||||
fonts: Font[]
|
fonts: Font[]
|
||||||
}[]) : ParsedFont[] {
|
}[]) : ParsedFont[] {
|
||||||
let fontsNameList: Font[] = []; const fontsList = [], subsList = []; let isNstr = true;
|
let fontsNameList: Font[] = []; const fontsList: { name: string, path: string, mime: string }[] = [], subsList: string[] = []; let isNstr = true;
|
||||||
for(const s of subs){
|
for(const s of subs){
|
||||||
fontsNameList.push(...s.fonts);
|
fontsNameList.push(...s.fonts);
|
||||||
subsList.push(s.language.locale);
|
subsList.push(s.language.locale);
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ class Req {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setNewCookie(setCookie: Record<string, string>, isAuth: boolean, fileData?: string){
|
setNewCookie(setCookie: Record<string, string>, isAuth: boolean, fileData?: string){
|
||||||
const cookieUpdated = []; let lastExp = 0;
|
const cookieUpdated: string[] = []; let lastExp = 0;
|
||||||
console.trace('Type of setCookie:', typeof setCookie, setCookie);
|
console.trace('Type of setCookie:', typeof setCookie, setCookie);
|
||||||
const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
|
const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
|
||||||
for(const cookieName of Object.keys(parsedCookie)){
|
for(const cookieName of Object.keys(parsedCookie)){
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export type NullRecord = Record | null;
|
||||||
function loadVtt(vttStr: string) {
|
function loadVtt(vttStr: string) {
|
||||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||||
const data = []; let lineBuf = [], record: NullRecord = null;
|
const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null;
|
||||||
// check lines
|
// check lines
|
||||||
for (const l of lines) {
|
for (const l of lines) {
|
||||||
const m = l.match(rx);
|
const m = l.match(rx);
|
||||||
|
|
@ -142,7 +142,7 @@ function convertTime(time: string, srtFormat = false) {
|
||||||
|
|
||||||
function toSubsTime(str: string, srtFormat: boolean) : string {
|
function toSubsTime(str: string, srtFormat: boolean) : string {
|
||||||
|
|
||||||
const n = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
|
const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
|
||||||
|
|
||||||
const msLen = srtFormat ? 3 : 2;
|
const msLen = srtFormat ? 3 : 2;
|
||||||
const hLen = srtFormat ? 2 : 1;
|
const hLen = srtFormat ? 2 : 1;
|
||||||
|
|
|
||||||
48
package.json
48
package.json
|
|
@ -35,14 +35,14 @@
|
||||||
"url": "https://github.com/anidl/multi-downloader-nx/issues"
|
"url": "https://github.com/anidl/multi-downloader-nx/issues"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "gui/electron/src/index.js",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.20.12",
|
"@babel/core": "^7.20.12",
|
||||||
"@babel/plugin-syntax-flow": "^7.18.6",
|
"@babel/plugin-syntax-flow": "^7.18.6",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.20.13",
|
"@babel/plugin-transform-react-jsx": "^7.20.13",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
|
@ -52,39 +52,35 @@
|
||||||
"iso-639": "^0.2.2",
|
"iso-639": "^0.2.2",
|
||||||
"lookpath": "^1.2.2",
|
"lookpath": "^1.2.2",
|
||||||
"m3u8-parsed": "^1.3.0",
|
"m3u8-parsed": "^1.3.0",
|
||||||
|
"open": "^8.4.2",
|
||||||
"sei-helper": "^3.3.0",
|
"sei-helper": "^3.3.0",
|
||||||
"typescript-eslint": "^0.0.1-alpha.0",
|
"typescript-eslint": "^0.0.1-alpha.0",
|
||||||
"webpack": "^5.75.0",
|
"ws": "^8.12.1",
|
||||||
"yaml": "^2.2.1",
|
"yaml": "^2.2.1",
|
||||||
"yargs": "^17.7.0"
|
"yargs": "^17.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.13",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
|
"@types/ws": "^8.5.4",
|
||||||
"@types/yargs": "^17.0.22",
|
"@types/yargs": "^17.0.22",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
"@typescript-eslint/parser": "^5.52.0",
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
|
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
|
||||||
"css-loader": "^6.7.3",
|
|
||||||
"electron": "23.1.0",
|
|
||||||
"electron-builder": "^23.6.0",
|
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"fork-ts-checker-webpack-plugin": "^7.3.0",
|
|
||||||
"node-loader": "^2.0.0",
|
|
||||||
"pkg": "^5.8.0",
|
"pkg": "^5.8.0",
|
||||||
"removeNPMAbsolutePaths": "^3.0.1",
|
"removeNPMAbsolutePaths": "^3.0.1",
|
||||||
"style-loader": "^3.3.1",
|
|
||||||
"ts-loader": "^9.4.2",
|
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "5.1.0-dev.20230227"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prestart": "pnpm run tsc test",
|
"prestart": "pnpm run tsc test",
|
||||||
"start": "pnpm prestart && cd lib && npx electron .",
|
"start": "pnpm prestart && cd lib && node gui.js",
|
||||||
"docs": "ts-node modules/build-docs.ts",
|
"docs": "ts-node modules/build-docs.ts",
|
||||||
"tsc": "ts-node tsc.ts",
|
"tsc": "ts-node tsc.ts",
|
||||||
"prebuild-cli": "pnpm run tsc false false",
|
"prebuild-cli": "pnpm run tsc false false",
|
||||||
|
|
@ -101,33 +97,5 @@
|
||||||
"eslint-fix": "eslint *.js modules --fix",
|
"eslint-fix": "eslint *.js modules --fix",
|
||||||
"pretest": "pnpm run tsc",
|
"pretest": "pnpm run tsc",
|
||||||
"test": "pnpm run pretest && cd lib && node modules/build windows64 && node modules/build ubuntu64 && node modules/build macos64"
|
"test": "pnpm run pretest && cd lib && node modules/build windows64 && node modules/build ubuntu64 && node modules/build macos64"
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "github.com/anidl",
|
|
||||||
"mac": {
|
|
||||||
"category": "public.app-category.developer-tools",
|
|
||||||
"darkModeSupport": true
|
|
||||||
},
|
|
||||||
"dmg": {
|
|
||||||
"iconSize": 160,
|
|
||||||
"contents": [
|
|
||||||
{
|
|
||||||
"x": 180,
|
|
||||||
"y": 170
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 480,
|
|
||||||
"y": 170,
|
|
||||||
"type": "link",
|
|
||||||
"path": "/Applications"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
"deb"
|
|
||||||
],
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1638
pnpm-lock.yaml
1638
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
13
tsc.ts
13
tsc.ts
|
|
@ -18,7 +18,8 @@ if (!isTest)
|
||||||
if (!isGUI)
|
if (!isGUI)
|
||||||
buildIgnore = buildIgnore.concat([
|
buildIgnore = buildIgnore.concat([
|
||||||
'./gui*',
|
'./gui*',
|
||||||
'./build*'
|
'./build*',
|
||||||
|
'gui.ts'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ export { ignore };
|
||||||
|
|
||||||
process.stdout.write('✓\nCopying files... ');
|
process.stdout.write('✓\nCopying files... ');
|
||||||
if (!isTest && isGUI) {
|
if (!isTest && isGUI) {
|
||||||
copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'electron', 'build'));
|
copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'server', 'build'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = readDir(__dirname);
|
const files = readDir(__dirname);
|
||||||
|
|
@ -99,9 +100,6 @@ export { ignore };
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdout.write('✓\nInstalling dependencies... ');
|
process.stdout.write('✓\nInstalling dependencies... ');
|
||||||
if (!isTest && !isGUI) {
|
|
||||||
alterJSON();
|
|
||||||
}
|
|
||||||
if (!isTest) {
|
if (!isTest) {
|
||||||
const dependencies = exec(`pnpm install ${isGUI ? '' : '-P'}`, {
|
const dependencies = exec(`pnpm install ${isGUI ? '' : '-P'}`, {
|
||||||
cwd: path.join(__dirname, 'lib')
|
cwd: path.join(__dirname, 'lib')
|
||||||
|
|
@ -112,11 +110,6 @@ export { ignore };
|
||||||
process.stdout.write('✓\n');
|
process.stdout.write('✓\n');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function alterJSON() {
|
|
||||||
packageJSON.main = 'index.js';
|
|
||||||
fs.writeFileSync(path.join('lib', 'package.json'), JSON.stringify(packageJSON, null, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDir (dir: string): {
|
function readDir (dir: string): {
|
||||||
path: string,
|
path: string,
|
||||||
stats: fs.Stats
|
stats: fs.Stats
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue