10 Commits

Author SHA1 Message Date
Godzil
d1457bb893 1.1.13 2017-01-28 13:38:20 +00:00
Godzil
8dfd1b447c Add a log objet to do some fancy output on the command line (not fully enabled under windows as it need some tests) 2017-01-28 13:38:14 +00:00
Godzil
ce63ae9a16 Upgrade to 1.1.12 to fix login issue 2017-01-23 21:13:13 +00:00
Godzil
70d80ccd17 Update dependency to more recent version, and correct a few warnings reported by ts 2017-01-23 21:06:34 +00:00
Manoël Trapier
7833fbe292 Merge pull request #13 from majewskim/master
Fix a login issue
2017-01-23 20:46:10 +00:00
Mateusz Majewski
fa6aa74442 Merge branch 'master' into master 2017-01-18 12:49:49 +02:00
Mateusz Majewski
fe2ed9fb76 Fixing login issue by bypassing the login form and making a request directly. 2017-01-18 11:19:41 +02:00
Mateusz Majewski
cc655b9e00 Fixing login issue by bypassing the login form and making a request directly. 2017-01-18 11:08:45 +02:00
Manoël Trapier
e1d2a55a01 Update README.md 2016-10-21 17:21:36 +01:00
Manoël Trapier
a31de0ef9d Remove .js from the name 2016-10-21 17:21:05 +01:00
9 changed files with 130 additions and 76 deletions

View File

@@ -1,6 +1,6 @@
# Crunchy.js: a fork of Deathspike/CrunchyRoll.js
# Crunchy: a fork of Deathspike/CrunchyRoll.js
*Crunchy.js* 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.
## Motivation
@@ -10,7 +10,7 @@
This application is not endorsed or affliated with *CrunchyRoll*. The usage of this application enables episodes to be downloaded for offline convenience which may be forbidden by law in your country. Usage of this application may also cause a violation of the agreed *Terms of Service* between you and the stream provider. A tool is not responsible for your actions; please make an informed decision prior to using this application.
**PLEASE USE THIS TOOL ONLY IF YOU HAVE A PREMIUM ACCOUNT**
**PLEASE _ONLY_ USE THIS TOOL IF YOU HAVE A _PREMIUM ACCOUNT_**
## Configuration

View File

