From 566c4c174e996aff149e7d77ac871da7446a6623 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 20 Apr 2015 16:09:21 -0400 Subject: [PATCH 01/39] make sure the user understand this is for real --- www/js/util.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/js/util.js b/www/js/util.js index b24387de..38b86088 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2514,6 +2514,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 }); From e2c3b2daad4f4dde8b79e16c2949ed61e57118fa Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 23 Apr 2015 21:49:01 -0500 Subject: [PATCH 02/39] Fix PM maxlength and throttling --- lib/channel/chat.js | 31 ++++++++++++++++++++++--------- www/js/callbacks.js | 2 ++ www/js/util.js | 4 ++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/channel/chat.js b/lib/channel/chat.js index d7527344..8c10cd61 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -22,6 +22,12 @@ const TYPE_PM = { meta: "object,optional" }; +const DEFAULT_ANTIFLOOD = { + burst: 4, + sustained: 1, + cooldown: 4 +}; + 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(DEFAULT_ANTIFLOOD)) { + user.socket.emit("cooldown", 1000 / DEFAULT_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,15 +255,16 @@ ChatModule.prototype.processChatMsg = function (user, data) { } var msgobj = this.formatMessage(user.getName(), data); + var antiflood = DEFAULT_ANTIFLOOD; if (this.channel.modules.options && - this.channel.modules.options.get("chat_antiflood") && - user.account.effectiveRank < 2) { + this.channel.modules.options.get("chat_antiflood")) { - var antiflood = this.channel.modules.options.get("chat_antiflood_params"); - if (user.chatLimiter.throttle(antiflood)) { - user.socket.emit("cooldown", 1000 / antiflood.sustained); - return; - } + antiflood = this.channel.modules.options.get("chat_antiflood_params"); + } + + if (user.chatLimiter.throttle(antiflood)) { + user.socket.emit("cooldown", 1000 / antiflood.sustained); + return; } if (user.is(Flags.U_SMUTED)) { diff --git a/www/js/callbacks.js b/www/js/callbacks.js index 25e0a804..1314ace3 100644 --- a/www/js/callbacks.js +++ b/www/js/callbacks.js @@ -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)); }, diff --git a/www/js/util.js b/www/js/util.js index 38b86088..0e97838c 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2586,10 +2586,14 @@ function initPm(user) { var buffer = $("
").addClass("pm-buffer linewrap").appendTo(body); $("
").appendTo(body); var 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() === "") { From 7debb7afa7b439d525bf701bafe3a4e6330f950c Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 27 Apr 2015 12:21:21 -0500 Subject: [PATCH 03/39] Update limits for chat_antiflood_params --- lib/channel/chat.js | 14 +++++++------- lib/channel/opts.js | 9 +++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/channel/chat.js b/lib/channel/chat.js index 8c10cd61..539a6b97 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -22,10 +22,10 @@ const TYPE_PM = { meta: "object,optional" }; -const DEFAULT_ANTIFLOOD = { - burst: 4, - sustained: 1, - cooldown: 4 +// Limit to 10 messages/sec +const MIN_ANTIFLOOD = { + burst: 10, + sustained: 10 }; function ChatModule(channel) { @@ -198,8 +198,8 @@ ChatModule.prototype.handlePm = function (user, data) { return; } - if (user.chatLimiter.throttle(DEFAULT_ANTIFLOOD)) { - user.socket.emit("cooldown", 1000 / DEFAULT_ANTIFLOOD.sustained); + if (user.chatLimiter.throttle(MIN_ANTIFLOOD)) { + user.socket.emit("cooldown", 1000 / MIN_ANTIFLOOD.sustained); return; } @@ -255,7 +255,7 @@ ChatModule.prototype.processChatMsg = function (user, data) { } var msgobj = this.formatMessage(user.getName(), data); - var antiflood = DEFAULT_ANTIFLOOD; + var antiflood = MIN_ANTIFLOOD; if (this.channel.modules.options && this.channel.modules.options.get("chat_antiflood")) { diff --git a/lib/channel/opts.js b/lib/channel/opts.js index acdde7a0..45455324 100644 --- a/lib/channel/opts.js +++ b/lib/channel/opts.js @@ -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, From 7782ba4ae519784adda3967da1c38360d3ee8662 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 27 Apr 2015 12:22:52 -0500 Subject: [PATCH 04/39] Subject moderators to MIN_ANTIFLOOD rather than channel limit --- lib/channel/chat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/channel/chat.js b/lib/channel/chat.js index 539a6b97..4b9109d5 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -257,7 +257,8 @@ 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")) { + this.channel.modules.options.get("chat_antiflood") && + user.account.effectiveRank < 2) { antiflood = this.channel.modules.options.get("chat_antiflood_params"); } From 1ff9f5648b2d85dfa14095818adcfe7059aaff3f Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 27 Apr 2015 13:09:32 -0500 Subject: [PATCH 05/39] Update MIN_ANTIFLOOD to be the same as the limits for channel settings --- lib/channel/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/channel/chat.js b/lib/channel/chat.js index 4b9109d5..a6e71b7b 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -24,7 +24,7 @@ const TYPE_PM = { // Limit to 10 messages/sec const MIN_ANTIFLOOD = { - burst: 10, + burst: 20, sustained: 10 }; From 2dd1db166aa18202f0001d1134315f547170edb9 Mon Sep 17 00:00:00 2001 From: Xaekai Date: Wed, 6 May 2015 12:14:56 -0700 Subject: [PATCH 06/39] Fix annoyance: Clicking Remove video scrolls to top --- templates/channel.jade | 2 +- www/js/util.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/channel.jade b/templates/channel.jade index c0f1aa9d..b1b953b8 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -20,7 +20,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 diff --git a/www/js/util.js b/www/js/util.js index 0e97838c..dfd1e0b8 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -1730,7 +1730,7 @@ function handleVideoResize() { $(window).resize(handleWindowResize); handleWindowResize(); -function removeVideo() { +function removeVideo(event) { try { PLAYER.setVolume(0); if (PLAYER.type === "rv") { @@ -1741,6 +1741,7 @@ function removeVideo() { $("#videowrap").remove(); $("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-md-12"); + if (event) event.preventDefault(); } /* channel administration stuff */ From 73fc5dd72481601068c85726d2aa27990b12ad67 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 10 May 2015 23:02:24 -0500 Subject: [PATCH 07/39] Fix ffprobe title detection --- lib/ffmpeg.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 276e85b2..2ed885e5 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -112,8 +112,8 @@ function parse(meta) { 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; + if (meta.format["tag:title"]) { + data.title = meta.format["tag:title"]; } data.seconds = Math.ceil(parseFloat(meta.format.duration)); return data; @@ -151,7 +151,7 @@ function ffprobe(filename, cb) { data = {}; } else { var kv = line.split("="); - data[kv[0]] = kv[1]; + data[kv[0].toLowerCase()] = kv[1]; } }); From 389dd0d5abb25a0d26113a4c5401b3956c6cfffb Mon Sep 17 00:00:00 2001 From: calzoneman Date: Tue, 12 May 2015 13:50:59 -0500 Subject: [PATCH 08/39] Initial emote list implementation --- templates/channel.jade | 12 +++ www/css/cytube.css | 37 ++++++++- www/css/themes/bootstrap-theme.min.css | 6 +- www/css/themes/cyborg.css | 2 +- www/css/themes/light.css | 6 +- www/css/themes/modern.css | 4 +- www/css/themes/slate.css | 2 +- www/js/callbacks.js | 1 + www/js/paginator.js | 104 +++++++++++++++++++++++++ www/js/ui.js | 4 + www/js/util.js | 88 +++++++++++++++++++++ 11 files changed, 256 insertions(+), 10 deletions(-) diff --git a/templates/channel.jade b/templates/channel.jade index b1b953b8..f5eb1968 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -55,6 +55,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") @@ -172,6 +173,17 @@ 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 + #emotelist-paginator-container + table + tbody + .modal-footer #channeloptions.modal.fade(tabindex="-1", role="dialog", aria-hidden="true") .modal-dialog .modal-content diff --git a/www/css/cytube.css b/www/css/cytube.css index 8441008e..1317c420 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -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,32 @@ 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; +} diff --git a/www/css/themes/bootstrap-theme.min.css b/www/css/themes/bootstrap-theme.min.css index 54daf5ae..52e83bba 100644 --- a/www/css/themes/bootstrap-theme.min.css +++ b/www/css/themes/bootstrap-theme.min.css @@ -28,10 +28,14 @@ footer { background-color: #eeeeee !important; } -.profile-box, .user-dropdown { +.profile-box, .user-dropdown, #emotelist td { background-color: #ffffff; } +#emotelist td { + border: 1px solid #cccccc; +} + .chat-shadow { color: #aaaaaa; } diff --git a/www/css/themes/cyborg.css b/www/css/themes/cyborg.css index 14a6050f..cde33171 100644 --- a/www/css/themes/cyborg.css +++ b/www/css/themes/cyborg.css @@ -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; } diff --git a/www/css/themes/light.css b/www/css/themes/light.css index 5fc33777..2c916648 100644 --- a/www/css/themes/light.css +++ b/www/css/themes/light.css @@ -22,10 +22,14 @@ footer { background-color: #eeeeee !important; } -.profile-box, .user-dropdown { +.profile-box, .user-dropdown, #emotelist td { background-color: #ffffff; } +#emotelist td { + border: 1px solid #cccccc; +} + .chat-shadow { color: #aaaaaa; } diff --git a/www/css/themes/modern.css b/www/css/themes/modern.css index 6673f22c..119a018f 100644 --- a/www/css/themes/modern.css +++ b/www/css/themes/modern.css @@ -39,7 +39,7 @@ input.form-control[type="email"], textarea.form-control { background-color: rgba(65, 69, 74, 0.7) !important; } -.profile-box, .user-dropdown { +.profile-box, .user-dropdown, #emotelist td { color: #c8c8c8; background-color: rgba(28, 30, 34, 0.95); border: 1px solid #000000 !important; @@ -176,4 +176,4 @@ input.form-control[type="email"], textarea.form-control { margin-bottom: 9px; min-height: 20px; padding: 10px 19px !important; -} \ No newline at end of file +} diff --git a/www/css/themes/slate.css b/www/css/themes/slate.css index 0c46cda1..2e309158 100644 --- a/www/css/themes/slate.css +++ b/www/css/themes/slate.css @@ -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; } diff --git a/www/js/callbacks.js b/www/js/callbacks.js index 1314ace3..fada290f 100644 --- a/www/js/callbacks.js +++ b/www/js/callbacks.js @@ -1028,6 +1028,7 @@ Callbacks = { var tbl = $("#cs-emotes table"); tbl.data("entries", data); formatCSEmoteList(); + EMOTELIST.emoteListChanged = true; }, updateEmote: function (data) { diff --git a/www/js/paginator.js b/www/js/paginator.js index 84a27677..99a71baf 100644 --- a/www/js/paginator.js +++ b/www/js/paginator.js @@ -103,3 +103,107 @@ 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); + 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); + } +}; diff --git a/www/js/ui.js b/www/js/ui.js index deb79303..c0f8d68a 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -762,3 +762,7 @@ applyOpts(); }); } })(); + +$("#emotelistbtn").click(function () { + EMOTELIST.show(); +}); diff --git a/www/js/util.js b/www/js/util.js index dfd1e0b8..24be452d 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2846,3 +2846,91 @@ function googlePlusSimulator2014(data) { data.contentType = data.meta.gpdirect[q].contentType; return data; } + +function EmoteList() { + this.modal = $("#emotelist"); + this.modal.on("hiddn.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.show = function () { + if (this.emoteListChanged) { + this.emotes = CHANNEL.emotes.slice().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; + } + }); + 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; + } + + 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 - 1); + 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 || val.charAt(val.length - 1).match(/\s/)) { + chatline.value = emote.name; + } else { + 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(); From 691ec3055cd4fbf4d709a9fbfff8fb11ed3ff06f Mon Sep 17 00:00:00 2001 From: calzoneman Date: Tue, 12 May 2015 13:53:19 -0500 Subject: [PATCH 09/39] Fix emote insertion behavior --- www/js/util.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/www/js/util.js b/www/js/util.js index 24be452d..391d6d2f 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2915,9 +2915,12 @@ EmoteList.prototype.loadPage = function (page) { img.title = emote.name; img.onclick = function () { var val = chatline.value; - if (!val || val.charAt(val.length - 1).match(/\s/)) { + if (!val) { chatline.value = emote.name; } else { + if (!val.charAt(val.length - 1).match(/\s/)) { + chatline.value += " "; + } chatline.value += " " + emote.name; } From d3e2433ee639923c49741448b0d5571ef3ff36fc Mon Sep 17 00:00:00 2001 From: calzoneman Date: Tue, 12 May 2015 18:24:03 -0500 Subject: [PATCH 10/39] Fix emote background on light themes --- www/css/themes/bootstrap-theme.min.css | 4 ++-- www/css/themes/light.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/css/themes/bootstrap-theme.min.css b/www/css/themes/bootstrap-theme.min.css index 52e83bba..29fbaabd 100644 --- a/www/css/themes/bootstrap-theme.min.css +++ b/www/css/themes/bootstrap-theme.min.css @@ -28,12 +28,12 @@ footer { background-color: #eeeeee !important; } -.profile-box, .user-dropdown, #emotelist td { +.profile-box, .user-dropdown { background-color: #ffffff; } #emotelist td { - border: 1px solid #cccccc; + background-color: #f0f0f0; } .chat-shadow { diff --git a/www/css/themes/light.css b/www/css/themes/light.css index 2c916648..d097a62b 100644 --- a/www/css/themes/light.css +++ b/www/css/themes/light.css @@ -22,12 +22,12 @@ footer { background-color: #eeeeee !important; } -.profile-box, .user-dropdown, #emotelist td { +.profile-box, .user-dropdown { background-color: #ffffff; } #emotelist td { - border: 1px solid #cccccc; + background-color: #f0f0f0; } .chat-shadow { From 8927613da738566e29dc6effdc7c19554fdc9ecb Mon Sep 17 00:00:00 2001 From: calzoneman Date: Wed, 13 May 2015 12:17:32 -0500 Subject: [PATCH 11/39] Add emote search, sort toggle --- templates/channel.jade | 7 +++++++ www/css/themes/modern.css | 11 ++++++++++- www/js/data.js | 3 ++- www/js/paginator.js | 1 + www/js/ui.js | 21 +++++++++++++++++++++ www/js/util.js | 34 +++++++++++++++++++++++----------- 6 files changed, 64 insertions(+), 13 deletions(-) diff --git a/templates/channel.jade b/templates/channel.jade index f5eb1968..305d162f 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -180,6 +180,13 @@ html(lang="en") button.close(data-dismiss="modal", aria-hidden="true") × h4 Emote List .modal-body + form.form-inline(action="javascript:void(0)") + .form-group + input#emotelist-search.form-control(type="text", placeholder="Search") + .checkbox + label + input#emotelist-alphabetical(type="checkbox") + | Sort alphabetically #emotelist-paginator-container table tbody diff --git a/www/css/themes/modern.css b/www/css/themes/modern.css index 119a018f..035cef9d 100644 --- a/www/css/themes/modern.css +++ b/www/css/themes/modern.css @@ -39,13 +39,22 @@ input.form-control[type="email"], textarea.form-control { background-color: rgba(65, 69, 74, 0.7) !important; } -.profile-box, .user-dropdown, #emotelist td { +.profile-box, .user-dropdown { color: #c8c8c8; background-color: rgba(28, 30, 34, 0.95); border: 1px solid #000000 !important; 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; diff --git a/www/js/data.js b/www/js/data.js index 7f29ddd0..9076a31b 100644 --- a/www/js/data.js +++ b/www/js/data.js @@ -114,7 +114,8 @@ var USEROPTS = { default_quality : getOrDefault("default_quality", ""), 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) }; /* Backwards compatibility check */ diff --git a/www/js/paginator.js b/www/js/paginator.js index 99a71baf..720f7708 100644 --- a/www/js/paginator.js +++ b/www/js/paginator.js @@ -161,6 +161,7 @@ NewPaginator.prototype.loadButtons = function (page) { } 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) { diff --git a/www/js/ui.js b/www/js/ui.js index c0f8d68a..f830fd46 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -766,3 +766,24 @@ applyOpts(); $("#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); +}); diff --git a/www/js/util.js b/www/js/util.js index 391d6d2f..5478dfa1 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2858,9 +2858,10 @@ function EmoteList() { this.page = 0; } -EmoteList.prototype.show = function () { - if (this.emoteListChanged) { - this.emotes = CHANNEL.emotes.slice().sort(function (a, b) { +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(); @@ -2872,13 +2873,24 @@ EmoteList.prototype.show = function () { return 0; } }); - 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; + } + + 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(); @@ -2891,7 +2903,7 @@ EmoteList.prototype.loadPage = function (page) { var row; var start = page * this.itemsPerPage; if (start >= this.emotes.length) return; - var end = Math.min(start + this.itemsPerPage, this.emotes.length - 1); + var end = Math.min(start + this.itemsPerPage, this.emotes.length); var _this = this; for (var i = start; i < end; i++) { From 86bd20d5cce1afda9711e2b4d6321c63439bc1e3 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Wed, 13 May 2015 12:19:03 -0500 Subject: [PATCH 12/39] Minor fix for emote insertion --- www/js/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/util.js b/www/js/util.js index 5478dfa1..f97c5350 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2933,7 +2933,7 @@ EmoteList.prototype.loadPage = function (page) { if (!val.charAt(val.length - 1).match(/\s/)) { chatline.value += " "; } - chatline.value += " " + emote.name; + chatline.value += emote.name; } _this.modal.modal("hide"); From 2c90d289191f9e4cf02da4b72033869125d8775d Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 14 May 2015 11:25:31 -0500 Subject: [PATCH 13/39] Tweak checkbox placement --- templates/channel.jade | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/channel.jade b/templates/channel.jade index 305d162f..b2fea8e6 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -180,9 +180,9 @@ html(lang="en") button.close(data-dismiss="modal", aria-hidden="true") × h4 Emote List .modal-body - form.form-inline(action="javascript:void(0)") - .form-group - input#emotelist-search.form-control(type="text", placeholder="Search") + .pull-left + input#emotelist-search.form-control(type="text", placeholder="Search") + .pull-right .checkbox label input#emotelist-alphabetical(type="checkbox") From c64d446b38fa6ffcbd3f49e9d1f0edd8ebea7c05 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 14 May 2015 11:37:02 -0500 Subject: [PATCH 14/39] Fix button spacing when HTML is minified --- templates/channel.jade | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/channel.jade b/templates/channel.jade index b2fea8e6..ed0bbe5b 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -55,6 +55,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 From 8b69485448e1338117010c1df4ef89a110abd575 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 14 May 2015 11:42:26 -0500 Subject: [PATCH 15/39] Show emote list button in chat only --- www/js/util.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/js/util.js b/www/js/util.js index f97c5350..070039bc 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -1688,7 +1688,14 @@ function chatOnly() { .click(function () { $("#channeloptions").modal(); }); + $("").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(); } From 7bc247ede2a58e2dced6000c5c0bb26b1c3a152f Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 14 May 2015 13:14:45 -0500 Subject: [PATCH 16/39] Fix 'remove video' option --- www/js/ui.js | 9 +++++++-- www/js/util.js | 4 +--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/www/js/ui.js b/www/js/ui.js index f830fd46..9a76fd7d 100644 --- a/www/js/ui.js +++ b/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,13 +756,13 @@ 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(); }); } diff --git a/www/js/util.js b/www/js/util.js index 070039bc..033d7af0 100644 --- a/www/js/util.js +++ b/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(); @@ -1718,7 +1716,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(); From 88be0e1e92901892092a8268934fdef76597e45e Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sat, 16 May 2015 23:36:04 -0500 Subject: [PATCH 17/39] Don't crash if ffprobe is missing --- lib/ffmpeg.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 2ed885e5..b2739fb7 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -31,9 +31,12 @@ exports.query = function (filename, cb) { ffprobe(filename, function (err, meta) { if (err) { - if (meta.stderr && meta.stderr.match(/Protocol not found/)) { + if (meta && meta.stderr && meta.stderr.match(/Protocol not found/)) { return cb("Link uses a protocol unsupported by this server's ffmpeg"); + } else if (err.code && err.code === "ENOENT") { + return cb("Server is missing ffprobe"); } else { + Logger.errlog.log(err.stack || err); return cb("Unable to query file data with ffmpeg"); } } @@ -120,7 +123,11 @@ function parse(meta) { } function ffprobe(filename, cb) { + var err; var ff = spawn("ffprobe", ["-show_streams", "-show_format", filename]); + ff.on("error", function (err_) { + err = err_; + }); var outbuf = ""; var errbuf = ""; @@ -133,7 +140,10 @@ function ffprobe(filename, cb) { ff.on("close", function (code) { if (code !== 0) { - return cb("ffprobe exited with nonzero exit code", { stderr: errbuf }); + if (!err) { + err = "ffprobe exited with nonzero exit code"; + } + return cb(err, { stderr: errbuf }); } var lines = outbuf.split("\n"); From 7f269784b15369304ca5f2de3e0ad8332dc74a4c Mon Sep 17 00:00:00 2001 From: Xaekai Date: Tue, 19 May 2015 00:03:02 -0700 Subject: [PATCH 18/39] Fix shadowmuted users usage of greentext and slash commands --- lib/channel/chat.js | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/channel/chat.js b/lib/channel/chat.js index a6e71b7b..a395d521 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -268,6 +268,25 @@ ChatModule.prototype.processChatMsg = function (user, data) { 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; + } + } + if (user.is(Flags.U_SMUTED)) { this.shadowMutedUsers().forEach(function (u) { u.socket.emit("chatMsg", msgobj); @@ -284,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) { From 5f1f985dd05fc61ceb21554a08d22f2d3399b958 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Tue, 19 May 2015 19:48:08 -0400 Subject: [PATCH 19/39] Rewrite ffmpeg module --- config.template.yaml | 3 + lib/config.js | 3 +- lib/ffmpeg.js | 256 ++++++++++++++++++++++++------------------- 3 files changed, 150 insertions(+), 112 deletions(-) diff --git a/config.template.yaml b/config.template.yaml index 85ecd3d8..4da9d424 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -202,6 +202,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: [] diff --git a/lib/config.js b/lib/config.js index d549fe0a..9f49afe1 100644 --- a/lib/config.js +++ b/lib/config.js @@ -100,7 +100,8 @@ var defaults = { }, "channel-blacklist": [], ffmpeg: { - enabled: false + enabled: false, + "ffprobe-exec": "ffprobe" }, "link-domain-blacklist": [], setuid: { diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index b2739fb7..b044b746 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -2,6 +2,8 @@ var Logger = require("./logger"); var Config = require("./config"); var spawn = require("child_process").spawn; +var SUPPORTS_JSON = true; + var acceptedCodecs = { "mov/h264": true, "flv/h264": true, @@ -19,6 +21,124 @@ var audioOnlyContainers = { "mp3": true }; +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.title; + 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) { + 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 (SUPPORTS_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(/-of/)) { + Logger.errlog.log("Warning: ffprobe does not support -of json. " + + "Assuming it will have old output format."); + SUPPORTS_JSON = false; + return ffprobe(filename, cb); + } + + if (!childErr) childErr = new Error(stderr); + return cb(childErr); + } + + var result; + if (SUPPORTS_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,56 +149,55 @@ exports.query = function (filename, cb) { "or HTTPS"); } - ffprobe(filename, function (err, meta) { + exports.ffprobe(filename, function (err, data) { if (err) { - if (meta && meta.stderr && meta.stderr.match(/Protocol not found/)) { + if (err.message && err.message.match(/protocol not found/i)) { return cb("Link uses a protocol unsupported by this server's ffmpeg"); } else if (err.code && err.code === "ENOENT") { - return cb("Server is missing ffprobe"); + 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 { Logger.errlog.log(err.stack || err); return cb("Unable to query file data with ffmpeg"); } } - meta = parse(meta); - if (meta == null) { - return cb("Unknown error"); + try { + data = reformatData(data); + } catch (e) { + Logger.errlog.log(err.stack || err); + return cb("Unable to query file data with ffmpeg"); } - if (isVideo(meta)) { - var codec = meta.container + "/" + meta.vcodec; - - if (!(codec in acceptedCodecs)) { - return cb("Unsupported video codec " + codec); + if (data.medium === "video") { + if (!acceptedCodecs.hasOwnProperty(data.type)) { + return cb("Unsupported video codec " + data.type); } - var data = { - title: meta.title || "Raw Video", - duration: Math.ceil(meta.seconds) || "--:--", - bitrate: meta.bitrate, - codec: codec + data = { + title: data.title || "Raw Video", + duration: data.duration, + bitrate: data.bitrate, + codec: data.type }; cb(null, data); - } else if (isAudio(meta)) { + } else if (data.medium === "audio") { var codec = meta.acodec; - if (!(codec in acceptedAudioCodecs)) { - return cb("Unsupported audio codec " + codec); + if (!acceptedAudioCodecs.hasOwnProperty(data.type)) { + return cb("Unsupported audio codec " + data.type); } - var data = { - title: meta.title || "Raw Audio", - duration: Math.ceil(meta.seconds) || "--:--", - bitrate: meta.bitrate, + data = { + title: data.title || "Raw Audio", + duration: data.duration, + bitrate: data.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 " + @@ -86,88 +205,3 @@ exports.query = function (filename, cb) { } }); }; - -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["tag:title"]) { - data.title = meta.format["tag:title"]; - } - data.seconds = Math.ceil(parseFloat(meta.format.duration)); - return data; -} - -function ffprobe(filename, cb) { - var err; - var ff = spawn("ffprobe", ["-show_streams", "-show_format", filename]); - ff.on("error", function (err_) { - err = err_; - }); - - 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) { - if (!err) { - err = "ffprobe exited with nonzero exit code"; - } - return cb(err, { 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].toLowerCase()] = kv[1]; - } - }); - - cb(null, { - streams: streams, - format: format - }); - }); -} From 54f2ad7c5cada197fa038847d200188d927cf763 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Tue, 19 May 2015 22:07:55 -0400 Subject: [PATCH 20/39] Fixes --- lib/ffmpeg.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index b044b746..972af6c8 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -2,7 +2,7 @@ var Logger = require("./logger"); var Config = require("./config"); var spawn = require("child_process").spawn; -var SUPPORTS_JSON = true; +var USE_JSON = true; var acceptedCodecs = { "mov/h264": true, @@ -62,13 +62,13 @@ function reformatData(data) { if (isNaN(bitrate)) bitrate = 0; reformatted.bitrate = bitrate; - reformatted.title = data.format.tags.title; + 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) { + if (!reformatted.title && stream.tags) { reformatted.title = stream.tags.title; } } else if (stream.codec_type === "audio") { @@ -90,7 +90,7 @@ function reformatData(data) { exports.ffprobe = function ffprobe(filename, cb) { var childErr; var args = ["-show_streams", "-show_format", filename]; - if (SUPPORTS_JSON) args = ["-of", "json"].concat(args); + if (USE_JSON) args = ["-of", "json"].concat(args); var child = spawn(Config.get("ffmpeg.ffprobe-exec"), args); var stdout = ""; var stderr = ""; @@ -109,10 +109,10 @@ exports.ffprobe = function ffprobe(filename, cb) { child.on("close", function (code) { if (code !== 0) { - if (stderr.match(/-of/)) { + 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."); - SUPPORTS_JSON = false; + USE_JSON = false; return ffprobe(filename, cb); } @@ -121,7 +121,7 @@ exports.ffprobe = function ffprobe(filename, cb) { } var result; - if (SUPPORTS_JSON) { + if (USE_JSON) { try { result = JSON.parse(stdout); } catch (e) { @@ -166,7 +166,7 @@ exports.query = function (filename, cb) { try { data = reformatData(data); } catch (e) { - Logger.errlog.log(err.stack || err); + Logger.errlog.log(e.stack || e); return cb("Unable to query file data with ffmpeg"); } @@ -184,17 +184,15 @@ exports.query = function (filename, cb) { cb(null, data); } else if (data.medium === "audio") { - var codec = meta.acodec; - - if (!acceptedAudioCodecs.hasOwnProperty(data.type)) { - return cb("Unsupported audio codec " + data.type); + 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: codec + codec: data.acodec }; cb(null, data); From 35b2920c52603a27116f01dd94ccf79e0ccb133c Mon Sep 17 00:00:00 2001 From: Anthony Parsons Date: Thu, 21 May 2015 17:48:00 +0100 Subject: [PATCH 21/39] Fix a typo --- www/js/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/util.js b/www/js/util.js index 033d7af0..761c4552 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2854,7 +2854,7 @@ function googlePlusSimulator2014(data) { function EmoteList() { this.modal = $("#emotelist"); - this.modal.on("hiddn.bs.modal", unhidePlayer); + this.modal.on("hidden.bs.modal", unhidePlayer); this.table = document.querySelector("#emotelist table"); this.cols = 5; this.itemsPerPage = 25; From 9081a5fee1693fd4c6cc11477844c34495aa2ff3 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 21 May 2015 13:13:27 -0400 Subject: [PATCH 22/39] package.json: bump cytubefilters --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f273aa26..e0ac463c 100644 --- a/package.json +++ b/package.json @@ -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#f4bb2efd", "express": "^4.11.1", "express-minify": "^0.1.3", "graceful-fs": "^3.0.5", From f94c8bc8f1e605d8f460bad7ab8fbeb3784bf0fc Mon Sep 17 00:00:00 2001 From: calzoneman Date: Fri, 22 May 2015 10:29:24 -0400 Subject: [PATCH 23/39] Replace   with CSS margin --- templates/channel.jade | 1 - www/css/cytube.css | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/channel.jade b/templates/channel.jade index ed0bbe5b..b2fea8e6 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -55,7 +55,6 @@ 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 diff --git a/www/css/cytube.css b/www/css/cytube.css index 1317c420..12f53b2a 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -621,3 +621,7 @@ table td { #emotelist-paginator-container { text-align: center; } + +#leftcontrols .btn { + margin-right: 5px; +} From cd0cc69fd856bc9e8a69141f23d30a87383fa869 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sat, 23 May 2015 14:36:13 -0400 Subject: [PATCH 24/39] Gracefully handle HTTP errors in ffprobe --- lib/ffmpeg.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 972af6c8..602c9cfe 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -151,12 +151,19 @@ exports.query = function (filename, cb) { exports.ffprobe(filename, function (err, data) { if (err) { - if (err.message && err.message.match(/protocol not found/i)) { - return cb("Link uses a protocol unsupported by this server's ffmpeg"); - } else if (err.code && err.code === "ENOENT") { + 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 ffmpeg"); + + var m = err.message.match(/(http error .*)/i); + if (m) return cb(m[1]); + + Logger.errlog.log(err.stack || err); + return cb("Unable to query file data with ffmpeg"); } else { Logger.errlog.log(err.stack || err); return cb("Unable to query file data with ffmpeg"); From c4add8f14211d884ea11900f19bf31fb39026e8a Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:06:02 -0400 Subject: [PATCH 25/39] Preflight raw file requests to get better error messages --- lib/ffmpeg.js | 155 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 53 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 602c9cfe..e81a7345 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -1,6 +1,9 @@ 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 USE_JSON = true; @@ -21,6 +24,47 @@ var audioOnlyContainers = { "mp3": true }; +function testUrl(url, cb, redirected) { + 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 (redirected) { + return cb("Too many redirects. Please provide a direct link to the " + "file"); + } + return testUrl(res.headers['location'], cb, true); + } + + if (res.statusCode !== 200) { + return cb("HTTP " + res.statusCode + " " + res.statusMessage); + } + + if (!/^audio|^video/.test(res.headers['content-type'])) { + return cb("Server did not return an audio or video file"); + } + + cb(); + }); + + req.on("error", function (err) { + cb(err); + }); + + req.end(); +} + function readOldFormat(buf) { var lines = buf.split("\n"); var tmp = { tags: {} }; @@ -149,64 +193,69 @@ exports.query = function (filename, cb) { "or HTTPS"); } - exports.ffprobe(filename, function (err, data) { + testUrl(filename, function (err) { 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 ffmpeg"); + return cb(err); + } - var m = err.message.match(/(http error .*)/i); - if (m) return cb(m[1]); + 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"); - Logger.errlog.log(err.stack || err); + Logger.errlog.log(err.stack || err); + return cb("Unable to query file data with ffmpeg"); + } else { + 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"); + } + + if (data.medium === "video") { + if (!acceptedCodecs.hasOwnProperty(data.type)) { + return cb("Unsupported video codec " + data.type); + } + + data = { + title: data.title || "Raw Video", + duration: data.duration, + bitrate: data.bitrate, + codec: data.type + }; + + 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 { - Logger.errlog.log(err.stack || err); - return cb("Unable to query file data with ffmpeg"); + 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."); } - } - - try { - data = reformatData(data); - } catch (e) { - Logger.errlog.log(e.stack || e); - return cb("Unable to query file data with ffmpeg"); - } - - if (data.medium === "video") { - if (!acceptedCodecs.hasOwnProperty(data.type)) { - return cb("Unsupported video codec " + data.type); - } - - data = { - title: data.title || "Raw Video", - duration: data.duration, - bitrate: data.bitrate, - codec: data.type - }; - - 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."); - } + }); }); }; From 334c0d933be12f5aac56948a47ccbfebc1c806cd Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:09:56 -0400 Subject: [PATCH 26/39] Fix typo --- lib/ffmpeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index e81a7345..ce16cad6 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -41,7 +41,7 @@ function testUrl(url, cb, redirected) { if (res.statusCode === 301 || res.statusCode === 302) { if (redirected) { - return cb("Too many redirects. Please provide a direct link to the " + return cb("Too many redirects. Please provide a direct link to the " + "file"); } return testUrl(res.headers['location'], cb, true); From 18199b32ad5dc9b62440a14d0c50af7db5139fb3 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:19:59 -0400 Subject: [PATCH 27/39] Add status message map for pre-node v0.12 servers --- lib/ffmpeg.js | 5 +++- lib/status-messages.js | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 lib/status-messages.js diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index ce16cad6..ee6d664c 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -4,6 +4,7 @@ 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; @@ -48,7 +49,9 @@ function testUrl(url, cb, redirected) { } if (res.statusCode !== 200) { - return cb("HTTP " + res.statusCode + " " + res.statusMessage); + var message = statusMessages[res.statusCode]; + if (!message) message = ""; + return cb("HTTP " + res.statusCode + " " + message); } if (!/^audio|^video/.test(res.headers['content-type'])) { diff --git a/lib/status-messages.js b/lib/status-messages.js new file mode 100644 index 00000000..1df4969b --- /dev/null +++ b/lib/status-messages.js @@ -0,0 +1,59 @@ +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' +}; From 241db797d3dff024979aa4ed1f1a7d4d8febc9c0 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:22:08 -0400 Subject: [PATCH 28/39] Add node comment to status-messages.js --- lib/status-messages.js | 138 ++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/lib/status-messages.js b/lib/status-messages.js index 1df4969b..193b1cc9 100644 --- a/lib/status-messages.js +++ b/lib/status-messages.js @@ -1,59 +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' + 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" }; From 9b3a71d84f4e68e49cff9908e5141b48d032d082 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 24 May 2015 11:23:43 -0400 Subject: [PATCH 29/39] Fix typo --- lib/status-messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/status-messages.js b/lib/status-messages.js index 193b1cc9..ae6a0142 100644 --- a/lib/status-messages.js +++ b/lib/status-messages.js @@ -60,7 +60,7 @@ module.exports = { 415: "Unsupported Media Type", 416: "Requested Range Not Satisfiable", 417: "Expectation Failed", - 418: "I\"m a teapot", + 418: "I'm a teapot", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", From a81f691d4ec09d3193f8abeea7a5db44e23ee251 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Mon, 25 May 2015 16:04:27 -0400 Subject: [PATCH 30/39] Allow 2 redirects --- lib/ffmpeg.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index ee6d664c..a793ffb0 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -25,7 +25,8 @@ var audioOnlyContainers = { "mp3": true }; -function testUrl(url, cb, redirected) { +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://"); @@ -41,11 +42,11 @@ function testUrl(url, cb, redirected) { req.abort(); if (res.statusCode === 301 || res.statusCode === 302) { - if (redirected) { + if (redirCount > 2) { return cb("Too many redirects. Please provide a direct link to the " + "file"); } - return testUrl(res.headers['location'], cb, true); + return testUrl(res.headers['location'], cb, redirCount + 1); } if (res.statusCode !== 200) { From d9f06a50de5977effe86f8bbc4a37ac44e7487d6 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 4 Jun 2015 00:57:51 -0400 Subject: [PATCH 31/39] Fix 484 --- www/js/data.js | 1 + www/js/util.js | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/www/js/data.js b/www/js/data.js index 9076a31b..f3fa0945 100644 --- a/www/js/data.js +++ b/www/js/data.js @@ -43,6 +43,7 @@ var IGNORED = []; var CHATHIST = []; var CHATHISTIDX = 0; var CHATTHROTTLE = false; +var CHATMAXSIZE = 100; var SCROLLCHAT = true; var LASTCHAT = { name: "" diff --git a/www/js/util.js b/www/js/util.js index 761c4552..79004f8e 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -1490,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(); @@ -1509,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" || From 0d8a389e05bfa3eaf7157554c02c636d40e1052c Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 7 Jun 2015 11:45:23 -0400 Subject: [PATCH 32/39] Remove YouTube v2 API fallback since v2 is dead --- config.template.yaml | 12 +- lib/config.js | 5 +- lib/get-info.js | 257 +------------------------------------------ 3 files changed, 16 insertions(+), 258 deletions(-) diff --git a/config.template.yaml b/config.template.yaml index 4da9d424..78782b1c 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -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 diff --git a/lib/config.js b/lib/config.js index 9f49afe1..8239b7fb 100644 --- a/lib/config.js +++ b/lib/config.js @@ -352,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."); } diff --git a/lib/get-info.js b/lib/get-info.js index 52a36813..8b852119 100644 --- a/lib/get-info.js +++ b/lib/get-info.js @@ -66,7 +66,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"); } @@ -86,7 +87,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) { @@ -108,7 +110,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) { @@ -730,254 +733,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>/); - if (m === null) - m = buffer.match(/([^<]+)<\/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); - } - }); - }, }; /** From 32d92855601b9e6c0fa20bcaeec743a16362bb72 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 14 Jun 2015 06:41:01 -0400 Subject: [PATCH 33/39] Temporary fix for vimeoWorkaround --- lib/get-info.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/get-info.js b/lib/get-info.js index 8b852119..e06394f4 100644 --- a/lib/get-info.js +++ b/lib/get-info.js @@ -749,6 +749,7 @@ function vimeoWorkaround(id, cb) { var inner = function () { var options = { host: "player.vimeo.com", + port: 443, path: "/video/" + id, headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:29.0) Gecko/20100101 Firefox/29.0", @@ -811,7 +812,7 @@ function vimeoWorkaround(id, cb) { } }; - urlRetrieve(http, options, function (status, buffer) { + urlRetrieve(https, options, function (status, buffer) { if (status !== 200) { setImmediate(function () { cb({}); From 9451e3978cc567675355816e40e50dc1e231919a Mon Sep 17 00:00:00 2001 From: calzoneman Date: Mon, 15 Jun 2015 08:32:11 -0400 Subject: [PATCH 34/39] Cut down on unneccessary ffprobe error logging --- lib/ffmpeg.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index a793ffb0..844fb369 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -46,7 +46,7 @@ function testUrl(url, cb, redirCount) { return cb("Too many redirects. Please provide a direct link to the " + "file"); } - return testUrl(res.headers['location'], cb, redirCount + 1); + return testUrl(res.headers["location"], cb, redirCount + 1); } if (res.statusCode !== 200) { @@ -55,8 +55,9 @@ function testUrl(url, cb, redirCount) { 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"); + 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(); @@ -214,10 +215,14 @@ exports.query = function (filename, cb) { return cb("Link uses a protocol unsupported by this server's " + "version of ffmpeg"); - Logger.errlog.log(err.stack || err); + // 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 { - Logger.errlog.log(err.stack || err); + if (!/(av|ff)probe/.test(String(err))) + Logger.errlog.log(err.stack || err); return cb("Unable to query file data with ffmpeg"); } } From 14c25ef8c172448b64aac5c8051662d9b3ed87f0 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Fri, 19 Jun 2015 08:39:11 -0400 Subject: [PATCH 35/39] Add explicit warning that custom embeds cannot be synchronized --- templates/channel.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/channel.jade b/templates/channel.jade index b2fea8e6..5bcb98e2 100644 --- a/templates/channel.jade +++ b/templates/channel.jade @@ -125,7 +125,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 <iframe> and <object> tags. + | Acceptable embed codes are <iframe> and <object> tags. CUSTOM EMBEDS CANNOT BE SYNCHRONIZED. textarea#customembed-content.input-block-level.form-control(rows="3") #playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12 .vertical-spacer From f43e46c7163d1f145138f141655ac8959f9cb963 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Fri, 19 Jun 2015 14:49:49 -0400 Subject: [PATCH 36/39] Fix loading no_emotes setting --- www/js/data.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/data.js b/www/js/data.js index f3fa0945..f4659e7e 100644 --- a/www/js/data.js +++ b/www/js/data.js @@ -116,7 +116,8 @@ var USEROPTS = { boop : getOrDefault("boop", "never"), secure_connection : getOrDefault("secure_connection", false), show_shadowchat : getOrDefault("show_shadowchat", false), - emotelist_sort : getOrDefault("emotelist_sort", true) + emotelist_sort : getOrDefault("emotelist_sort", true), + no_emotes : getOrDefault("no_emotes", false) }; /* Backwards compatibility check */ From c28dc0d3d24360385f0ab9bd9481cde40c3f7aad Mon Sep 17 00:00:00 2001 From: calzoneman Date: Fri, 19 Jun 2015 16:44:25 -0400 Subject: [PATCH 37/39] Fix #489 Channels are occasionally plagued by trolls who confuse users by "hijacking" names of other users in the channel. This is accomplished by replacing certain letters with visually similar letters (in fact, indistinguishable in some sans-serif fonts), e.g. replacing lowercase 'l' with capital 'I' This commit replaces capital 'I', lowercase 'l', digit '1', lowercase 'o', uppercase 'O', and digit '0' with '_' and changes the matching for isUsernameTaken() to a LIKE query. Since '_' is a single character wildcard, this causes the database to treat a username with one of these simple replacements as already registered. --- lib/database/accounts.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/database/accounts.js b/lib/database/accounts.js index 51f0a101..f270e5d7 100644 --- a/lib/database/accounts.js +++ b/lib/database/accounts.js @@ -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); From c2b4033903c7d495cc193e011061bbb95cf3a994 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Wed, 24 Jun 2015 17:32:32 -0400 Subject: [PATCH 38/39] package: bump cytubefilters --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e0ac463c..7ef3a6b3 100644 --- a/package.json +++ b/package.json @@ -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#f4bb2efd", + "cytubefilters": "git://github.com/calzoneman/cytubefilters#65e685ad", "express": "^4.11.1", "express-minify": "^0.1.3", "graceful-fs": "^3.0.5", From e3d12007b3acdd55fedd6761bcc757c660c4c187 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 5 Jul 2015 17:52:51 -0700 Subject: [PATCH 39/39] Fix css/js textboxes on channelCSSJS frame --- www/js/callbacks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/callbacks.js b/www/js/callbacks.js index fada290f..3c12b7f6 100644 --- a/www/js/callbacks.js +++ b/www/js/callbacks.js @@ -281,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) { $("