mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
Inital
This commit is contained in:
parent
1c080693fd
commit
5abc0115df
23 changed files with 5288 additions and 804 deletions
31
.eslintrc.js
31
.eslintrc.js
|
|
@ -1,31 +0,0 @@
|
|||
module.exports = {
|
||||
'env': {
|
||||
'commonjs': true,
|
||||
'es2021': true,
|
||||
'node': true
|
||||
},
|
||||
'extends': 'eslint:recommended',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 12
|
||||
},
|
||||
'rules': {
|
||||
'no-empty': [
|
||||
'error',
|
||||
{ allowEmptyCatch: true }
|
||||
],
|
||||
'indent': [
|
||||
'error',
|
||||
4,
|
||||
{ 'SwitchCase': 1 }
|
||||
],
|
||||
'linebreak-style': 'off',
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
]
|
||||
}
|
||||
};
|
||||
33
.eslintrc.json
Normal file
33
.eslintrc.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,13 +3,13 @@
|
|||
/_builds/*
|
||||
/node_modules/
|
||||
/videos/*.json
|
||||
/videos/*.ts
|
||||
.DS_Store
|
||||
ffmpeg
|
||||
mkvmerge
|
||||
token.yml
|
||||
*.exe
|
||||
*.dll
|
||||
*.ts
|
||||
*.mkv
|
||||
*.mp4
|
||||
*.ass
|
||||
|
|
|
|||
28
@types/hls-download.d.ts
vendored
Normal file
28
@types/hls-download.d.ts
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
declare module 'hls-download' {
|
||||
export default class hlsDownload {
|
||||
constructor(options: {
|
||||
m3u8json: {
|
||||
segments: {
|
||||
|
||||
}[],
|
||||
mediaSequence?: number,
|
||||
},
|
||||
output?: string,
|
||||
threads?: number,
|
||||
retries?: number,
|
||||
offset?: number,
|
||||
baseurl?: string,
|
||||
proxy?: string,
|
||||
skipInit?: boolean,
|
||||
timeout?: number
|
||||
})
|
||||
async download() : {
|
||||
ok: boolean,
|
||||
parts: {
|
||||
first: number,
|
||||
total: number,
|
||||
compleated: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
@types/items.d.ts
vendored
Normal file
237
@types/items.d.ts
vendored
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
export interface Item {
|
||||
// Added later
|
||||
id: string,
|
||||
id_split: (number|string)[]
|
||||
// Added from the start
|
||||
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
|
||||
synopsis: string;
|
||||
mediaCategory: ContentType;
|
||||
mostRecentSvodUsEndTimestamp: number;
|
||||
quality: QualityClass;
|
||||
genres: Genre[];
|
||||
titleImages: TitleImages;
|
||||
engAllTerritoryAvail: EngAllTerritoryAvail;
|
||||
thumb: string;
|
||||
mostRecentSvodJpnAllTerrStartTimestamp: number;
|
||||
title: string;
|
||||
starRating: number;
|
||||
primaryAvail: PrimaryAvail;
|
||||
access: Access[];
|
||||
version: Version[];
|
||||
mostRecentSvodJpnAllTerrEndTimestamp: number;
|
||||
itemId: number;
|
||||
versionAudio: VersionAudio;
|
||||
contentType: ContentType;
|
||||
mostRecentSvodUsStartTimestamp: number;
|
||||
poster: string;
|
||||
mostRecentSvodEngAllTerrEndTimestamp: number;
|
||||
mostRecentSvodJpnUsStartTimestamp: number;
|
||||
mostRecentSvodJpnUsEndTimestamp: number;
|
||||
mostRecentSvodStartTimestamp: number;
|
||||
mostRecentSvod: MostRecent;
|
||||
altAvail: AltAvail;
|
||||
ids: IDs;
|
||||
mostRecentSvodUs: MostRecent;
|
||||
item: Item;
|
||||
mostRecentSvodEngAllTerrStartTimestamp: number;
|
||||
audio: Audio[];
|
||||
mostRecentAvod: MostRecent;
|
||||
}
|
||||
|
||||
export enum Access {
|
||||
AVODSimulcastEnglish = "A-VOD_Simulcast_English",
|
||||
AVODUncutEnglish = "A-VOD_Uncut_English",
|
||||
SVODSimulcastEnglish = "SVOD_Simulcast_English",
|
||||
SVODUncutEnglish = "SVOD_Uncut_English",
|
||||
}
|
||||
|
||||
export enum AltAvail {
|
||||
MostRecentSvodJpnUs = "most_recent_svod_jpn_us",
|
||||
}
|
||||
|
||||
export enum Audio {
|
||||
English = "English",
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
Episode = "episode",
|
||||
Ova = "ova",
|
||||
}
|
||||
|
||||
export enum EngAllTerritoryAvail {
|
||||
MostRecentSvodEngAllTerr = "most_recent_svod_eng_all_terr",
|
||||
}
|
||||
|
||||
export enum Genre {
|
||||
ActionAdventure = "Action/Adventure",
|
||||
Comedy = "Comedy",
|
||||
Drama = "Drama",
|
||||
Fantasy = "Fantasy",
|
||||
}
|
||||
|
||||
export interface IDs {
|
||||
externalShowId: ID;
|
||||
externalSeasonId: ExternalSeasonID;
|
||||
externalEpisodeId: string;
|
||||
externalAsianId?: string
|
||||
}
|
||||
|
||||
export enum ExternalSeasonID {
|
||||
TrsS11 = "TRS-S1-1",
|
||||
TrsS22 = "TRS-S2-2",
|
||||
}
|
||||
|
||||
export enum ID {
|
||||
Trs = "TRS",
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
seasonTitle: SeasonTitle;
|
||||
seasonId: number;
|
||||
episodeOrder: number;
|
||||
episodeSlug: string;
|
||||
created: Date;
|
||||
titleSlug: TitleSlug;
|
||||
episodeNum: string;
|
||||
episodeId: number;
|
||||
titleId: number;
|
||||
seasonNum: string;
|
||||
ratings: Array<string[]>;
|
||||
showImage: string;
|
||||
titleName: TitleName;
|
||||
runtime: string;
|
||||
episodeName: string;
|
||||
seasonOrder: number;
|
||||
titleExternalId: ID;
|
||||
}
|
||||
|
||||
export enum SeasonTitle {
|
||||
Season1 = "Season 1",
|
||||
Season2 = "Season 2",
|
||||
}
|
||||
|
||||
export enum TitleName {
|
||||
ThatTimeIGotReincarnatedAsASlime = "That Time I Got Reincarnated as a Slime",
|
||||
}
|
||||
|
||||
export enum TitleSlug {
|
||||
ThatTimeIGotReincarnatedAsASlime = "that-time-i-got-reincarnated-as-a-slime",
|
||||
}
|
||||
|
||||
export interface MostRecent {
|
||||
image?: string;
|
||||
siblingStartTimestamp?: string;
|
||||
devices?: Device[];
|
||||
availId?: number;
|
||||
distributor?: Distributor;
|
||||
quality?: MostRecentAvodQuality;
|
||||
endTimestamp?: string;
|
||||
mediaCategory?: ContentType;
|
||||
isPromo?: boolean;
|
||||
siblingType?: Purchase;
|
||||
version?: Version;
|
||||
territory?: Territory;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
versionId?: number;
|
||||
tier?: Device | null;
|
||||
purchase?: Purchase;
|
||||
startTimestamp?: string;
|
||||
language?: Audio;
|
||||
itemTitle?: string;
|
||||
ids?: MostRecentAvodIDS;
|
||||
experience?: number;
|
||||
siblingEndTimestamp?: string;
|
||||
item?: Item;
|
||||
subscriptionRequired?: boolean;
|
||||
purchased?: boolean;
|
||||
}
|
||||
|
||||
export enum Device {
|
||||
All = "All",
|
||||
}
|
||||
|
||||
export enum Distributor {
|
||||
FunimationVenue = "FunimationVenue",
|
||||
}
|
||||
|
||||
export interface MostRecentAvodIDS {
|
||||
externalSeasonId: ExternalSeasonID;
|
||||
externalAsianId: null;
|
||||
externalShowId: ID;
|
||||
externalEpisodeId: string;
|
||||
externalEnglishId: string;
|
||||
externalAlphaId: string;
|
||||
}
|
||||
|
||||
export enum Purchase {
|
||||
AVOD = "A-VOD",
|
||||
Dfov = "DFOV",
|
||||
Est = "EST",
|
||||
Svod = "SVOD",
|
||||
}
|
||||
|
||||
export enum MostRecentAvodQuality {
|
||||
HD1080 = "HD 1080",
|
||||
}
|
||||
|
||||
export enum Territory {
|
||||
Usa = "USA",
|
||||
}
|
||||
|
||||
export enum Version {
|
||||
Simulcast = "Simulcast",
|
||||
Uncut = "Uncut",
|
||||
}
|
||||
|
||||
export interface MostRecentSvodJpnUs {
|
||||
}
|
||||
|
||||
export enum PrimaryAvail {
|
||||
MostRecentSvodUs = "most_recent_svod_us",
|
||||
}
|
||||
|
||||
export interface QualityClass {
|
||||
quality: QualityQuality;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export enum QualityQuality {
|
||||
HD = "HD",
|
||||
SD = "SD",
|
||||
}
|
||||
|
||||
export interface TitleImages {
|
||||
showThumbnail: string;
|
||||
showBackgroundSite: string;
|
||||
showDetailHeaderDesktop: string;
|
||||
continueWatchingDesktop: string;
|
||||
showDetailHeroSite: string;
|
||||
appleHorizontalBannerShow: string;
|
||||
backgroundImageXbox_360: string;
|
||||
applePosterCover: string;
|
||||
showDetailBoxArtTablet: string;
|
||||
featuredShowBackgroundTablet: string;
|
||||
backgroundImageAppletvfiretv: string;
|
||||
newShowDetailHero: string;
|
||||
showDetailHeroDesktop: string;
|
||||
showKeyart: string;
|
||||
continueWatchingMobile: string;
|
||||
featuredSpotlightShowPhone: string;
|
||||
appleHorizontalBannerMovie: string;
|
||||
featuredSpotlightShowTablet: string;
|
||||
showDetailBoxArtPhone: string;
|
||||
featuredShowBackgroundPhone: string;
|
||||
appleSquareCover: string;
|
||||
backgroundVideo: string;
|
||||
showMasterKeyArt: string;
|
||||
newShowDetailHeroPhone: string;
|
||||
showDetailBoxArtXbox_360: string;
|
||||
showDetailHeaderMobile: string;
|
||||
showLogo: string;
|
||||
}
|
||||
|
||||
export interface VersionAudio {
|
||||
Uncut?: Audio[];
|
||||
Simulcast: Audio[];
|
||||
}
|
||||
48
@types/m3u8-parsed.d.ts
vendored
Normal file
48
@types/m3u8-parsed.d.ts
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
declare module 'm3u8-parsed' {
|
||||
export default function (data: string): {
|
||||
allowCache: boolean,
|
||||
discontinuityStarts: [],
|
||||
segments: {
|
||||
duration: number,
|
||||
byterange?: {
|
||||
length: number,
|
||||
offset: number
|
||||
},
|
||||
uri: string,
|
||||
key: {
|
||||
method: string,
|
||||
uri: string,
|
||||
},
|
||||
timeline: number
|
||||
}[],
|
||||
version: number,
|
||||
mediaGroups?: {
|
||||
[type: string]: {
|
||||
[index: string]: {
|
||||
[language: string]: {
|
||||
default: boolean,
|
||||
autoselect: boolean,
|
||||
language: string,
|
||||
uri: string
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
playlists?: {
|
||||
uri: string,
|
||||
timeline: number,
|
||||
attributes: {
|
||||
"CLOSED-CAPTIONS": string,
|
||||
"AUDIO": string,
|
||||
"FRAME-RATE": number,
|
||||
"RESOLUTION": {
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
"CODECS": string,
|
||||
"AVERAGE-BANDWIDTH": string,
|
||||
"BANDWIDTH": number
|
||||
}
|
||||
}[],
|
||||
}
|
||||
}
|
||||
3
@types/sei-helper.d.ts
vendored
Normal file
3
@types/sei-helper.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
declare module 'sei-helper' {
|
||||
export async function question(qStr: string): string;
|
||||
}
|
||||
|
|
@ -1,45 +1,42 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// modules build-in
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// package json
|
||||
const packageJson = require('./package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString())
|
||||
|
||||
// program name
|
||||
console.log(`\n=== Funimation Downloader NX ${packageJson.version} ===\n`);
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
|
||||
// modules extra
|
||||
const shlp = require('sei-helper');
|
||||
const m3u8 = require('m3u8-parsed');
|
||||
const hlsDownload = require('hls-download');
|
||||
import * as shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
import hlsDownload from 'hls-download';
|
||||
|
||||
// extra
|
||||
const appYargs = require('./modules/module.app-args');
|
||||
const yamlCfg = require('./modules/module.cfg-loader');
|
||||
const vttConvert = require('./modules/module.vttconvert');
|
||||
import * as appYargs from './modules/module.app-args';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
import vttConvert from './modules/module.vttconvert';
|
||||
|
||||
// types
|
||||
import { Item } from "./@types/items";
|
||||
|
||||
// params
|
||||
const cfg = yamlCfg.loadCfg();
|
||||
let token = yamlCfg.loadFuniToken();
|
||||
|
||||
// cli
|
||||
const argv = appYargs.appArgv(cfg.cli);
|
||||
|
||||
// Import modules after argv has been exported
|
||||
const getData = require('./modules/module.getdata.js');
|
||||
const merger = require('./modules/module.merger');
|
||||
const parseSelect = require('./modules/module.parseSelect');
|
||||
import getData from './modules/module.getdata.js';
|
||||
import merger from './modules/module.merger';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
|
||||
// check page
|
||||
if(!isNaN(parseInt(argv.p, 10)) && parseInt(argv.p, 10) > 0){
|
||||
argv.p = parseInt(argv.p, 10);
|
||||
}
|
||||
else{
|
||||
argv.p = 1;
|
||||
}
|
||||
argv.p = 1;
|
||||
|
||||
// fn variables
|
||||
let title = '',
|
||||
|
|
@ -61,7 +58,7 @@ let title = '',
|
|||
else if(argv.search){
|
||||
searchShow();
|
||||
}
|
||||
else if(argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0){
|
||||
else if(argv.s && argv.s > 0){
|
||||
getShow();
|
||||
}
|
||||
else{
|
||||
|
|
@ -71,24 +68,26 @@ let title = '',
|
|||
|
||||
// auth
|
||||
async function auth(){
|
||||
let authOpts = {};
|
||||
authOpts.user = await shlp.question('[Q] LOGIN/EMAIL');
|
||||
authOpts.pass = await shlp.question('[Q] PASSWORD ');
|
||||
let authOpts = {
|
||||
user: await shlp.question('[Q] LOGIN/EMAIL'),
|
||||
pass: await shlp.question('[Q] PASSWORD ')
|
||||
};
|
||||
let authData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: '/auth/login/',
|
||||
useProxy: true,
|
||||
auth: authOpts,
|
||||
debug: argv.debug,
|
||||
});
|
||||
if(authData.ok){
|
||||
authData = JSON.parse(authData.res.body);
|
||||
if(authData.token){
|
||||
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', authData.token.slice(0,8),'*'.repeat(32));
|
||||
yamlCfg.saveFuniToken({'token': authData.token});
|
||||
yamlCfg.saveFuniToken({'token': resJSON.token});
|
||||
} else {
|
||||
console.log('[ERROR]%s\n', ' No token found');
|
||||
if (argv.debug) {
|
||||
console.log(resJSON);
|
||||
}
|
||||
else if(authData.error){
|
||||
console.log('[ERROR]%s\n', authData.error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -96,30 +95,29 @@ async function auth(){
|
|||
|
||||
// search show
|
||||
async function searchShow(){
|
||||
let qs = {unique: true, limit: 100, q: argv.search, offset: (argv.p-1)*1000 };
|
||||
let qs = {unique: true, limit: 100, q: argv.search, offset: 0 };
|
||||
let searchData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: '/source/funimation/search/auto/',
|
||||
querystring: qs,
|
||||
token: token,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
debug: argv.debug,
|
||||
});
|
||||
if(!searchData.ok){return;}
|
||||
searchData = JSON.parse(searchData.res.body);
|
||||
if(searchData.detail){
|
||||
console.log(`[ERROR] ${searchData.detail}`);
|
||||
if(!searchData.ok || !searchData.res){return;}
|
||||
const searchDataJSON = JSON.parse(searchData.res.body);
|
||||
if(searchDataJSON.detail){
|
||||
console.log(`[ERROR] ${searchDataJSON.detail}`);
|
||||
return;
|
||||
}
|
||||
if(searchData.items && searchData.items.hits){
|
||||
let shows = searchData.items.hits;
|
||||
if(searchDataJSON.items && searchDataJSON.items.hits){
|
||||
let shows = searchDataJSON.items.hits;
|
||||
console.log('[INFO] Search Results:');
|
||||
for(let ssn in shows){
|
||||
console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
|
||||
}
|
||||
}
|
||||
console.log('[INFO] Total shows found: %s\n',searchData.count);
|
||||
console.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
|
||||
}
|
||||
|
||||
// get show
|
||||
|
|
@ -127,27 +125,32 @@ async function getShow(){
|
|||
// show main data
|
||||
let showData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/title/${parseInt(argv.s, 10)}`,
|
||||
url: `/source/catalog/title/${argv.s}`,
|
||||
token: token,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
debug: argv.debug,
|
||||
});
|
||||
// check errors
|
||||
if(!showData.ok){return;}
|
||||
showData = JSON.parse(showData.res.body);
|
||||
if(showData.status){
|
||||
console.log('[ERROR] Error #%d: %s\n', showData.status, showData.data.errors[0].detail);
|
||||
if(!showData.ok || !showData.res){return;}
|
||||
const showDataJSON = JSON.parse(showData.res.body);
|
||||
if(showDataJSON.status){
|
||||
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
|
||||
process.exit(1);
|
||||
}
|
||||
else if(!showData.items || showData.items.length<1){
|
||||
else if(!showDataJSON.items || showDataJSON.items.length<1){
|
||||
console.log('[ERROR] Show not found\n');
|
||||
process.exit(0);
|
||||
}
|
||||
showData = showData.items[0];
|
||||
console.log('[#%s] %s (%s)',showData.id,showData.title,showData.releaseYear);
|
||||
const showDataItem = showDataJSON.items[0];
|
||||
console.log('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
|
||||
// show episodes
|
||||
let qs = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: parseInt(argv.s, 10) };
|
||||
let qs: {
|
||||
limit: number,
|
||||
sort: string,
|
||||
sort_direction: string,
|
||||
title_id: number,
|
||||
language?: string
|
||||
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: argv.s as number };
|
||||
if(argv.alt){ qs.language = 'English'; }
|
||||
let episodesData = await getData({
|
||||
baseUrl: api_host,
|
||||
|
|
@ -155,24 +158,26 @@ async function getShow(){
|
|||
querystring: qs,
|
||||
token: token,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
debug: argv.debug,
|
||||
});
|
||||
if(!episodesData.ok){return;}
|
||||
if(!episodesData.ok || !episodesData.res){return;}
|
||||
|
||||
|
||||
let epsDataArr = JSON.parse(episodesData.res.body).items;
|
||||
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
|
||||
let epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
|
||||
let epSelEpsTxt = [], epSelList, typeIdLen = 0, epIdLen = 4;
|
||||
|
||||
const parseEpStr = (epStr) => {
|
||||
epStr = epStr.match(epNumRegex);
|
||||
if(epStr.length > 2){
|
||||
epStr = [...epStr].splice(1);
|
||||
epStr[0] = epStr[0] ? epStr[0] : '';
|
||||
return epStr;
|
||||
const parseEpStr = (epStr: string) => {
|
||||
const match = epStr.match(epNumRegex);
|
||||
if (!match) {
|
||||
console.error('[ERROR] No match found')
|
||||
return ['', '']
|
||||
}
|
||||
else return [ '', epStr[0] ];
|
||||
if(match.length > 2){
|
||||
const spliced = [...match].splice(1);
|
||||
spliced[0] = spliced[0] ? spliced[0] : '';
|
||||
return spliced;
|
||||
}
|
||||
else return [ '', match[0] ];
|
||||
};
|
||||
|
||||
epsDataArr = epsDataArr.map(e => {
|
||||
|
|
@ -192,23 +197,23 @@ async function getShow(){
|
|||
return e;
|
||||
});
|
||||
|
||||
epSelList = parseSelect(argv.e);
|
||||
epSelList = parseSelect(argv.e as string);
|
||||
|
||||
let fnSlug = [], is_selected = false;
|
||||
|
||||
let eps = epsDataArr;
|
||||
epsDataArr.sort((a, b) => {
|
||||
if (a.item.seasonOrder < b.item.seasonOrder && a.id < b.id) {
|
||||
if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (a.item.seasonOrder > b.item.seasonOrder && a.id > b.id) {
|
||||
if (a.item.seasonOrder > b.item.seasonOrder && a.id.localeCompare(b.id) > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for(let e in eps){
|
||||
eps[e].id_split[1] = parseInt(eps[e].id_split[1]).toString().padStart(epIdLen, '0');
|
||||
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(epIdLen, '0');
|
||||
let epStrId = eps[e].id_split.join('');
|
||||
// select
|
||||
is_selected = false;
|
||||
9
modules/iso639.d.ts
vendored
Normal file
9
modules/iso639.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
declare module 'iso-639' {
|
||||
export type iso639Type = {
|
||||
[key: string]: {
|
||||
'639-1'?: string,
|
||||
'639-2'?: string
|
||||
}
|
||||
}
|
||||
export const iso_639_2: iso639Type;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
const yargs = require('yargs');
|
||||
import yargs from 'yargs';
|
||||
|
||||
const availableFilenameVars = [
|
||||
'title',
|
||||
|
|
@ -12,9 +12,11 @@ const availableFilenameVars = [
|
|||
const subLang = ['enUS', 'esLA', 'ptBR'];
|
||||
const dubLang = ['enUS', 'esLA', 'ptBR', 'zhMN', 'jaJP'];
|
||||
|
||||
const appArgv = (cfg) => {
|
||||
const appArgv = (cfg: {
|
||||
[key: string]: unknown
|
||||
}) => {
|
||||
// init
|
||||
const parseDefault = (key, _default) => {
|
||||
const parseDefault = (key: string, _default: unknown) => {
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
|
||||
return cfg[key];
|
||||
} else
|
||||
|
|
@ -207,6 +209,13 @@ const appArgv = (cfg) => {
|
|||
type: 'number',
|
||||
default: parseDefault('timeout', 60 * 1000)
|
||||
})
|
||||
.option('debug', {
|
||||
group: 'Utilities:',
|
||||
describe: 'Used to enter debug mode. Please use this flag when opening an issue to get more information'
|
||||
+ '\n!Be careful! - Your token might be exposed so make sure to delete it!',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
})
|
||||
// help
|
||||
.option('help', {
|
||||
alias: 'h',
|
||||
|
|
@ -222,7 +231,7 @@ const appArgv = (cfg) => {
|
|||
])
|
||||
|
||||
// --
|
||||
.argv;
|
||||
.parseSync();
|
||||
// Resolve unwanted arrays
|
||||
if (argv.allDubs)
|
||||
argv.dub = dubLang;
|
||||
|
|
@ -230,7 +239,7 @@ const appArgv = (cfg) => {
|
|||
argv.subLang = subLang;
|
||||
for (let key in argv) {
|
||||
if (argv[key] instanceof Array && !(key === 'subLang' || key === 'dub')) {
|
||||
argv[key] = argv[key].pop();
|
||||
argv[key] = (argv[key] as Array<unknown>).pop();
|
||||
}
|
||||
}
|
||||
return argv;
|
||||
|
|
@ -238,7 +247,7 @@ const appArgv = (cfg) => {
|
|||
|
||||
const showHelp = yargs.showHelp;
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
appArgv,
|
||||
showHelp,
|
||||
availableFilenameVars,
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
const path = require('path');
|
||||
const yaml = require('yaml');
|
||||
const fs = require('fs-extra');
|
||||
const { lookpath } = require('lookpath');
|
||||
|
||||
// new-cfg
|
||||
const workingDir = process.pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
|
||||
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
|
||||
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
|
||||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||
const tokenFile = path.join(workingDir, 'config', 'token');
|
||||
|
||||
const loadYamlCfgFile = (file, isSess) => {
|
||||
if(fs.existsSync(`${file}.user.yml`) && !isSess){
|
||||
file += '.user';
|
||||
}
|
||||
file += '.yml';
|
||||
if(fs.existsSync(file)){
|
||||
try{
|
||||
return yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR]', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const loadCfg = () => {
|
||||
// load cfgs
|
||||
const cfg = {
|
||||
bin: '',
|
||||
dir: loadYamlCfgFile(dirCfgFile),
|
||||
cli: loadYamlCfgFile(cliCfgFile),
|
||||
};
|
||||
// check each cfg object
|
||||
for(const ctype of Object.keys(cfg)){
|
||||
if(typeof cfg[ctype] !== 'object' || cfg[ctype] === null || Array.isArray(cfg[ctype])){
|
||||
cfg[ctype] = {};
|
||||
}
|
||||
}
|
||||
// set defaults for dirs
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
content: '${wdir}/videos/',
|
||||
trash: '${wdir}/videos/_trash/',
|
||||
};
|
||||
for(const dir of Object.keys(defaultDirs)){
|
||||
if(!Object.prototype.hasOwnProperty.call(cfg.dir, dir) || typeof cfg.dir[dir] != 'string'){
|
||||
cfg.dir[dir] = defaultDirs[dir];
|
||||
}
|
||||
if (!path.isAbsolute(cfg.dir[dir])){
|
||||
if(cfg.dir[dir].match(/^\${wdir}/)){
|
||||
cfg.dir[dir] = cfg.dir[dir].replace(/^\${wdir}/, '');
|
||||
}
|
||||
cfg.dir[dir] = path.join(workingDir, cfg.dir[dir]);
|
||||
}
|
||||
}
|
||||
if(!fs.existsSync(cfg.dir.content)){
|
||||
try{
|
||||
fs.ensureDirSync(cfg.dir.content);
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Content directory not accessible!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!fs.existsSync(cfg.dir.trash)){
|
||||
cfg.dir.trash = cfg.dir.content;
|
||||
}
|
||||
// output
|
||||
return cfg;
|
||||
};
|
||||
|
||||
const loadBinCfg = async () => {
|
||||
let binCfg = loadYamlCfgFile(binCfgFile);
|
||||
// binaries
|
||||
const defaultBin = {
|
||||
ffmpeg: '${wdir}/bin/ffmpeg/ffmpeg',
|
||||
mkvmerge: '${wdir}/bin/mkvtoolnix/mkvmerge',
|
||||
};
|
||||
for(const dir of Object.keys(defaultBin)){
|
||||
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
|
||||
binCfg[dir] = defaultBin[dir];
|
||||
}
|
||||
if (!path.isAbsolute(binCfg[dir]) && binCfg[dir].match(/^\${wdir}/)){
|
||||
binCfg[dir] = binCfg[dir].replace(/^\${wdir}/, '');
|
||||
binCfg[dir] = path.join(workingDir, binCfg[dir]);
|
||||
}
|
||||
binCfg[dir] = await lookpath(binCfg[dir]);
|
||||
binCfg[dir] = binCfg[dir] ? binCfg[dir] : false;
|
||||
if(!binCfg[dir]){
|
||||
const binFile = await lookpath(path.basename(defaultBin[dir]));
|
||||
binCfg[dir] = binFile ? binFile : binCfg[dir];
|
||||
}
|
||||
}
|
||||
return binCfg;
|
||||
};
|
||||
|
||||
const loadFuniToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile, true);
|
||||
if (token === null) token = false;
|
||||
else if (token.token === null) token = false;
|
||||
else token = token.token;
|
||||
// info if token not set
|
||||
if(!token){
|
||||
console.log('[INFO] Token not set!\n');
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveFuniToken = (data) => {
|
||||
const cfgFolder = path.dirname(tokenFile);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loadCfg,
|
||||
loadBinCfg,
|
||||
loadFuniToken,
|
||||
saveFuniToken,
|
||||
};
|
||||
148
modules/module.cfg-loader.ts
Normal file
148
modules/module.cfg-loader.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
import fs from 'fs-extra';
|
||||
import { lookpath } from 'lookpath';
|
||||
|
||||
// new-cfg
|
||||
const workingDir = (process as NodeJS.Process & {
|
||||
pkg?: unknown
|
||||
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
|
||||
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
|
||||
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
|
||||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||
const tokenFile = path.join(workingDir, 'config', 'token');
|
||||
|
||||
const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => {
|
||||
if(fs.existsSync(`${file}.user.yml`) && !isSess){
|
||||
file += '.user';
|
||||
}
|
||||
file += '.yml';
|
||||
if(fs.existsSync(file)){
|
||||
try{
|
||||
return yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR]', e);
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
return {} as T;
|
||||
};
|
||||
|
||||
export type ConfigObject = {
|
||||
dir: {
|
||||
content: string,
|
||||
trash: string,
|
||||
fonts: string;
|
||||
},
|
||||
bin: {
|
||||
ffmpeg?: string,
|
||||
mkvmerge?: string
|
||||
},
|
||||
cli: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
const loadCfg = () : ConfigObject => {
|
||||
// load cfgs
|
||||
const defaultCfg: ConfigObject = {
|
||||
bin: {},
|
||||
dir: loadYamlCfgFile<{
|
||||
content: string,
|
||||
trash: string,
|
||||
fonts: string
|
||||
}>(dirCfgFile),
|
||||
cli: loadYamlCfgFile<{
|
||||
[key: string]: any
|
||||
}>(cliCfgFile),
|
||||
};
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
content: '${wdir}/videos/',
|
||||
trash: '${wdir}/videos/_trash/',
|
||||
};
|
||||
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
|
||||
defaultCfg.dir = defaultDirs;
|
||||
}
|
||||
|
||||
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
|
||||
for (const key of keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
|
||||
defaultCfg.dir[key] = defaultDirs[key];
|
||||
}
|
||||
if (!path.isAbsolute(defaultCfg.dir[key])) {
|
||||
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
if(!fs.existsSync(defaultCfg.dir.content)){
|
||||
try{
|
||||
fs.ensureDirSync(defaultCfg.dir.content);
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Content directory not accessible!');
|
||||
return defaultCfg;
|
||||
}
|
||||
}
|
||||
if(!fs.existsSync(defaultCfg.dir.trash)){
|
||||
defaultCfg.dir.trash = defaultCfg.dir.content;
|
||||
}
|
||||
// output
|
||||
return defaultCfg;
|
||||
};
|
||||
|
||||
const loadBinCfg = async () => {
|
||||
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
|
||||
// binaries
|
||||
const defaultBin = {
|
||||
ffmpeg: '${wdir}/bin/ffmpeg/ffmpeg',
|
||||
mkvmerge: '${wdir}/bin/mkvtoolnix/mkvmerge',
|
||||
};
|
||||
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
|
||||
for(const dir of keys){
|
||||
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
|
||||
binCfg[dir] = defaultBin[dir];
|
||||
}
|
||||
if (!path.isAbsolute(binCfg[dir] as string) && (binCfg[dir] as string).match(/^\${wdir}/)){
|
||||
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
|
||||
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
|
||||
}
|
||||
binCfg[dir] = await lookpath(binCfg[dir] as string);
|
||||
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
|
||||
if(!binCfg[dir]){
|
||||
const binFile = await lookpath(path.basename(defaultBin[dir]));
|
||||
binCfg[dir] = binFile ? binFile : binCfg[dir];
|
||||
}
|
||||
}
|
||||
return binCfg;
|
||||
};
|
||||
|
||||
const loadFuniToken = () => {
|
||||
const loadedToken = loadYamlCfgFile<{
|
||||
token?: string
|
||||
}>(tokenFile, true);
|
||||
let token: false|string = false;
|
||||
if (loadedToken && loadedToken.token)
|
||||
token = loadedToken.token;
|
||||
// info if token not set
|
||||
if(!token){
|
||||
console.log('[INFO] Token not set!\n');
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveFuniToken = (data: {
|
||||
token?: string
|
||||
}) => {
|
||||
const cfgFolder = path.dirname(tokenFile);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
export { loadBinCfg, loadCfg, loadFuniToken, saveFuniToken };
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
const got = require('got');
|
||||
|
||||
// Used for future updates
|
||||
// const argv = require('../funi').argv;
|
||||
//
|
||||
// const lang = {
|
||||
// 'ptBR': {
|
||||
// langCode: 'pt-BR',
|
||||
// regionCode: 'BR'
|
||||
// },
|
||||
// 'esLA': {
|
||||
// langCode: 'es-LA',
|
||||
// regionCode: 'MX'
|
||||
// }
|
||||
// };
|
||||
|
||||
// do req
|
||||
const getData = async (options) => {
|
||||
let regionHeaders = {};
|
||||
|
||||
|
||||
let gOptions = {
|
||||
url: options.url,
|
||||
headers: {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
|
||||
'Accept-Encoding': 'gzip',
|
||||
...regionHeaders
|
||||
}
|
||||
};
|
||||
if(options.responseType) {
|
||||
gOptions.responseType = options.responseType;
|
||||
}
|
||||
if(options.baseUrl){
|
||||
gOptions.prefixUrl = options.baseUrl;
|
||||
gOptions.url = gOptions.url.replace(/^\//,'');
|
||||
}
|
||||
if(options.querystring){
|
||||
gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`;
|
||||
}
|
||||
if(options.auth){
|
||||
gOptions.method = 'POST';
|
||||
gOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
gOptions.headers['Origin'] = 'https://www.funimation.com';
|
||||
gOptions.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01';
|
||||
gOptions.headers['Accept-Encoding'] = 'gzip, deflate, br';
|
||||
gOptions.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0';
|
||||
gOptions.body = `username=${encodeURIComponent(options.auth.user)}&password=${encodeURIComponent(options.auth.pass)}`;
|
||||
}
|
||||
if(options.useToken && options.token){
|
||||
gOptions.headers.Authorization = `Token ${options.token}`;
|
||||
}
|
||||
if(options.dinstid){
|
||||
gOptions.headers.devicetype = 'Android Phone';
|
||||
}
|
||||
// debug
|
||||
gOptions.hooks = {
|
||||
beforeRequest: [
|
||||
(gotOpts) => {
|
||||
if(options.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(gotOpts);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
try {
|
||||
let res = await got(gOptions);
|
||||
if(res.body && (options.responseType !== 'buffer' && res.body.match(/^</))){
|
||||
throw { name: 'HTMLError', res };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
res,
|
||||
};
|
||||
}
|
||||
catch(error){
|
||||
if(options.debug){
|
||||
console.log(error);
|
||||
}
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[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);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = getData;
|
||||
129
modules/module.getdata.ts
Normal file
129
modules/module.getdata.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import got, { OptionsOfUnknownResponseBody, ReadError, Response, ResponseType } from 'got';
|
||||
|
||||
// Used for future updates
|
||||
// const argv = require('../funi').argv;
|
||||
//
|
||||
// const lang = {
|
||||
// 'ptBR': {
|
||||
// langCode: 'pt-BR',
|
||||
// regionCode: 'BR'
|
||||
// },
|
||||
// 'esLA': {
|
||||
// langCode: 'es-LA',
|
||||
// regionCode: 'MX'
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
export type Options = {
|
||||
url: string,
|
||||
responseType?: ResponseType,
|
||||
baseUrl?: string,
|
||||
querystring?: Record<string, any>,
|
||||
auth?: {
|
||||
user: string,
|
||||
pass: string
|
||||
},
|
||||
useToken?: boolean,
|
||||
token?: string|boolean,
|
||||
dinstid?: boolean,
|
||||
debug?: boolean
|
||||
}
|
||||
const getData = async <T = string>(options: Options) => {
|
||||
const regionHeaders = {};
|
||||
|
||||
|
||||
const gOptions = {
|
||||
url: options.url,
|
||||
headers: {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
|
||||
'Accept-Encoding': 'gzip',
|
||||
...regionHeaders
|
||||
}
|
||||
} as OptionsOfUnknownResponseBody;
|
||||
if(options.responseType) {
|
||||
gOptions.responseType = options.responseType;
|
||||
}
|
||||
if(options.baseUrl){
|
||||
gOptions.prefixUrl = options.baseUrl;
|
||||
gOptions.url = gOptions.url?.toString().replace(/^\//,'');
|
||||
}
|
||||
if(options.querystring){
|
||||
gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`;
|
||||
}
|
||||
if(options.auth){
|
||||
gOptions.method = 'POST';
|
||||
const newHeaders = {
|
||||
...gOptions.headers,
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'Origin': 'https://ww.funimation.com',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'
|
||||
};
|
||||
gOptions.headers = newHeaders;
|
||||
gOptions.body = `username=${encodeURIComponent(options.auth.user)}&password=${encodeURIComponent(options.auth.pass)}`;
|
||||
}
|
||||
if(options.useToken && options.token){
|
||||
gOptions.headers = {
|
||||
...gOptions.headers,
|
||||
Authorization: `Token ${options.token}`
|
||||
};
|
||||
}
|
||||
if(options.dinstid){
|
||||
gOptions.headers = {
|
||||
...gOptions.headers,
|
||||
devicetype: 'Android Phone'
|
||||
};
|
||||
}
|
||||
// debug
|
||||
gOptions.hooks = {
|
||||
beforeRequest: [
|
||||
(gotOpts) => {
|
||||
if(options.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(gotOpts);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
try {
|
||||
const res = await got(gOptions);
|
||||
if(res.body && (options.responseType !== 'buffer' && (res.body as string).match(/^</))){
|
||||
throw { name: 'HTMLError', res };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
res: {
|
||||
...res,
|
||||
body: res.body as T
|
||||
},
|
||||
};
|
||||
}
|
||||
catch(_error){
|
||||
const error = _error as {
|
||||
name: string,
|
||||
} & ReadError & {
|
||||
res: Response<unknown>
|
||||
};
|
||||
if(options.debug){
|
||||
console.log(error);
|
||||
}
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[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);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default getData;
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
const iso639 = require('iso-639');
|
||||
|
||||
/**
|
||||
* @param {Array<object>} bin config paths
|
||||
* @param {boolean} use mp4 format
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
// check mergers programs
|
||||
const checkMerger = (bin, useMP4format) => {
|
||||
const merger = {
|
||||
MKVmerge: bin.mkvmerge,
|
||||
FFmpeg: bin.ffmpeg,
|
||||
};
|
||||
if( !useMP4format && !merger.MKVmerge ){
|
||||
console.log('[WARN] MKVMerge not found, skip using this...');
|
||||
merger.MKVmerge = false;
|
||||
}
|
||||
if( !merger.MKVmerge && !merger.FFmpeg || useMP4format && !merger.FFmpeg ){
|
||||
console.log('[WARN] FFmpeg not found, skip using this...');
|
||||
merger.FFmpeg = false;
|
||||
}
|
||||
return merger;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array<object>} videoAndAudio
|
||||
* @param {Array<object>} onlyVid
|
||||
* @param {Array<object>} onlyAudio
|
||||
* @param {Array<object>} subtitles
|
||||
* @param {string} output
|
||||
* @returns {string}
|
||||
*/
|
||||
const buildCommandFFmpeg = (simul, videoAndAudio, onlyVid, onlyAudio, subtitles, output) => {
|
||||
let args = [];
|
||||
let metaData = [];
|
||||
|
||||
let index = 0;
|
||||
let audioIndex = 0;
|
||||
let hasVideo = false;
|
||||
for (let vid of videoAndAudio) {
|
||||
args.push(`-i "${vid.path}"`);
|
||||
if (!hasVideo) {
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${getLanguageCode(vid.lang, vid.lang)}`);
|
||||
metaData.push(`-metadata:s:v:${index} title="[Funimation]"`);
|
||||
hasVideo = true;
|
||||
} else {
|
||||
metaData.push(`-map ${index}:a`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${getLanguageCode(vid.lang, vid.lang)}`);
|
||||
}
|
||||
audioIndex++;
|
||||
index++;
|
||||
}
|
||||
|
||||
for (let vid of onlyVid) {
|
||||
if (!hasVideo) {
|
||||
args.push(`-i "${vid.path}"`);
|
||||
metaData.push(`-map ${index} -map -${index}:a`);
|
||||
metaData.push(`-metadata:s:v:${index} title="[Funimation]"`);
|
||||
hasVideo = true;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let aud of onlyAudio) {
|
||||
args.push(`-i "${aud.path}"`);
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${getLanguageCode(aud.lang, aud.lang)}`);
|
||||
index++;
|
||||
audioIndex++;
|
||||
}
|
||||
|
||||
for (let index in subtitles) {
|
||||
let sub = subtitles[index];
|
||||
args.push(`-i "${sub.file}"`);
|
||||
}
|
||||
|
||||
args.push(...metaData);
|
||||
args.push(...subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||
args.push(
|
||||
'-c:v copy',
|
||||
'-c:a copy'
|
||||
);
|
||||
args.push(output.split('.').pop().toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass');
|
||||
args.push(...subtitles.map((sub, subindex) => `-metadata:s:${index + subindex} language=${getLanguageCode(sub.language)}`));
|
||||
args.push(`"${output}"`);
|
||||
return args.join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} videoFile
|
||||
* @param {object} audioFile
|
||||
* @param {Array<object>} subtitles
|
||||
* @returns {string}
|
||||
*/
|
||||
const buildCommandMkvMerge = (simul, videoAndAudio, onlyVid, onlyAudio, subtitles, output) => {
|
||||
let args = [];
|
||||
|
||||
let hasVideo = false;
|
||||
|
||||
args.push(`-o "${output}"`);
|
||||
args.push(
|
||||
'--no-date',
|
||||
'--disable-track-statistics-tags',
|
||||
'--engage no_variable_data',
|
||||
);
|
||||
|
||||
for (let vid of onlyVid) {
|
||||
if (!hasVideo) {
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--no-audio'
|
||||
);
|
||||
let trackName = subDict[vid.lang] + (simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${getLanguageCode(vid.lang, vid.lang)}`);
|
||||
hasVideo = true;
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
}
|
||||
|
||||
for (let vid of videoAndAudio) {
|
||||
if (!hasVideo) {
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--audio-tracks 1'
|
||||
);
|
||||
let trackName = subDict[vid.lang] + (simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push('--track-name', `1:"${trackName}"`);
|
||||
args.push(`--language 1:${getLanguageCode(vid.lang, vid.lang)}`);
|
||||
hasVideo = true;
|
||||
} else {
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 1'
|
||||
);
|
||||
let trackName = subDict[vid.lang] + (simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `1:"${trackName}"`);
|
||||
args.push(`--language 1:${getLanguageCode(vid.lang, vid.lang)}`);
|
||||
}
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
|
||||
for (let aud of onlyAudio) {
|
||||
let trackName = subDict[aud.lang] + (simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${getLanguageCode(aud.lang, aud.lang)}`);
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 0'
|
||||
);
|
||||
args.push(`"${aud.path}"`);
|
||||
}
|
||||
|
||||
if (subtitles.length > 0) {
|
||||
for (let subObj of subtitles) {
|
||||
let trackName = subDict[subObj.language] + (simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push('--language', `0:${getLanguageCode(subObj.language)}`);
|
||||
args.push(`"${subObj.file}"`);
|
||||
}
|
||||
} else {
|
||||
args.push(
|
||||
'--no-subtitles',
|
||||
'--no-attachments'
|
||||
);
|
||||
}
|
||||
|
||||
return args.join(' ');
|
||||
};
|
||||
const subDict = {
|
||||
'en': 'English (United State)',
|
||||
'es': 'Español (Latinoamericano)',
|
||||
'pt': 'Português (Brasil)',
|
||||
'ja': '日本語',
|
||||
'cmn': '官話'
|
||||
};
|
||||
const getLanguageCode = (from, _default = 'eng') => {
|
||||
if (from === 'cmn') return 'chi';
|
||||
for (let lang in iso639.iso_639_2) {
|
||||
let langObj = iso639.iso_639_2[lang];
|
||||
if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) {
|
||||
return langObj['639-2'];
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
checkMerger,
|
||||
getLanguageCode,
|
||||
buildCommandFFmpeg,
|
||||
buildCommandMkvMerge
|
||||
};
|
||||
182
modules/module.merger.ts
Normal file
182
modules/module.merger.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import * as iso639 from 'iso-639';
|
||||
|
||||
export type MergerInput = {
|
||||
path: string,
|
||||
lang: string,
|
||||
}
|
||||
|
||||
export type SubtitleInput = {
|
||||
language: string,
|
||||
file: string,
|
||||
}
|
||||
|
||||
export type MergerOptions = {
|
||||
videoAndAudio: MergerInput[],
|
||||
onlyVid: MergerInput[],
|
||||
onlyAudio: MergerInput[],
|
||||
subtitels: SubtitleInput[],
|
||||
output: string,
|
||||
simul?: boolean
|
||||
}
|
||||
|
||||
class Merger {
|
||||
private subDict = {
|
||||
'en': 'English (United State)',
|
||||
'es': 'Español (Latinoamericano)',
|
||||
'pt': 'Português (Brasil)',
|
||||
'ja': '日本語',
|
||||
'cmn': '官話'
|
||||
} as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
constructor(private options: MergerOptions) {}
|
||||
|
||||
public FFmpeg() : string {
|
||||
const args = [];
|
||||
const metaData = [];
|
||||
|
||||
let index = 0;
|
||||
let audioIndex = 0;
|
||||
let hasVideo = false;
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
args.push(`-i "${vid.path}"`);
|
||||
if (!hasVideo) {
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${Merger.getLanguageCode(vid.lang, vid.lang)}`);
|
||||
metaData.push(`-metadata:s:v:${index} title="[Funimation]"`);
|
||||
hasVideo = true;
|
||||
} else {
|
||||
metaData.push(`-map ${index}:a`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${Merger.getLanguageCode(vid.lang, vid.lang)}`);
|
||||
}
|
||||
audioIndex++;
|
||||
index++;
|
||||
}
|
||||
|
||||
for (const vid of this.options.onlyVid) {
|
||||
if (!hasVideo) {
|
||||
args.push(`-i "${vid.path}"`);
|
||||
metaData.push(`-map ${index} -map -${index}:a`);
|
||||
metaData.push(`-metadata:s:v:${index} title="[Funimation]"`);
|
||||
hasVideo = true;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const aud of this.options.onlyAudio) {
|
||||
args.push(`-i "${aud.path}"`);
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${Merger.getLanguageCode(aud.lang, aud.lang)}`);
|
||||
index++;
|
||||
audioIndex++;
|
||||
}
|
||||
|
||||
for (const index in this.options.subtitels) {
|
||||
const sub = this.options.subtitels[index];
|
||||
args.push(`-i "${sub.file}"`);
|
||||
}
|
||||
|
||||
args.push(...metaData);
|
||||
args.push(...this.options.subtitels.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||
args.push(
|
||||
'-c:v copy',
|
||||
'-c:a copy'
|
||||
);
|
||||
args.push(this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass');
|
||||
args.push(...this.options.subtitels.map((sub, subindex) => `-metadata:s:${index + subindex} language=${Merger.getLanguageCode(sub.language)}`));
|
||||
args.push(`"${this.options.output}"`);
|
||||
return args.join(' ');
|
||||
}
|
||||
|
||||
public static getLanguageCode = (from: string, _default = 'eng'): string => {
|
||||
if (from === 'cmn') return 'chi';
|
||||
for (const lang in iso639.iso_639_2) {
|
||||
const langObj = iso639.iso_639_2[lang];
|
||||
if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) {
|
||||
return langObj['639-2'] as string;
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
};
|
||||
|
||||
public MkvMerge = () => {
|
||||
const args = [];
|
||||
|
||||
let hasVideo = false;
|
||||
|
||||
args.push(`-o "${this.options.output}"`);
|
||||
args.push(
|
||||
'--no-date',
|
||||
'--disable-track-statistics-tags',
|
||||
'--engage no_variable_data',
|
||||
);
|
||||
|
||||
for (const vid of this.options.onlyVid) {
|
||||
if (!hasVideo) {
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--no-audio'
|
||||
);
|
||||
const trackName = this.subDict[vid.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${Merger.getLanguageCode(vid.lang, vid.lang)}`);
|
||||
hasVideo = true;
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
if (!hasVideo) {
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--audio-tracks 1'
|
||||
);
|
||||
const trackName = this.subDict[vid.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push('--track-name', `1:"${trackName}"`);
|
||||
args.push(`--language 1:${Merger.getLanguageCode(vid.lang, vid.lang)}`);
|
||||
hasVideo = true;
|
||||
} else {
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 1'
|
||||
);
|
||||
const trackName = this.subDict[vid.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `1:"${trackName}"`);
|
||||
args.push(`--language 1:${Merger.getLanguageCode(vid.lang, vid.lang)}`);
|
||||
}
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
|
||||
for (const aud of this.options.onlyAudio) {
|
||||
const trackName = this.subDict[aud.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${Merger.getLanguageCode(aud.lang, aud.lang)}`);
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 0'
|
||||
);
|
||||
args.push(`"${aud.path}"`);
|
||||
}
|
||||
|
||||
if (this.options.subtitels.length > 0) {
|
||||
for (const subObj of this.options.subtitels) {
|
||||
const trackName = this.subDict[subObj.language] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]');
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push('--language', `0:${Merger.getLanguageCode(subObj.language)}`);
|
||||
args.push(`"${subObj.file}"`);
|
||||
}
|
||||
} else {
|
||||
args.push(
|
||||
'--no-subtitles',
|
||||
'--no-attachments'
|
||||
);
|
||||
}
|
||||
|
||||
return args.join(' ');
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default Merger;
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
/**
|
||||
* @param {string} selectString
|
||||
* @returns {{
|
||||
* isSelected: (val: string) => boolean,
|
||||
* values: string[]
|
||||
* }}
|
||||
*/
|
||||
module.exports = (selectString) => {
|
||||
if (!selectString)
|
||||
return {
|
||||
values: [],
|
||||
isSelected: () => false
|
||||
};
|
||||
let parts = selectString.split(',');
|
||||
let select = [];
|
||||
|
||||
|
||||
parts.forEach(part => {
|
||||
if (part.includes('-')) {
|
||||
let splits = part.split('-');
|
||||
if (splits.length !== 2) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
let firstPart = splits[0];
|
||||
let 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}"`);
|
||||
return;
|
||||
}
|
||||
let letters = firstPart.substring(0, match[0].length);
|
||||
let number = parseInt(firstPart.substring(match[0].length));
|
||||
let b = parseInt(splits[1]);
|
||||
if (isNaN(number) || isNaN(b)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = number; i <= b; i++) {
|
||||
select.push(`${letters}${i}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
let a = parseInt(firstPart);
|
||||
let b = parseInt(splits[1]);
|
||||
if (isNaN(a) || isNaN(b)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = a; i <= b; i++) {
|
||||
select.push(`${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
let 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}"`);
|
||||
return;
|
||||
}
|
||||
let letters = part.substring(0, match[0].length);
|
||||
let number = parseInt(part.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
select.push(`${letters}${number}`);
|
||||
} else {
|
||||
select.push(`${parseInt(part)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
values: select,
|
||||
isSelected: (st) => {
|
||||
let match = st.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
return false;
|
||||
}
|
||||
let letter = st.substring(0, match[0].length);
|
||||
let number = parseInt(st.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return select.includes(`${letter}${number}`);
|
||||
} else {
|
||||
return select.includes(`${parseInt(st)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
94
modules/module.parseSelect.ts
Normal file
94
modules/module.parseSelect.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
const parseSelect = (selectString: string) : {
|
||||
isSelected: (val: string) => boolean,
|
||||
values: string[]
|
||||
} => {
|
||||
if (!selectString)
|
||||
return {
|
||||
values: [],
|
||||
isSelected: () => false
|
||||
};
|
||||
const parts = selectString.split(',');
|
||||
const select: string[] = [];
|
||||
|
||||
|
||||
parts.forEach(part => {
|
||||
if (part.includes('-')) {
|
||||
const splits = part.split('-');
|
||||
if (splits.length !== 2) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstPart = splits[0];
|
||||
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}"`);
|
||||
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}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = number; i <= b; i++) {
|
||||
select.push(`${letters}${i}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
const a = parseInt(firstPart);
|
||||
const b = parseInt(splits[1]);
|
||||
if (isNaN(a) || isNaN(b)) {
|
||||
console.log(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = a; i <= b; i++) {
|
||||
select.push(`${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
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}"`);
|
||||
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}"`);
|
||||
return;
|
||||
}
|
||||
select.push(`${letters}${number}`);
|
||||
} else {
|
||||
select.push(`${parseInt(part)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
values: select,
|
||||
isSelected: (st) => {
|
||||
const match = st.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
return false;
|
||||
}
|
||||
const letter = st.substring(0, match[0].length);
|
||||
const number = parseInt(st.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return select.includes(`${letter}${number}`);
|
||||
} else {
|
||||
return select.includes(`${parseInt(st)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default parseSelect;
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
// vtt loader
|
||||
function loadVtt(vttStr) {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||
let data = [], lineBuf = [], record = null;
|
||||
// check lines
|
||||
for (let l of lines) {
|
||||
let m = l.match(rx);
|
||||
if (m) {
|
||||
if (lineBuf.length > 0) {
|
||||
lineBuf.pop();
|
||||
}
|
||||
if (record !== null) {
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
record = {
|
||||
time_start: m[1],
|
||||
time_end: m[2],
|
||||
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => (p[c[0]] = c[1]) && p, {}),
|
||||
};
|
||||
lineBuf = [];
|
||||
continue;
|
||||
}
|
||||
lineBuf.push(l);
|
||||
}
|
||||
if (record !== null) {
|
||||
if (lineBuf[lineBuf.length - 1] === '') {
|
||||
lineBuf.pop();
|
||||
}
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// ass specific
|
||||
function convertToAss(vttStr, lang, fontSize){
|
||||
let ass = [
|
||||
'\ufeff[Script Info]',
|
||||
`Title: ${lang}`,
|
||||
'ScriptType: v4.00+',
|
||||
'PlayResX: 1280',
|
||||
'PlayResY: 720',
|
||||
'WrapStyle: 0',
|
||||
'ScaledBorderAndShadow: yes',
|
||||
'',
|
||||
'[V4+ Styles]',
|
||||
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, '
|
||||
+ 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, '
|
||||
+ 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
|
||||
`Style: Main,Noto Sans,${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`,
|
||||
`Style: MainTop,Noto Sans,${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10`,
|
||||
'',
|
||||
'[Events]',
|
||||
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
|
||||
];
|
||||
|
||||
let vttData = loadVtt(vttStr);
|
||||
for (let l of vttData) {
|
||||
l = convertToAssLine(l, 'Main');
|
||||
ass = ass.concat(l);
|
||||
}
|
||||
|
||||
return ass.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToAssLine(l, style) {
|
||||
let start = convertTime(l.time_start);
|
||||
let end = convertTime(l.time_end);
|
||||
let text = convertToAssText(l.text);
|
||||
|
||||
// debugger
|
||||
if (l.ext_param.line === '7%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
if (l.ext_param.line === '10%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
|
||||
}
|
||||
|
||||
function convertToAssText(text) {
|
||||
text = text
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\n/g, '\\N')
|
||||
.replace(/\\N +/g, '\\N')
|
||||
.replace(/ +\\N/g, '\\N')
|
||||
.replace(/(\\N)+/g, '\\N')
|
||||
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
|
||||
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
|
||||
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/<[^>]>/g, '')
|
||||
.replace(/\\N$/, '')
|
||||
.replace(/ +$/, '');
|
||||
return text;
|
||||
}
|
||||
|
||||
// srt specific
|
||||
function convertToSrt(vttStr){
|
||||
let srt = [], srtLineIdx = 0;
|
||||
|
||||
let vttData = loadVtt(vttStr);
|
||||
for (let l of vttData) {
|
||||
srtLineIdx++;
|
||||
l = convertToSrtLine(l, srtLineIdx);
|
||||
srt = srt.concat(l);
|
||||
}
|
||||
|
||||
return srt.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToSrtLine(l, idx) {
|
||||
let bom = idx == 1 ? '\ufeff' : '';
|
||||
let start = convertTime(l.time_start, true);
|
||||
let end = convertTime(l.time_end, true);
|
||||
let text = l.text;
|
||||
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
|
||||
}
|
||||
|
||||
// time parser
|
||||
function convertTime(time, srtFormat) {
|
||||
let mTime = time.match(/([\d:]*)\.?(\d*)/);
|
||||
if (!mTime){
|
||||
return srtFormat ? '00:00:00,000' : '0:00:00.00';
|
||||
}
|
||||
return toSubsTime(mTime[0], srtFormat);
|
||||
}
|
||||
|
||||
function toSubsTime(str, srtFormat) {
|
||||
|
||||
let n = [], x, sx;
|
||||
x = str.split(/[:.]/).map(x => Number(x));
|
||||
|
||||
let msLen = srtFormat ? 3 : 2;
|
||||
let hLen = srtFormat ? 2 : 1;
|
||||
|
||||
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
|
||||
sx = x[0]*60*60 + x[1]*60 + x[2] + Number(x[3]);
|
||||
sx = sx.toFixed(msLen).split('.');
|
||||
|
||||
|
||||
n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen));
|
||||
sx = Number(sx[0]);
|
||||
|
||||
n.unshift(padTimeNum(':', sx%60, 2));
|
||||
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
|
||||
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
|
||||
|
||||
return n.join('');
|
||||
}
|
||||
|
||||
function padTimeNum(sep, input, pad){
|
||||
return sep + ('' + input).padStart(pad, '0');
|
||||
}
|
||||
|
||||
// export module
|
||||
module.exports = (vttStr, toSrt, lang = 'English', fontSize) => {
|
||||
const convert = toSrt ? convertToSrt : convertToAss;
|
||||
return convert(vttStr, lang, fontSize);
|
||||
};
|
||||
174
modules/module.vttconvert.ts
Normal file
174
modules/module.vttconvert.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// vtt loader
|
||||
export type Record = {
|
||||
text?: string,
|
||||
time_start?: string,
|
||||
time_end?: string,
|
||||
ext_param?: unknown
|
||||
};
|
||||
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;
|
||||
// check lines
|
||||
for (const l of lines) {
|
||||
const m = l.match(rx);
|
||||
if (m) {
|
||||
if (lineBuf.length > 0) {
|
||||
lineBuf.pop();
|
||||
}
|
||||
if (record !== null) {
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
record = {
|
||||
time_start: m[1],
|
||||
time_end: m[2],
|
||||
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}),
|
||||
};
|
||||
lineBuf = [];
|
||||
continue;
|
||||
}
|
||||
lineBuf.push(l);
|
||||
}
|
||||
if (record !== null) {
|
||||
if (lineBuf[lineBuf.length - 1] === '') {
|
||||
lineBuf.pop();
|
||||
}
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// ass specific
|
||||
function convertToAss(vttStr: string, lang: string, fontSize: number){
|
||||
let ass = [
|
||||
'\ufeff[Script Info]',
|
||||
`Title: ${lang}`,
|
||||
'ScriptType: v4.00+',
|
||||
'PlayResX: 1280',
|
||||
'PlayResY: 720',
|
||||
'WrapStyle: 0',
|
||||
'ScaledBorderAndShadow: yes',
|
||||
'',
|
||||
'[V4+ Styles]',
|
||||
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, '
|
||||
+ 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, '
|
||||
+ 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
|
||||
`Style: Main,Noto Sans,${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`,
|
||||
`Style: MainTop,Noto Sans,${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10`,
|
||||
'',
|
||||
'[Events]',
|
||||
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
|
||||
];
|
||||
|
||||
const vttData = loadVtt(vttStr);
|
||||
for (const l of vttData) {
|
||||
const line = convertToAssLine(l, 'Main');
|
||||
ass = ass.concat(line);
|
||||
}
|
||||
|
||||
return ass.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToAssLine(l: Record, style: string) {
|
||||
const start = convertTime(l.time_start as string);
|
||||
const end = convertTime(l.time_end as string);
|
||||
const text = convertToAssText(l.text as string);
|
||||
|
||||
// debugger
|
||||
if ((l.ext_param as any).line === '7%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
if ((l.ext_param as any).line === '10%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
|
||||
}
|
||||
|
||||
function convertToAssText(text: string) {
|
||||
text = text
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\n/g, '\\N')
|
||||
.replace(/\\N +/g, '\\N')
|
||||
.replace(/ +\\N/g, '\\N')
|
||||
.replace(/(\\N)+/g, '\\N')
|
||||
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
|
||||
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
|
||||
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/<[^>]>/g, '')
|
||||
.replace(/\\N$/, '')
|
||||
.replace(/ +$/, '');
|
||||
return text;
|
||||
}
|
||||
|
||||
// srt specific
|
||||
function convertToSrt(vttStr: string){
|
||||
let srt: string[] = [], srtLineIdx = 0;
|
||||
|
||||
const vttData = loadVtt(vttStr);
|
||||
for (const l of vttData) {
|
||||
srtLineIdx++;
|
||||
const line = convertToSrtLine(l, srtLineIdx);
|
||||
srt = srt.concat(line);
|
||||
}
|
||||
|
||||
return srt.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToSrtLine(l: Record, idx: number) : string {
|
||||
const bom = idx == 1 ? '\ufeff' : '';
|
||||
const start = convertTime(l.time_start as string, true);
|
||||
const end = convertTime(l.time_end as string, true);
|
||||
const text = l.text;
|
||||
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
|
||||
}
|
||||
|
||||
// time parser
|
||||
function convertTime(time: string, srtFormat = false) {
|
||||
const mTime = time.match(/([\d:]*)\.?(\d*)/);
|
||||
if (!mTime){
|
||||
return srtFormat ? '00:00:00,000' : '0:00:00.00';
|
||||
}
|
||||
return toSubsTime(mTime[0], srtFormat);
|
||||
}
|
||||
|
||||
function toSubsTime(str: string, srtFormat: boolean) : string {
|
||||
|
||||
const n = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
|
||||
|
||||
const msLen = srtFormat ? 3 : 2;
|
||||
const hLen = srtFormat ? 2 : 1;
|
||||
|
||||
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
|
||||
sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]);
|
||||
sx = sx.toFixed(msLen).split('.');
|
||||
|
||||
|
||||
n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen));
|
||||
sx = Number(sx[0]);
|
||||
|
||||
n.unshift(padTimeNum(':', sx%60, 2));
|
||||
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
|
||||
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
|
||||
|
||||
return n.join('');
|
||||
}
|
||||
|
||||
function padTimeNum(sep: string, input: string|number , pad:number){
|
||||
return sep + ('' + input).padStart(pad, '0');
|
||||
}
|
||||
|
||||
// export module
|
||||
const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number) => {
|
||||
const convert = toSrt ? convertToSrt : convertToAss;
|
||||
return convert(vttStr, lang, fontSize);
|
||||
};
|
||||
export default _default;
|
||||
4058
package-lock.json
generated
4058
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -36,9 +36,15 @@
|
|||
"yargs": "^17.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/yargs": "^17.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.2.0",
|
||||
"@typescript-eslint/parser": "^5.2.0",
|
||||
"eslint": "^7.30.0",
|
||||
"pkg": "^5.3.3",
|
||||
"removeNPMAbsolutePaths": "^2.0.0"
|
||||
"removeNPMAbsolutePaths": "^2.0.0",
|
||||
"typescript": "^4.4.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build-win64": "node modules/build win64",
|
||||
|
|
|
|||
69
tsconfig.json
Normal file
69
tsconfig.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue