vidinfo
Version:
Get video information from different APIs.
474 lines (451 loc) • 19.6 kB
JavaScript
/*
VidInfo - Louis T. <LouisT@ltdev.im>
https://github.com/LouisT/VidInfo
*/
(function(){
var http_get = require('./libs/http_get.js'),
fs = require('fs'),
package = require('./package.json');
/*
Fix fs.existsSync for node v0.6.x
*/
if (!('existsSync' in fs)) {
fs.existsSync = require('path').existsSync;
};
var VidInfo = function (settings) {
if (!(this instanceof VidInfo)) {
return new VidInfo(settings);
};
// Look at the README for available settings.
this.settings = settings||{format:true};
// Set the default location of enabled APIs.
if (!('enabled' in this.settings)) {
this.settings['enabled'] = __dirname+'/apis/enabled/';
};
// Set the default location of disabled APIs.
if (!('disabled' in this.settings)) {
this.settings['disabled'] = __dirname+'/apis/disabled/';
};
// User-Agent sent on API requests.
this.userAgent = 'Mozilla/5.0+(compatible; '+package.name+'/'+package.version+'; '+package.homepage+')';
// Import supported APIs. (./apis/enabled/)
this.importAPIs();
};
// Pull information from the url.
VidInfo.prototype.byURL = function (url,cb,opts) {
var apidat,
opts = opts||{};
if (!cb) {
return false; // Needs a callback!
};
if (!('error' in (apidat = this.detect(url,null,opts)))) {
this.doRequest(apidat['url'],apidat,cb,opts);
} else {
cb(apidat,true);
};
};
// Depricate <obj>.byurl for <obj>.byURL -- better standardization.
VidInfo.prototype.byurl = function (url,cb,opts) {
console.warn('NOTICE: byurl is depricated, please use byURL.');
this.byURL(url,cb,opts);
};
// Get information for an ID for the specified API.
VidInfo.prototype.byID = function (id,api,cb,opts) {
if (!cb) {
return false; // Needs a callback!
};
var opts = opts||{},
api = this.byShortcut(api);
if ((api in this.apis)) {
var apidat = this.copyObj(this.apis[api]),
fields = ((apidat['fields']||[]).join((apidat['fieldsJoiner']||','))),
foropts = {id:id,fields:fields};
// If "matches" is true, the "id" must be an array.
if (apidat["matches"]) {
if (this.getType(id) !== "Array") {
return cb({error:true,message:'First argument must be an array.'},true);
} else {
foropts = id;
};
};
if (('apikey' in opts || 'needkey' in apidat)) {
if (!('apikey' in opts)) {
return cb({error:true,message:'API key is required!'},true);
} else {
// If `foropts` is array, always push the API key to the end.
if (this.getType(foropts) == "Array") {
foropts.push(opts['apikey']);
} else {
foropts['apikey'] = opts['apikey'];
};
};
};
if (('basicauth' in opts || 'basicauth' in apidat)) {
if (!('basicauth' in opts)) {
return cb({error:true,message:'Basic auth `username:password` is required!'},true);
} else {
foropts['basicauth'] = opts['basicauth'];
};
};
this.doRequest(this.stringFormat(apidat["url"],foropts),apidat,cb,opts);
} else {
cb({error:true,message:'No such API!'},true);
};
};
// Depricate <obj>.byid for <obj>.byID -- better standardization.
VidInfo.prototype.byid = function (id,cb,opts) {
console.warn('NOTICE: byid is depricated, please use byID.');
this.byID(id,cb,opts);
};
// Detect what API to use by video url.
VidInfo.prototype.detect = function (url,cb,opts) {
var opts = opts||{},
matches;
apiloop:
for (var api in this.apis) {
var apidat = this.copyObj(this.apis[api]),
fields = ((apidat['fields']||[]).join((apidat['fieldsJoiner']||',')));
if (('keys' in opts && api in opts['keys'])) {
opts['apikey'] = opts['keys'][api];
};
if (!(apidat['regex'] instanceof Array)) {
apidat['regex'] = [apidat['regex']];
};
for (var num in apidat['regex']) {
var regex = apidat['regex'][num];
if ((matches = regex.exec(url)) !== null) {
// Return full URL or a parsed ID.
// This is rather hackish. Find a better way to do this.
var id = (apidat["matches"]?matches:(apidat['fullurl']?matches[0]:matches[1])),
// If "matches" is true, build the URL using the `RegEx.exec` result.
foropts = (apidat["matches"]?matches:{id:id,fields:fields});
// Remove the first element.
if (this.getType(foropts) == "Array") {
foropts.shift();
};
if (('apikey' in opts || 'needkey' in apidat)) {
if (!('apikey' in opts)) {
apidat['error'] = true;
apidat['message'] = 'API key is required!';
} else {
// If `foropts` is array, always push the API key to the end.
if (this.getType(foropts) == "Array") {
foropts.push(opts['apikey']);
} else {
foropts['apikey'] = opts['apikey'];
};
};
};
if (('basicauth' in opts || 'basicauth' in apidat)) {
if (!('basicauth' in opts)) {
apidat['error'] = true;
apidat['message'] = 'Basic auth `username:password` is required!';
} else {
foropts['basicauth'] = opts['basicauth'];
};
};
apidat['url'] = this.stringFormat(apidat["url"],foropts);
apidat['api'] = api;
apidat['id'] = id;
break apiloop;
};
};
};
// Return an error if not successful.
if (!('id' in apidat) && !('error' in apidat)) {
apidat = {error:true,message:'Could not detect API for supplied URL.'};
};
if (!cb) {
return apidat;
} else {
cb(apidat,(('error' in apidat)?true:false));
};
};
// Get ALL IDs within a string.
VidInfo.prototype.detectAll = function (str,cb,opts) {
var str = str.trim().split(/\s+/),
strlen = str.length,
ret = {},
opts = opts||{};
// Attempt to get the API key by API shortcut.
if (('keys' in opts)) {
for (var apin in opts['keys']) {
opts['keys'][this.byShortcut(apin)] = opts['keys'][apin];
};
};
// Scan the split string for URLs.
for (var i = 0; i < strlen; i++) {
var detected = this.detect(str[i],false,opts);
if (('api' in detected && 'id' in detected)) {
// Check if `api` has already been added to `ret`.
if (!(detected['api'] in ret)) {
// Store video IDs in `ids` to prevent detecting the same links.
ret[detected['api']] = {ids:[],matches:[]};
};
// Check to see if the video link was already added to the object.
if (opts['nocheck'] || ret[detected['api']]['ids'].indexOf(detected['id']) == -1) {
ret[detected['api']]['ids'].push(detected['id']);
ret[detected['api']]['matches'].push(detected);
};
};
};
// Set `empty` in ret if there are no detections.
if ((!Object.keys(ret).length)) {
ret['empty'] = true;
};
if (!cb) {
return ret;
} else {
// Set `error` to true if `empty` is in ret.
cb(ret,('empty' in ret));
}
};
// Make http requests! -- Moved from 'byURL' and 'byID'
VidInfo.prototype.doRequest = function (url,apidat,cb,opts) {
// Default to JSON for requests. Custom user agent, accept everything!
var getopts = {type:'json',headers:{'User-Agent':this.userAgent,'Accept':'*/*'}};
// Support for JSON, CVS and INI. Maybe XML at some point.
if (apidat['type']) {
getopts['type'] = apidat['type'];
};
// Run the request.
var formatter = (opts['formatter']||(apidat['formatter']||false)),
parent = this;
http_get(url,getopts,function(content,error) {
(formatter&&parent.settings['format']?formatter:cb)(content,error,cb);
});
};
// Do not overwrite an existing object, just copy it!
VidInfo.prototype.copyObj = function (obj) {
var newObj = ((obj.constructor===Array)?[]:{});
for (var key in obj) {
if ((this.getType(obj[key]).match(/(object|array)/i))) {
newObj[key] = this.copyObj(obj[key]);
} else {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
};
};
};
return newObj;
};
// Try and detect the API by shortcut.
VidInfo.prototype.byShortcut = function (api) {
if (!(api in this.apis)) {
for (var apin in this.apis) {
if ('shortcuts' in this.apis[apin] && this.apis[apin].shortcuts.indexOf(api) > -1) {
api = apin;
};
};
};
return api;
};
// Format strings.
VidInfo.prototype.stringFormat = function (str,opts) {
return str.replace(/{(\\?:)([^}]+)}/g,function(m,o,k) {
return (opts[k]?encodeURIComponent(opts[k]):m);
});
};
// Get the type of an object.
VidInfo.prototype.getType = function (obj) {
return Object.prototype.toString.call(obj).match(/^\[object (.*)\]$/)[1];
};
// Get location of API file.
VidInfo.prototype.getAPILocation = function (api) {
var api = (api.indexOf('.js')===-1?api+'.js':api),
location = false;
// XXX: Stop using fs.existsSync!?
if (fs.existsSync(this.settings.disabled+api)) {
location = {is:false,path:this.settings.disabled,file:api};
} else if (fs.existsSync(this.settings.enabled+api)) {
location = {is:true,path:this.settings.enabled,file:api};
};
return location;
};
// Enable an API.
VidInfo.prototype.enable = function (api,nomove) {
var location = this.getAPILocation(api), nf;
// No API!? What is this nonsense!
if (!location) {
return false;
};
// Already in the "enabled" folder, or nomove.
if (nomove || location.is === true) {
return this.__loadAPI.call(this,location['path']+location['file'],location['file']);
} else {
// Move to the "enabled" folder, then load!
if ((nf = this.__moveFile(location['file'],location['is']))) {
return this.__loadAPI.call(this,nf,location['file']);
};
};
return false;
};
// Load an API! Moved from "enable" because it was dumb.
VidInfo.prototype.__loadAPI = function (location,file) {
try {
api = file.split('.')[0];
this.apis[api] = require(location);
// Remove the file from required cache. (Bad idea?)
delete require.cache[require.resolve(location)];
this.enabled[api] = true;
if (('disabled' in this && api in this.disabled)) {
delete this.disabled[api];
};
// Attempt to add shortcuts if available.
if (('shortcuts' in this.apis[api])) {
return this.addShortcuts(api);
} else {
return true;
};
} catch (e) {
return false;
};
};
// Disable an API.
VidInfo.prototype.disable = function (api,nomove) {
var location = this.getAPILocation(api), nf;
// No API!? What is this nonsense!
if (!location) {
return false;
};
// Already in the "disabled" folder, or nomove.
if (nomove || location.is === false) {
return this.__unloadAPI.call(this,location['path']+location['file'],location['file']);
} else {
// Move to the "enabled" filder, then load!
if ((nf = this.__moveFile(location['file'],location['is']))) {
return this.__unloadAPI.call(this,nf,location['file']);
};
};
return false;
};
// Unload an API! Moved from "disable" because it was dumb.
VidInfo.prototype.__unloadAPI = function (location,api) {
try {
var api = api.split('.')[0],
tmp = require(location);
this.disabled[api] = true;
if (('enabled' in this && api in this.enabled)) {
delete this.enabled[api];
};
var shortcuts = [api],
shortcuts = shortcuts.concat(tmp.shortcuts||[]);
for (var num in shortcuts) {
if ((shortcuts[num] in this)) {
delete this[shortcuts[num]];
};
};
// Remove the file from required cache. (Bad idea?)
delete require.cache[require.resolve(location)];
return true;
} catch (e) {
return false;
};
};
// Move files around. No need for a user to use this!
VidInfo.prototype.__moveFile = function (file,mode) {
try {
var paths;
// XXX: Is this hacky!?
switch (mode) {
case false:
paths = [this.settings.enabled,this.settings.disabled];
break;
case true: default:
paths = [this.settings.disabled,this.settings.enabled];
break;
};
var newfile = paths[0]+file,
oldfile = paths[1]+file;
// Move the file from "enabled" to "disabled" -- ignored if "nomove" is passed!
// XXX: Stop using fs.renameSync and fs.existsSync!?
fs.renameSync(oldfile,newfile);
// Check if the move was successful.
return (fs.existsSync(newfile)?newfile:false);
} catch (e) {
return false;
};
};
// Import enabled APIs, make a list of disabled.
VidInfo.prototype.importAPIs = function () {
// List of enabled.
var enabledFiles = fs.readdirSync(this.settings.enabled);
// List of disabled.
var disabledFiles = fs.readdirSync(this.settings.disabled);
// Build the list of disabled APIs.
this.disabled = {};
for (var num in disabledFiles) {
this.disable(disabledFiles[num]);
};
// Build the list of enabled APIs.
this.enabled = {};
this.apis = {};
for (var enabled in enabledFiles) {
this.enable(enabledFiles[enabled]);
};
// Return true or false, depending on loaded modules.
return (!!Object.keys(this.apis).length);
};
// Generate an embed.ly config.
// XXX: Moved from ./embedlyGenerator.js, needs improvement!
VidInfo.prototype.genEmbedly = function (cb,servurl) {
// Do nothing at the end if there is no callback.
cb = cb||function(){};
servurl = servurl||'http://api.embed.ly/1/services';
http_get(servurl,{type:'json'},function (list,err) {
if (err) return; // Do nothing... for now.
fs.readFile("./libs/template.js", function (err, data) {
if (err) return; // Do nothing... for now.
var temp = data.toString(),
shortcuts = [],
regex = [];
for (var num in list) {
var data = list[num];
if (data.type == "video") {
for (var reg in data['regex']) {
regex.push(new RegExp(data['regex'][reg].replace(/\*/gi,'(?:.*)').replace(/\//gi,'\\/'),'i'));
};
// Prevent adding the same shortcut multiple times.
var sc = data.name.toLowerCase().replace('www','');
if (shortcuts.indexOf(sc) === -1) {
shortcuts.push(sc);
};
};
};
var spc = new Array(16).join(' ');
var obj = {REGEX:regex.join(',\n'+spc),SHORTCUTS:'\''+shortcuts.join('\',\n'+spc+'\'')+'\'',SERVICEURL:servurl},
location = __dirname+'/apis/embedly.js';
data = VidInfo.prototype.stringFormat(temp,obj);
fs.writeFile(location,data,function (err) {
if (!err) {
cb({message:'File saved to '+location,location:location,success:true});
} else {
cb({message:'Could not save to '+location,location:location,success:false});
};
});
});
});
};
// Add 'byID' shortcuts. See ./examples/byapi.js
VidInfo.prototype.addShortcuts = function (api) {
if (!(api in this.apis)) {
return false;
};
var shortcuts = [api];
if (('shortcuts' in this.apis[api])) {
shortcuts = shortcuts.concat(this.apis[api].shortcuts||[]);
};
for (var num in shortcuts) {
if (!(shortcuts[num] in this)) {
// Now with 100% less bind!
this[shortcuts[num]] = (function (api) {
return function (id,cb,opts) {
this.byID(id,api,cb,opts);
};
})(api);
};
};
return true;
};
module.exports = VidInfo;
}).call(this);