Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2853334d7f | ||
|
|
69dd28d31b | ||
|
|
56afce02ea | ||
|
|
bc4697061e | ||
|
|
55ffe85f77 | ||
|
|
ec8c2c7716 | ||
|
|
714a528f8b | ||
|
|
8314d91bd7 | ||
|
|
5bd31f9e0b | ||
|
|
95a93930f3 | ||
|
|
4a9e1d0410 | ||
|
|
1eacd0a5ca |
1
LICENSE
1
LICENSE
@@ -1,4 +1,5 @@
|
|||||||
Copyright (c) 2015 Roel van Uden
|
Copyright (c) 2015 Roel van Uden
|
||||||
|
Copyright (c) 2016 Manoel <Godzil> Trapier
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"author": "Godzil",
|
"author": "Godzil",
|
||||||
"description": "Crunchy.js is a fork of Crunchyroll.js, capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
|
"description": "Crunchy.js is a fork of Crunchyroll.js, capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
|
||||||
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"anime",
|
"anime",
|
||||||
"download",
|
"download",
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/Godzil/crunchyroll.js.git"
|
"url": "git://github.com/Godzil/crunchyroll.js.git"
|
||||||
},
|
},
|
||||||
"version": "1.1.8",
|
"version": "1.1.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"crunchy": "./bin/crunchy"
|
"crunchy": "./bin/crunchy"
|
||||||
},
|
},
|
||||||
|
|||||||
105
src/episode.ts
105
src/episode.ts
@@ -11,11 +11,11 @@ import xml2js = require('xml2js');
|
|||||||
/**
|
/**
|
||||||
* Streams the episode to disk.
|
* Streams the episode to disk.
|
||||||
*/
|
*/
|
||||||
export default function(config: IConfig, address: string, done: (err: Error) => void) {
|
export default function(config: IConfig, address: string, done: (err: Error, ign: boolean) => void) {
|
||||||
scrapePage(config, address, (err, page) => {
|
scrapePage(config, address, (err, page) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err, false);
|
||||||
scrapePlayer(config, address, page.id, (err, player) => {
|
scrapePlayer(config, address, page.id, (err, player) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err, false);
|
||||||
download(config, page, player, done);
|
download(config, page, player, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -24,37 +24,72 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
|||||||
/**
|
/**
|
||||||
* Completes a download and writes the message with an elapsed time.
|
* Completes a download and writes the message with an elapsed time.
|
||||||
*/
|
*/
|
||||||
function complete(message: string, begin: number, done: (err: Error) => void) {
|
function complete(message: string, begin: number, done: (err: Error, ign: boolean) => void) {
|
||||||
var timeInMs = Date.now() - begin;
|
var timeInMs = Date.now() - begin;
|
||||||
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
|
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
|
||||||
var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
|
var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
|
||||||
var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
|
var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
|
||||||
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
|
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
|
||||||
done(null);
|
done(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exist..
|
||||||
|
*/
|
||||||
|
function fileExist(path: string) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.statSync(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e) { }
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the subtitle and video.
|
* Downloads the subtitle and video.
|
||||||
*/
|
*/
|
||||||
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error) => void) {
|
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error, ign: boolean) => void) {
|
||||||
var series = config.series || page.series;
|
var series = config.series || page.series;
|
||||||
var fileName = name(config, page, series);
|
series = series.replace("/","_").replace("'","_").replace(":","_");
|
||||||
|
var fileName = name(config, page, series, "").replace("/","_").replace("'","_").replace(":","_");
|
||||||
var filePath = path.join(config.output || process.cwd(), series, fileName);
|
var filePath = path.join(config.output || process.cwd(), series, fileName);
|
||||||
|
if (fileExist(filePath + ".mkv"))
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
console.info("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+"'...");
|
||||||
|
}
|
||||||
|
|
||||||
mkdirp(path.dirname(filePath), (err: Error) => {
|
mkdirp(path.dirname(filePath), (err: Error) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err, false);
|
||||||
downloadSubtitle(config, player, filePath, err => {
|
downloadSubtitle(config, player, filePath, err => {
|
||||||
if (err) return done(err);
|
if (err) return done(err, false);
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
console.log('Fetching ' + fileName);
|
if (player.video.file != undefined)
|
||||||
downloadVideo(config, page, player, filePath, err => {
|
{
|
||||||
if (err) return done(err);
|
console.log('Fetching ' + fileName);
|
||||||
if (config.merge) return complete('Finished ' + fileName, now, done);
|
downloadVideo(config, page, player, filePath, err => {
|
||||||
var isSubtited = Boolean(player.subtitle);
|
if (err) return done(err, false);
|
||||||
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, err => {
|
if (config.merge) return complete('Finished ' + fileName, now, done);
|
||||||
if (err) return done(err);
|
var isSubtited = Boolean(player.subtitle);
|
||||||
complete('Finished ' + fileName, now, done);
|
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, err => {
|
||||||
|
if (err) return done(err, false);
|
||||||
|
complete('Finished ' + fileName, now, done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log('Ignoring ' + fileName + ': not released yet');
|
||||||
|
done(null, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -96,11 +131,13 @@ function downloadVideo(config: IConfig,
|
|||||||
/**
|
/**
|
||||||
* Names the file based on the config, page, series and tag.
|
* Names the file based on the config, page, series and tag.
|
||||||
*/
|
*/
|
||||||
function name(config: IConfig, page: IEpisodePage, series: string) {
|
function name(config: IConfig, page: IEpisodePage, series: string, extra: string) {
|
||||||
var episode = (page.episode < 10 ? '0' : '') + page.episode;
|
var episodeNum = parseInt(page.episode, 10);
|
||||||
var volume = (page.volume < 10 ? '0' : '') + page.volume;
|
var volumeNum = parseInt(page.volume, 10);
|
||||||
|
var episode = (episodeNum < 10 ? '0' : '') + page.episode;
|
||||||
|
var volume = (volumeNum < 10 ? '0' : '') + page.volume;
|
||||||
var tag = config.tag || 'CrunchyRoll';
|
var tag = config.tag || 'CrunchyRoll';
|
||||||
return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
|
return series + ' - s' + volume + 'e' + episode +' - [' + tag + ']' + extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,15 +159,29 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
|||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var $ = cheerio.load(result);
|
var $ = cheerio.load(result);
|
||||||
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
|
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
|
||||||
var regexp = /-\s+(?:Watch\s+)?(.+?)(?:\s+Season\s+([0-9]+))?(?:\s+-)?\s+Episode\s+([0-9]+)/;
|
var 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]?)/;
|
||||||
var data = regexp.exec($('title').text());
|
var look = $('#showmedia_about_media').text();
|
||||||
if (!swf || !data) return done(new Error('Invalid page.'));
|
var seasonTitle = $('span[itemprop="title"]').text();
|
||||||
|
var data = regexp.exec(look);
|
||||||
|
|
||||||
|
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\’...');
|
||||||
|
done(null, {
|
||||||
|
id: id,
|
||||||
|
episode: "0",
|
||||||
|
series: seasonTitle,
|
||||||
|
swf: swf[1],
|
||||||
|
volume: "0"
|
||||||
|
});
|
||||||
|
}
|
||||||
done(null, {
|
done(null, {
|
||||||
id: id,
|
id: id,
|
||||||
episode: parseInt(data[3], 10),
|
episode: data[3],
|
||||||
series: data[1],
|
series: data[1],
|
||||||
swf: swf[1],
|
swf: swf[1],
|
||||||
volume: parseInt(data[2], 10) || 1
|
volume: data[2] || "1"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/interface/IEpisodePage.d.ts
vendored
4
src/interface/IEpisodePage.d.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
interface IEpisodePage {
|
interface IEpisodePage {
|
||||||
id: number;
|
id: number;
|
||||||
episode: number;
|
episode: string;
|
||||||
series: string;
|
series: string;
|
||||||
volume: number;
|
volume: string;
|
||||||
swf: string;
|
swf: string;
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/interface/ISeriesEpisode.d.ts
vendored
2
src/interface/ISeriesEpisode.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
interface ISeriesEpisode {
|
interface ISeriesEpisode {
|
||||||
address: string;
|
address: string;
|
||||||
episode: number;
|
episode: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import request = require('request');
|
import request = require('request');
|
||||||
|
import cheerio = require('cheerio');
|
||||||
|
|
||||||
var isAuthenticated = false;
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a GET request for the resource.
|
* Performs a GET request for the resource.
|
||||||
@@ -33,21 +41,71 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
|
|||||||
*/
|
*/
|
||||||
function authenticate(config: IConfig, done: (err: Error) => void) {
|
function authenticate(config: IConfig, done: (err: Error) => void) {
|
||||||
if (isAuthenticated || !config.pass || !config.user) return done(null);
|
if (isAuthenticated || !config.pass || !config.user) return done(null);
|
||||||
|
/* First just request the login page */
|
||||||
var options = {
|
var options = {
|
||||||
form: {
|
headers: defaultHeaders,
|
||||||
formname: 'RpcApiUser_Login',
|
|
||||||
fail_url: 'https://www.crunchyroll.com/login',
|
|
||||||
name: config.user,
|
|
||||||
password: config.pass
|
|
||||||
},
|
|
||||||
jar: true,
|
jar: true,
|
||||||
url: 'https://www.crunchyroll.com/?a=formhandler'
|
url: 'https://www.crunchyroll.com/login'
|
||||||
};
|
}
|
||||||
request.post(options, (err: Error) => {
|
|
||||||
|
request(options, (err: Error, rep: any, body: any) =>
|
||||||
|
{
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
isAuthenticated = true;
|
var $ = cheerio.load(body);
|
||||||
done(null);
|
|
||||||
});
|
/* 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!"));
|
||||||
|
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
request.post(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 =
|
||||||
|
{
|
||||||
|
headers: defaultHeaders,
|
||||||
|
jar: true,
|
||||||
|
url: 'http://www.crunchyroll.com/'
|
||||||
|
}
|
||||||
|
request(options, (err: Error, rep: string, body: string) =>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,7 +114,8 @@ function authenticate(config: IConfig, done: (err: Error) => void) {
|
|||||||
function modify(options: string|request.Options): request.Options {
|
function modify(options: string|request.Options): request.Options {
|
||||||
if (typeof options !== 'string') {
|
if (typeof options !== 'string') {
|
||||||
options.jar = true;
|
options.jar = true;
|
||||||
|
options.headers = defaultHeaders;
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
return {jar: true, url: options.toString()};
|
return {jar: true, headers: defaultHeaders, url: options.toString()};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,22 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
|||||||
var i = 0;
|
var i = 0;
|
||||||
(function next() {
|
(function next() {
|
||||||
if (i >= page.episodes.length) return done(null);
|
if (i >= page.episodes.length) return done(null);
|
||||||
download(cache, config, address, page.episodes[i], err => {
|
download(cache, config, address, page.episodes[i], (err, ignored) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var newCache = JSON.stringify(cache, null, ' ');
|
if ((ignored == false) || (ignored == undefined))
|
||||||
fs.writeFile(persistentPath, newCache, err => {
|
{
|
||||||
if (err) return done(err);
|
var newCache = JSON.stringify(cache, null, ' ');
|
||||||
|
fs.writeFile(persistentPath, newCache, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
i += 1;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
i += 1;
|
i += 1;
|
||||||
next();
|
next();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
@@ -40,14 +48,14 @@ function download(cache: {[address: string]: number},
|
|||||||
config: IConfig,
|
config: IConfig,
|
||||||
baseAddress: string,
|
baseAddress: string,
|
||||||
item: ISeriesEpisode,
|
item: ISeriesEpisode,
|
||||||
done: (err: Error) => void) {
|
done: (err: Error, ign: boolean) => void) {
|
||||||
if (!filter(config, item)) return done(null);
|
if (!filter(config, item)) return done(null, false);
|
||||||
var address = url.resolve(baseAddress, item.address);
|
var address = url.resolve(baseAddress, item.address);
|
||||||
if (cache[address]) return done(null);
|
if (cache[address]) return done(null, false);
|
||||||
episode(config, address, err => {
|
episode(config, address, (err, ignored) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err, false);
|
||||||
cache[address] = Date.now();
|
cache[address] = Date.now();
|
||||||
done(null);
|
done(null, ignored);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +65,8 @@ function download(cache: {[address: string]: number},
|
|||||||
function filter(config: IConfig, item: ISeriesEpisode) {
|
function filter(config: IConfig, item: ISeriesEpisode) {
|
||||||
// Filter on chapter.
|
// Filter on chapter.
|
||||||
var episodeFilter = config.episode;
|
var episodeFilter = config.episode;
|
||||||
if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
|
if (episodeFilter > 0 && parseInt(item.episode, 10) <= episodeFilter) return false;
|
||||||
if (episodeFilter < 0 && item.episode >= -episodeFilter) return false;
|
if (episodeFilter < 0 && parseInt(item.episode, 10) >= -episodeFilter) return false;
|
||||||
|
|
||||||
// Filter on volume.
|
// Filter on volume.
|
||||||
var volumeFilter = config.volume;
|
var volumeFilter = config.volume;
|
||||||
@@ -75,18 +83,18 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
|
|||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var $ = cheerio.load(result);
|
var $ = cheerio.load(result);
|
||||||
var title = $('span[itemprop=name]').text();
|
var title = $('span[itemprop=name]').text();
|
||||||
if (!title) return done(new Error('Invalid page.'));
|
if (!title) return done(new Error('Invalid page.(' + address + ')'));
|
||||||
var episodes: ISeriesEpisode[] = [];
|
var episodes: ISeriesEpisode[] = [];
|
||||||
$('.episode').each((i, el) => {
|
$('.episode').each((i, el) => {
|
||||||
if ($(el).children('img[src*=coming_soon]').length) return;
|
if ($(el).children('img[src*=coming_soon]').length) return;
|
||||||
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
||||||
var regexp = /Episode\s+([0-9]+)\s*$/i;
|
var regexp = /Episode\s+((PV )?[S0-9][P0-9.]*[a-fA-F]?)\s*$/i;
|
||||||
var episode = regexp.exec($(el).children('.series-title').text());
|
var episode = regexp.exec($(el).children('.series-title').text());
|
||||||
var address = $(el).attr('href');
|
var address = $(el).attr('href');
|
||||||
if (!address || !episode) return;
|
if (!address || !episode) return;
|
||||||
episodes.push({
|
episodes.push({
|
||||||
address: address,
|
address: address,
|
||||||
episode: parseInt(episode[0], 10),
|
episode: episode[1],
|
||||||
volume: volume ? parseInt(volume[0], 10) : 1
|
volume: volume ? parseInt(volume[0], 10) : 1
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user