Typescript
This commit is contained in:
commit
f0745d199c
38 changed files with 7345 additions and 2046 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
lib
|
||||||
|
/videos/*.ts
|
||||||
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
.github/workflows/eslint.yml
vendored
24
.github/workflows/eslint.yml
vendored
|
|
@ -1,24 +0,0 @@
|
||||||
name: eslint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: 'npm'
|
|
||||||
- run: npm i
|
|
||||||
- run: npx eslint .
|
|
||||||
2
.github/workflows/release-matrix.yml
vendored
2
.github/workflows/release-matrix.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
upload_url: ${{ github.event.release.upload_url }}
|
||||||
asset_name: ${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}-${{ env.BUILD_TYPE }}.7z
|
asset_name: ${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}-${{ env.BUILD_TYPE }}.7z
|
||||||
asset_path: ./_builds/${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}-${{ env.BUILD_TYPE }}.7z
|
asset_path: ./lib/_builds/${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}-${{ env.BUILD_TYPE }}.7z
|
||||||
asset_content_type: application/x-7z-compressed
|
asset_content_type: application/x-7z-compressed
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
33
.github/workflows/test.yml
vendored
Normal file
33
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Style and build test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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'
|
||||||
|
- run: npm i
|
||||||
|
- run: npx 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'
|
||||||
|
- run: npm i
|
||||||
|
- run: npm run test
|
||||||
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,16 +3,17 @@
|
||||||
/_builds/*
|
/_builds/*
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/videos/*.json
|
/videos/*.json
|
||||||
|
/videos/*.ts
|
||||||
.DS_Store
|
.DS_Store
|
||||||
ffmpeg
|
ffmpeg
|
||||||
mkvmerge
|
mkvmerge
|
||||||
token.yml
|
token.yml
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
*.dll
|
||||||
*.ts
|
|
||||||
*.mkv
|
*.mkv
|
||||||
*.mp4
|
*.mp4
|
||||||
*.ass
|
*.ass
|
||||||
*.srt
|
*.srt
|
||||||
*.resume
|
*.resume
|
||||||
*.user.yml
|
*.user.yml
|
||||||
|
lib
|
||||||
|
|
|
||||||
4
@types/downloadedFile.d.ts
vendored
Normal file
4
@types/downloadedFile.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type DownloadedFile = {
|
||||||
|
path: string,
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
391
@types/episode.d.ts
vendored
Normal file
391
@types/episode.d.ts
vendored
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
|
export interface EpisodeData {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
mediaDict: { [key: string]: string };
|
||||||
|
episodeSlug: string;
|
||||||
|
starRating: number;
|
||||||
|
parent: EpisodeDataParent;
|
||||||
|
number: string;
|
||||||
|
description: string;
|
||||||
|
filename: string;
|
||||||
|
seriesBanner: string;
|
||||||
|
media: Media[];
|
||||||
|
externalItemId: string;
|
||||||
|
contentId: string;
|
||||||
|
metaItems: MetaItems;
|
||||||
|
thumb: string;
|
||||||
|
type: Type;
|
||||||
|
default: { [key: string]: Default };
|
||||||
|
published: boolean;
|
||||||
|
versions: VersionClass[];
|
||||||
|
mediaCategory: string;
|
||||||
|
order: number;
|
||||||
|
seriesVersions: any[];
|
||||||
|
source: Source;
|
||||||
|
ids: EpisodeDataIDS;
|
||||||
|
runtime: string;
|
||||||
|
siblings: PreviousSeasonEpisode[];
|
||||||
|
seriesTitle: string;
|
||||||
|
seriesSlug: string;
|
||||||
|
next: Next;
|
||||||
|
previousSeasonEpisode: PreviousSeasonEpisode;
|
||||||
|
seasonTitle: string;
|
||||||
|
quality: Quality;
|
||||||
|
ratings: Array<string[]>;
|
||||||
|
languages: TitleElement[];
|
||||||
|
releaseDate: string;
|
||||||
|
historicalSelections: HistoricalSelections;
|
||||||
|
userRating: UserRating;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Default {
|
||||||
|
items: DefaultItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultItem {
|
||||||
|
languages: string[];
|
||||||
|
territories: string[];
|
||||||
|
version: null;
|
||||||
|
value: Value[];
|
||||||
|
devices: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Value {
|
||||||
|
name: MetaType;
|
||||||
|
value: string;
|
||||||
|
label: Label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Label {
|
||||||
|
Rating = 'Rating',
|
||||||
|
RatingSystem = 'Rating System',
|
||||||
|
ReleaseDate = 'Release Date',
|
||||||
|
Synopsis = 'Synopsis',
|
||||||
|
SynopsisType = 'Synopsis Type',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MetaType {
|
||||||
|
Rating = 'rating',
|
||||||
|
RatingSystemType = 'RatingSystemType',
|
||||||
|
ReleaseDate = 'release-date',
|
||||||
|
Synopsis = 'synopsis',
|
||||||
|
Synopsistype = 'synopsistype',
|
||||||
|
VideoRatingType = 'VideoRatingType',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoricalSelections {
|
||||||
|
version: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeDataIDS {
|
||||||
|
externalShowId: string;
|
||||||
|
externalSeasonId: string;
|
||||||
|
externalEpisodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TitleElement {
|
||||||
|
Empty = '',
|
||||||
|
English = 'English',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Media {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
experienceType: string;
|
||||||
|
created: string;
|
||||||
|
createdBy: string;
|
||||||
|
itemFieldData: Next;
|
||||||
|
keyPath: string;
|
||||||
|
filename: string;
|
||||||
|
complianceStatus: null;
|
||||||
|
events: any[];
|
||||||
|
clients: string[];
|
||||||
|
qcStatus: null;
|
||||||
|
qcStatusDate: null;
|
||||||
|
image: string;
|
||||||
|
thumb: string;
|
||||||
|
ext: string;
|
||||||
|
avails: Avail[];
|
||||||
|
version: string;
|
||||||
|
startTimecode: null;
|
||||||
|
endTimecode: null;
|
||||||
|
versionId: string;
|
||||||
|
mediaType: string;
|
||||||
|
status: string;
|
||||||
|
languages: LanguageClass[];
|
||||||
|
territories: any[];
|
||||||
|
devices: any[];
|
||||||
|
keyType: string;
|
||||||
|
purpose: null;
|
||||||
|
externalItemId: null | string;
|
||||||
|
proxyId: null;
|
||||||
|
externalDbId: null;
|
||||||
|
mediaChildren: MediaChild[];
|
||||||
|
isDefault: boolean;
|
||||||
|
parent: MediaChildParent;
|
||||||
|
filePath: null | string;
|
||||||
|
mediaInfo: Next;
|
||||||
|
type: string;
|
||||||
|
approved: boolean;
|
||||||
|
mediaKey: string;
|
||||||
|
itemFields: any[];
|
||||||
|
source: Source;
|
||||||
|
fieldData: Next;
|
||||||
|
sourceId: null | string;
|
||||||
|
timecodeOverride: null;
|
||||||
|
seriesTitle: string;
|
||||||
|
episodeTitle: string;
|
||||||
|
genre: any[];
|
||||||
|
txDate: string;
|
||||||
|
description: string;
|
||||||
|
synopsis: string;
|
||||||
|
resolution: null;
|
||||||
|
restrictedAccess: boolean;
|
||||||
|
createdById: string;
|
||||||
|
userIdsWithAccess: any[];
|
||||||
|
runtime?: number;
|
||||||
|
language?: TitleElement;
|
||||||
|
purchased: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Avail {
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
endDate: string;
|
||||||
|
startDate: string;
|
||||||
|
ids: AvailIDS;
|
||||||
|
originalAirDate: null;
|
||||||
|
physicalReleaseDate: null;
|
||||||
|
preorderDate: null;
|
||||||
|
language: TitleElement;
|
||||||
|
territory: string;
|
||||||
|
territoryCode: string;
|
||||||
|
license: string;
|
||||||
|
parentAvail: null;
|
||||||
|
item: number;
|
||||||
|
version: string;
|
||||||
|
applyToLevel: null;
|
||||||
|
availLevel: string;
|
||||||
|
availDisplayCode: string;
|
||||||
|
availStatus: string;
|
||||||
|
bundleOnly: boolean;
|
||||||
|
contentOwnerOrganization: string;
|
||||||
|
currency: null;
|
||||||
|
price: null;
|
||||||
|
purchase: string;
|
||||||
|
priceValue: string;
|
||||||
|
resolutionFormat: null;
|
||||||
|
runtimeMilliseconds: null;
|
||||||
|
seasonOrEpisodeNumber: null;
|
||||||
|
tmsid: null;
|
||||||
|
deviceList: string;
|
||||||
|
tvodSku: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailIDS {
|
||||||
|
externalSeasonId: string;
|
||||||
|
externalAsianId: null;
|
||||||
|
externalShowId: string;
|
||||||
|
externalEpisodeId: string;
|
||||||
|
externalEnglishId: string;
|
||||||
|
externalAlphaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Next = Record<string, unknown>
|
||||||
|
|
||||||
|
export interface LanguageClass {
|
||||||
|
code: string;
|
||||||
|
id: number;
|
||||||
|
title: TitleElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaChild {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
experienceType: string;
|
||||||
|
created: string;
|
||||||
|
createdBy: string;
|
||||||
|
itemFieldData: Next;
|
||||||
|
keyPath: null;
|
||||||
|
filename: string;
|
||||||
|
complianceStatus: null;
|
||||||
|
events: any[];
|
||||||
|
clients: string[];
|
||||||
|
qcStatus: null;
|
||||||
|
qcStatusDate: null;
|
||||||
|
image: string;
|
||||||
|
ext: string;
|
||||||
|
avails: any[];
|
||||||
|
version: string;
|
||||||
|
startTimecode: null;
|
||||||
|
endTimecode: null;
|
||||||
|
versionId: string;
|
||||||
|
mediaType: string;
|
||||||
|
status: string;
|
||||||
|
languages: LanguageClass[];
|
||||||
|
territories: any[];
|
||||||
|
devices: any[];
|
||||||
|
keyType: string;
|
||||||
|
purpose: null;
|
||||||
|
externalItemId: string;
|
||||||
|
proxyId: null;
|
||||||
|
externalDbId: null;
|
||||||
|
mediaChildren: any[];
|
||||||
|
isDefault: boolean;
|
||||||
|
parent: MediaChildParent;
|
||||||
|
filePath: string;
|
||||||
|
mediaInfo: MediaInfo;
|
||||||
|
type: string;
|
||||||
|
approved: boolean;
|
||||||
|
mediaKey: null;
|
||||||
|
itemFields: any[];
|
||||||
|
source: Source;
|
||||||
|
fieldData: Next;
|
||||||
|
sourceId: null;
|
||||||
|
timecodeOverride: null;
|
||||||
|
seriesTitle: string;
|
||||||
|
episodeTitle: string;
|
||||||
|
genre: any[];
|
||||||
|
txDate: string;
|
||||||
|
description: string;
|
||||||
|
synopsis: string;
|
||||||
|
resolution: null | string;
|
||||||
|
restrictedAccess: boolean;
|
||||||
|
createdById: string;
|
||||||
|
userIdsWithAccess: any[];
|
||||||
|
language: TitleElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaInfo {
|
||||||
|
imageAspectRatio: null | string;
|
||||||
|
format: string;
|
||||||
|
scanMode: null | string;
|
||||||
|
burnedInSubtitleLanguage: string;
|
||||||
|
screenAspectRatio: null | string;
|
||||||
|
subtitleFormat: null | string;
|
||||||
|
subtitleContent: null | string;
|
||||||
|
frameHeight: number | null;
|
||||||
|
frameWidth: number | null;
|
||||||
|
video: Video;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Video {
|
||||||
|
codecId: null | string;
|
||||||
|
container: null | string;
|
||||||
|
encodingRate: number | null;
|
||||||
|
frameRate: null | string;
|
||||||
|
height: number | null;
|
||||||
|
width: number | null;
|
||||||
|
duration: number | null;
|
||||||
|
bitRate: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaChildParent {
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
catalogParent: CatalogParent;
|
||||||
|
slug: string;
|
||||||
|
grandparentId: number;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogParent {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Source {
|
||||||
|
Dbb = 'dbb',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetaItems {
|
||||||
|
items: Items;
|
||||||
|
filters: Filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filters {
|
||||||
|
territory: any[];
|
||||||
|
language: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Items {
|
||||||
|
'release-date': AnimationProductionStudio;
|
||||||
|
rating: AnimationProductionStudio;
|
||||||
|
synopsis: AnimationProductionStudio;
|
||||||
|
'animation-production-studio': AnimationProductionStudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnimationProductionStudio {
|
||||||
|
items: AnimationProductionStudioItem[];
|
||||||
|
label: string;
|
||||||
|
id: number;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnimationProductionStudioItem {
|
||||||
|
id: number;
|
||||||
|
metaType: MetaType;
|
||||||
|
metaTypeId: string;
|
||||||
|
client: null;
|
||||||
|
languages: TitleElement;
|
||||||
|
territories: string;
|
||||||
|
devices: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
value: Value[];
|
||||||
|
approved: boolean;
|
||||||
|
version: null;
|
||||||
|
source: Source;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeDataParent {
|
||||||
|
seasonId: number;
|
||||||
|
seasonNumber: string;
|
||||||
|
title: string;
|
||||||
|
titleSlug: string;
|
||||||
|
titleType: string;
|
||||||
|
titleId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreviousSeasonEpisode {
|
||||||
|
seasonTitle?: string;
|
||||||
|
mediaCategory: Type;
|
||||||
|
thumb: string;
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
number: string;
|
||||||
|
id: number;
|
||||||
|
version: string[];
|
||||||
|
order: number;
|
||||||
|
slug: string;
|
||||||
|
season?: number;
|
||||||
|
languages?: TitleElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Type {
|
||||||
|
Episode = 'episode',
|
||||||
|
Ova = 'ova',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Quality {
|
||||||
|
quality: string;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserRating {
|
||||||
|
overall: number;
|
||||||
|
ja: number;
|
||||||
|
eng: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionClass {
|
||||||
|
compliance_approved: boolean;
|
||||||
|
title: string;
|
||||||
|
version_id: string;
|
||||||
|
is_default: boolean;
|
||||||
|
runtime: string;
|
||||||
|
external_id: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
26
@types/hls-download.d.ts
vendored
Normal file
26
@types/hls-download.d.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
declare module 'hls-download' {
|
||||||
|
export default class hlsDownload {
|
||||||
|
constructor(options: {
|
||||||
|
m3u8json: {
|
||||||
|
segments: Record<string, unknown>[],
|
||||||
|
mediaSequence?: number,
|
||||||
|
},
|
||||||
|
output?: string,
|
||||||
|
threads?: number,
|
||||||
|
retries?: number,
|
||||||
|
offset?: number,
|
||||||
|
baseurl?: string,
|
||||||
|
proxy?: string,
|
||||||
|
skipInit?: boolean,
|
||||||
|
timeout?: number
|
||||||
|
})
|
||||||
|
async download() : Promise<{
|
||||||
|
ok: boolean,
|
||||||
|
parts: {
|
||||||
|
first: number,
|
||||||
|
total: number,
|
||||||
|
compleated: number
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
169
@types/items.d.ts
vendored
Normal file
169
@types/items.d.ts
vendored
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
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 ContentType {
|
||||||
|
Episode = 'episode',
|
||||||
|
Ova = 'ova',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDs {
|
||||||
|
externalShowId: ID;
|
||||||
|
externalSeasonId: ExternalSeasonID;
|
||||||
|
externalEpisodeId: string;
|
||||||
|
externalAsianId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
seasonTitle: string;
|
||||||
|
seasonId: number;
|
||||||
|
episodeOrder: number;
|
||||||
|
episodeSlug: string;
|
||||||
|
created: Date;
|
||||||
|
titleSlug: string;
|
||||||
|
episodeNum: string;
|
||||||
|
episodeId: number;
|
||||||
|
titleId: number;
|
||||||
|
seasonNum: string;
|
||||||
|
ratings: Array<string[]>;
|
||||||
|
showImage: string;
|
||||||
|
titleName: string;
|
||||||
|
runtime: string;
|
||||||
|
episodeName: string;
|
||||||
|
seasonOrder: number;
|
||||||
|
titleExternalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 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 Version {
|
||||||
|
Simulcast = 'Simulcast',
|
||||||
|
Uncut = 'Uncut',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MostRecentSvodJpnUs = Record<string, any>
|
||||||
|
|
||||||
|
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/pkg.d.ts
vendored
Normal file
3
@types/pkg.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'pkg' {
|
||||||
|
export async function exec(config: string[]);
|
||||||
|
}
|
||||||
3
@types/removeNPMAbsolutePaths.d.ts
vendored
Normal file
3
@types/removeNPMAbsolutePaths.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'removeNPMAbsolutePaths' {
|
||||||
|
export default async function modulesCleanup(path: string);
|
||||||
|
}
|
||||||
5
@types/sei-helper.d.ts
vendored
Normal file
5
@types/sei-helper.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module 'sei-helper' {
|
||||||
|
export async function question(qStr: string): string;
|
||||||
|
export function cleanupFilename(str: string): string;
|
||||||
|
export function exec(str: string, str1: string, str2: string);
|
||||||
|
}
|
||||||
28
@types/streamData.d.ts
vendored
Normal file
28
@types/streamData.d.ts
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
|
export interface StreamData {
|
||||||
|
items: Item[];
|
||||||
|
watchHistorySaveInterval: number;
|
||||||
|
errors?: Error[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Error {
|
||||||
|
detail: string,
|
||||||
|
code: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
src: string;
|
||||||
|
kind: string;
|
||||||
|
isPromo: boolean;
|
||||||
|
videoType: string;
|
||||||
|
aips: Aip[];
|
||||||
|
experienceId: string;
|
||||||
|
showAds: boolean;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Aip {
|
||||||
|
out: number;
|
||||||
|
in: number;
|
||||||
|
}
|
||||||
7
@types/subtitleObject.d.ts
vendored
Normal file
7
@types/subtitleObject.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export type Subtitle = {
|
||||||
|
path: string,
|
||||||
|
ext: string,
|
||||||
|
langName: string,
|
||||||
|
language: string,
|
||||||
|
file?: string
|
||||||
|
}
|
||||||
777
funi.js
777
funi.js
|
|
@ -1,777 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// modules build-in
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// package json
|
|
||||||
const packageJson = require('./package.json');
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
|
|
||||||
// extra
|
|
||||||
const appYargs = require('./modules/module.app-args');
|
|
||||||
const yamlCfg = require('./modules/module.cfg-loader');
|
|
||||||
const vttConvert = require('./modules/module.vttconvert');
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
|
|
||||||
// check page
|
|
||||||
if(!isNaN(parseInt(argv.p, 10)) && parseInt(argv.p, 10) > 0){
|
|
||||||
argv.p = parseInt(argv.p, 10);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
argv.p = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn variables
|
|
||||||
let title = '',
|
|
||||||
showTitle = '',
|
|
||||||
fnEpNum = 0,
|
|
||||||
fnOutput = [],
|
|
||||||
season = 0,
|
|
||||||
tsDlPath = [],
|
|
||||||
stDlPath = [];
|
|
||||||
|
|
||||||
// main
|
|
||||||
(async () => {
|
|
||||||
// load binaries
|
|
||||||
cfg.bin = await yamlCfg.loadBinCfg();
|
|
||||||
// select mode
|
|
||||||
if(argv.auth){
|
|
||||||
auth();
|
|
||||||
}
|
|
||||||
else if(argv.search){
|
|
||||||
searchShow();
|
|
||||||
}
|
|
||||||
else if(argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0){
|
|
||||||
getShow();
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
appYargs.showHelp();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// auth
|
|
||||||
async function auth(){
|
|
||||||
let authOpts = {};
|
|
||||||
authOpts.user = await shlp.question('[Q] LOGIN/EMAIL');
|
|
||||||
authOpts.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){
|
|
||||||
console.log('[INFO] Authentication success, your token: %s%s\n', authData.token.slice(0,8),'*'.repeat(32));
|
|
||||||
yamlCfg.saveFuniToken({'token': authData.token});
|
|
||||||
}
|
|
||||||
else if(authData.error){
|
|
||||||
console.log('[ERROR]%s\n', authData.error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// search show
|
|
||||||
async function searchShow(){
|
|
||||||
let qs = {unique: true, limit: 100, q: argv.search, offset: (argv.p-1)*1000 };
|
|
||||||
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}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(searchData.items && searchData.items.hits){
|
|
||||||
let shows = searchData.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get show
|
|
||||||
async function getShow(){
|
|
||||||
// show main data
|
|
||||||
let showData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: `/source/catalog/title/${parseInt(argv.s, 10)}`,
|
|
||||||
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);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
else if(!showData.items || showData.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);
|
|
||||||
// show episodes
|
|
||||||
let qs = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: parseInt(argv.s, 10) };
|
|
||||||
if(argv.alt){ qs.language = 'English'; }
|
|
||||||
let episodesData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: '/funimation/episodes/',
|
|
||||||
querystring: qs,
|
|
||||||
token: token,
|
|
||||||
useToken: true,
|
|
||||||
useProxy: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!episodesData.ok){return;}
|
|
||||||
|
|
||||||
|
|
||||||
let epsDataArr = 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;
|
|
||||||
}
|
|
||||||
else return [ '', epStr[0] ];
|
|
||||||
};
|
|
||||||
|
|
||||||
epsDataArr = epsDataArr.map(e => {
|
|
||||||
const baseId = e.ids.externalAsianId ? e.ids.externalAsianId : e.ids.externalEpisodeId;
|
|
||||||
e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), '');
|
|
||||||
if(e.id.match(epNumRegex)){
|
|
||||||
const epMatch = parseEpStr(e.id);
|
|
||||||
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
|
|
||||||
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
|
|
||||||
e.id_split = epMatch;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
|
|
||||||
console.log('[ERROR] FAILED TO PARSE: ', e.id);
|
|
||||||
e.id_split = [ 'ZZZ', 9999 ];
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
|
|
||||||
epSelList = parseSelect(argv.e);
|
|
||||||
|
|
||||||
let fnSlug = [], is_selected = false;
|
|
||||||
|
|
||||||
let eps = epsDataArr;
|
|
||||||
epsDataArr.sort((a, b) => {
|
|
||||||
if (a.item.seasonOrder < b.item.seasonOrder && a.id < b.id) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.item.seasonOrder > b.item.seasonOrder && a.id > b.id) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
for(let e in eps){
|
|
||||||
eps[e].id_split[1] = parseInt(eps[e].id_split[1]).toString().padStart(epIdLen, '0');
|
|
||||||
let epStrId = eps[e].id_split.join('');
|
|
||||||
// select
|
|
||||||
is_selected = false;
|
|
||||||
if (argv.all || epSelList.isSelected(epStrId)) {
|
|
||||||
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug});
|
|
||||||
epSelEpsTxt.push(epStrId);
|
|
||||||
is_selected = true;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
is_selected = false;
|
|
||||||
}
|
|
||||||
// console vars
|
|
||||||
let tx_snum = eps[e].item.seasonNum==1?'':` S${eps[e].item.seasonNum}`;
|
|
||||||
let tx_type = eps[e].mediaCategory != 'episode' ? eps[e].mediaCategory : '';
|
|
||||||
let tx_enum = eps[e].item.episodeNum && eps[e].item.episodeNum !== '' ?
|
|
||||||
`#${(eps[e].item.episodeNum < 10 ? '0' : '')+eps[e].item.episodeNum}` : '#'+eps[e].item.episodeId;
|
|
||||||
let qua_str = eps[e].quality.height ? eps[e].quality.quality + eps[e].quality.height : 'UNK';
|
|
||||||
let aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : '';
|
|
||||||
let rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??';
|
|
||||||
// console string
|
|
||||||
eps[e].id_split[0] = eps[e].id_split[0].padStart(typeIdLen, ' ');
|
|
||||||
epStrId = eps[e].id_split.join('');
|
|
||||||
let conOut = `[${epStrId}] `;
|
|
||||||
conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `;
|
|
||||||
conOut += `(${rtm_str}) [${qua_str+aud_str}]`;
|
|
||||||
conOut += is_selected ? ' (selected)' : '';
|
|
||||||
conOut += eps.length-1 == e ? '\n' : '';
|
|
||||||
console.log(conOut);
|
|
||||||
}
|
|
||||||
if(fnSlug.length < 1){
|
|
||||||
console.log('[INFO] Episodes not selected!\n');
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
|
|
||||||
for(let fnEp=0;fnEp<fnSlug.length;fnEp++){
|
|
||||||
await getEpisode(fnSlug[fnEp]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEpisode(fnSlug){
|
|
||||||
let episodeData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`,
|
|
||||||
token: token,
|
|
||||||
useToken: true,
|
|
||||||
useProxy: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!episodeData.ok){return;}
|
|
||||||
let ep = JSON.parse(episodeData.res.body).items[0], streamIds = [];
|
|
||||||
// build fn
|
|
||||||
showTitle = ep.parent.title;
|
|
||||||
title = ep.title;
|
|
||||||
season = parseInt(ep.parent.seasonNumber);
|
|
||||||
if(ep.mediaCategory != 'Episode'){
|
|
||||||
ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
|
|
||||||
}
|
|
||||||
fnEpNum = isNaN(parseInt(ep.number)) ? ep.number : parseInt(ep.number);
|
|
||||||
|
|
||||||
// is uncut
|
|
||||||
let uncut = {
|
|
||||||
Japanese: false,
|
|
||||||
English: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// end
|
|
||||||
console.log(
|
|
||||||
'[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):');
|
|
||||||
|
|
||||||
// map medias
|
|
||||||
let media = ep.media.map(function(m){
|
|
||||||
if(m.mediaType == 'experience'){
|
|
||||||
if(m.version.match(/uncut/i)){
|
|
||||||
uncut[m.language] = true;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: m.id,
|
|
||||||
language: m.language,
|
|
||||||
version: m.version,
|
|
||||||
type: m.experienceType,
|
|
||||||
subtitles: getSubsUrl(m.mediaChildren),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
return { id: 0, type: '' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const dubType = {
|
|
||||||
'enUS': 'English',
|
|
||||||
'esLA': 'Spanish (Latin Am)',
|
|
||||||
'ptBR': 'Portuguese (Brazil)',
|
|
||||||
'zhMN': 'Chinese (Mandarin, PRC)',
|
|
||||||
'jaJP': 'Japanese'
|
|
||||||
};
|
|
||||||
|
|
||||||
// select
|
|
||||||
stDlPath = [];
|
|
||||||
for(let m of media){
|
|
||||||
let selected = false;
|
|
||||||
if(m.id > 0 && m.type == 'Non-Encrypted'){
|
|
||||||
let dub_type = m.language;
|
|
||||||
let localSubs = [];
|
|
||||||
let selUncut = !argv.simul && uncut[dub_type] && m.version.match(/uncut/i)
|
|
||||||
? true
|
|
||||||
: (!uncut[dub_type] || argv.simul && m.version.match(/simulcast/i) ? true : false);
|
|
||||||
for (let curDub of argv.dub) {
|
|
||||||
if(dub_type == dubType[curDub] && selUncut){
|
|
||||||
streamIds.push({
|
|
||||||
id: m.id,
|
|
||||||
lang: merger.getLanguageCode(curDub, curDub.slice(0, -2))
|
|
||||||
});
|
|
||||||
stDlPath.push(...m.subtitles);
|
|
||||||
localSubs = m.subtitles;
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
|
||||||
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.langName}'`).join(', ')} for subtitles)` : ''
|
|
||||||
}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let already = [];
|
|
||||||
stDlPath = stDlPath.filter(a => {
|
|
||||||
if (already.includes(a.language)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
already.push(a.language);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(streamIds.length <1){
|
|
||||||
console.log('[ERROR] Track not selected\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
tsDlPath = [];
|
|
||||||
for (let streamId of streamIds) {
|
|
||||||
let streamData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: `/source/catalog/video/${streamId.id}/signed`,
|
|
||||||
token: token,
|
|
||||||
dinstid: 'uuid',
|
|
||||||
useToken: true,
|
|
||||||
useProxy: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!streamData.ok){return;}
|
|
||||||
streamData = JSON.parse(streamData.res.body);
|
|
||||||
if(streamData.errors){
|
|
||||||
console.log('[ERROR] Error #%s: %s\n',streamData.errors[0].code,streamData.errors[0].detail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
for(let u in streamData.items){
|
|
||||||
if(streamData.items[u].videoType == 'm3u8'){
|
|
||||||
tsDlPath.push({
|
|
||||||
path: streamData.items[u].src,
|
|
||||||
lang: streamId.lang
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(tsDlPath.length < 1){
|
|
||||||
console.log('[ERROR] Unknown error\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
await downloadStreams();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubsUrl(m){
|
|
||||||
if(argv.nosubs && !argv.sub){
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let subLangs = argv.subLang;
|
|
||||||
|
|
||||||
const subType = {
|
|
||||||
'enUS': 'English',
|
|
||||||
'esLA': 'Spanish (Latin Am)',
|
|
||||||
'ptBR': 'Portuguese (Brazil)'
|
|
||||||
};
|
|
||||||
|
|
||||||
let subLangAvailable = m.some(a => subLangs.some(subLang => a.ext == 'vtt' && a.language === subType[subLang]));
|
|
||||||
|
|
||||||
if (!subLangAvailable) {
|
|
||||||
subLangs = [ 'enUS' ];
|
|
||||||
}
|
|
||||||
|
|
||||||
let found = [];
|
|
||||||
|
|
||||||
for(let i in m){
|
|
||||||
let fpp = m[i].filePath.split('.');
|
|
||||||
let fpe = fpp[fpp.length-1];
|
|
||||||
for (let lang of subLangs) {
|
|
||||||
if(fpe == 'vtt' && m[i].language === subType[lang]) {
|
|
||||||
found.push({
|
|
||||||
path: m[i].filePath,
|
|
||||||
ext: `.${lang}`,
|
|
||||||
langName: subType[lang],
|
|
||||||
language: m[i].languages[0].code || lang.slice(0, 2)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadStreams(){
|
|
||||||
|
|
||||||
// req playlist
|
|
||||||
|
|
||||||
let purvideo = [];
|
|
||||||
let puraudio = [];
|
|
||||||
let audioAndVideo = [];
|
|
||||||
for (let streamPath of tsDlPath) {
|
|
||||||
let plQualityReq = await getData({
|
|
||||||
url: streamPath.path,
|
|
||||||
useProxy: (argv.ssp ? false : true),
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!plQualityReq.ok){return;}
|
|
||||||
|
|
||||||
let plQualityLinkList = m3u8(plQualityReq.res.body);
|
|
||||||
|
|
||||||
let mainServersList = [
|
|
||||||
'vmfst-api.prd.funimationsvc.com',
|
|
||||||
'd33et77evd9bgg.cloudfront.net',
|
|
||||||
'd132fumi6di1wa.cloudfront.net',
|
|
||||||
'funiprod.akamaized.net',
|
|
||||||
];
|
|
||||||
|
|
||||||
let plServerList = [],
|
|
||||||
plStreams = {},
|
|
||||||
plLayersStr = [],
|
|
||||||
plLayersRes = {},
|
|
||||||
plMaxLayer = 1,
|
|
||||||
plNewIds = 1,
|
|
||||||
plAud = { uri: '' };
|
|
||||||
|
|
||||||
// new uris
|
|
||||||
let vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/;
|
|
||||||
if(plQualityLinkList.playlists[0].uri.match(vplReg)){
|
|
||||||
let audioKey = Object.keys(plQualityLinkList.mediaGroups.AUDIO).pop();
|
|
||||||
if(plQualityLinkList.mediaGroups.AUDIO[audioKey]){
|
|
||||||
let audioData = plQualityLinkList.mediaGroups.AUDIO[audioKey],
|
|
||||||
audioEl = Object.keys(audioData);
|
|
||||||
audioData = audioData[audioEl[0]];
|
|
||||||
plAud = { ...audioData, ...{ langStr: audioEl[0] } };
|
|
||||||
}
|
|
||||||
plQualityLinkList.playlists.sort((a, b) => {
|
|
||||||
let av = parseInt(a.uri.match(vplReg)[3]);
|
|
||||||
let bv = parseInt(b.uri.match(vplReg)[3]);
|
|
||||||
if(av > bv){
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (av < bv) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let s of plQualityLinkList.playlists){
|
|
||||||
if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){
|
|
||||||
// set layer and max layer
|
|
||||||
let plLayerId = 0;
|
|
||||||
if(s.uri.match(/_Layer(\d+)\.m3u8/)){
|
|
||||||
plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8/)[1]);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
plLayerId = plNewIds, plNewIds++;
|
|
||||||
}
|
|
||||||
plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
|
|
||||||
// set urls and servers
|
|
||||||
let plUrlDl = s.uri;
|
|
||||||
let plServer = new URL(plUrlDl).host;
|
|
||||||
if(!plServerList.includes(plServer)){
|
|
||||||
plServerList.push(plServer);
|
|
||||||
}
|
|
||||||
if(!Object.keys(plStreams).includes(plServer)){
|
|
||||||
plStreams[plServer] = {};
|
|
||||||
}
|
|
||||||
if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){
|
|
||||||
console.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
plStreams[plServer][plLayerId] = plUrlDl;
|
|
||||||
}
|
|
||||||
// set plLayersStr
|
|
||||||
let plResolution = s.attributes.RESOLUTION;
|
|
||||||
plLayersRes[plLayerId] = plResolution;
|
|
||||||
let plBandwidth = Math.round(s.attributes.BANDWIDTH/1024);
|
|
||||||
if(plLayerId<10){
|
|
||||||
plLayerId = plLayerId.toString().padStart(2,' ');
|
|
||||||
}
|
|
||||||
let qualityStrAdd = `${plLayerId}: ${plResolution.width}x${plResolution.height} (${plBandwidth}KiB/s)`;
|
|
||||||
let qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g,'\\$1'),'m');
|
|
||||||
let qualityStrMatch = !plLayersStr.join('\r\n').match(qualityStrRegx);
|
|
||||||
if(qualityStrMatch){
|
|
||||||
plLayersStr.push(qualityStrAdd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(s.uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let s of mainServersList){
|
|
||||||
if(plServerList.includes(s)){
|
|
||||||
plServerList.splice(plServerList.indexOf(s), 1);
|
|
||||||
plServerList.unshift(s);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof argv.q == 'object' && argv.q.length > 1){
|
|
||||||
argv.q = argv.q[argv.q.length-1];
|
|
||||||
}
|
|
||||||
|
|
||||||
argv.q = argv.q < 1 || argv.q > plMaxLayer ? plMaxLayer : argv.q;
|
|
||||||
|
|
||||||
let plSelectedServer = plServerList[argv.x-1];
|
|
||||||
let plSelectedList = plStreams[plSelectedServer];
|
|
||||||
let videoUrl = argv.x < plServerList.length+1 && plSelectedList[argv.q] ? plSelectedList[argv.q] : '';
|
|
||||||
|
|
||||||
plLayersStr.sort();
|
|
||||||
console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
|
|
||||||
console.log(`[INFO] Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
|
||||||
|
|
||||||
if(videoUrl != ''){
|
|
||||||
console.log(`[INFO] Selected layer: ${argv.q} (${plLayersRes[argv.q].width}x${plLayersRes[argv.q].height}) @ ${plSelectedServer}`);
|
|
||||||
console.log('[INFO] Stream URL:',videoUrl);
|
|
||||||
|
|
||||||
fnOutput = parseFileName(argv.fileName, title, fnEpNum, showTitle, season, plLayersRes[argv.q].width, plLayersRes[argv.q].height);
|
|
||||||
if (fnOutput.length < 1)
|
|
||||||
throw new Error('Invalid path', fnOutput);
|
|
||||||
console.log(`[INFO] Output filename: ${fnOutput.join(path.sep)}.ts`);
|
|
||||||
}
|
|
||||||
else if(argv.x > plServerList.length){
|
|
||||||
console.log('[ERROR] Server not selected!\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[ERROR] Layer not selected!\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dlFailed = false;
|
|
||||||
let dlFailedA = false;
|
|
||||||
|
|
||||||
await fs.promises.mkdir(path.join(cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true });
|
|
||||||
|
|
||||||
video: if (!argv.novids) {
|
|
||||||
if (plAud.uri && (purvideo.length > 0 || audioAndVideo.length > 0)) {
|
|
||||||
break video;
|
|
||||||
} else if (!plAud.uri && (audioAndVideo.some(a => a.lang === streamPath.lang) || puraudio.some(a => a.lang === streamPath.lang))) {
|
|
||||||
break video;
|
|
||||||
}
|
|
||||||
// download video
|
|
||||||
let reqVideo = await getData({
|
|
||||||
url: videoUrl,
|
|
||||||
useProxy: (argv.ssp ? false : true),
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if (!reqVideo.ok) { break video; }
|
|
||||||
|
|
||||||
let chunkList = m3u8(reqVideo.res.body);
|
|
||||||
|
|
||||||
let tsFile = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud.uri ? '' : '.' + streamPath.lang )}`);
|
|
||||||
dlFailed = !await downloadFile(tsFile, chunkList);
|
|
||||||
if (!dlFailed) {
|
|
||||||
if (plAud.uri) {
|
|
||||||
purvideo.push({
|
|
||||||
path: `${tsFile}.ts`,
|
|
||||||
lang: plAud.language
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
audioAndVideo.push({
|
|
||||||
path: `${tsFile}.ts`,
|
|
||||||
lang: streamPath.lang
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[INFO] Skip video downloading...\n');
|
|
||||||
}
|
|
||||||
audio: if (!argv.noaudio && plAud.uri) {
|
|
||||||
// download audio
|
|
||||||
if (audioAndVideo.some(a => a.lang === plAud.language) || puraudio.some(a => a.lang === plAud.language))
|
|
||||||
break audio;
|
|
||||||
let reqAudio = await getData({
|
|
||||||
url: plAud.uri,
|
|
||||||
useProxy: (argv.ssp ? false : true),
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if (!reqAudio.ok) { return; }
|
|
||||||
|
|
||||||
let chunkListA = m3u8(reqAudio.res.body);
|
|
||||||
|
|
||||||
let tsFileA = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language}`);
|
|
||||||
|
|
||||||
dlFailedA = !await downloadFile(tsFileA, chunkListA);
|
|
||||||
if (!dlFailedA)
|
|
||||||
puraudio.push({
|
|
||||||
path: `${tsFileA}.ts`,
|
|
||||||
lang: plAud.language
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add subs
|
|
||||||
let subsExt = !argv.mp4 || argv.mp4 && argv.ass ? '.ass' : '.srt';
|
|
||||||
let addSubs = true;
|
|
||||||
|
|
||||||
// download subtitles
|
|
||||||
if(stDlPath.length > 0){
|
|
||||||
console.log('[INFO] Downloading subtitles...');
|
|
||||||
for (let subObject of stDlPath) {
|
|
||||||
let subsSrc = await getData({
|
|
||||||
url: subObject.path,
|
|
||||||
useProxy: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(subsSrc.ok){
|
|
||||||
let assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.langName, argv.fontSize);
|
|
||||||
subObject.file = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.subtitle${subObject.ext}${subsExt}`);
|
|
||||||
fs.writeFileSync(subObject.file, assData);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[ERROR] Failed to download subtitles!');
|
|
||||||
addSubs = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (addSubs)
|
|
||||||
console.log('[INFO] Subtitles downloaded!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
|
||||||
console.log('\n[INFO] Unable to locate a video AND audio file\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(argv.skipmux){
|
|
||||||
console.log('[INFO] Skipping muxing...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check exec
|
|
||||||
const mergerBin = await merger.checkMerger(cfg.bin, argv.mp4);
|
|
||||||
|
|
||||||
if ( argv.novids ){
|
|
||||||
console.log('[INFO] Video not downloaded. Skip muxing video.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergers
|
|
||||||
if(!argv.mp4 && !mergerBin.MKVmerge){
|
|
||||||
console.log('[WARN] MKVMerge not found...');
|
|
||||||
}
|
|
||||||
if(!mergerBin.MKVmerge && !mergerBin.FFmpeg || argv.mp4 && !mergerBin.MKVmerge){
|
|
||||||
console.log('[WARN] FFmpeg not found...');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!argv.mp4 && mergerBin.MKVmerge){
|
|
||||||
let ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
|
||||||
let command = merger.buildCommandMkvMerge(argv.simul, audioAndVideo, purvideo, puraudio, stDlPath, `${path.join(cfg.dir.content,
|
|
||||||
...fnOutput)}.${ffext}`);
|
|
||||||
shlp.exec('mkvmerge', `"${mergerBin.MKVmerge}"`, command);
|
|
||||||
}
|
|
||||||
else if(mergerBin.FFmpeg){
|
|
||||||
let ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
|
||||||
let command = merger.buildCommandFFmpeg(argv.simul, audioAndVideo, purvideo, puraudio, stDlPath, `${path.join(cfg.dir.content,
|
|
||||||
...fnOutput)}.${ffext}`);
|
|
||||||
shlp.exec('ffmpeg',`"${mergerBin.FFmpeg}"`,command);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('\n[INFO] Done!\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (argv.nocleanup)
|
|
||||||
return;
|
|
||||||
|
|
||||||
audioAndVideo.concat(puraudio).concat(purvideo).forEach(a => fs.unlinkSync(a.path));
|
|
||||||
stDlPath.forEach(subObject => fs.unlinkSync(subObject.file));
|
|
||||||
console.log('\n[INFO] Done!\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadFile(filename, chunkList) {
|
|
||||||
const downloadStatus = await new hlsDownload({
|
|
||||||
m3u8json: chunkList,
|
|
||||||
output: `${filename + '.ts'}`,
|
|
||||||
timeout: argv.timeout,
|
|
||||||
pcount: argv.partsize
|
|
||||||
}).download();
|
|
||||||
|
|
||||||
return downloadStatus.ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} input
|
|
||||||
* @param {string} title
|
|
||||||
* @param {number|string} episode
|
|
||||||
* @param {string} showTitle
|
|
||||||
* @param {number} season
|
|
||||||
* @param {number} width
|
|
||||||
* @param {number} height
|
|
||||||
* @returns {Array<string>}
|
|
||||||
*/
|
|
||||||
function parseFileName(input, title, episode, showTitle, season, width, height) {
|
|
||||||
const varRegex = /\${[A-Za-z1-9]+}/g;
|
|
||||||
const vars = input.match(varRegex);
|
|
||||||
for (let i = 0; i < vars.length; i++) {
|
|
||||||
const type = vars[i];
|
|
||||||
switch (type.slice(2, -1).toLowerCase()) {
|
|
||||||
case 'title':
|
|
||||||
input = input.replace(vars[i], title);
|
|
||||||
break;
|
|
||||||
case 'episode': {
|
|
||||||
if (typeof episode === 'number') {
|
|
||||||
let len = episode.toFixed(0).toString().length;
|
|
||||||
input = input.replace(vars[i], len < argv.numbers ? '0'.repeat(argv.numbers - len) + episode : episode);
|
|
||||||
} else {
|
|
||||||
input = input.replace(vars[i], episode);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'showtitle':
|
|
||||||
input = input.replace(vars[i], showTitle);
|
|
||||||
break;
|
|
||||||
case 'season': {
|
|
||||||
let len = season.toFixed(0).toString().length;
|
|
||||||
input = input.replace(vars[i], len < argv.numbers ? '0'.repeat(argv.numbers - len) + season : season);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'width':
|
|
||||||
input = input.replace(vars[i], width);
|
|
||||||
break;
|
|
||||||
case 'height':
|
|
||||||
input = input.replace(vars[i], height);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return input.split(path.sep).map(a => shlp.cleanupFilename(a));
|
|
||||||
}
|
|
||||||
805
funi.ts
Normal file
805
funi.ts
Normal file
|
|
@ -0,0 +1,805 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// modules build-in
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// package json
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
// program name
|
||||||
|
console.log(`\n=== Funimation Downloader NX ${packageJson.version} ===\n`);
|
||||||
|
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||||
|
|
||||||
|
// modules extra
|
||||||
|
import * as shlp from 'sei-helper';
|
||||||
|
import m3u8 from 'm3u8-parsed';
|
||||||
|
import hlsDownload from 'hls-download';
|
||||||
|
|
||||||
|
// extra
|
||||||
|
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();
|
||||||
|
const token = yamlCfg.loadFuniToken();
|
||||||
|
// cli
|
||||||
|
const argv = appYargs.appArgv(cfg.cli);
|
||||||
|
|
||||||
|
// Import modules after argv has been exported
|
||||||
|
import getData from './modules/module.getdata.js';
|
||||||
|
import merger, { SubtitleInput } from './modules/module.merger';
|
||||||
|
import parseSelect from './modules/module.parseSelect';
|
||||||
|
import { EpisodeData, MediaChild } from './@types/episode';
|
||||||
|
import { Subtitle } from './@types/subtitleObject';
|
||||||
|
import { StreamData } from './@types/streamData';
|
||||||
|
import { DownloadedFile } from './@types/downloadedFile';
|
||||||
|
|
||||||
|
// check page
|
||||||
|
argv.p = 1;
|
||||||
|
|
||||||
|
// fn variables
|
||||||
|
let title = '',
|
||||||
|
showTitle = '',
|
||||||
|
fnEpNum: string|number = 0,
|
||||||
|
fnOutput: string[] = [],
|
||||||
|
season = 0,
|
||||||
|
tsDlPath: {
|
||||||
|
path: string,
|
||||||
|
lang: string,
|
||||||
|
}[] = [],
|
||||||
|
stDlPath: Subtitle[] = [];
|
||||||
|
|
||||||
|
// main
|
||||||
|
(async () => {
|
||||||
|
// load binaries
|
||||||
|
cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
// select mode
|
||||||
|
if(argv.auth){
|
||||||
|
auth();
|
||||||
|
}
|
||||||
|
else if(argv.search){
|
||||||
|
searchShow();
|
||||||
|
}
|
||||||
|
else if(argv.s && argv.s > 0){
|
||||||
|
getShow();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
appYargs.showHelp();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// auth
|
||||||
|
async function auth(){
|
||||||
|
const authOpts = {
|
||||||
|
user: await shlp.question('[Q] LOGIN/EMAIL'),
|
||||||
|
pass: await shlp.question('[Q] PASSWORD ')
|
||||||
|
};
|
||||||
|
const authData = await getData({
|
||||||
|
baseUrl: api_host,
|
||||||
|
url: '/auth/login/',
|
||||||
|
auth: authOpts,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
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));
|
||||||
|
yamlCfg.saveFuniToken({'token': resJSON.token});
|
||||||
|
} else {
|
||||||
|
console.log('[ERROR]%s\n', ' No token found');
|
||||||
|
if (argv.debug) {
|
||||||
|
console.log(resJSON);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// search show
|
||||||
|
async function searchShow(){
|
||||||
|
const qs = {unique: true, limit: 100, q: argv.search, offset: 0 };
|
||||||
|
const searchData = await getData({
|
||||||
|
baseUrl: api_host,
|
||||||
|
url: '/source/funimation/search/auto/',
|
||||||
|
querystring: qs,
|
||||||
|
token: token,
|
||||||
|
useToken: true,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if(!searchData.ok || !searchData.res){return;}
|
||||||
|
const searchDataJSON = JSON.parse(searchData.res.body);
|
||||||
|
if(searchDataJSON.detail){
|
||||||
|
console.log(`[ERROR] ${searchDataJSON.detail}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(searchDataJSON.items && searchDataJSON.items.hits){
|
||||||
|
const shows = searchDataJSON.items.hits;
|
||||||
|
console.log('[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.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get show
|
||||||
|
async function getShow(){
|
||||||
|
// show main data
|
||||||
|
const showData = await getData({
|
||||||
|
baseUrl: api_host,
|
||||||
|
url: `/source/catalog/title/${argv.s}`,
|
||||||
|
token: token,
|
||||||
|
useToken: true,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
// check errors
|
||||||
|
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(!showDataJSON.items || showDataJSON.items.length<1){
|
||||||
|
console.log('[ERROR] Show not found\n');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const showDataItem = showDataJSON.items[0];
|
||||||
|
console.log('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
|
||||||
|
// show episodes
|
||||||
|
const 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'; }
|
||||||
|
const episodesData = await getData({
|
||||||
|
baseUrl: api_host,
|
||||||
|
url: '/funimation/episodes/',
|
||||||
|
querystring: qs,
|
||||||
|
token: token,
|
||||||
|
useToken: true,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if(!episodesData.ok || !episodesData.res){return;}
|
||||||
|
|
||||||
|
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
|
||||||
|
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
|
||||||
|
const epSelEpsTxt = []; let typeIdLen = 0, epIdLen = 4;
|
||||||
|
|
||||||
|
const parseEpStr = (epStr: string) => {
|
||||||
|
const match = epStr.match(epNumRegex);
|
||||||
|
if (!match) {
|
||||||
|
console.error('[ERROR] No match found');
|
||||||
|
return ['', ''];
|
||||||
|
}
|
||||||
|
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 => {
|
||||||
|
const baseId = e.ids.externalAsianId ? e.ids.externalAsianId : e.ids.externalEpisodeId;
|
||||||
|
e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), '');
|
||||||
|
if(e.id.match(epNumRegex)){
|
||||||
|
const epMatch = parseEpStr(e.id);
|
||||||
|
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
|
||||||
|
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
|
||||||
|
e.id_split = epMatch;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
|
||||||
|
console.log('[ERROR] FAILED TO PARSE: ', e.id);
|
||||||
|
e.id_split = [ 'ZZZ', 9999 ];
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const epSelList = parseSelect(argv.e as string);
|
||||||
|
|
||||||
|
const fnSlug: {
|
||||||
|
title: string,
|
||||||
|
episode: string
|
||||||
|
}[] = []; let is_selected = false;
|
||||||
|
|
||||||
|
const eps = epsDataArr;
|
||||||
|
epsDataArr.sort((a, b) => {
|
||||||
|
if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.item.seasonOrder > b.item.seasonOrder && a.id.localeCompare(b.id) > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for(const e in eps){
|
||||||
|
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;
|
||||||
|
if (argv.all || epSelList.isSelected(epStrId)) {
|
||||||
|
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug});
|
||||||
|
epSelEpsTxt.push(epStrId);
|
||||||
|
is_selected = true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
is_selected = false;
|
||||||
|
}
|
||||||
|
// console vars
|
||||||
|
const tx_snum = eps[e].item.seasonNum=='1'?'':` S${eps[e].item.seasonNum}`;
|
||||||
|
const tx_type = eps[e].mediaCategory != 'episode' ? eps[e].mediaCategory : '';
|
||||||
|
const tx_enum = eps[e].item.episodeNum && eps[e].item.episodeNum !== '' ?
|
||||||
|
`#${(parseInt(eps[e].item.episodeNum) < 10 ? '0' : '')+eps[e].item.episodeNum}` : '#'+eps[e].item.episodeId;
|
||||||
|
const qua_str = eps[e].quality.height ? eps[e].quality.quality + eps[e].quality.height : 'UNK';
|
||||||
|
const aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : '';
|
||||||
|
const rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??';
|
||||||
|
// console string
|
||||||
|
eps[e].id_split[0] = eps[e].id_split[0].toString().padStart(typeIdLen, ' ');
|
||||||
|
epStrId = eps[e].id_split.join('');
|
||||||
|
let conOut = `[${epStrId}] `;
|
||||||
|
conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `;
|
||||||
|
conOut += `(${rtm_str}) [${qua_str+aud_str}]`;
|
||||||
|
conOut += is_selected ? ' (selected)' : '';
|
||||||
|
conOut += eps.length-1 == parseInt(e) ? '\n' : '';
|
||||||
|
console.log(conOut);
|
||||||
|
}
|
||||||
|
if(fnSlug.length < 1){
|
||||||
|
console.log('[INFO] Episodes not selected!\n');
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
|
||||||
|
for(let fnEp=0;fnEp<fnSlug.length;fnEp++){
|
||||||
|
await getEpisode(fnSlug[fnEp]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEpisode(fnSlug: {
|
||||||
|
title: string,
|
||||||
|
episode: string
|
||||||
|
}) {
|
||||||
|
const episodeData = await getData({
|
||||||
|
baseUrl: api_host,
|
||||||
|
url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`,
|
||||||
|
token: token,
|
||||||
|
useToken: true,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if(!episodeData.ok || !episodeData.res){return;}
|
||||||
|
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds = [];
|
||||||
|
// build fn
|
||||||
|
showTitle = ep.parent.title;
|
||||||
|
title = ep.title;
|
||||||
|
season = parseInt(ep.parent.seasonNumber);
|
||||||
|
if(ep.mediaCategory != 'Episode'){
|
||||||
|
ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
|
||||||
|
}
|
||||||
|
fnEpNum = isNaN(parseInt(ep.number)) ? ep.number : parseInt(ep.number);
|
||||||
|
|
||||||
|
// is uncut
|
||||||
|
const uncut = {
|
||||||
|
Japanese: false,
|
||||||
|
English: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// end
|
||||||
|
console.log(
|
||||||
|
'[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):');
|
||||||
|
|
||||||
|
// map medias
|
||||||
|
const media = ep.media.map(function(m){
|
||||||
|
if(m.mediaType == 'experience'){
|
||||||
|
if(m.version.match(/uncut/i) && m.language){
|
||||||
|
uncut[m.language] = true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
language: m.language,
|
||||||
|
version: m.version,
|
||||||
|
type: m.experienceType,
|
||||||
|
subtitles: getSubsUrl(m.mediaChildren),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return { id: 0, type: '' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dubType = {
|
||||||
|
'enUS': 'English',
|
||||||
|
'esLA': 'Spanish (Latin Am)',
|
||||||
|
'ptBR': 'Portuguese (Brazil)',
|
||||||
|
'zhMN': 'Chinese (Mandarin, PRC)',
|
||||||
|
'jaJP': 'Japanese'
|
||||||
|
};
|
||||||
|
|
||||||
|
// select
|
||||||
|
stDlPath = [];
|
||||||
|
for(const m of media){
|
||||||
|
let selected = false;
|
||||||
|
if(m.id > 0 && m.type == 'Non-Encrypted'){
|
||||||
|
const dub_type = m.language;
|
||||||
|
if (!dub_type)
|
||||||
|
continue;
|
||||||
|
let localSubs: Subtitle[] = [];
|
||||||
|
const selUncut = !argv.simul && uncut[dub_type] && m.version?.match(/uncut/i)
|
||||||
|
? true
|
||||||
|
: (!uncut[dub_type] || argv.simul && m.version?.match(/simulcast/i) ? true : false);
|
||||||
|
for (const curDub of (argv.dub as appYargs.possibleDubs)) {
|
||||||
|
if(dub_type == dubType[curDub] && selUncut){
|
||||||
|
streamIds.push({
|
||||||
|
id: m.id,
|
||||||
|
lang: merger.getLanguageCode(curDub, curDub.slice(0, -2))
|
||||||
|
});
|
||||||
|
stDlPath.push(...m.subtitles);
|
||||||
|
localSubs = m.subtitles;
|
||||||
|
selected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
||||||
|
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.langName}'`).join(', ')} for subtitles)` : ''
|
||||||
|
}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const already: string[] = [];
|
||||||
|
stDlPath = stDlPath.filter(a => {
|
||||||
|
if (already.includes(a.language)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
already.push(a.language);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(streamIds.length <1){
|
||||||
|
console.log('[ERROR] Track not selected\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
tsDlPath = [];
|
||||||
|
for (const streamId of streamIds) {
|
||||||
|
const streamData = await getData({
|
||||||
|
baseUrl: api_host,
|
||||||
|
url: `/source/catalog/video/${streamId.id}/signed`,
|
||||||
|
token: token,
|
||||||
|
dinstid: 'uuid',
|
||||||
|
useToken: true,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if(!streamData.ok || !streamData.res){return;}
|
||||||
|
const streamDataRes = JSON.parse(streamData.res.body) as StreamData;
|
||||||
|
if(streamDataRes.errors){
|
||||||
|
console.log('[ERROR] Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
for(const u in streamDataRes.items){
|
||||||
|
if(streamDataRes.items[u].videoType == 'm3u8'){
|
||||||
|
tsDlPath.push({
|
||||||
|
path: streamDataRes.items[u].src,
|
||||||
|
lang: streamId.lang
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(tsDlPath.length < 1){
|
||||||
|
console.log('[ERROR] Unknown error\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
await downloadStreams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubsUrl(m: MediaChild[]) : Subtitle[] {
|
||||||
|
if(argv.nosubs && !argv.sub){
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let subLangs = argv.subLang as appYargs.possibleSubs;
|
||||||
|
|
||||||
|
const subType = {
|
||||||
|
'enUS': 'English',
|
||||||
|
'esLA': 'Spanish (Latin Am)',
|
||||||
|
'ptBR': 'Portuguese (Brazil)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const subLangAvailable = m.some(a => subLangs.some(subLang => a.ext == 'vtt' && a.language === subType[subLang]));
|
||||||
|
|
||||||
|
if (!subLangAvailable) {
|
||||||
|
subLangs = [ 'enUS' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
const found: Subtitle[] = [];
|
||||||
|
|
||||||
|
for(const i in m){
|
||||||
|
const fpp = m[i].filePath.split('.');
|
||||||
|
const fpe = fpp[fpp.length-1];
|
||||||
|
for (const lang of subLangs) {
|
||||||
|
if(fpe == 'vtt' && m[i].language === subType[lang]) {
|
||||||
|
found.push({
|
||||||
|
path: m[i].filePath,
|
||||||
|
ext: `.${lang}`,
|
||||||
|
langName: subType[lang],
|
||||||
|
language: m[i].languages[0].code || lang.slice(0, 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadStreams(){
|
||||||
|
|
||||||
|
// req playlist
|
||||||
|
|
||||||
|
const purvideo: DownloadedFile[] = [];
|
||||||
|
const puraudio: DownloadedFile[] = [];
|
||||||
|
const audioAndVideo: DownloadedFile[] = [];
|
||||||
|
for (const streamPath of tsDlPath) {
|
||||||
|
const plQualityReq = await getData({
|
||||||
|
url: streamPath.path,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if(!plQualityReq.ok || !plQualityReq.res){return;}
|
||||||
|
|
||||||
|
const plQualityLinkList = m3u8(plQualityReq.res.body);
|
||||||
|
|
||||||
|
const mainServersList = [
|
||||||
|
'vmfst-api.prd.funimationsvc.com',
|
||||||
|
'd33et77evd9bgg.cloudfront.net',
|
||||||
|
'd132fumi6di1wa.cloudfront.net',
|
||||||
|
'funiprod.akamaized.net',
|
||||||
|
];
|
||||||
|
|
||||||
|
const plServerList: string[] = [],
|
||||||
|
plStreams: Record<string|number, {
|
||||||
|
[key: string]: string
|
||||||
|
}> = {},
|
||||||
|
plLayersStr = [],
|
||||||
|
plLayersRes: Record<string|number, {
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}> = {};
|
||||||
|
let plMaxLayer = 1,
|
||||||
|
plNewIds = 1,
|
||||||
|
plAud: {
|
||||||
|
uri: string,
|
||||||
|
langStr: string,
|
||||||
|
language: string
|
||||||
|
} = { uri: '', langStr: '', language: '' };
|
||||||
|
|
||||||
|
// new uris
|
||||||
|
const vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/;
|
||||||
|
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');
|
||||||
|
if(plQualityLinkList.mediaGroups.AUDIO[audioKey]){
|
||||||
|
const audioDataParts = plQualityLinkList.mediaGroups.AUDIO[audioKey],
|
||||||
|
audioEl = Object.keys(audioDataParts);
|
||||||
|
const audioData = audioDataParts[audioEl[0]];
|
||||||
|
plAud = { ...audioData, ...{ langStr: audioEl[0] } };
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const av = parseInt(aMatch[3]);
|
||||||
|
const bv = parseInt(bMatch[3]);
|
||||||
|
if(av > bv){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (av < bv) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const s of plQualityLinkList.playlists){
|
||||||
|
if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){
|
||||||
|
// set layer and max layer
|
||||||
|
let plLayerId: number|string = 0;
|
||||||
|
const match = s.uri.match(/_Layer(\d+)\.m3u8/);
|
||||||
|
if(match){
|
||||||
|
plLayerId = parseInt(match[1]);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
plLayerId = plNewIds, plNewIds++;
|
||||||
|
}
|
||||||
|
plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
|
||||||
|
// set urls and servers
|
||||||
|
const plUrlDl = s.uri;
|
||||||
|
const plServer = new URL(plUrlDl).host;
|
||||||
|
if(!plServerList.includes(plServer)){
|
||||||
|
plServerList.push(plServer);
|
||||||
|
}
|
||||||
|
if(!Object.keys(plStreams).includes(plServer)){
|
||||||
|
plStreams[plServer] = {};
|
||||||
|
}
|
||||||
|
if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){
|
||||||
|
console.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
plStreams[plServer][plLayerId] = plUrlDl;
|
||||||
|
}
|
||||||
|
// set plLayersStr
|
||||||
|
const plResolution = s.attributes.RESOLUTION;
|
||||||
|
plLayersRes[plLayerId] = plResolution;
|
||||||
|
const plBandwidth = Math.round(s.attributes.BANDWIDTH/1024);
|
||||||
|
if(plLayerId<10){
|
||||||
|
plLayerId = plLayerId.toString().padStart(2,' ');
|
||||||
|
}
|
||||||
|
const qualityStrAdd = `${plLayerId}: ${plResolution.width}x${plResolution.height} (${plBandwidth}KiB/s)`;
|
||||||
|
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g,'\\$1'),'m');
|
||||||
|
const qualityStrMatch = !plLayersStr.join('\r\n').match(qualityStrRegx);
|
||||||
|
if(qualityStrMatch){
|
||||||
|
plLayersStr.push(qualityStrAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(s.uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const s of mainServersList){
|
||||||
|
if(plServerList.includes(s)){
|
||||||
|
plServerList.splice(plServerList.indexOf(s), 1);
|
||||||
|
plServerList.unshift(s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
argv.q = argv.q < 1 || argv.q > plMaxLayer ? plMaxLayer : argv.q;
|
||||||
|
|
||||||
|
const plSelectedServer = plServerList[argv.x-1];
|
||||||
|
const plSelectedList = plStreams[plSelectedServer];
|
||||||
|
const videoUrl = argv.x < plServerList.length+1 && plSelectedList[argv.q] ? plSelectedList[argv.q] : '';
|
||||||
|
|
||||||
|
plLayersStr.sort();
|
||||||
|
console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||||
|
console.log(`[INFO] Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
||||||
|
|
||||||
|
if(videoUrl != ''){
|
||||||
|
console.log(`[INFO] Selected layer: ${argv.q} (${plLayersRes[argv.q].width}x${plLayersRes[argv.q].height}) @ ${plSelectedServer}`);
|
||||||
|
console.log('[INFO] Stream URL:',videoUrl);
|
||||||
|
|
||||||
|
fnOutput = parseFileName(argv.fileName, title, fnEpNum, showTitle, season, plLayersRes[argv.q].width, plLayersRes[argv.q].height);
|
||||||
|
if (fnOutput.length < 1)
|
||||||
|
throw new Error(`Invalid path generated for input ${argv.fileName}`);
|
||||||
|
console.log(`[INFO] Output filename: ${fnOutput.join(path.sep)}.ts`);
|
||||||
|
}
|
||||||
|
else if(argv.x > plServerList.length){
|
||||||
|
console.log('[ERROR] Server not selected!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.log('[ERROR] Layer not selected!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dlFailed = false;
|
||||||
|
let dlFailedA = false;
|
||||||
|
|
||||||
|
await fs.promises.mkdir(path.join(cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true });
|
||||||
|
|
||||||
|
video: if (!argv.novids) {
|
||||||
|
if (plAud.uri && (purvideo.length > 0 || audioAndVideo.length > 0)) {
|
||||||
|
break video;
|
||||||
|
} else if (!plAud.uri && (audioAndVideo.some(a => a.lang === streamPath.lang) || puraudio.some(a => a.lang === streamPath.lang))) {
|
||||||
|
break video;
|
||||||
|
}
|
||||||
|
// download video
|
||||||
|
const reqVideo = await getData({
|
||||||
|
url: videoUrl,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if (!reqVideo.ok || !reqVideo.res) { break video; }
|
||||||
|
|
||||||
|
const chunkList = m3u8(reqVideo.res.body);
|
||||||
|
|
||||||
|
const tsFile = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud.uri ? '' : '.' + streamPath.lang )}`);
|
||||||
|
dlFailed = !await downloadFile(tsFile, chunkList);
|
||||||
|
if (!dlFailed) {
|
||||||
|
if (plAud.uri) {
|
||||||
|
purvideo.push({
|
||||||
|
path: `${tsFile}.ts`,
|
||||||
|
lang: plAud.language
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
audioAndVideo.push({
|
||||||
|
path: `${tsFile}.ts`,
|
||||||
|
lang: streamPath.lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.log('[INFO] Skip video downloading...\n');
|
||||||
|
}
|
||||||
|
audio: if (!argv.noaudio && plAud.uri) {
|
||||||
|
// download audio
|
||||||
|
if (audioAndVideo.some(a => a.lang === plAud.language) || puraudio.some(a => a.lang === plAud.language))
|
||||||
|
break audio;
|
||||||
|
const reqAudio = await getData({
|
||||||
|
url: plAud.uri,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if (!reqAudio.ok || !reqAudio.res) { return; }
|
||||||
|
|
||||||
|
const chunkListA = m3u8(reqAudio.res.body);
|
||||||
|
|
||||||
|
const tsFileA = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language}`);
|
||||||
|
|
||||||
|
dlFailedA = !await downloadFile(tsFileA, chunkListA);
|
||||||
|
if (!dlFailedA)
|
||||||
|
puraudio.push({
|
||||||
|
path: `${tsFileA}.ts`,
|
||||||
|
lang: plAud.language
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add subs
|
||||||
|
const subsExt = !argv.mp4 || argv.mp4 && argv.ass ? '.ass' : '.srt';
|
||||||
|
let addSubs = true;
|
||||||
|
|
||||||
|
// download subtitles
|
||||||
|
if(stDlPath.length > 0){
|
||||||
|
console.log('[INFO] Downloading subtitles...');
|
||||||
|
for (const subObject of stDlPath) {
|
||||||
|
const subsSrc = await getData({
|
||||||
|
url: subObject.path,
|
||||||
|
debug: argv.debug,
|
||||||
|
});
|
||||||
|
if(subsSrc.ok && subsSrc.res){
|
||||||
|
const assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.langName, argv.fontSize);
|
||||||
|
subObject.file = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.subtitle${subObject.ext}${subsExt}`);
|
||||||
|
fs.writeFileSync(subObject.file, assData);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.log('[ERROR] Failed to download subtitles!');
|
||||||
|
addSubs = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addSubs)
|
||||||
|
console.log('[INFO] Subtitles downloaded!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
||||||
|
console.log('\n[INFO] Unable to locate a video AND audio file\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(argv.skipmux){
|
||||||
|
console.log('[INFO] Skipping muxing...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check exec
|
||||||
|
const mergerBin = merger.checkMerger(cfg.bin, argv.mp4);
|
||||||
|
|
||||||
|
if ( argv.novids ){
|
||||||
|
console.log('[INFO] Video not downloaded. Skip muxing video.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergers
|
||||||
|
if(!argv.mp4 && !mergerBin.MKVmerge){
|
||||||
|
console.log('[WARN] MKVMerge not found...');
|
||||||
|
}
|
||||||
|
if(!mergerBin.MKVmerge && !mergerBin.FFmpeg || argv.mp4 && !mergerBin.MKVmerge){
|
||||||
|
console.log('[WARN] FFmpeg not found...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
||||||
|
const mergeInstance = new merger({
|
||||||
|
onlyAudio: puraudio,
|
||||||
|
onlyVid: purvideo,
|
||||||
|
output: `${path.join(cfg.dir.content, ...fnOutput)}.${ffext}`,
|
||||||
|
subtitels: stDlPath as SubtitleInput[],
|
||||||
|
videoAndAudio: audioAndVideo,
|
||||||
|
simul: argv.simul
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!argv.mp4 && mergerBin.MKVmerge){
|
||||||
|
const command = mergeInstance.MkvMerge();
|
||||||
|
shlp.exec('mkvmerge', `"${mergerBin.MKVmerge}"`, command);
|
||||||
|
}
|
||||||
|
else if(mergerBin.FFmpeg){
|
||||||
|
const command = mergeInstance.FFmpeg();
|
||||||
|
shlp.exec('ffmpeg',`"${mergerBin.FFmpeg}"`,command);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.log('\n[INFO] Done!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (argv.nocleanup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
audioAndVideo.concat(puraudio).concat(purvideo).forEach(a => fs.unlinkSync(a.path));
|
||||||
|
stDlPath.forEach(subObject => subObject.file && fs.unlinkSync(subObject.file));
|
||||||
|
console.log('\n[INFO] Done!\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(filename: string, chunkList: {
|
||||||
|
segments: Record<string, unknown>[],
|
||||||
|
}) {
|
||||||
|
const downloadStatus = await new hlsDownload({
|
||||||
|
m3u8json: chunkList,
|
||||||
|
output: `${filename + '.ts'}`,
|
||||||
|
timeout: argv.timeout,
|
||||||
|
threads: argv.partsize
|
||||||
|
}).download();
|
||||||
|
|
||||||
|
return downloadStatus.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFileName(input: string, title: string, episode:number|string, showTitle: string, season: number, width: number, height: number): string[] {
|
||||||
|
const varRegex = /\${[A-Za-z1-9]+}/g;
|
||||||
|
const vars = input.match(varRegex);
|
||||||
|
if (!vars)
|
||||||
|
return [input];
|
||||||
|
for (let i = 0; i < vars.length; i++) {
|
||||||
|
const type = vars[i];
|
||||||
|
switch (type.slice(2, -1).toLowerCase()) {
|
||||||
|
case 'title':
|
||||||
|
input = input.replace(vars[i], title);
|
||||||
|
break;
|
||||||
|
case 'episode': {
|
||||||
|
if (typeof episode === 'number') {
|
||||||
|
const len = episode.toFixed(0).toString().length;
|
||||||
|
input = input.replace(vars[i], len < argv.numbers ? '0'.repeat(argv.numbers - len) + episode : episode.toString());
|
||||||
|
} else {
|
||||||
|
input = input.replace(vars[i], episode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'showtitle':
|
||||||
|
input = input.replace(vars[i], showTitle);
|
||||||
|
break;
|
||||||
|
case 'season': {
|
||||||
|
const len = season.toFixed(0).toString().length;
|
||||||
|
input = input.replace(vars[i], len < argv.numbers ? '0'.repeat(argv.numbers - len) + season : season.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'width':
|
||||||
|
input = input.replace(vars[i], width.toString());
|
||||||
|
break;
|
||||||
|
case 'height':
|
||||||
|
input = input.replace(vars[i], height.toString());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input.split(path.sep).map(a => shlp.cleanupFilename(a));
|
||||||
|
}
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// build requirements
|
|
||||||
const pkg = require('../package.json');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const modulesCleanup = require('removeNPMAbsolutePaths');
|
|
||||||
const { exec } = require('pkg');
|
|
||||||
|
|
||||||
const buildsDir = './_builds';
|
|
||||||
const nodeVer = 'node14-';
|
|
||||||
|
|
||||||
// main
|
|
||||||
(async function(){
|
|
||||||
const buildStr = `${pkg.name}-${pkg.version}`;
|
|
||||||
const acceptableBuilds = ['win64','linux64','macos64'];
|
|
||||||
const buildType = process.argv[2];
|
|
||||||
if(!acceptableBuilds.includes(buildType)){
|
|
||||||
console.error('[ERROR] unknown build type!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
await modulesCleanup('.');
|
|
||||||
if(!fs.existsSync(buildsDir)){
|
|
||||||
fs.mkdirSync(buildsDir);
|
|
||||||
}
|
|
||||||
const buildFull = `${buildStr}-${buildType}`;
|
|
||||||
const buildDir = `${buildsDir}/${buildFull}`;
|
|
||||||
if(fs.existsSync(buildDir)){
|
|
||||||
fs.removeSync(buildDir);
|
|
||||||
}
|
|
||||||
fs.mkdirSync(buildDir);
|
|
||||||
const buildConfig = [
|
|
||||||
pkg.main,
|
|
||||||
'--target', nodeVer + getTarget(buildType),
|
|
||||||
'--output', `${buildDir}/${pkg.short_name}`,
|
|
||||||
];
|
|
||||||
console.log(`[Build] Build configuration: ${buildFull}`);
|
|
||||||
try {
|
|
||||||
await exec(buildConfig);
|
|
||||||
}
|
|
||||||
catch(e){
|
|
||||||
console.log(e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
fs.mkdirSync(`${buildDir}/bin`);
|
|
||||||
fs.mkdirSync(`${buildDir}/config`);
|
|
||||||
fs.mkdirSync(`${buildDir}/videos`);
|
|
||||||
fs.copySync('./bin/', `${buildDir}/bin/`);
|
|
||||||
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('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
|
|
||||||
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
|
|
||||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
|
||||||
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
|
||||||
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
|
||||||
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
|
||||||
}
|
|
||||||
require('child_process').execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
|
|
||||||
}());
|
|
||||||
|
|
||||||
function getTarget(bt){
|
|
||||||
switch(bt){
|
|
||||||
case 'win64':
|
|
||||||
return 'windows-x64';
|
|
||||||
case 'linux64':
|
|
||||||
return 'linux-x64';
|
|
||||||
case 'macos64':
|
|
||||||
return 'macos-x64';
|
|
||||||
default:
|
|
||||||
return 'windows-x64';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
74
modules/build.ts
Normal file
74
modules/build.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// build requirements
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import pkg from '../package.json';
|
||||||
|
import modulesCleanup from 'removeNPMAbsolutePaths';
|
||||||
|
import { exec } from 'pkg';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
const buildsDir = './_builds';
|
||||||
|
const nodeVer = 'node14-';
|
||||||
|
|
||||||
|
// main
|
||||||
|
(async function(){
|
||||||
|
const buildStr = `${pkg.name}-${pkg.version}`;
|
||||||
|
const acceptableBuilds = ['win64','linux64','macos64'];
|
||||||
|
const buildType = process.argv[2];
|
||||||
|
if(!acceptableBuilds.includes(buildType)){
|
||||||
|
console.error('[ERROR] unknown build type!');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await modulesCleanup('.');
|
||||||
|
if(!fs.existsSync(buildsDir)){
|
||||||
|
fs.mkdirSync(buildsDir);
|
||||||
|
}
|
||||||
|
const buildFull = `${buildStr}-${buildType}`;
|
||||||
|
const buildDir = `${buildsDir}/${buildFull}`;
|
||||||
|
if(fs.existsSync(buildDir)){
|
||||||
|
fs.removeSync(buildDir);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(buildDir);
|
||||||
|
const buildConfig = [
|
||||||
|
pkg.main,
|
||||||
|
'--target', nodeVer + getTarget(buildType),
|
||||||
|
'--output', `${buildDir}/${pkg.short_name}`,
|
||||||
|
];
|
||||||
|
console.log(`[Build] Build configuration: ${buildFull}`);
|
||||||
|
try {
|
||||||
|
await exec(buildConfig);
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
console.log(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(`${buildDir}/bin`);
|
||||||
|
fs.mkdirSync(`${buildDir}/config`);
|
||||||
|
fs.mkdirSync(`${buildDir}/videos`);
|
||||||
|
fs.copySync('./bin/', `${buildDir}/bin/`);
|
||||||
|
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('./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(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
||||||
|
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
||||||
|
}
|
||||||
|
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
|
||||||
|
}());
|
||||||
|
|
||||||
|
function getTarget(bt: string) : string {
|
||||||
|
switch(bt){
|
||||||
|
case 'win64':
|
||||||
|
return 'windows-x64';
|
||||||
|
case 'linux64':
|
||||||
|
return 'linux-x64';
|
||||||
|
case 'macos64':
|
||||||
|
return 'macos-x64';
|
||||||
|
default:
|
||||||
|
return 'windows-x64';
|
||||||
|
}
|
||||||
|
}
|
||||||
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,247 +0,0 @@
|
||||||
const yargs = require('yargs');
|
|
||||||
|
|
||||||
const availableFilenameVars = [
|
|
||||||
'title',
|
|
||||||
'episode',
|
|
||||||
'showTitle',
|
|
||||||
'season',
|
|
||||||
'width',
|
|
||||||
'height'
|
|
||||||
];
|
|
||||||
|
|
||||||
const subLang = ['enUS', 'esLA', 'ptBR'];
|
|
||||||
const dubLang = ['enUS', 'esLA', 'ptBR', 'zhMN', 'jaJP'];
|
|
||||||
|
|
||||||
const appArgv = (cfg) => {
|
|
||||||
// init
|
|
||||||
const parseDefault = (key, _default) => {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
|
|
||||||
return cfg[key];
|
|
||||||
} else
|
|
||||||
return _default;
|
|
||||||
};
|
|
||||||
|
|
||||||
const argv = yargs.parserConfiguration({
|
|
||||||
'duplicate-arguments-array': true,
|
|
||||||
'camel-case-expansion': false
|
|
||||||
})
|
|
||||||
// main
|
|
||||||
.wrap(Math.min(120)) // yargs.terminalWidth()
|
|
||||||
.help(false).version(false)
|
|
||||||
.usage('Usage: $0 [options]')
|
|
||||||
// auth
|
|
||||||
.option('auth', {
|
|
||||||
group: 'Authentication:',
|
|
||||||
describe: 'Enter authentication mode',
|
|
||||||
type: 'boolean',
|
|
||||||
})
|
|
||||||
// search
|
|
||||||
.option('search', {
|
|
||||||
alias: 'f',
|
|
||||||
group: 'Search:',
|
|
||||||
describe: 'Search show ids',
|
|
||||||
type: 'string',
|
|
||||||
})
|
|
||||||
// select show and eps
|
|
||||||
.option('s', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Sets the show id',
|
|
||||||
type: 'number',
|
|
||||||
})
|
|
||||||
.option('e', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Select episode ids (comma-separated, hyphen-sequence)',
|
|
||||||
type: 'string',
|
|
||||||
})
|
|
||||||
.option('all', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Used to download all episodes from the show',
|
|
||||||
type: 'boolean',
|
|
||||||
default: parseDefault('all', false)
|
|
||||||
})
|
|
||||||
.option('partsize', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'The amount of parts that should be downloaded in paralell',
|
|
||||||
type: 'number',
|
|
||||||
default: parseDefault('partsize', 10)
|
|
||||||
})
|
|
||||||
// quality
|
|
||||||
.option('q', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Select video layer (0 is max)',
|
|
||||||
choices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
||||||
default: parseDefault('videoLayer', 7),
|
|
||||||
type: 'number',
|
|
||||||
})
|
|
||||||
// alt listing
|
|
||||||
.option('alt', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Alternative episode listing (if available)',
|
|
||||||
default: parseDefault('altList', false),
|
|
||||||
type: 'boolean',
|
|
||||||
})
|
|
||||||
// switch to subs
|
|
||||||
.option('dub', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Download non-Japanese Dub (English Dub mode by default)',
|
|
||||||
choices: dubLang,
|
|
||||||
default: parseDefault('dub', 'enUS'),
|
|
||||||
type: 'array',
|
|
||||||
})
|
|
||||||
.option('subLang', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Set the subtitle language (English is default and fallback)',
|
|
||||||
default: parseDefault('subLang', 'enUS'),
|
|
||||||
choices: subLang,
|
|
||||||
type: 'array'
|
|
||||||
})
|
|
||||||
.option('fontSize', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Used to set the fontsize of the subtitles',
|
|
||||||
default: parseDefault('fontSize', 55),
|
|
||||||
type: 'number'
|
|
||||||
})
|
|
||||||
.option('allSubs', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'If set to true, all available subs will get downloaded',
|
|
||||||
default: false,
|
|
||||||
type: 'boolean'
|
|
||||||
})
|
|
||||||
.option('allDubs', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'If set to true, all available dubs will get downloaded',
|
|
||||||
default: false,
|
|
||||||
type: 'boolean'
|
|
||||||
})
|
|
||||||
// simulcast
|
|
||||||
.option('simul', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Force downloading simulcast ver. instead of uncut ver. (if uncut ver. available)',
|
|
||||||
default: parseDefault('forceSimul', false),
|
|
||||||
type: 'boolean',
|
|
||||||
})
|
|
||||||
// server number
|
|
||||||
.option('x', {
|
|
||||||
alias: 'server',
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Select server',
|
|
||||||
choices: [1, 2, 3, 4],
|
|
||||||
default: parseDefault('nServer', 1),
|
|
||||||
type: 'number',
|
|
||||||
})
|
|
||||||
// skip
|
|
||||||
.option('noaudio', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Skip downloading audio',
|
|
||||||
type: 'boolean'
|
|
||||||
})
|
|
||||||
.option('novids', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
alias: 'skipdl',
|
|
||||||
describe: 'Skip downloading video',
|
|
||||||
type: 'boolean',
|
|
||||||
})
|
|
||||||
.option('nosubs', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Skip downloading subtitles for English Dub (if available)',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false
|
|
||||||
})
|
|
||||||
// proxy
|
|
||||||
.option('proxy', {
|
|
||||||
group: 'Proxy:',
|
|
||||||
describe: 'Set http(s)/socks proxy WHATWG url',
|
|
||||||
default: parseDefault('proxy', false),
|
|
||||||
hidden: true,
|
|
||||||
})
|
|
||||||
.option('proxy-auth', {
|
|
||||||
group: 'Proxy:',
|
|
||||||
describe: 'Colon-separated username and password for proxy',
|
|
||||||
default: parseDefault('proxy_auth', false),
|
|
||||||
hidden: true,
|
|
||||||
})
|
|
||||||
.option('ssp', {
|
|
||||||
group: 'Proxy:',
|
|
||||||
describe: 'Don\'t use proxy for stream and subtitles downloading',
|
|
||||||
default: parseDefault('proxy_ssp', false),
|
|
||||||
hidden: true,
|
|
||||||
type: 'boolean',
|
|
||||||
})
|
|
||||||
// muxing
|
|
||||||
.option('skipmux', {
|
|
||||||
group: 'Muxing:',
|
|
||||||
describe: 'Skip muxing video and subtitles',
|
|
||||||
type: 'boolean',
|
|
||||||
})
|
|
||||||
.option('mp4', {
|
|
||||||
group: 'Muxing:',
|
|
||||||
describe: 'Mux into mp4',
|
|
||||||
default: parseDefault('mp4mux', false),
|
|
||||||
type: 'boolean'
|
|
||||||
})
|
|
||||||
// filenaming
|
|
||||||
.option('fileName', {
|
|
||||||
group: 'Filename Template:',
|
|
||||||
describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou may use ${availableFilenameVars
|
|
||||||
.map(a => `'${a}'`).join(', ')} as variables.`,
|
|
||||||
type: 'string',
|
|
||||||
default: parseDefault('fileName', '[Funimation] ${showTitle} - ${episode} [${height}p]')
|
|
||||||
})
|
|
||||||
.option('numbers', {
|
|
||||||
group: 'Filename Template:',
|
|
||||||
describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']]
|
|
||||||
.map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`,
|
|
||||||
type: 'number',
|
|
||||||
default: parseDefault('numbers', 2)
|
|
||||||
})
|
|
||||||
// util
|
|
||||||
.option('nocleanup', {
|
|
||||||
group: 'Utilities:',
|
|
||||||
describe: 'Dont\'t delete the input files after muxing',
|
|
||||||
default: parseDefault('noCleanUp', false),
|
|
||||||
type: 'boolean'
|
|
||||||
})
|
|
||||||
.option('timeout', {
|
|
||||||
group: 'Downloading:',
|
|
||||||
describe: 'Set the timeout of all download reqests. Set in millisecods',
|
|
||||||
type: 'number',
|
|
||||||
default: parseDefault('timeout', 60 * 1000)
|
|
||||||
})
|
|
||||||
// help
|
|
||||||
.option('help', {
|
|
||||||
alias: 'h',
|
|
||||||
group: 'Help:',
|
|
||||||
describe: 'Show this help',
|
|
||||||
type: 'boolean'
|
|
||||||
})
|
|
||||||
// usage
|
|
||||||
.example([
|
|
||||||
['$0 --search "My Hero"', 'search "My Hero" in title'],
|
|
||||||
['$0 -s 124389 -e 1,2,3', 'download episodes 1-3 from show with id 124389'],
|
|
||||||
['$0 -s 124389 -e 1-3,2-7,s1-2', 'download episodes 1-7 and "S"-episodes 1-2 from show with id 124389'],
|
|
||||||
])
|
|
||||||
|
|
||||||
// --
|
|
||||||
.argv;
|
|
||||||
// Resolve unwanted arrays
|
|
||||||
if (argv.allDubs)
|
|
||||||
argv.dub = dubLang;
|
|
||||||
if (argv.allSubs)
|
|
||||||
argv.subLang = subLang;
|
|
||||||
for (let key in argv) {
|
|
||||||
if (argv[key] instanceof Array && !(key === 'subLang' || key === 'dub')) {
|
|
||||||
argv[key] = argv[key].pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return argv;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showHelp = yargs.showHelp;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
appArgv,
|
|
||||||
showHelp,
|
|
||||||
availableFilenameVars,
|
|
||||||
dubLang,
|
|
||||||
subLang
|
|
||||||
};
|
|
||||||
262
modules/module.app-args.ts
Normal file
262
modules/module.app-args.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
import yargs from 'yargs';
|
||||||
|
|
||||||
|
const availableFilenameVars = [
|
||||||
|
'title',
|
||||||
|
'episode',
|
||||||
|
'showTitle',
|
||||||
|
'season',
|
||||||
|
'width',
|
||||||
|
'height'
|
||||||
|
];
|
||||||
|
|
||||||
|
export type possibleDubs = (
|
||||||
|
'enUS' | 'esLA' | 'ptBR' | 'zhMN' | 'jaJP'
|
||||||
|
)[];
|
||||||
|
export type possibleSubs = (
|
||||||
|
'enUS' | 'esLA' | 'ptBR'
|
||||||
|
)[];
|
||||||
|
const subLang: possibleSubs = ['enUS', 'esLA', 'ptBR'];
|
||||||
|
const dubLang: possibleDubs = ['enUS', 'esLA', 'ptBR', 'zhMN', 'jaJP'];
|
||||||
|
|
||||||
|
|
||||||
|
const appArgv = (cfg: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}) => {
|
||||||
|
// init
|
||||||
|
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
|
||||||
|
return cfg[key] as T;
|
||||||
|
} else
|
||||||
|
return _default;
|
||||||
|
};
|
||||||
|
const argv = yargs.parserConfiguration({
|
||||||
|
'duplicate-arguments-array': true,
|
||||||
|
'camel-case-expansion': false
|
||||||
|
})
|
||||||
|
// main
|
||||||
|
.wrap(Math.min(120)) // yargs.terminalWidth()
|
||||||
|
.help(false).version(false)
|
||||||
|
.usage('Usage: $0 [options]')
|
||||||
|
// auth
|
||||||
|
.option('auth', {
|
||||||
|
group: 'Authentication:',
|
||||||
|
describe: 'Enter authentication mode',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
// search
|
||||||
|
.option('search', {
|
||||||
|
alias: 'f',
|
||||||
|
group: 'Search:',
|
||||||
|
describe: 'Search show ids',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
// select show and eps
|
||||||
|
.option('s', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Sets the show id',
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
.option('e', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Select episode ids (comma-separated, hyphen-sequence)',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
.option('all', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Used to download all episodes from the show',
|
||||||
|
type: 'boolean',
|
||||||
|
default: parseDefault<boolean>('all', false)
|
||||||
|
})
|
||||||
|
.option('partsize', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'The amount of parts that should be downloaded in paralell',
|
||||||
|
type: 'number',
|
||||||
|
default: parseDefault<number>('partsize', 10)
|
||||||
|
})
|
||||||
|
// quality
|
||||||
|
.option('q', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Select video layer (0 is max)',
|
||||||
|
choices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
default: parseDefault<number>('videoLayer', 7),
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
// alt listing
|
||||||
|
.option('alt', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Alternative episode listing (if available)',
|
||||||
|
default: parseDefault<boolean>('altList', false),
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
// switch to subs
|
||||||
|
.option('dub', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Download non-Japanese Dub (English Dub mode by default)',
|
||||||
|
choices: dubLang,
|
||||||
|
default: parseDefault<possibleDubs>('dub', ['enUS']),
|
||||||
|
type: 'array',
|
||||||
|
})
|
||||||
|
.option('subLang', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Set the subtitle language (English is default and fallback)',
|
||||||
|
default: parseDefault<possibleSubs>('subLang', ['enUS']),
|
||||||
|
choices: subLang,
|
||||||
|
type: 'array'
|
||||||
|
})
|
||||||
|
.option('fontSize', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Used to set the fontsize of the subtitles',
|
||||||
|
default: parseDefault<number>('fontSize', 55),
|
||||||
|
type: 'number'
|
||||||
|
})
|
||||||
|
.option('allSubs', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'If set to true, all available subs will get downloaded',
|
||||||
|
default: false,
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
.option('allDubs', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'If set to true, all available dubs will get downloaded',
|
||||||
|
default: false,
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
// simulcast
|
||||||
|
.option('simul', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Force downloading simulcast ver. instead of uncut ver. (if uncut ver. available)',
|
||||||
|
default: parseDefault<boolean>('forceSimul', false),
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
// server number
|
||||||
|
.option('x', {
|
||||||
|
alias: 'server',
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Select server',
|
||||||
|
choices: [1, 2, 3, 4],
|
||||||
|
default: parseDefault<number>('nServer', 1),
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
// skip
|
||||||
|
.option('noaudio', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Skip downloading audio',
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
.option('novids', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
alias: 'skipdl',
|
||||||
|
describe: 'Skip downloading video',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.option('nosubs', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Skip downloading subtitles for English Dub (if available)',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
// proxy
|
||||||
|
.option('proxy', {
|
||||||
|
group: 'Proxy:',
|
||||||
|
describe: 'Set http(s)/socks proxy WHATWG url',
|
||||||
|
default: parseDefault<boolean>('proxy', false),
|
||||||
|
hidden: true,
|
||||||
|
})
|
||||||
|
.option('proxy-auth', {
|
||||||
|
group: 'Proxy:',
|
||||||
|
describe: 'Colon-separated username and password for proxy',
|
||||||
|
default: parseDefault<string|boolean>('proxy_auth', false),
|
||||||
|
hidden: true,
|
||||||
|
})
|
||||||
|
.option('ssp', {
|
||||||
|
group: 'Proxy:',
|
||||||
|
describe: 'Don\'t use proxy for stream and subtitles downloading',
|
||||||
|
default: parseDefault<boolean>('proxy_ssp', false),
|
||||||
|
hidden: true,
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
// muxing
|
||||||
|
.option('skipmux', {
|
||||||
|
group: 'Muxing:',
|
||||||
|
describe: 'Skip muxing video and subtitles',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.option('mp4', {
|
||||||
|
group: 'Muxing:',
|
||||||
|
describe: 'Mux into mp4',
|
||||||
|
default: parseDefault<boolean>('mp4mux', false),
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
// filenaming
|
||||||
|
.option('fileName', {
|
||||||
|
group: 'Filename Template:',
|
||||||
|
describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou may use ${availableFilenameVars
|
||||||
|
.map(a => `'${a}'`).join(', ')} as variables.`,
|
||||||
|
type: 'string',
|
||||||
|
default: parseDefault<string>('fileName', '[Funimation] ${showTitle} - ${episode} [${height}p]')
|
||||||
|
})
|
||||||
|
.option('numbers', {
|
||||||
|
group: 'Filename Template:',
|
||||||
|
describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']]
|
||||||
|
.map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`,
|
||||||
|
type: 'number',
|
||||||
|
default: parseDefault<number>('numbers', 2)
|
||||||
|
})
|
||||||
|
// util
|
||||||
|
.option('nocleanup', {
|
||||||
|
group: 'Utilities:',
|
||||||
|
describe: 'Dont\'t delete the input files after muxing',
|
||||||
|
default: parseDefault<boolean>('noCleanUp', false),
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
.option('timeout', {
|
||||||
|
group: 'Downloading:',
|
||||||
|
describe: 'Set the timeout of all download reqests. Set in millisecods',
|
||||||
|
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',
|
||||||
|
group: 'Help:',
|
||||||
|
describe: 'Show this help',
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
// usage
|
||||||
|
.example([
|
||||||
|
['$0 --search "My Hero"', 'search "My Hero" in title'],
|
||||||
|
['$0 -s 124389 -e 1,2,3', 'download episodes 1-3 from show with id 124389'],
|
||||||
|
['$0 -s 124389 -e 1-3,2-7,s1-2', 'download episodes 1-7 and "S"-episodes 1-2 from show with id 124389'],
|
||||||
|
])
|
||||||
|
|
||||||
|
// --
|
||||||
|
.parseSync();
|
||||||
|
// Resolve unwanted arrays
|
||||||
|
if (argv.allDubs)
|
||||||
|
argv.dub = dubLang;
|
||||||
|
if (argv.allSubs)
|
||||||
|
argv.subLang = subLang;
|
||||||
|
for (const key in argv) {
|
||||||
|
if (argv[key] instanceof Array && !(key === 'subLang' || key === 'dub')) {
|
||||||
|
argv[key] = (argv[key] as Array<unknown>).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return argv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showHelp = yargs.showHelp;
|
||||||
|
|
||||||
|
export {
|
||||||
|
appArgv,
|
||||||
|
showHelp,
|
||||||
|
availableFilenameVars,
|
||||||
|
dubLang,
|
||||||
|
subLang
|
||||||
|
};
|
||||||
|
|
@ -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|string,
|
||||||
|
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
|
|
||||||
};
|
|
||||||
205
modules/module.merger.ts
Normal file
205
modules/module.merger.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
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(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
public static checkMerger(bin: {
|
||||||
|
mkvmerge?: string,
|
||||||
|
ffmpeg?: string
|
||||||
|
}, useMP4format: boolean) {
|
||||||
|
const merger: {
|
||||||
|
MKVmerge: undefined|string|false,
|
||||||
|
FFmpeg: undefined|string|false
|
||||||
|
} = {
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
4738
package-lock.json
generated
4738
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "funimation-downloader-nx",
|
"name": "funimation-downloader-nx",
|
||||||
"short_name": "funi",
|
"short_name": "funi",
|
||||||
"version": "4.12.5",
|
"version": "5.0.0",
|
||||||
"description": "Download videos from Funimation via cli.",
|
"description": "Download videos from Funimation via cli.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"download",
|
"download",
|
||||||
|
|
@ -36,16 +36,29 @@
|
||||||
"yargs": "^17.2.1"
|
"yargs": "^17.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"eslint": "^7.30.0",
|
||||||
"pkg": "^5.3.3",
|
"pkg": "^5.3.3",
|
||||||
"removeNPMAbsolutePaths": "^2.0.0"
|
"removeNPMAbsolutePaths": "^2.0.0",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-win64": "node modules/build win64",
|
"tsc": "ts-node tsc.ts",
|
||||||
"build-linux64": "node modules/build linux64",
|
"prebuild-win64": "npm run tsc",
|
||||||
"build-macos64": "node modules/build macos64",
|
"prebuild-linux64": "npm run tsc",
|
||||||
|
"prebuild-maxos64": "npm run tsc",
|
||||||
|
"build-win64": "cd lib && node modules/build win64",
|
||||||
|
"build-linux64": "cd lib && node modules/build linux64",
|
||||||
|
"build-macos64": "cd lib && node modules/build macos64",
|
||||||
"eslint": "eslint *.js modules",
|
"eslint": "eslint *.js modules",
|
||||||
"eslint-fix": "eslint *.js modules --fix",
|
"eslint-fix": "eslint *.js modules --fix",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"pretest": "npm run tsc",
|
||||||
|
"test": "cd lib && node modules/build win64 && node modules/build linux64 && node modules/build macos64"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
tsc.ts
Normal file
63
tsc.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { removeSync, copyFileSync } from 'fs-extra';
|
||||||
|
|
||||||
|
const ignore = [
|
||||||
|
'.git',
|
||||||
|
'lib',
|
||||||
|
'node_modules',
|
||||||
|
'@types'
|
||||||
|
].map(a => path.join(__dirname, a));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
removeSync('lib');
|
||||||
|
const tsc = exec('npx tsc');
|
||||||
|
tsc.stdout?.on('data', console.log);
|
||||||
|
tsc.stderr?.on('data', console.log);
|
||||||
|
|
||||||
|
tsc.on('close', () => {
|
||||||
|
const files = readDir(__dirname);
|
||||||
|
const filtered = files.filter(a => {
|
||||||
|
if (a.stats.isFile()) {
|
||||||
|
return a.path.split('.').pop() !== 'ts';
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
filtered.forEach(item => {
|
||||||
|
const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, ''));
|
||||||
|
if (item.stats.isDirectory()) {
|
||||||
|
if (!fs.existsSync(itemPath))
|
||||||
|
fs.mkdirSync(itemPath);
|
||||||
|
} else {
|
||||||
|
copyFileSync(item.path, itemPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
const readDir = (dir: string) : {
|
||||||
|
path: string,
|
||||||
|
stats: fs.Stats
|
||||||
|
}[] => {
|
||||||
|
const items: {
|
||||||
|
path: string,
|
||||||
|
stats: fs.Stats
|
||||||
|
}[] = [];
|
||||||
|
const content = fs.readdirSync(dir);
|
||||||
|
for (const item of content) {
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
if (ignore.some(a => itemPath.startsWith(a)))
|
||||||
|
continue;
|
||||||
|
const stats = fs.statSync(itemPath);
|
||||||
|
items.push({
|
||||||
|
path: itemPath,
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
items.push(...readDir(itemPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
74
tsconfig.json
Normal file
74
tsconfig.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"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": "./lib", /* 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 */
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"./videos",
|
||||||
|
"./tsc.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue