4.5.0
This commit is contained in:
commit
0d8ef58d98
14 changed files with 1183 additions and 0 deletions
27
.eslintrc.yml
Normal file
27
.eslintrc.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
env:
|
||||
commonjs: true
|
||||
es6: true
|
||||
node: true
|
||||
extends: 'eslint:recommended'
|
||||
globals:
|
||||
Atomics: readonly
|
||||
SharedArrayBuffer: readonly
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
rules:
|
||||
no-empty:
|
||||
- error
|
||||
- { "allowEmptyCatch": true }
|
||||
indent:
|
||||
- error
|
||||
- 4
|
||||
- { "SwitchCase": 1 }
|
||||
linebreak-style:
|
||||
- error
|
||||
- windows
|
||||
quotes:
|
||||
- error
|
||||
- single
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please fill in the placeholders.-->
|
||||
|
||||
** Main info: **
|
||||
|
||||
Script version:
|
||||
Show ID:
|
||||
Episode ID:
|
||||
|
||||
** Additional Info: **
|
||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/node_modules/
|
||||
.DS_Store
|
||||
ffmpeg
|
||||
mkvmerge
|
||||
token.yml
|
||||
package-lock.json
|
||||
*.exe
|
||||
*.dll
|
||||
*.ts
|
||||
*.mkv
|
||||
*.mp4
|
||||
*.srt
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 AniDL
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
0
bin/.gitkeep
Normal file
0
bin/.gitkeep
Normal file
3
cmd-here.bat
Normal file
3
cmd-here.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@echo off
|
||||
title CmdHere
|
||||
cmd /k PROMPT @$S$P$_$_$G$S
|
||||
2
config/bin-path.yml
Normal file
2
config/bin-path.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ffmpeg: ./bin/ffmpeg
|
||||
mkvmerge: ./bin/mkvmerge
|
||||
7
config/cli-defaults.yml
Normal file
7
config/cli-defaults.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
releaseGroup: Funimation
|
||||
videoLayer: 7
|
||||
fileSuffix: SIZEp
|
||||
nServer: 1
|
||||
mp4mux: false
|
||||
muxSubs: false
|
||||
noCleanUp: false
|
||||
2
config/dir-path.yml
Normal file
2
config/dir-path.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
content: ./videos/
|
||||
trash: ./videos/_trash/
|
||||
169
docs/CHANGELOG.md
Normal file
169
docs/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
## Change Log
|
||||
|
||||
### 4.5.0 (2020/01/21)
|
||||
- Resume downloading
|
||||
- Known bug: Proxy not working
|
||||
|
||||
### 4.4.2 (2019/07/21)
|
||||
- Better proxy handling for stream download
|
||||
|
||||
### 4.4.1 (2019/07/21)
|
||||
- Fixed proxy for stream download
|
||||
|
||||
### 4.4.0 (2019/06/04)
|
||||
- Added `--novids` option (Thanks to @subdiox)
|
||||
- Update modules
|
||||
|
||||
### 4.3.2 (2019/05/09)
|
||||
- Code improvements
|
||||
- Fix `hls-download` error printing
|
||||
|
||||
### 4.3.1 (2019/05/09)
|
||||
- Fix auto detection max quality (Regression in d7d280c)
|
||||
|
||||
### 4.3.0 (2019/05/09)
|
||||
- Better server selection (Closes #42)
|
||||
|
||||
### 4.2.1 (2019/05/04)
|
||||
- Filter duplicate urls for cloudfront.net (Closes #40)
|
||||
|
||||
### 4.2.0 (2019/05/02)
|
||||
- Replace `request` module with `got`
|
||||
- Changed proxy cli options
|
||||
- Changed `login` option name to `auth`
|
||||
- Changed `hls-download` parallel download configuration from 5 parts to 10
|
||||
- Update modules
|
||||
|
||||
### 4.1.0 (2019/04/05)
|
||||
- CLI options for login moved to CUI
|
||||
- Removed showing set token at startup
|
||||
|
||||
### 4.0.5 (2019/02/09)
|
||||
- Fix downloading shows with autoselect max quality
|
||||
|
||||
### 4.0.4 (2019/01/26)
|
||||
- Fix search when shows not found
|
||||
- Update modules
|
||||
|
||||
### 4.0.3 (2018/12/06)
|
||||
- Select only non-encrypted (HLS) streams, encrypted streams is MPEG-DASH
|
||||
|
||||
### 4.0.2 (2018/11/25)
|
||||
- Fix typos and update modules
|
||||
|
||||
### 4.0.1 (2018/11/23)
|
||||
- Code refactoring and small fixes
|
||||
|
||||
### 4.0.0 RC 1 (2018/11/17)
|
||||
- Select range of episodes using hyphen-sequence
|
||||
- Skip muxing if executables not found
|
||||
- Fixed typos and duplicate options
|
||||
|
||||
### 4.0.0 Beta 2 (2018/11/12)
|
||||
- Select alternative server
|
||||
- Updated readme
|
||||
|
||||
### 4.0.0 Beta 1 (2018/11/10)
|
||||
- Rearrange folders structure
|
||||
- Configuration changed to yaml format
|
||||
- Muxing changed to MKV by default
|
||||
- tsMuxeR+mp4box replaced with FFMPEG
|
||||
- Updated commands help and readme
|
||||
- Fixed typos and duplicate options
|
||||
- `ttml2srt` moved to separate module
|
||||
- Drop `m3u8-stream-list` module
|
||||
- Code improvements
|
||||
|
||||
### 3.2.8 (2018/06/16)
|
||||
- Fix video request when token not specified
|
||||
|
||||
### 3.2.7 (2018/06/15)
|
||||
- Update modules
|
||||
|
||||
### 3.2.6 (2018/02/18)
|
||||
- Fix commands help
|
||||
|
||||
### 3.2.5 (2018/02/12)
|
||||
- Fixes and update modules
|
||||
|
||||
### 3.2.4 (2018/02/01)
|
||||
- Update modules
|
||||
|
||||
### 3.2.3 (2018/01/31)
|
||||
- Rearrange folders structure
|
||||
|
||||
### 3.2.2 (2018/01/16)
|
||||
- Update modules
|
||||
|
||||
### 3.2.1 (2018/01/16)
|
||||
- Update modules
|
||||
- Small fixes
|
||||
|
||||
### 3.2.0 (2018/01/16)
|
||||
- `hls-download` module moved to independent module
|
||||
- Auth for socks proxy
|
||||
|
||||
### 3.1.0 (2017/12/30)
|
||||
- Convert DXFP (TTML) subtitles to SRT format
|
||||
|
||||
### 3.0.1 (2017/12/05)
|
||||
- Check subtitles availability
|
||||
- Download subtitles in SRT format instead of VTT
|
||||
- Extended hls download progress info
|
||||
|
||||
### 3.0.0 Beta 3 (2017/12/03)
|
||||
- Restored MKV and MP4 muxing
|
||||
- Convert VTT subtitles to SRT format
|
||||
|
||||
### 3.0.0 Beta 2 (2017/10/18)
|
||||
- Fix video downloading
|
||||
|
||||
### 3.0.0 Beta 1 (2017/10/17)
|
||||
- Major code changes and improvements
|
||||
- Drop Streamlink and added own module for hls download
|
||||
|
||||
### 2.5.0 (2017/09/04)
|
||||
- `nosubs` option
|
||||
- Request video with app api
|
||||
|
||||
### 2.4.1 (2017/09/02)
|
||||
- Fixed typo in package.json
|
||||
- Fix #11: URL for getting video stream url was changed
|
||||
|
||||
### 2.4.0 (2017/07/04)
|
||||
- IPv4 Socks5 proxy support
|
||||
|
||||
### 2.3.3 (2017/06/19)
|
||||
- Removed forgotten debug code
|
||||
|
||||
### 2.3.2 (2017/06/19)
|
||||
- Fix #5: Script fails to multiplex unique file names
|
||||
|
||||
### 2.3.1 (2017/04/29)
|
||||
- Code improvements
|
||||
|
||||
### 2.3.0 (2017/04/27)
|
||||
- Code improvements
|
||||
|
||||
### 2.2.5 (2017/04/17)
|
||||
- Minor code improvements and fixes
|
||||
|
||||
### 2.1.4 (2017/04/10)
|
||||
- Minor changes
|
||||
|
||||
### 2.1.3 (2017/04/10)
|
||||
- Minor changes and fixes
|
||||
|
||||
### 2.1.2 (2017/04/10)
|
||||
- Fix config path
|
||||
|
||||
### 2.1.1 (2017/04/10)
|
||||
- Minor text changes
|
||||
- Fix config
|
||||
- Minor changes
|
||||
|
||||
### 2.1.0 (2017/04/10)
|
||||
- First stable release
|
||||
|
||||
### 2.0.0 Beta (lost in time)
|
||||
- First public release
|
||||
84
docs/README.md
Normal file
84
docs/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Funimation Downloader NX
|
||||
|
||||
Funimation Downloader NX is capable of downloading videos from the *Funimation* streaming service.
|
||||
|
||||
Fork of @seiya-dev's Funimation Downloader NX
|
||||
|
||||
## Legal Warning
|
||||
|
||||
This application is not endorsed by or affiliated with *Funimation*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* NodeJS >= 12.4.0 (https://nodejs.org/)
|
||||
* NPM >= 6.9.0 (https://www.npmjs.org/)
|
||||
* ffmpeg >= 4.0.0 (https://www.videohelp.com/software/ffmpeg)
|
||||
* MKVToolNix >= 20.0.0 (https://www.videohelp.com/software/MKVToolNix)
|
||||
|
||||
### Paths Configuration
|
||||
|
||||
By default this application uses the following paths to programs (main executables):
|
||||
* `./bin/mkvmerge`
|
||||
* `./bin/ffmpeg`
|
||||
|
||||
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
|
||||
|
||||
### Node Modules
|
||||
|
||||
After installing NodeJS with NPM go to directory with `package.json` file and type: `npm i`.
|
||||
* [check dependencies](https://david-dm.org/anidl/funimation-downloader-nx)
|
||||
|
||||
## CLI Options
|
||||
|
||||
### Authentication
|
||||
|
||||
* `--auth` enter auth mode
|
||||
|
||||
### Get Show ID
|
||||
|
||||
* `--search <s>` sets the show title for search
|
||||
|
||||
### Download Video
|
||||
|
||||
* `-s <i> -e <s>` sets the show id and episode ids (comma-separated, hyphen-sequence)
|
||||
* `-q <i>` sets the video layer quality [1...10] (optional, 0 is max)
|
||||
* `--alt` alternative episode listing (if available)
|
||||
* `--sub` switch from English dub to Japanese dub with subtitles
|
||||
* `--simul` force select simulcast version instead of uncut version
|
||||
* `-x` select server
|
||||
* `--novids` skip download videos (for downloading subtitles only)
|
||||
* `--nosubs` skip download subtitles for Dub (if available)
|
||||
|
||||
### Proxy
|
||||
|
||||
* `--proxy <s>` http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080)
|
||||
* `--proxy-auth <s>` Colon-separated username and password for proxy
|
||||
* `--ssp` don't use proxy for stream downloading
|
||||
|
||||
### Muxing
|
||||
|
||||
`[note] this application mux into mkv by default`
|
||||
* `--mp4` mux into mp4
|
||||
* `--mks` add subtitles to mkv or mp4 (if available)
|
||||
|
||||
### Filenaming (optional)
|
||||
|
||||
* `-a <s>` release group ("Funimation" by default)
|
||||
* `-t <s>` show title override
|
||||
* `--ep <s>` episode number override (ignored in batch mode)
|
||||
* `--suffix <s>` filename suffix override (first "SIZEp" will be replaced with actual video size, "SIZEp" by default)
|
||||
|
||||
### Utility
|
||||
|
||||
* `--nocleanup` move unnecessary files to trash folder after completion instead of deleting
|
||||
* `-h`, `--help` show all options
|
||||
|
||||
## Filename Template
|
||||
|
||||
[`release group`] `title` - `episode` [`suffix`].`extension`
|
||||
|
||||
## CLI Examples
|
||||
|
||||
* `node funi --search "My Hero"` search "My Hero" in title
|
||||
* `node funi -s 124389 -e 1,2,3` download episodes 1-3 from show with id 124389
|
||||
* `node funi -s 124389 -e 1-3,2-7,s1-2` download episodes 1-7 and "S"-episodes 1-2 from show with id 124389
|
||||
801
funi.js
Normal file
801
funi.js
Normal file
|
|
@ -0,0 +1,801 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// modules build-in
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
// package json
|
||||
const packageJson = require(path.join(__dirname,'package.json'));
|
||||
|
||||
// program name
|
||||
console.log(`\n=== Funimation Downloader NX ${packageJson.version} ===\n`);
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
|
||||
// request
|
||||
const got = require('got').extend({
|
||||
headers: { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0' },
|
||||
});
|
||||
|
||||
// modules extra
|
||||
const yaml = require('yaml');
|
||||
const shlp = require('sei-helper');
|
||||
const yargs = require('yargs');
|
||||
const FormData = require('form-data');
|
||||
const { lookpath } = require('lookpath');
|
||||
|
||||
// m3u8 and ttml
|
||||
const m3u8 = require('m3u8-parsed');
|
||||
const streamdl = require('hls-download');
|
||||
const { ttml2srt } = require('ttml2srt');
|
||||
|
||||
// get cfg file
|
||||
function getYamlCfg(file){
|
||||
let data = {};
|
||||
if(fs.existsSync(file)){
|
||||
try{
|
||||
data = yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
return data;
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// new-cfg
|
||||
const cfgFolder = __dirname + '/config';
|
||||
const binCfgFile = path.join(cfgFolder,'bin-path.yml');
|
||||
const dirCfgFile = path.join(cfgFolder,'dir-path.yml');
|
||||
const cliCfgFile = path.join(cfgFolder,'cli-defaults.yml');
|
||||
const tokenFile = path.join(cfgFolder,'token.yml');
|
||||
|
||||
// params
|
||||
let cfg = {
|
||||
bin: getYamlCfg(binCfgFile),
|
||||
dir: getYamlCfg(dirCfgFile),
|
||||
cli: getYamlCfg(cliCfgFile),
|
||||
};
|
||||
|
||||
// token
|
||||
let token = getYamlCfg(tokenFile);
|
||||
token = token.token ? token.token : false;
|
||||
|
||||
// info if token not set
|
||||
if(!token){
|
||||
console.log('[INFO] Token not set!\n');
|
||||
}
|
||||
|
||||
// cli
|
||||
let argv = yargs
|
||||
.wrap(Math.min(100))
|
||||
.usage('Usage: $0 [options]')
|
||||
.help(false).version(false)
|
||||
|
||||
// auth
|
||||
.describe('auth','Enter auth mode')
|
||||
|
||||
// search
|
||||
.describe('search','Sets the show title for search')
|
||||
|
||||
// params
|
||||
.describe('s','Sets the show id')
|
||||
.describe('e','Select episode ids (comma-separated, hyphen-sequence)')
|
||||
|
||||
.describe('q','Video layer (0 is max)')
|
||||
.choices('q', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
.default('q', cfg.cli.videoLayer)
|
||||
|
||||
.describe('alt','Alternative episode listing (if available)')
|
||||
.boolean('alt')
|
||||
|
||||
.describe('sub','Subtitles mode (Dub mode by default)')
|
||||
.boolean('sub')
|
||||
|
||||
.describe('simul','Forсe download simulcast version instead of uncut')
|
||||
.boolean('simul')
|
||||
|
||||
.describe('x','Select server')
|
||||
.choices('x', [1, 2, 3, 4])
|
||||
.default('x', cfg.cli.nServer)
|
||||
|
||||
.describe('novids', 'Skip download videos')
|
||||
.boolean('novids')
|
||||
|
||||
.describe('nosubs','Skip download subtitles for Dub (if available)')
|
||||
.boolean('nosubs')
|
||||
|
||||
// proxy
|
||||
.describe('proxy','http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080/)')
|
||||
.describe('proxy-auth','Colon-separated username and password for proxy')
|
||||
.describe('ssp','Ignore proxy settings for stream downloading')
|
||||
.boolean('ssp')
|
||||
|
||||
.describe('mp4','Mux into mp4')
|
||||
.boolean('mp4')
|
||||
.default('mp4',cfg.cli.mp4mux)
|
||||
.describe('mks','Add subtitles to mkv or mp4 (if available)')
|
||||
.boolean('mks')
|
||||
.default('mks',cfg.cli.muxSubs)
|
||||
|
||||
.describe('a','Filenaming: Release group')
|
||||
.default('a',cfg.cli.releaseGroup)
|
||||
.describe('t','Filenaming: Series title override')
|
||||
.describe('ep','Filenaming: Episode number override (ignored in batch mode)')
|
||||
.describe('suffix','Filenaming: Filename suffix override (first "SIZEp" will be replaced with actual video size)')
|
||||
.default('suffix',cfg.cli.fileSuffix)
|
||||
|
||||
// util
|
||||
.describe('nocleanup','move temporary files to trash folder instead of deleting')
|
||||
.boolean('nocleanup')
|
||||
.default('nocleanup',cfg.cli.noCleanUp)
|
||||
|
||||
// help
|
||||
.describe('h','Show this help')
|
||||
.alias('h','help')
|
||||
.boolean('h')
|
||||
|
||||
.version(false)
|
||||
.help(false)
|
||||
.argv;
|
||||
|
||||
// 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 fnTitle = '',
|
||||
fnEpNum = '',
|
||||
fnSuffix = '',
|
||||
fnOutput = '',
|
||||
tsDlPath = false,
|
||||
stDlPath = false,
|
||||
batchDL = false;
|
||||
|
||||
// go to work folder
|
||||
try {
|
||||
fs.accessSync(cfg.dir.content, fs.R_OK | fs.W_OK);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
console.log('[ERROR] %s',e.messsage);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// go to content folder (remove it in future version!)
|
||||
process.chdir(cfg.dir.content);
|
||||
|
||||
// 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{
|
||||
yargs.showHelp();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
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));
|
||||
fs.writeFileSync(tokenFile,yaml.stringify({'token':authData.token}));
|
||||
}
|
||||
else if(authData.error){
|
||||
console.log('[ERROR]',authData.error,'\n');
|
||||
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,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
});
|
||||
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)}`,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
});
|
||||
// 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,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
});
|
||||
if(!episodesData.ok){return;}
|
||||
let eps = JSON.parse(episodesData.res.body).items, fnSlug = [], is_selected = false;
|
||||
argv.e = typeof argv.e == 'number' || typeof argv.e == 'string' ? argv.e.toString() : '';
|
||||
argv.e = argv.e.match(',') ? argv.e.split(',') : [argv.e];
|
||||
let epSelList = argv.e, epSelRanges = [], epSelEps = [];
|
||||
epSelList = epSelList.map((e)=>{
|
||||
if(e.match('-')){
|
||||
e = e.split('-');
|
||||
if( e[0].match(/^(?:[A-Z]|)\d+$/i) && e[1].match(/^\d+$/) ){
|
||||
e[0] = e[0].replace(/^(?:([A-Z])|)(0+)/i,'$1');
|
||||
let letter = e[0].match(/^([A-Z])\d+$/i) ? e[0].match(/^([A-Z])\d+$/i)[1].toUpperCase() : '';
|
||||
e[0] = e[0].replace(/^[A-Z](\d+)$/i,'$1');
|
||||
e[0] = parseInt(e[0]);
|
||||
e[1] = parseInt(e[1]);
|
||||
if(e[0] < e[1]){
|
||||
for(let i=e[0];i<e[1]+1;i++){
|
||||
epSelRanges.push(letter+i);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
else{
|
||||
return (letter+e[0]);
|
||||
}
|
||||
}
|
||||
else{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
else if(e.match(/^(?:[A-Z]|)\d+$/i)){
|
||||
return e.replace(/^(?:([A-Z])|)(0+)/i,'$1').toUpperCase();
|
||||
}
|
||||
else{
|
||||
return '';
|
||||
}
|
||||
});
|
||||
epSelList = [...new Set(epSelList.concat(epSelRanges))];
|
||||
// parse episodes list
|
||||
for(let e in eps){
|
||||
let showStrId = eps[e].ids.externalShowId;
|
||||
let epStrId = eps[e].ids.externalEpisodeId.replace(new RegExp('^'+showStrId),'');
|
||||
// select
|
||||
if(epSelList.includes(epStrId.replace(/^(?:([A-Z])|)(0+)/,'$1'))){
|
||||
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug});
|
||||
epSelEps.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 < 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
|
||||
let episodeIdStr = epStrId;
|
||||
let conOut = `[${episodeIdStr}] `;
|
||||
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){
|
||||
batchDL = true;
|
||||
}
|
||||
if(fnSlug.length<1){
|
||||
console.log('[INFO] Episodes not selected!\n');
|
||||
process.exit();
|
||||
}
|
||||
else{
|
||||
console.log('[INFO] Selected Episodes: %s\n',epSelEps.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}/`,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
});
|
||||
if(!episodeData.ok){return;}
|
||||
let ep = JSON.parse(episodeData.res.body).items[0], streamId = 0;
|
||||
// build fn
|
||||
fnTitle = argv.t ? argv.t : ep.parent.title;
|
||||
ep.number = isNaN(ep.number) ? ep.number : ( parseInt(ep.number, 10) < 10 ? '0' + ep.number : ep.number );
|
||||
if(ep.mediaCategory != 'Episode'){
|
||||
ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
|
||||
}
|
||||
fnEpNum = argv.ep && !batchDL ? ( parseInt(argv.ep, 10) < 10 ? '0' + argv.ep : argv.ep ) : 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: '' };
|
||||
}
|
||||
});
|
||||
// select
|
||||
media = media.reverse();
|
||||
for(let m of media){
|
||||
let selected = false;
|
||||
if(m.id > 0 && m.type == 'Non-Encrypted'){
|
||||
let dub_type = m.language;
|
||||
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);
|
||||
if(dub_type == 'Japanese' && argv.sub && selUncut){
|
||||
streamId = m.id;
|
||||
stDlPath = m.subtitles;
|
||||
selected = true;
|
||||
}
|
||||
else if(dub_type == 'English' && !argv.sub && selUncut){
|
||||
streamId = m.id;
|
||||
stDlPath = m.subtitles;
|
||||
selected = true;
|
||||
}
|
||||
console.log(`[#${m.id}] ${dub_type} [${m.version}]`,(selected?'(selected)':''));
|
||||
}
|
||||
}
|
||||
if(streamId<1){
|
||||
console.log('[ERROR] Track not selected\n');
|
||||
return;
|
||||
}
|
||||
else{
|
||||
let streamData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/video/${streamId}/signed`,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
dinstid: 'uuid',
|
||||
});
|
||||
if(!streamData.ok){return;}
|
||||
streamData = JSON.parse(streamData.res.body);
|
||||
tsDlPath = false;
|
||||
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 = streamData.items[u].src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!tsDlPath){
|
||||
console.log('[ERROR] Unknown error\n');
|
||||
return;
|
||||
}
|
||||
else{
|
||||
await downloadStreams();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSubsUrl(m){
|
||||
if(argv.nosubs && !argv.sub){
|
||||
return false;
|
||||
}
|
||||
for(let i in m){
|
||||
let fpp = m[i].filePath.split('.');
|
||||
let fpe = fpp[fpp.length-1];
|
||||
if(fpe == 'dfxp'){ // dfxp, srt, vtt
|
||||
return m[i].filePath;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function downloadStreams(){
|
||||
|
||||
// req playlist
|
||||
let plQualityReq = await getData({
|
||||
url: tsDlPath,
|
||||
useProxy: (argv.ssp ? false : true),
|
||||
});
|
||||
if(!plQualityReq.ok){return;}
|
||||
|
||||
let plQualityLinkList = m3u8(plQualityReq.res.body);
|
||||
|
||||
let mainServersList = [
|
||||
'd132fumi6di1wa.cloudfront.net',
|
||||
'funiprod.akamaized.net'
|
||||
];
|
||||
|
||||
let plServerList = [],
|
||||
plStreams = {},
|
||||
plLayersStr = [],
|
||||
plLayersRes = {},
|
||||
plMaxLayer = 1;
|
||||
|
||||
for(let s of plQualityLinkList.playlists){
|
||||
// set layer and max layer
|
||||
let plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8$/)[1]);
|
||||
plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
|
||||
// set urls and servers
|
||||
let plUrlDl = s.uri;
|
||||
let plServer = plUrlDl.split('/')[2];
|
||||
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.height}p`;
|
||||
plLayersRes[plLayerId] = plResolution;
|
||||
let plBandwidth = Math.round(s.attributes.BANDWIDTH/1024);
|
||||
if(plLayerId<10){
|
||||
plLayerId = plLayerId.toString().padStart(2,' ');
|
||||
}
|
||||
let qualityStrAdd = `${plLayerId}: ${plResolution} (${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);
|
||||
}
|
||||
}
|
||||
|
||||
for(let 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;
|
||||
|
||||
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]}) @ ${plSelectedServer}`);
|
||||
console.log('[INFO] Stream URL:',videoUrl);
|
||||
fnSuffix = argv.suffix.replace('SIZEp',plLayersRes[argv.q]);
|
||||
fnOutput = shlp.cleanupFilename(`[${argv.a}] ${fnTitle} - ${fnEpNum} [${fnSuffix}]`);
|
||||
console.log(`[INFO] Output filename: ${fnOutput}`);
|
||||
}
|
||||
else if(argv.x > plServerList.length){
|
||||
console.log('[ERROR] Server not selected!\n');
|
||||
return;
|
||||
}
|
||||
else{
|
||||
console.log('[ERROR] Layer not selected!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!argv.novids) {
|
||||
// download video
|
||||
let reqVideo = await getData({
|
||||
url: videoUrl,
|
||||
useProxy: (argv.ssp ? false : true),
|
||||
});
|
||||
if (!reqVideo.ok) { return; }
|
||||
|
||||
let chunkList = m3u8(reqVideo.res.body);
|
||||
chunkList.baseUrl = videoUrl.split('/').slice(0, -1).join('/') + '/';
|
||||
|
||||
let proxyHLS = false;
|
||||
if (argv.proxy && !argv.ssp) {
|
||||
try {
|
||||
proxyHLS = {};
|
||||
proxyHLS.url = buildProxyUrl(argv.proxy,argv['proxy-auth']);
|
||||
}
|
||||
catch(e){
|
||||
console.log(`\n[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
|
||||
console.log('[WARN] Skiping...');
|
||||
proxyHLS = false;
|
||||
}
|
||||
}
|
||||
|
||||
let tsFile = `${fnOutput}.ts`;
|
||||
let resumeFile = `${tsFile}.resume`;
|
||||
let streamOffset = 0;
|
||||
if(fs.existsSync(tsFile) && fs.existsSync(resumeFile)){
|
||||
try{
|
||||
let resume = JSON.parse(fs.readFileSync(resumeFile, 'utf-8'));
|
||||
if(resume.total == chunkList.segments.length && resume.completed != resume.total){
|
||||
streamOffset = resume.completed;
|
||||
}
|
||||
}
|
||||
catch(e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
let streamdlParams = {
|
||||
fn: tsFile,
|
||||
m3u8json: chunkList,
|
||||
baseurl: chunkList.baseUrl,
|
||||
pcount: 10,
|
||||
partsOffset: streamOffset,
|
||||
proxy: (proxyHLS ? proxyHLS : false)
|
||||
};
|
||||
let dldata = await new streamdl(streamdlParams).download();
|
||||
if (!dldata.ok) {
|
||||
fs.writeFileSync(resumeFile, JSON.stringify(dldata.parts));
|
||||
console.log(`[ERROR] ${dldata.error}\n`);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
console.log('[INFO] Video downloaded!\n');
|
||||
}
|
||||
}
|
||||
else{
|
||||
console.log('[INFO] Skip video downloading...\n');
|
||||
}
|
||||
|
||||
// download subtitles
|
||||
if(stDlPath){
|
||||
console.log('[INFO] Downloading subtitles...');
|
||||
console.log(stDlPath);
|
||||
let subsSrc = await getData({
|
||||
url: stDlPath,
|
||||
useProxy: true,
|
||||
});
|
||||
if(subsSrc.ok){
|
||||
let srtData = ttml2srt(subsSrc.res.body);
|
||||
fs.writeFileSync(`${fnOutput}.srt`,srtData);
|
||||
console.log('[INFO] Subtitles downloaded!');
|
||||
}
|
||||
else{
|
||||
console.log('[ERROR] Failed to download subtitles!');
|
||||
argv.mks = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(!fs.statSync(`${fnOutput}.ts`).isFile()){
|
||||
console.log('\n[INFO] TS file not found, skip muxing video...\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// add subs
|
||||
let addSubs = argv.mks && stDlPath ? true : false;
|
||||
|
||||
// check exec path
|
||||
let mkvmergebinfile = await lookpath(path.join(cfg.bin.mkvmerge));
|
||||
let ffmpegbinfile = await lookpath(path.join(cfg.bin.ffmpeg));
|
||||
// check exec
|
||||
if( !argv.mp4 && !mkvmergebinfile ){
|
||||
console.log('[WARN] MKVMerge not found, skip using this...');
|
||||
cfg.bin.mkvmerge = false;
|
||||
}
|
||||
if( !mkvmergebinfile && !ffmpegbinfile || argv.mp4 && !ffmpegbinfile ){
|
||||
console.log('[WARN] FFmpeg not found, skip using this...');
|
||||
cfg.bin.ffmpeg = false;
|
||||
}
|
||||
|
||||
// ftag
|
||||
argv.ftag = argv.ftag ? argv.ftag : argv.a;
|
||||
argv.ftag = shlp.cleanupFilename(argv.ftag);
|
||||
|
||||
// select muxer
|
||||
if(!argv.mp4 && cfg.bin.mkvmerge){
|
||||
// mux to mkv
|
||||
let mkvmux = [];
|
||||
mkvmux.push('-o',`${fnOutput}.mkv`);
|
||||
mkvmux.push('--no-date','--disable-track-statistics-tags','--engage','no_variable_data');
|
||||
mkvmux.push('--track-name',`0:[${argv.ftag}]`);
|
||||
mkvmux.push('--language',`1:${argv.sub?'jpn':'eng'}`);
|
||||
mkvmux.push('--video-tracks','0','--audio-tracks','1');
|
||||
mkvmux.push('--no-subtitles','--no-attachments');
|
||||
mkvmux.push(`${fnOutput}.ts`);
|
||||
if(addSubs){
|
||||
mkvmux.push('--language','0:eng');
|
||||
mkvmux.push(`${fnOutput}.srt`);
|
||||
}
|
||||
fs.writeFileSync(`${fnOutput}.json`,JSON.stringify(mkvmux,null,' '));
|
||||
shlp.exec('mkvmerge',`"${cfg.bin.mkvmerge}"`,`@"${fnOutput}.json"`);
|
||||
fs.unlinkSync(`${fnOutput}.json`);
|
||||
}
|
||||
else if(cfg.bin.ffmpeg){
|
||||
let ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
||||
let ffmux = `-i "${fnOutput}.ts" `;
|
||||
ffmux += addSubs ? `-i "${fnOutput}.srt" ` : '';
|
||||
ffmux += '-map 0 -c:v copy -c:a copy ';
|
||||
ffmux += addSubs ? '-map 1 ' : '';
|
||||
ffmux += addSubs && !argv.mp4 ? '-c:s srt ' : '';
|
||||
ffmux += addSubs && argv.mp4 ? '-c:s mov_text ' : '';
|
||||
ffmux += '-metadata encoding_tool="no_variable_data" ';
|
||||
ffmux += `-metadata:s:v:0 title="[${argv.a}]" -metadata:s:a:0 language=${argv.sub?'jpn':'eng'} `;
|
||||
ffmux += addSubs ? '-metadata:s:s:0 language=eng ' : '';
|
||||
ffmux += `"${fnOutput}.${ffext}"`;
|
||||
// mux to mkv
|
||||
shlp.exec('ffmpeg',`"${cfg.bin.ffmpeg}"`,ffmux);
|
||||
}
|
||||
else{
|
||||
console.log('\n[INFO] Done!\n');
|
||||
return;
|
||||
}
|
||||
if(argv.nocleanup){
|
||||
fs.renameSync(fnOutput+'.ts', path.join(cfg.dir.trash,`/${fnOutput}.ts`));
|
||||
if(stDlPath && argv.mks){
|
||||
fs.renameSync(fnOutput+'.srt', path.join(cfg.dir.trash,`/${fnOutput}.srt`));
|
||||
}
|
||||
}
|
||||
else{
|
||||
fs.unlinkSync(fnOutput+'.ts', path.join(cfg.dir.trash,`/${fnOutput}.ts`));
|
||||
if(stDlPath && argv.mks){
|
||||
fs.unlinkSync(fnOutput+'.srt', path.join(cfg.dir.trash,`/${fnOutput}.srt`));
|
||||
}
|
||||
}
|
||||
console.log('\n[INFO] Done!\n');
|
||||
}
|
||||
|
||||
// get data from url
|
||||
async function getData(options){
|
||||
let gOptions = { url: options.url, headers: {} };
|
||||
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.body = new FormData();
|
||||
gOptions.body.append('username', options.auth.user);
|
||||
gOptions.body.append('password', options.auth.pass);
|
||||
}
|
||||
if(options.useToken && token){
|
||||
gOptions.headers.Authorization = `Token ${token}`;
|
||||
}
|
||||
if(options.dinstid){
|
||||
gOptions.headers.devicetype = 'Android Phone';
|
||||
}
|
||||
if(options.useProxy && argv.proxy){
|
||||
try{
|
||||
const ProxyAgent = require('proxy-agent');
|
||||
let proxyUrl = buildProxyUrl(argv.proxy,argv['proxy-auth']);
|
||||
gOptions.agent = new ProxyAgent(proxyUrl);
|
||||
gOptions.timeout = 10000;
|
||||
}
|
||||
catch(e){
|
||||
console.log(`\n[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
|
||||
console.log('[WARN] Skiping...');
|
||||
argv.proxy = false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if(argv.debug){
|
||||
console.log('[Debug] REQ:', gOptions);
|
||||
}
|
||||
let res = await got(gOptions);
|
||||
if(res.body && res.body.match(/^</)){
|
||||
throw { name: 'HTMLError', res };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
res,
|
||||
};
|
||||
}
|
||||
catch(error){
|
||||
if(argv.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,
|
||||
};
|
||||
}
|
||||
}
|
||||
function buildProxyUrl(proxyBaseUrl,proxyAuth){
|
||||
let proxyCfg = new URL(proxyBaseUrl);
|
||||
if(!proxyCfg.hostname || !proxyCfg.port){
|
||||
throw new Error();
|
||||
}
|
||||
if(proxyAuth && proxyAuth.match(':')){
|
||||
proxyCfg.auth = proxyAuth;
|
||||
}
|
||||
return url.format({
|
||||
protocol: proxyCfg.protocol,
|
||||
slashes: true,
|
||||
auth: proxyCfg.auth,
|
||||
hostname: proxyCfg.hostname,
|
||||
port: proxyCfg.port,
|
||||
});
|
||||
}
|
||||
37
package.json
Normal file
37
package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "funimation-downloader-nx",
|
||||
"version": "4.5.0",
|
||||
"description": "Download videos from Funimation via cli.",
|
||||
"keywords": [
|
||||
"download",
|
||||
"downloader",
|
||||
"funimation",
|
||||
"funimationnow",
|
||||
"util",
|
||||
"utility",
|
||||
"cli"
|
||||
],
|
||||
"author": "AniDL",
|
||||
"homepage": "https://github.com/anidl/funimation-downloader-nx",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/anidl/funimation-downloader-nx.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/anidl/funimation-downloader-nx/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "funi.js",
|
||||
"dependencies": {
|
||||
"form-data": "^3.0.0",
|
||||
"got": "^10.2.2",
|
||||
"hls-download": "^2.3.1",
|
||||
"lookpath": "^1.0.4",
|
||||
"m3u8-parsed": "^1.2.0",
|
||||
"proxy-agent": "^3.1.1",
|
||||
"sei-helper": "^3.3.0",
|
||||
"ttml2srt": "^1.1.0",
|
||||
"yaml": "^1.7.2",
|
||||
"yargs": "^15.1.0"
|
||||
}
|
||||
}
|
||||
0
videos/_trash/.gitkeep
Normal file
0
videos/_trash/.gitkeep
Normal file
Loading…
Reference in a new issue