37 Commits

Author SHA1 Message Date
Godzil
978a3282a4 1.3.0 2018-07-30 22:47:33 +01:00
Manoël Trapier
9f0195bebc Update README.md 2018-07-30 22:47:33 +01:00
Manoël Trapier
ea20108222 Update README.md 2018-07-30 22:47:33 +01:00
Godzil
4ee814864c Add support for changing the batchfile on the command line 2018-07-30 22:47:33 +01:00
Godzil
4cbfd691c3 add missing package 2018-07-30 22:47:32 +01:00
Godzil
7c04fb7282 Make tslint happy! 2018-07-30 22:47:29 +01:00
Godzil
849c7612aa A bit of code reformating and add an option to regenerate the .crpersistant file in case it become corrupted and Crunchy try to redownload everything. 2018-07-30 22:46:29 +01:00
Roei Elisha
6ad4cbed0a make login work 2018-07-30 22:45:28 +01:00
Godzil
9e2f5401d0 Update tslint.json 2018-07-30 22:44:18 +01:00
Roei Elisha
b064b97f2d fix linter problems 2018-05-22 23:55:10 +03:00
Godzil
b248405437 1.2.2 2018-05-09 22:33:45 +01:00
Godzil
bf941819a8 remove unwwanted parameter 2018-05-09 22:31:50 +01:00
Godzil
fcae53baae Node 5, 6 and 7 seems to not like something. Delete them from Travis build 2018-05-08 21:50:58 +01:00
Godzil
05ead50c0d Let's try to make travis happy with older node version 2018-05-08 21:49:11 +01:00
Godzil
0a80f80f91 1.2.1 2018-05-08 21:37:43 +01:00
Godzil
3bf5fea735 Make Crunchy to properly return a return code when running fine or failing 2018-05-08 21:37:34 +01:00
Godzil
3a95994cc2 1.2.0 2018-03-29 22:33:15 +01:00
Godzil
a29870691b Update deps 2018-03-29 22:33:06 +01:00
Godzil
547fdc4aa0 Add a way to select the resolution. Use 1080p by default
Fix #58
2018-03-29 22:29:13 +01:00
Godzil
c78552795f 1.1.22 2018-03-29 20:41:45 +01:00
Godzil
090c7e4789 Trying to fix #59 by adding a referer to the header. Seems to fix it but need to be throughfully tested.. 2018-03-29 20:40:17 +01:00
Godzil
bf8e1fe80f Update cloudscraper 2018-03-29 20:38:38 +01:00
Manoël Trapier
7344ce3d61 Update README.md 2018-01-31 17:09:58 +00:00
Godzil
c642e76cce Make sure that it is rebuild before publishing 2017-12-27 05:34:22 +01:00
Godzil
8ef27066f6 1.1.21 2017-12-27 05:19:28 +01:00
Godzil
621df26b58 Try to make travis happy (again) 2017-12-27 05:16:58 +01:00
Godzil
8060b1b73b Update travis definition 2017-12-27 04:58:21 +01:00
Godzil
11f6b3feff Make tslint happy 2017-12-27 04:57:45 +01:00
Godzil
537639f2a8 Simplify tsconfig to no longer list .ts file, also simplify commands as typings is no longuer there 2017-12-27 04:57:24 +01:00
Godzil
813f8a997d Completely remote typings to use TypeScript2.0 type management, update also some deps 2017-12-27 04:56:26 +01:00
Godzil
48544020a1 1.1.20 2017-09-16 22:58:27 +01:00
Godzil
cc68d21107 correct permissions 2017-09-16 22:54:49 +01:00
Godzil
acd91e2679 Add (unless) node minimum version in packages.json 2017-09-16 22:54:27 +01:00
Godzil
53f0a9462a Better filename forbidden character handling
Logs are a bit better
2017-09-16 22:51:49 +01:00
Godzil
10d71944d9 Fix lint error 2017-08-21 16:08:58 +02:00
Manoël Trapier
b5bbde7cdd Change to make travis npm happy 2017-08-21 14:24:23 +01:00
Manoël Trapier
c406bc70ee Sanitise more characters from filenames 2017-05-17 16:17:26 +01:00
23 changed files with 1603 additions and 338 deletions

View File

@@ -1,11 +1,10 @@
language: node_js language: node_js
sudo: false sudo: false
node_js: node_js:
- 5 - 8
- 6 - 9
before_install: before_install:
- npm install --dev - npm install --only=dev
script: script:
- npm run types - npm run build
- npm run compile
- npm test - npm test

View File