@@ -1,6 +1,6 @@
{
"author": "Godzil",
"description": "Crunchy.js is a fork of Crunchyroll.js, capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
"description": "Crunchy is a fork of Crunchyroll.js, capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
"license": "MIT",
"keywords": [
"anime",
@@ -12,16 +12,16 @@
"type": "git",
"url": "git://github.com/Godzil/crunchyroll.js.git"
},
"version": "1.1.11",
"version": "1.1.13",
"bin": {
"crunchy": "./bin/crunchy"
},
"dependencies": {
"big-integer": "1.4.4",
"cheerio": "0.18.0",
"cheerio": "0.22.0",
"commander": "2.6.0",
"mkdirp": "0.5.0",
"request": "2.53.0",
"request": "2.74.0",
"xml2js": "0.4.5"
},
"devDependencies": {

View File

@@ -7,6 +7,7 @@ import path = require('path');
import subtitle from './subtitle/index';
import video from './video/index';
import xml2js = require('xml2js');
import log = require('./log');
/**
* Streams the episode to disk.
@@ -24,12 +25,12 @@ export default function(config: IConfig, address: string, done: (err: Error, ign
/**
* Completes a download and writes the message with an elapsed time.
*/
function complete(message: string, begin: number, done: (err: Error, ign: boolean) => void) {
function complete(epName: string, message: string, begin: number, done: (err: Error, ign: boolean) => void) {
var timeInMs = Date.now() - begin;
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
log.dispEpisode(epName, message + ' (' + hours + ':' + minutes + ':' + seconds + ')', true);
done(null, false);
}
@@ -57,14 +58,14 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
if (fileExist(filePath + ".mkv"))
{
var count = 0;
console.info("File '"+fileName+"' already exist...");
log.warn("File '"+fileName+"' already exist...");
do
{
count = count + 1;
fileName = name(config, page, series, "-" + count).replace("/","_").replace("'","_").replace(":","_");
filePath = path.join(config.output || process.cwd(), series, fileName);
} while(fileExist(filePath + ".mkv"))
console.info("Renaming to '"+fileName+"'...");
log.warn("Renaming to '"+fileName+"'...");
}
mkdirp(path.dirname(filePath), (err: Error) => {
@@ -74,20 +75,20 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
var now = Date.now();
if (player.video.file != undefined)
{
console.log('Fetching ' + fileName);
log.dispEpisode(fileName, 'Fetching...', false);
downloadVideo(config, page, player, filePath, err => {
if (err) return done(err, false);
if (config.merge) return complete('Finished ' + fileName, now, done);
if (config.merge) return complete(fileName, 'Finished!', now, done);
var isSubtited = Boolean(player.subtitle);
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, err => {
if (err) return done(err, false);
complete('Finished ' + fileName, now, done);
complete(fileName, 'Finished!', now, done);
});
});
}
else
{
console.log('Ignoring ' + fileName + ': not released yet');
log.dispEpisode(fileName, 'Ignoring: not released yet', true);
done(null, true);
}
});
@@ -166,8 +167,8 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
if (!swf || !data)
{
console.info('Something wrong in the page at '+address+' (data are: '+look+')');
console.info('Setting Season to 0 and episode to \0\...');
log.warn('Something wrong in the page at '+address+' (data are: '+look+')');
log.warn('Setting Season to 0 and episode to \0\...');
done(null, {
id: id,
episode: "0",

71
src/log.ts Normal file
View File

@@ -0,0 +1,71 @@
'use strict';
import os = require('os');
export function error(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * ERROR: ' + str);
}
else
{
/* Do more fancy output */
console.error(' \x1B[1;31m* ERROR\x1B[0m: ' + str);
}
}
export function info(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * INFO : ' + str);
}
else
{
/* Do more fancy output */
console.log(' \x1B[1;32m* INFO \x1B[0m: ' + str);
}
}
export function debug(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * DEBUG: ' + str);
}
else
{
/* Do more fancy output */
console.log(' \x1B[1;35m* DEBUG\x1B[0m: ' + str);
}
}
export function warn(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * WARN : ' + str);
}
else
{
/* Do more fancy output */
console.log(' \x1B[1;33m* WARN \x1B[0m: ' + str);
}
}
export function dispEpisode(name: string, status: string, addNL: boolean)
{
if (os.platform() === 'win32')
{
process.stdout.write(' > ' + name + ' : ' + status + '\x1B[0G');
}
else
{
/* Do more fancy output */
process.stdout.write(' \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G');
}
if (addNL)
{
console.log('');
}
}

View File

@@ -1,14 +1,15 @@
'use strict';
import request = require('request');
import cheerio = require('cheerio');
import log = require('./log');
var isAuthenticated = false;
var isPremium = false;
var defaultHeaders:request.Headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
'Connection': 'keep-alive'
};
var 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.
@@ -41,71 +42,50 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
*/
function authenticate(config: IConfig, done: (err: Error) => void) {
if (isAuthenticated || !config.pass || !config.user) return done(null);
/* First just request the login page */
var options = {
/* Bypass the login page and send a login request directly */
var options =
{
headers: defaultHeaders,
jar: true,
url: 'https://www.crunchyroll.com/login'
}
request(options, (err: Error, rep: any, body: any) =>
gzip: false,
url: 'https://www.crunchyroll.com/?a=formhandler&formname=RpcApiUser_Login&name=' + config.user + '&password=' + config.pass
};
request(options, (err: Error, rep: string, body: string) =>
{
if (err) return done(err);
var $ = cheerio.load(body);
/* Get the token from the login page */
var token = $('input[name="login_form[_token]"]').attr("value");
if (token == "") return done(new Error("Can't find token!"));
/* 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.
*/
var 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,
url: 'https://www.crunchyroll.com/login'
url: 'http://www.crunchyroll.com/'
};
request.post(options, (err: Error, rep: string, body: string) =>
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.
*/
var options =
var $ = cheerio.load(body);
/* Check if auth worked */
var regexps = /ga\(\'set\', \'dimension[5-8]\', \'([^']*)\'\);/g;
var dims = regexps.exec($('script').text());
for (var i = 1; i < 5; i++)
{
headers: defaultHeaders,
jar: true,
url: 'http://www.crunchyroll.com/'
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered')) { isAuthenticated = true; }
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus')) { isPremium = true; }
}
request(options, (err: Error, rep: string, body: string) =>
if (isAuthenticated === false)
{
if (err) return done(err);
var $ = cheerio.load(body);
/* Check if auth worked */
var regexps = /ga\(\'set\', \'dimension[5-8]\', \'([^']*)\'\);/g
var dims = regexps.exec($('script').text())
for(var 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)
{
var error = $('ul.message, li.error').text();
return done(new Error("Authentication failed: " + error));
}
if (isPremium == false) { console.info("Do not use this app without a premium account."); }
else { console.info("You have a premium account! Good!"); }
done(null);
})
})
})
var 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);
});
});
}
/**

View File

@@ -5,6 +5,7 @@ import fs = require('fs');
import request = require('./request');
import path = require('path');
import url = require('url');
import log = require('./log');
var persistent = '.crpersistent';
/**
@@ -84,6 +85,7 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
var $ = cheerio.load(result);
var title = $('span[itemprop=name]').text();
if (!title) return done(new Error('Invalid page.(' + address + ')'));
log.info("Checking availability for " + title);
var episodes: ISeriesEpisode[] = [];
$('.episode').each((i, el) => {
if ($(el).children('img[src*=coming_soon]').length) return;

View File

@@ -2,6 +2,7 @@
import childProcess = require('child_process');
import path = require('path');
import os = require('os');
import log = require('../log');
/**
* Streams the video to disk.
@@ -19,7 +20,7 @@ import os = require('os');
}
else if (mode == "HLS")
{
console.info("Experimental FFMPEG, MAY FAIL!!!");
log.debug("Experimental FFMPEG, MAY FAIL!!!");
var cmd=command("ffmpeg") + ' ' +
'-i "' + rtmpInputPath + '" ' +
'-c copy -bsf:a aac_adtstoasc ' +
@@ -30,7 +31,7 @@ import os = require('os');
}
else
{
console.error("No such mode: " + mode);
log.error("No such mode: " + mode);
}
}

View File

@@ -18,6 +18,7 @@
"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",

View File

@@ -47,8 +47,6 @@
"no-var-requires": true,
"one-line": [true,
"check-catch",
"check-else",
"check-open-brace",
"check-whitespace"
],
"quotemark": [true, "single"],