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
|
# YouTube v3 API key
|
||||||
# See https://developers.google.com/youtube/registering_an_application
|
# See https://developers.google.com/youtube/registering_an_application
|
||||||
# Google is closing the v2 API (which allowed anonymous requests) on
|
# YouTube links will not work without this!
|
||||||
# April 20, 2015 so you must register a v3 API key now.
|
# Instructions:
|
||||||
# NOTE: You must generate a Server key under Public API access, NOT a
|
# 1. Go to https://console.developers.google.com/project
|
||||||
# browser key.
|
# 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: ''
|
youtube-v3-key: ''
|
||||||
# Minutes between saving channel state to disk
|
# Minutes between saving channel state to disk
|
||||||
channel-save-interval: 5
|
channel-save-interval: 5
|
||||||
|
@ -202,6 +206,9 @@ channel-blacklist: []
|
||||||
# * ffmpeg must be installed on the server
|
# * ffmpeg must be installed on the server
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
enabled: false
|
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: []
|
link-domain-blacklist: []
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,12 @@ const TYPE_PM = {
|
||||||
meta: "object,optional"
|
meta: "object,optional"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Limit to 10 messages/sec
|
||||||
|
const MIN_ANTIFLOOD = {
|
||||||
|
burst: 20,
|
||||||
|
sustained: 10
|
||||||
|
};
|
||||||
|
|
||||||
function ChatModule(channel) {
|
function ChatModule(channel) {
|
||||||
ChannelModule.apply(this, arguments);
|
ChannelModule.apply(this, arguments);
|
||||||
this.buffer = [];
|
this.buffer = [];
|
||||||
|
@ -192,7 +198,13 @@ ChatModule.prototype.handlePm = function (user, data) {
|
||||||
return;
|
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;
|
var to = null;
|
||||||
for (var i = 0; i < this.channel.users.length; i++) {
|
for (var i = 0; i < this.channel.users.length; i++) {
|
||||||
if (this.channel.users[i].getLowerName() === data.to) {
|
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";
|
meta.addClass = "greentext";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,13 +255,34 @@ ChatModule.prototype.processChatMsg = function (user, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var msgobj = this.formatMessage(user.getName(), data);
|
var msgobj = this.formatMessage(user.getName(), data);
|
||||||
|
var antiflood = MIN_ANTIFLOOD;
|
||||||
if (this.channel.modules.options &&
|
if (this.channel.modules.options &&
|
||||||
this.channel.modules.options.get("chat_antiflood") &&
|
this.channel.modules.options.get("chat_antiflood") &&
|
||||||
user.account.effectiveRank < 2) {
|
user.account.effectiveRank < 2) {
|
||||||
|
|
||||||
var antiflood = this.channel.modules.options.get("chat_antiflood_params");
|
antiflood = this.channel.modules.options.get("chat_antiflood_params");
|
||||||
if (user.chatLimiter.throttle(antiflood)) {
|
}
|
||||||
user.socket.emit("cooldown", 1000 / antiflood.sustained);
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,27 +303,7 @@ ChatModule.prototype.processChatMsg = function (user, data) {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.sendMessage(msgobj);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ChatModule.prototype.formatMessage = function (username, data) {
|
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) {
|
OptionsModule.prototype.save = function (data) {
|
||||||
|
@ -216,11 +221,15 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
|
||||||
b = 1;
|
b = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b = Math.min(20, b);
|
||||||
|
|
||||||
var s = parseFloat(data.chat_antiflood_params.sustained);
|
var s = parseFloat(data.chat_antiflood_params.sustained);
|
||||||
if (isNaN(s) || s <= 0) {
|
if (isNaN(s) || s <= 0) {
|
||||||
s = 1;
|
s = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s = Math.min(10, s);
|
||||||
|
|
||||||
var c = b / s;
|
var c = b / s;
|
||||||
this.opts.chat_antiflood_params = {
|
this.opts.chat_antiflood_params = {
|
||||||
burst: b,
|
burst: b,
|
||||||
|
|
|
@ -100,7 +100,8 @@ var defaults = {
|
||||||
},
|
},
|
||||||
"channel-blacklist": [],
|
"channel-blacklist": [],
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
enabled: false
|
enabled: false,
|
||||||
|
"ffprobe-exec": "ffprobe"
|
||||||
},
|
},
|
||||||
"link-domain-blacklist": [],
|
"link-domain-blacklist": [],
|
||||||
setuid: {
|
setuid: {
|
||||||
|
@ -351,9 +352,8 @@ function preprocessConfig(cfg) {
|
||||||
require("cytube-mediaquery/lib/provider/youtube").setApiKey(
|
require("cytube-mediaquery/lib/provider/youtube").setApiKey(
|
||||||
cfg["youtube-v3-key"]);
|
cfg["youtube-v3-key"]);
|
||||||
} else {
|
} else {
|
||||||
Logger.errlog.log("Warning: No YouTube v3 API key set. YouTube lookups will " +
|
Logger.errlog.log("Warning: No YouTube v3 API key set. YouTube links will " +
|
||||||
"fall back to the v2 API, which is scheduled for closure soon after " +
|
"not work. See youtube-v3-key in config.template.yaml and " +
|
||||||
"April 20, 2015. See " +
|
|
||||||
"https://developers.google.com/youtube/registering_an_application for " +
|
"https://developers.google.com/youtube/registering_an_application for " +
|
||||||
"information on registering an API key.");
|
"information on registering an API key.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,15 @@ var Logger = require("../logger");
|
||||||
var registrationLock = {};
|
var registrationLock = {};
|
||||||
var blackHole = function () { };
|
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 = {
|
module.exports = {
|
||||||
init: function () {
|
init: function () {
|
||||||
},
|
},
|
||||||
|
@ -15,7 +24,7 @@ module.exports = {
|
||||||
* Check if a username is taken
|
* Check if a username is taken
|
||||||
*/
|
*/
|
||||||
isUsernameTaken: function (name, callback) {
|
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) {
|
function (err, rows) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, true);
|
callback(err, true);
|
||||||
|
|
353
lib/ffmpeg.js
353
lib/ffmpeg.js
|
@ -1,6 +1,12 @@
|
||||||
var Logger = require("./logger");
|
var Logger = require("./logger");
|
||||||
var Config = require("./config");
|
var Config = require("./config");
|
||||||
var spawn = require("child_process").spawn;
|
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 = {
|
var acceptedCodecs = {
|
||||||
"mov/h264": true,
|
"mov/h264": true,
|
||||||
|
@ -19,6 +25,169 @@ var audioOnlyContainers = {
|
||||||
"mp3": true
|
"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) {
|
exports.query = function (filename, cb) {
|
||||||
if (!Config.get("ffmpeg.enabled")) {
|
if (!Config.get("ffmpeg.enabled")) {
|
||||||
return cb("Raw file playback is not enabled on this server");
|
return cb("Raw file playback is not enabled on this server");
|
||||||
|
@ -29,135 +198,73 @@ exports.query = function (filename, cb) {
|
||||||
"or HTTPS");
|
"or HTTPS");
|
||||||
}
|
}
|
||||||
|
|
||||||
ffprobe(filename, function (err, meta) {
|
testUrl(filename, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (meta.stderr && meta.stderr.match(/Protocol not found/)) {
|
return cb(err);
|
||||||
return cb("Link uses a protocol unsupported by this server's ffmpeg");
|
}
|
||||||
} else {
|
|
||||||
|
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");
|
return cb("Unable to query file data with ffmpeg");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
meta = parse(meta);
|
if (data.medium === "video") {
|
||||||
if (meta == null) {
|
if (!acceptedCodecs.hasOwnProperty(data.type)) {
|
||||||
return cb("Unknown error");
|
return cb("Unsupported video codec " + data.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(meta)) {
|
data = {
|
||||||
var codec = meta.container + "/" + meta.vcodec;
|
title: data.title || "Raw Video",
|
||||||
|
duration: data.duration,
|
||||||
|
bitrate: data.bitrate,
|
||||||
|
codec: data.type
|
||||||
|
};
|
||||||
|
|
||||||
if (!(codec in acceptedCodecs)) {
|
cb(null, data);
|
||||||
return cb("Unsupported video codec " + codec);
|
} 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 */
|
/* youtube.com */
|
||||||
yt: function (id, callback) {
|
yt: function (id, callback) {
|
||||||
if (!Config.get("youtube-v3-key")) {
|
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 */
|
/* youtube.com playlists */
|
||||||
yp: function (id, callback) {
|
yp: function (id, callback) {
|
||||||
if (!Config.get("youtube-v3-key")) {
|
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) {
|
YouTube.lookupPlaylist(id).then(function (videos) {
|
||||||
|
@ -119,7 +121,8 @@ var Getters = {
|
||||||
/* youtube.com search */
|
/* youtube.com search */
|
||||||
ytSearch: function (query, callback) {
|
ytSearch: function (query, callback) {
|
||||||
if (!Config.get("youtube-v3-key")) {
|
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) {
|
YouTube.search(query).then(function (res) {
|
||||||
|
@ -582,254 +585,6 @@ var Getters = {
|
||||||
var media = new Media(id, title, "--:--", "hb");
|
var media = new Media(id, title, "--:--", "hb");
|
||||||
callback(false, media);
|
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 = {
|
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",
|
"cookie-parser": "^1.3.3",
|
||||||
"csrf": "^2.0.6",
|
"csrf": "^2.0.6",
|
||||||
"cytube-mediaquery": "git://github.com/CyTube/mediaquery",
|
"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": "^4.11.1",
|
||||||
"express-minify": "^0.1.3",
|
"express-minify": "^0.1.3",
|
||||||
"graceful-fs": "^3.0.5",
|
"graceful-fs": "^3.0.5",
|
||||||
|
|
|
@ -21,7 +21,7 @@ html(lang="en")
|
||||||
b.caret
|
b.caret
|
||||||
ul.dropdown-menu
|
ul.dropdown-menu
|
||||||
li: a(href="#" onclick="javascript:chatOnly()") Chat Only
|
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)
|
mixin navloginlogout(cname)
|
||||||
section#mainpage
|
section#mainpage
|
||||||
.container
|
.container
|
||||||
|
@ -56,6 +56,7 @@ html(lang="en")
|
||||||
#controlsrow.row
|
#controlsrow.row
|
||||||
#leftcontrols.col-lg-5.col-md-5
|
#leftcontrols.col-lg-5.col-md-5
|
||||||
button#newpollbtn.btn.btn-sm.btn-default New Poll
|
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
|
#rightcontrols.col-lg-7.col-md-7
|
||||||
#plcontrol.btn-group.pull-left
|
#plcontrol.btn-group.pull-left
|
||||||
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
|
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")
|
input.add-temp(type="checkbox")
|
||||||
| Add as temporary
|
| Add as temporary
|
||||||
| Paste the embed code below and click Next or At End.
|
| 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")
|
textarea#customembed-content.input-block-level.form-control(rows="3")
|
||||||
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
|
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
|
||||||
.vertical-spacer
|
.vertical-spacer
|
||||||
|
@ -173,6 +174,24 @@ html(lang="en")
|
||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-primary(type="button", data-dismiss="modal", onclick="javascript:saveUserOptions()") Save
|
button.btn.btn-primary(type="button", data-dismiss="modal", onclick="javascript:saveUserOptions()") Save
|
||||||
button.btn.btn-default(type="button", data-dismiss="modal") Close
|
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")
|
#channeloptions.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
|
||||||
.modal-dialog
|
.modal-dialog
|
||||||
.modal-content
|
.modal-content
|
||||||
|
|
|
@ -557,15 +557,15 @@ body.chatOnly .pm-panel, body.chatOnly .pm-panel-placeholder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.modal {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
min-width: 600px!important;
|
min-width: 600px!important;
|
||||||
max-width: 1200px!important;
|
max-width: 1200px!important;
|
||||||
width: auto!important;
|
width: auto!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-dialog-nonfluid.modal-dialog {
|
||||||
|
max-width: 600px!important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table td {
|
table td {
|
||||||
|
@ -592,3 +592,36 @@ table td {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-top-width: 0;
|
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;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#emotelist td {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-shadow {
|
.chat-shadow {
|
||||||
color: #aaaaaa;
|
color: #aaaaaa;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ input.form-control[type="email"], textarea.form-control {
|
||||||
color: #c8c8c8;
|
color: #c8c8c8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-box, .user-dropdown {
|
.profile-box, .user-dropdown, #emotelist td {
|
||||||
color: #c8c8c8;
|
color: #c8c8c8;
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@ footer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#emotelist td {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-shadow {
|
.chat-shadow {
|
||||||
color: #aaaaaa;
|
color: #aaaaaa;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,15 @@ input.form-control[type="email"], textarea.form-control {
|
||||||
border-radius: 0px !important;
|
border-radius: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#emotelist table {
|
||||||
|
background-color: #2a2d30;
|
||||||
|
}
|
||||||
|
|
||||||
|
#emotelist td {
|
||||||
|
background-color: rgba(28, 30, 34, 0.95);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-image {
|
.profile-image {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
border: solid 1px #000000 !important;
|
border: solid 1px #000000 !important;
|
||||||
|
|
|
@ -51,7 +51,7 @@ input.form-control[type="email"], textarea.form-control {
|
||||||
background-image: linear-gradient(#484e55, #3a3f44 60%, #313539) !important;
|
background-image: linear-gradient(#484e55, #3a3f44 60%, #313539) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-box, .user-dropdown {
|
.profile-box, .user-dropdown, #emotelist td {
|
||||||
color: #c8c8c8;
|
color: #c8c8c8;
|
||||||
background-color: #161a20;
|
background-color: #161a20;
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,7 @@ Callbacks = {
|
||||||
cooldown: function (time) {
|
cooldown: function (time) {
|
||||||
time = time + 200;
|
time = time + 200;
|
||||||
$("#chatline").css("color", "#ff0000");
|
$("#chatline").css("color", "#ff0000");
|
||||||
|
$(".pm-input").css("color", "#ff0000");
|
||||||
if (CHATTHROTTLE && $("#chatline").data("throttle_timer")) {
|
if (CHATTHROTTLE && $("#chatline").data("throttle_timer")) {
|
||||||
clearTimeout($("#chatline").data("throttle_timer"));
|
clearTimeout($("#chatline").data("throttle_timer"));
|
||||||
}
|
}
|
||||||
|
@ -131,6 +132,7 @@ Callbacks = {
|
||||||
$("#chatline").data("throttle_timer", setTimeout(function () {
|
$("#chatline").data("throttle_timer", setTimeout(function () {
|
||||||
CHATTHROTTLE = false;
|
CHATTHROTTLE = false;
|
||||||
$("#chatline").css("color", "");
|
$("#chatline").css("color", "");
|
||||||
|
$(".pm-input").css("color", "");
|
||||||
}, time));
|
}, time));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -279,7 +281,7 @@ Callbacks = {
|
||||||
channelCSSJS: function(data) {
|
channelCSSJS: function(data) {
|
||||||
$("#chancss").remove();
|
$("#chancss").remove();
|
||||||
CHANNEL.css = data.css;
|
CHANNEL.css = data.css;
|
||||||
$("#csstext").val(data.css);
|
$("#cs-csstext").val(data.css);
|
||||||
if(data.css && !USEROPTS.ignore_channelcss) {
|
if(data.css && !USEROPTS.ignore_channelcss) {
|
||||||
$("<style/>").attr("type", "text/css")
|
$("<style/>").attr("type", "text/css")
|
||||||
.attr("id", "chancss")
|
.attr("id", "chancss")
|
||||||
|
@ -289,7 +291,7 @@ Callbacks = {
|
||||||
|
|
||||||
$("#chanjs").remove();
|
$("#chanjs").remove();
|
||||||
CHANNEL.js = data.js;
|
CHANNEL.js = data.js;
|
||||||
$("#jstext").val(data.js);
|
$("#cs-jstext").val(data.js);
|
||||||
|
|
||||||
if(data.js && !USEROPTS.ignore_channeljs) {
|
if(data.js && !USEROPTS.ignore_channeljs) {
|
||||||
var src = data.js
|
var src = data.js
|
||||||
|
@ -1015,6 +1017,7 @@ Callbacks = {
|
||||||
var tbl = $("#cs-emotes table");
|
var tbl = $("#cs-emotes table");
|
||||||
tbl.data("entries", data);
|
tbl.data("entries", data);
|
||||||
formatCSEmoteList();
|
formatCSEmoteList();
|
||||||
|
EMOTELIST.emoteListChanged = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEmote: function (data) {
|
updateEmote: function (data) {
|
||||||
|
|
|
@ -44,6 +44,7 @@ var IGNORED = [];
|
||||||
var CHATHIST = [];
|
var CHATHIST = [];
|
||||||
var CHATHISTIDX = 0;
|
var CHATHISTIDX = 0;
|
||||||
var CHATTHROTTLE = false;
|
var CHATTHROTTLE = false;
|
||||||
|
var CHATMAXSIZE = 100;
|
||||||
var SCROLLCHAT = true;
|
var SCROLLCHAT = true;
|
||||||
var LASTCHAT = {
|
var LASTCHAT = {
|
||||||
name: ""
|
name: ""
|
||||||
|
@ -115,7 +116,9 @@ var USEROPTS = {
|
||||||
default_quality : getOrDefault("default_quality", "auto"),
|
default_quality : getOrDefault("default_quality", "auto"),
|
||||||
boop : getOrDefault("boop", "never"),
|
boop : getOrDefault("boop", "never"),
|
||||||
secure_connection : getOrDefault("secure_connection", false),
|
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 */
|
/* Backwards compatibility check */
|
||||||
|
|
|
@ -103,3 +103,108 @@
|
||||||
return p;
|
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();
|
applyOpts();
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
var embed = document.querySelector("#videowrap .embed-responsive");
|
||||||
|
if (!embed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window.MutationObserver === "function") {
|
if (typeof window.MutationObserver === "function") {
|
||||||
var mr = new MutationObserver(function (records) {
|
var mr = new MutationObserver(function (records) {
|
||||||
records.forEach(function (record) {
|
records.forEach(function (record) {
|
||||||
|
@ -751,14 +756,39 @@ applyOpts();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mr.observe($("#videowrap").find(".embed-responsive")[0], { childList: true });
|
mr.observe(embed, { childList: true });
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* DOMNodeInserted is deprecated. This code is here only as a fallback
|
* DOMNodeInserted is deprecated. This code is here only as a fallback
|
||||||
* for browsers that do not support MutationObserver
|
* 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();
|
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) {
|
if(USEROPTS.hidevid) {
|
||||||
$("#qualitywrap").html("");
|
|
||||||
removeVideo();
|
removeVideo();
|
||||||
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-lg-12 col-md-12");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#chatbtn").remove();
|
$("#chatbtn").remove();
|
||||||
|
@ -1492,10 +1490,7 @@ function addChatMessage(data) {
|
||||||
div.mouseleave(function() {
|
div.mouseleave(function() {
|
||||||
$(".nick-hover").removeClass("nick-hover");
|
$(".nick-hover").removeClass("nick-hover");
|
||||||
});
|
});
|
||||||
// Cap chatbox at most recent 100 messages
|
trimChatBuffer();
|
||||||
if($("#messagebuffer").children().length > 100) {
|
|
||||||
$($("#messagebuffer").children()[0]).remove();
|
|
||||||
}
|
|
||||||
if(SCROLLCHAT)
|
if(SCROLLCHAT)
|
||||||
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) {
|
function pingMessage(isHighlight) {
|
||||||
if (!FOCUSED) {
|
if (!FOCUSED) {
|
||||||
if (!TITLE_BLINK && (USEROPTS.blink_title === "always" ||
|
if (!TITLE_BLINK && (USEROPTS.blink_title === "always" ||
|
||||||
|
@ -1688,7 +1695,14 @@ function chatOnly() {
|
||||||
.click(function () {
|
.click(function () {
|
||||||
$("#channeloptions").modal();
|
$("#channeloptions").modal();
|
||||||
});
|
});
|
||||||
|
$("<span/>").addClass("label label-default pull-right pointer")
|
||||||
|
.text("Emote List")
|
||||||
|
.appendTo($("#chatheader"))
|
||||||
|
.click(function () {
|
||||||
|
EMOTELIST.show();
|
||||||
|
});
|
||||||
setVisible("#showchansettings", CLIENT.rank >= 2);
|
setVisible("#showchansettings", CLIENT.rank >= 2);
|
||||||
|
|
||||||
$("body").addClass("chatOnly");
|
$("body").addClass("chatOnly");
|
||||||
handleWindowResize();
|
handleWindowResize();
|
||||||
}
|
}
|
||||||
|
@ -1711,7 +1725,7 @@ function handleVideoResize() {
|
||||||
var intv, ticks = 0;
|
var intv, ticks = 0;
|
||||||
var resize = function () {
|
var resize = function () {
|
||||||
if (++ticks > 10) clearInterval(intv);
|
if (++ticks > 10) clearInterval(intv);
|
||||||
if ($("#ytapiplayer").parent().height() === 0) return;
|
if ($("#ytapiplayer").parent().outerHeight() <= 0) return;
|
||||||
clearInterval(intv);
|
clearInterval(intv);
|
||||||
|
|
||||||
var responsiveFrame = $("#ytapiplayer").parent();
|
var responsiveFrame = $("#ytapiplayer").parent();
|
||||||
|
@ -1730,7 +1744,7 @@ function handleVideoResize() {
|
||||||
$(window).resize(handleWindowResize);
|
$(window).resize(handleWindowResize);
|
||||||
handleWindowResize();
|
handleWindowResize();
|
||||||
|
|
||||||
function removeVideo() {
|
function removeVideo(event) {
|
||||||
try {
|
try {
|
||||||
PLAYER.setVolume(0);
|
PLAYER.setVolume(0);
|
||||||
if (PLAYER.type === "rv") {
|
if (PLAYER.type === "rv") {
|
||||||
|
@ -1741,6 +1755,7 @@ function removeVideo() {
|
||||||
|
|
||||||
$("#videowrap").remove();
|
$("#videowrap").remove();
|
||||||
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-md-12");
|
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-md-12");
|
||||||
|
if (event) event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* channel administration stuff */
|
/* channel administration stuff */
|
||||||
|
@ -2514,6 +2529,11 @@ function formatUserPlaylistList() {
|
||||||
.attr("title", "Delete playlist")
|
.attr("title", "Delete playlist")
|
||||||
.appendTo(btns)
|
.appendTo(btns)
|
||||||
.click(function () {
|
.click(function () {
|
||||||
|
var really = confirm("Are you sure you want to delete" +
|
||||||
|
" this playlist? This cannot be undone.");
|
||||||
|
if (!really) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socket.emit("deletePlaylist", {
|
socket.emit("deletePlaylist", {
|
||||||
name: pl.name
|
name: pl.name
|
||||||
});
|
});
|
||||||
|
@ -2581,10 +2601,14 @@ function initPm(user) {
|
||||||
var buffer = $("<div/>").addClass("pm-buffer linewrap").appendTo(body);
|
var buffer = $("<div/>").addClass("pm-buffer linewrap").appendTo(body);
|
||||||
$("<hr/>").appendTo(body);
|
$("<hr/>").appendTo(body);
|
||||||
var input = $("<input/>").addClass("form-control pm-input").attr("type", "text")
|
var input = $("<input/>").addClass("form-control pm-input").attr("type", "text")
|
||||||
|
.attr("maxlength", 240)
|
||||||
.appendTo(body);
|
.appendTo(body);
|
||||||
|
|
||||||
input.keydown(function (ev) {
|
input.keydown(function (ev) {
|
||||||
if (ev.keyCode === 13) {
|
if (ev.keyCode === 13) {
|
||||||
|
if (CHATTHROTTLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var meta = {};
|
var meta = {};
|
||||||
var msg = input.val();
|
var msg = input.val();
|
||||||
if (msg.trim() === "") {
|
if (msg.trim() === "") {
|
||||||
|
@ -2836,3 +2860,106 @@ function googlePlusSimulator2014(data) {
|
||||||
data.contentType = data.meta.gpdirect[q].contentType;
|
data.contentType = data.meta.gpdirect[q].contentType;
|
||||||
return data;
|
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