@@ -1,6 +1,6 @@
# Crunchy: a fork of Deathspike/CrunchyRoll.js # Crunchy: a fork of Deathspike/CrunchyRoll.js
[![Issue Stats](http://issuestats.com/github/Godzil/Crunchy/badge/issue)](http://issuestats.com/github/Godzil/Crunchy) [![Travis CI](https://travis-ci.org/Godzil/Crunchy.svg?branch=master)](https://travis-ci.org/Godzil/Crunchy) [![Issue Stats](http://issuestats.com/github/Godzil/Crunchy/badge/issue)](http://issuestats.com/github/Godzil/Crunchy) [![Travis CI](https://travis-ci.org/Godzil/Crunchy.svg?branch=master)](https://travis-ci.org/Godzil/Crunchy) [![Maintainability](https://api.codeclimate.com/v1/badges/413c7ca11c0805b1ef3e/maintainability)](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
*Crunchy* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file. *Crunchy* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file.
@@ -21,30 +21,39 @@ It is recommended to enable authentication (`-p` and `-u`) so your account permi
## Prerequisites ## Prerequisites
* NodeJS >= 5.x (http://nodejs.org/) * NodeJS >= 8.1 (http://nodejs.org/)
* NPM >= 2.5.x (https://www.npmjs.org/) * NPM >= 5.8 (https://www.npmjs.org/)
## Installation ## Installation
Use the applicable instructions to install. Is your operating system not listed? Please ask or contribute! Use the applicable instructions to install. Is your operating system not listed? Please ask or contribute!
### Debian (Mint, Ubuntu, etc) ### Linux (Debian, Mint, Ubuntu, etc)
1. Run in *Terminal*: `sudo apt-get install nodejs npm mkvtoolnix rtmpdump ffmpeg` 1. Run in *Terminal*: `sudo apt-get install nodejs npm mkvtoolnix rtmpdump ffmpeg`
2. Run in *Terminal*: `sudo ln -s /usr/bin/nodejs /usr/bin/node` 2. Run in *Terminal*: `sudo ln -s /usr/bin/nodejs /usr/bin/node`
3. Run in *Terminal*: `sudo npm install -g crunchy` 3. Run in *Terminal*: `sudo npm install -g crunchy`
#### Updating:
1. Run in *Terminal*: `sudo npm update -g crunchy`
### Mac OS X ### Mac OS X
1. Install *Homebrew* following the instructions at http://brew.sh/ 1. Install *Homebrew* following the instructions at http://brew.sh/
2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump ffmpeg` 2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump ffmpeg`
3. Run in *Terminal*: `npm install -g crunchy` 3. Run in *Terminal*: `npm install -g crunchy`
#### Updating:
1. Run in *Terminal*: `sudo npm update -g crunchy`
### Windows ### Windows
1. Install *NodeJS* following the instructions at http://nodejs.org/ 1. Install *NodeJS* following the instructions at http://nodejs.org/
3. Run in *Command Prompt*: `npm install -g crunchy` 3. Run in *Command Prompt*: `npm install -g crunchy`
#### Updating:
1. Run in *Command Prompt*: `npm update -g crunchy`
## Instructions ## Instructions
Use the applicable instructions for the interface of your choice (currently limited to command-line). Use the applicable instructions for the interface of your choice (currently limited to command-line).

0
bin/crunchy Normal file → Executable file
View File

10
bin/crunchy.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
PARAMS=$*
for i in {1..20}; do
crunchy ${PARAMS}
if [ $? == 0 ]; then
break
fi
echo "Going to retry..."
sleep 3
done

1175
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,31 +12,43 @@
"type": "git", "type": "git",
"url": "git://github.com/Godzil/crunchyroll.js.git" "url": "git://github.com/Godzil/crunchyroll.js.git"
}, },
"version": "1.1.19", "engines": {
"node": ">=5.0"
},
"version": "1.3.0",
"bin": { "bin": {
"crunchy": "./bin/crunchy" "crunchy": "./bin/crunchy",
"crunchy.sh": "./bin/crunchy.sh"
}, },
"dependencies": { "dependencies": {
"big-integer": "^1.4.4", "big-integer": "^1.6.30",
"bluebird": "^3.5.1",
"cheerio": "^0.22.0", "cheerio": "^0.22.0",
"cloudscraper": "^1.4.1", "cloudscraper": "^1.5.0",
"commander": "^2.6.0", "commander": "^2.15.1",
"fs-extra": "^2.0.0", "fs-extra": "^6.0.1",
"mkdirp": "^0.5.0", "mkdirp": "^0.5.0",
"request": "^2.74.0", "request": "^2.87.0",
"request-promise": "^4.2.2",
"xml2js": "^0.4.5" "xml2js": "^0.4.5"
}, },
"devDependencies": { "devDependencies": {
"@types/bluebird": "^3.5.20",
"@types/cheerio": "^0.22.7",
"@types/fs-extra": "^5.0.2",
"@types/mkdirp": "^0.5.2",
"@types/request": "^2.47.0",
"@types/request-promise": "^4.1.41",
"@types/xml2js": "^0.4.2",
"tsconfig-lint": "^0.12.0", "tsconfig-lint": "^0.12.0",
"tslint": "^4.4.2", "tslint": "^5.10.0",
"typescript": "^2.2.0", "typescript": "^2.9.1"
"typings": "^2.1.0"
}, },
"scripts": { "scripts": {
"prepublish": "npm run types && tsc", "prepublishOnly": "npm run build",
"compile": "tsc", "compile": "tsc",
"test": "tslint -c ./tslint.json --project ./tsconfig.json ./src/**/*.ts", "build": "tsc",
"types": "typings install", "test": "tslint --project .",
"start": "node ./bin/crunchy" "start": "node ./bin/crunchy"
}, },
"bugs": { "bugs": {

View File

@@ -2,33 +2,65 @@
import commander = require('commander'); import commander = require('commander');
import fs = require('fs'); import fs = require('fs');
import path = require('path'); import path = require('path');
import log = require('./log');
import series from './series'; import series from './series';
/* correspondances between resolution and value CR excpect */
const resol_table: { [id: string]: IResolData; } =
{
360: {quality: '60', format: '106'},
480: {quality: '61', format: '106'},
720: {quality: '62', format: '106'},
1080: {quality: '80', format: '108'},
};
/** /**
* Streams the batch of series to disk. * Streams the batch of series to disk.
*/ */
export default function(args: string[], done: (err?: Error) => void) export default function(args: string[], done: (err?: Error) => void)
{ {
const config = parse(args); const config = parse(args);
const batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt'); const batchPath = path.join(config.output || process.cwd(), config.batch);
tasks(config, batchPath, (err, tasks) => // set resolution
if (config.resolution)
{
try
{
config.video_format = resol_table[config.resolution].format;
config.video_quality = resol_table[config.resolution].quality;
}
catch (e)
{
log.warn('Invalid resolution ' + config.resolution + 'p. Setting to 1080p');
config.video_format = resol_table['1080'].format;
config.video_quality = resol_table['1080'].quality;
}
}
else
{
/* 1080 by default */
config.video_format = resol_table['1080'].format;
config.video_quality = resol_table['1080'].quality;
}
tasks(config, batchPath, (err, tasksArr) =>
{ {
if (err) if (err)
{ {
return done(err); return done(err);
} }
let i = 0; let i = 0;
(function next() (function next()
{ {
if (i >= tasks.length) if (i >= tasksArr.length)
{ {
return done(); return done();
} }
series(tasks[i].config, tasks[i].address, (errin) => series(tasksArr[i].config, tasksArr[i].address, (errin) =>
{ {
if (errin) if (errin)
{ {
@@ -150,6 +182,10 @@ function parse(args: string[]): IConfigLine
.option('-o, --output <s>', 'The output path.') .option('-o, --output <s>', 'The output path.')
.option('-s, --series <s>', 'The series override.') .option('-s, --series <s>', 'The series override.')
.option('-n, --filename <s>', 'The name override.') .option('-n, --filename <s>', 'The name override.')
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)') .option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)', 'CrunchyRoll')
.option('-r, --resolution <s>', 'The video resolution. (Default: 1080 (360, 480, 720, 1080))',
'1080')
.option('-g, --rebuildcrp', 'Rebuild the crpersistant file.')
.option('-b, --batch <s>', 'Batch file', 'CrunchyRoll.txt')
.parse(args); .parse(args);
} }

View File

@@ -6,5 +6,8 @@ batch(process.argv, (err: any) =>
if (err) if (err)
{ {
console.error(err.stack || err); console.error(err.stack || err);
process.exit(-1);
} }
console.info('Done!');
process.exit(0);
}); });

View File

@@ -2,7 +2,7 @@
import cheerio = require('cheerio'); import cheerio = require('cheerio');
import fs = require('fs'); import fs = require('fs');
import mkdirp = require('mkdirp'); import mkdirp = require('mkdirp');
import request = require('./request'); import my_request = require('./my_request');
import path = require('path'); import path = require('path');
import subtitle from './subtitle/index'; import subtitle from './subtitle/index';
import video from './video/index'; import video from './video/index';
@@ -57,12 +57,18 @@ function fileExist(path: string)
{ {
fs.statSync(path); fs.statSync(path);
return true; return true;
} catch (e) }
catch (e)
{ {
return false; return false;
} }
} }
function sanitiseFileName(str: string)
{
return str.replace(/[\/':\?\*"<>\.]/g, '_');
}
/** /**
* Downloads the subtitle and video. * Downloads the subtitle and video.
*/ */
@@ -70,25 +76,37 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
{ {
let series = config.series || page.series; let series = config.series || page.series;
series = series.replace('/', '_').replace('\'', '_').replace(':', '_'); series = sanitiseFileName(series);
let fileName = name(config, page, series, '').replace('/', '_').replace('\'', '_').replace(':', '_'); let fileName = sanitiseFileName(name(config, page, series, ''));
let filePath = path.join(config.output || process.cwd(), series, fileName); let filePath = path.join(config.output || process.cwd(), series, fileName);
if (fileExist(filePath + '.mkv')) if (fileExist(filePath + '.mkv'))
{ {
let count = 0; let count = 0;
if (config.rebuildcrp)
{
log.warn('Adding \'' + fileName + '\' to the DB...');
return done(null, false);
}
log.warn('File \'' + fileName + '\' already exist...'); log.warn('File \'' + fileName + '\' already exist...');
do do
{ {
count = count + 1; count = count + 1;
fileName = name(config, page, series, '-' + count).replace('/', '_').replace('\'', '_').replace(':', '_'); fileName = sanitiseFileName(name(config, page, series, '-' + count));
filePath = path.join(config.output || process.cwd(), series, fileName); filePath = path.join(config.output || process.cwd(), series, fileName);
} while (fileExist(filePath + '.mkv')); } while (fileExist(filePath + '.mkv'));
log.warn('Renaming to \'' + fileName + '\'...'); log.warn('Renaming to \'' + fileName + '\'...');
} }
if (config.rebuildcrp)
{
return done(null, true);
}
mkdirp(path.dirname(filePath), (errM: Error) => mkdirp(path.dirname(filePath), (errM: Error) =>
{ {
if (errM) if (errM)
@@ -96,6 +114,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
return done(errM, false); return done(errM, false);
} }
log.dispEpisode(fileName, 'Fetching...', false);
downloadSubtitle(config, player, filePath, (errDS) => downloadSubtitle(config, player, filePath, (errDS) =>
{ {
if (errDS) if (errDS)
@@ -106,7 +125,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
const now = Date.now(); const now = Date.now();
if (player.video.file !== undefined) if (player.video.file !== undefined)
{ {
log.dispEpisode(fileName, 'Fetching...', false); log.dispEpisode(fileName, 'Fetching video...', false);
downloadVideo(config, page, player, filePath, (errDV) => downloadVideo(config, page, player, filePath, (errDV) =>
{ {
if (errDV) if (errDV)
@@ -121,6 +140,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
const isSubtited = Boolean(player.subtitle); const isSubtited = Boolean(player.subtitle);
log.dispEpisode(fileName, 'Merging...', false);
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, (errVM) => video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, (errVM) =>
{ {
if (errVM) if (errVM)
@@ -239,7 +259,7 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
return done(new Error('Invalid address.')); return done(new Error('Invalid address.'));
} }
request.get(config, address, (err, result) => my_request.get(config, address, (err, result) =>
{ {
if (err) if (err)
{ {
@@ -251,7 +271,7 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
const regexp = /\s*([^\n\r\t\f]+)\n?\s*[^0-9]*([0-9][\-0-9.]*)?,?\n?\s\s*[^0-9]*((PV )?[S0-9][P0-9.]*[a-fA-F]?)/; const regexp = /\s*([^\n\r\t\f]+)\n?\s*[^0-9]*([0-9][\-0-9.]*)?,?\n?\s\s*[^0-9]*((PV )?[S0-9][P0-9.]*[a-fA-F]?)/;
const look = $('#showmedia_about_media').text(); const look = $('#showmedia_about_media').text();
const seasonTitle = $('span[itemprop="title"]').text(); const seasonTitle = $('span[itemprop="title"]').text();
let episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, ''); const episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, '');
const data = regexp.exec(look); const data = regexp.exec(look);
if (!swf || !data) if (!swf || !data)
@@ -295,8 +315,13 @@ function scrapePlayer(config: IConfig, address: string, id: number, done: (err:
return done(new Error('Invalid address.')); return done(new Error('Invalid address.'));
} }
request.post(config, { my_request.post(config, {
form: {current_page: address}, form: {
current_page: address,
video_format: config.video_format,
video_quality: config.video_quality,
media_id: id
},
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id, url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id,
}, (err, result) => }, (err, result) =>
{ {
@@ -321,9 +346,9 @@ function scrapePlayer(config: IConfig, address: string, id: number, done: (err:
let streamMode = 'RTMP'; let streamMode = 'RTMP';
if (player['default:preload'].stream_info.host === '') if (player['default:preload'].stream_info.host === '')
{ {
streamMode = 'HLS'; streamMode = 'HLS';
} }
done(null, { done(null, {
subtitle: isSubtitled ? { subtitle: isSubtitled ? {

View File

@@ -14,4 +14,9 @@ interface IConfig {
series?: string; series?: string;
filename?: string; filename?: string;
tag?: string; tag?: string;
resolution?: string;
video_format?: string;
video_quality?: string;
rebuildcrp?: boolean;
batch?: string;
} }

4
src/interface/IResolData.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
interface IResolData {
quality: string;
format: string;
}

View File

@@ -28,10 +28,10 @@ export function warn(str: string)
export function dispEpisode(name: string, status: string, addNL: boolean) export function dispEpisode(name: string, status: string, addNL: boolean)
{ {
/* Do fancy output */ /* Do fancy output */
process.stdout.write(' \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G'); process.stdout.write('\x1B[K \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G');
if (addNL) if (addNL)
{ {
console.log(''); console.log('');
} }
} }

219
src/my_request.ts Normal file
View File

@@ -0,0 +1,219 @@
'use strict';
import cheerio = require('cheerio');
import request = require('request');
import rp = require('request-promise');
import Promise = require('bluebird');
import log = require('./log');
import { RequestPromise } from 'request-promise';
import { Response } from 'request';
// tslint:disable-next-line:no-var-requires
const cloudscraper = require('cloudscraper');
let isAuthenticated = false;
let isPremium = false;
const defaultHeaders: request.Headers =
{
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; x64; rv:58.0) Gecko/20100101 Firefox/58.0',
'Connection': 'keep-alive',
'Referer': 'https://www.crunchyroll.com/login',
};
function generateDeviceId(): string
{
let id = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++)
{
id += possible.charAt(Math.floor(Math.random() * possible.length));
}
return id;
}
function startSession(): Promise<string>
{
return rp(
{
method: 'GET',
url: 'CR_SESSION_URL',
qs:
{
device_id: generateDeviceId(),
device_type: 'CR_DEVICE_TYPE',
access_token: 'CR_SESSION_KEY',
version: 'CR_API_VERSION',
locale: 'CR_LOCALE',
},
json: true,
})
.then((response: any) =>
{
return response.data.session_id;
});
}
function login(sessionId: string, user: string, pass: string): Promise<any>
{
return rp(
{
method: 'POST',
url: 'CR_LOGIN_URL',
form:
{
account: user,
password: pass,
session_id: sessionId,
version: 'CR_API_VERSION',
},
json: true,
})
.then((response) =>
{
if (response.error) throw new Error('Login failed: ' + response.message);
return response.data;
});
}
// TODO: logout
/**
* Performs a GET request for the resource.
*/
export function get(config: IConfig, options: string|request.Options, done: (err: Error, result?: string) => void)
{
authenticate(config, (err) =>
{
if (err)
{
return done(err);
}
cloudscraper.request(modify(options, 'GET'), (error: Error, response: any, body: any) =>
{
if (error) return done(error);
done(null, typeof body === 'string' ? body : String(body));
});
});
}
/**
* Performs a POST request for the resource.
*/
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
{
authenticate(config, (err) =>
{
if (err)
{
return done(err);
}
cloudscraper.request(modify(options, 'POST'), (error: Error, response: any, body: any) =>
{
if (error)
{
return done(error);
}
done(null, typeof body === 'string' ? body : String(body));
});
});
}
/**
* Authenticates using the configured pass and user.
*/
function authenticate(config: IConfig, done: (err: Error) => void)
{
if (isAuthenticated || !config.pass || !config.user)
{
return done(null);
}
startSession()
.then((sessionId: string) =>
{
defaultHeaders.Cookie = `sess_id=${sessionId}; c_locale=enUS`;
return login(sessionId, config.user, config.pass);
})
.then((userData) =>
{
/**
* The page return with a meta based redirection, as we wan't to check that everything is fine, reload
* the main page. A bit convoluted, but more sure.
*/
const options =
{
headers: defaultHeaders,
jar: true,
url: 'http://www.crunchyroll.com/',
method: 'GET',
};
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
{
if (err)
{
return done(err);
}
const $ = cheerio.load(body);
/* Check if auth worked */
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
const dims = regexps.exec($('script').text());
for (let i = 1; i < 5; i++)
{
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered'))
{
isAuthenticated = true;
}
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus'))
{
isPremium = true;
}
}
if (isAuthenticated === false)
{
const error = $('ul.message, li.error').text();
return done(new Error('Authentication failed: ' + error));
}
if (isPremium === false)
{
log.warn('Do not use this app without a premium account.');
}
else
{
log.info('You have a premium account! Good!');
}
done(null);
});
})
.catch(done);
}
/**
* Modifies the options to use the authenticated cookie jar.
*/
function modify(options: string|request.Options, reqMethod: string): request.Options
{
if (typeof options !== 'string')
{
options.jar = true;
options.headers = defaultHeaders;
options.method = reqMethod;
return options;
}
return {
jar: true,
headers: defaultHeaders,
url: options.toString(),
method: reqMethod
};
}

View File

@@ -1,190 +0,0 @@
'use strict';
import request = require('request');
import cheerio = require('cheerio');
import log = require('./log');
const cloudscraper = require('cloudscraper');
let isAuthenticated = false;
let isPremium = false;
const defaultHeaders: request.Headers =
{
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
'Connection': 'keep-alive'
};
/**
* Performs a GET request for the resource.
*/
export function get(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
{
authenticate(config, err =>
{
if (err)
{
return done(err);
}
cloudscraper.request(modify(options, 'GET'), (err: Error, response: any, body: any) =>
{
if (err)
{
return done(err);
}
done(null, typeof body === 'string' ? body : String(body));
});
});
}
/**
* Performs a POST request for the resource.
*/
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
{
authenticate(config, err =>
{
if (err)
{
return done(err);
}
cloudscraper.request(modify(options, 'POST'), (err: Error, response: any, body: any) =>
{
if (err)
{
return done(err);
}
done(null, typeof body === 'string' ? body : String(body));
});
});
}
/**
* Authenticates using the configured pass and user.
*/
function authenticate(config: IConfig, done: (err: Error) => void)
{
if (isAuthenticated || !config.pass || !config.user)
{
return done(null);
}
/* Bypass the login page and send a login request directly */
let options =
{
headers: defaultHeaders,
jar: true,
gzip: false,
method: 'GET',
url: 'https://www.crunchyroll.com/login'
};
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
{
if (err) return done(err);
const $ = cheerio.load(body);
/* Get the token from the login page */
const token = $('input[name="login_form[_token]"]').attr('value');
if (token === '')
{
return done(new Error('Can`t find token!'));
}
let options =
{
headers: defaultHeaders,
form:
{
'login_form[redirect_url]': '/',
'login_form[name]': config.user,
'login_form[password]': config.pass,
'login_form[_token]': token
},
jar: true,
gzip: false,
method: 'POST',
url: 'https://www.crunchyroll.com/login'
};
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
{
if (err)
{
return done(err);
}
/* The page return with a meta based redirection, as we wan't to check that everything is fine, reload
* the main page. A bit convoluted, but more sure.
*/
let options =
{
headers: defaultHeaders,
jar: true,
url: 'http://www.crunchyroll.com/',
method: 'GET'
};
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
{
if (err)
{
return done(err);
}
let $ = cheerio.load(body);
/* Check if auth worked */
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
const dims = regexps.exec($('script').text());
for (let i = 1; i < 5; i++)
{
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered'))
{
isAuthenticated = true;
}
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus'))
{
isPremium = true;
}
}
if (isAuthenticated === false)
{
const error = $('ul.message, li.error').text();
return done(new Error('Authentication failed: ' + error));
}
if (isPremium === false)
{
log.warn('Do not use this app without a premium account.');
}
else
{
log.info('You have a premium account! Good!');
}
done(null);
});
});
});
}
/**
* Modifies the options to use the authenticated cookie jar.
*/
function modify(options: string|request.Options, reqMethod: string): request.Options
{
if (typeof options !== 'string')
{
options.jar = true;
options.headers = defaultHeaders;
options.method = reqMethod;
return options;
}
return { jar: true, headers: defaultHeaders, url: options.toString(), method: reqMethod };
}

View File

@@ -2,8 +2,8 @@
import cheerio = require('cheerio'); import cheerio = require('cheerio');
import episode from './episode'; import episode from './episode';
import fs = require('fs'); import fs = require('fs');
const fse = require('fs-extra'); import fse = require('fs-extra');
import request = require('./request'); import my_request = require('./my_request');
import path = require('path'); import path = require('path');
import url = require('url'); import url = require('url');
import log = require('./log'); import log = require('./log');
@@ -153,12 +153,12 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
episode: '', episode: '',
volume: 0, volume: 0,
}); });
done(null, {episodes: episodes.reverse(), series: ""}); done(null, {episodes: episodes.reverse(), series: ''});
} }
else else
{ {
let episodeCount = 0; let episodeCount = 0;
request.get(config, address, (err, result) => { my_request.get(config, address, (err, result) => {
if (err) { if (err) {
return done(err); return done(err);
} }
@@ -195,7 +195,7 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
}); });
if (episodeCount === 0) if (episodeCount === 0)
{ {
log.warn("No episodes found for " + title + ". Could it be a movie?"); log.warn('No episodes found for ' + title + '. Could it be a movie?');
} }
done(null, {episodes: episodes.reverse(), series: title}); done(null, {episodes: episodes.reverse(), series: title});
}); });

View File

@@ -1,14 +1,14 @@
/* tslint:disable:no-bitwise false */ /* tslint:disable:no-bitwise false */
'use strict'; 'use strict';
import crypto = require('crypto');
import bigInt = require('big-integer'); import bigInt = require('big-integer');
import crypto = require('crypto');
import zlib = require('zlib'); import zlib = require('zlib');
/** /**
* Decodes the data. * Decodes the data.
*/ */
export default function(id: number, iv: Buffer|string, data: Buffer|string, export default function(id: number, iv: Buffer|string, data: Buffer|string,
done: (err?: Error, result?: Buffer) => void) done: (err?: Error, result?: Buffer) => void)
{ {
try try
{ {

View File

@@ -33,10 +33,10 @@ export default function(input: string|Buffer, done: (err: Error, subtitle?: stri
*/ */
function event(block: ISubtitleEvent): string function event(block: ISubtitleEvent): string
{ {
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'; const format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
return '[Events]\n' + return '[Events]\n' +
'Format: ' + format + '\n' + [].concat(block.event).map(style => ('Dialogue: 0,' + 'Format: ' + format + '\n' + [].concat(block.event).map((style) => ('Dialogue: 0,' +
style.$.start + ',' + style.$.start + ',' +
style.$.end + ',' + style.$.end + ',' +
style.$.style + ',' + style.$.style + ',' +
@@ -70,13 +70,13 @@ function script(block: ISubtitle): string
*/ */
function style(block: ISubtitleStyle): string function style(block: ISubtitleStyle): string
{ {
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' + const format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' + 'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' + 'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
'MarginL,MarginR,MarginV,Encoding'; 'MarginL,MarginR,MarginV,Encoding';
return '[V4+ Styles]\n' + return '[V4+ Styles]\n' +
'Format: ' + format + '\n' + [].concat(block.style).map(style => 'Style: ' + 'Format: ' + format + '\n' + [].concat(block.style).map((style) => 'Style: ' +
style.$.name + ',' + style.$.name + ',' +
style.$.font_name + ',' + style.$.font_name + ',' +
style.$.font_size + ',' + style.$.font_size + ',' +

View File

@@ -2,7 +2,7 @@
import ass from './ass'; import ass from './ass';
import srt from './srt'; import srt from './srt';
export default <IFormatterTable> { export default {
ass: ass, ass,
srt: srt srt
}; } as IFormatterTable;

View File

@@ -1,22 +1,23 @@
'use strict'; 'use strict';
import childProcess = require('child_process'); import childProcess = require('child_process');
import fs = require('fs'); import fs = require('fs');
import path = require('path');
import os = require('os'); import os = require('os');
import path = require('path');
import subtitle from '../subtitle/index'; import subtitle from '../subtitle/index';
/** /**
* Merges the subtitle and video files into a Matroska Multimedia Container. * Merges the subtitle and video files into a Matroska Multimedia Container.
*/ */
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string, export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string,
streamMode: string, done: (err: Error) => void) streamMode: string, done: (err: Error) => void)
{ {
const subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass'); const subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
let videoPath = filePath; let videoPath = filePath;
if (streamMode === 'RTMP') if (streamMode === 'RTMP')
{ {
videoPath += path.extname(rtmpInputPath); videoPath += path.extname(rtmpInputPath);
} }
else else
{ {

View File

@@ -1,7 +1,8 @@
'use strict'; 'use strict';
import childProcess = require('child_process'); import childProcess = require('child_process');
import path = require('path');
import os = require('os'); import os = require('os');
import path = require('path');
import log = require('../log'); import log = require('../log');
/** /**
@@ -10,30 +11,31 @@ import log = require('../log');
export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string, export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string,
fileExt: string, mode: string, done: (err: Error) => void) fileExt: string, mode: string, done: (err: Error) => void)
{ {
if (mode === 'RTMP') if (mode === 'RTMP')
{ {
childProcess.exec(command('rtmpdump') + ' ' + childProcess.exec(command('rtmpdump') + ' ' +
'-r "' + rtmpUrl + '" ' + '-r "' + rtmpUrl + '" ' +
'-y "' + rtmpInputPath + '" ' + '-y "' + rtmpInputPath + '" ' +
'-W "' + swfUrl + '" ' + '-W "' + swfUrl + '" ' +
'-o "' + filePath + fileExt + '"', { '-o "' + filePath + fileExt + '"', {
maxBuffer: Infinity, maxBuffer: Infinity,
}, done); }, done);
} }
else if (mode === 'HLS') else if (mode === 'HLS')
{ {
const cmd = command('ffmpeg') + ' ' + const cmd = command('ffmpeg') + ' ' +
'-i "' + rtmpInputPath + '" ' + '-i "' + rtmpInputPath + '" ' +
'-c copy -bsf:a aac_adtstoasc ' + '-c copy -bsf:a aac_adtstoasc ' +
'"' + filePath + '.mp4"'; '"' + filePath + '.mp4"';
childProcess.exec(cmd, { childProcess.exec(cmd,
maxBuffer: Infinity, {
}, done); maxBuffer: Infinity,
} }, done);
else }
{ else
log.error('No such mode: ' + mode); {
} log.error('No such mode: ' + mode);
}
} }
/** /**
@@ -43,7 +45,7 @@ function command(exe: string): string
{ {
if (os.platform() !== 'win32') if (os.platform() !== 'win32')
{ {
return exe; return exe;
} }
return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"'; return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"';

View File

@@ -1,5 +1,5 @@
{ {
"version": "1.5.1-beta", "target": "es6",
"compilerOptions": { "compilerOptions": {
"declaration": true, "declaration": true,
"noImplicitAny": true, "noImplicitAny": true,
@@ -7,40 +7,8 @@
"module": "commonjs", "module": "commonjs",
"outDir": "dist", "outDir": "dist",
"sourceMap": true, "sourceMap": true,
"target": "es5" "lib": [
}, "es2015"
"filesGlob": [ ]
"src/**/*.ts", }
"typings/**/*.ts"
],
"files": [
"src/batch.ts",
"src/cli.ts",
"src/episode.ts",
"src/index.ts",
"src/log.ts",
"src/interface/IConfig.d.ts",
"src/interface/IConfigLine.d.ts",
"src/interface/IConfigTask.d.ts",
"src/interface/IEpisodePage.d.ts",
"src/interface/IEpisodePlayer.d.ts",
"src/interface/IEpisodePlayerConfig.d.ts",
"src/interface/IFormatterTable.d.ts",
"src/interface/ISeries.d.ts",
"src/interface/ISeriesEpisode.d.ts",
"src/interface/ISubtitle.d.ts",
"src/interface/ISubtitleEvent.d.ts",
"src/interface/ISubtitleStyle.d.ts",
"src/request.ts",
"src/series.ts",
"src/subtitle/decode.ts",
"src/subtitle/formats/ass.ts",
"src/subtitle/formats/index.ts",
"src/subtitle/formats/srt.ts",
"src/subtitle/index.ts",
"src/video/index.ts",
"src/video/merge.ts",
"src/video/stream.ts",
"typings/index.d.ts"
]
} }

View File

@@ -9,23 +9,22 @@
"curly": false, "curly": false,
"eofline": false, "eofline": false,
"forin": true, "forin": true,
"indent": [true, 2], "indent": [true, "spaces", 2],
"interface-name": true, "interface-name": true,
"jsdoc-format": true, "jsdoc-format": true,
"label-position": true, "label-position": true,
"max-line-length": [true, 140], "max-line-length": [true, 140],
"member-ordering": [true, "member-ordering": false,
"public-before-private", "no-shadowed-variable": false,
"static-before-instance",
"variables-before-functions"
],
"array-type": [true, "array"], "array-type": [true, "array"],
"trailing-comma": false,
"no-any": false, "no-any": false,
"no-arg": true, "no-arg": true,
"no-bitwise": true, "no-bitwise": true,
"space-within-parens": false,
"no-object-literal-type-assertion": false,
"no-console": [true, "no-console": [true,
"debug", "debug",
"info",
"time", "time",
"timeEnd", "timeEnd",
"trace" "trace"
@@ -42,7 +41,6 @@
"no-use-before-declare": false, "no-use-before-declare": false,
"no-var-requires": true, "no-var-requires": true,
"one-line": [true, "one-line": [true,
"check-catch",
"check-whitespace" "check-whitespace"
], ],
"quotemark": [true, "single"], "quotemark": [true, "single"],
@@ -67,6 +65,8 @@
"check-operator", "check-operator",
"check-separator", "check-separator",
"check-type" "check-type"
] ],
"object-literal-sort-keys": false,
"ordered-imports": false
} }
} }

View File

@@ -1,13 +0,0 @@
{
"name": "crunchy",
"globalDependencies": {
"node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"commander": "github:DefinitelyTyped/DefinitelyTyped/commander/commander.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"xml2js": "github:DefinitelyTyped/DefinitelyTyped/xml2js/xml2js.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"cheerio": "github:DefinitelyTyped/DefinitelyTyped/cheerio/cheerio.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"mkdirp": "github:DefinitelyTyped/DefinitelyTyped/mkdirp/mkdirp.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"request": "github:DefinitelyTyped/DefinitelyTyped/request/request.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"big-integer": "github:DefinitelyTyped/DefinitelyTyped/big-integer/big-integer.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
"form-data": "github:DefinitelyTyped/DefinitelyTyped/form-data/form-data.d.ts#3882d337bb0808cde9fe4c08012508a48c135482"
}
}