mirror of https://github.com/calzoneman/sync.git
Resolve merge conflict
This commit is contained in:
commit
70be8a6713
|
@ -120,10 +120,14 @@ mail:
|
|||
|
||||
# YouTube v3 API key
|
||||
# See https://developers.google.com/youtube/registering_an_application
|
||||
# Google is closing the v2 API (which allowed anonymous requests) on
|
||||
# April 20, 2015 so you must register a v3 API key now.
|
||||
# NOTE: You must generate a Server key under Public API access, NOT a
|
||||
# browser key.
|
||||
# YouTube links will not work without this!
|
||||
# Instructions:
|
||||
# 1. Go to https://console.developers.google.com/project
|
||||
# 2. Create a new API project
|
||||
# 3. On the left sidebar, click "Credentials" under "APIs & auth"
|
||||
# 4. Click "Create new Key" under "Public API access"
|
||||
# 5. Click "Server key"
|
||||
# 6. Under "APIs & auth" click "YouTube Data API" and then click "Enable API"
|
||||
youtube-v3-key: ''
|
||||
# Minutes between saving channel state to disk
|
||||
channel-save-interval: 5
|
||||
|
@ -202,6 +206,9 @@ channel-blacklist: []
|
|||
# * ffmpeg must be installed on the server
|
||||
ffmpeg:
|
||||
enabled: false
|
||||
# Executable name for ffprobe if it is not "ffprobe". On Debian and Ubuntu (on which
|
||||
# libav is used rather than ffmpeg proper), this is "avprobe"
|
||||
ffprobe-exec: 'ffprobe'
|
||||
|
||||
link-domain-blacklist: []
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"
|
||||
};
|
|
@ -14,7 +14,7 @@
|
|||
"cookie-parser": "^1.3.3",
|
||||
"csrf": "^2.0.6",
|
||||
"cytube-mediaquery": "git://github.com/CyTube/mediaquery",
|
||||
"cytubefilters": "git://github.com/calzoneman/cytubefilters#7663b3a9",
|
||||
"cytubefilters": "git://github.com/calzoneman/cytubefilters#65e685ad",
|
||||
"express": "^4.11.1",
|
||||
"express-minify": "^0.1.3",
|
||||
"graceful-fs": "^3.0.5",
|
||||
|
|
|
@ -21,7 +21,7 @@ html(lang="en")
|
|||
b.caret
|
||||
ul.dropdown-menu
|
||||
li: a(href="#" onclick="javascript:chatOnly()") Chat Only
|
||||
li: a(href="#" onclick="javascript:removeVideo()") Remove Video
|
||||
li: a(href="#" onclick="javascript:removeVideo(event)") Remove Video
|
||||
mixin navloginlogout(cname)
|
||||
section#mainpage
|
||||
.container
|
||||
|
@ -56,6 +56,7 @@ html(lang="en")
|
|||
#controlsrow.row
|
||||
#leftcontrols.col-lg-5.col-md-5
|
||||
button#newpollbtn.btn.btn-sm.btn-default New Poll
|
||||
button#emotelistbtn.btn.btn-sm.btn-default Emote List
|
||||
#rightcontrols.col-lg-7.col-md-7
|
||||
#plcontrol.btn-group.pull-left
|
||||
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
|
||||
|
@ -125,7 +126,7 @@ html(lang="en")
|
|||
input.add-temp(type="checkbox")
|
||||
| Add as temporary
|
||||
| Paste the embed code below and click Next or At End.
|
||||
| Acceptable embed codes are <code><iframe></code> and <code><object></code> tags.
|
||||
| Acceptable embed codes are <code><iframe></code> and <code><object></code> tags. <strong>CUSTOM EMBEDS CANNOT BE SYNCHRONIZED.</strong>
|
||||
textarea#customembed-content.input-block-level.form-control(rows="3")
|
||||
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
|
||||
.vertical-spacer
|
||||
|
@ -173,6 +174,24 @@ html(lang="en")
|
|||
.modal-footer
|
||||
button.btn.btn-primary(type="button", data-dismiss="modal", onclick="javascript:saveUserOptions()") Save
|
||||
button.btn.btn-default(type="button", data-dismiss="modal") Close
|
||||
#emotelist.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
|
||||
.modal-dialog.modal-dialog-nonfluid
|
||||
.modal-content
|
||||
.modal-header
|
||||
button.close(data-dismiss="modal", aria-hidden="true") ×
|
||||
h4 Emote List
|
||||
.modal-body
|
||||
.pull-left
|
||||
input#emotelist-search.form-control(type="text", placeholder="Search")
|
||||
.pull-right
|
||||
.checkbox
|
||||
label
|
||||
input#emotelist-alphabetical(type="checkbox")
|
||||
| Sort alphabetically
|
||||
#emotelist-paginator-container
|
||||
table
|
||||
tbody
|
||||
.modal-footer
|
||||
#channeloptions.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
|
|
|
@ -557,15 +557,15 @@ body.chatOnly .pm-panel, body.chatOnly .pm-panel-placeholder {
|
|||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modal {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
min-width: 600px!important;
|
||||
max-width: 1200px!important;
|
||||
width: auto!important;
|
||||
}
|
||||
|
||||
.modal-dialog-nonfluid.modal-dialog {
|
||||
max-width: 600px!important;
|
||||
}
|
||||
}
|
||||
|
||||
table td {
|
||||
|
@ -592,3 +592,36 @@ table td {
|
|||
border: 1px solid;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
#emotelist table {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.emote-preview-container {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
float: left;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.emote-preview-hax {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.emote-preview {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#emotelist-paginator-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#leftcontrols .btn {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ footer {
|
|||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#emotelist td {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.chat-shadow {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ input.form-control[type="email"], textarea.form-control {
|
|||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.profile-box, .user-dropdown {
|
||||
.profile-box, .user-dropdown, #emotelist td {
|
||||
color: #c8c8c8;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,10 @@ footer {
|
|||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#emotelist td {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.chat-shadow {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,15 @@ input.form-control[type="email"], textarea.form-control {
|
|||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
#emotelist table {
|
||||
background-color: #2a2d30;
|
||||
}
|
||||
|
||||
#emotelist td {
|
||||
background-color: rgba(28, 30, 34, 0.95);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.profile-image {
|
||||
border-radius: 0px;
|
||||
border: solid 1px #000000 !important;
|
||||
|
@ -176,4 +185,4 @@ input.form-control[type="email"], textarea.form-control {
|
|||
margin-bottom: 9px;
|
||||
min-height: 20px;
|
||||
padding: 10px 19px !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ input.form-control[type="email"], textarea.form-control {
|
|||
background-image: linear-gradient(#484e55, #3a3f44 60%, #313539) !important;
|
||||
}
|
||||
|
||||
.profile-box, .user-dropdown {
|
||||
.profile-box, .user-dropdown, #emotelist td {
|
||||
color: #c8c8c8;
|
||||
background-color: #161a20;
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ Callbacks = {
|
|||
cooldown: function (time) {
|
||||
time = time + 200;
|
||||
$("#chatline").css("color", "#ff0000");
|
||||
$(".pm-input").css("color", "#ff0000");
|
||||
if (CHATTHROTTLE && $("#chatline").data("throttle_timer")) {
|
||||
clearTimeout($("#chatline").data("throttle_timer"));
|
||||
}
|
||||
|
@ -131,6 +132,7 @@ Callbacks = {
|
|||
$("#chatline").data("throttle_timer", setTimeout(function () {
|
||||
CHATTHROTTLE = false;
|
||||
$("#chatline").css("color", "");
|
||||
$(".pm-input").css("color", "");
|
||||
}, time));
|
||||
},
|
||||
|
||||
|
@ -279,7 +281,7 @@ Callbacks = {
|
|||
channelCSSJS: function(data) {
|
||||
$("#chancss").remove();
|
||||
CHANNEL.css = data.css;
|
||||
$("#csstext").val(data.css);
|
||||
$("#cs-csstext").val(data.css);
|
||||
if(data.css && !USEROPTS.ignore_channelcss) {
|
||||
$("<style/>").attr("type", "text/css")
|
||||
.attr("id", "chancss")
|
||||
|
@ -289,7 +291,7 @@ Callbacks = {
|
|||
|
||||
$("#chanjs").remove();
|
||||
CHANNEL.js = data.js;
|
||||
$("#jstext").val(data.js);
|
||||
$("#cs-jstext").val(data.js);
|
||||
|
||||
if(data.js && !USEROPTS.ignore_channeljs) {
|
||||
var src = data.js
|
||||
|
@ -1015,6 +1017,7 @@ Callbacks = {
|
|||
var tbl = $("#cs-emotes table");
|
||||
tbl.data("entries", data);
|
||||
formatCSEmoteList();
|
||||
EMOTELIST.emoteListChanged = true;
|
||||
},
|
||||
|
||||
updateEmote: function (data) {
|
||||
|
|
|
@ -44,6 +44,7 @@ var IGNORED = [];
|
|||
var CHATHIST = [];
|
||||
var CHATHISTIDX = 0;
|
||||
var CHATTHROTTLE = false;
|
||||
var CHATMAXSIZE = 100;
|
||||
var SCROLLCHAT = true;
|
||||
var LASTCHAT = {
|
||||
name: ""
|
||||
|
@ -115,7 +116,9 @@ var USEROPTS = {
|
|||
default_quality : getOrDefault("default_quality", "auto"),
|
||||
boop : getOrDefault("boop", "never"),
|
||||
secure_connection : getOrDefault("secure_connection", false),
|
||||
show_shadowchat : getOrDefault("show_shadowchat", false)
|
||||
show_shadowchat : getOrDefault("show_shadowchat", false),
|
||||
emotelist_sort : getOrDefault("emotelist_sort", true),
|
||||
no_emotes : getOrDefault("no_emotes", false)
|
||||
};
|
||||
|
||||
/* Backwards compatibility check */
|
||||
|
|
|
@ -103,3 +103,108 @@
|
|||
return p;
|
||||
};
|
||||
})();
|
||||
|
||||
function NewPaginator(numItems, itemsPerPage, pageLoader) {
|
||||
this.numItems = numItems;
|
||||
this.itemsPerPage = itemsPerPage;
|
||||
this.elem = document.createElement("ul");
|
||||
this.elem.className = "pagination";
|
||||
this.btnBefore = 3;
|
||||
this.btnAfter = 3;
|
||||
this.pageLoader = pageLoader;
|
||||
}
|
||||
|
||||
NewPaginator.prototype.makeButton = function (target, text) {
|
||||
var li = document.createElement("li");
|
||||
var btn = document.createElement("a");
|
||||
btn.href = "javascript:void(0)";
|
||||
btn.innerHTML = text;
|
||||
var _this = this;
|
||||
if (target !== null) {
|
||||
btn.onclick = function (event) {
|
||||
if (this.parentNode.className === "disabled") {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
_this.loadPage(target);
|
||||
};
|
||||
}
|
||||
|
||||
li.appendChild(btn);
|
||||
return li;
|
||||
};
|
||||
|
||||
NewPaginator.prototype.makeBreak = function () {
|
||||
var btn = this.makeButton(null, "…");
|
||||
btn.className = "disabled";
|
||||
return btn;
|
||||
};
|
||||
|
||||
NewPaginator.prototype.loadButtons = function (page) {
|
||||
this.elem.innerHTML = "";
|
||||
|
||||
var first = this.makeButton(0, "First");
|
||||
this.elem.appendChild(first);
|
||||
if (page === 0) {
|
||||
first.className = "disabled";
|
||||
}
|
||||
|
||||
var prev = this.makeButton(page - 1, "«");
|
||||
this.elem.appendChild(prev);
|
||||
if (page === 0) {
|
||||
prev.className = "disabled";
|
||||
}
|
||||
|
||||
if (page > this.btnBefore) {
|
||||
var sep = this.makeBreak();
|
||||
this.elem.appendChild(sep);
|
||||
}
|
||||
|
||||
var numPages = Math.ceil(this.numItems / this.itemsPerPage);
|
||||
numPages = Math.max(numPages, 1);
|
||||
var numBtns = Math.min(this.btnBefore + this.btnAfter + 1, numPages);
|
||||
var start;
|
||||
if (page < this.btnBefore) {
|
||||
start = 0;
|
||||
} else if (page > numPages - this.btnAfter - 1) {
|
||||
start = numPages - numBtns;
|
||||
} else {
|
||||
start = page - this.btnBefore;
|
||||
}
|
||||
var end = start + numBtns;
|
||||
|
||||
var _this = this;
|
||||
for (var i = start; i < end; i++) {
|
||||
(function (i) {
|
||||
var btn = _this.makeButton(i, String(i + 1));
|
||||
_this.elem.appendChild(btn);
|
||||
if (i === page) {
|
||||
btn.className = "disabled";
|
||||
}
|
||||
})(i);
|
||||
}
|
||||
|
||||
if (page < numPages - this.btnAfter - 1) {
|
||||
var sep = this.makeBreak();
|
||||
this.elem.appendChild(sep);
|
||||
}
|
||||
|
||||
var next = this.makeButton(page + 1, "»");
|
||||
this.elem.appendChild(next);
|
||||
if (page === numPages - 1) {
|
||||
next.className = "disabled";
|
||||
}
|
||||
|
||||
var last = this.makeButton(numPages - 1, "Last");
|
||||
this.elem.appendChild(last);
|
||||
if (page === numPages - 1) {
|
||||
last.className = "disabled";
|
||||
}
|
||||
};
|
||||
|
||||
NewPaginator.prototype.loadPage = function (page) {
|
||||
this.loadButtons(page);
|
||||
if (this.pageLoader) {
|
||||
this.pageLoader(page);
|
||||
}
|
||||
};
|
||||
|
|
34
www/js/ui.js
34
www/js/ui.js
|
@ -740,6 +740,11 @@ $("#channeloptions li > a[data-toggle='tab']").on("shown.bs.tab", function () {
|
|||
applyOpts();
|
||||
|
||||
(function () {
|
||||
var embed = document.querySelector("#videowrap .embed-responsive");
|
||||
if (!embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.MutationObserver === "function") {
|
||||
var mr = new MutationObserver(function (records) {
|
||||
records.forEach(function (record) {
|
||||
|
@ -751,14 +756,39 @@ applyOpts();
|
|||
});
|
||||
});
|
||||
|
||||
mr.observe($("#videowrap").find(".embed-responsive")[0], { childList: true });
|
||||
mr.observe(embed, { childList: true });
|
||||
} else {
|
||||
/*
|
||||
* DOMNodeInserted is deprecated. This code is here only as a fallback
|
||||
* for browsers that do not support MutationObserver
|
||||
*/
|
||||
$("#videowrap").find(".embed-responsive")[0].addEventListener("DOMNodeInserted", function (ev) {
|
||||
embed.addEventListener("DOMNodeInserted", function (ev) {
|
||||
if (ev.target.id === "ytapiplayer") handleVideoResize();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
$("#emotelistbtn").click(function () {
|
||||
EMOTELIST.show();
|
||||
});
|
||||
|
||||
$("#emotelist-search").keyup(function () {
|
||||
var value = this.value.toLowerCase();
|
||||
if (value) {
|
||||
EMOTELIST.filter = function (emote) {
|
||||
return emote.name.toLowerCase().indexOf(value) >= 0;
|
||||
};
|
||||
} else {
|
||||
EMOTELIST.filter = null;
|
||||
}
|
||||
EMOTELIST.handleChange();
|
||||
EMOTELIST.loadPage(0);
|
||||
});
|
||||
|
||||
$("#emotelist-alphabetical").prop("checked", USEROPTS.emotelist_sort);
|
||||
$("#emotelist-alphabetical").change(function () {
|
||||
USEROPTS.emotelist_sort = this.checked;
|
||||
setOpt("emotelist_sort", USEROPTS.emotelist_sort);
|
||||
EMOTELIST.handleChange();
|
||||
EMOTELIST.loadPage(0);
|
||||
});
|
||||
|
|
143
www/js/util.js
143
www/js/util.js
|
@ -733,9 +733,7 @@ function applyOpts() {
|
|||
}
|
||||
|
||||
if(USEROPTS.hidevid) {
|
||||
$("#qualitywrap").html("");
|
||||
removeVideo();
|
||||
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-lg-12 col-md-12");
|
||||
}
|
||||
|
||||
$("#chatbtn").remove();
|
||||
|
@ -1492,10 +1490,7 @@ function addChatMessage(data) {
|
|||
div.mouseleave(function() {
|
||||
$(".nick-hover").removeClass("nick-hover");
|
||||
});
|
||||
// Cap chatbox at most recent 100 messages
|
||||
if($("#messagebuffer").children().length > 100) {
|
||||
$($("#messagebuffer").children()[0]).remove();
|
||||
}
|
||||
trimChatBuffer();
|
||||
if(SCROLLCHAT)
|
||||
scrollChat();
|
||||
|
||||
|
@ -1511,6 +1506,18 @@ function addChatMessage(data) {
|
|||
|
||||
}
|
||||
|
||||
function trimChatBuffer() {
|
||||
var maxSize = window.CHATMAXSIZE;
|
||||
if (!maxSize || typeof maxSize !== "number")
|
||||
maxSize = parseInt(maxSize || 100, 10) || 100;
|
||||
var buffer = document.getElementById("messagebuffer");
|
||||
var count = buffer.childNodes.length - maxSize;
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
buffer.firstChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function pingMessage(isHighlight) {
|
||||
if (!FOCUSED) {
|
||||
if (!TITLE_BLINK && (USEROPTS.blink_title === "always" ||
|
||||
|
@ -1688,7 +1695,14 @@ function chatOnly() {
|
|||
.click(function () {
|
||||
$("#channeloptions").modal();
|
||||
});
|
||||
$("<span/>").addClass("label label-default pull-right pointer")
|
||||
.text("Emote List")
|
||||
.appendTo($("#chatheader"))
|
||||
.click(function () {
|
||||
EMOTELIST.show();
|
||||
});
|
||||
setVisible("#showchansettings", CLIENT.rank >= 2);
|
||||
|
||||
$("body").addClass("chatOnly");
|
||||
handleWindowResize();
|
||||
}
|
||||
|
@ -1711,7 +1725,7 @@ function handleVideoResize() {
|
|||
var intv, ticks = 0;
|
||||
var resize = function () {
|
||||
if (++ticks > 10) clearInterval(intv);
|
||||
if ($("#ytapiplayer").parent().height() === 0) return;
|
||||
if ($("#ytapiplayer").parent().outerHeight() <= 0) return;
|
||||
clearInterval(intv);
|
||||
|
||||
var responsiveFrame = $("#ytapiplayer").parent();
|
||||
|
@ -1730,7 +1744,7 @@ function handleVideoResize() {
|
|||
$(window).resize(handleWindowResize);
|
||||
handleWindowResize();
|
||||
|
||||
function removeVideo() {
|
||||
function removeVideo(event) {
|
||||
try {
|
||||
PLAYER.setVolume(0);
|
||||
if (PLAYER.type === "rv") {
|
||||
|
@ -1741,6 +1755,7 @@ function removeVideo() {
|
|||
|
||||
$("#videowrap").remove();
|
||||
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-md-12");
|
||||
if (event) event.preventDefault();
|
||||
}
|
||||
|
||||
/* channel administration stuff */
|
||||
|
@ -2514,6 +2529,11 @@ function formatUserPlaylistList() {
|
|||
.attr("title", "Delete playlist")
|
||||
.appendTo(btns)
|
||||
.click(function () {
|
||||
var really = confirm("Are you sure you want to delete" +
|
||||
" this playlist? This cannot be undone.");
|
||||
if (!really) {
|
||||
return;
|
||||
}
|
||||
socket.emit("deletePlaylist", {
|
||||
name: pl.name
|
||||
});
|
||||
|
@ -2581,10 +2601,14 @@ function initPm(user) {
|
|||
var buffer = $("<div/>").addClass("pm-buffer linewrap").appendTo(body);
|
||||
$("<hr/>").appendTo(body);
|
||||
var input = $("<input/>").addClass("form-control pm-input").attr("type", "text")
|
||||
.attr("maxlength", 240)
|
||||
.appendTo(body);
|
||||
|
||||
input.keydown(function (ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
if (CHATTHROTTLE) {
|
||||
return;
|
||||
}
|
||||
var meta = {};
|
||||
var msg = input.val();
|
||||
if (msg.trim() === "") {
|
||||
|
@ -2836,3 +2860,106 @@ function googlePlusSimulator2014(data) {
|
|||
data.contentType = data.meta.gpdirect[q].contentType;
|
||||
return data;
|
||||
}
|
||||
|
||||
function EmoteList() {
|
||||
this.modal = $("#emotelist");
|
||||
this.modal.on("hidden.bs.modal", unhidePlayer);
|
||||
this.table = document.querySelector("#emotelist table");
|
||||
this.cols = 5;
|
||||
this.itemsPerPage = 25;
|
||||
this.emotes = [];
|
||||
this.emoteListChanged = true;
|
||||
this.page = 0;
|
||||
}
|
||||
|
||||
EmoteList.prototype.handleChange = function () {
|
||||
this.emotes = CHANNEL.emotes.slice();
|
||||
if (USEROPTS.emotelist_sort) {
|
||||
this.emotes.sort(function (a, b) {
|
||||
var x = a.name.toLowerCase();
|
||||
var y = b.name.toLowerCase();
|
||||
|
||||
if (x < y) {
|
||||
return -1;
|
||||
} else if (x > y) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.filter) {
|
||||
this.emotes = this.emotes.filter(this.filter);
|
||||
}
|
||||
|
||||
this.paginator = new NewPaginator(this.emotes.length, this.itemsPerPage,
|
||||
this.loadPage.bind(this));
|
||||
var container = document.getElementById("emotelist-paginator-container");
|
||||
container.innerHTML = "";
|
||||
container.appendChild(this.paginator.elem);
|
||||
this.paginator.loadPage(this.page);
|
||||
this.emoteListChanged = false;
|
||||
};
|
||||
|
||||
EmoteList.prototype.show = function () {
|
||||
if (this.emoteListChanged) {
|
||||
this.handleChange();
|
||||
}
|
||||
|
||||
this.modal.modal();
|
||||
};
|
||||
|
||||
EmoteList.prototype.loadPage = function (page) {
|
||||
var tbody = this.table.children[0];
|
||||
tbody.innerHTML = "";
|
||||
|
||||
var row;
|
||||
var start = page * this.itemsPerPage;
|
||||
if (start >= this.emotes.length) return;
|
||||
var end = Math.min(start + this.itemsPerPage, this.emotes.length);
|
||||
var _this = this;
|
||||
|
||||
for (var i = start; i < end; i++) {
|
||||
if ((i - start) % this.cols === 0) {
|
||||
row = document.createElement("tr");
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
(function (emote) {
|
||||
var td = document.createElement("td");
|
||||
td.className = "emote-preview-container";
|
||||
|
||||
// Trick element to vertically align the emote within the container
|
||||
var hax = document.createElement("span");
|
||||
hax.className = "emote-preview-hax";
|
||||
td.appendChild(hax);
|
||||
|
||||
var img = document.createElement("img");
|
||||
img.src = emote.image;
|
||||
img.className = "emote-preview";
|
||||
img.title = emote.name;
|
||||
img.onclick = function () {
|
||||
var val = chatline.value;
|
||||
if (!val) {
|
||||
chatline.value = emote.name;
|
||||
} else {
|
||||
if (!val.charAt(val.length - 1).match(/\s/)) {
|
||||
chatline.value += " ";
|
||||
}
|
||||
chatline.value += emote.name;
|
||||
}
|
||||
|
||||
_this.modal.modal("hide");
|
||||
chatline.focus();
|
||||
};
|
||||
|
||||
td.appendChild(img);
|
||||
row.appendChild(td);
|
||||
})(this.emotes[i]);
|
||||
}
|
||||
|
||||
this.page = page;
|
||||
};
|
||||
|
||||
window.EMOTELIST = new EmoteList();
|
||||
|
|
Loading…
Reference in New Issue