evdh
Version:
evdh : EisF Video Download Helper, auto download videos on web pages.
1,201 lines (893 loc) • 24.2 kB
JavaScript
/* dl.js, dl: download part or evdh: EisF Video Download Helper, sceext <sceext@foxmail.com> 2009EisF2015, 2015.02
* version 0.1.3.0 test201502142024 (public version)
* author sceext <sceext@foxmail.com> 2015.02
* copyright 2015 sceext
*
* This is FREE SOFTWARE, released under GNU GPLv3+
* please see README.md and LICENSE for more information.
*
* evdh : EisF Video Download Helper, auto download videos with analyse service provided by flv.cn (api.flvxz.com)
* Copyright (C) 2015 sceext <sceext@foxmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* require import modules */
// node.js modules
var http = require('http');
var fs = require('fs');
// evdh modules
var _b = require('./b.js');
var _log = require('./log.js');
// global config object
var etc = {};
etc.memory_buffer_size = 2048; // unit KB
etc.user_agent = ''; // user agent to download
// import time_log for global use
var time_log = function(output){
_log.time_log(output);
};
/* objects */
/* object o_http_requester start ohr */
function o_http_requester() {
// methods
this.request = ohr_request;
// private methods
this._on_response = ohr_on_response;
this._on_error = ohr_on_error;
this._on_data = ohr_on_data;
this._on_end = ohr_on_end;
// attributes
this.callback = null; // finish callback(error);
this.status_code = 0; // http response status code
this.header = null; // http response header object
this.data = null; // http response data body, type is Buffer not string
// private attributes
this._data = []; // array of data piece Buffer s
this._response = null; // node.js http module, response module
}
function ohr_request(option) {
var b = this;
// use http module to send a http request
var request = http.request(option, function(response){
b._response = response;
b._on_response();
});
// end the request
request.end();
}
function ohr_on_response() {
var res = this._response;
// save data
this.status_code = res.statusCode;
this.header = res.headers;
var b = this;
// to receive data
res.on('data', function(data){
b._on_data(data);
});
res.on('end', function(){
b._on_end();
});
res.on('error', function(err){
b._on_error(err);
});
// reset this data
this._data = [];
}
function ohr_on_error(err) {
this.callback(err); // error callback
}
function ohr_on_data(data) {
// save data
this._data.push(data);
}
function ohr_on_end() {
// concat data
this.data = Buffer.concat(this._data);
// done, ok
this.callback(null); // no error
}
/* end o_http_requester object */
/* object o_http_file_dl start ohf
* download file with single thread and single file, support continue download after break
*/
function o_http_file_dl() {
// methods
this.start = ohf_start; // start download file
this.stop = ohf_stop; // pause download
this.continue_ = ohf_continue; // continue to download;
this.get_status = ohf_get_status; // get download status
// status object
// ed_byte : downloaded byte
// status : download status, [done, doing, error, none]
// error : error info
//
// extra attributes
// start_time : download start, or continue time, Date() object
// end_time : finshed or stop time, Date() object
// old_byte : continue, old_byte
// private methods
this._init = ohf_init;
this._fg_cb = ohf_fg_cb; // o_http_file_get callback
this._fr_cb = ohf_fr_cb; // o_file_writer callback
this._back_error = ohf_back_error; // error callback
this._continue_next = ohf_continue_next;
// attributes
this.url = ''; // URL of file to download
this.file = ''; // path of file to save from http download
this.callback = null; // event callback(error); finished, error
// private attributes
this._status = 'none';
this._ed_byte = 0;
this._error = null;
this._start_time = null;
this._end_time = null;
this._old_byte = 0;
// sub objects
this._hfg = null; // object http file get
this._ofr = null; // object file writer
// init this
this._init();
}
function ohf_init() {
// create file writer
this._ofr = new o_file_writer();
// create http file get
this._hfg = new o_http_file_get();
// set callback
var b = this;
this._ofr.callback = function(err){
b._fr_cb(err);
};
this._hfg.callback = function(err, end, data){
b._fg_cb(err, end, data);
};
// reset this status
this._status = 'none';
}
function ohf_get_status() {
var s = {}; // status object
s.ed_byte = this._ed_byte;
s.status = this._status;
s.error = this._error;
s.start_time = this._start_time;
s.end_time = this._end_time;
s.old_byte = this._old_byte;
return s;
}
function ohf_start() {
// start create write file
this._ofr.write_file(this.file);
// set buffer size
this._ofr.o_mb.set_buffer_size(etc.memory_buffer_size * 1024);
// start http request
this._hfg.request(this.url);
// set this status
this._status = 'doing';
this._start_time = new Date();
this._end_time = null;
// reset ed byte
this._ed_byte = 0;
}
function ohf_stop() {
// stop http file get
this._hfg.stop();
// set this flag
this._status = 'none';
this._start_time = null;
this._end_time = new Date();
}
function ohf_continue() { // NOTE this function has not been finished now
var _host = {};
var b = this;
var _next = function(step, host){
b._continue_next(step, host);
};
// set this status to doing just now
this._status = 'doing';
// set start time now
this._start_time = new Date();
_next(1, _host);
}
function ohf_continue_next(_step, _host) {
var b = this;
var _next = function(step, host){
b._continue_next(step, host);
};
switch (_step) {
case 1: // first step
// check file exist
fs.exists(this.file, function(exist){
_host.exist = exist;
_next(_step + 1, _host);
});
break;
case 2: // got file exist status
if (_host.exist) {
// check file size
_b.get_file_size(this.file, function(err, size){
if (err) {
this._error = err;
this._back_error();
} else {
_host.file_size = size;
_next(_step + 1, _host);
}
});
} else { // file not exist, should use start instead
this.start();
}
break;
case 3: // got file size
// start request
// append file
this._ofr.append_file(this.file);
// set buffer size
this._ofr.o_mb.set_buffer_size(etc.memory_buffer_size * 1024);
// set http request
this._hfg.done_byte = _host.file_size;
// start http request
this._hfg.request(this.url);
// set this status
this._status = 'doing';
this._start_time = new Date();
this._end_time = null;
this._old_byte = _host.file_size;
// set this ed byte
this._ed_byte = _host.file_size;
break;
case 0: // ok finish step
break;
case -1: // error step
break;
default: // step error
time_log('ERROR: dl.js ohf_continue_next: step error ! ');
}
}
function ohf_fg_cb(err, end, data) { // http file get callback
// check err
if (err) {
// process with 302 Found
if (err.status_code == 302) {
var location = err.location;
var old_url = this.url;
// re request
this.url = location;
this._hfg.request(this.url);
// reset this status
this._status = 'doing';
this._error = null;
// console log
time_log('INFO: 302 Found location [[' + location + ']] ');
console.log(''); // not a good idea, should make a event callback instead, later, reserved now
// done
return;
}
// other error
this._error = err;
this._back_error();
return;
}
// check end
if (end) {
// call file writer end
this._ofr.end();
}
// check data
if (data) {
// get data size
var new_data_size = data.length;
// push data
this._ofr.push(data);
// count data size
this._ed_byte += new_data_size;
}
}
function ohf_fr_cb(err) { // file writer callback
if (err) {
// error callback
this._error = err;
this._back_error();
} else {
// set finish flag
this._status = 'done';
this._end_time = new Date();
// finish callback
this.callback(null);
}
}
function ohf_back_error() {
if (!this._error) {
this._error = true;
}
// set this status
this._status = 'error';
this.callback(this._error);
}
/* end o_http_file_dl object */
/* object o_http_file_get start ohg */
function o_http_file_get() {
// methods
this.request = ohg_request;
this.stop = ohg_stop; // NOTE stop function does not work very well
// private methods
this._on_res = ohg_on_res; // http response
this._on_data = ohg_on_data; // http response read stream, on data
this._on_end = ohg_on_end;
this._on_error = ohg_on_error;
// attributes
this.callback = null; // event finish callback(error, end, data);
this.ok_code = 200; // http ok code, default 200
this.done_byte = 0; // used for Range, and 206 ok_code
// private attributes
this._res = null; // http response object
this._error = null; // error info object
}
function ohg_request(request_url) {
// get url info
var info = _b.get_url_info(request_url);
// make request option
var opt = {};
opt.hostname = info.hostname;
opt.port = info.port;
opt.path = info.path;
opt.method = 'GET';
// headers
opt.headers = {};
opt.headers['User-Agent'] = etc.user_agent;
// check done byte
if (this.done_byte > 0) {
// use range
var range = 'bytes=' + this.done_byte + '-';
// set header
opt.headers['Range'] = range;
// set ok_code
this.ok_code = 206;
} else {
this.ok_code = 200;
}
// start http request
var b = this;
var request = http.request(opt, function(response){
b._res = response;
b._on_res();
});
// end and send request
request.end();
// reset this error
this._error = null;
}
function ohg_on_res() {
var res = this._res;
// check status code
var code_found = 302; // process with 302
var status_code = res.statusCode;
if (status_code == code_found) {
// re request
var err = {};
err.type = '302 Found';
err.status_code = status_code;
err.location = res.headers['location'];
// on error
this._error = err;
this._on_error();
return;
}
if (status_code != this.ok_code) { // error
// make error object
var err = {};
err.status_code = status_code;
err.header = res.headers;
err.res = res;
err.type = 'http status code not ' + this.ok_code;
// on error
this._error = err;
this._on_error();
return;
}
// ready to receive data
// set res read stream event listerers
var b = this;
res.on('data', function(data){
b._on_data(data);
});
res.on('end', function(){
b._on_end();
});
res.on('close', function(){
b._on_end();
});
res.on('error', function(err){
b._error = err;
b._on_error();
});
}
function ohg_on_data(data) {
// callback data
this.callback(null, null, data);
}
function ohg_on_end() {
// callback end
if (this._error) {
this.callback(this._error, true, null);
} else {
this.callback(null, true, null);
}
}
function ohg_on_error() {
if (!this._error) {
this._error = true;
}
// error callback
this.callback(this._error, null, null);
}
function ohg_stop() {
// try to close read stream
try {
fs.close(this._res.fd, function(){});
} catch (e) {
}
// NOTE some bug there, reserved
// force end
this._on_end();
}
/* end o_http_file_get object */
/* object o_file_writer start ofw, write use o_mbuffer */
function o_file_writer() {
// methods
this.write_file = ofw_write_file;
this.append_file = ofw_append_file;
this.push = ofw_push; // push data
this.end = ofw_end;
// private methods
this._open_file = ofw_open_file;
this._get_data = ofw_get_data;
this._got_data = ofw_got_data;
this._on_error = ofw_on_error;
this._clean_up = ofw_clean_up;
// attributes
this.callback = null; // finish callback(error); on end
// sub objects
this.o_mb = null; // memory buffer, o_mbuffer
// private attributes
this._ws = null; // node.js write stream
this._error = null; // error info
}
function ofw_write_file(fpath) {
this._open_file(fpath, 'w'); // w normal flag
}
function ofw_append_file(fpath) {
this._open_file(fpath, 'a'); // a append flag
}
function ofw_open_file(fpath, open_flag) {
// create memory buffer
this.o_mb = new o_mbuffer();
// create write stream, use given flag
this._ws = fs.createWriteStream(fpath, {
flags: open_flag,
});
// set event listeners
var b = this;
var ws = this._ws;
ws.on('drain', function(){
b._get_data(); // to get data to write
});
ws.on('error', function(err){
this._error = err;
b._on_error();
});
// should first try to get data
this._get_data();
}
function ofw_on_error() {
// clean up
this._clean_up();
// error callback
if (!this._error) {
this._error = true;
}
this.callback(this._error);
}
function ofw_clean_up() {
// end memory buffer
try {
this.o_mb.end();
} catch (e) {
}
// close write steam
try {
fs.close(this._ws.fd);
this._ws = null;
} catch (e) {
}
}
function ofw_push(data) {
// put data in memory buffer
this.o_mb.push(data);
}
function ofw_end() {
// call end to memory buffer
this.o_mb.end();
}
function ofw_get_data() {
// try to get data from memory buffer
var b = this;
this.o_mb.get_data(function(err, end, data){
b._got_data(err, end, data);
});
}
function ofw_got_data(err, end, data) {
// memory buffer callback
// check error
if (err) {
this._error = err;
this._on_error();
return;
}
// check end
if (end) {
// finish callback
this.callback(null);
}
// write data
if (data) {
this._ws.write(data);
}
}
/* end o_file_writer object */
/* object o_mbuffer start omb
* memory buffer object, NOTE there is not a reset function, one buffer can be only used once
*/
function o_mbuffer() {
// methods
this.set_buffer_size = omb_set_buffer_size;
this.set_timeout_ms = omb_set_timeout_ms;
this.push = omb_push; // put new data into buffer
this.end = omb_end; // end put data, clear buffer
this.get_data = omb_get_data; // read data from buffer, async,
// only when buffer is full or end, it will callback
// private methods
this._flush = omb_flush; // flush buffer, get_data callback and clear buffer
this._check_flush = omb_check_flush; // check if need flush
this._check_flush2 = omb_check_flush2; // check callback function
this._reset_timer = omb_reset_timer; // reset flush timer
this._clear_timer = omb_clear_timer;
this._on_time = omb_on_time; // flush timer, on time
// attributes
// private attributes
this._flush_timeout_ms = 20000; // flush timeout 20s
this._buffer_size = 4096; // buffer size in byte
this._content_size = 0; // size of data in buffer
this._datas = []; // buffer Buffer data block list
this._callback = null; // callback function
this._id_timeout = null; // timeout id, used to cancel timeout
this._end = false; // end flag
this._timeout = false; // timeout flag
this._flag_no_more_timer = false;
// init this
// reset timer
this._reset_timer();
}
function omb_set_timeout_ms(time_ms) {
this._flush_timeout_ms = time_ms;
this._reset_timer();
}
function omb_set_buffer_size(buffer_size_byte) {
var bsb = buffer_size_byte;
// check new buffer size
if (bsb < 4096) { // min buffer size, 4KB
this._buffer_size = 4096;
} else {
this._buffer_size = bsb;
}
// check if need flush
this._check_flush();
return this._buffer_size;
}
function omb_push(new_data) {
// add new data to this data list
this._datas.push(new_data);
// add content size byte
this._content_size += new_data.length;
// check flush
this._check_flush();
}
function omb_end() {
// set end flag
this._end = true;
// set no more timer flag
this._flag_no_more_timer = true;
// clear timer, timer no longer needed
this._clear_timer();
// check flush
this._check_flush();
}
function omb_get_data(callback) { // event callback(error, end, data); error end or data
// set callback
this._callback = callback;
// check flush
this._check_flush();
}
function omb_flush() {
// check end flag
if (this._end) {
// check no data
if ((this._content_size < 1) || (this._datas.length < 1)) {
var callback = this._callback;
// callback, end but no data
callback(null, true, null);
// done
return;
}
}
// make data
var data = Buffer.concat(this._datas);
// clear this data
this._datas = [];
// reset content size
this._content_size = 0;
// reset timer
this._reset_timer();
var callback = this._callback;
// clear callback function
this._callback = null;
// flush callback
callback(null, null, data); // with data, no end
// reset timer again
this._reset_timer();
// done
}
function omb_check_flush() {
// check end flag
if (this._end) {
// should flush
this._check_flush2();
return;
}
// check timeout flag
if (this._timeout) {
// clear timeout flag
this._timeout = false;
// should flush
this._check_flush2();
return;
}
// check full
if (this._content_size >= this._buffer_size) {
// should flush
this._check_flush2();
return;
}
}
function omb_check_flush2() {
var do_flush = false;
// check end flag
if (this._end) {
// should flush, no matter if have data
do_flush = true;
} else {
// check if have data
if (this._content_size < 1) {
// no data, not flush
return;
}
if (this._datas.length < 1) {
// no data, not flush
return;
}
}
// not callback now, later
var b = this;
setTimeout(function(){
// check callback
if (typeof b._callback == 'function') { // should flush
b._flush();
}
}, 0);
}
function omb_clear_timer() {
// check if there is a timer
if (this._id_timeout !== null) { // cancel it
clearTimeout(this._id_timeout);
this._id_timeout = null;
}
}
function omb_reset_timer() {
// clear old timer
this._clear_timer();
if (this._flag_no_more_timer) {
return; // do not set timer any more
}
// set a new timer
var b = this;
this._id_timeout = setTimeout(function(){
b._on_time();
}, this._flush_timeout_ms);
}
function omb_on_time() {
// set timeout flag
this._timeout = true;
// reset timer
this._reset_timer();
// flush
this._check_flush();
}
/* end o_mbuffer object */
/* object o_host_file_dl start ohd */
function o_host_file_dl() {
// methods
this.add_item = ohd_add_item; // add download item
this.rm_item = ohd_rm_item; // remove download item
this.get_log = ohd_get_log; // export log object
this.set_log = ohd_set_log; // add log items, import log object
this.start_item = ohd_start_item; // start download item
this.continue_item = ohd_continue_item; // continue download item
this.stop_item = ohd_stop_item; // pause download item
this.get_status = ohd_get_status; // get download item status
this.get_id_list = ohd_get_id_list;
// private methods
this._on_event = ohd_on_event; // sub object event callback
// attributes
this.callback = null; // event callback(id, err); sub object event callback
// private attributes
this._log = null; // log info object
this._item_num = 0; // number of download items
this._index = {}; // index of id of download item
}
function ohd_add_item(id, url, file) { // add item, or reset exist item, marked by id
var item;
// check if it exist
if (this._index[id]) {
item = this._index[id];
} else { // create new object
var item2 = {};
// create download sub object
item2.od = new o_http_file_dl();
// add it to index
this._index[id] = item2;
this._item_num ++; // add one item
item = this._index[id];
}
// set item
item.url = url;
item.file = file;
// done
return false; // error false
}
function ohd_rm_item(id) { // NOTE only remove, no other action like auto stop will happen
var item;
// check if it exist
if (!this._index[id]) { // not exist
return true; // not found item
}
// remove it
this._index[id] = null;
this._item_num --; // sub item num
// done
return false;
}
function ohd_get_log() { // export part log object, array of items
var olog = []; // log object, array of item
for (var i in this._index) {
var item2 = this._index[i];
if (item2 === null) {
continue;
}
var item = {};
item.id = i;
item.url = item2.url;
item.file = item2.file;
olog.push(item);
}
// done
return olog;
}
function ohd_set_log(log_obj) { // add log, download items, log_obj is a array of items
var olog = log_obj;
// add each item
for (var i = 0; i < olog.length; i++) {
var item = olog[i];
// add it
this.add_item(item.id, item.url, item.file);
}
// done
return false;
}
function ohd_start_item(id) {
// check item exist
if (!this._index[id]) {
return true;
}
// get item
var item = this._index[id];
var item_id = id;
// set sub object
item.od.url = item.url;
item.od.file = item.file;
var b = this;
item.od.callback = function(err){
b._on_event(item_id, err);
};
// start it
item.od.start();
// done
return false;
}
function ohd_continue_item(id) {
// check item exist
if (!this._index[id]) {
return true;
}
// get item
var item = this._index[id];
var item_id = id;
// set sub object
item.od.url = item.url;
item.od.file = item.file;
var b = this;
item.od.callback = function(err){
b._on_event(item_id, err);
};
// continue it
item.od.continue_();
// done
return false;
}
function ohd_stop_item(id) {
// check item exist
if (!this._index[id]) {
return true;
}
// stop it
var item = this._index[id];
item.od.stop();
// done
return false;
}
function ohd_on_event(id, err) {
// just callback
this.callback(id, err);
}
function ohd_get_status(id) {
// check item exist
if (!this._index[id]) {
return null;
}
// get its status
var item = this._index[id];
var status = item.od.get_status();
// done
return status;
}
function ohd_get_id_list() {
var list = [];
for (var i in this._index) {
if (this._index[i] != null) {
list.push(i);
}
}
// done
return list;
}
/* end o_host_file_dl object */
/* exports */
exports.etc = etc;
// objects
exports.o_http_requester = o_http_requester; // ohr
exports.o_http_file_dl = o_http_file_dl; // ohf
exports.o_http_file_get = o_http_file_get; // ohg
exports.o_file_writer = o_file_writer; // ofw
exports.o_mbuffer = o_mbuffer; // omb
exports.o_host_file_dl = o_host_file_dl; // ohd
/* end dl.js */