commit
758f1c4e4e
84 changed files with 3023 additions and 3704 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
**/node_modules
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
lib
|
||||
/videos/*.ts
|
||||
build
|
||||
dev.js
|
||||
dev.js
|
||||
tsc.ts
|
||||
|
|
@ -21,7 +21,18 @@
|
|||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["gui/react/**/*"],
|
||||
"rules": {
|
||||
"no-console": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 2,
|
||||
"react/prop-types": 0,
|
||||
"react-hooks/exhaustive-deps": 0,
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"indent": [
|
||||
"error",
|
||||
|
|
|
|||
32
.github/workflows/release-matrix.yml
vendored
32
.github/workflows/release-matrix.yml
vendored
|
|
@ -14,20 +14,17 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 6.0.2
|
||||
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
pnpm install
|
||||
|
||||
- name: Get name and version from package.json
|
||||
run: |
|
||||
test -n $(node -p -e "require('./package.json').name") &&
|
||||
|
|
@ -36,7 +33,6 @@ jobs:
|
|||
echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1
|
||||
- name: Make build
|
||||
run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }}
|
||||
|
||||
- name: Upload release
|
||||
uses: actions/upload-release-asset@v1
|
||||
with:
|
||||
|
|
@ -46,3 +42,25 @@ jobs:
|
|||
asset_content_type: application/x-7z-compressed
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
push: ${{ github.ref == 'refs/heads/master' }}
|
||||
tags: |
|
||||
"izuco/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
|||
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
|
|
@ -11,28 +11,29 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: pnpm dlx eslint .
|
||||
- run: pnpx eslint .
|
||||
test:
|
||||
needs: eslint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: pnpm run test
|
||||
|
||||
|
|
|
|||
32
@types/hls-download.d.ts
vendored
32
@types/hls-download.d.ts
vendored
|
|
@ -1,32 +0,0 @@
|
|||
declare module 'hls-download' {
|
||||
import type { ProgressData } from './messageHandler';
|
||||
export type HLSCallback = (data: ProgressData) => unknown;
|
||||
export type HLSOptions = {
|
||||
m3u8json: {
|
||||
segments: Record<string, unknown>[],
|
||||
mediaSequence?: number,
|
||||
},
|
||||
output?: string,
|
||||
threads?: number,
|
||||
retries?: number,
|
||||
offset?: number,
|
||||
baseurl?: string,
|
||||
proxy?: string,
|
||||
skipInit?: boolean,
|
||||
timeout?: number,
|
||||
fsRetryTime?: number,
|
||||
override?: 'Y'|'y'|'N'|'n'|'C'|'c'
|
||||
callback?: HLSCallback
|
||||
}
|
||||
export default class hlsDownload {
|
||||
constructor(options: HLSOptions)
|
||||
async download() : Promise<{
|
||||
ok: boolean,
|
||||
parts: {
|
||||
first: number,
|
||||
total: number,
|
||||
compleated: number
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
18
@types/messageHandler.d.ts
vendored
18
@types/messageHandler.d.ts
vendored
|
|
@ -10,12 +10,19 @@ export interface MessageHandler {
|
|||
availableDubCodes: () => Promise<string[]>,
|
||||
availableSubCodes: () => Promise<string[]>,
|
||||
handleDefault: (name: string) => Promise<any>,
|
||||
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
|
||||
resolveItems: (data: ResolveItemsData) => Promise<boolean>,
|
||||
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
|
||||
downloadItem: (data) => void,
|
||||
isDownloading: () => boolean,
|
||||
downloadItem: (data: QueueItem) => void,
|
||||
isDownloading: () => Promise<boolean>,
|
||||
writeToClipboard: (text: string) => void,
|
||||
openFolder: (path: FolderTypes) => void,
|
||||
openFile: (data: [FolderTypes, string]) => void,
|
||||
openURL: (data: string) => void;
|
||||
getQueue: () => Promise<QueueItem[]>,
|
||||
removeFromQueue: (index: number) => void,
|
||||
clearQueue: () => void,
|
||||
setDownloadQueue: (data: boolean) => void,
|
||||
getDownloadQueue: () => Promise<boolean>
|
||||
}
|
||||
|
||||
export type FolderTypes = 'content' | 'config';
|
||||
|
|
@ -23,7 +30,6 @@ export type FolderTypes = 'content' | 'config';
|
|||
export type QueueItem = {
|
||||
title: string,
|
||||
episode: string,
|
||||
ids: string[],
|
||||
fileName: string,
|
||||
dlsubs: string[],
|
||||
parent: {
|
||||
|
|
@ -34,13 +40,15 @@ export type QueueItem = {
|
|||
dlVideoOnce: boolean,
|
||||
dubLang: string[],
|
||||
image: string
|
||||
}
|
||||
} & ResolveItemsData
|
||||
|
||||
export type ResolveItemsData = {
|
||||
id: string,
|
||||
dubLang: string[],
|
||||
all: boolean,
|
||||
but: boolean,
|
||||
novids: boolean,
|
||||
noaudio: boolean
|
||||
dlVideoOnce: boolean,
|
||||
e: string,
|
||||
fileName: string,
|
||||
|
|
|
|||
6
@types/randomEvents.d.ts
vendored
6
@types/randomEvents.d.ts
vendored
|
|
@ -1,8 +1,10 @@
|
|||
import { ExtendedProgress } from './messageHandler';
|
||||
import { ExtendedProgress, QueueItem } from './messageHandler';
|
||||
|
||||
export type RandomEvents = {
|
||||
progress: ExtendedProgress,
|
||||
finish: undefined
|
||||
finish: undefined,
|
||||
queueChange: QueueItem[],
|
||||
current: QueueItem|undefined
|
||||
}
|
||||
|
||||
export interface RandomEvent<T extends keyof RandomEvents> {
|
||||
|
|
|
|||
45
@types/ws.d.ts
vendored
Normal file
45
@types/ws.d.ts
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { GUIConfig } from '../modules/module.cfg-loader';
|
||||
import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, 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, boolean],
|
||||
'listEpisodes': [string, EpisodeListResponse],
|
||||
'downloadItem': [QueueItem, 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],
|
||||
'setuped': [undefined, boolean],
|
||||
'setupServer': [GUIConfig, boolean],
|
||||
'requirePassword': [undefined, boolean],
|
||||
'getQueue': [undefined, QueueItem[]],
|
||||
'removeFromQueue': [number, undefined],
|
||||
'clearQueue': [undefined, undefined],
|
||||
'setDownloadQueue': [boolean, undefined],
|
||||
'getDownloadQueue': [undefined, boolean]
|
||||
}
|
||||
11
Dockerfile
11
Dockerfile
|
|
@ -15,13 +15,13 @@ RUN echo 'ffmpeg: "./bin/ffmpeg/ffmpeg"\nmkvmerge: "./bin/mkvtoolnix/mkvmerge"'
|
|||
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm i
|
||||
RUN pnpm run build-ubuntu-cli
|
||||
RUN pnpm run build-ubuntu-gui
|
||||
|
||||
# Move build to new Clean Image
|
||||
|
||||
FROM node
|
||||
WORKDIR "/app"
|
||||
COPY --from=builder /app/lib/_builds/multi-downloader-nx-ubuntu64-cli ./
|
||||
COPY --from=builder /app/lib/_builds/multi-downloader-nx-ubuntu64-gui ./
|
||||
|
||||
# Install mkvmerge and ffmpeg
|
||||
|
||||
|
|
@ -29,10 +29,11 @@ RUN mkdir -p /app/bin/mkvtoolnix
|
|||
RUN mkdir -p /app/bin/ffmpeg
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install xdg-utils -y
|
||||
RUN apt-get install mkvtoolnix -y
|
||||
RUN apt-get install ffmpeg -y
|
||||
#RUN apt-get install ffmpeg -y
|
||||
|
||||
RUN mv /usr/bin/mkvmerge /app/bin/mkvtoolnix/mkvmerge
|
||||
RUN mv /usr/bin/ffmpeg /app/bin/ffmpeg/ffmpeg
|
||||
#RUN mv /usr/bin/ffmpeg /app/bin/ffmpeg/ffmpeg
|
||||
|
||||
CMD [ "/bin/bash" ]
|
||||
CMD [ "/app/aniDL" ]
|
||||
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
|
||||
1
config/gui.yml
Normal file
1
config/gui.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
port: 3000
|
||||
3
config/setup.json
Normal file
3
config/setup.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"setuped": true
|
||||
}
|
||||
261
crunchy.ts
261
crunchy.ts
|
|
@ -5,9 +5,10 @@ import fs from 'fs-extra';
|
|||
// package program
|
||||
import packageJson from './package.json';
|
||||
// plugins
|
||||
import { console } from './modules/log';
|
||||
import shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
import streamdl from 'hls-download';
|
||||
import streamdl from './modules/hls-download';
|
||||
|
||||
// custom modules
|
||||
import * as fontsData from './modules/module.fontsData';
|
||||
|
|
@ -16,14 +17,6 @@ import * as yamlCfg from './modules/module.cfg-loader';
|
|||
import * as yargs from './modules/module.app-args';
|
||||
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||
|
||||
export type sxItem = {
|
||||
language: langsData.LanguageItem,
|
||||
path: string,
|
||||
file: string
|
||||
title: string,
|
||||
fonts: Font[]
|
||||
}
|
||||
|
||||
// args
|
||||
|
||||
// load req
|
||||
|
|
@ -41,6 +34,14 @@ import { AvailableFilenameVars, getDefault } from './modules/module.args';
|
|||
import { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
|
||||
export type sxItem = {
|
||||
language: langsData.LanguageItem,
|
||||
path: string,
|
||||
file: string
|
||||
title: string,
|
||||
fonts: Font[]
|
||||
}
|
||||
|
||||
export default class Crunchy implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
private token: Record<string, any>;
|
||||
|
|
@ -60,7 +61,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
public async cli() {
|
||||
console.log(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
const argv = yargs.appArgv(this.cfg.cli);
|
||||
|
||||
// load binaries
|
||||
|
|
@ -103,7 +104,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if (selected.isOk) {
|
||||
for (const select of selected.value) {
|
||||
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }))) {
|
||||
console.log(`[ERROR] Unable to download selected episode ${select.episodeNumber}`);
|
||||
console.error(`Unable to download selected episode ${select.episodeNumber}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -117,14 +118,14 @@ export default class Crunchy implements ServiceClass {
|
|||
else if(argv.s && argv.s.match(/^[0-9A-Z]{9}$/)){
|
||||
await this.refreshToken();
|
||||
if (argv.dubLang.length > 1) {
|
||||
console.log('[INFO] One show can only be downloaded with one dub. Use --srz instead.');
|
||||
console.info('One show can only be downloaded with one dub. Use --srz instead.');
|
||||
}
|
||||
argv.dubLang = [argv.dubLang[0]];
|
||||
const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all);
|
||||
if (selected.isOk) {
|
||||
for (const select of selected.value) {
|
||||
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }))) {
|
||||
console.log(`[ERROR] Unable to download selected episode ${select.episodeNumber}`);
|
||||
console.error(`Unable to download selected episode ${select.episodeNumber}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -136,24 +137,24 @@ export default class Crunchy implements ServiceClass {
|
|||
const selected = await this.getObjectById(argv.e, false);
|
||||
for (const select of selected as Partial<CrunchyEpMeta>[]) {
|
||||
if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false }))) {
|
||||
console.log(`[ERROR] Unable to download selected episode ${select.episodeNumber}`);
|
||||
console.error(`Unable to download selected episode ${select.episodeNumber}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
console.log('[INFO] No option selected or invalid value entered. Try --help.');
|
||||
console.info('No option selected or invalid value entered. Try --help.');
|
||||
}
|
||||
}
|
||||
|
||||
public async getFonts() {
|
||||
console.log('[INFO] Downloading fonts...');
|
||||
console.info('Downloading fonts...');
|
||||
const fonts = Object.values(fontsData.fontFamilies).reduce((pre, curr) => pre.concat(curr));
|
||||
for(const f of fonts) {
|
||||
const fontLoc = path.join(this.cfg.dir.fonts, f);
|
||||
if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){
|
||||
console.log(`[INFO] ${f} already downloaded!`);
|
||||
console.info(`${f} already downloaded!`);
|
||||
}
|
||||
else{
|
||||
const fontFolder = path.dirname(fontLoc);
|
||||
|
|
@ -164,20 +165,20 @@ export default class Crunchy implements ServiceClass {
|
|||
fs.ensureDirSync(fontFolder);
|
||||
}
|
||||
catch(e){
|
||||
console.log();
|
||||
console.info('');
|
||||
}
|
||||
const fontUrl = fontsData.root + f;
|
||||
const getFont = await this.req.getData<Buffer>(fontUrl, { binary: true });
|
||||
if(getFont.ok && getFont.res){
|
||||
fs.writeFileSync(fontLoc, getFont.res.body);
|
||||
console.log(`[INFO] Downloaded: ${f}`);
|
||||
console.info(`Downloaded: ${f}`);
|
||||
}
|
||||
else{
|
||||
console.log(`[WARN] Failed to download: ${f}`);
|
||||
console.warn(`Failed to download: ${f}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('[INFO] All required fonts downloaded!');
|
||||
console.info('All required fonts downloaded!');
|
||||
}
|
||||
|
||||
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||
|
|
@ -194,14 +195,14 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.log('[ERROR] Authentication failed!');
|
||||
console.error('Authentication failed!');
|
||||
return { isOk: false, reason: new Error('Authentication failed') };
|
||||
}
|
||||
this.token = JSON.parse(authReq.res.body);
|
||||
this.token.expires = new Date(Date.now() + this.token.expires_in);
|
||||
yamlCfg.saveCRToken(this.token);
|
||||
await this.getProfile();
|
||||
console.log('[INFO] Your Country: %s', this.token.country);
|
||||
console.info('Your Country: %s', this.token.country);
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.log('[ERROR] Authentication failed!');
|
||||
console.error('Authentication failed!');
|
||||
return;
|
||||
}
|
||||
this.token = JSON.parse(authReq.res.body);
|
||||
|
|
@ -227,7 +228,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async getProfile() : Promise<boolean> {
|
||||
if(!this.token.access_token){
|
||||
console.log('[ERROR] No access token!');
|
||||
console.error('No access token!');
|
||||
return false;
|
||||
}
|
||||
const profileReqOptions = {
|
||||
|
|
@ -238,11 +239,11 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
const profileReq = await this.req.getData(api.beta_profile, profileReqOptions);
|
||||
if(!profileReq.ok || !profileReq.res){
|
||||
console.log('[ERROR] Get profile failed!');
|
||||
console.error('Get profile failed!');
|
||||
return false;
|
||||
}
|
||||
const profile = JSON.parse(profileReq.res.body);
|
||||
console.log('[INFO] USER: %s (%s)', profile.username, profile.email);
|
||||
console.info('USER: %s (%s)', profile.username, profile.email);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +257,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if (!(Date.now() > new Date(this.token.expires).getTime()) && ifNeeded) {
|
||||
return;
|
||||
} else {
|
||||
//console.log('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.');
|
||||
//console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.');
|
||||
}
|
||||
const authData = new URLSearchParams({
|
||||
'refresh_token': this.token.refresh_token,
|
||||
|
|
@ -270,7 +271,7 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.log('[ERROR] Authentication failed!');
|
||||
console.error('Authentication failed!');
|
||||
return;
|
||||
}
|
||||
this.token = JSON.parse(authReq.res.body);
|
||||
|
|
@ -281,14 +282,14 @@ export default class Crunchy implements ServiceClass {
|
|||
if (!silent)
|
||||
await this.getProfile();
|
||||
} else {
|
||||
console.log('[INFO] USER: Anonymous');
|
||||
console.info('USER: Anonymous');
|
||||
}
|
||||
await this.getCMStoken();
|
||||
}
|
||||
|
||||
public async getCMStoken(){
|
||||
if(!this.token.access_token){
|
||||
console.log('[ERROR] No access token!');
|
||||
console.error('No access token!');
|
||||
return;
|
||||
}
|
||||
const cmsTokenReqOpts = {
|
||||
|
|
@ -299,17 +300,17 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
const cmsTokenReq = await this.req.getData(api.beta_cmsToken, cmsTokenReqOpts);
|
||||
if(!cmsTokenReq.ok || !cmsTokenReq.res){
|
||||
console.log('[ERROR] Authentication CMS token failed!');
|
||||
console.error('Authentication CMS token failed!');
|
||||
return;
|
||||
}
|
||||
this.cmsToken = JSON.parse(cmsTokenReq.res.body);
|
||||
console.log('[INFO] Your Country: %s\n', this.cmsToken.cms?.bucket.split('/')[1]);
|
||||
console.info('Your Country: %s\n', this.cmsToken.cms?.bucket.split('/')[1]);
|
||||
}
|
||||
|
||||
public async getCmsData(){
|
||||
// check token
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
// opts
|
||||
|
|
@ -325,15 +326,15 @@ export default class Crunchy implements ServiceClass {
|
|||
].join('');
|
||||
const indexReq = await this.req.getData(indexReqOpts);
|
||||
if(!indexReq.ok || ! indexReq.res){
|
||||
console.log('[ERROR] Get CMS index FAILED!');
|
||||
console.error('Get CMS index FAILED!');
|
||||
return;
|
||||
}
|
||||
console.log(JSON.parse(indexReq.res.body));
|
||||
console.info(JSON.parse(indexReq.res.body));
|
||||
}
|
||||
|
||||
public async doSearch(data: SearchData): Promise<SearchResponse>{
|
||||
if(!this.token.access_token){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return { isOk: false, reason: new Error('Not authenticated') };
|
||||
}
|
||||
const searchReqOpts = {
|
||||
|
|
@ -352,12 +353,12 @@ export default class Crunchy implements ServiceClass {
|
|||
}).toString();
|
||||
const searchReq = await this.req.getData(`${api.search}?${searchParams}`, searchReqOpts);
|
||||
if(!searchReq.ok || ! searchReq.res){
|
||||
console.log('[ERROR] Search FAILED!');
|
||||
console.error('Search FAILED!');
|
||||
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||
}
|
||||
const searchResults = JSON.parse(searchReq.res.body) as CrunchySearch;
|
||||
if(searchResults.total < 1){
|
||||
console.log('[INFO] Nothing Found!');
|
||||
console.info('Nothing Found!');
|
||||
return { isOk: true, value: [] };
|
||||
}
|
||||
|
||||
|
|
@ -368,23 +369,23 @@ export default class Crunchy implements ServiceClass {
|
|||
'episode': 'Found episodes'
|
||||
};
|
||||
for(const search_item of searchResults.data){
|
||||
console.log('[INFO] %s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]);
|
||||
console.info('%s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]);
|
||||
// calculate pages
|
||||
const pageCur = searchStart > 0 ? Math.ceil(searchStart/5) + 1 : 1;
|
||||
const pageMax = Math.ceil(search_item.count/5);
|
||||
// pages per category
|
||||
if(search_item.count < 1){
|
||||
console.log(' [INFO] Nothing Found...');
|
||||
console.info(' Nothing Found...');
|
||||
}
|
||||
if(search_item.count > 0){
|
||||
if(pageCur > pageMax){
|
||||
console.log(' [INFO] Last page is %s...', pageMax);
|
||||
console.info(' Last page is %s...', pageMax);
|
||||
continue;
|
||||
}
|
||||
for(const item of search_item.items){
|
||||
await this.logObject(item);
|
||||
}
|
||||
console.log(` [INFO] Total results: ${search_item.count} (Page: ${pageCur}/${pageMax})`);
|
||||
console.info(` Total results: ${search_item.count} (Page: ${pageCur}/${pageMax})`);
|
||||
}
|
||||
}
|
||||
const toSend = searchResults.data.filter(a => a.type === 'series' || a.type === 'movie_listing');
|
||||
|
|
@ -404,7 +405,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async logObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){
|
||||
if(this.debug){
|
||||
console.log(item);
|
||||
console.info(item);
|
||||
}
|
||||
pad = pad ?? 2;
|
||||
getSeries = getSeries === undefined ? true : getSeries;
|
||||
|
|
@ -455,13 +456,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 +524,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);
|
||||
|
|
@ -542,7 +543,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
// show entry
|
||||
console.log(
|
||||
console.info(
|
||||
'%s%s[%s] %s%s%s',
|
||||
''.padStart(item.isSelected ? pad-1 : pad, ' '),
|
||||
item.isSelected ? '✓' : '',
|
||||
|
|
@ -553,27 +554,27 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
);
|
||||
if(item.last_public){
|
||||
console.log(''.padStart(pad+1, ' '), '- Last updated:', item.last_public);
|
||||
console.info(''.padStart(pad+1, ' '), '- Last updated:', item.last_public);
|
||||
}
|
||||
if(item.subtitle_locales){
|
||||
iMetadata.subtitle_locales = item.subtitle_locales;
|
||||
}
|
||||
if (item.versions && audio_languages.length > 0) {
|
||||
console.log(
|
||||
console.info(
|
||||
'%s- Versions: %s',
|
||||
''.padStart(pad + 2, ' '),
|
||||
langsData.parseSubtitlesArray(audio_languages)
|
||||
);
|
||||
}
|
||||
if(iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0){
|
||||
console.log(
|
||||
console.info(
|
||||
'%s- Subtitles: %s',
|
||||
''.padStart(pad + 2, ' '),
|
||||
langsData.parseSubtitlesArray(iMetadata.subtitle_locales)
|
||||
);
|
||||
}
|
||||
if(item.availability_notes){
|
||||
console.log(
|
||||
console.info(
|
||||
'%s- Availability notes: %s',
|
||||
''.padStart(pad + 2, ' '),
|
||||
item.availability_notes.replace(/\[[^\]]*\]?/gm, '')
|
||||
|
|
@ -581,11 +582,11 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
if(item.type == 'series' && getSeries){
|
||||
await this.logSeriesById(item.id, pad, true);
|
||||
console.log();
|
||||
console.info('');
|
||||
}
|
||||
if(item.type == 'movie_listing' && getMovieListing){
|
||||
await this.logMovieListingById(item.id, pad+2);
|
||||
console.log();
|
||||
console.info('');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -595,7 +596,7 @@ export default class Crunchy implements ServiceClass {
|
|||
hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false;
|
||||
// check token
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
// opts
|
||||
|
|
@ -609,7 +610,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if(!hideSeriesTitle){
|
||||
const seriesReq = await this.req.getData(`${api.cms}/series/${id}?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!seriesReq.ok || !seriesReq.res){
|
||||
console.log('[ERROR] Series Request FAILED!');
|
||||
console.error('Series Request FAILED!');
|
||||
return;
|
||||
}
|
||||
const seriesData = JSON.parse(seriesReq.res.body);
|
||||
|
|
@ -618,13 +619,13 @@ export default class Crunchy implements ServiceClass {
|
|||
// seasons list
|
||||
const seriesSeasonListReq = await this.req.getData(`${api.cms}/series/${id}/seasons?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){
|
||||
console.log('[ERROR] Series Request FAILED!');
|
||||
console.error('Series Request FAILED!');
|
||||
return;
|
||||
}
|
||||
// parse data
|
||||
const seasonsList = JSON.parse(seriesSeasonListReq.res.body) as SeriesSearch;
|
||||
if(seasonsList.total < 1){
|
||||
console.log('[INFO] Series is empty!');
|
||||
console.info('Series is empty!');
|
||||
return;
|
||||
}
|
||||
for(const item of seasonsList.data){
|
||||
|
|
@ -635,7 +636,7 @@ export default class Crunchy implements ServiceClass {
|
|||
public async logMovieListingById(id: string, pad?: number){
|
||||
pad = pad || 2;
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
const movieListingReqOpts = [
|
||||
|
|
@ -651,12 +652,12 @@ export default class Crunchy implements ServiceClass {
|
|||
].join('');
|
||||
const movieListingReq = await this.req.getData(movieListingReqOpts);
|
||||
if(!movieListingReq.ok || !movieListingReq.res){
|
||||
console.log('[ERROR] Movie Listing Request FAILED!');
|
||||
console.error('Movie Listing Request FAILED!');
|
||||
return;
|
||||
}
|
||||
const movieListing = JSON.parse(movieListingReq.res.body);
|
||||
if(movieListing.total < 1){
|
||||
console.log('[INFO] Movie Listing is empty!');
|
||||
console.info('Movie Listing is empty!');
|
||||
return;
|
||||
}
|
||||
for(const item of movieListing.items){
|
||||
|
|
@ -666,7 +667,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async getNewlyAdded(page?: number){
|
||||
if(!this.token.access_token){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
const newlyAddedReqOpts = {
|
||||
|
|
@ -682,11 +683,11 @@ export default class Crunchy implements ServiceClass {
|
|||
}).toString();
|
||||
const newlyAddedReq = await this.req.getData(`${api.beta_browse}?${newlyAddedParams}`, newlyAddedReqOpts);
|
||||
if(!newlyAddedReq.ok || !newlyAddedReq.res){
|
||||
console.log('[ERROR] Get newly added FAILED!');
|
||||
console.error('Get newly added FAILED!');
|
||||
return;
|
||||
}
|
||||
const newlyAddedResults = JSON.parse(newlyAddedReq.res.body);
|
||||
console.log('[INFO] Newly added:');
|
||||
console.info('Newly added:');
|
||||
for(const i of newlyAddedResults.items){
|
||||
await this.logObject(i, 2);
|
||||
}
|
||||
|
|
@ -694,12 +695,12 @@ export default class Crunchy implements ServiceClass {
|
|||
const itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.api_beta).searchParams.get('start') as string);
|
||||
const pageCur = itemPad > 0 ? Math.ceil(itemPad/25) + 1 : 1;
|
||||
const pageMax = Math.ceil(newlyAddedResults.total/25);
|
||||
console.log(` [INFO] Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`);
|
||||
console.info(` Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`);
|
||||
}
|
||||
|
||||
public async getSeasonById(id: string, numbers: number, e: string|undefined, but: boolean, all: boolean) : Promise<ResponseBase<CrunchyEpMeta[]>> {
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return { isOk: false, reason: new Error('Authentication required') };
|
||||
}
|
||||
|
||||
|
|
@ -714,7 +715,7 @@ export default class Crunchy implements ServiceClass {
|
|||
//get show info
|
||||
const showInfoReq = await this.req.getData(`${api.cms}/seasons/${id}?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!showInfoReq.ok || !showInfoReq.res){
|
||||
console.log('[ERROR] Show Request FAILED!');
|
||||
console.error('Show Request FAILED!');
|
||||
return { isOk: false, reason: new Error('Show request failed. No more information provided.') };
|
||||
}
|
||||
const showInfo = JSON.parse(showInfoReq.res.body);
|
||||
|
|
@ -723,7 +724,7 @@ export default class Crunchy implements ServiceClass {
|
|||
//get episode info
|
||||
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${id}/episodes?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!reqEpsList.ok || !reqEpsList.res){
|
||||
console.log('[ERROR] Episode List Request FAILED!');
|
||||
console.error('Episode List Request FAILED!');
|
||||
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
|
||||
}
|
||||
const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
|
||||
|
|
@ -735,7 +736,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const epNumLen = numbers;
|
||||
|
||||
if(episodeList.total < 1){
|
||||
console.log(' [INFO] Season is empty!');
|
||||
console.info(' Season is empty!');
|
||||
return { isOk: true, value: [] };
|
||||
}
|
||||
|
||||
|
|
@ -808,10 +809,10 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
// display
|
||||
if(selectedMedia.length < 1){
|
||||
console.log('\n[INFO] Episodes not selected!\n');
|
||||
console.info('\nEpisodes not selected!\n');
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.info('');
|
||||
return { isOk: true, value: selectedMedia };
|
||||
}
|
||||
|
||||
|
|
@ -831,19 +832,19 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async getObjectById(e?: string, earlyReturn?: boolean): Promise<ObjectInfo|Partial<CrunchyEpMeta>[]|undefined> {
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
const doEpsFilter = parseSelect(e as string);
|
||||
|
||||
if(doEpsFilter.values.length < 1){
|
||||
console.log('\n[INFO] Objects not selected!\n');
|
||||
console.info('\nObjects not selected!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5
|
||||
console.log('[INFO] Requested object ID: %s', doEpsFilter.values.join(', '));
|
||||
console.info('Requested object ID: %s', doEpsFilter.values.join(', '));
|
||||
|
||||
const AuthHeaders = {
|
||||
headers: {
|
||||
|
|
@ -855,10 +856,10 @@ export default class Crunchy implements ServiceClass {
|
|||
// reqs
|
||||
const objectReq = await this.req.getData(`${api.cms}/objects/${doEpsFilter.values.join(',')}?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!objectReq.ok || !objectReq.res){
|
||||
console.log('[ERROR] Objects Request FAILED!');
|
||||
console.error('Objects Request FAILED!');
|
||||
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
|
||||
const objectInfo = JSON.parse(objectReq.error.res.body as string);
|
||||
console.log('[INFO] Body:', JSON.stringify(objectInfo, null, '\t'));
|
||||
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
|
||||
objectInfo.error = true;
|
||||
return objectInfo;
|
||||
}
|
||||
|
|
@ -870,7 +871,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'){
|
||||
|
|
@ -913,7 +914,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
await this.logObject(item, 2);
|
||||
}
|
||||
console.log();
|
||||
console.info('');
|
||||
return selectedMedia;
|
||||
}
|
||||
|
||||
|
|
@ -923,7 +924,7 @@ export default class Crunchy implements ServiceClass {
|
|||
error: boolean
|
||||
} | undefined> {
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -937,7 +938,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const files: DownloadedMedia[] = [];
|
||||
|
||||
if(medias.data.every(a => !a.playback)){
|
||||
console.log('[WARN] Video not available!');
|
||||
console.warn('Video not available!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -946,7 +947,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
|
||||
for (const mMeta of medias.data) {
|
||||
console.log(`[INFO] Requesting: [${mMeta.mediaId}] ${mediaName}`);
|
||||
console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`);
|
||||
|
||||
//Make sure token is up to date
|
||||
await this.refreshToken(true, true);
|
||||
|
|
@ -962,7 +963,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if (mMeta.versions && mMeta.lang) {
|
||||
mediaId = mMeta.versions.find(a => a.audio_locale == mMeta.lang?.cr_locale)?.media_guid as string;
|
||||
if (!mediaId) {
|
||||
console.log('[ERROR] Selected language not found.');
|
||||
console.error('Selected language not found.');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -973,10 +974,10 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
let playbackReq = await this.req.getData(`${api.cms}/videos/${mediaId}/streams`, AuthHeaders);
|
||||
if(!playbackReq.ok || !playbackReq.res){
|
||||
console.log('[ERROR] Request Stream URLs FAILED! Attempting fallback');
|
||||
console.error('Request Stream URLs FAILED! Attempting fallback');
|
||||
playbackReq = await this.req.getData(`${domain.api_beta}${mMeta.playback}`, AuthHeaders);
|
||||
if(!playbackReq.ok || !playbackReq.res){
|
||||
console.log('[ERROR] Fallback Request Stream URLs FAILED!');
|
||||
console.error('Fallback Request Stream URLs FAILED!');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -998,7 +999,7 @@ export default class Crunchy implements ServiceClass {
|
|||
} as Variable;
|
||||
}));
|
||||
|
||||
let streams = [];
|
||||
let streams: any[] = [];
|
||||
let hsLangs: string[] = [];
|
||||
const pbStreams = pbData.data[0];
|
||||
|
||||
|
|
@ -1021,7 +1022,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
if(streams.length < 1){
|
||||
console.log('[WARN] No full streams found!');
|
||||
console.warn('No full streams found!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -1044,7 +1045,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
if(options.hslang != 'none'){
|
||||
if(hsLangs.indexOf(options.hslang) > -1){
|
||||
console.log('[INFO] Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language);
|
||||
console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language);
|
||||
streams = streams.filter((s) => {
|
||||
if(s.hardsub_lang == '-'){
|
||||
return false;
|
||||
|
|
@ -1053,9 +1054,9 @@ export default class Crunchy implements ServiceClass {
|
|||
});
|
||||
}
|
||||
else{
|
||||
console.log('[WARN] Selected stream with %s hardsubs not available', langsData.locale2language(options.hslang).language);
|
||||
console.warn('Selected stream with %s hardsubs not available', langsData.locale2language(options.hslang).language);
|
||||
if(hsLangs.length > 0){
|
||||
console.log('[WARN] Try other hardsubs stream:', hsLangs.join(', '));
|
||||
console.warn('Try other hardsubs stream:', hsLangs.join(', '));
|
||||
}
|
||||
dlFailed = true;
|
||||
}
|
||||
|
|
@ -1068,13 +1069,13 @@ export default class Crunchy implements ServiceClass {
|
|||
return true;
|
||||
});
|
||||
if(streams.length < 1){
|
||||
console.log('[WARN] Raw streams not available!');
|
||||
console.warn('Raw streams not available!');
|
||||
if(hsLangs.length > 0){
|
||||
console.log('[WARN] Try hardsubs stream:', hsLangs.join(', '));
|
||||
console.warn('Try hardsubs stream:', hsLangs.join(', '));
|
||||
}
|
||||
dlFailed = true;
|
||||
}
|
||||
console.log('[INFO] Selecting raw stream');
|
||||
console.info('Selecting raw stream');
|
||||
}
|
||||
|
||||
let curStream:
|
||||
|
|
@ -1086,19 +1087,19 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
streams.forEach((s, i) => {
|
||||
const isSelected = options.kstream == i + 1 ? '✓' : ' ';
|
||||
console.log('[INFO] Full stream found! (%s%s: %s )', isSelected, i + 1, s.type);
|
||||
console.info('Full stream found! (%s%s: %s )', isSelected, i + 1, s.type);
|
||||
});
|
||||
|
||||
console.log('[INFO] Downloading video...');
|
||||
console.info('Downloading video...');
|
||||
curStream = streams[options.kstream-1];
|
||||
|
||||
console.log('[INFO] Playlists URL: %s (%s)', curStream.url, curStream.type);
|
||||
console.info('Playlists URL: %s (%s)', curStream.url, curStream.type);
|
||||
}
|
||||
|
||||
if(!options.novids && !dlFailed && curStream !== undefined){
|
||||
const streamPlaylistsReq = await this.req.getData(curStream.url);
|
||||
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||
console.log('[ERROR] CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||
dlFailed = true;
|
||||
}
|
||||
else{
|
||||
|
|
@ -1139,7 +1140,7 @@ export default class Crunchy implements ServiceClass {
|
|||
&& plStreams[plServer][plResolutionText] != pl.uri
|
||||
&& typeof plStreams[plServer][plResolutionText] != 'undefined'
|
||||
){
|
||||
console.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
console.error(`Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
}
|
||||
else{
|
||||
plStreams[plServer][plResolutionText] = pl.uri;
|
||||
|
|
@ -1170,7 +1171,7 @@ export default class Crunchy implements ServiceClass {
|
|||
});
|
||||
let quality = options.q === 0 ? plQuality.length : options.q;
|
||||
if(quality > plQuality.length) {
|
||||
console.log(`[WARN] The requested quality of ${options.q} is greater than the maximun ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`);
|
||||
console.warn(`The requested quality of ${options.q} is greater than the maximun ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`);
|
||||
quality = plQuality.length;
|
||||
}
|
||||
// When best selected video quality is already downloaded
|
||||
|
|
@ -1181,8 +1182,8 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : '';
|
||||
console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.log(`[INFO] Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
|
||||
|
||||
if(selPlUrl != ''){
|
||||
variables.push({
|
||||
|
|
@ -1196,18 +1197,18 @@ export default class Crunchy implements ServiceClass {
|
|||
});
|
||||
const lang = langsData.languages.find(a => a.code === curStream?.audio_lang);
|
||||
if (!lang) {
|
||||
console.log(`[ERROR] Unable to find language for code ${curStream.audio_lang}`);
|
||||
console.error(`Unable to find language for code ${curStream.audio_lang}`);
|
||||
return;
|
||||
}
|
||||
console.log(`[INFO] Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
|
||||
console.log('[INFO] Stream URL:', selPlUrl);
|
||||
console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
|
||||
console.info('Stream URL:', selPlUrl);
|
||||
// TODO check filename
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep);
|
||||
console.log(`[INFO] Output filename: ${outFile}`);
|
||||
console.info(`Output filename: ${outFile}`);
|
||||
const chunkPage = await this.req.getData(selPlUrl);
|
||||
if(!chunkPage.ok || !chunkPage.res){
|
||||
console.log('[ERROR] CAN\'T FETCH VIDEO PLAYLIST!');
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
|
||||
dlFailed = true;
|
||||
}
|
||||
else{
|
||||
|
|
@ -1215,7 +1216,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const totalParts = chunkPlaylist.segments.length;
|
||||
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||
console.log('[INFO] Total parts in stream:', totalParts, mathMsg);
|
||||
console.info('Total parts in stream:', totalParts, mathMsg);
|
||||
const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
|
|
@ -1242,7 +1243,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}) : undefined
|
||||
}).download();
|
||||
if(!dlStreamByPl.ok){
|
||||
console.log(`[ERROR] DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
|
||||
console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
|
||||
dlFailed = true;
|
||||
}
|
||||
files.push({
|
||||
|
|
@ -1254,14 +1255,14 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
else{
|
||||
console.log('[ERROR] Quality not selected!\n');
|
||||
console.error('Quality not selected!\n');
|
||||
dlFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(options.novids){
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
console.log('[INFO] Downloading skipped!');
|
||||
console.info('Downloading skipped!');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1270,7 +1271,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
if(options.hslang != 'none'){
|
||||
console.log('[WARN] Subtitles downloading disabled for hardsubs streams.');
|
||||
console.warn('Subtitles downloading disabled for hardsubs streams.');
|
||||
options.skipsubs = true;
|
||||
}
|
||||
|
||||
|
|
@ -1307,7 +1308,7 @@ export default class Crunchy implements ServiceClass {
|
|||
sxData.title = `${langItem.language} / ${sxData.title}`;
|
||||
sxData.fonts = fontsData.assFonts(sBody) as Font[];
|
||||
fs.writeFileSync(sxData.path, sBody);
|
||||
console.log(`[INFO] Subtitle downloaded: ${sxData.file}`);
|
||||
console.info(`Subtitle downloaded: ${sxData.file}`);
|
||||
files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
|
|
@ -1315,17 +1316,17 @@ export default class Crunchy implements ServiceClass {
|
|||
});
|
||||
}
|
||||
else{
|
||||
console.log(`[WARN] Failed to download subtitle: ${sxData.file}`);
|
||||
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
console.log('[WARN] Can\'t find urls for subtitles!');
|
||||
console.warn('Can\'t find urls for subtitles!');
|
||||
}
|
||||
}
|
||||
else{
|
||||
console.log('[INFO] Subtitles downloading skipped!');
|
||||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
@ -1338,7 +1339,7 @@ export default class Crunchy implements ServiceClass {
|
|||
public async muxStreams(data: DownloadedMedia[], options: CrunchyMuxOptions) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
return console.log('[INFO] Skip muxing since no vids are downloaded');
|
||||
return console.info('Skip muxing since no vids are downloaded');
|
||||
const merger = new Merger({
|
||||
onlyVid: [],
|
||||
skipSubMux: options.skipSubMux,
|
||||
|
|
@ -1385,7 +1386,7 @@ export default class Crunchy implements ServiceClass {
|
|||
await merger.merge('ffmpeg', bin.FFmpeg);
|
||||
isMuxed = true;
|
||||
} else{
|
||||
console.log('\n[INFO] Done!\n');
|
||||
console.info('\nDone!\n');
|
||||
return;
|
||||
}
|
||||
if (isMuxed && !options.nocleanup)
|
||||
|
|
@ -1465,7 +1466,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
for (const key of Object.keys(episodes)) {
|
||||
const item = episodes[key];
|
||||
console.log(`[${key}] ${
|
||||
console.info(`[${key}] ${
|
||||
item.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? item.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd()
|
||||
} - Season ${item.items[0].season_number} - ${item.items[0].title} [${
|
||||
item.items.map((a, index) => {
|
||||
|
|
@ -1477,7 +1478,7 @@ export default class Crunchy implements ServiceClass {
|
|||
//TODO: Sort episodes to have specials at the end
|
||||
|
||||
if (!serieshasversions) {
|
||||
console.log('[WARN] Couldn\'t find versions on some episodes, fell back to old method.');
|
||||
console.warn('Couldn\'t find versions on some episodes, fell back to old method.');
|
||||
}
|
||||
|
||||
return { data: episodes, list: Object.entries(episodes).map(([key, value]) => {
|
||||
|
|
@ -1500,13 +1501,13 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async downloadFromSeriesID(id: string, data: CurnchyMultiDownload) : Promise<ResponseBase<CrunchyEpMeta[]>> {
|
||||
const { data: episodes } = await this.listSeriesID(id);
|
||||
console.log();
|
||||
console.log('-'.repeat(30));
|
||||
console.log();
|
||||
console.info('');
|
||||
console.info('-'.repeat(30));
|
||||
console.info('');
|
||||
const selected = this.itemSelectMultiDub(episodes, data.dubLang, data.but, data.all, data.e);
|
||||
for (const key of Object.keys(selected)) {
|
||||
const item = selected[key];
|
||||
console.log(`[S${item.season}E${item.episodeNumber}] - ${item.episodeTitle} [${
|
||||
console.info(`[S${item.season}E${item.episodeNumber}] - ${item.episodeTitle} [${
|
||||
item.data.map(a => {
|
||||
return `✓ ${a.lang?.name || 'Unknown Language'}`;
|
||||
}).join(', ')
|
||||
|
|
@ -1611,7 +1612,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async parseSeriesById(id: string) {
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1625,13 +1626,13 @@ export default class Crunchy implements ServiceClass {
|
|||
// seasons list
|
||||
const seriesSeasonListReq = await this.req.getData(`${api.cms}/series/${id}/seasons?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){
|
||||
console.log('[ERROR] Series Request FAILED!');
|
||||
console.error('Series Request FAILED!');
|
||||
return;
|
||||
}
|
||||
// parse data
|
||||
const seasonsList = JSON.parse(seriesSeasonListReq.res.body) as SeriesSearch;
|
||||
if(seasonsList.total < 1){
|
||||
console.log('[INFO] Series is empty!');
|
||||
console.info('Series is empty!');
|
||||
return;
|
||||
}
|
||||
return seasonsList;
|
||||
|
|
@ -1639,7 +1640,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async getSeasonDataById(item: SeriesSearchItem, log = false){
|
||||
if(!this.cmsToken.cms){
|
||||
console.log('[ERROR] Authentication required!');
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1653,7 +1654,7 @@ export default class Crunchy implements ServiceClass {
|
|||
//get show info
|
||||
const showInfoReq = await this.req.getData(`${api.cms}/seasons/${item.id}?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!showInfoReq.ok || !showInfoReq.res){
|
||||
console.log('[ERROR] Show Request FAILED!');
|
||||
console.error('Show Request FAILED!');
|
||||
return;
|
||||
}
|
||||
const showInfo = JSON.parse(showInfoReq.res.body);
|
||||
|
|
@ -1662,13 +1663,13 @@ export default class Crunchy implements ServiceClass {
|
|||
//get episode info
|
||||
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${item.id}/episodes?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!reqEpsList.ok || !reqEpsList.res){
|
||||
console.log('[ERROR] Episode List Request FAILED!');
|
||||
console.error('Episode List Request FAILED!');
|
||||
return;
|
||||
}
|
||||
const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
|
||||
|
||||
if(episodeList.total < 1){
|
||||
console.log(' [INFO] Season is empty!');
|
||||
console.info(' Season is empty!');
|
||||
return;
|
||||
}
|
||||
return episodeList;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# multi-downloader-nx (3.3.7v)
|
||||
# multi-downloader-nx (3.4.0v)
|
||||
|
||||
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||
|
||||
|
|
@ -379,4 +379,5 @@ If true, the tool won't check for updates
|
|||
| --- | --- | --- | --- | --- | ---|
|
||||
| Both | `--help ` | `boolean` | `No`| `-h` | `NaN` |
|
||||
|
||||
Show the help output
|
||||
Show the help output
|
||||
### GUI
|
||||
|
|
|
|||
115
funi.ts
115
funi.ts
|
|
@ -3,15 +3,13 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
|
||||
// package json
|
||||
import packageJson from './package.json';
|
||||
|
||||
// program name
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
import packageJson from './package.json';
|
||||
|
||||
// modules extra
|
||||
import { console } from './modules/log';
|
||||
import * as shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
import hlsDownload, { HLSCallback } from 'hls-download';
|
||||
import hlsDownload, { HLSCallback } from './modules/hls-download';
|
||||
|
||||
// extra
|
||||
import * as appYargs from './modules/module.app-args';
|
||||
|
|
@ -19,7 +17,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
|
||||
|
||||
|
|
@ -39,7 +37,10 @@ import { TitleElement } from './@types/episode';
|
|||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData, ResponseBase } from './@types/messageHandler';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { SubtitleRequest } from './@types/funiSubtitleRequest';
|
||||
import { SubtitleRequest } from './@types/funiSubtitleRequest';
|
||||
|
||||
// program name
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
// check page
|
||||
|
||||
// fn variables
|
||||
|
|
@ -73,7 +74,7 @@ export default class Funi implements ServiceClass {
|
|||
const argv = appYargs.appArgv(this.cfg.cli);
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
console.log(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
if (argv.allDubs) {
|
||||
argv.dubLang = langsData.dubLanguageCodes;
|
||||
}
|
||||
|
|
@ -98,7 +99,7 @@ export default class Funi implements ServiceClass {
|
|||
else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){
|
||||
const data = await this.getShow(true, { id: parseInt(argv.s), but: argv.but, all: argv.all, e: argv.e });
|
||||
if (!data.isOk) {
|
||||
console.log(`[ERROR] ${data.reason.message}`);
|
||||
console.error(`${data.reason.message}`);
|
||||
return false;
|
||||
}
|
||||
let ok = true;
|
||||
|
|
@ -112,7 +113,7 @@ export default class Funi implements ServiceClass {
|
|||
return ok;
|
||||
}
|
||||
else{
|
||||
console.log('[INFO] No option selected or invalid value entered. Try --help.');
|
||||
console.info('No option selected or invalid value entered. Try --help.');
|
||||
}
|
||||
}
|
||||
public async auth(data: AuthData): Promise<AuthResponse> {
|
||||
|
|
@ -129,14 +130,14 @@ export default class Funi implements ServiceClass {
|
|||
if(authData.ok && authData.res){
|
||||
const resJSON = JSON.parse(authData.res.body);
|
||||
if(resJSON.token){
|
||||
console.log('[INFO] Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
|
||||
console.info('Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
|
||||
yamlCfg.saveFuniToken({'token': resJSON.token});
|
||||
this.token = resJSON.token;
|
||||
return { isOk: true, value: undefined };
|
||||
} else {
|
||||
console.log('[ERROR]%s\n', ' No token found');
|
||||
console.info('[ERROR]%s\n', ' No token found');
|
||||
if (this.debug) {
|
||||
console.log(resJSON);
|
||||
console.info(resJSON);
|
||||
}
|
||||
return { isOk: false, reason: new Error(resJSON) };
|
||||
}
|
||||
|
|
@ -159,18 +160,18 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
const searchDataJSON = JSON.parse(searchData.res.body);
|
||||
if(searchDataJSON.detail){
|
||||
console.log(`[ERROR] ${searchDataJSON.detail}`);
|
||||
console.error(`${searchDataJSON.detail}`);
|
||||
return { isOk: false, reason: new Error(searchDataJSON.defail) };
|
||||
}
|
||||
if(searchDataJSON.items && searchDataJSON.items.hits && log){
|
||||
const shows = searchDataJSON.items.hits;
|
||||
console.log('[INFO] Search Results:');
|
||||
console.info('Search Results:');
|
||||
for(const ssn in shows){
|
||||
console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
|
||||
console.info(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
|
||||
}
|
||||
}
|
||||
if (log)
|
||||
console.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
|
||||
console.info('Total shows found: %s\n',searchDataJSON.count);
|
||||
return { isOk: true, value: searchDataJSON };
|
||||
}
|
||||
|
||||
|
|
@ -186,15 +187,15 @@ export default class Funi implements ServiceClass {
|
|||
if(!showData.ok || !showData.res){ return { isOk: false, reason: new Error('ShowData is not ok') }; }
|
||||
const showDataJSON = JSON.parse(showData.res.body);
|
||||
if(showDataJSON.status){
|
||||
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
|
||||
console.error('Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
|
||||
return { isOk: false, reason: new Error(showDataJSON.data.errors[0].detail) };
|
||||
}
|
||||
else if(!showDataJSON.items || showDataJSON.items.length<1){
|
||||
console.log('[ERROR] Show not found\n');
|
||||
console.error('Show not found\n');
|
||||
return { isOk: false, reason: new Error('Show not found') };
|
||||
}
|
||||
const showDataItem = showDataJSON.items[0];
|
||||
console.log('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
|
||||
console.info('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
|
||||
// show episodes
|
||||
const qs: {
|
||||
limit: number,
|
||||
|
|
@ -219,7 +220,7 @@ export default class Funi implements ServiceClass {
|
|||
const parseEpStr = (epStr: string) => {
|
||||
const match = epStr.match(epNumRegex);
|
||||
if (!match) {
|
||||
console.error('[ERROR] No match found');
|
||||
console.error('No match found');
|
||||
return ['', ''];
|
||||
}
|
||||
if(match.length > 2){
|
||||
|
|
@ -241,7 +242,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
else{
|
||||
Funi.typeIdLen = 3 > Funi.typeIdLen? 3 : Funi.typeIdLen;
|
||||
console.log('[ERROR] FAILED TO PARSE: ', e.id);
|
||||
console.error('FAILED TO PARSE: ', e.id);
|
||||
e.id_split = [ 'ZZZ', 9999 ];
|
||||
}
|
||||
return e;
|
||||
|
|
@ -266,7 +267,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){
|
||||
|
|
@ -308,16 +309,16 @@ export default class Funi implements ServiceClass {
|
|||
conOut += `(${rtm_str}) [${qua_str+aud_str}]`;
|
||||
conOut += is_selected ? ' (selected)' : '';
|
||||
conOut += eps.length-1 == parseInt(e) ? '\n' : '';
|
||||
console.log(conOut);
|
||||
console.info(conOut);
|
||||
}
|
||||
if(fnSlug.length < 1){
|
||||
if (log)
|
||||
console.log('[INFO] Episodes not selected!\n');
|
||||
console.info('Episodes not selected!\n');
|
||||
return { isOk: true, value: [] } ;
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
|
||||
console.info('Selected Episodes: %s\n',epSelEpsTxt.join(', '));
|
||||
return { isOk: true, value: fnSlug };
|
||||
}
|
||||
}
|
||||
|
|
@ -331,7 +332,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'){
|
||||
|
|
@ -347,15 +348,15 @@ export default class Funi implements ServiceClass {
|
|||
|
||||
// end
|
||||
if (log) {
|
||||
console.log(
|
||||
'[INFO] %s - S%sE%s - %s',
|
||||
console.info(
|
||||
'%s - S%sE%s - %s',
|
||||
ep.parent.title,
|
||||
(ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
|
||||
(ep.number ? ep.number : '?'),
|
||||
ep.title
|
||||
);
|
||||
|
||||
console.log('[INFO] Available streams (Non-Encrypted):');
|
||||
console.info('Available streams (Non-Encrypted):');
|
||||
}
|
||||
// map medias
|
||||
const media = await Promise.all(ep.media.map(async (m) =>{
|
||||
|
|
@ -406,7 +407,7 @@ export default class Funi implements ServiceClass {
|
|||
if (!subsToDisplay.includes(a.lang))
|
||||
subsToDisplay.push(a.lang);
|
||||
});
|
||||
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
||||
console.info(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
||||
localSubs && localSubs.length > 0 && selected ? ` (using ${subsToDisplay.map(a => `'${a.name}'`).join(', ')} for subtitles)` : ''
|
||||
}`);
|
||||
}
|
||||
|
|
@ -424,7 +425,7 @@ export default class Funi implements ServiceClass {
|
|||
});
|
||||
if(streamIds.length < 1){
|
||||
if (log)
|
||||
console.log('[ERROR] Track not selected\n');
|
||||
console.error('Track not selected\n');
|
||||
return { isOk: false, reason: new Error('Track not selected') };
|
||||
}
|
||||
else{
|
||||
|
|
@ -442,7 +443,7 @@ export default class Funi implements ServiceClass {
|
|||
const streamDataRes = JSON.parse(streamData.res.body) as StreamData;
|
||||
if(streamDataRes.errors){
|
||||
if (log)
|
||||
console.log('[ERROR] Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
|
||||
console.info('Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
|
||||
return { isOk: false, reason: new Error(streamDataRes.errors[0].detail) };
|
||||
}
|
||||
else{
|
||||
|
|
@ -459,7 +460,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
if(tsDlPath.length < 1){
|
||||
if (log)
|
||||
console.log('[ERROR] Unknown error\n');
|
||||
console.error('Unknown error\n');
|
||||
return { isOk: false, reason: new Error('Unknown error') };
|
||||
}
|
||||
else{
|
||||
|
|
@ -508,7 +509,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
|
||||
|
|
@ -525,7 +526,7 @@ export default class Funi implements ServiceClass {
|
|||
if(plQualityLinkList.playlists[0].uri.match(vplReg)){
|
||||
const audioKey = Object.keys(plQualityLinkList.mediaGroups.AUDIO).pop();
|
||||
if (!audioKey)
|
||||
return console.log('[ERROR] No audio key found');
|
||||
return console.error('No audio key found');
|
||||
if(plQualityLinkList.mediaGroups.AUDIO[audioKey]){
|
||||
const audioDataParts = plQualityLinkList.mediaGroups.AUDIO[audioKey],
|
||||
audioEl = Object.keys(audioDataParts);
|
||||
|
|
@ -535,7 +536,7 @@ export default class Funi implements ServiceClass {
|
|||
language = langsData.languages.find(a => a.funi_name_lagacy === audioEl[0] || ((a.funi_name ?? a.name) === audioEl[0]));
|
||||
if (!language) {
|
||||
if (log)
|
||||
console.log(`[ERROR] Unable to find language for locale ${audioData.language} or name ${audioEl[0]}`);
|
||||
console.error(`Unable to find language for locale ${audioData.language} or name ${audioEl[0]}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -547,7 +548,7 @@ export default class Funi implements ServiceClass {
|
|||
plQualityLinkList.playlists.sort((a, b) => {
|
||||
const aMatch = a.uri.match(vplReg), bMatch = b.uri.match(vplReg);
|
||||
if (!aMatch || !bMatch) {
|
||||
console.log('[ERROR] Unable to match');
|
||||
console.info('Unable to match');
|
||||
return 0;
|
||||
}
|
||||
const av = parseInt(aMatch[3]);
|
||||
|
|
@ -584,7 +585,7 @@ export default class Funi implements ServiceClass {
|
|||
plStreams[plServer] = {};
|
||||
}
|
||||
if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){
|
||||
console.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
console.warn(`Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
}
|
||||
else{
|
||||
plStreams[plServer][plLayerId] = plUrlDl;
|
||||
|
|
@ -604,7 +605,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
}
|
||||
else {
|
||||
console.log(s.uri);
|
||||
console.info(s.uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -622,8 +623,8 @@ export default class Funi implements ServiceClass {
|
|||
|
||||
plLayersStr.sort();
|
||||
if (log) {
|
||||
console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.log(`[INFO] Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
||||
}
|
||||
|
||||
const selectedQuality = data.q === 0 || data.q > Object.keys(plLayersRes).length
|
||||
|
|
@ -633,8 +634,8 @@ export default class Funi implements ServiceClass {
|
|||
|
||||
if(videoUrl != ''){
|
||||
if (log) {
|
||||
console.log(`[INFO] Selected layer: ${selectedQuality} (${plLayersRes[selectedQuality].width}x${plLayersRes[selectedQuality].height}) @ ${plSelectedServer}`);
|
||||
console.log('[INFO] Stream URL:',videoUrl);
|
||||
console.info(`Selected layer: ${selectedQuality} (${plLayersRes[selectedQuality].width}x${plLayersRes[selectedQuality].height}) @ ${plSelectedServer}`);
|
||||
console.info('Stream URL:',videoUrl);
|
||||
}
|
||||
|
||||
fnOutput = parseFileName(data.fileName, ([
|
||||
|
|
@ -656,16 +657,16 @@ export default class Funi implements ServiceClass {
|
|||
if (fnOutput.length < 1)
|
||||
throw new Error(`Invalid path generated for input ${data.fileName}`);
|
||||
if (log)
|
||||
console.log(`[INFO] Output filename: ${fnOutput.join(path.sep)}.ts`);
|
||||
console.info(`Output filename: ${fnOutput.join(path.sep)}.ts`);
|
||||
}
|
||||
else if(data.x > plServerList.length){
|
||||
if (log)
|
||||
console.log('[ERROR] Server not selected!\n');
|
||||
console.error('Server not selected!\n');
|
||||
return;
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.log('[ERROR] Layer not selected!\n');
|
||||
console.error('Layer not selected!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -715,7 +716,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
else{
|
||||
if (log)
|
||||
console.log('[INFO] Skip video downloading...\n');
|
||||
console.info('Skip video downloading...\n');
|
||||
}
|
||||
audio: if (plAud && !data.noaudio) {
|
||||
// download audio
|
||||
|
|
@ -756,7 +757,7 @@ export default class Funi implements ServiceClass {
|
|||
// download subtitles
|
||||
if(stDlPath.length > 0){
|
||||
if (log)
|
||||
console.log('[INFO] Downloading subtitles...');
|
||||
console.info('Downloading subtitles...');
|
||||
for (const subObject of stDlPath) {
|
||||
const subsSrc = await getData({
|
||||
url: subObject.url,
|
||||
|
|
@ -769,24 +770,24 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
else{
|
||||
if (log)
|
||||
console.log('[ERROR] Failed to download subtitles!');
|
||||
console.error('Failed to download subtitles!');
|
||||
addSubs = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addSubs && log)
|
||||
console.log('[INFO] Subtitles downloaded!');
|
||||
console.info('Subtitles downloaded!');
|
||||
}
|
||||
|
||||
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
||||
if (log)
|
||||
console.log('\n[INFO] Unable to locate a video AND audio file\n');
|
||||
console.info('\nUnable to locate a video AND audio file\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.skipmux){
|
||||
if (log)
|
||||
console.log('[INFO] Skipping muxing...');
|
||||
console.info('Skipping muxing...');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -796,7 +797,7 @@ export default class Funi implements ServiceClass {
|
|||
|
||||
if ( data.novids ){
|
||||
if (log)
|
||||
console.log('[INFO] Video not downloaded. Skip muxing video.');
|
||||
console.info('Video not downloaded. Skip muxing video.');
|
||||
}
|
||||
|
||||
const ffext = !data.mp4 ? 'mkv' : 'mp4';
|
||||
|
|
@ -835,7 +836,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
else{
|
||||
if (log)
|
||||
console.log('\n[INFO] Done!\n');
|
||||
console.info('\nDone!\n');
|
||||
return true;
|
||||
}
|
||||
if (data.nocleanup) {
|
||||
|
|
@ -844,7 +845,7 @@ export default class Funi implements ServiceClass {
|
|||
|
||||
mergeInstance.cleanUp();
|
||||
if (log)
|
||||
console.log('\n[INFO] Done!\n');
|
||||
console.info('\nDone!\n');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -878,7 +879,7 @@ export default class Funi implements ServiceClass {
|
|||
querystring: { deviceType: 'web' }
|
||||
});
|
||||
if (!subs.ok || !subs.res || !subs.res.body) {
|
||||
console.log('[ERROR] Subtitle Request failed.');
|
||||
console.error('Subtitle Request failed.');
|
||||
return [];
|
||||
}
|
||||
const parsed: SubtitleRequest = JSON.parse(subs.res.body);
|
||||
|
|
|
|||
3
gui.ts
Normal file
3
gui.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
process.env.isGUI = 'true';
|
||||
import './modules/log';
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
2
gui/react/.env
Normal file
2
gui/react/.env
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
PORT=3002
|
||||
CI=false
|
||||
|
|
@ -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'}
|
||||
|
|
|
|||
4
gui/react/src/@types/FC.d.ts
vendored
4
gui/react/src/@types/FC.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
type FCWithChildren<T = {}> = React.FC<{
|
||||
type FCWithChildren<T = object> = React.FC<{
|
||||
children?: React.ReactNode[]|React.ReactNode
|
||||
}>
|
||||
} & T>
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Button, TextField, Box } from '@mui/material';
|
||||
import { messageChannelContext } from './provider/MessageChannel';
|
||||
import Layout from './Layout';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Layout />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
import React from "react";
|
||||
import AuthButton from "./components/AuthButton";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import MainFrame from "./components/MainFrame/MainFrame";
|
||||
import LogoutButton from "./components/LogoutButton";
|
||||
import AddToQueue from "./components/AddToQueue/AddToQueue";
|
||||
import React from 'react';
|
||||
import AuthButton from './components/AuthButton';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import MainFrame from './components/MainFrame/MainFrame';
|
||||
import LogoutButton from './components/LogoutButton';
|
||||
import AddToQueue from './components/AddToQueue/AddToQueue';
|
||||
import { messageChannelContext } from './provider/MessageChannel';
|
||||
import { ClearAll, Folder } from "@mui/icons-material";
|
||||
import useStore from "./hooks/useStore";
|
||||
import StartQueueButton from "./components/StartQueue";
|
||||
import { ClearAll, Folder } from '@mui/icons-material';
|
||||
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 }}>
|
||||
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')}>Open Output Directory</Button>
|
||||
<Button variant="contained" startIcon={<ClearAll />} onClick={() => dispatch({ type: 'queue', payload: [], extraInfo: { force: true } })}>Clear Queue</Button>
|
||||
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() }>Clear Queue</Button>
|
||||
</Box>
|
||||
<AddToQueue />
|
||||
<StartQueueButton />
|
||||
</Box>
|
||||
<MainFrame />
|
||||
</Box>;
|
||||
}
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Container, Box, ThemeProvider, createTheme, Theme } from "@mui/material";
|
||||
import React from 'react';
|
||||
import { Container, Box, ThemeProvider, createTheme, Theme } from '@mui/material';
|
||||
|
||||
const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
|
||||
return createTheme({
|
||||
|
|
@ -11,11 +11,11 @@ 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>
|
||||
</ThemeProvider>;
|
||||
}
|
||||
};
|
||||
|
||||
export default Style;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { Add } from "@mui/icons-material";
|
||||
import { Box, Button, Dialog, Divider } from "@mui/material";
|
||||
import React from "react";
|
||||
import DownloadSelector from "./DownloadSelector/DownloadSelector";
|
||||
import EpisodeListing from "./DownloadSelector/Listing/EpisodeListing";
|
||||
import SearchBox from "./SearchBox/SearchBox";
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Box, Button, Dialog, Divider } from '@mui/material';
|
||||
import React from 'react';
|
||||
import DownloadSelector from './DownloadSelector/DownloadSelector';
|
||||
import EpisodeListing from './DownloadSelector/Listing/EpisodeListing';
|
||||
import SearchBox from './SearchBox/SearchBox';
|
||||
|
||||
const AddToQueue: React.FC = () => {
|
||||
const [isOpen, setOpen] = React.useState(false);
|
||||
|
|
@ -21,7 +21,7 @@ const AddToQueue: React.FC = () => {
|
|||
<Add />
|
||||
Add to Queue
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default AddToQueue;
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import React from "react";
|
||||
import { Backdrop, Box, Button, Checkbox, Chip, FormControl, FormControlLabel, IconButton, InputLabel, MenuItem, OutlinedInput, Select, TextField } from "@mui/material";
|
||||
import useStore from "../../../hooks/useStore";
|
||||
import MultiSelect from "../../reusable/MultiSelect";
|
||||
import { messageChannelContext } from "../../../provider/MessageChannel";
|
||||
import React from 'react';
|
||||
import { Box, Button, TextField } from '@mui/material';
|
||||
import useStore from '../../../hooks/useStore';
|
||||
import MultiSelect from '../../reusable/MultiSelect';
|
||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { useSnackbar } from "notistack";
|
||||
import { Folder } from "@mui/icons-material";
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
type DownloadSelectorProps = {
|
||||
onFinish?: () => unknown
|
||||
|
|
@ -48,22 +47,14 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
const addToQueue = async () => {
|
||||
setLoading(true);
|
||||
const res = await messageHandler?.resolveItems(store.downloadOptions);
|
||||
if (!res || !res.isOk) {
|
||||
console.error(res);
|
||||
setLoading(false);
|
||||
if (!res)
|
||||
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'queue',
|
||||
payload: res.value
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
if (onFinish)
|
||||
onFinish();
|
||||
}
|
||||
};
|
||||
|
||||
const listEpisodes = async () => {
|
||||
if (!store.downloadOptions.id) {
|
||||
|
|
@ -85,7 +76,7 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ m: 2, gap: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
|
|
@ -93,7 +84,7 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
dispatch({
|
||||
type: 'downloadOptions',
|
||||
payload: { ...store.downloadOptions, id: e.target.value }
|
||||
})
|
||||
});
|
||||
}} label='Item ID' />
|
||||
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
|
||||
const parsed = parseInt(e.target.value);
|
||||
|
|
@ -102,13 +93,13 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
dispatch({
|
||||
type: 'downloadOptions',
|
||||
payload: { ...store.downloadOptions, q: parsed }
|
||||
})
|
||||
});
|
||||
}} label='Quality Level (0 for max)' />
|
||||
<TextField disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
|
||||
dispatch({
|
||||
type: 'downloadOptions',
|
||||
payload: { ...store.downloadOptions, e: e.target.value }
|
||||
})
|
||||
});
|
||||
}} label='Episode Select' />
|
||||
<MultiSelect
|
||||
title='Dub Languages'
|
||||
|
|
@ -137,7 +128,7 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
dispatch({
|
||||
type: 'downloadOptions',
|
||||
payload: { ...store.downloadOptions, fileName: e.target.value }
|
||||
})
|
||||
});
|
||||
}} sx={{ width: '50%' }} label='Filename' />
|
||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download all</Button>
|
||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download all but</Button>
|
||||
|
|
@ -149,7 +140,7 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
<LoadingButton loading={loading} onClick={listEpisodes} variant='contained'>List episodes</LoadingButton>
|
||||
<LoadingButton loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default DownloadSelector;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Box, List, ListItem, Typography, Divider, Dialog, Select, MenuItem, FormControl, InputLabel, Checkbox } from "@mui/material";
|
||||
import { CheckBox, CheckBoxOutlineBlank } from '@mui/icons-material'
|
||||
import React from "react";
|
||||
import useStore from "../../../../hooks/useStore";
|
||||
import { Box, List, ListItem, Typography, Divider, Dialog, Select, MenuItem, FormControl, InputLabel, Checkbox } from '@mui/material';
|
||||
import { CheckBox, CheckBoxOutlineBlank } from '@mui/icons-material';
|
||||
import React from 'react';
|
||||
import useStore from '../../../../hooks/useStore';
|
||||
|
||||
|
||||
const EpisodeListing: React.FC = () => {
|
||||
|
|
@ -17,13 +17,13 @@ const EpisodeListing: React.FC = () => {
|
|||
s.push(season);
|
||||
}
|
||||
return s;
|
||||
}, [ store.episodeListing ])
|
||||
}, [ store.episodeListing ]);
|
||||
|
||||
const [selected, setSelected] = React.useState<string[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelected(parseSelect(store.downloadOptions.e));
|
||||
}, [ store.episodeListing ])
|
||||
}, [ store.episodeListing ]);
|
||||
|
||||
const close = () => {
|
||||
dispatch({
|
||||
|
|
@ -36,96 +36,96 @@ const EpisodeListing: React.FC = () => {
|
|||
...store.downloadOptions,
|
||||
e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}`
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return <Dialog open={store.episodeListing.length > 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px 20px' }}>
|
||||
<Typography color='text.primary' variant="h5" sx={{ textAlign: 'center', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px 20px' }}>
|
||||
<Typography color='text.primary' variant="h5" sx={{ textAlign: 'center', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||
Episodes
|
||||
</Typography>
|
||||
<FormControl sx={{ mr: 2, mt: 2 }}>
|
||||
<InputLabel id='seasonSelectLabel'>Season</InputLabel>
|
||||
<Select labelId="seasonSelectLabel" label='Season' value={season} onChange={(e) => setSeason(e.target.value)}>
|
||||
<MenuItem value='all'>Show all Epsiodes</MenuItem>
|
||||
{seasons.map((a, index) => {
|
||||
return <MenuItem value={a} key={`MenuItem_SeasonSelect_${index}`}>
|
||||
{a}
|
||||
</MenuItem>
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<List>
|
||||
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 1fr 5fr' }}>
|
||||
<Checkbox
|
||||
indeterminate={store.episodeListing.some(a => selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))}
|
||||
checked={store.episodeListing.every(a => selected.includes(a.e))}
|
||||
onChange={() => {
|
||||
if (selected.length > 0) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
setSelected(store.episodeListing.map(a => a.e));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{store.episodeListing.filter((a) => season === 'all' ? true : a.season === season).map((item, index, { length }) => {
|
||||
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
|
||||
const isSelected = selected.includes(e.toString());
|
||||
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`} sx={{
|
||||
backdropFilter: isSelected ? 'brightness(1.5)' : '',
|
||||
'&:hover': {
|
||||
backdropFilter: 'brightness(1.5)'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
let arr: string[] = [];
|
||||
if (isSelected) {
|
||||
arr = [...selected.filter(a => a !== e.toString())];
|
||||
} else {
|
||||
arr = [...selected, e.toString()];
|
||||
}
|
||||
setSelected(arr.filter(a => a.length > 0));
|
||||
}}>
|
||||
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}>
|
||||
{ isSelected ? <CheckBox /> : <CheckBoxOutlineBlank /> }
|
||||
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
|
||||
{e}
|
||||
</Typography>
|
||||
<img style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img}></img>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
|
||||
<Typography color='text.primary' variant="h5">
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography color='text.primary'>
|
||||
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography color='text.primary'>
|
||||
{item.description}
|
||||
</Typography>
|
||||
<FormControl sx={{ mr: 2, mt: 2 }}>
|
||||
<InputLabel id='seasonSelectLabel'>Season</InputLabel>
|
||||
<Select labelId="seasonSelectLabel" label='Season' value={season} onChange={(e) => setSeason(e.target.value)}>
|
||||
<MenuItem value='all'>Show all Epsiodes</MenuItem>
|
||||
{seasons.map((a, index) => {
|
||||
return <MenuItem value={a} key={`MenuItem_SeasonSelect_${index}`}>
|
||||
{a}
|
||||
</MenuItem>;
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<List>
|
||||
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 1fr 5fr' }}>
|
||||
<Checkbox
|
||||
indeterminate={store.episodeListing.some(a => selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))}
|
||||
checked={store.episodeListing.every(a => selected.includes(a.e))}
|
||||
onChange={() => {
|
||||
if (selected.length > 0) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
setSelected(store.episodeListing.map(a => a.e));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{store.episodeListing.filter((a) => season === 'all' ? true : a.season === season).map((item, index, { length }) => {
|
||||
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
|
||||
const isSelected = selected.includes(e.toString());
|
||||
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`} sx={{
|
||||
backdropFilter: isSelected ? 'brightness(1.5)' : '',
|
||||
'&:hover': {
|
||||
backdropFilter: 'brightness(1.5)'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
let arr: string[] = [];
|
||||
if (isSelected) {
|
||||
arr = [...selected.filter(a => a !== e.toString())];
|
||||
} else {
|
||||
arr = [...selected, e.toString()];
|
||||
}
|
||||
setSelected(arr.filter(a => a.length > 0));
|
||||
}}>
|
||||
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}>
|
||||
{ isSelected ? <CheckBox /> : <CheckBoxOutlineBlank /> }
|
||||
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
|
||||
{e}
|
||||
</Typography>
|
||||
<img style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img} alt="thumbnail" />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
|
||||
<Typography color='text.primary' variant="h5">
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography color='text.primary'>
|
||||
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'fit-content 1fr' }}>
|
||||
<Typography>
|
||||
<br />
|
||||
Available audio languages: {item.lang.join(', ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
{index < length - 1 && <Divider />}
|
||||
</Box>
|
||||
})}
|
||||
</List>
|
||||
</Dialog>
|
||||
}
|
||||
<Typography color='text.primary'>
|
||||
{item.description}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'fit-content 1fr' }}>
|
||||
<Typography>
|
||||
<br />
|
||||
Available audio languages: {item.lang.join(', ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
{index < length - 1 && <Divider />}
|
||||
</Box>;
|
||||
})}
|
||||
</List>
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
const parseSelect = (s: string): string[] => {
|
||||
const ret: string[] = [];
|
||||
s.split(',').forEach(item => {
|
||||
if (item.includes('-')) {
|
||||
let split = item.split('-');
|
||||
const split = item.split('-');
|
||||
if (split.length !== 2)
|
||||
return;
|
||||
const match = split[0].match(/[A-Za-z]+/);
|
||||
|
|
@ -156,8 +156,8 @@ const parseSelect = (s: string): string[] => {
|
|||
} else {
|
||||
ret.push(item);
|
||||
}
|
||||
})
|
||||
});
|
||||
return [...new Set(ret)];
|
||||
}
|
||||
};
|
||||
|
||||
export default EpisodeListing;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import React from "react";
|
||||
import { Box, ClickAwayListener, Divider, List, ListItem, Paper, TextField, Typography } from "@mui/material";
|
||||
import { SearchResponse } from "../../../../../../@types/messageHandler";
|
||||
import useStore from "../../../hooks/useStore";
|
||||
import { messageChannelContext } from "../../../provider/MessageChannel";
|
||||
import React from 'react';
|
||||
import { Box, ClickAwayListener, Divider, List, ListItem, Paper, TextField, Typography } from '@mui/material';
|
||||
import { SearchResponse } from '../../../../../../@types/messageHandler';
|
||||
import useStore from '../../../hooks/useStore';
|
||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||
import './SearchBox.css';
|
||||
import ContextMenu from "../../reusable/ContextMenu";
|
||||
import { useSnackbar } from "notistack";
|
||||
import ContextMenu from '../../reusable/ContextMenu';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
const SearchBox: React.FC = () => {
|
||||
const messageHandler = React.useContext(messageChannelContext);
|
||||
|
|
@ -52,13 +52,13 @@ const SearchBox: React.FC = () => {
|
|||
searchResult.value.map((a, ind, arr) => {
|
||||
const imageRef = React.createRef<HTMLImageElement>();
|
||||
return <Box key={a.id}>
|
||||
<ListItem className='listitem-hover' onClick={(e) => {
|
||||
<ListItem className='listitem-hover' onClick={() => {
|
||||
selectItem(a.id);
|
||||
setFocus(false);
|
||||
}}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ width: '20%', height: '100%', pr: 2 }}>
|
||||
<img ref={imageRef} src={a.image} style={{ width: '100%', height: '100%' }}/>
|
||||
<img ref={imageRef} src={a.image} style={{ width: '100%', height: 'auto' }} alt="thumbnail"/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '70%' }}>
|
||||
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
|
||||
|
|
@ -83,13 +83,13 @@ const SearchBox: React.FC = () => {
|
|||
});
|
||||
}} ]} popupItem={imageRef} />
|
||||
{(ind < arr.length - 1) && <Divider />}
|
||||
</Box>
|
||||
</Box>;
|
||||
})
|
||||
: <></>}
|
||||
: <></>}
|
||||
</List>
|
||||
</Paper>}
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
}
|
||||
</ClickAwayListener>;
|
||||
};
|
||||
|
||||
export default SearchBox;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material";
|
||||
import { Check, Close } from '@mui/icons-material'
|
||||
import React from "react";
|
||||
import { messageChannelContext } from "../provider/MessageChannel";
|
||||
import Require from "./Require";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material';
|
||||
import { Check, Close } from '@mui/icons-material';
|
||||
import React from 'react';
|
||||
import { messageChannelContext } from '../provider/MessageChannel';
|
||||
import Require from './Require';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
const AuthButton: React.FC = () => {
|
||||
const snackbar = useSnackbar();
|
||||
|
|
@ -24,9 +24,9 @@ const AuthButton: React.FC = () => {
|
|||
|
||||
const checkAuth = async () => {
|
||||
setAuthed((await messageChannel?.checkToken())?.isOk ?? false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => { checkAuth() }, []);
|
||||
React.useEffect(() => { checkAuth(); }, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!messageChannel)
|
||||
|
|
@ -52,7 +52,7 @@ const AuthButton: React.FC = () => {
|
|||
}
|
||||
await checkAuth();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <Require value={messageChannel}>
|
||||
<Dialog open={open}>
|
||||
|
|
@ -106,7 +106,7 @@ const AuthButton: React.FC = () => {
|
|||
</DialogActions>
|
||||
</Dialog>
|
||||
<Button startIcon={authed ? <Check />: <Close />} variant="contained" onClick={() => setOpen(true)}>Authenticate</Button>
|
||||
</Require>
|
||||
}
|
||||
</Require>;
|
||||
};
|
||||
|
||||
export default AuthButton;
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
import { ExitToApp } from "@mui/icons-material";
|
||||
import { Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import useStore from "../hooks/useStore";
|
||||
import { messageChannelContext } from "../provider/MessageChannel";
|
||||
import Require from "./Require";
|
||||
import { ExitToApp } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import React from 'react';
|
||||
import useStore from '../hooks/useStore';
|
||||
import { messageChannelContext } from '../provider/MessageChannel';
|
||||
import Require from './Require';
|
||||
|
||||
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
|
||||
})
|
||||
});
|
||||
else
|
||||
alert('Unable to change service');
|
||||
}
|
||||
};
|
||||
|
||||
return <Require value={messageChannel}>
|
||||
<Button
|
||||
|
|
@ -29,8 +29,8 @@ const LogoutButton: React.FC = () => {
|
|||
>
|
||||
Service select
|
||||
</Button>
|
||||
</Require>
|
||||
</Require>;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export default LogoutButton;
|
||||
|
|
@ -1,48 +1,40 @@
|
|||
import React from "react";
|
||||
import { ExtendedProgress } from "../../../../../../@types/messageHandler";
|
||||
import { RandomEvent } from "../../../../../../@types/randomEvents";
|
||||
import useStore from "../../../hooks/useStore";
|
||||
import { messageChannelContext } from "../../../provider/MessageChannel";
|
||||
import React from 'react';
|
||||
import { ExtendedProgress, QueueItem } from '../../../../../../@types/messageHandler';
|
||||
import { RandomEvent } from '../../../../../../@types/randomEvents';
|
||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||
|
||||
const useDownloadManager = () => {
|
||||
const [ { currentDownload }, dispatch ] = useStore();
|
||||
const messageHandler = React.useContext(messageChannelContext);
|
||||
|
||||
const [progressData, setProgressData] = React.useState<ExtendedProgress|undefined>();
|
||||
const [current, setCurrent] = React.useState<undefined|QueueItem>();
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (ev: RandomEvent<'progress'>) => {
|
||||
console.log(ev.data);
|
||||
setProgressData(ev.data);
|
||||
}
|
||||
messageHandler?.randomEvents.on('progress', handler);
|
||||
};
|
||||
|
||||
const currentHandler = (ev: RandomEvent<'current'>) => {
|
||||
setCurrent(ev.data);
|
||||
};
|
||||
|
||||
const finishHandler = () => {
|
||||
setProgressData(undefined);
|
||||
dispatch({
|
||||
type: 'finish',
|
||||
payload: undefined
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
messageHandler?.randomEvents.on('progress', handler);
|
||||
messageHandler?.randomEvents.on('current', currentHandler);
|
||||
messageHandler?.randomEvents.on('finish', finishHandler);
|
||||
|
||||
return () => {
|
||||
messageHandler?.randomEvents.removeListener('progress', handler);
|
||||
messageHandler?.randomEvents.removeListener('finish', finishHandler)
|
||||
messageHandler?.randomEvents.removeListener('finish', finishHandler);
|
||||
messageHandler?.randomEvents.removeListener('current', currentHandler);
|
||||
};
|
||||
}, [messageHandler, dispatch]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDownload)
|
||||
return;
|
||||
if (messageHandler?.isDownloading())
|
||||
return;
|
||||
console.log('start download');
|
||||
messageHandler?.downloadItem(currentDownload);
|
||||
}, [currentDownload, messageHandler]);
|
||||
}, [messageHandler]);
|
||||
|
||||
|
||||
return progressData;
|
||||
}
|
||||
return { data: progressData, current};
|
||||
};
|
||||
|
||||
export default useDownloadManager;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Box } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import './MainFrame.css';
|
||||
import Queue from "./Queue/Queue";
|
||||
import Queue from './Queue/Queue';
|
||||
|
||||
const MainFrame: React.FC = () => {
|
||||
return <Box sx={{ marginLeft: 1 }}>
|
||||
<Queue />
|
||||
</Box>
|
||||
}
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default MainFrame;
|
||||
|
|
@ -1,16 +1,23 @@
|
|||
import { Box, Button, Divider, LinearProgress, Skeleton, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import useStore from "../../../hooks/useStore";
|
||||
import { Box, Button, CircularProgress, Divider, LinearProgress, Skeleton, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||
import { queueContext } from '../../../provider/QueueProvider';
|
||||
|
||||
import useDownloadManager from "../DownloadManager/DownloadManager";
|
||||
import useDownloadManager from '../DownloadManager/DownloadManager';
|
||||
|
||||
const Queue: React.FC = () => {
|
||||
const data = useDownloadManager();
|
||||
const [{ queue, currentDownload }, dispatch] = useStore();
|
||||
const { data, current } = useDownloadManager();
|
||||
const queue = React.useContext(queueContext);
|
||||
const msg = React.useContext(messageChannelContext);
|
||||
|
||||
|
||||
if (!msg)
|
||||
return <>Never</>;
|
||||
|
||||
return data || queue.length > 0 ? <>
|
||||
{data && <>
|
||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
|
||||
<img src={data.downloadInfo.image} height='200px' width='100%' alt="Thumbnail" />
|
||||
<img src={data.downloadInfo.image} height='auto' width='100%' alt="Thumbnail" />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr max-content' }}>
|
||||
|
|
@ -36,27 +43,27 @@ const Queue: React.FC = () => {
|
|||
</>
|
||||
}
|
||||
{
|
||||
!data && currentDownload && <>
|
||||
current && !data && <>
|
||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
|
||||
<img src={currentDownload.image} height='200px' width='100%' alt="Thumbnail" />
|
||||
<img src={current.image} height='auto' width='100%' alt="Thumbnail" />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr max-content' }}>
|
||||
<Typography variant='h5' color='text.primary'>
|
||||
{currentDownload.title}
|
||||
{current.title}
|
||||
</Typography>
|
||||
<Typography variant='h5' color='text.primary'>
|
||||
Languages: {currentDownload.dubLang}
|
||||
Language: <CircularProgress variant="indeterminate" />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant='h6' color='text.primary'>
|
||||
{currentDownload.parent.title}
|
||||
{current.parent.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress variant='indeterminate' sx={{ height: '10px' }} />
|
||||
<Box>
|
||||
<Typography variant="body1" color='text.primary'>
|
||||
Waiting for download to start
|
||||
0 / ? parts (0% | X:XX | 0 MB/s | 0MB)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -67,7 +74,7 @@ const Queue: React.FC = () => {
|
|||
{queue.map((queueItem, index, { length }) => {
|
||||
return <Box key={`queue_item_${index}`}>
|
||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
|
||||
<img src={queueItem.image} height='200px' width='100%' alt="Thumbnail" />
|
||||
<img src={queueItem.image} height='auto' width='100%' alt="Thumbnail" />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px' }}>
|
||||
|
|
@ -87,15 +94,7 @@ const Queue: React.FC = () => {
|
|||
Quality: {queueItem.q}
|
||||
</Typography>
|
||||
<Button onClick={() => {
|
||||
const override = [...queue];
|
||||
override.splice(index, 1);
|
||||
dispatch({
|
||||
type: 'queue',
|
||||
payload: override,
|
||||
extraInfo: {
|
||||
force: true
|
||||
}
|
||||
});
|
||||
msg.removeFromQueue(index);
|
||||
}} sx={{ position: 'relative', left: '50%', transform: 'translateX(-50%)', width: '60%' }} variant="outlined" color="warning">
|
||||
Remove from Queue
|
||||
</Button>
|
||||
|
|
@ -115,8 +114,8 @@ const Queue: React.FC = () => {
|
|||
<Skeleton variant='text' height={'100%'} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</Box>;
|
||||
};
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
time = Math.floor(time / 1000);
|
||||
|
|
@ -124,6 +123,6 @@ const formatTime = (time: number) => {
|
|||
time = time % 60;
|
||||
|
||||
return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`;
|
||||
}
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
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;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Box, Backdrop, CircularProgress } from "@mui/material";
|
||||
import React from 'react';
|
||||
import { Box, Backdrop, CircularProgress } from '@mui/material';
|
||||
|
||||
export type RequireType<T> = {
|
||||
value?: T
|
||||
|
|
@ -8,7 +8,7 @@ export type RequireType<T> = {
|
|||
const Require = <T, >(props: React.PropsWithChildren<RequireType<T>>) => {
|
||||
return props.value === undefined ? <Backdrop open>
|
||||
<CircularProgress />
|
||||
</Backdrop> : <Box>{props.children}</Box>
|
||||
}
|
||||
</Backdrop> : <Box>{props.children}</Box>;
|
||||
};
|
||||
|
||||
export default Require;
|
||||
|
|
@ -1,35 +1,41 @@
|
|||
import { ExitToApp, PauseCircleFilled, PlayCircleFilled } from "@mui/icons-material";
|
||||
import { Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import useStore from "../hooks/useStore";
|
||||
import { messageChannelContext } from "../provider/MessageChannel";
|
||||
import Require from "./Require";
|
||||
import { PauseCircleFilled, PlayCircleFilled } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { messageChannelContext } from '../provider/MessageChannel';
|
||||
import Require from './Require';
|
||||
|
||||
const StartQueueButton: React.FC = () => {
|
||||
const messageChannel = React.useContext(messageChannelContext);
|
||||
const [store, dispatch] = useStore();
|
||||
const [start, setStart] = React.useState(false);
|
||||
const msg = React.useContext(messageChannelContext);
|
||||
|
||||
const change = () => {
|
||||
if (messageChannel?.isDownloading() && store.downloadQueue)
|
||||
alert("The current download will be finished before the queue stops")
|
||||
dispatch({
|
||||
type: 'downloadQueue',
|
||||
payload: !store.downloadQueue
|
||||
})
|
||||
}
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (!msg)
|
||||
return alert('Invalid state: msg not found');
|
||||
setStart(await msg.getDownloadQueue());
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const change = async () => {
|
||||
if (await messageChannel?.isDownloading())
|
||||
alert('The current download will be finished before the queue stops');
|
||||
msg?.setDownloadQueue(!start);
|
||||
setStart(!start);
|
||||
};
|
||||
|
||||
return <Require value={messageChannel}>
|
||||
<Button
|
||||
startIcon={store.downloadQueue ? <PauseCircleFilled /> : <PlayCircleFilled /> }
|
||||
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
|
||||
variant='contained'
|
||||
onClick={change}
|
||||
>
|
||||
{
|
||||
store.downloadQueue ? 'Stop Queue' : 'Start Queue'
|
||||
start ? 'Stop Queue' : 'Start Queue'
|
||||
}
|
||||
</Button>
|
||||
</Require>
|
||||
</Require>;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export default StartQueueButton;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { StyledOptions } from "@emotion/styled";
|
||||
import { Box, Button, Card, Divider, List, Typography, SxProps } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Box, Button, Divider, List, SxProps } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export type Option = {
|
||||
text: string,
|
||||
|
|
@ -35,32 +34,32 @@ function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
|
|||
ev.preventDefault();
|
||||
setAnchor({ x: ev.x + 10, y: ev.y + 10 });
|
||||
setShow(true);
|
||||
}
|
||||
};
|
||||
ref.current.addEventListener('contextmenu', listener);
|
||||
|
||||
return () => {
|
||||
if (ref.current)
|
||||
ref.current.removeEventListener('contextmenu', listener)
|
||||
ref.current.removeEventListener('contextmenu', listener);
|
||||
};
|
||||
}, [ props.popupItem ])
|
||||
}, [ props.popupItem ]);
|
||||
|
||||
return show ? <Box sx={{ zIndex: 9999, p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
|
||||
<List sx={{ p: 0, m: 0 }}>
|
||||
{props.options.map((item, i) => {
|
||||
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
|
||||
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
|
||||
item.onClick();
|
||||
setShow(false);
|
||||
}} sx={buttonSx}>
|
||||
{item.text}
|
||||
</Button>
|
||||
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
|
||||
item.onClick();
|
||||
setShow(false);
|
||||
}} sx={buttonSx}>
|
||||
{item.text}
|
||||
</Button>;
|
||||
})}
|
||||
<Divider />
|
||||
<Button fullWidth color='inherit' onClick={() => setShow(false)} sx={buttonSx} >
|
||||
Close
|
||||
</Button>
|
||||
</List>
|
||||
</Box> : <></>
|
||||
</Box> : <></>;
|
||||
}
|
||||
|
||||
export default ContextMenu;
|
||||
|
|
@ -19,6 +19,6 @@ const LinearProgressWithLabel: React.FC<LinearProgressWithLabelProps> = (props)
|
|||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LinearProgressWithLabel;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from "@mui/material";
|
||||
import React from 'react';
|
||||
import { FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from '@mui/material';
|
||||
|
||||
export type MultiSelectProps = {
|
||||
values: string[],
|
||||
|
|
@ -41,7 +41,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
|
|||
multiple
|
||||
value={(props.selected ?? [])}
|
||||
onChange={e => {
|
||||
const val = typeof e.target.value === "string" ? e.target.value.split(",") : e.target.value;
|
||||
const val = typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value;
|
||||
if (props.allOption && val.includes('all')) {
|
||||
if (props.values.length === val.length - 1)
|
||||
props.onChange([]);
|
||||
|
|
@ -68,7 +68,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
|
|||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { StoreAction, StoreContext, StoreState } from "../provider/Store";
|
||||
import React from 'react';
|
||||
import { StoreAction, StoreContext, StoreState } from '../provider/Store';
|
||||
|
||||
const useStore = () => {
|
||||
const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch<StoreAction<keyof StoreState>>]>);
|
||||
|
|
@ -7,6 +7,6 @@ const useStore = () => {
|
|||
throw new Error('useStore must be used under Store');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
};
|
||||
|
||||
export default useStore;
|
||||
|
|
@ -4,11 +4,12 @@ import App from './App';
|
|||
import ServiceProvider from './provider/ServiceProvider';
|
||||
import Style from './Style';
|
||||
import MessageChannel from './provider/MessageChannel';
|
||||
import { IconButton } from "@mui/material";
|
||||
import { CloseOutlined } from "@mui/icons-material";
|
||||
import { IconButton } from '@mui/material';
|
||||
import { CloseOutlined } from '@mui/icons-material';
|
||||
import { SnackbarProvider, SnackbarKey } from 'notistack';
|
||||
import Store from './provider/Store';
|
||||
import ErrorHandler from './provider/ErrorHandler';
|
||||
import QueueProvider from './provider/QueueProvider';
|
||||
|
||||
const notistackRef = React.createRef<SnackbarProvider>();
|
||||
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
|
||||
|
|
@ -17,28 +18,28 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => {
|
|||
};
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
const root = createRoot(container as HTMLElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ErrorHandler>
|
||||
<Store>
|
||||
<SnackbarProvider
|
||||
ref={notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={onClickDismiss(key)} color="inherit">
|
||||
<CloseOutlined />
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<Style>
|
||||
<MessageChannel>
|
||||
<ServiceProvider>
|
||||
<ErrorHandler>
|
||||
<Store>
|
||||
<SnackbarProvider
|
||||
ref={notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={onClickDismiss(key)} color="inherit">
|
||||
<CloseOutlined />
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<Style>
|
||||
<MessageChannel>
|
||||
<ServiceProvider>
|
||||
<QueueProvider>
|
||||
<App />
|
||||
</ServiceProvider>
|
||||
</MessageChannel>
|
||||
</Style>
|
||||
</SnackbarProvider>
|
||||
</Store>
|
||||
</ErrorHandler>
|
||||
</React.StrictMode>
|
||||
</QueueProvider>
|
||||
</ServiceProvider>
|
||||
</MessageChannel>
|
||||
</Style>
|
||||
</SnackbarProvider>
|
||||
</Store>
|
||||
</ErrorHandler>
|
||||
);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Backdrop, Box, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export default class ErrorHandler extends React.Component<{
|
||||
children: React.ReactNode|React.ReactNode[]
|
||||
|
|
@ -14,26 +14,26 @@ export default class ErrorHandler extends React.Component<{
|
|||
children: React.ReactNode|React.ReactNode[]
|
||||
}) {
|
||||
super(props);
|
||||
this.state = { error: undefined }
|
||||
this.state = { error: undefined };
|
||||
}
|
||||
|
||||
componentDidCatch(er: Error, stack: React.ErrorInfo) {
|
||||
this.setState({ error: { er, stack } })
|
||||
this.setState({ error: { er, stack } });
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return this.state.error ?
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', p: 2 }}>
|
||||
<Typography variant='body1' color='red'>
|
||||
{`${this.state.error.er.name}: ${this.state.error.er.message}`}
|
||||
<br/>
|
||||
{this.state.error.stack.componentStack.split('\n').map(a => {
|
||||
return <>
|
||||
{a}
|
||||
<br/>
|
||||
</>
|
||||
})}
|
||||
</Typography>
|
||||
</Box> : this.props.children;
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', p: 2 }}>
|
||||
<Typography variant='body1' color='red'>
|
||||
{`${this.state.error.er.name}: ${this.state.error.er.message}`}
|
||||
<br/>
|
||||
{this.state.error.stack.componentStack.split('\n').map(a => {
|
||||
return <>
|
||||
{a}
|
||||
<br/>
|
||||
</>;
|
||||
})}
|
||||
</Typography>
|
||||
</Box> : this.props.children;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,24 @@
|
|||
import React from 'react';
|
||||
import type { MessageHandler } from '../../../../@types/messageHandler';
|
||||
import type { IpcRenderer, IpcRendererEvent } from "electron";
|
||||
import { MessageHandler } 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 { Avatar, Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { v4 } from 'uuid';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { LockOutlined, PowerSettingsNew } from '@mui/icons-material';
|
||||
import { GUIConfig } from '../../../../modules/module.cfg-loader';
|
||||
|
||||
|
||||
export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => boolean });
|
||||
export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> });
|
||||
|
||||
export class RandomEventHandler {
|
||||
private handler: {
|
||||
[eventName in keyof RandomEvents]: Handler<eventName>[]
|
||||
} = {
|
||||
progress: [],
|
||||
finish: []
|
||||
finish: [],
|
||||
queueChange: [],
|
||||
current: []
|
||||
};
|
||||
|
||||
public on<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
|
||||
|
|
@ -36,54 +40,200 @@ 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>();
|
||||
const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>();
|
||||
const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting');
|
||||
const [isSetuped, setIsSetuped] = React.useState<'waiting'|'yes'|'no'>('waiting');
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
React.useEffect(() => {
|
||||
const wss = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
|
||||
wss.addEventListener('open', () => {
|
||||
setPublicWS(wss);
|
||||
});
|
||||
wss.addEventListener('error', () => {
|
||||
enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' });
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (!publicWS)
|
||||
return;
|
||||
setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no');
|
||||
setIsSetuped((await messageAndResponse(publicWS, { name: 'setuped', data: undefined })).data ? 'yes' : 'no');
|
||||
})();
|
||||
}, [publicWS]);
|
||||
|
||||
const connect = (ev?: React.FormEvent<HTMLFormElement>) => {
|
||||
let search = new URLSearchParams();
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
const formData = new FormData(ev.currentTarget);
|
||||
const password = formData.get('password')?.toString();
|
||||
if (!password)
|
||||
return enqueueSnackbar('Please provide both a username and password', {
|
||||
variant: 'error'
|
||||
});
|
||||
search = new URLSearchParams({
|
||||
password
|
||||
});
|
||||
}
|
||||
|
||||
const wws = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/ws?${search}`, );
|
||||
wws.addEventListener('open', () => {
|
||||
console.log('[INFO] [WS] Connected');
|
||||
setSocket(wws);
|
||||
});
|
||||
wws.addEventListener('error', (er) => {
|
||||
console.error('[ERROR] [WS]', er);
|
||||
enqueueSnackbar('Unable to connect to server. Please check the password and try again.', {
|
||||
variant: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const setup = async (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (!socket)
|
||||
return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' });
|
||||
const formData = new FormData(ev.currentTarget);
|
||||
const password = formData.get('password');
|
||||
const data = {
|
||||
port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000,
|
||||
password: password ? password.toString() : undefined
|
||||
} as GUIConfig;
|
||||
await messageAndResponse(socket, { name: 'setupServer', data });
|
||||
enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, {
|
||||
variant: 'success',
|
||||
persist: true
|
||||
});
|
||||
enqueueSnackbar('Please restart the server now.', {
|
||||
variant: 'info',
|
||||
persist: true
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
return () => {
|
||||
ipcRenderer.removeListener('randomEvent', listener);
|
||||
};
|
||||
}, [ ipcRenderer ]);
|
||||
socket.addEventListener('message', listener);
|
||||
return () => {
|
||||
socket.removeEventListener('message', listener);
|
||||
};
|
||||
}, [ socket ]);
|
||||
|
||||
if (usePassword === 'waiting')
|
||||
return <></>;
|
||||
|
||||
if (socket === undefined) {
|
||||
if (usePassword === 'no') {
|
||||
connect(undefined);
|
||||
return <></>;
|
||||
}
|
||||
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||
<LockOutlined />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5" color="text.primary">
|
||||
Login
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={connect} sx={{ mt: 1 }}>
|
||||
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" required label={'Password'} />
|
||||
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Login</Button>
|
||||
<Typography color="text.secondary" align='center' component="p" variant='body2'>
|
||||
You need to login in order to use this tool.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
if (isSetuped === 'no') {
|
||||
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||
<PowerSettingsNew />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5" color="text.primary">
|
||||
Confirm
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={setup} sx={{ mt: 1 }}>
|
||||
<TextField name="port" margin='normal' type="number" fullWidth variant="filled" required label={'Port'} defaultValue={3000} />
|
||||
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" label={'Password'} />
|
||||
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Confirm</Button>
|
||||
<Typography color="text.secondary" align='center' component="p" variant='body2'>
|
||||
Please enter data that will be set to use this tool.
|
||||
<br />
|
||||
Leave blank to use no password (NOT RECOMMENDED)!
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
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 }),
|
||||
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
|
||||
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
|
||||
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
|
||||
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
|
||||
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data,
|
||||
};
|
||||
|
||||
return <messageChannelContext.Provider value={messageHandler}>
|
||||
{children}
|
||||
|
|
|
|||
35
gui/react/src/provider/QueueProvider.tsx
Normal file
35
gui/react/src/provider/QueueProvider.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { QueueItem } from '../../../../@types/messageHandler';
|
||||
import { messageChannelContext } from './MessageChannel';
|
||||
import { RandomEvent } from '../../../../@types/randomEvents';
|
||||
|
||||
export const queueContext = React.createContext<QueueItem[]>([]);
|
||||
|
||||
const QueueProvider: FCWithChildren = ({ children }) => {
|
||||
const msg = React.useContext(messageChannelContext);
|
||||
|
||||
const [ready, setReady] = React.useState(false);
|
||||
const [queue, setQueue] = React.useState<QueueItem[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (msg && !ready) {
|
||||
msg.getQueue().then(data => {
|
||||
setQueue(data);
|
||||
setReady(true);
|
||||
});
|
||||
}
|
||||
const listener = (ev: RandomEvent<'queueChange'>) => {
|
||||
setQueue(ev.data);
|
||||
};
|
||||
msg?.randomEvents.on('queueChange', listener);
|
||||
return () => {
|
||||
msg?.randomEvents.removeListener('queueChange', listener);
|
||||
};
|
||||
}, [ msg ]);
|
||||
|
||||
return <queueContext.Provider value={queue}>
|
||||
{children}
|
||||
</queueContext.Provider>;
|
||||
};
|
||||
|
||||
export default QueueProvider;
|
||||
|
|
@ -15,7 +15,7 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
|
|||
type: 'service',
|
||||
payload: s
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return service === undefined ?
|
||||
<Box>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Episode, QueueItem } from '../../../../@types/messageHandler';
|
||||
import { Episode } from '../../../../@types/messageHandler';
|
||||
import { dubLanguageCodes } from '../../../../modules/module.langsData';
|
||||
|
||||
export type DownloadOptions = {
|
||||
|
|
@ -17,13 +17,9 @@ export type DownloadOptions = {
|
|||
}
|
||||
|
||||
export type StoreState = {
|
||||
downloadQueue: boolean,
|
||||
queue: QueueItem[],
|
||||
episodeListing: Episode[];
|
||||
downloadOptions: DownloadOptions,
|
||||
service: 'crunchy'|'funi'|undefined,
|
||||
currentDownload?: QueueItem,
|
||||
finish?: undefined
|
||||
}
|
||||
|
||||
export type StoreAction<T extends (keyof StoreState)> = {
|
||||
|
|
@ -34,36 +30,12 @@ export type StoreAction<T extends (keyof StoreState)> = {
|
|||
|
||||
const Reducer = <T extends keyof StoreState,>(state: StoreState, action: StoreAction<T>): StoreState => {
|
||||
switch(action.type) {
|
||||
case "queue":
|
||||
state.queue = action.extraInfo?.force ? action.payload as QueueItem[] : state.queue.concat(action.payload as QueueItem[]);
|
||||
if (state.currentDownload === undefined && state.queue.length > 0 && state.downloadQueue) {
|
||||
state.currentDownload = state.queue[0];
|
||||
state.queue = state.queue.slice(1);
|
||||
}
|
||||
return { ...state };
|
||||
case "finish":
|
||||
if (state.queue.length > 0 && state.downloadQueue) {
|
||||
state.currentDownload = state.queue[0];
|
||||
state.queue = state.queue.slice(1);
|
||||
} else {
|
||||
state.currentDownload = undefined;
|
||||
}
|
||||
return { ...state }
|
||||
case 'downloadQueue':
|
||||
state.downloadQueue = action.payload as boolean;
|
||||
if (state.queue.length > 0 && state.downloadQueue && state.currentDownload === undefined) {
|
||||
state.currentDownload = state.queue[0];
|
||||
state.queue = state.queue.slice(1);
|
||||
}
|
||||
return {...state}
|
||||
default:
|
||||
return { ...state, [action.type]: action.payload }
|
||||
default:
|
||||
return { ...state, [action.type]: action.payload };
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: StoreState = {
|
||||
downloadQueue: false,
|
||||
queue: [],
|
||||
downloadOptions: {
|
||||
id: '',
|
||||
q: 0,
|
||||
|
|
@ -110,5 +82,5 @@ const Store: FCWithChildren = ({children}) => {
|
|||
};
|
||||
|
||||
/* Importent Notice -- The 'queue' generic will be overriden */
|
||||
export const StoreContext = React.createContext<[StoreState, React.Dispatch<StoreAction<'queue'>>]>([initialState, undefined as any]);
|
||||
export const StoreContext = React.createContext<[StoreState, React.Dispatch<StoreAction<'downloadOptions'>>]>([initialState, undefined as any]);
|
||||
export default Store;
|
||||
31
gui/server/index.ts
Normal file
31
gui/server/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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';
|
||||
import { PublicWebSocket } from './websocket';
|
||||
import { console } from '../../modules/log';
|
||||
|
||||
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.info(`GUI server started on port ${cfg.gui.port}`);
|
||||
});
|
||||
|
||||
new PublicWebSocket(server);
|
||||
new ServiceHandler(server);
|
||||
|
||||
open(`http://localhost:${cfg.gui.port}`);
|
||||
126
gui/server/serviceHandler.ts
Normal file
126
gui/server/serviceHandler.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { ServerResponse } from 'http';
|
||||
import { Server } from 'http';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { MessageHandler } from '../../@types/messageHandler';
|
||||
import Funi from '../../funi';
|
||||
import { setSetuped, writeYamlCfgFile } from '../../modules/module.cfg-loader';
|
||||
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('setupServer', ({ data }, respond) => {
|
||||
writeYamlCfgFile('gui', data);
|
||||
setSetuped(true);
|
||||
respond(true);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
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(false);
|
||||
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('getQueue', async (_, respond) => {
|
||||
respond(await this.service?.getQueue() ?? []);
|
||||
});
|
||||
this.ws.events.on('removeFromQueue', async ({ data }, respond) => {
|
||||
this.service?.removeFromQueue(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('clearQueue', async (_, respond) => {
|
||||
this.service?.clearQueue();
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('setDownloadQueue', async ({ data }, respond) => {
|
||||
this.service?.setDownloadQueue(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('getDownloadQueue', async (_, respond) => {
|
||||
respond(await this.service?.getDownloadQueue() ?? false);
|
||||
});
|
||||
this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false));
|
||||
}
|
||||
|
||||
}
|
||||
130
gui/server/services/base.ts
Normal file
130
gui/server/services/base.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { DownloadInfo, FolderTypes, ProgressData, QueueItem } 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';
|
||||
import { console } from '../../../modules/log';
|
||||
|
||||
export default class Base {
|
||||
|
||||
constructor(private ws: WebSocketHandler) {}
|
||||
|
||||
private downloading = false;
|
||||
|
||||
private queue: QueueItem[] = [];
|
||||
private workOnQueue = false;
|
||||
|
||||
setDownloading(downloading: boolean) {
|
||||
this.downloading = downloading;
|
||||
}
|
||||
|
||||
getDownloading() {
|
||||
return this.downloading;
|
||||
}
|
||||
|
||||
alertError(error: Error) {
|
||||
console.error(`${error}`);
|
||||
}
|
||||
|
||||
makeProgressHandler(videoInfo: DownloadInfo) {
|
||||
return ((data: ProgressData) => {
|
||||
this.sendMessage({
|
||||
name: 'progress',
|
||||
data: {
|
||||
downloadInfo: videoInfo,
|
||||
progress: data
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public async getQueue(): Promise<QueueItem[]> {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
public async removeFromQueue(index: number) {
|
||||
this.queue.splice(index, 1);
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public async clearQueue() {
|
||||
this.queue = [];
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public addToQueue(data: QueueItem[]) {
|
||||
this.queue = this.queue.concat(...data);
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public setDownloadQueue(data: boolean) {
|
||||
this.workOnQueue = data;
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public async getDownloadQueue(): Promise<boolean> {
|
||||
return this.workOnQueue;
|
||||
}
|
||||
|
||||
private async queueChange() {
|
||||
this.sendMessage({ name: 'queueChange', data: this.queue });
|
||||
if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) {
|
||||
this.setDownloading(true);
|
||||
this.sendMessage({ name: 'current', data: this.queue[0] });
|
||||
this.downloadItem(this.queue[0]);
|
||||
this.queue = this.queue.slice(1);
|
||||
this.queueChange();
|
||||
}
|
||||
}
|
||||
|
||||
public async onFinish() {
|
||||
this.sendMessage({ name: 'current', data: undefined });
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
//Overriten
|
||||
// eslint-disable-next-line
|
||||
public async downloadItem(_: QueueItem) {
|
||||
throw new Error('downloadItem not overriden');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +1,114 @@
|
|||
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, ResolveItemsData, 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';
|
||||
import { console } from '../../../modules/log';
|
||||
|
||||
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<boolean> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
||||
if (!res.isOk)
|
||||
return res.isOk;
|
||||
this.addToQueue(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
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.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.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);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default CrunchyHandler;
|
||||
|
|
@ -1,115 +1,118 @@
|
|||
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, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, 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';
|
||||
import { console } from '../../../modules/log';
|
||||
|
||||
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<boolean> {
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
|
||||
if (!res.isOk)
|
||||
return res.isOk;
|
||||
this.addToQueue(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,
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.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: QueueItem) {
|
||||
this.setDownloading(true);
|
||||
console.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);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default FunimationHandler;
|
||||
122
gui/server/websocket.ts
Normal file
122
gui/server/websocket.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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 '.';
|
||||
import { isSetuped } from '../../modules/module.cfg-loader';
|
||||
import { console } from '../../modules/log';
|
||||
|
||||
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.info(`[WS] Connection from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.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.error(`[WS] ${er}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!this.wsServer.shouldHandle(request))
|
||||
return;
|
||||
if (!this.authenticate(request)) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`);
|
||||
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.error(`[WS] ${er}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private authenticate(request: IncomingMessage): boolean {
|
||||
const search = new URL(`http://${request.headers.host}${request.url}`).searchParams;
|
||||
return cfg.gui.password === (search.get('password') ?? undefined);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PublicWebSocket {
|
||||
private wsServer: ws.Server;
|
||||
|
||||
constructor(server: Server) {
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' });
|
||||
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (msg) => {
|
||||
const data = JSON.parse(msg.toString()) as UnknownWSMessage;
|
||||
switch (data.name) {
|
||||
case 'setuped':
|
||||
this.send(socket, data.id, data.name, isSetuped());
|
||||
break;
|
||||
case 'requirePassword':
|
||||
this.send(socket, data.id, data.name, cfg.gui.password !== undefined);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!this.wsServer.shouldHandle(request))
|
||||
return;
|
||||
this.wsServer.handleUpgrade(request, socket, head, socket => {
|
||||
this.wsServer.emit('connection', socket, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private send(client: ws.WebSocket, id: string, name: string, data: any) {
|
||||
client.send(JSON.stringify({
|
||||
data,
|
||||
id,
|
||||
name
|
||||
}), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
15
index.ts
15
index.ts
|
|
@ -1,3 +1,4 @@
|
|||
import { console } from './modules/log';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { appArgv, overrideArguments } from './modules/module.app-args';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
|
|
@ -12,29 +13,29 @@ import update from './modules/module.updater';
|
|||
await update(argv.update);
|
||||
|
||||
if (argv.all && argv.but) {
|
||||
console.log('[ERROR] --all and --but exclude each other!');
|
||||
console.error('--all and --but exclude each other!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (argv.addArchive) {
|
||||
if (argv.service === 'funi') {
|
||||
if (argv.s === undefined)
|
||||
return console.log('[ERROR] `-s` not found');
|
||||
return console.error('`-s` not found');
|
||||
addToArchive({
|
||||
service: 'funi',
|
||||
type: 's'
|
||||
}, argv.s);
|
||||
console.log('[INFO] Added %s to the downloadArchive list', argv.s);
|
||||
console.info('Added %s to the downloadArchive list', argv.s);
|
||||
} else if (argv.service === 'crunchy') {
|
||||
if (argv.s === undefined && argv.series === undefined)
|
||||
return console.log('[ERROR] `-s` or `--srz` not found');
|
||||
return console.error('`-s` or `--srz` not found');
|
||||
if (argv.s && argv.series)
|
||||
return console.log('[ERROR] Both `-s` and `--srz` found');
|
||||
return console.error('Both `-s` and `--srz` found');
|
||||
addToArchive({
|
||||
service: 'crunchy',
|
||||
type: argv.s === undefined ? 'srz' : 's'
|
||||
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||
console.log('[INFO] Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||
}
|
||||
} else if (argv.downloadArchive) {
|
||||
const ids = makeCommand(argv.service);
|
||||
|
|
@ -45,7 +46,7 @@ import update from './modules/module.updater';
|
|||
if (key.endsWith('crunchy.js') || key.endsWith('funi.js'))
|
||||
delete require.cache[key];
|
||||
});
|
||||
const service = new (argv.service === 'funi' ? (await import('./funi')).default : (await import('./crunchy')).default) as ServiceClass;
|
||||
const service = new (argv.service === 'funi' ? (await import('./funi')).default : (await import('./crunchy')).default)() as ServiceClass;
|
||||
await service.cli();
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import pkg from '../package.json';
|
|||
import modulesCleanup from 'removeNPMAbsolutePaths';
|
||||
import { exec } from 'pkg';
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import { console } from './log';
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const nodeVer = 'node16-';
|
||||
|
|
@ -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)){
|
||||
|
|
@ -80,23 +30,23 @@ async function buildBinary(buildType: BuildTypes) {
|
|||
if(!fs.existsSync(buildsDir)){
|
||||
fs.mkdirSync(buildsDir);
|
||||
}
|
||||
const buildFull = `${buildStr}-${buildType}-cli`;
|
||||
const buildFull = `${buildStr}-${buildType}-${gui ? 'gui' : 'cli'}`;
|
||||
const buildDir = `${buildsDir}/${buildFull}`;
|
||||
if(fs.existsSync(buildDir)){
|
||||
fs.removeSync(buildDir);
|
||||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
const buildConfig = [
|
||||
pkg.main,
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
'--target', nodeVer + getTarget(buildType),
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
];
|
||||
console.log(`[Build] Build configuration: ${buildFull}`);
|
||||
console.info(`[Build] Build configuration: ${buildFull}`);
|
||||
try {
|
||||
await exec(buildConfig);
|
||||
}
|
||||
catch(e){
|
||||
console.log(e);
|
||||
console.info(e);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.mkdirSync(`${buildDir}/config`);
|
||||
|
|
@ -104,11 +54,16 @@ async function buildBinary(buildType: BuildTypes) {
|
|||
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
|
||||
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
|
||||
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
||||
fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`);
|
||||
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
|
||||
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
|
||||
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`);
|
||||
}
|
||||
|
|
|
|||
442
modules/hls-download.ts
Normal file
442
modules/hls-download.ts
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
// build-in
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import url from 'url';
|
||||
|
||||
// extra
|
||||
import shlp from 'sei-helper';
|
||||
import got, { Response } from 'got';
|
||||
|
||||
import { console } from './log';
|
||||
import { ProgressData } from '../@types/messageHandler';
|
||||
|
||||
// The following function should fix an issue with downloading. For more information see https://github.com/sindresorhus/got/issues/1489
|
||||
const fixMiddleWare = (res: Response) => {
|
||||
const isResponseOk = (response: Response) => {
|
||||
const {statusCode} = response;
|
||||
const limitStatusCode = response.request.options.followRedirect ? 299 : 399;
|
||||
|
||||
return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304;
|
||||
};
|
||||
if (isResponseOk(res)) {
|
||||
res.request.destroy();
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export type HLSCallback = (data: ProgressData) => unknown;
|
||||
|
||||
type M3U8Json = {
|
||||
segments: Record<string, unknown>[],
|
||||
mediaSequence?: number,
|
||||
}
|
||||
|
||||
type Segment = {
|
||||
uri: string
|
||||
key: Key,
|
||||
byterange?: {
|
||||
offset: number,
|
||||
length: number
|
||||
}
|
||||
}
|
||||
|
||||
type Key = {
|
||||
uri: string,
|
||||
iv: number[]
|
||||
}
|
||||
|
||||
export type HLSOptions = {
|
||||
m3u8json: M3U8Json,
|
||||
output?: string,
|
||||
threads?: number,
|
||||
retries?: number,
|
||||
offset?: number,
|
||||
baseurl?: string,
|
||||
skipInit?: boolean,
|
||||
timeout?: number,
|
||||
fsRetryTime?: number,
|
||||
override?: 'Y'|'y'|'N'|'n'|'C'|'c'
|
||||
callback?: HLSCallback
|
||||
}
|
||||
|
||||
type Data = {
|
||||
parts: {
|
||||
first: number,
|
||||
total: number,
|
||||
completed: number
|
||||
},
|
||||
m3u8json: M3U8Json,
|
||||
outputFile: string,
|
||||
threads: number,
|
||||
retries: number,
|
||||
offset: number,
|
||||
baseurl?: string
|
||||
skipInit?: boolean,
|
||||
keys: {
|
||||
[uri: string]: Buffer|string
|
||||
},
|
||||
timeout: number,
|
||||
checkPartLength: boolean,
|
||||
isResume: boolean,
|
||||
bytesDownloaded: number,
|
||||
waitTime: number,
|
||||
callback?: HLSCallback,
|
||||
override?: string,
|
||||
dateStart: number
|
||||
}
|
||||
|
||||
// hls class
|
||||
class hlsDownload {
|
||||
private data: Data;
|
||||
constructor(options: HLSOptions){
|
||||
// check playlist
|
||||
if(
|
||||
!options
|
||||
|| !options.m3u8json
|
||||
|| !options.m3u8json.segments
|
||||
|| options.m3u8json.segments.length === 0
|
||||
){
|
||||
throw new Error('Playlist is empty!');
|
||||
}
|
||||
// init options
|
||||
this.data = {
|
||||
parts: {
|
||||
first: options.m3u8json.mediaSequence || 0,
|
||||
total: options.m3u8json.segments.length,
|
||||
completed: 0,
|
||||
},
|
||||
m3u8json: options.m3u8json,
|
||||
outputFile: options.output || 'stream.ts',
|
||||
threads: options.threads || 5,
|
||||
retries: options.retries || 4,
|
||||
offset: options.offset || 0,
|
||||
baseurl: options.baseurl,
|
||||
skipInit: options.skipInit,
|
||||
keys: {},
|
||||
timeout: options.timeout ? options.timeout : 60 * 1000,
|
||||
checkPartLength: true,
|
||||
isResume: options.offset ? options.offset > 0 : false,
|
||||
bytesDownloaded: 0,
|
||||
waitTime: options.fsRetryTime ?? 1000 * 5,
|
||||
callback: options.callback,
|
||||
override: options.override,
|
||||
dateStart: 0
|
||||
};
|
||||
}
|
||||
async download(){
|
||||
// set output
|
||||
const fn = this.data.outputFile;
|
||||
// try load resume file
|
||||
if(fs.existsSync(fn) && fs.existsSync(`${fn}.resume`) && this.data.offset < 1){
|
||||
try{
|
||||
console.info('Resume data found! Trying to resume...');
|
||||
const resumeData = JSON.parse(fs.readFileSync(`${fn}.resume`, 'utf-8'));
|
||||
if(
|
||||
resumeData.total == this.data.m3u8json.segments.length
|
||||
&& resumeData.completed != resumeData.total
|
||||
&& !isNaN(resumeData.completed)
|
||||
){
|
||||
console.info('Resume data is ok!');
|
||||
this.data.offset = resumeData.completed;
|
||||
this.data.isResume = true;
|
||||
}
|
||||
else{
|
||||
console.warn(' Resume data is wrong!');
|
||||
console.warn({
|
||||
resume: { total: resumeData.total, dled: resumeData.completed },
|
||||
current: { total: this.data.m3u8json.segments.length },
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(e){
|
||||
console.error('Resume failed, downloading will be not resumed!');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
// ask before rewrite file
|
||||
if (fs.existsSync(`${fn}`) && !this.data.isResume) {
|
||||
let rwts = this.data.override ?? await shlp.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`);
|
||||
rwts = rwts || 'N';
|
||||
if (['Y', 'y'].includes(rwts[0])) {
|
||||
console.info(`Deleting «${fn}»...`);
|
||||
fs.unlinkSync(fn);
|
||||
}
|
||||
else if (['C', 'c'].includes(rwts[0])) {
|
||||
return { ok: true, parts: this.data.parts };
|
||||
}
|
||||
else {
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// show output filename
|
||||
if (fs.existsSync(fn) && this.data.isResume) {
|
||||
console.info(`Adding content to «${fn}»...`);
|
||||
}
|
||||
else{
|
||||
console.info(`Saving stream to «${fn}»...`);
|
||||
}
|
||||
// start time
|
||||
this.data.dateStart = Date.now();
|
||||
let segments = this.data.m3u8json.segments;
|
||||
// download init part
|
||||
if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
|
||||
console.info('Download and save init part...');
|
||||
const initSeg = segments[0].map as Segment;
|
||||
if(segments[0].key){
|
||||
initSeg.key = segments[0].key as Key;
|
||||
}
|
||||
try{
|
||||
const initDl = await this.downloadPart(initSeg, 0, 0);
|
||||
fs.writeFileSync(fn, initDl.dec, { flag: 'a' });
|
||||
fs.writeFileSync(`${fn}.resume`, JSON.stringify({
|
||||
completed: 0,
|
||||
total: this.data.m3u8json.segments.length
|
||||
}));
|
||||
console.info('Init part downloaded.');
|
||||
}
|
||||
catch(e: any){
|
||||
console.error(`Part init download error:\n\t${e.message}`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
else if(segments[0].map && this.data.offset === 0 && this.data.skipInit){
|
||||
console.warn('Skipping init part can lead to broken video!');
|
||||
}
|
||||
// resuming ...
|
||||
if(this.data.offset > 0){
|
||||
segments = segments.slice(this.data.offset);
|
||||
console.info(`Resuming download from part ${this.data.offset+1}...`);
|
||||
this.data.parts.completed = this.data.offset;
|
||||
}
|
||||
// dl process
|
||||
for (let p = 0; p < segments.length / this.data.threads; p++) {
|
||||
// set offsets
|
||||
const offset = p * this.data.threads;
|
||||
const dlOffset = offset + this.data.threads;
|
||||
// map download threads
|
||||
const krq = new Map(), prq = new Map();
|
||||
const res = [];
|
||||
let errcnt = 0;
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++){
|
||||
const curp = segments[px];
|
||||
const key = curp.key as Key;
|
||||
if(key && !krq.has(key.uri) && !this.data.keys[key.uri as string]){
|
||||
krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all(krq.values());
|
||||
} catch (er: any) {
|
||||
console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++){
|
||||
const curp = segments[px] as Segment;
|
||||
prq.set(px, this.downloadPart(curp, px, this.data.offset));
|
||||
}
|
||||
for (let i = prq.size; i--;) {
|
||||
try {
|
||||
const r = await Promise.race(prq.values());
|
||||
prq.delete(r.p);
|
||||
res[r.p - offset] = r.dec;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('Part %s download error:\n\t%s',
|
||||
error.p + 1 + this.data.offset, error.message);
|
||||
prq.delete(error.p);
|
||||
errcnt++;
|
||||
}
|
||||
}
|
||||
// catch error
|
||||
if (errcnt > 0) {
|
||||
console.error(`${errcnt} parts not downloaded`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
// write downloaded
|
||||
for (const r of res) {
|
||||
let error = 0;
|
||||
while (error < 3) {
|
||||
try {
|
||||
fs.writeFileSync(fn, r, { flag: 'a' });
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(`Unable to write to file '${fn}' (Attempt ${error+1}/3)`);
|
||||
console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
|
||||
}
|
||||
error++;
|
||||
}
|
||||
if (error === 3) {
|
||||
console.error(`Unable to write content to '${fn}'.`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// log downloaded
|
||||
const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails
|
||||
const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg;
|
||||
this.data.parts.completed = downloadedSeg + this.data.offset;
|
||||
const data = extFn.getDownloadInfo(
|
||||
this.data.dateStart, downloadedSeg, totalSeg,
|
||||
this.data.bytesDownloaded
|
||||
);
|
||||
fs.writeFileSync(`${fn}.resume`, JSON.stringify({
|
||||
completed: this.data.parts.completed,
|
||||
total: totalSeg
|
||||
}));
|
||||
console.info(`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${shlp.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${(data.downloadSpeed / 1000000).toPrecision(2)}Mb/s)`);
|
||||
if (this.data.callback)
|
||||
this.data.callback({ total: this.data.parts.total, cur: this.data.parts.completed, bytes: this.data.bytesDownloaded, percent: data.percent, time: data.time, downloadSpeed: data.downloadSpeed });
|
||||
}
|
||||
// return result
|
||||
fs.unlinkSync(`${fn}.resume`);
|
||||
return { ok: true, parts: this.data.parts };
|
||||
}
|
||||
async downloadPart(seg: Segment, segIndex: number, segOffset: number){
|
||||
const sURI = extFn.getURI(seg.uri, this.data.baseurl);
|
||||
let decipher, part, dec;
|
||||
const p = segIndex;
|
||||
try {
|
||||
if (seg.key != undefined) {
|
||||
decipher = await this.getKey(seg.key, p, segOffset);
|
||||
}
|
||||
part = await extFn.getData(p, sURI, {
|
||||
...(seg.byterange ? {
|
||||
Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset+seg.byterange.length-1}`
|
||||
} : {})
|
||||
}, segOffset, false, this.data.timeout, this.data.retries, [
|
||||
(res, retryWithMergedOptions) => {
|
||||
if(this.data.checkPartLength && res.headers['content-length']){
|
||||
if(!res.body || (res.body as any).length != res.headers['content-length']){
|
||||
// 'Part not fully downloaded'
|
||||
return retryWithMergedOptions();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
]);
|
||||
if(this.data.checkPartLength && !(part as any).headers['content-length']){
|
||||
this.data.checkPartLength = false;
|
||||
console.warn(`Part ${segIndex+segOffset+1}: can't check parts size!`);
|
||||
}
|
||||
if (decipher == undefined) {
|
||||
this.data.bytesDownloaded += (part.body as Buffer).byteLength;
|
||||
return { dec: part.body, p };
|
||||
}
|
||||
dec = decipher.update(part.body as Buffer);
|
||||
dec = Buffer.concat([dec, decipher.final()]);
|
||||
this.data.bytesDownloaded += dec.byteLength;
|
||||
}
|
||||
catch (error: any) {
|
||||
error.p = p;
|
||||
throw error;
|
||||
}
|
||||
return { dec, p };
|
||||
}
|
||||
async downloadKey(key: Key, segIndex: number, segOffset: number){
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
if (!this.data.keys[kURI]) {
|
||||
try {
|
||||
const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true, this.data.timeout, this.data.retries, [
|
||||
(res, retryWithMergedOptions) => {
|
||||
if (!res || !res.body) {
|
||||
// 'Key get error'
|
||||
return retryWithMergedOptions();
|
||||
}
|
||||
if((res.body as any).length != 16){
|
||||
// 'Key not fully downloaded'
|
||||
return retryWithMergedOptions();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
]);
|
||||
return rkey;
|
||||
}
|
||||
catch (error: any) {
|
||||
error.p = segIndex;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async getKey(key: Key, segIndex: number, segOffset: number){
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
const p = segIndex;
|
||||
if (!this.data.keys[kURI]) {
|
||||
try{
|
||||
const rkey = await this.downloadKey(key, segIndex, segOffset);
|
||||
if (!rkey)
|
||||
throw new Error();
|
||||
this.data.keys[kURI] = rkey.body;
|
||||
}
|
||||
catch (error: any) {
|
||||
error.p = p;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// get ivs
|
||||
const iv = Buffer.alloc(16);
|
||||
const ivs = key.iv ? key.iv : [0, 0, 0, p + 1];
|
||||
for (let i = 0; i < ivs.length; i++) {
|
||||
iv.writeUInt32BE(ivs[i], i * 4);
|
||||
}
|
||||
return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
|
||||
}
|
||||
}
|
||||
|
||||
const extFn = {
|
||||
getURI: (uri: string, baseurl?: string) => {
|
||||
const httpURI = /^https{0,1}:/.test(uri);
|
||||
if (!baseurl && !httpURI) {
|
||||
throw new Error('No base and not http(s) uri');
|
||||
}
|
||||
else if (httpURI) {
|
||||
return uri;
|
||||
}
|
||||
return baseurl + uri;
|
||||
},
|
||||
getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => {
|
||||
const dateElapsed = Date.now() - dateStart;
|
||||
const percentFxd = parseInt((partsDL / partsTotal * 100).toFixed());
|
||||
const percent = percentFxd < 100 ? percentFxd : (partsTotal == partsDL ? 100 : 99);
|
||||
const revParts = dateElapsed * (partsTotal / partsDL - 1);
|
||||
const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
|
||||
return { percent, time: revParts, downloadSpeed };
|
||||
},
|
||||
getData: (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean, timeout: number, retry: number, afterResponse: ((res: Response, retryWithMergedOptions: () => Response) => Response)[]) => {
|
||||
// get file if uri is local
|
||||
if (uri.startsWith('file://')) {
|
||||
return {
|
||||
body: fs.readFileSync(url.fileURLToPath(uri)),
|
||||
};
|
||||
}
|
||||
// base options
|
||||
headers = headers && typeof headers == 'object' ? headers : {};
|
||||
const options = { headers, retry, responseType: 'buffer', hooks: {
|
||||
beforeRequest: [
|
||||
(options: Record<string, Record<string, unknown>>) => {
|
||||
if(!options.headers['user-agent']){
|
||||
options.headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0';
|
||||
}
|
||||
// console.log(' - Req:', options.url.pathname);
|
||||
}
|
||||
],
|
||||
afterResponse: [(fixMiddleWare as (r: Response, s: () => Response) => Response)].concat(afterResponse || []),
|
||||
beforeRetry: [
|
||||
(_: any, error: Error, retryCount: number) => {
|
||||
if(error){
|
||||
const partType = isKey ? 'Key': 'Part';
|
||||
const partIndx = partIndex + 1 + segOffset;
|
||||
console.warn('%s %s: %d attempt to retrieve data', partType, partIndx, retryCount + 1);
|
||||
console.error(`\t${error.message}`);
|
||||
}
|
||||
}
|
||||
]
|
||||
}} as Record<string, unknown>;
|
||||
// proxy
|
||||
options.timeout = timeout;
|
||||
// do request
|
||||
return got(uri, options);
|
||||
}
|
||||
};
|
||||
|
||||
export default hlsDownload;
|
||||
66
modules/log.ts
Normal file
66
modules/log.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { workingDir } from './module.cfg-loader';
|
||||
import log4js from 'log4js';
|
||||
|
||||
const logFolder = path.join(workingDir, 'logs');
|
||||
const latest = path.join(logFolder, 'latest.log');
|
||||
|
||||
const makeLogFolder = () => {
|
||||
if (!fs.existsSync(logFolder))
|
||||
fs.mkdirSync(logFolder);
|
||||
if (fs.existsSync(latest)) {
|
||||
const stats = fs.statSync(latest);
|
||||
fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`));
|
||||
}
|
||||
};
|
||||
|
||||
const makeLogger = () => {
|
||||
const oldLog = global.console.log;
|
||||
global.console.log = (data) => {
|
||||
oldLog(`Unexpected use of console.log. Use the log4js logger instead. ${data}`);
|
||||
};
|
||||
makeLogFolder();
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
console: {
|
||||
type: 'console', layout: {
|
||||
type: 'pattern',
|
||||
pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
filename: latest,
|
||||
layout: {
|
||||
type: 'pattern',
|
||||
pattern: '%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categories: {
|
||||
default: {
|
||||
appenders: ['console', 'file'],
|
||||
level: 'all',
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getLogger = () => {
|
||||
if (!log4js.isConfigured())
|
||||
makeLogger();
|
||||
return log4js.getLogger();
|
||||
};
|
||||
|
||||
export const console = getLogger();
|
||||
|
|
@ -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'
|
||||
|
|
@ -715,10 +716,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 +734,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;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import path from 'path';
|
|||
import yaml from 'yaml';
|
||||
import fs from 'fs-extra';
|
||||
import { lookpath } from 'lookpath';
|
||||
import { console } from './log';
|
||||
|
||||
// new-cfg
|
||||
const workingDir = (process as NodeJS.Process & {
|
||||
|
|
@ -12,8 +13,10 @@ 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 setupFile = path.join(workingDir, 'config', 'setup');
|
||||
const tokenFile = {
|
||||
funi: path.join(workingDir, 'config', 'funi_token'),
|
||||
cr: path.join(workingDir, 'config', 'cr_token')
|
||||
|
|
@ -23,7 +26,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`);
|
||||
});
|
||||
|
|
@ -39,13 +42,29 @@ const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: b
|
|||
return yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR]', e);
|
||||
console.error('[ERROR]', e);
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
return {} as T;
|
||||
};
|
||||
|
||||
export type WriteObjects = {
|
||||
gui: GUIConfig
|
||||
}
|
||||
|
||||
const writeYamlCfgFile = <T extends keyof WriteObjects>(file: T, data: WriteObjects[T]) => {
|
||||
const fn = path.join(workingDir, 'config', `${file}.yml`);
|
||||
if (fs.existsSync(fn))
|
||||
fs.removeSync(fn);
|
||||
fs.writeFileSync(fn, yaml.stringify(data));
|
||||
};
|
||||
|
||||
export type GUIConfig = {
|
||||
port: number,
|
||||
password?: string
|
||||
};
|
||||
|
||||
export type ConfigObject = {
|
||||
dir: {
|
||||
content: string,
|
||||
|
|
@ -59,7 +78,8 @@ export type ConfigObject = {
|
|||
},
|
||||
cli: {
|
||||
[key: string]: any
|
||||
}
|
||||
},
|
||||
gui: GUIConfig
|
||||
}
|
||||
|
||||
const loadCfg = () : ConfigObject => {
|
||||
|
|
@ -75,6 +95,7 @@ const loadCfg = () : ConfigObject => {
|
|||
cli: loadYamlCfgFile<{
|
||||
[key: string]: any
|
||||
}>(cliCfgFile),
|
||||
gui: loadYamlCfgFile<GUIConfig>(guiCfgFile)
|
||||
};
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
|
|
@ -100,7 +121,7 @@ const loadCfg = () : ConfigObject => {
|
|||
fs.ensureDirSync(defaultCfg.dir.content);
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Content directory not accessible!');
|
||||
console.error('Content directory not accessible!');
|
||||
return defaultCfg;
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +181,7 @@ const saveCRSession = (data: Record<string, unknown>) => {
|
|||
fs.writeFileSync(`${sessCfgFile}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save session file to disk!');
|
||||
console.error('Can\'t save session file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -179,7 +200,7 @@ const saveCRToken = (data: Record<string, unknown>) => {
|
|||
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save token file to disk!');
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -192,7 +213,7 @@ const loadFuniToken = () => {
|
|||
token = loadedToken.token;
|
||||
// info if token not set
|
||||
if(!token){
|
||||
console.log('[INFO] Token not set!\n');
|
||||
console.info('[INFO] Token not set!\n');
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
|
@ -206,12 +227,33 @@ const saveFuniToken = (data: {
|
|||
fs.writeFileSync(`${tokenFile.funi}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save token file to disk!');
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const cfgDir = path.join(workingDir, 'config');
|
||||
|
||||
const isSetuped = (): boolean => {
|
||||
const fn = `${setupFile}.json`;
|
||||
if (!fs.existsSync(fn))
|
||||
return false;
|
||||
return JSON.parse(fs.readFileSync(fn).toString()).setuped;
|
||||
};
|
||||
|
||||
const setSetuped = (bool: boolean) => {
|
||||
const fn = `${setupFile}.json`;
|
||||
if (bool) {
|
||||
fs.writeFileSync(fn, JSON.stringify({
|
||||
setuped: true
|
||||
}, null, 2));
|
||||
} else {
|
||||
if (fs.existsSync(fn)) {
|
||||
fs.removeSync(fn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
loadBinCfg,
|
||||
loadCfg,
|
||||
|
|
@ -221,6 +263,9 @@ export {
|
|||
saveCRToken,
|
||||
loadCRToken,
|
||||
loadCRSession,
|
||||
isSetuped,
|
||||
setSetuped,
|
||||
writeYamlCfgFile,
|
||||
sessCfgFile,
|
||||
cfgDir
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import child_process from 'child_process';
|
|||
import fs from 'fs-extra';
|
||||
import { Headers } from 'got';
|
||||
import path from 'path';
|
||||
import { console } from './log';
|
||||
|
||||
export type CurlOptions = {
|
||||
headers?: Headers,
|
||||
|
|
@ -73,7 +74,7 @@ const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache
|
|||
|
||||
try{
|
||||
if(options.curlDebug){
|
||||
console.log(curlComm, '\n');
|
||||
console.info(curlComm, '\n');
|
||||
}
|
||||
child_process.execSync(curlComm, { stdio: 'inherit', windowsHide: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as shlp from 'sei-helper';
|
||||
import path from 'path';
|
||||
import { AvailableFilenameVars } from './module.args';
|
||||
|
||||
import { console } from './log';
|
||||
|
||||
export type Variable<T extends string = AvailableFilenameVars> = ({
|
||||
type: 'number',
|
||||
|
|
@ -25,7 +25,7 @@ const parseFileName = (input: string, variables: Variable[], numbers: number, ov
|
|||
const varName = type.slice(2, -1);
|
||||
const use = overridenVars.find(a => a.name === varName);
|
||||
if (use === undefined) {
|
||||
console.log(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`);
|
||||
console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(',');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import got, { OptionsOfUnknownResponseBody, ReadError, Response, ResponseType } from 'got';
|
||||
import { console } from './log';
|
||||
|
||||
// Used for future updates
|
||||
// const argv = require('../funi').argv;
|
||||
|
|
@ -83,8 +84,8 @@ const getData = async <T = string>(options: Options) => {
|
|||
beforeRequest: [
|
||||
(gotOpts) => {
|
||||
if(options.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(gotOpts);
|
||||
console.debug('GOT OPTIONS:');
|
||||
console.debug(gotOpts);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -109,17 +110,17 @@ const getData = async <T = string>(options: Options) => {
|
|||
res: Response<unknown>
|
||||
};
|
||||
if(options.debug){
|
||||
console.log(error);
|
||||
console.debug(error);
|
||||
}
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
console.error(`${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
}
|
||||
else if(error.name && error.name == 'HTMLError' && error.res && error.res.body){
|
||||
console.log(`[ERROR] ${error.name}:`);
|
||||
console.log(error.res.body);
|
||||
console.error(`${error.name}:`);
|
||||
console.error(error.res.body);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
|
||||
console.error(`${error.name}: ${error.code||error.message}`);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import fs from 'fs';
|
|||
import { LanguageItem } from './module.langsData';
|
||||
import { AvailableMuxer } from './module.args';
|
||||
import { exec } from './sei-helper-fixes';
|
||||
import { console } from './log';
|
||||
|
||||
export type MergerInput = {
|
||||
path: string,
|
||||
|
|
@ -56,8 +57,8 @@ class Merger {
|
|||
}
|
||||
|
||||
public FFmpeg() : string {
|
||||
const args = [];
|
||||
const metaData = [];
|
||||
const args: string[] = [];
|
||||
const metaData: string[] = [];
|
||||
|
||||
let index = 0;
|
||||
let audioIndex = 0;
|
||||
|
|
@ -137,7 +138,7 @@ class Merger {
|
|||
};
|
||||
|
||||
public MkvMerge = () => {
|
||||
const args = [];
|
||||
const args: string[] = [];
|
||||
|
||||
let hasVideo = false;
|
||||
|
||||
|
|
@ -224,7 +225,6 @@ class Merger {
|
|||
}
|
||||
if (this.options.fonts && this.options.fonts.length > 0) {
|
||||
for (const f of this.options.fonts) {
|
||||
console.log(f.path);
|
||||
args.push('--attachment-name', f.name);
|
||||
args.push('--attachment-mime-type', f.mime);
|
||||
args.push('--attach-file', `"${f.path}"`);
|
||||
|
|
@ -261,9 +261,9 @@ class Merger {
|
|||
FFmpeg: bin.ffmpeg
|
||||
};
|
||||
} else if (useMP4format) {
|
||||
console.log('[WARN] FFmpeg not found, skip muxing...');
|
||||
console.warn('FFmpeg not found, skip muxing...');
|
||||
} else if (!bin.mkvmerge) {
|
||||
console.log('[WARN] MKVMerge not found, skip muxing...');
|
||||
console.warn('MKVMerge not found, skip muxing...');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
@ -272,18 +272,18 @@ 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);
|
||||
}
|
||||
fontsNameList = [...new Set(fontsNameList)];
|
||||
if(subsList.length > 0){
|
||||
console.log('\n[INFO] Subtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
|
||||
console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
|
||||
isNstr = false;
|
||||
}
|
||||
if(fontsNameList.length > 0){
|
||||
console.log((isNstr ? '\n' : '') + '[INFO] Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
|
||||
console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
|
||||
}
|
||||
for(const f of fontsNameList){
|
||||
const fontFiles = fontFamilies[f];
|
||||
|
|
@ -315,18 +315,18 @@ class Merger {
|
|||
break;
|
||||
}
|
||||
if (command === undefined) {
|
||||
console.log('[WARN] Unable to merge files.');
|
||||
console.warn('Unable to merge files.');
|
||||
return;
|
||||
}
|
||||
console.log(`[INFO][${type}] Started merging`);
|
||||
console.info(`[${type}] Started merging`);
|
||||
const res = exec(type, `"${bin}"`, command);
|
||||
if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) {
|
||||
console.log(`[INFO][${type}] Mkvmerge finished with at least one warning`);
|
||||
console.info(`[${type}] Mkvmerge finished with at least one warning`);
|
||||
} else if (!res.isOk) {
|
||||
console.log(res.err);
|
||||
console.log(`[ERROR][${type}] Merging failed with exit code ${res.err.code}`);
|
||||
console.error(res.err);
|
||||
console.error(`[${type}] Merging failed with exit code ${res.err.code}`);
|
||||
} else {
|
||||
console.log(`[INFO][${type} Done]`);
|
||||
console.info(`[${type} Done]`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { console } from './log';
|
||||
|
||||
const parseSelect = (selectString: string, but = false) : {
|
||||
isSelected: (val: string|string[]) => boolean,
|
||||
values: string[]
|
||||
|
|
@ -14,7 +16,7 @@ const parseSelect = (selectString: string, but = false) : {
|
|||
if (part.includes('-')) {
|
||||
const splits = part.split('-');
|
||||
if (splits.length !== 2) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -22,14 +24,14 @@ const parseSelect = (selectString: string, but = false) : {
|
|||
const match = firstPart.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
const letters = firstPart.substring(0, match[0].length);
|
||||
const number = parseInt(firstPart.substring(match[0].length));
|
||||
const b = parseInt(splits[1]);
|
||||
if (isNaN(number) || isNaN(b)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = number; i <= b; i++) {
|
||||
|
|
@ -40,7 +42,7 @@ const parseSelect = (selectString: string, but = false) : {
|
|||
const a = parseInt(firstPart);
|
||||
const b = parseInt(splits[1]);
|
||||
if (isNaN(a) || isNaN(b)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = a; i <= b; i++) {
|
||||
|
|
@ -56,13 +58,13 @@ const parseSelect = (selectString: string, but = false) : {
|
|||
const match = part.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
const letters = part.substring(0, match[0].length);
|
||||
const number = parseInt(part.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
select.push(`${letters}${number}`);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import shlp from 'sei-helper';
|
|||
import got, { Headers, Method, Options, ReadError, Response } from 'got';
|
||||
import cookieFile from './module.cookieFile';
|
||||
import * as yamlCfg from './module.cfg-loader';
|
||||
import { console } from './log';
|
||||
//import curlReq from './module.curl-req';
|
||||
|
||||
export type Params = {
|
||||
|
|
@ -68,15 +69,15 @@ class Req {
|
|||
options.followRedirect = params.followRedirect;
|
||||
}
|
||||
// if auth
|
||||
const loc = new URL(durl);
|
||||
//const loc = new URL(durl);
|
||||
// avoid cloudflare protection
|
||||
// debug
|
||||
options.hooks = {
|
||||
beforeRequest: [
|
||||
(options) => {
|
||||
if(this.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(options);
|
||||
console.debug('[DEBUG] GOT OPTIONS:');
|
||||
console.debug(options);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -99,21 +100,21 @@ class Req {
|
|||
res: Response<unknown>
|
||||
};
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
console.error(`${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code || error.message}`);
|
||||
console.error(`${error.name}: ${error.code || error.message}`);
|
||||
}
|
||||
if(error.response && !error.res){
|
||||
error.res = error.response;
|
||||
const docTitle = (error.res.body as string).match(/<title>(.*)<\/title>/);
|
||||
if(error.res.body && docTitle){
|
||||
console.log('[ERROR]', docTitle[1]);
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
}
|
||||
if(error.res && error.res.body && error.response.statusCode
|
||||
&& error.response.statusCode != 404 && error.response.statusCode != 403){
|
||||
console.log('[ERROR] Body:', error.res.body);
|
||||
console.error('Body:', error.res.body);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -122,7 +123,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)){
|
||||
|
|
@ -165,10 +166,10 @@ class Req {
|
|||
}
|
||||
if(cookieUpdated.length > 0){
|
||||
if(this.debug){
|
||||
console.log('[SAVING FILE]',`${this.sessCfg}.yml`);
|
||||
console.info('[SAVING FILE]',`${this.sessCfg}.yml`);
|
||||
}
|
||||
yamlCfg.saveCRSession(this.session);
|
||||
console.log(`[INFO] Cookies were updated! (${cookieUpdated.join(', ')})\n`);
|
||||
console.info(`Cookies were updated! (${cookieUpdated.join(', ')})\n`);
|
||||
}
|
||||
}
|
||||
checkCookieVal(chcookie: Record<string, string>){
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import tsConfig from '../tsconfig.json';
|
|||
import fsextra from 'fs-extra';
|
||||
import seiHelper from 'sei-helper';
|
||||
import { workingDir } from './module.cfg-loader';
|
||||
import { console } from './log';
|
||||
const updateFilePlace = path.join(workingDir, 'config', 'updates.json');
|
||||
|
||||
const updateIgnore = [
|
||||
|
|
@ -53,7 +54,7 @@ export default (async (force = false) => {
|
|||
return;
|
||||
}
|
||||
}
|
||||
console.log('Checking for updates...');
|
||||
console.info('Checking for updates...');
|
||||
const tagRequest = await got('https://api.github.com/repos/anidl/multi-downloader-nx/tags');
|
||||
const tags = JSON.parse(tagRequest.body) as GithubTag[];
|
||||
|
||||
|
|
@ -61,10 +62,10 @@ export default (async (force = false) => {
|
|||
const newer = tags.filter(a => {
|
||||
return isNewer(packageJson.version, a.name);
|
||||
});
|
||||
console.log(`Found ${tags.length} release tags and ${newer.length} that are new.`);
|
||||
console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`);
|
||||
|
||||
if (newer.length < 1) {
|
||||
console.log('[INFO] No new tags found');
|
||||
console.info('No new tags found');
|
||||
return done();
|
||||
}
|
||||
const newest = newer.sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0)[0];
|
||||
|
|
@ -72,7 +73,7 @@ export default (async (force = false) => {
|
|||
|
||||
const compareJSON = JSON.parse(compareRequest.body) as TagCompare;
|
||||
|
||||
console.log(`You are behind by ${compareJSON.ahead_by} releases!`);
|
||||
console.info(`You are behind by ${compareJSON.ahead_by} releases!`);
|
||||
const changedFiles = compareJSON.files.map(a => ({
|
||||
...a,
|
||||
filename: path.join(...a.filename.split('/'))
|
||||
|
|
@ -80,10 +81,10 @@ export default (async (force = false) => {
|
|||
return !updateIgnore.some(_filter => matchString(_filter, a.filename));
|
||||
});
|
||||
if (changedFiles.length < 1) {
|
||||
console.log('[INFO] No file changes found... updating package.json. If you think this is an error please get the newst version yourself.');
|
||||
console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.');
|
||||
return done(newest.name);
|
||||
}
|
||||
console.log(`Found file changes: \n${changedFiles.map(a => ` [${
|
||||
console.info(`Found file changes: \n${changedFiles.map(a => ` [${
|
||||
a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'
|
||||
}] ${a.filename}`).join('\n')}`);
|
||||
|
||||
|
|
@ -107,7 +108,7 @@ export default (async (force = false) => {
|
|||
}).outputText,
|
||||
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
|
||||
};
|
||||
console.log('✓ Transpiled %s', ret.path);
|
||||
console.info('✓ Transpiled %s', ret.path);
|
||||
return ret;
|
||||
} else {
|
||||
const ret = {
|
||||
|
|
@ -115,7 +116,7 @@ export default (async (force = false) => {
|
|||
content: (await got(a.raw_url)).body,
|
||||
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
|
||||
};
|
||||
console.log('✓ Got %s', ret.path);
|
||||
console.info('✓ Got %s', ret.path);
|
||||
return ret;
|
||||
}
|
||||
}));
|
||||
|
|
@ -124,13 +125,13 @@ export default (async (force = false) => {
|
|||
try {
|
||||
fsextra.ensureDirSync(path.dirname(a.path));
|
||||
fs.writeFileSync(path.join(__dirname, '..', a.path), a.content);
|
||||
console.log('✓ Written %s', a.path);
|
||||
console.info('✓ Written %s', a.path);
|
||||
} catch (er) {
|
||||
console.log('✗ Error while writing %s', a.path);
|
||||
console.info('✗ Error while writing %s', a.path);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[INFO] Done');
|
||||
console.info('Done');
|
||||
return done();
|
||||
}
|
||||
});
|
||||
|
|
@ -147,7 +148,7 @@ function done(newVersion?: string) {
|
|||
version: newVersion
|
||||
}, null, 4));
|
||||
}
|
||||
console.log('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.');
|
||||
console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.');
|
||||
}
|
||||
|
||||
function isNewer(curr: string, compare: string) : boolean {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import childProcess from 'child_process';
|
||||
import { console } from './log';
|
||||
|
||||
const exec = (pname: string, fpath: string, pargs: string, spc = false): {
|
||||
isOk: true
|
||||
|
|
@ -7,7 +8,7 @@ const exec = (pname: string, fpath: string, pargs: string, spc = false): {
|
|||
err: Error & { code: number }
|
||||
} => {
|
||||
pargs = pargs ? ' ' + pargs : '';
|
||||
console.log(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`);
|
||||
console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`);
|
||||
try {
|
||||
childProcess.execSync((fpath + pargs), { stdio: 'inherit' });
|
||||
return {
|
||||
|
|
|
|||
72
package.json
72
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "multi-downloader-nx",
|
||||
"short_name": "aniDL",
|
||||
"version": "3.3.7",
|
||||
"version": "3.4.0",
|
||||
"description": "Download videos from Funimation or Crunchyroll via cli",
|
||||
"keywords": [
|
||||
"download",
|
||||
|
|
@ -35,56 +35,52 @@
|
|||
"url": "https://github.com/anidl/multi-downloader-nx/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "gui/electron/src/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/plugin-syntax-flow": "^7.18.6",
|
||||
"@babel/plugin-transform-react-jsx": "^7.20.13",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"@babel/plugin-transform-react-jsx": "^7.21.0",
|
||||
"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",
|
||||
"fs-extra": "^11.1.0",
|
||||
"got": "^11.8.6",
|
||||
"hls-download": "^2.6.10",
|
||||
"iso-639": "^0.2.2",
|
||||
"log4js": "^6.8.0",
|
||||
"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",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
"ws": "^8.12.1",
|
||||
"yaml": "^2.2.1",
|
||||
"yargs": "^17.7.0"
|
||||
"yargs": "^17.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/node": "^18.14.6",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@types/yargs": "^17.0.22",
|
||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.54.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": "^8.35.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",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2987
pnpm-lock.yaml
2987
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
19
tsc.ts
19
tsc.ts
|
|
@ -2,7 +2,6 @@ import { ChildProcess, exec } from 'child_process';
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { removeSync, copyFileSync } from 'fs-extra';
|
||||
import packageJSON from './package.json';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
let buildIgnore: string[] = [];
|
||||
|
|
@ -13,12 +12,14 @@ const isGUI = !(argv.length > 1 && argv[1] === 'false');
|
|||
if (!isTest)
|
||||
buildIgnore = [
|
||||
'*/\\.env',
|
||||
'./config/setup.json'
|
||||
];
|
||||
|
||||
if (!isGUI)
|
||||
buildIgnore = buildIgnore.concat([
|
||||
'./gui*',
|
||||
'./build*'
|
||||
'./build*',
|
||||
'gui.ts'
|
||||
]);
|
||||
|
||||
|
||||
|
|
@ -77,6 +78,10 @@ export { ignore };
|
|||
|
||||
const react = exec('pnpm run build', {
|
||||
cwd: path.join(__dirname, 'gui', 'react'),
|
||||
env: {
|
||||
...process.env,
|
||||
CI: 'false'
|
||||
}
|
||||
});
|
||||
|
||||
await waitForProcess(react);
|
||||
|
|
@ -84,7 +89,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 +104,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 +114,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