Resolve merge conflict
This commit is contained in:
commit
70be8a6713
21 changed files with 752 additions and 432 deletions
|
|
@ -22,6 +22,12 @@ const TYPE_PM = {
|
|||
meta: "object,optional"
|
||||
};
|
||||
|
||||
// Limit to 10 messages/sec
|
||||
const MIN_ANTIFLOOD = {
|
||||
burst: 20,
|
||||
sustained: 10
|
||||
};
|
||||
|
||||
function ChatModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.buffer = [];
|
||||
|
|
@ -192,7 +198,13 @@ ChatModule.prototype.handlePm = function (user, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
var msg = data.msg.substring(0, 240);
|
||||
if (user.chatLimiter.throttle(MIN_ANTIFLOOD)) {
|
||||
user.socket.emit("cooldown", 1000 / MIN_ANTIFLOOD.sustained);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
data.msg = data.msg.substring(0, 240);
|
||||
var to = null;
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === data.to) {
|
||||
|
|
@ -216,7 +228,7 @@ ChatModule.prototype.handlePm = function (user, data) {
|
|||
}
|
||||
}
|
||||
|
||||
if (msg.indexOf(">") === 0) {
|
||||
if (data.msg.indexOf(">") === 0) {
|
||||
meta.addClass = "greentext";
|
||||
}
|
||||
|
||||
|
|
@ -243,13 +255,34 @@ ChatModule.prototype.processChatMsg = function (user, data) {
|
|||
}
|
||||
|
||||
var msgobj = this.formatMessage(user.getName(), data);
|
||||
var antiflood = MIN_ANTIFLOOD;
|
||||
if (this.channel.modules.options &&
|
||||
this.channel.modules.options.get("chat_antiflood") &&
|
||||
user.account.effectiveRank < 2) {
|
||||
|
||||
var antiflood = this.channel.modules.options.get("chat_antiflood_params");
|
||||
if (user.chatLimiter.throttle(antiflood)) {
|
||||
user.socket.emit("cooldown", 1000 / antiflood.sustained);
|
||||
antiflood = this.channel.modules.options.get("chat_antiflood_params");
|
||||
}
|
||||
|
||||
if (user.chatLimiter.throttle(antiflood)) {
|
||||
user.socket.emit("cooldown", 1000 / antiflood.sustained);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.indexOf(">") === 0) {
|
||||
msgobj.meta.addClass = "greentext";
|
||||
}
|
||||
|
||||
if (data.msg.indexOf("/") === 0) {
|
||||
var space = data.msg.indexOf(" ");
|
||||
var cmd;
|
||||
if (space < 0) {
|
||||
cmd = data.msg.substring(1);
|
||||
} else {
|
||||
cmd = data.msg.substring(1, space);
|
||||
}
|
||||
|
||||
if (cmd in this.commandHandlers) {
|
||||
this.commandHandlers[cmd](user, data.msg, data.meta);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -270,27 +303,7 @@ ChatModule.prototype.processChatMsg = function (user, data) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.indexOf("/") === 0) {
|
||||
var space = data.msg.indexOf(" ");
|
||||
var cmd;
|
||||
if (space < 0) {
|
||||
cmd = data.msg.substring(1);
|
||||
} else {
|
||||
cmd = data.msg.substring(1, space);
|
||||
}
|
||||
|
||||
if (cmd in this.commandHandlers) {
|
||||
this.commandHandlers[cmd](user, data.msg, data.meta);
|
||||
} else {
|
||||
this.sendMessage(msgobj);
|
||||
}
|
||||
} else {
|
||||
if (data.msg.indexOf(">") === 0) {
|
||||
msgobj.meta.addClass = "greentext";
|
||||
}
|
||||
this.sendMessage(msgobj);
|
||||
}
|
||||
this.sendMessage(msgobj);
|
||||
};
|
||||
|
||||
ChatModule.prototype.formatMessage = function (username, data) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ OptionsModule.prototype.load = function (data) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.opts.chat_antiflood_params.burst = Math.min(20,
|
||||
this.opts.chat_antiflood_params.burst);
|
||||
this.opts.chat_antiflood_params.sustained = Math.min(10,
|
||||
this.opts.chat_antiflood_params.sustained);
|
||||
};
|
||||
|
||||
OptionsModule.prototype.save = function (data) {
|
||||
|
|
@ -216,11 +221,15 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
|
|||
b = 1;
|
||||
}
|
||||
|
||||
b = Math.min(20, b);
|
||||
|
||||
var s = parseFloat(data.chat_antiflood_params.sustained);
|
||||
if (isNaN(s) || s <= 0) {
|
||||
s = 1;
|
||||
}
|
||||
|
||||
s = Math.min(10, s);
|
||||
|
||||
var c = b / s;
|
||||
this.opts.chat_antiflood_params = {
|
||||
burst: b,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ var defaults = {
|
|||
},
|
||||
"channel-blacklist": [],
|
||||
ffmpeg: {
|
||||
enabled: false
|
||||
enabled: false,
|
||||
"ffprobe-exec": "ffprobe"
|
||||
},
|
||||
"link-domain-blacklist": [],
|
||||
setuid: {
|
||||
|
|
@ -351,9 +352,8 @@ function preprocessConfig(cfg) {
|
|||
require("cytube-mediaquery/lib/provider/youtube").setApiKey(
|
||||
cfg["youtube-v3-key"]);
|
||||
} else {
|
||||
Logger.errlog.log("Warning: No YouTube v3 API key set. YouTube lookups will " +
|
||||
"fall back to the v2 API, which is scheduled for closure soon after " +
|
||||
"April 20, 2015. See " +
|
||||
Logger.errlog.log("Warning: No YouTube v3 API key set. YouTube links will " +
|
||||
"not work. See youtube-v3-key in config.template.yaml and " +
|
||||
"https://developers.google.com/youtube/registering_an_application for " +
|
||||
"information on registering an API key.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ var Logger = require("../logger");
|
|||
var registrationLock = {};
|
||||
var blackHole = function () { };
|
||||
|
||||
/**
|
||||
* Replaces look-alike characters with "_" (single character wildcard) for
|
||||
* use in LIKE queries. This prevents guests from taking names that look
|
||||
* visually identical to existing names in certain fonts.
|
||||
*/
|
||||
function wildcardSimilarChars(name) {
|
||||
return name.replace(/[Il1oO0]/g, "_");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function () {
|
||||
},
|
||||
|
|
@ -15,7 +24,7 @@ module.exports = {
|
|||
* Check if a username is taken
|
||||
*/
|
||||
isUsernameTaken: function (name, callback) {
|
||||
db.query("SELECT name FROM `users` WHERE name=?", [name],
|
||||
db.query("SELECT name FROM `users` WHERE name LIKE ?", [wildcardSimilarChars(name)],
|
||||
function (err, rows) {
|
||||
if (err) {
|
||||
callback(err, true);
|
||||
|
|
|
|||
353
lib/ffmpeg.js
353
lib/ffmpeg.js
|
|
@ -1,6 +1,12 @@
|
|||
var Logger = require("./logger");
|
||||
var Config = require("./config");
|
||||
var spawn = require("child_process").spawn;
|
||||
var https = require("https");
|
||||
var http = require("http");
|
||||
var urlparse = require("url");
|
||||
var statusMessages = require("./status-messages");
|
||||
|
||||
var USE_JSON = true;
|
||||
|
||||
var acceptedCodecs = {
|
||||
"mov/h264": true,
|
||||
|
|
@ -19,6 +25,169 @@ var audioOnlyContainers = {
|
|||
"mp3": true
|
||||
};
|
||||
|
||||
function testUrl(url, cb, redirCount) {
|
||||
if (!redirCount) redirCount = 0;
|
||||
var data = urlparse.parse(url);
|
||||
if (!/https?:/.test(data.protocol)) {
|
||||
return cb("Video links must start with http:// or https://");
|
||||
}
|
||||
|
||||
if (!data.hostname) {
|
||||
return cb("Invalid link");
|
||||
}
|
||||
|
||||
var transport = (data.protocol === "https:") ? https : http;
|
||||
data.method = "HEAD";
|
||||
var req = transport.request(data, function (res) {
|
||||
req.abort();
|
||||
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
if (redirCount > 2) {
|
||||
return cb("Too many redirects. Please provide a direct link to the " +
|
||||
"file");
|
||||
}
|
||||
return testUrl(res.headers["location"], cb, redirCount + 1);
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
var message = statusMessages[res.statusCode];
|
||||
if (!message) message = "";
|
||||
return cb("HTTP " + res.statusCode + " " + message);
|
||||
}
|
||||
|
||||
if (!/^audio|^video/.test(res.headers["content-type"])) {
|
||||
return cb("Server did not return an audio or video file, or sent the " +
|
||||
"wrong Content-Type");
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
|
||||
req.on("error", function (err) {
|
||||
cb(err);
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
function readOldFormat(buf) {
|
||||
var lines = buf.split("\n");
|
||||
var tmp = { tags: {} };
|
||||
var data = {
|
||||
streams: []
|
||||
};
|
||||
|
||||
lines.forEach(function (line) {
|
||||
if (line.match(/\[stream\]|\[format\]/i)) {
|
||||
return;
|
||||
} else if (line.match(/\[\/stream\]/i)) {
|
||||
data.streams.push(tmp);
|
||||
tmp = { tags: {} };
|
||||
} else if (line.match(/\[\/format\]/i)) {
|
||||
data.format = tmp;
|
||||
tmp = { tags: {} };
|
||||
} else {
|
||||
var kv = line.split("=");
|
||||
var key = kv[0].toLowerCase();
|
||||
if (key.indexOf("tag:") === 0) {
|
||||
tmp.tags[key.split(":")[1]] = kv[1];
|
||||
} else {
|
||||
tmp[key] = kv[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function reformatData(data) {
|
||||
var reformatted = {};
|
||||
|
||||
var duration = parseInt(data.format.duration, 10);
|
||||
if (isNaN(duration)) duration = "--:--";
|
||||
reformatted.duration = Math.ceil(duration);
|
||||
|
||||
var bitrate = parseInt(data.format.bit_rate, 10) / 1000;
|
||||
if (isNaN(bitrate)) bitrate = 0;
|
||||
reformatted.bitrate = bitrate;
|
||||
|
||||
reformatted.title = data.format.tags ? data.format.tags.title : null;
|
||||
var container = data.format.format_name.split(",")[0];
|
||||
|
||||
data.streams.forEach(function (stream) {
|
||||
if (stream.codec_type === "video") {
|
||||
reformatted.vcodec = stream.codec_name;
|
||||
if (!reformatted.title && stream.tags) {
|
||||
reformatted.title = stream.tags.title;
|
||||
}
|
||||
} else if (stream.codec_type === "audio") {
|
||||
reformatted.acodec = stream.codec_name;
|
||||
}
|
||||
});
|
||||
|
||||
if (reformatted.vcodec && !(audioOnlyContainers.hasOwnProperty(container))) {
|
||||
reformatted.type = [container, reformatted.vcodec].join("/");
|
||||
reformatted.medium = "video";
|
||||
} else if (reformatted.acodec) {
|
||||
reformatted.type = [container, reformatted.acodec].join("/");
|
||||
reformatted.medium = "audio";
|
||||
}
|
||||
|
||||
return reformatted;
|
||||
}
|
||||
|
||||
exports.ffprobe = function ffprobe(filename, cb) {
|
||||
var childErr;
|
||||
var args = ["-show_streams", "-show_format", filename];
|
||||
if (USE_JSON) args = ["-of", "json"].concat(args);
|
||||
var child = spawn(Config.get("ffmpeg.ffprobe-exec"), args);
|
||||
var stdout = "";
|
||||
var stderr = "";
|
||||
|
||||
child.on("error", function (err) {
|
||||
childErr = err;
|
||||
});
|
||||
|
||||
child.stdout.on("data", function (data) {
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
child.stderr.on("data", function (data) {
|
||||
stderr += data;
|
||||
});
|
||||
|
||||
child.on("close", function (code) {
|
||||
if (code !== 0) {
|
||||
if (stderr.match(/unrecognized option|json/i) && USE_JSON) {
|
||||
Logger.errlog.log("Warning: ffprobe does not support -of json. " +
|
||||
"Assuming it will have old output format.");
|
||||
USE_JSON = false;
|
||||
return ffprobe(filename, cb);
|
||||
}
|
||||
|
||||
if (!childErr) childErr = new Error(stderr);
|
||||
return cb(childErr);
|
||||
}
|
||||
|
||||
var result;
|
||||
if (USE_JSON) {
|
||||
try {
|
||||
result = JSON.parse(stdout);
|
||||
} catch (e) {
|
||||
return cb(new Error("Unable to parse ffprobe output: " + e.message));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
result = readOldFormat(stdout);
|
||||
} catch (e) {
|
||||
return cb(new Error("Unable to parse ffprobe output: " + e.message));
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
exports.query = function (filename, cb) {
|
||||
if (!Config.get("ffmpeg.enabled")) {
|
||||
return cb("Raw file playback is not enabled on this server");
|
||||
|
|
@ -29,135 +198,73 @@ exports.query = function (filename, cb) {
|
|||
"or HTTPS");
|
||||
}
|
||||
|
||||
ffprobe(filename, function (err, meta) {
|
||||
testUrl(filename, function (err) {
|
||||
if (err) {
|
||||
if (meta.stderr && meta.stderr.match(/Protocol not found/)) {
|
||||
return cb("Link uses a protocol unsupported by this server's ffmpeg");
|
||||
} else {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
exports.ffprobe(filename, function (err, data) {
|
||||
if (err) {
|
||||
if (err.code && err.code === "ENOENT") {
|
||||
return cb("Failed to execute `ffprobe`. Set ffmpeg.ffprobe-exec " +
|
||||
"to the correct name of the executable in config.yaml. " +
|
||||
"If you are using Debian or Ubuntu, it is probably " +
|
||||
"avprobe.");
|
||||
} else if (err.message) {
|
||||
if (err.message.match(/protocol not found/i))
|
||||
return cb("Link uses a protocol unsupported by this server's " +
|
||||
"version of ffmpeg");
|
||||
|
||||
// Ignore ffprobe error messages, they are common and most often
|
||||
// indicate a problem with the remote file, not with this code.
|
||||
if (!/(av|ff)probe/.test(String(err)))
|
||||
Logger.errlog.log(err.stack || err);
|
||||
return cb("Unable to query file data with ffmpeg");
|
||||
} else {
|
||||
if (!/(av|ff)probe/.test(String(err)))
|
||||
Logger.errlog.log(err.stack || err);
|
||||
return cb("Unable to query file data with ffmpeg");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
data = reformatData(data);
|
||||
} catch (e) {
|
||||
Logger.errlog.log(e.stack || e);
|
||||
return cb("Unable to query file data with ffmpeg");
|
||||
}
|
||||
}
|
||||
|
||||
meta = parse(meta);
|
||||
if (meta == null) {
|
||||
return cb("Unknown error");
|
||||
}
|
||||
if (data.medium === "video") {
|
||||
if (!acceptedCodecs.hasOwnProperty(data.type)) {
|
||||
return cb("Unsupported video codec " + data.type);
|
||||
}
|
||||
|
||||
if (isVideo(meta)) {
|
||||
var codec = meta.container + "/" + meta.vcodec;
|
||||
data = {
|
||||
title: data.title || "Raw Video",
|
||||
duration: data.duration,
|
||||
bitrate: data.bitrate,
|
||||
codec: data.type
|
||||
};
|
||||
|
||||
if (!(codec in acceptedCodecs)) {
|
||||
return cb("Unsupported video codec " + codec);
|
||||
cb(null, data);
|
||||
} else if (data.medium === "audio") {
|
||||
if (!acceptedAudioCodecs.hasOwnProperty(data.acodec)) {
|
||||
return cb("Unsupported audio codec " + data.acodec);
|
||||
}
|
||||
|
||||
data = {
|
||||
title: data.title || "Raw Audio",
|
||||
duration: data.duration,
|
||||
bitrate: data.bitrate,
|
||||
codec: data.acodec
|
||||
};
|
||||
|
||||
cb(null, data);
|
||||
} else {
|
||||
return cb("Parsed metadata did not contain a valid video or audio " +
|
||||
"stream. Either the file is invalid or it has a format " +
|
||||
"unsupported by this server's version of ffmpeg.");
|
||||
}
|
||||
|
||||
var data = {
|
||||
title: meta.title || "Raw Video",
|
||||
duration: Math.ceil(meta.seconds) || "--:--",
|
||||
bitrate: meta.bitrate,
|
||||
codec: codec
|
||||
};
|
||||
|
||||
cb(null, data);
|
||||
} else if (isAudio(meta)) {
|
||||
var codec = meta.acodec;
|
||||
|
||||
if (!(codec in acceptedAudioCodecs)) {
|
||||
return cb("Unsupported audio codec " + codec);
|
||||
}
|
||||
|
||||
var data = {
|
||||
title: meta.title || "Raw Audio",
|
||||
duration: Math.ceil(meta.seconds) || "--:--",
|
||||
bitrate: meta.bitrate,
|
||||
codec: codec
|
||||
};
|
||||
|
||||
cb(null, data);
|
||||
} else if (data.ffmpegErr.match(/Protocol not found/)) {
|
||||
return cb("This server is unable to load videos over the " +
|
||||
filename.split(":")[0] + " protocol.");
|
||||
} else {
|
||||
return cb("Parsed metadata did not contain a valid video or audio stream. " +
|
||||
"Either the file is invalid or it has a format unsupported by " +
|
||||
"this server's version of ffmpeg.");
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function isVideo(meta) {
|
||||
return meta.vcodec && !(meta.container in audioOnlyContainers);
|
||||
}
|
||||
|
||||
function isAudio(meta) {
|
||||
return meta.acodec;
|
||||
}
|
||||
|
||||
function parse(meta) {
|
||||
if (meta == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!meta.format) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
meta.streams.forEach(function (s) {
|
||||
if (s.codec_type === "video") {
|
||||
data.vcodec = s.codec_name;
|
||||
} else if (s.codec_type === "audio") {
|
||||
data.acodec = s.codec_name;
|
||||
}
|
||||
});
|
||||
|
||||
data.container = meta.format.format_name.split(",")[0];
|
||||
data.bitrate = parseInt(meta.format.bit_rate) / 1000;
|
||||
if (meta.format.tags) {
|
||||
data.title = meta.format.tags.title;
|
||||
}
|
||||
data.seconds = Math.ceil(parseFloat(meta.format.duration));
|
||||
return data;
|
||||
}
|
||||
|
||||
function ffprobe(filename, cb) {
|
||||
var ff = spawn("ffprobe", ["-show_streams", "-show_format", filename]);
|
||||
|
||||
var outbuf = "";
|
||||
var errbuf = "";
|
||||
ff.stdout.on("data", function (data) {
|
||||
outbuf += data;
|
||||
});
|
||||
ff.stderr.on("data", function (data) {
|
||||
errbuf += data;
|
||||
});
|
||||
|
||||
ff.on("close", function (code) {
|
||||
if (code !== 0) {
|
||||
return cb("ffprobe exited with nonzero exit code", { stderr: errbuf });
|
||||
}
|
||||
|
||||
var lines = outbuf.split("\n");
|
||||
var streams = [];
|
||||
var format = {};
|
||||
var data = {};
|
||||
lines.forEach(function (line) {
|
||||
if (line.match(/\[stream\]|\[format\]/i)) {
|
||||
return;
|
||||
} else if (line.match(/\[\/stream\]/i)) {
|
||||
streams.push(data);
|
||||
data = {};
|
||||
} else if (line.match(/\[\/format\]/i)) {
|
||||
format = data;
|
||||
data = {};
|
||||
} else {
|
||||
var kv = line.split("=");
|
||||
data[kv[0]] = kv[1];
|
||||
}
|
||||
});
|
||||
|
||||
cb(null, {
|
||||
streams: streams,
|
||||
format: format
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
257
lib/get-info.js
257
lib/get-info.js
|
|
@ -77,7 +77,8 @@ var Getters = {
|
|||
/* youtube.com */
|
||||
yt: function (id, callback) {
|
||||
if (!Config.get("youtube-v3-key")) {
|
||||
return Getters.yt2(id, callback);
|
||||
return callback("The YouTube API now requires an API key. Please see the " +
|
||||
"documentation for youtube-v3-key in config.template.yaml");
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -97,7 +98,8 @@ var Getters = {
|
|||
/* youtube.com playlists */
|
||||
yp: function (id, callback) {
|
||||
if (!Config.get("youtube-v3-key")) {
|
||||
return Getters.yp2(id, callback);
|
||||
return callback("The YouTube API now requires an API key. Please see the " +
|
||||
"documentation for youtube-v3-key in config.template.yaml");
|
||||
}
|
||||
|
||||
YouTube.lookupPlaylist(id).then(function (videos) {
|
||||
|
|
@ -119,7 +121,8 @@ var Getters = {
|
|||
/* youtube.com search */
|
||||
ytSearch: function (query, callback) {
|
||||
if (!Config.get("youtube-v3-key")) {
|
||||
return Getters.ytSearch2(query.split(" "), callback);
|
||||
return callback("The YouTube API now requires an API key. Please see the " +
|
||||
"documentation for youtube-v3-key in config.template.yaml");
|
||||
}
|
||||
|
||||
YouTube.search(query).then(function (res) {
|
||||
|
|
@ -582,254 +585,6 @@ var Getters = {
|
|||
var media = new Media(id, title, "--:--", "hb");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* youtube.com - old v2 API */
|
||||
yt2: function (id, callback) {
|
||||
var sv = Server.getServer();
|
||||
|
||||
var m = id.match(/([\w-]{11})/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
path: "/feeds/api/videos/" + id + "?v=2&alt=json",
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
if (Config.get("youtube-v2-key")) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private video", null);
|
||||
case 404:
|
||||
return callback("Video not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
var buffer = data;
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
/* Check for embedding restrictions */
|
||||
if (data.entry.yt$accessControl) {
|
||||
var ac = data.entry.yt$accessControl;
|
||||
for (var i = 0; i < ac.length; i++) {
|
||||
if (ac[i].action === "embed") {
|
||||
if (ac[i].permission === "denied") {
|
||||
callback("Embedding disabled", null);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var seconds = data.entry.media$group.yt$duration.seconds;
|
||||
var title = data.entry.title.$t;
|
||||
var meta = {};
|
||||
/* Check for country restrictions */
|
||||
if (data.entry.media$group.media$restriction) {
|
||||
var rest = data.entry.media$group.media$restriction;
|
||||
if (rest.length > 0) {
|
||||
if (rest[0].relationship === "deny") {
|
||||
meta.restricted = rest[0].$t;
|
||||
}
|
||||
}
|
||||
}
|
||||
var media = new Media(id, title, seconds, "yt", meta);
|
||||
callback(false, media);
|
||||
} catch (e) {
|
||||
// Gdata version 2 has the rather silly habit of
|
||||
// returning error codes in XML when I explicitly asked
|
||||
// for JSON
|
||||
var m = buffer.match(/<internalReason>([^<]+)<\/internalReason>/);
|
||||
if (m === null)
|
||||
m = buffer.match(/<code>([^<]+)<\/code>/);
|
||||
|
||||
var err = e;
|
||||
if (m) {
|
||||
if(m[1] === "too_many_recent_calls") {
|
||||
err = "YouTube is throttling the server right "+
|
||||
"now for making too many requests. "+
|
||||
"Please try again in a moment.";
|
||||
} else {
|
||||
err = m[1];
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* youtube.com playlists - old v2 api */
|
||||
yp2: function (id, callback, url) {
|
||||
/**
|
||||
* NOTE: callback may be called multiple times, once for each <= 25 video
|
||||
* batch of videos in the list. It will be called in order.
|
||||
*/
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var path = "/feeds/api/playlists/" + id + "?v=2&alt=json";
|
||||
/**
|
||||
* NOTE: the third parameter, url, is used to chain this retriever
|
||||
* multiple times to get all the videos from a playlist, as each
|
||||
* request only returns 25 videos.
|
||||
*/
|
||||
if (url !== undefined) {
|
||||
path = "/" + url.split("gdata.youtube.com")[1];
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
path: path,
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
if (Config.get("youtube-v2-key")) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private playlist", null);
|
||||
case 404:
|
||||
return callback("Playlist not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var vids = [];
|
||||
for(var i in data.feed.entry) {
|
||||
try {
|
||||
/**
|
||||
* FIXME: This should probably check for embed restrictions
|
||||
* and country restrictions on each video in the list
|
||||
*/
|
||||
var item = data.feed.entry[i];
|
||||
var id = item.media$group.yt$videoid.$t;
|
||||
var title = item.title.$t;
|
||||
var seconds = item.media$group.yt$duration.seconds;
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
vids.push(media);
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
|
||||
callback(false, vids);
|
||||
|
||||
var links = data.feed.link;
|
||||
for (var i in links) {
|
||||
if (links[i].rel === "next") {
|
||||
/* Look up the next batch of videos from the list */
|
||||
Getters["yp2"](id, callback, links[i].href);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
callback(e, null);
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/* youtube.com search - old v2 api */
|
||||
ytSearch2: function (terms, callback) {
|
||||
/**
|
||||
* terms is a list of words from the search query. Each word must be
|
||||
* encoded properly for use in the request URI
|
||||
*/
|
||||
for (var i in terms) {
|
||||
terms[i] = encodeURIComponent(terms[i]);
|
||||
}
|
||||
var query = terms.join("+");
|
||||
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
path: "/feeds/api/videos/?q=" + query + "&v=2&alt=json",
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
if (Config.get("youtube-v2-key")) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if (status !== 200) {
|
||||
callback("YouTube search: HTTP " + status, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var vids = [];
|
||||
for(var i in data.feed.entry) {
|
||||
try {
|
||||
/**
|
||||
* FIXME: This should probably check for embed restrictions
|
||||
* and country restrictions on each video in the list
|
||||
*/
|
||||
var item = data.feed.entry[i];
|
||||
var id = item.media$group.yt$videoid.$t;
|
||||
var title = item.title.$t;
|
||||
var seconds = item.media$group.yt$duration.seconds;
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
media.thumb = item.media$group.media$thumbnail[0];
|
||||
vids.push(media);
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
|
||||
callback(false, vids);
|
||||
} catch(e) {
|
||||
callback(e, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
83
lib/status-messages.js
Normal file
83
lib/status-messages.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// This status message map is taken from the node.js source code. The original
|
||||
// copyright notice for lib/_http_server.js is reproduced below.
|
||||
//
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module.exports = {
|
||||
100: "Continue",
|
||||
101: "Switching Protocols",
|
||||
102: "Processing",
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
203: "Non-Authoritative Information",
|
||||
204: "No Content",
|
||||
205: "Reset Content",
|
||||
206: "Partial Content",
|
||||
207: "Multi-Status",
|
||||
300: "Multiple Choices",
|
||||
301: "Moved Permanently",
|
||||
302: "Moved Temporarily",
|
||||
303: "See Other",
|
||||
304: "Not Modified",
|
||||
305: "Use Proxy",
|
||||
307: "Temporary Redirect",
|
||||
308: "Permanent Redirect",
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Time-out",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Request Entity Too Large",
|
||||
414: "Request-URI Too Large",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Requested Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "I'm a teapot",
|
||||
422: "Unprocessable Entity",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Unordered Collection",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Time-out",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
509: "Bandwidth Limit Exceeded",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required"
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue