diff --git a/channel.js b/channel.js index d5ef7023..e1e1a55c 100644 --- a/channel.js +++ b/channel.js @@ -25,6 +25,7 @@ var Auth = require("./auth.js"); var ChatCommand = require("./chatcommand.js"); var Filter = require("./filter.js").Filter; var ActionLog = require("./actionlog"); +var Playlist = require("./playlist"); var sanitize = require("validator").sanitize; var Channel = function(name) { @@ -35,10 +36,9 @@ var Channel = function(name) { // Initialize defaults this.registered = false; this.users = []; - this.queue = []; + this.playlist = new Playlist(this); this.library = {}; this.position = -1; - this.media = null; this.drinks = 0; this.leader = null; this.chatbuffer = []; @@ -59,6 +59,7 @@ var Channel = function(name) { playlistjump: 1.5, playlistaddlist: 1.5, playlistaddlive: 1.5, + exceedmaxlength: 2, addnontemp: 2, settemp: 2, playlistgeturl: 1.5, @@ -78,6 +79,7 @@ var Channel = function(name) { allow_voteskip: true, voteskip_ratio: 0.5, pagetitle: this.name, + maxlength: 0, externalcss: "", externaljs: "", chat_antiflood: false, @@ -156,31 +158,35 @@ Channel.prototype.loadDump = function() { try { this.logger.log("*** Loading channel dump from disk"); data = JSON.parse(data); - for(var i = 0; i < data.queue.length; i++) { - var e = data.queue[i]; - var m = new Media(e.id, e.title, e.seconds, e.type); - m.queueby = data.queue[i].queueby ? data.queue[i].queueby - : ""; - if(e.temp !== undefined) { - m.temp = e.temp; + /* Load the playlist */ + + // Old + if(data.queue) { + if(data.position < 0) + data.position = 0; + for(var i = 0; i < data.queue.length; i++) { + var e = data.queue[i]; + var m = new Media(e.id, e.title, e.seconds, e.type); + var p = this.playlist.makeItem(m); + p.queueby = data.queue[i].queueby ? data.queue[i].queueby + : ""; + p.temp = e.temp; + this.playlist.items.append(p); + if(i == data.position) + this.playlist.current = p; } - this.queue.push(m); + this.sendAll("playlist", this.playlist.items.toArray()); + this.broadcastPlaylistMeta(); + this.playlist.startPlayback(); } - this.sendAll("playlist", this.queue); - this.broadcastPlaylistMeta(); - // Backwards compatibility - if(data.currentPosition != undefined) { - this.position = data.currentPosition - 1; - } - else { - this.position = data.position - 1; - } - if(this.position < -1) - this.position = -1; - if(this.queue.length > 0) - this.playNext(); - if(this.media && data.currentTime) { - this.media.currentTime = data.currentTime; + // Current + else if(data.playlist) { + var chan = this; + this.playlist.load(data.playlist, function() { + chan.sendAll("playlist", chan.playlist.items.toArray()); + chan.broadcastPlaylistMeta(); + chan.playlist.startPlayback(data.playlist.time); + }); } for(var key in data.opts) { // Gotta love backwards compatibility @@ -204,13 +210,13 @@ Channel.prototype.loadDump = function() { if(f[0] != undefined) { var filt = new Filter("", f[0], "g", f[1]); filt.active = f[2]; - this.updateFilter(filt); + this.updateFilter(filt, false); } else { var filt = new Filter(f.name, f.source, f.flags, f.replace); filt.active = f.active; filt.filterlinks = f.filterlinks; - this.updateFilter(filt); + this.updateFilter(filt, false); } } this.broadcastChatFilters(); @@ -232,7 +238,7 @@ Channel.prototype.loadDump = function() { } catch(e) { Logger.errlog.log("Channel dump load failed: "); - Logger.errlog.log(e); + Logger.errlog.log(e.stack); } }.bind(this)); } @@ -247,7 +253,7 @@ Channel.prototype.saveDump = function() { var dump = { position: this.position, currentTime: this.media ? this.media.currentTime : 0, - queue: this.queue, + playlist: this.playlist.dump(), opts: this.opts, permissions: this.permissions, filters: filts, @@ -294,7 +300,7 @@ Channel.prototype.tryRegister = function(user) { }); } else { - if(Database.registerChannel(this.name)) { + if(Database.registerChannel(this.name, user.name)) { ActionLog.record(user.ip, user.name, "channel-register-success", [this.name]); this.registered = true; this.initialized = true; @@ -728,14 +734,15 @@ Channel.prototype.sendChannelRanks = function(user) { } Channel.prototype.sendPlaylist = function(user) { - user.socket.emit("playlist", this.queue); - user.socket.emit("setPosition", this.position); + user.socket.emit("playlist", this.playlist.items.toArray()); + if(this.playlist.current) + user.socket.emit("setCurrent", this.playlist.current.uid); user.socket.emit("setPlaylistMeta", this.plmeta); } Channel.prototype.sendMediaUpdate = function(user) { - if(this.media != null) { - user.socket.emit("changeMedia", this.media.fullupdate()); + if(this.playlist.current != null) { + user.socket.emit("changeMedia", this.playlist.current.media.fullupdate()); } } @@ -780,12 +787,15 @@ Channel.prototype.sendAllWithPermission = function(perm, msg, data) { Channel.prototype.broadcastPlaylistMeta = function() { var total = 0; - for(var i = 0; i < this.queue.length; i++) { - total += this.queue[i].seconds; + var iter = this.playlist.items.first; + while(iter !== null) { + if(iter.media !== null) + total += iter.media.seconds; + iter = iter.next; } var timestr = formatTime(total); var packet = { - count: this.queue.length, + count: this.playlist.items.length, time: timestr }; this.plmeta = packet; @@ -1003,29 +1013,41 @@ function isLive(type) { || type == "im";// Imgur album } -Channel.prototype.queueAdd = function(media, idx) { - this.queue.splice(idx, 0, media); - this.sendAll("queue", { - media: media.pack(), - pos: idx - }); - this.broadcastPlaylistMeta(); - if(this.queue.length == 1) { - this.playNext(); +Channel.prototype.queueAdd = function(item, after) { + var chan = this; + function afterAdd() { + chan.sendAll("queue", { + item: item.pack(), + after: after + }); + chan.broadcastPlaylistMeta(); } + if(after === "prepend") + this.playlist.prepend(item, afterAdd); + else if(after === "append") + this.playlist.append(item, afterAdd); + else + this.playlist.insertAfter(item, after, afterAdd); } -Channel.prototype.autoTemp = function(media, user) { - if(isLive(media.type)) { - media.temp = true; +Channel.prototype.autoTemp = function(item, user) { + if(isLive(item.media.type)) { + item.temp = true; } if(!this.hasPermission(user, "addnontemp")) { - media.temp = true; + item.temp = true; } } Channel.prototype.enqueue = function(data, user, callback) { - var idx = data.pos == "next" ? this.position + 1 : this.queue.length; + var after = ""; + var current = this.playlist.current; + if(data.pos == "next") { + after = current ? current.uid : "prepend"; + } + else if(data.pos == "end") { + after = "append"; + } if(isLive(data.type) && !this.hasPermission(user, "playlistaddlive")) { user.socket.emit("queueFail", "You don't have permission to queue livestreams"); @@ -1035,9 +1057,10 @@ Channel.prototype.enqueue = function(data, user, callback) { // Prefer cache over looking up new data if(data.id in this.library) { var media = this.library[data.id].dup(); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); this.logger.log("*** Queued from cache: id=" + data.id); if(callback) callback(); @@ -1058,71 +1081,79 @@ Channel.prototype.enqueue = function(data, user, callback) { user.socket.emit("queueFail", err); return; } - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); this.cacheMedia(media); if(data.type == "yp") - idx++; + after = item.uid; if(callback) callback(); }.bind(this)); break; case "li": - var media = new Media(data.id, "Livestream - " + data.id, "--:--", "li"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var media = new Media(data.id, "Livestream.com - " + data.id, "--:--", "li"); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); break; case "tw": - var media = new Media(data.id, "Twitch - " + data.id, "--:--", "tw"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var media = new Media(data.id, "Twitch.tv - " + data.id, "--:--", "tw"); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); break; case "jt": - var media = new Media(data.id, "JustinTV - " + data.id, "--:--", "jt"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var media = new Media(data.id, "Justin.tv - " + data.id, "--:--", "jt"); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); break; case "us": InfoGetter.getUstream(data.id, function(id) { - var media = new Media(id, "Ustream - " + data.id, "--:--", "us"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var media = new Media(id, "Ustream.tv - " + data.id, "--:--", "us"); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); }.bind(this)); break; case "rt": var media = new Media(data.id, "Livestream", "--:--", "rt"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); break; case "jw": var media = new Media(data.id, "JWPlayer Stream - " + data.id, "--:--", "jw"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); break; case "im": var media = new Media(data.id, "Imgur Album", "--:--", "im"); - media.queueby = user ? user.name : ""; - this.autoTemp(media, user); - this.queueAdd(media, idx); + var item = this.playlist.makeItem(media); + item.queueby = user ? user.name : ""; + this.autoTemp(item, user); + this.queueAdd(item, after); if(callback) callback(); break; @@ -1157,37 +1188,89 @@ Channel.prototype.tryQueue = function(user, data) { } if(data.list) - this.enqueueList(data, user); + this.addMediaList(data, user); else - this.enqueue(data, user); + this.addMedia(data, user); } -Channel.prototype.enqueueList = function(data, user) { +Channel.prototype.addMedia = function(data, user) { + data.temp = isLive(data.type) || !this.hasPermission(user, "addnontemp"); + data.queueby = user ? user.name : ""; + data.maxlength = this.hasPermission(user, "exceedmaxlength") ? 0 : this.opts.maxlength; + var chan = this; + if(data.id in this.library) { + var m = this.library[data.id].dup(); + if(data.maxlength && m.seconds > data.maxlength) { + user.socket.emit("queueFail", "Media is too long!"); + return; + } + + data.media = m; + this.playlist.addCachedMedia(data, function (err, item) { + if(err) { + if(err === true) + err = false; + if(user) + user.socket.emit("queueFail", err); + return; + } + else { + chan.sendAll("queue", { + item: item.pack(), + after: item.prev ? item.prev.uid : "prepend" + }); + chan.broadcastPlaylistMeta(); + } + }); + return; + } + if(isLive(data.type) && !this.hasPermission(user, "playlistaddlive")) { + user.socket.emit("queueFail", "You don't have permission to queue livestreams"); + return; + } + + data.temp = isLive(data.type) || !this.hasPermission(user, "addnontemp"); + data.queueby = user ? user.name : ""; + + this.playlist.addMedia(data, function(err, item) { + if(err) { + if(err === true) + err = false; + if(user) + user.socket.emit("queueFail", err); + return; + } + else { + chan.sendAll("queue", { + item: item.pack(), + after: item.prev ? item.prev.uid : "prepend" + }); + chan.broadcastPlaylistMeta(); + chan.cacheMedia(item.media); + } + }); +} + +Channel.prototype.addMediaList = function(data, user) { var pl = data.list; var chan = this; - // Queue in reverse order for qnext - if(data.pos == "next") { - var i = pl.length; - var cback = function() { - i--; - if(i > 0) { - pl[i].pos = "next"; - chan.enqueue(pl[i], user, cback); - } + this.playlist.addMediaList(data, function(err, item) { + if(err) { + if(err === true) + err = false; + if(user) + user.socket.emit("queueFail", err); + return; } - this.enqueue(pl[0], user, cback); - } - else { - var i = 0; - var cback = function() { - i++; - if(i < pl.length) { - pl[i].pos = "end"; - chan.enqueue(pl[i], user, cback); - } + else { + chan.sendAll("queue", { + item: item.pack(), + after: item.prev ? item.prev.uid : "prepend" + }); + chan.broadcastPlaylistMeta(); + chan.cacheMedia(item.media); } - this.enqueue(pl[i], user, cback); - } + }); } Channel.prototype.tryQueuePlaylist = function(user, data) { @@ -1206,19 +1289,21 @@ Channel.prototype.tryQueuePlaylist = function(user, data) { var pl = Database.loadUserPlaylist(user.name, data.name); data.list = pl; - this.enqueueList(data, user); + this.addMediaList(data, user); } -Channel.prototype.setTemp = function(idx, temp) { - var med = this.queue[idx]; - med.temp = temp; +Channel.prototype.setTemp = function(uid, temp) { + var item = this.playlist.items.find(uid); + if(!item) + return; + item.temp = temp; this.sendAll("setTemp", { - position: idx, + uid: uid, temp: temp }); if(!temp) { - this.cacheMedia(med); + this.cacheMedia(item.media); } } @@ -1226,42 +1311,24 @@ Channel.prototype.trySetTemp = function(user, data) { if(!this.hasPermission(user, "settemp")) { return; } - if(typeof data.position != "number" || typeof data.temp != "boolean") { - return; - } - if(data.position < 0 || data.position >= this.queue.length) { + if(typeof data.uid != "number" || typeof data.temp != "boolean") { return; } - this.setTemp(data.position, data.temp); + this.setTemp(data.uid, data.temp); } -Channel.prototype.dequeue = function(position, removeonly) { - if(position < 0 || position >= this.queue.length) { - return; +Channel.prototype.dequeue = function(uid) { + var chan = this; + function afterDelete() { + chan.sendAll("delete", { + uid: uid + }); + chan.broadcastPlaylistMeta(); } - - this.queue.splice(position, 1); - this.sendAll("delete", { - position: position - }); - this.broadcastPlaylistMeta(); - - if(removeonly) + if(!this.playlist.remove(uid, afterDelete)) return; - - // If you remove the currently playing video, play the next one - if(position == this.position) { - this.position--; - this.playNext(); - return; - } - // If you remove a video whose position is before the one currently - // playing, you have to reduce the position of the one playing - if(position < this.position) { - this.position--; - } } Channel.prototype.tryDequeue = function(user, data) { @@ -1288,59 +1355,19 @@ Channel.prototype.tryUncache = function(user, data) { } Channel.prototype.playNext = function() { - var pos = this.position + 1 >= this.queue.length ? 0 : this.position + 1; - this.jumpTo(pos); + this.playlist.next(); } Channel.prototype.tryPlayNext = function(user) { if(!this.hasPermission(user, "playlistjump")) { return; - } + } - this.playNext(); + this.playNext(); } -Channel.prototype.jumpTo = function(pos) { - if(pos >= this.queue.length || pos < 0) { - return; - } - - // Reset voteskip - this.voteskip = false; - this.broadcastVoteskipUpdate(); - this.drinks = 0; - this.broadcastDrinks(); - - var old = this.position; - if(this.media && this.media.temp && old != pos) { - this.dequeue(old, true); - if(pos > old && pos > 0) { - pos--; - } - } - if(pos >= this.queue.length || pos < 0) { - return; - } - if(this.media) { - delete this.media["currentTime"]; - delete this.media["paused"]; - } - this.position = pos; - var oid = this.media ? this.media.id : ""; - this.media = this.queue[this.position]; - this.media.currentTime = -1; - this.media.paused = false; - - this.sendAll("changeMedia", this.media.fullupdate()); - this.sendAll("setPosition", this.position); - - // If it's not a livestream, enable autolead - if(this.leader == null && !isLive(this.media.type)) { - this.time = new Date().getTime(); - if(this.media.id != oid) { - mediaUpdate(this, this.media.id); - } - } +Channel.prototype.jumpTo = function(uid) { + return this.playlist.jump(uid); } Channel.prototype.tryJumpTo = function(user, data) { @@ -1356,10 +1383,8 @@ Channel.prototype.tryJumpTo = function(user, data) { } Channel.prototype.clearqueue = function() { - this.queue = []; - for(var i = 0; i < this.users.length; i++) { - this.sendPlaylist(this.users[i]); - } + this.playlist.clear(); + this.sendAll("playlist", this.playlist.items.toArray()); this.broadcastPlaylistMeta(); } @@ -1372,20 +1397,20 @@ Channel.prototype.tryClearqueue = function(user) { Channel.prototype.shufflequeue = function() { var n = []; - var current = false; - while(this.queue.length > 0) { - var i = parseInt(Math.random() * this.queue.length); - n.push(this.queue[i]); - if(!current && i == this.position) { - this.position = n.length - 1; - current = true; - } - this.queue.splice(i, 1); + var pl = this.playlist.items.toArray(false); + this.playlist.clear(); + while(pl.length > 0) { + var i = parseInt(Math.random() * pl.length); + var item = this.playlist.makeItem(pl[i].media); + item.temp = pl[i].temp; + item.queueby = pl[i].queueby; + this.playlist.items.append(item); + pl.splice(i, 1); } - this.queue = n; - this.sendAll("playlist", this.queue); - this.sendAll("setPosition", this.position); + this.playlist.current = this.playlist.items.first; + this.sendAll("playlist", this.playlist.items.toArray()); this.sendAll("setPlaylistMeta", this.plmeta); + this.playlist.startPlayback(); } Channel.prototype.tryShufflequeue = function(user) { @@ -1400,73 +1425,53 @@ Channel.prototype.tryUpdate = function(user, data) { return; } - if(data == null || - data.id == undefined || data.currentTime == undefined) { + if(typeof data.id !== "string" || typeof data.currentTime !== "number") return; - } - if(this.media == null) { + if(this.playlist.current === null) { return; } - if(isLive(this.media.type) && this.media.type != "jw") { + if(isLive(this.playlist.current.media.type) + && this.playlist.current.media.type != "jw") { return; } - if(this.media.id != data.id) { + if(this.playlist.current.media.id != data.id) { return; } - this.media.currentTime = data.currentTime; - this.media.paused = data.paused; - this.sendAll("mediaUpdate", this.media.timeupdate()); + this.playlist.current.media.currentTime = data.currentTime; + this.playlist.current.media.paused = data.paused; + this.sendAll("mediaUpdate", this.playlist.current.media.timeupdate()); } Channel.prototype.move = function(data, user) { - if(data.from < 0 || data.from >= this.queue.length) { - return; - } - if(data.to < 0 || data.to > this.queue.length) { - return; + var chan = this; + function afterMove() { + var moveby = user && user.name ? user.name : null; + if(typeof data.moveby !== "undefined") + moveby = data.moveby; + + chan.sendAll("moveVideo", { + from: data.from, + after: data.after, + moveby: moveby + }); } - var media = this.queue[data.from]; - var to = data.to > data.from ? data.to + 1 : data.to; - var from = data.to > data.from ? data.from : data.from + 1; - var moveby = user && user.name ? user.name : null; - if(typeof data.moveby !== "undefined") - moveby = data.moveby; - - this.queue.splice(to, 0, media); - this.queue.splice(from, 1); - this.sendAll("moveVideo", { - from: data.from, - to: data.to, - moveby: moveby - }); - - // Account for moving things around the active video - if(data.from < this.position && data.to >= this.position) { - this.position--; - } - else if(data.from > this.position && data.to < this.position) { - this.position++ - } - else if(data.from == this.position) { - this.position = data.to; - } + this.playlist.move(data.from, data.after, afterMove); } Channel.prototype.tryMove = function(user, data) { if(!this.hasPermission(user, "playlistmove")) { - return; + return; } - if(typeof data.from !== "number" || typeof data.to !== "number") { - return; - } + if(typeof data.from !== "number" || (typeof data.after !== "number" && typeof data.after !== "string")) + return; - this.move(data, user); + this.move(data, user); } /* REGION Polls */ @@ -1571,7 +1576,7 @@ Channel.prototype.removeFilter = function(filter) { this.broadcastChatFilters(); } -Channel.prototype.updateFilter = function(filter) { +Channel.prototype.updateFilter = function(filter, emit) { if(filter.name == "") filter.name = filter.source; var found = false; @@ -1585,7 +1590,8 @@ Channel.prototype.updateFilter = function(filter) { if(!found) { this.filters.push(filter); } - this.broadcastChatFilters(); + if(emit !== false) + this.broadcastChatFilters(); } Channel.prototype.tryUpdateFilter = function(user, f) { @@ -1933,17 +1939,21 @@ Channel.prototype.changeLeader = function(name) { } if(name == "") { this.logger.log("*** Resuming autolead"); - if(this.media != null && !isLive(this.media.type)) { - this.media.paused = false; + /* + if(this.playlist.current != null && !isLive(this.playlist.current.media.type)) { + this.playlist.current.media.paused = false; this.time = new Date().getTime(); this.i = 0; - mediaUpdate(this, this.media.id); + mediaUpdate(this, this.playlist.current.media.id); } + */ + this.playlist.lead(true); return; } for(var i = 0; i < this.users.length; i++) { if(this.users[i].name == name) { this.logger.log("*** Assigned leader: " + name); + this.playlist.lead(false); this.leader = this.users[i]; if(this.users[i].rank < 1.5) { this.users[i].oldrank = this.users[i].rank; diff --git a/database.js b/database.js index 6855b1e9..108ff8ae 100644 --- a/database.js +++ b/database.js @@ -105,6 +105,7 @@ function init() { var query = ["CREATE TABLE IF NOT EXISTS `channels` (", "`id` INT NOT NULL AUTO_INCREMENT,", "`name` VARCHAR(255) NOT NULL,", + "`owner` VARCHAR(20) NOT NULL,", "PRIMARY KEY(`id`))", "ENGINE = MyISAM;"].join(""); var results = db.querySync(query); @@ -249,7 +250,7 @@ function globalUnbanIP(ip) { /* REGION Channel Registration/Loading */ -function registerChannel(name) { +function registerChannel(name, owner) { if(!name.match(/^[a-zA-Z0-9-_]+$/)) { return false; } @@ -305,8 +306,8 @@ function registerChannel(name) { // Insert into channel table query = createQuery( - "INSERT INTO `channels` VALUES (NULL, ?)", - [name] + "INSERT INTO `channels` VALUES (NULL, ?, ?)", + [name, owner] ); results = db.querySync(query); @@ -866,10 +867,10 @@ function saveUserPlaylist(pl, user, name) { var time = 0; for(var i = 0; i < pl.length; i++) { var e = { - id: pl[i].id, - type: pl[i].type + id: pl[i].media.id, + type: pl[i].media.type }; - time += pl[i].seconds; + time += pl[i].media.seconds; pl2.push(e); } var count = pl2.length; diff --git a/get-info.js b/get-info.js index 9afb5b74..31819d55 100644 --- a/get-info.js +++ b/get-info.js @@ -17,7 +17,7 @@ var Media = require("./media.js").Media; // Helper function for making an HTTP request and getting the result // as JSON function getJSON(options, callback) { - var req = http.request(options, function(res){ + var req = http.request(options, function(res) { var buffer = ""; res.setEncoding("utf8"); res.on("data", function (chunk) { @@ -47,7 +47,7 @@ function getJSON(options, callback) { // Dailymotion uses HTTPS for anonymous requests... [](/picard) function getJSONHTTPS(options, callback) { - var req = https.request(options, function(res){ + var req = https.request(options, function(res) { var buffer = ""; res.setEncoding("utf8"); res.on("data", function (chunk) { @@ -87,6 +87,7 @@ exports.getYTInfo = function(id, callback) { timeout: 1000}, callback); } +// Look up a YouTube playlist exports.getYTPlaylist = function(id, callback, url) { var path = "/feeds/api/playlists/" + id + "?v=2&alt=json"; if(url) { @@ -101,6 +102,7 @@ exports.getYTPlaylist = function(id, callback, url) { timeout: 1000}, callback); } +// Search YouTube exports.searchYT = function(terms, callback) { // I really miss Python's list comprehensions for(var i = 0; i < terms.length; i++) { @@ -157,13 +159,8 @@ exports.getYTSearchResults = function(query, callback) { } // Look up Soundcloud metadata -// Whoever designed this should rethink it. I'll submit a feedback -// form on their website. exports.getSCInfo = function(url, callback) { const SC_CLIENT = "2e0c82ab5a020f3a7509318146128abd"; - // SoundCloud is dumb - // I have to request the API URL for the given input URL - // Because the sound ID isn"t in the URL getJSON({ host: "api.soundcloud.com", port: 80, @@ -254,7 +251,7 @@ exports.getMedia = function(id, type, callback) { } catch(e) { Logger.errlog.log("getMedia failed: "); - Logger.errlog.log(e); + Logger.errlog.log(e.stack); callback(true, null); } }); @@ -329,7 +326,7 @@ exports.getMedia = function(id, type, callback) { } try { - + var vids = []; for(var i = 0; i < data.feed.entry.length; i++) { try { var item = data.feed.entry[i]; @@ -338,13 +335,15 @@ exports.getMedia = function(id, type, callback) { var title = item.title.$t; var seconds = item.media$group.yt$duration.seconds; var media = new Media(id, title, seconds, "yt"); - callback(false, media); + vids.push(media); } catch(e) { Logger.errlog.log("getMedia failed: "); Logger.errlog.log(e); } } + callback(false, vids); + var links = data.feed.link; for(var i = 0; i < links.length; i++) { @@ -360,6 +359,31 @@ exports.getMedia = function(id, type, callback) { } } exports.getYTPlaylist(id, cback); + break; + case "li": + case "tw": + case "jt": + case "us": + case "jw": + const prefix = { + "li": "Livestream.com - ", + "tw": "Twitch.tv - ", + "jt": "Justin.tv - ", + "us": "Ustream.tv - ", + "jw": "JWPlayer Stream - " + }; + var media = new Media(data.id, prefix[data.type] + data.id, "--:--", data.type); + callback(false, media); + break; + case "rt": + case "im": + const names = { + "rt": "Livestream", + "im": "Imgur Album" + }; + var media = new Media(data.id, names[data.type], "--:--", data.type); + callback(false, media); + break; default: break; } diff --git a/media.js b/media.js index 0f04b7e0..3febcf6e 100644 --- a/media.js +++ b/media.js @@ -51,14 +51,10 @@ var Media = function(id, title, seconds, type) { this.seconds = 0; } this.type = type; - this.queueby = ""; - this.temp = false; } Media.prototype.dup = function() { var m = new Media(this.id, this.title, this.seconds, this.type); - m.queueby = this.queueby; - m.temp = this.temp; return m; } @@ -71,8 +67,6 @@ Media.prototype.pack = function() { seconds: this.seconds, duration: this.duration, type: this.type, - queueby: this.queueby, - temp: this.temp }; } @@ -87,8 +81,6 @@ Media.prototype.fullupdate = function() { type: this.type, currentTime: this.currentTime, paused: this.paused, - queueby: this.queueby, - temp: this.temp }; } diff --git a/playlist.js b/playlist.js new file mode 100644 index 00000000..1164f178 --- /dev/null +++ b/playlist.js @@ -0,0 +1,461 @@ +ULList = require("./ullist").ULList; +var Media = require("./media").Media; +var InfoGetter = require("./get-info"); + +function PlaylistItem(media, uid) { + this.media = media; + this.uid = uid; + this.temp = false; + this.queueby = ""; + this.prev = null; + this.next = null; +} + +PlaylistItem.prototype.pack = function() { + return { + media: this.media.pack(), + uid: this.uid, + temp: this.temp, + queueby: this.queueby + }; +} + +function Playlist(chan) { + this.items = new ULList(); + this.next_uid = 0; + this._leadInterval = false; + this._lastUpdate = 0; + this._counter = 0; + this.leading = true; + this.callbacks = { + "changeMedia": [], + "mediaUpdate": [], + "remove": [], + }; + this.lock = false; + this.action_queue = []; + this._qaInterval = false; + + if(chan) { + var pl = this; + this.on("mediaUpdate", function(m) { + chan.sendAll("mediaUpdate", m.timeupdate()); + }); + this.on("changeMedia", function(m) { + chan.sendAll("setCurrent", pl.current.uid); + chan.sendAll("changeMedia", m.fullupdate()); + }); + this.on("remove", function(item) { + chan.sendAll("delete", { + uid: item.uid + }); + }); + } +} + +Playlist.prototype.queueAction = function(data) { + this.action_queue.push(data); + if(this._qaInterval) + return; + var pl = this; + this._qaInterval = setInterval(function() { + var data = pl.action_queue.shift(); + if(data.waiting) { + if(!("expire" in data)) + data.expire = Date.now() + 10000; + if(Date.now() < data.expire) + pl.action_queue.unshift(data); + } + else + data.fn(); + if(pl.action_queue.length == 0) { + clearInterval(pl._qaInterval); + pl._qaInterval = false; + } + }, 100); +} + +Playlist.prototype.dump = function() { + var arr = this.items.toArray(); + var pos = 0; + for(var i in arr) { + if(this.current && arr[i].uid == this.current.uid) { + pos = i; + break; + } + } + + var time = 0; + if(this.current) + time = this.current.media.currentTime; + + return { + pl: arr, + pos: pos, + time: time + }; +} + +Playlist.prototype.die = function () { + this.clear(); + if(this._leadInterval) { + clearInterval(this._leadInterval); + this._leadInterval = false; + } +} + +Playlist.prototype.load = function(data, callback) { + this.clear(); + for(var i in data.pl) { + var e = data.pl[i].media; + var m = new Media(e.id, e.title, e.seconds, e.type); + var it = this.makeItem(m); + it.temp = data.pl[i].temp; + it.queueby = data.pl[i].queueby; + this.items.append(it); + if(i == parseInt(data.pos)) { + this.current = it; + } + } + + if(callback) + callback(); +} + +Playlist.prototype.on = function(ev, fn) { + if(typeof fn === "undefined") { + var pl = this; + return function() { + for(var i = 0; i < pl.callbacks[ev].length; i++) { + pl.callbacks[ev][i].apply(this, arguments); + } + } + } + else if(typeof fn === "function") { + this.callbacks[ev].push(fn); + } +} + +Playlist.prototype.makeItem = function(media) { + return new PlaylistItem(media, this.next_uid++); +} + +Playlist.prototype.add = function(item, pos) { + var success; + if(pos == "append") + success = this.items.append(item); + else if(pos == "prepend") + success = this.items.prepend(item); + else + success = this.items.insertAfter(item, pos); + + if(success && this.items.length == 1) { + this.current = item; + this.startPlayback(); + } + + return success; +} + +Playlist.prototype.addCachedMedia = function(data, callback) { + var pos = "append"; + if(data.pos == "next") { + if(!this.current) + pos = "prepend"; + else + pos = this.current.uid; + } + + var it = this.makeItem(data.media); + it.temp = data.temp; + it.queueby = data.queueby; + + var pl = this; + + var action = { + fn: function() { + if(pl.add(it, pos)) + callback(false, it); + }, + waiting: false + }; + this.queueAction(action); +} + +Playlist.prototype.addMedia = function(data, callback) { + + if(data.type == "yp") { + this.addYouTubePlaylist(data, callback); + return; + } + + var pos = "append"; + if(data.pos == "next") { + if(!this.current) + pos = "prepend"; + else + pos = this.current.uid; + } + + var it = this.makeItem(null); + var pl = this; + var action = { + fn: function() { + if(pl.add(it, pos)) { + callback(false, it); + } + }, + waiting: true + }; + this.queueAction(action); + + InfoGetter.getMedia(data.id, data.type, function(err, media) { + if(err) { + action.expire = 0; + callback(err, null); + return; + } + + if(data.maxlength && media.seconds > data.maxlength) { + action.expire = 0; + callback("Media is too long!", null); + return; + } + + it.media = media; + it.temp = data.temp; + it.queueby = data.queueby; + action.waiting = false; + }); +} + +Playlist.prototype.addMediaList = function(data, callback) { + var start = false; + if(data.pos == "next") { + data.list = data.list.reverse(); + start = data.list[data.list.length - 1]; + } + + var pl = this; + data.list.forEach(function(x) { + x.pos = data.pos; + if(start && x == start) { + pl.addMedia(x, function (err, item) { + if(err) { + callback(err, item); + } + else { + callback(err, item); + pl.current = item; + pl.startPlayback(); + } + }); + } + else { + pl.addMedia(x, callback); + } + }); +} + +Playlist.prototype.addYouTubePlaylist = function(data, callback) { + var pos = "append"; + if(data.pos == "next") { + if(!this.current) + pos = "prepend"; + else + pos = this.current.uid; + } + + var pl = this; + InfoGetter.getMedia(data.id, data.type, function(err, vids) { + if(err) { + callback(err, null); + return; + } + + vids.forEach(function(media) { + var it = pl.makeItem(media); + it.temp = data.temp; + it.queueby = data.queueby; + pl.queueAction({ + fn: function() { + if(pl.add(it, pos)) + callback(false, it); + }, + }); + }); + }); +} + +Playlist.prototype.remove = function(uid, callback) { + var pl = this; + this.queueAction({ + fn: function() { + var item = pl.items.find(uid); + if(pl.items.remove(uid)) { + if(item == pl.current) + pl._next(); + if(callback) + callback(); + } + }, + waiting: false + }); +} + +Playlist.prototype.move = function(from, after, callback) { + var pl = this; + this.queueAction({ + fn: function() { + pl._move(from, after, callback); + }, + waiting: false + }); +} + +Playlist.prototype._move = function(from, after, callback) { + var it = this.items.find(from); + if(!this.items.remove(from)) + return; + + if(after === "prepend") { + if(!this.items.prepend(it)) + return; + } + + else if(after === "append") { + if(!this.items.append(it)) + return; + } + + else if(!this.items.insertAfter(it, after)) + return; + + callback(); +} + +Playlist.prototype.next = function() { + if(!this.current) + return; + + var it = this.current; + this._next(); + + if(it.temp) { + var pl = this; + this.remove(it.uid, function() { + pl.on("remove")(it); + }); + } + + return this.current; +} + +Playlist.prototype._next = function() { + if(!this.current) + return; + this.current = this.current.next; + if(this.current === null && this.items.first !== null) + this.current = this.items.first; + + if(this.current) { + this.startPlayback(); + } +} + +Playlist.prototype.jump = function(uid) { + if(!this.current) + return false; + + var jmp = this.items.find(uid); + if(!jmp) + return false; + + var it = this.current; + + this.current = jmp; + + if(this.current) { + this.startPlayback(); + } + + if(it.temp) { + var pl = this; + this.remove(it.uid, function () { + pl.on("remove")(it); + }); + } + + return this.current; +} + +Playlist.prototype.clear = function() { + this.items.clear(); + this.next_uid = 0; + this.current = null; + clearInterval(this._leadInterval); +} + +Playlist.prototype.lead = function(lead) { + this.leading = lead; + var pl = this; + if(!this.leading && this._leadInterval) { + clearInterval(this._leadInterval); + this._leadInterval = false; + } + else if(this.leading && !this._leadInterval) { + this._leadInterval = setInterval(function() { + pl._leadLoop(); + }, 1000); + } +} + +Playlist.prototype.startPlayback = function(time) { + if(!this.current || !this.current.media) + return false; + this.current.media.paused = false; + this.current.media.currentTime = time || -1; + var pl = this; + if(this._leadInterval) { + clearInterval(this._leadInterval); + this._leadInterval = false; + } + this.on("changeMedia")(this.current.media); + if(this.leading && !isLive(this.current.media.type)) { + this.on("changeMedia")(this.current.media); + this._lastUpdate = Date.now(); + this._leadInterval = setInterval(function() { + pl._leadLoop(); + }, 1000); + } +} + +function isLive(type) { + return type == "li" // Livestream.com + || type == "tw" // Twitch.tv + || type == "jt" // Justin.tv + || type == "rt" // RTMP + || type == "jw" // JWPlayer + || type == "us" // Ustream.tv + || type == "im";// Imgur album +} + +const UPDATE_INTERVAL = 5; + +Playlist.prototype._leadLoop = function() { + if(this.current == null) + return; + + this.current.media.currentTime += (Date.now() - this._lastUpdate) / 1000.0; + this._lastUpdate = Date.now(); + this._counter++; + + if(this.current.media.currentTime >= this.current.media.seconds + 2) { + this.next(); + } + else if(this._counter % UPDATE_INTERVAL == 0) { + this.on("mediaUpdate")(this.current.media); + } +} + +module.exports = Playlist; diff --git a/server.js b/server.js index 167e2da5..c5b3e83b 100644 --- a/server.js +++ b/server.js @@ -185,6 +185,7 @@ exports.unload = function(chan) { if(chan.registered) { chan.saveDump(); } + chan.playlist.die(); exports.channels[chan.name] = null; delete exports.channels[chan.name]; } diff --git a/ullist.js b/ullist.js new file mode 100644 index 00000000..1af00537 --- /dev/null +++ b/ullist.js @@ -0,0 +1,153 @@ +/* + ullist.js + + Description: Defines ULList, which represents a doubly linked list + in which each item has a unique identifier stored in the `uid` field. + +*/ + +function ULList() { + this.first = null; + this.last = null; + this.length = 0; +} + +/* Add an item to the beginning of the list */ +ULList.prototype.prepend = function(item) { + if(this.first !== null) { + item.next = this.first; + this.first.prev = item; + } + else { + this.last = item; + } + this.first = item; + this.first.prev = null; + this.length++; + return true; +} + +/* Add an item to the end of the list */ +ULList.prototype.append = function(item) { + if(this.last !== null) { + item.prev = this.last; + this.last.next = item; + } + else { + this.first = item; + } + this.last = item; + this.last.next = null; + this.length++; + return true; +} + +/* Insert an item after one which has a specified UID */ +ULList.prototype.insertAfter = function(item, uid) { + var after = this.find(uid); + + if(!after) + return false; + + // Update links + item.next = after.next; + if(item.next) + item.next.prev = item; + item.prev = after; + after.next = item; + + // New end of list + if(after == this.last) + this.last = item; + + this.length++; + + return true; +} + +/* Insert an item before one that has a specified UID */ +ULList.prototype.insertBefore = function(item, uid) { + var before = this.find(uid); + + if(!before) + return false; + + // Update links + item.next = before; + item.prev = before.prev; + if(item.prev) + item.prev.next = item; + before.prev = item; + + // New beginning of list + if(before == this.first) + this.first = item; + + this.length++; + + return true; +} + +/* Remove an item from the list */ +ULList.prototype.remove = function(uid) { + var item = this.find(uid); + if(!item) + return false; + + // Boundary conditions + if(item == this.first) + this.first = item.next; + if(item == this.last) + this.last = item.prev; + + // General case + if(item.prev) + item.prev.next = item.next; + if(item.next) + item.next.prev = item.prev; + + this.length--; + return true; +} + +/* Find an element in the list, return false if specified UID not found */ +ULList.prototype.find = function(uid) { + // Can't possibly find it in an empty list + if(this.first === null) + return false; + + var item = this.first; + var iter = this.first; + while(iter !== null && item.uid != uid) { + item = iter; + iter = iter.next; + } + + if(item && item.uid == uid) + return item; + return false; +} + +/* Clear all elements from the list */ +ULList.prototype.clear = function() { + this.first = null; + this.last = null; + this.length = 0; +} + +/* Dump the contents of the list into an array */ +ULList.prototype.toArray = function(pack) { + var arr = new Array(this.length); + var item = this.first; + var i = 0; + while(item !== null) { + if(pack !== false && typeof item.pack == "function") + arr[i++] = item.pack(); + else + arr[i++] = item; + item = item.next; + } + return arr; +} + +exports.ULList = ULList; diff --git a/update.js b/update.js index b1a8435b..dc1602c5 100644 --- a/update.js +++ b/update.js @@ -1,21 +1,61 @@ var Config = require("./config.js"); var Database = require("./database.js"); -Config.DEBUG = true; +//Config.DEBUG = true; Database.setup(Config); Database.init(); var query; var db = Database.getConnection(); // Check for already existing -query = "SELECT email FROM registrations WHERE 1"; +query = "SELECT owner FROM channels WHERE 1"; if(!db.querySync(query)) { - query = "ALTER TABLE registrations ADD email VARCHAR(255) NOT NULL"; + query = "ALTER TABLE channels ADD owner VARCHAR(20) NOT NULL"; var res = db.querySync(query); if(!res) { console.log(db); console.log("Update failed!"); } + else { + populateChannelOwners(); + } } db.closeSync(); process.exit(0); + +function populateChannelOwners() { + query = "SELECT * FROM channels WHERE 1"; + var res = db.querySync(query); + if(!res) { + console.log(db); + console.log("Update failed!"); + return; + } + + var channels = res.fetchAllSync(); + channels.forEach(function(chan) { + chan = chan.name; + query = "SELECT name FROM `chan_"+chan+"_ranks` WHERE rank>=10 ORDER BY rank"; + res = db.querySync(query); + if(!res) { + console.log(db); + console.log("failed to fix "+chan); + return; + } + + var results = res.fetchAllSync(); + if(results.length == 0) { + console.log("bad channel: " + chan); + return; + } + var owner = results[0].name; + query = "UPDATE channels SET owner='"+owner+"' WHERE name='"+chan+"'"; + console.log("setting owner=" + owner + " for /r/" + chan); + res = db.querySync(query); + if(!res) { + console.log(db); + console.log("Update failed!"); + return; + } + }); +} diff --git a/user.js b/user.js index 516d1359..e5378d34 100644 --- a/user.js +++ b/user.js @@ -484,7 +484,7 @@ User.prototype.initCallbacks = function() { return; } - var pl = this.channel.queue; + var pl = this.channel.playlist.items.toArray(); var result = Database.saveUserPlaylist(pl, this.name, data.name); this.socket.emit("savePlaylist", { success: result, diff --git a/www/assets/js/callbacks.js b/www/assets/js/callbacks.js index b11e84b0..32d60c4c 100644 --- a/www/assets/js/callbacks.js +++ b/www/assets/js/callbacks.js @@ -650,25 +650,35 @@ Callbacks = { }, queue: function(data) { - // Wait until pending movements are completed - if(PL_MOVING || PL_ADDING || PL_DELETING) { - setTimeout(function() { - Callbacks.queue(data); - }, 100); - return; - } - var li = makeQueueEntry(data.media, true); - li.hide(); - var idx = data.pos; - var q = $("#queue"); - li.attr("title", data.media.queueby - ? ("Added by: " + data.media.queueby) - : "Added by: Unknown"); - if(idx < q.children().length - 1) - li.insertBefore(q.children()[idx]) - else - li.appendTo(q); - li.show("blind"); + queueAction({ + fn: function () { + var li = makeQueueEntry(data.item, true); + li.hide(); + var q = $("#queue"); + li.attr("title", data.item.queueby + ? ("Added by: " + data.item.queueby) + : "Added by: Unknown"); + if(data.after === "prepend") { + li.prependTo(q); + li.show("blind"); + return true; + } + else if(data.after === "append") { + li.appendTo(q); + li.show("blind"); + return true; + } + else { + var liafter = playlistFind(data.after); + if(!liafter) { + return false; + } + li.insertAfter(liafter); + li.show("blind"); + return true; + } + } + }); }, queueFail: function(data) { @@ -681,89 +691,77 @@ Callbacks = { }, setTemp: function(data) { - var li = $("#queue").children()[data.position]; - li = $(li); + var li = $(".pluid-" + data.uid); + if(li.length == 0) + return false; + if(data.temp) li.addClass("queue_temp"); else li.removeClass("queue_temp"); + var btn = li.find(".qbtn-tmp"); - btn.data("temp", data.temp); - if(data.temp) { - btn.html(btn.html().replace("Make Temporary", - "Make Permanent")); - } - else { - btn.html(btn.html().replace("Make Permanent", - "Make Temporary")); + if(btn.length > 0) { + btn.data("temp", data.temp); + if(data.temp) { + btn.html(btn.html().replace("Make Temporary", + "Make Permanent")); + } + else { + btn.html(btn.html().replace("Make Permanent", + "Make Temporary")); + } } }, "delete": function(data) { - // Wait until any pending manipulation is finished - if(PL_MOVING || PL_ADDING || PL_DELETING) { - setTimeout(function() { - Callbacks["delete"](data); - }, 100); - return; - } - var li = $("#queue").children()[data.position]; - $(li).remove(); + queueAction({ + fn: function () { + var li = $(".pluid-" + data.uid); + li.hide("blind", function() { + li.remove(); + }); + return true; + } + }); }, moveVideo: function(data) { - // Wait until any pending manipulation is finished - if(PL_MOVING || PL_ADDING || PL_DELETING) { - setTimeout(function() { - Callbacks.moveVideo(position); - }, 100); - return; + if(data.moveby != CLIENT.name) { + queueAction({ + fn: function () { + playlistMove(data.from, data.after); + return true; + } + }); } - if(data.from < POSITION && data.to >= POSITION) - POSITION--; - else if(data.from > POSITION && data.to <= POSITION) - POSITION++; - else if(data.from == POSITION) - POSITION = data.to; - if(data.moveby != CLIENT.name) - playlistMove(data.from, data.to); }, - setPosition: function(position) { - // Wait until any pending manipulation is finished - if(PL_MOVING || PL_ADDING || PL_DELETING) { - setTimeout(function() { - Callbacks.setPosition(position); - }, 100); - return; - } - $("#queue li").each(function() { - $(this).removeClass("queue_active"); + setCurrent: function(uid) { + queueAction({ + fn: function () { + PL_CURRENT = uid; + var qli = $("#queue li"); + qli.removeClass("queue_active"); + var li = $(".pluid-" + uid); + if(li.length == 0) { + return false; + } + + li.addClass("queue_active"); + scrollQueue(); + return true; + }, + can_wait: true }); - if(position < 0) - return; - POSITION = position; - var linew = $("#queue").children()[POSITION]; - // jQuery UI's sortable thingy kinda fucks this up initially - // Wait until it's done - if(!$(linew).hasClass("queue_entry")) { - setTimeout(function() { - Callbacks.setPosition(position); - }, 100); - return; - } - $(linew).addClass("queue_active"); - - $("#queue").scrollTop(0); - var scroll = $(linew).position().top - $("#queue").position().top; - $("#queue").scrollTop(scroll); - - if(CHANNEL.opts.allow_voteskip) - $("#voteskip").attr("disabled", false); }, changeMedia: function(data) { + if(CHANNEL.opts.allow_voteskip) + $("#voteskip").attr("disabled", false); + $("#currenttitle").text("Currently Playing: " + data.title); + if(data.type != "sc" && PLAYER.type == "sc") // [](/goddamnitmango) fixSoundcloudShit(); @@ -859,7 +857,6 @@ Callbacks = { for(var i = 0; i < data.options.length; i++) { (function(i) { var callback = function() { - console.log("vote", i); socket.emit("vote", { option: i }); @@ -990,11 +987,14 @@ Callbacks = { } } } + +var SOCKET_DEBUG = true; setupCallbacks = function() { - console.log(socket); for(var key in Callbacks) { (function(key) { socket.on(key, function(data) { + if(SOCKET_DEBUG) + console.log(key, data); Callbacks[key](data); }); })(key); diff --git a/www/assets/js/channelsettings.js b/www/assets/js/channelsettings.js index bf2d5d0a..f222578f 100644 --- a/www/assets/js/channelsettings.js +++ b/www/assets/js/channelsettings.js @@ -49,9 +49,21 @@ genPermissionsEditor(); $("#chanopts_submit").click(function() { + var hms = $("#opt_maxlength").val().split(":"); + var len = 0; + if(hms.length == 3) { + len = parseInt(hms[0]) * 3600 + parseInt(hms[1]) * 60 + parseInt(hms[2]); + } + else if(hms.length == 2) { + len = parseInt(hms[0]) * 60 + parseInt(hms[1]); + } + else { + len = parseInt(hms[0]); + } socket.emit("setOptions", { allow_voteskip: $("#opt_allow_voteskip").prop("checked"), voteskip_ratio: parseFloat($("#opt_voteskip_ratio").val()), + maxlength: len, pagetitle: $("#opt_pagetitle").val() || CHANNEL.name, externalcss: $("#opt_externalcss").val(), externaljs: $("#opt_externaljs").val(), diff --git a/www/assets/js/data.js b/www/assets/js/data.js index 9f86b361..cb7a0fe4 100644 --- a/www/assets/js/data.js +++ b/www/assets/js/data.js @@ -40,7 +40,7 @@ if($("#ytapiplayer").length > 0) { var VWIDTH = $("#ytapiplayer").parent().css("width").replace("px", ""); var VHEIGHT = ""+parseInt(parseInt(VWIDTH) * 9 / 16); } -var POSITION = -1; +var MEDIA = { hash: "" }; var PL_MOVING = false; var PL_ADDING = false; var PL_DELETING = false; @@ -64,8 +64,8 @@ var KICKED = false; var NAME = readCookie("cytube_uname"); var SESSION = readCookie("cytube_session"); var LEADTMR = false; -var PL_FROM = 0; -var PL_TO = 0; +var PL_FROM = ""; +var PL_AFTER = ""; var FILTER_FROM = 0; var FILTER_TO = 0; var NO_STORAGE = typeof localStorage == "undefined" || localStorage === null; diff --git a/www/assets/js/ui.js b/www/assets/js/ui.js index 78468de5..2f3b5a5b 100644 --- a/www/assets/js/ui.js +++ b/www/assets/js/ui.js @@ -211,16 +211,18 @@ $("#mediarefresh").click(function() { $("#queue").sortable({ start: function(ev, ui) { - PL_FROM = ui.item.prevAll().length; + PL_FROM = ui.item.data("uid"); }, update: function(ev, ui) { - PL_TO = ui.item.prevAll().length; - if(PL_TO != PL_FROM) { - socket.emit("moveMedia", { - from: PL_FROM, - to: PL_TO - }); - } + var prev = ui.item.prevAll(); + if(prev.length == 0) + PL_AFTER = "prepend"; + else + PL_AFTER = $(prev[0]).data("uid"); + socket.emit("moveMedia", { + from: PL_FROM, + after: PL_AFTER + }); } }); $("#queue").disableSelection(); @@ -337,12 +339,14 @@ $("#shuffleplaylist").click(function() { /* layout stuff */ $(window).resize(function() { - VWIDTH = $("#ytapiplayer").parent().css("width").replace("px", ""); - var VHEIGHT = ""+parseInt(parseInt(VWIDTH) * 9 / 16); + VWIDTH = $("#queue").css("width").replace("px", ""); + VHEIGHT = ""+parseInt(parseInt(VWIDTH) * 9 / 16); $("#messagebuffer").css("height", (VHEIGHT - 31) + "px"); $("#userlist").css("height", (VHEIGHT - 31) + "px"); - $("#ytapiplayer").attr("width", VWIDTH); - $("#ytapiplayer").attr("height", VHEIGHT); + if($("#ytapiplayer").length > 0) { + $("#ytapiplayer").attr("width", VWIDTH); + $("#ytapiplayer").attr("height", VHEIGHT); + } }); diff --git a/www/assets/js/util.js b/www/assets/js/util.js index ffbe59d5..5c6da1c8 100644 --- a/www/assets/js/util.js +++ b/www/assets/js/util.js @@ -204,7 +204,48 @@ function addUserDropdown(entry, name) { /* queue stuff */ -function makeQueueEntry(video, addbtns) { +function scrollQueue() { + var li = playlistFind(PL_CURRENT); + if(!li) + return; + + li = $(li); + $("#queue").scrollTop(0); + var scroll = li.position().top - $("#queue").position().top; + $("#queue").scrollTop(scroll); +} + +function makeQueueEntry(item, addbtns) { + var video = item.media; + var li = $("
  • "); + li.addClass("queue_entry"); + li.addClass("pluid-" + item.uid); + li.data("uid", item.uid); + li.data("media", video); + li.data("temp", item.temp); + if(video.thumb) { + $("").attr("src", video.thumb.url) + .css("float", "left") + .css("clear", "both") + .appendTo(li); + } + var title = $("").addClass("qe_title").appendTo(li) + .text(video.title) + .attr("href", formatURL(video)) + .attr("target", "_blank"); + var time = $("").addClass("qe_time").appendTo(li); + time.text(video.duration); + var clear = $("
    ").addClass("qe_clear").appendTo(li); + if(item.temp) { + li.addClass("queue_temp"); + } + + if(addbtns) + addQueueButtons(li); + return li; +} + +function makeSearchEntry(video) { var li = $("
  • "); li.addClass("queue_entry"); li.data("media", video); @@ -221,12 +262,7 @@ function makeQueueEntry(video, addbtns) { var time = $("").addClass("qe_time").appendTo(li); time.text(video.duration); var clear = $("
    ").addClass("qe_clear").appendTo(li); - if(video.temp) { - li.addClass("queue_temp"); - } - if(addbtns) - addQueueButtons(li); return li; } @@ -238,8 +274,7 @@ function addQueueButtons(li) { $("
    + +
    + +
    + +
    +

    Admin-Only Controls