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[]>>,
|
||||
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
|
||||
downloadItem: (data) => void,
|
||||
isDownloading: () => boolean,
|
||||
isDownloading: () => Promise<boolean>,
|
||||
writeToClipboard: (text: string) => void,
|
||||
openFolder: (path: FolderTypes) => void,
|
||||
openFile: (data: [FolderTypes, string]) => void,
|
||||
openURL: (data: string) => void;
|
||||
}
|
||||
|
||||
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
|
||||
item.title = item.title != '' ? item.title : 'NO_TITLE';
|
||||
// static data
|
||||
const oMetadata = [],
|
||||
oBooleans = [],
|
||||
const oMetadata: string[] = [],
|
||||
oBooleans: string[] = [],
|
||||
tMetadata = item.type + '_metadata',
|
||||
iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record<string, any>,
|
||||
iTitle = [ item.title ];
|
||||
|
||||
const audio_languages = [];
|
||||
const audio_languages: string[] = [];
|
||||
|
||||
// set object booleans
|
||||
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 showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||
// make obj ids
|
||||
const objects_ids = [];
|
||||
const objects_ids: string[] = [];
|
||||
objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id);
|
||||
if(item.seq_id){
|
||||
objects_ids.unshift(item.seq_id);
|
||||
|
|
@ -870,7 +870,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return objectInfo;
|
||||
}
|
||||
|
||||
const selectedMedia = [];
|
||||
const selectedMedia: Partial<CrunchyEpMeta>[] = [];
|
||||
|
||||
for(const item of objectInfo.data){
|
||||
if(item.type != 'episode' && item.type != 'movie'){
|
||||
|
|
@ -998,7 +998,7 @@ export default class Crunchy implements ServiceClass {
|
|||
} as Variable;
|
||||
}));
|
||||
|
||||
let streams = [];
|
||||
let streams: any[] = [];
|
||||
let hsLangs: string[] = [];
|
||||
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';
|
||||
|
||||
// types
|
||||
import { Item } from './@types/items';
|
||||
import type { Item } from './@types/items.js';
|
||||
|
||||
// params
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ export default class Funi implements ServiceClass {
|
|||
return showList;
|
||||
const eps = showList.value;
|
||||
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){
|
||||
|
|
@ -331,7 +331,7 @@ export default class Funi implements ServiceClass {
|
|||
debug: this.debug,
|
||||
});
|
||||
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
|
||||
season = parseInt(ep.parent.seasonNumber);
|
||||
if(ep.mediaCategory != 'Episode'){
|
||||
|
|
@ -508,7 +508,7 @@ export default class Funi implements ServiceClass {
|
|||
plStreams: Record<string|number, {
|
||||
[key: string]: string
|
||||
}> = {},
|
||||
plLayersStr = [],
|
||||
plLayersStr: string[] = [],
|
||||
plLayersRes: Record<string|number, {
|
||||
width: 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-dom": "^18.2.0",
|
||||
"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": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
|
|
@ -43,5 +46,8 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,14 @@ specifiers:
|
|||
'@types/node': ^18.14.0
|
||||
'@types/react': ^18.0.25
|
||||
'@types/react-dom': ^18.0.11
|
||||
'@types/uuid': ^9.0.1
|
||||
notistack: ^2.0.8
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
react-scripts: 5.0.1
|
||||
typescript: ^4.9.5
|
||||
uuid: ^9.0.0
|
||||
ws: ^8.12.1
|
||||
|
||||
dependencies:
|
||||
'@babel/core': 7.20.12
|
||||
|
|
@ -35,6 +38,11 @@ dependencies:
|
|||
react-dom: 18.2.0_react@18.2.0
|
||||
react-scripts: 5.0.1_pegpel5nwbugtuutvxsiaw5kjq
|
||||
typescript: 4.9.5
|
||||
uuid: 9.0.0
|
||||
ws: 8.12.1
|
||||
|
||||
devDependencies:
|
||||
'@types/uuid': 9.0.1
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -2753,6 +2761,10 @@ packages:
|
|||
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
|
||||
dev: false
|
||||
|
||||
/@types/uuid/9.0.1:
|
||||
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
|
||||
dev: true
|
||||
|
||||
/@types/ws/8.5.4:
|
||||
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
|
||||
dependencies:
|
||||
|
|
@ -9805,6 +9817,11 @@ packages:
|
|||
hasBin: true
|
||||
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:
|
||||
resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@ import { messageChannelContext } from './provider/MessageChannel';
|
|||
import { ClearAll, Folder } from "@mui/icons-material";
|
||||
import useStore from "./hooks/useStore";
|
||||
import StartQueueButton from "./components/StartQueue";
|
||||
import MenuBar from "./components/MenuBar/MenuBar";
|
||||
|
||||
const Layout: React.FC = () => {
|
||||
|
||||
const messageHandler = React.useContext(messageChannelContext);
|
||||
const [, dispatch] = useStore();
|
||||
|
||||
return <Box>
|
||||
<Box sx={{ height: 50, mb: 4, display: 'flex', gap: 1 }}>
|
||||
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<MenuBar />
|
||||
<Box sx={{ height: 50, mb: 4, display: 'flex', gap: 1, mt: 3 }}>
|
||||
<LogoutButton />
|
||||
<AuthButton />
|
||||
<Box sx={{ display: 'flex', gap: 1, height: 36 }}>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
|
|||
|
||||
const Style: FCWithChildren = ({children}) => {
|
||||
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 }}/>
|
||||
{children}
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ const LogoutButton: React.FC = () => {
|
|||
const messageChannel = React.useContext(messageChannelContext);
|
||||
const [, dispatch] = useStore();
|
||||
|
||||
const logout = () => {
|
||||
if (messageChannel?.isDownloading())
|
||||
const logout = async () => {
|
||||
if (await messageChannel?.isDownloading())
|
||||
return alert('You are currently downloading. Please finish the download first.');
|
||||
if (messageChannel?.logout())
|
||||
if (await messageChannel?.logout())
|
||||
dispatch({
|
||||
type: 'service',
|
||||
payload: undefined
|
||||
|
|
|
|||
|
|
@ -33,12 +33,14 @@ const useDownloadManager = () => {
|
|||
}, [messageHandler, dispatch]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDownload)
|
||||
return;
|
||||
if (messageHandler?.isDownloading())
|
||||
return;
|
||||
console.log('start download');
|
||||
messageHandler?.downloadItem(currentDownload);
|
||||
(async () => {
|
||||
if (!currentDownload)
|
||||
return;
|
||||
if (await messageHandler?.isDownloading())
|
||||
return;
|
||||
console.log('start download');
|
||||
messageHandler?.downloadItem(currentDownload);
|
||||
})();
|
||||
}, [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 [store, dispatch] = useStore();
|
||||
|
||||
const change = () => {
|
||||
if (messageChannel?.isDownloading() && store.downloadQueue)
|
||||
const change = async () => {
|
||||
if (await messageChannel?.isDownloading() && store.downloadQueue)
|
||||
alert("The current download will be finished before the queue stops")
|
||||
dispatch({
|
||||
type: 'downloadQueue',
|
||||
|
|
|
|||
|
|
@ -19,26 +19,24 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => {
|
|||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ErrorHandler>
|
||||
<Store>
|
||||
<SnackbarProvider
|
||||
ref={notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={onClickDismiss(key)} color="inherit">
|
||||
<CloseOutlined />
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<Style>
|
||||
<MessageChannel>
|
||||
<ServiceProvider>
|
||||
<App />
|
||||
</ServiceProvider>
|
||||
</MessageChannel>
|
||||
</Style>
|
||||
</SnackbarProvider>
|
||||
</Store>
|
||||
</ErrorHandler>
|
||||
</React.StrictMode>
|
||||
<ErrorHandler>
|
||||
<Store>
|
||||
<SnackbarProvider
|
||||
ref={notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={onClickDismiss(key)} color="inherit">
|
||||
<CloseOutlined />
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<Style>
|
||||
<MessageChannel>
|
||||
<ServiceProvider>
|
||||
<App />
|
||||
</ServiceProvider>
|
||||
</MessageChannel>
|
||||
</Style>
|
||||
</SnackbarProvider>
|
||||
</Store>
|
||||
</ErrorHandler>
|
||||
);
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import type { MessageHandler } from '../../../../@types/messageHandler';
|
||||
import type { IpcRenderer, IpcRendererEvent } from "electron";
|
||||
import type { AuthData, AuthResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
|
||||
import useStore from '../hooks/useStore';
|
||||
|
||||
import type { MessageTypes, WSMessage, WSMessageWithID } from '../../../../@types/ws';
|
||||
import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
|
||||
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 {
|
||||
private handler: {
|
||||
|
|
@ -36,53 +36,93 @@ export class RandomEventHandler {
|
|||
|
||||
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 [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(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const currentService = await ipcRenderer.invoke('type');
|
||||
if (currentService !== undefined)
|
||||
return dispatch({ type: 'service', payload: currentService });
|
||||
if (store.service !== currentService)
|
||||
ipcRenderer.invoke('setup', store.service)
|
||||
if (!socket)
|
||||
return;
|
||||
const currentService = await messageAndResponse(socket, { name: 'type', data: undefined });
|
||||
if (currentService.data !== undefined)
|
||||
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(() => {
|
||||
if (!socket)
|
||||
return;
|
||||
/* finish is a placeholder */
|
||||
const listener = (_: IpcRendererEvent, initalData: RandomEvent<'finish'>) => {
|
||||
const eventName = initalData.name as keyof RandomEvents;
|
||||
const data = initalData as unknown as RandomEvent<typeof eventName>;
|
||||
|
||||
const listener = (initalData: MessageEvent<string>) => {
|
||||
const data = JSON.parse(initalData.data) as RandomEvent<'finish'>;
|
||||
randomEventHandler.emit(data.name, data);
|
||||
}
|
||||
ipcRenderer.on('randomEvent', listener);
|
||||
socket.addEventListener('message', listener);
|
||||
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 = {
|
||||
auth: async (data) => await ipcRenderer.invoke('auth', data),
|
||||
checkToken: async () => await ipcRenderer.invoke('checkToken'),
|
||||
search: async (data) => await ipcRenderer.invoke('search', data),
|
||||
handleDefault: async (data) => await ipcRenderer.invoke('default', data),
|
||||
availableDubCodes: async () => await ipcRenderer.invoke('availableDubCodes'),
|
||||
availableSubCodes: async () => await ipcRenderer.invoke('availableSubCodes'),
|
||||
resolveItems: async (data) => await ipcRenderer.invoke('resolveItems', data),
|
||||
listEpisodes: async (data) => await ipcRenderer.invoke('listEpisodes', data),
|
||||
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
|
||||
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
|
||||
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
|
||||
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
|
||||
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data,
|
||||
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
|
||||
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
|
||||
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
|
||||
randomEvents: randomEventHandler,
|
||||
downloadItem: (data) => ipcRenderer.invoke('downloadItem', data),
|
||||
isDownloading: () => ipcRenderer.sendSync('isDownloading'),
|
||||
writeToClipboard: async (data) => await ipcRenderer.invoke('writeToClipboard', data),
|
||||
openFolder: async (data) => await ipcRenderer.invoke('openFolder', data),
|
||||
logout: () => ipcRenderer.sendSync('changeProvider')
|
||||
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
|
||||
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
|
||||
writeToClipboard: async (data) => messageAndResponse(socket, { name: 'writeToClipboard', data }),
|
||||
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
|
||||
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}>
|
||||
|
|
|
|||
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 Crunchy from '../../../../crunchy';
|
||||
import { ArgvType } from '../../../../modules/module.app-args';
|
||||
import { buildDefault, getDefault } from '../../../../modules/module.args';
|
||||
import { languages, subtitleLanguagesFilter } from '../../../../modules/module.langsData';
|
||||
import Base from './base';
|
||||
|
||||
class CrunchyHandler extends Base implements MessageHandler {
|
||||
private crunchy: Crunchy;
|
||||
constructor(window: BrowserWindow) {
|
||||
super(window);
|
||||
this.crunchy = new Crunchy();
|
||||
this.crunchy.refreshToken();
|
||||
}
|
||||
|
||||
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.crunchy.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray = [];
|
||||
for(const language of languages){
|
||||
if (language.cr_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
||||
if (!res.isOk)
|
||||
return res;
|
||||
return { isOk: true, value: res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: a.data.map(a => a.mediaId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.season.toString()
|
||||
},
|
||||
e: a.e,
|
||||
image: a.image,
|
||||
episode: a.episodeNumber
|
||||
};
|
||||
}) };
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
||||
const crunchySearch = await this.crunchy.doSearch(data);
|
||||
if (!crunchySearch.isOk) {
|
||||
this.crunchy.refreshToken();
|
||||
return crunchySearch;
|
||||
}
|
||||
return { isOk: true, value: crunchySearch.value };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
if (await this.crunchy.getProfile()) {
|
||||
return { isOk: true, value: undefined };
|
||||
} else {
|
||||
return { isOk: false, reason: new Error('') };
|
||||
}
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.crunchy.doAuth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
|
||||
this.setDownloading(true);
|
||||
const _default = buildDefault() as ArgvType;
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
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',
|
||||
novids: data.novids }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(res.reason);
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||
import Crunchy from '../../../crunchy';
|
||||
import { ArgvType } from '../../../modules/module.app-args';
|
||||
import { buildDefault, getDefault } from '../../../modules/module.args';
|
||||
import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData';
|
||||
import WebSocketHandler from '../websocket';
|
||||
import Base from './base';
|
||||
|
||||
class CrunchyHandler extends Base implements MessageHandler {
|
||||
private crunchy: Crunchy;
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.crunchy = new Crunchy();
|
||||
this.crunchy.refreshToken();
|
||||
}
|
||||
|
||||
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.crunchy.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.cr_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
||||
if (!res.isOk)
|
||||
return res;
|
||||
return { isOk: true, value: res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: a.data.map(a => a.mediaId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.season.toString()
|
||||
},
|
||||
e: a.e,
|
||||
image: a.image,
|
||||
episode: a.episodeNumber
|
||||
};
|
||||
}) };
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
||||
const crunchySearch = await this.crunchy.doSearch(data);
|
||||
if (!crunchySearch.isOk) {
|
||||
this.crunchy.refreshToken();
|
||||
return crunchySearch;
|
||||
}
|
||||
return { isOk: true, value: crunchySearch.value };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
if (await this.crunchy.getProfile()) {
|
||||
return { isOk: true, value: undefined };
|
||||
} else {
|
||||
return { isOk: false, reason: new Error('') };
|
||||
}
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.crunchy.doAuth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
|
||||
this.setDownloading(true);
|
||||
const _default = buildDefault() as ArgvType;
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
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',
|
||||
novids: data.novids }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(res.reason);
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
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 Funimation from '../../../../funi';
|
||||
import { ArgvType } from '../../../../modules/module.app-args';
|
||||
import { buildDefault, getDefault } from '../../../../modules/module.args';
|
||||
import { languages, subtitleLanguagesFilter } from '../../../../modules/module.langsData';
|
||||
import Base from './base';
|
||||
|
||||
class FunimationHandler extends Base implements MessageHandler {
|
||||
private funi: Funimation;
|
||||
constructor(window: BrowserWindow) {
|
||||
super(window);
|
||||
this.funi = new Funimation();
|
||||
}
|
||||
|
||||
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
const request = await this.funi.listShowItems(parse);
|
||||
if (!request.isOk)
|
||||
return request;
|
||||
return { isOk: true, value: request.value.map(item => ({
|
||||
e: item.id_split.join(''),
|
||||
lang: item.audio ?? [],
|
||||
name: item.title,
|
||||
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
|
||||
seasonTitle: item.seasonTitle,
|
||||
episode: item.episodeNum,
|
||||
id: item.id,
|
||||
img: item.thumb,
|
||||
description: item.synopsis,
|
||||
time: item.runtime ?? item.item.runtime
|
||||
})) };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.funi.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray = [];
|
||||
for(const language of languages){
|
||||
if (language.funi_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
||||
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
|
||||
if (!res.isOk)
|
||||
return res;
|
||||
return { isOk: true, value: res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.episodeID],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.seasonNumber
|
||||
},
|
||||
image: a.image,
|
||||
e: a.episodeID,
|
||||
episode: a.epsiodeNumber,
|
||||
};
|
||||
}) };
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
||||
const funiSearch = await this.funi.searchShow(false, data);
|
||||
if (!funiSearch.isOk)
|
||||
return funiSearch;
|
||||
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
|
||||
image: a.image.showThumbnail,
|
||||
name: a.title,
|
||||
desc: a.description,
|
||||
id: a.id,
|
||||
lang: a.languages,
|
||||
rating: a.starRating
|
||||
})) };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
return this.funi.checkToken();
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.funi.auth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
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 _default = buildDefault() as ArgvType;
|
||||
if (!res.isOk)
|
||||
return this.alertError(res.reason);
|
||||
|
||||
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',
|
||||
noaudio: data.noaudio, novids: data.novids });
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||
import Funimation from '../../../funi';
|
||||
import { ArgvType } from '../../../modules/module.app-args';
|
||||
import { buildDefault, getDefault } from '../../../modules/module.args';
|
||||
import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData';
|
||||
import WebSocketHandler from '../websocket';
|
||||
import Base from './base';
|
||||
|
||||
class FunimationHandler extends Base implements MessageHandler {
|
||||
private funi: Funimation;
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.funi = new Funimation();
|
||||
}
|
||||
|
||||
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
const request = await this.funi.listShowItems(parse);
|
||||
if (!request.isOk)
|
||||
return request;
|
||||
return { isOk: true, value: request.value.map(item => ({
|
||||
e: item.id_split.join(''),
|
||||
lang: item.audio ?? [],
|
||||
name: item.title,
|
||||
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
|
||||
seasonTitle: item.seasonTitle,
|
||||
episode: item.episodeNum,
|
||||
id: item.id,
|
||||
img: item.thumb,
|
||||
description: item.synopsis,
|
||||
time: item.runtime ?? item.item.runtime
|
||||
})) };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.funi.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.funi_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
|
||||
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
|
||||
if (!res.isOk)
|
||||
return res;
|
||||
return { isOk: true, value: res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.episodeID],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.seasonNumber
|
||||
},
|
||||
image: a.image,
|
||||
e: a.episodeID,
|
||||
episode: a.epsiodeNumber,
|
||||
};
|
||||
}) };
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.log(`[DEBUG] Got search options: ${JSON.stringify(data)}`);
|
||||
const funiSearch = await this.funi.searchShow(false, data);
|
||||
if (!funiSearch.isOk)
|
||||
return funiSearch;
|
||||
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
|
||||
image: a.image.showThumbnail,
|
||||
name: a.title,
|
||||
desc: a.description,
|
||||
id: a.id,
|
||||
lang: a.languages,
|
||||
rating: a.starRating
|
||||
})) };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
return this.funi.checkToken();
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.funi.auth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
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 _default = buildDefault() as ArgvType;
|
||||
if (!res.isOk)
|
||||
return this.alertError(res.reason);
|
||||
|
||||
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',
|
||||
noaudio: data.noaudio, novids: data.novids });
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
? Array.isArray(argument.default)
|
||||
? JSON.stringify(argument.default)
|
||||
: argument.default.default
|
||||
: (argument.default as any).default
|
||||
: 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 isGUI = process.argv[3] === 'true';
|
||||
|
||||
if (isGUI) {
|
||||
buildGUI(buildType);
|
||||
} else {
|
||||
buildBinary(buildType);
|
||||
}
|
||||
buildBinary(buildType, isGUI);
|
||||
})();
|
||||
|
||||
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
|
||||
async function buildBinary(buildType: BuildTypes) {
|
||||
async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||
const buildStr = 'multi-downloader-nx';
|
||||
const acceptableBuilds = ['windows64','ubuntu64','macos64'];
|
||||
if(!acceptableBuilds.includes(buildType)){
|
||||
|
|
@ -87,7 +37,7 @@ async function buildBinary(buildType: BuildTypes) {
|
|||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
const buildConfig = [
|
||||
pkg.main,
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
'--target', nodeVer + getTarget(buildType),
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
];
|
||||
|
|
@ -109,6 +59,10 @@ async function buildBinary(buildType: BuildTypes) {
|
|||
fs.copySync('./package.json', `${buildDir}/package.json`);
|
||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
||||
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`)){
|
||||
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import yargs, { Choices } from 'yargs';
|
|||
import { args, AvailableMuxer, groups } from './module.args';
|
||||
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;
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ const getArgv = (cfg: { [key:string]: unknown }) => {
|
|||
...a,
|
||||
group: groups[a.group],
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ const groups = {
|
|||
'fileName': 'Filename Template:',
|
||||
'debug': 'Debug:',
|
||||
'util': 'Utilities:',
|
||||
'help': 'Help:'
|
||||
'help': 'Help:',
|
||||
'gui': 'GUI:'
|
||||
};
|
||||
|
||||
export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'season' | 'width' | 'height' | 'service'
|
||||
|
|
@ -703,6 +704,16 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
default: {
|
||||
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 (Array.isArray(option.default))
|
||||
return option.default as T;
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, option.default.name ?? option.name)) {
|
||||
return cfg[option.default.name ?? option.name];
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, (option.default as any).name ?? option.name)) {
|
||||
return cfg[(option.default as any).name ?? option.name];
|
||||
} else {
|
||||
return option.default.default as T;
|
||||
return (option.default as any).default as T;
|
||||
}
|
||||
} else {
|
||||
return option.default as T;
|
||||
|
|
@ -733,7 +744,7 @@ const buildDefault = () => {
|
|||
if (Array.isArray(item.default)) {
|
||||
data[item.name] = item.default;
|
||||
} else {
|
||||
data[item.default.name ?? item.name] = item.default.default;
|
||||
data[(item.default as any).name ?? item.name] = (item.default as any).default;
|
||||
}
|
||||
} else {
|
||||
data[item.name] = item.default;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export { workingDir };
|
|||
|
||||
const binCfgFile = path.join(workingDir, 'config', 'bin-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 sessCfgFile = path.join(workingDir, 'config', 'session');
|
||||
const tokenFile = {
|
||||
|
|
@ -23,7 +24,7 @@ export const ensureConfig = () => {
|
|||
if (!fs.existsSync(path.join(workingDir, 'config')))
|
||||
fs.mkdirSync(path.join(workingDir, 'config'));
|
||||
if (process.env.contentDirectory)
|
||||
[binCfgFile, dirCfgFile, cliCfgFile].forEach(a => {
|
||||
[binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => {
|
||||
if (!fs.existsSync(`${a}.yml`))
|
||||
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
|
||||
});
|
||||
|
|
@ -59,6 +60,10 @@ export type ConfigObject = {
|
|||
},
|
||||
cli: {
|
||||
[key: string]: any
|
||||
},
|
||||
gui: {
|
||||
port: number,
|
||||
password: string
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +80,10 @@ const loadCfg = () : ConfigObject => {
|
|||
cli: loadYamlCfgFile<{
|
||||
[key: string]: any
|
||||
}>(cliCfgFile),
|
||||
gui: loadYamlCfgFile<{
|
||||
port: number,
|
||||
password: string
|
||||
}>(guiCfgFile)
|
||||
};
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const fontFamilies = {
|
|||
// collect styles from ass string
|
||||
function assFonts(ass: string){
|
||||
const strings = ass.replace(/\r/g,'').split('\n');
|
||||
const styles = [];
|
||||
const styles: string[] = [];
|
||||
for(const s of strings){
|
||||
if(s.match(/^Style: /)){
|
||||
const addStyle = s.split(',');
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const languages: LanguageItem[] = [
|
|||
|
||||
// construct dub language codes
|
||||
const dubLanguageCodes = (() => {
|
||||
const dubLanguageCodesArray = [];
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ class Merger {
|
|||
}
|
||||
|
||||
public FFmpeg() : string {
|
||||
const args = [];
|
||||
const metaData = [];
|
||||
const args: string[] = [];
|
||||
const metaData: string[] = [];
|
||||
|
||||
let index = 0;
|
||||
let audioIndex = 0;
|
||||
|
|
@ -137,7 +137,7 @@ class Merger {
|
|||
};
|
||||
|
||||
public MkvMerge = () => {
|
||||
const args = [];
|
||||
const args: string[] = [];
|
||||
|
||||
let hasVideo = false;
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ class Merger {
|
|||
language: LanguageItem,
|
||||
fonts: Font[]
|
||||
}[]) : 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){
|
||||
fontsNameList.push(...s.fonts);
|
||||
subsList.push(s.language.locale);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class Req {
|
|||
}
|
||||
}
|
||||
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);
|
||||
const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
|
||||
for(const cookieName of Object.keys(parsedCookie)){
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export type NullRecord = Record | null;
|
|||
function loadVtt(vttStr: string) {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
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
|
||||
for (const l of lines) {
|
||||
const m = l.match(rx);
|
||||
|
|
@ -142,7 +142,7 @@ function convertTime(time: string, srtFormat = false) {
|
|||
|
||||
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 hLen = srtFormat ? 2 : 1;
|
||||
|
|
|
|||
48
package.json
48
package.json
|
|
@ -35,14 +35,14 @@
|
|||
"url": "https://github.com/anidl/multi-downloader-nx/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "gui/electron/src/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-syntax-flow": "^7.18.6",
|
||||
"@babel/plugin-transform-react-jsx": "^7.20.13",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
|
|
@ -52,39 +52,35 @@
|
|||
"iso-639": "^0.2.2",
|
||||
"lookpath": "^1.2.2",
|
||||
"m3u8-parsed": "^1.3.0",
|
||||
"open": "^8.4.2",
|
||||
"sei-helper": "^3.3.0",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"webpack": "^5.75.0",
|
||||
"ws": "^8.12.1",
|
||||
"yaml": "^2.2.1",
|
||||
"yargs": "^17.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@types/yargs": "^17.0.22",
|
||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@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-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"fork-ts-checker-webpack-plugin": "^7.3.0",
|
||||
"node-loader": "^2.0.0",
|
||||
"pkg": "^5.8.0",
|
||||
"removeNPMAbsolutePaths": "^3.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "5.1.0-dev.20230227"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"tsc": "ts-node tsc.ts",
|
||||
"prebuild-cli": "pnpm run tsc false false",
|
||||
|
|
@ -101,33 +97,5 @@
|
|||
"eslint-fix": "eslint *.js modules --fix",
|
||||
"pretest": "pnpm run tsc",
|
||||
"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)
|
||||
buildIgnore = buildIgnore.concat([
|
||||
'./gui*',
|
||||
'./build*'
|
||||
'./build*',
|
||||
'gui.ts'
|
||||
]);
|
||||
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ export { ignore };
|
|||
|
||||
process.stdout.write('✓\nCopying files... ');
|
||||
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);
|
||||
|
|
@ -99,9 +100,6 @@ export { ignore };
|
|||
});
|
||||
|
||||
process.stdout.write('✓\nInstalling dependencies... ');
|
||||
if (!isTest && !isGUI) {
|
||||
alterJSON();
|
||||
}
|
||||
if (!isTest) {
|
||||
const dependencies = exec(`pnpm install ${isGUI ? '' : '-P'}`, {
|
||||
cwd: path.join(__dirname, 'lib')
|
||||
|
|
@ -112,11 +110,6 @@ export { ignore };
|
|||
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): {
|
||||
path: string,
|
||||
stats: fs.Stats
|
||||
|
|
|
|||
Loading…
Reference in a new issue