CyTube/lib/channel/playlist.js
Calvin Montgomery 9deff9bdb1 Change charset for certain fields to utf8mb4
The underlying cause of #419 is the default utf8 collation in MySQL/MariaDB, which only supports the base plane of Unicode (\u0000-\uffff).  By changing the collation to utf8mb4_general_ci, stuff like ban reasons and profile text may have emoji and other non-base-plane Unicode.

The charset for playlist titles is NOT changed, and non-base-plane characters are replaced by question marks.  This is because switching to utf8mb4 would make the primary key too long.
2014-12-14 21:53:25 -05:00

1360 lines
39 KiB
JavaScript

var ChannelModule = require("./module"); var ULList = require("../ullist");
var AsyncQueue = require("../asyncqueue");
var Media = require("../media");
var util = require("../utilities");
var InfoGetter = require("../get-info");
var vimeoWorkaround = InfoGetter.vimeoWorkaround;
var Config = require("../config");
var Flags = require("../flags");
var db = require("../database");
var Logger = require("../logger");
const MAX_ITEMS = Config.get("playlist.max-items");
const TYPE_QUEUE = {
id: "string,boolean",
type: "string",
pos: "string",
title: "string,boolean,optional",
duration: "number,optional",
temp: "boolean,optional"
};
const TYPE_SET_TEMP = {
uid: "number",
temp: "boolean"
};
const TYPE_MOVE_MEDIA = {
from: "number",
after: "string,number"
};
const TYPE_ASSIGN_LEADER = {
name: "string"
};
const TYPE_MEDIA_UPDATE = {
id: "string",
currentTime: "number",
paused: "boolean,optional",
type: "string,optional"
};
const TYPE_CLONE_PLAYLIST = {
name: "string"
};
const TYPE_QUEUE_PLAYLIST = {
name: "string",
pos: "string",
temp: "boolean,optional"
};
function PlaylistItem(media, opts) {
if (typeof opts !== "object") {
opts = {};
}
this.media = media;
this.uid = opts.uid;
this.temp = Boolean(opts.temp);
this.queueby = (typeof opts.queueby === "string") ? opts.queueby : "";
this.next = null;
this.prev = null;
}
PlaylistItem.prototype = {
pack: function () {
return {
media: this.media.pack(),
uid: this.uid,
temp: this.temp,
queueby: this.queueby
};
}
};
function PlaylistModule(channel) {
ChannelModule.apply(this, arguments);
this.items = new ULList();
this.meta = {
count: 0,
rawTime: 0,
time: util.formatTime(0)
};
this.current = null;
this._nextuid = 0;
this.semaphore = new AsyncQueue();
this.leader = null;
this._leadInterval = false;
this._lastUpdate = 0;
this._counter = 0;
this._gdRefreshTimer = false;
if (this.channel.modules.chat) {
this.channel.modules.chat.registerCommand("/clean", this.handleClean.bind(this));
this.channel.modules.chat.registerCommand("/cleantitle", this.handleClean.bind(this));
}
}
PlaylistModule.prototype = Object.create(ChannelModule.prototype);
PlaylistModule.prototype.load = function (data) {
var self = this;
var playlist = data.playlist;
if (typeof playlist !== "object" || !("pl" in playlist)) {
return;
}
var i = 0;
playlist.pos = parseInt(playlist.pos);
playlist.pl.forEach(function (item) {
var m = new Media(item.media.id, item.media.title, item.media.seconds,
item.media.type, item.media.meta || {});
var newitem = new PlaylistItem(m, {
uid: self._nextuid++,
temp: item.temp,
queueby: item.queueby
});
self.items.append(newitem);
self.meta.count++;
self.meta.rawTime += m.seconds;
if (playlist.pos === i) {
self.current = newitem;
}
i++;
});
self.meta.time = util.formatTime(self.meta.rawTime);
self.startPlayback(playlist.time);
};
PlaylistModule.prototype.save = function (data) {
var arr = this.items.toArray().map(function (item) {
/* Clear Google Docs and Vimeo meta */
if (item.media && item.media.meta) {
delete item.media.meta.object;
delete item.media.meta.params;
delete item.media.meta.direct;
}
return item;
});
var pos = 0;
for (var i = 0; i < arr.length; i++) {
if (this.current && arr[i].uid == this.current.uid) {
pos = i;
break;
}
}
var time = 0;
if (this.current) {
time = this.current.media.currentTime;
}
data.playlist = {
pl: arr,
pos: pos,
time: time
};
};
PlaylistModule.prototype.unload = function () {
if (this._leadInterval) {
clearInterval(this._leadInterval);
this._leadInterval = false;
}
if (this._gdRefreshTimer) {
clearInterval(this._gdRefreshTimer);
this._gdRefreshTimer = false;
}
this.channel = null;
};
PlaylistModule.prototype.packInfo = function (data, isAdmin) {
if (this.current) {
data.mediatitle = this.current.media.title;
if (isAdmin) {
data.mediaLink = util.formatLink(this.current.media.id, this.current.media.type);
}
} else {
data.mediatitle = "(Nothing Playing)";
if (isAdmin) {
data.mediaLink = "#";
}
}
if (isAdmin) {
if (this.leader) {
data.leader = this.leader.getName();
} else {
data.leader = "[server]";
}
}
};
PlaylistModule.prototype.onUserPostJoin = function (user) {
this.sendPlaylist([user]);
this.sendChangeMedia([user]);
user.socket.typecheckedOn("queue", TYPE_QUEUE, this.handleQueue.bind(this, user));
user.socket.typecheckedOn("setTemp", TYPE_SET_TEMP, this.handleSetTemp.bind(this, user));
user.socket.typecheckedOn("moveMedia", TYPE_MOVE_MEDIA, this.handleMoveMedia.bind(this, user));
user.socket.on("delete", this.handleDelete.bind(this, user));
user.socket.on("jumpTo", this.handleJumpTo.bind(this, user));
user.socket.on("playNext", this.handlePlayNext.bind(this, user));
user.socket.typecheckedOn("assignLeader", TYPE_ASSIGN_LEADER, this.handleAssignLeader.bind(this, user));
user.socket.typecheckedOn("mediaUpdate", TYPE_MEDIA_UPDATE, this.handleUpdate.bind(this, user));
var self = this;
user.socket.on("playerReady", function () {
self.sendChangeMedia([user]);
});
user.socket.on("requestPlaylist", function () {
self.sendPlaylist([user]);
});
user.socket.on("clearPlaylist", this.handleClear.bind(this, user));
user.socket.on("shufflePlaylist", this.handleShuffle.bind(this, user));
/* User playlists */
user.socket.on("listPlaylists", this.handleListPlaylists.bind(this, user));
if (user.is(Flags.U_REGISTERED)) {
this.handleListPlaylists(user);
}
user.socket.typecheckedOn("clonePlaylist", TYPE_CLONE_PLAYLIST, this.handleClonePlaylist.bind(this, user));
user.socket.typecheckedOn("deletePlaylist", TYPE_CLONE_PLAYLIST, this.handleDeletePlaylist.bind(this, user));
user.socket.typecheckedOn("queuePlaylist", TYPE_QUEUE_PLAYLIST, this.handleQueuePlaylist.bind(this, user));
};
PlaylistModule.prototype.onUserPart = function (user) {
if (this.leader === user) {
this.leader = null;
this.channel.broadcastAll("setLeader", "");
this.channel.logger.log("[playlist] Resuming autolead");
if (this.current !== null) {
this.current.media.paused = false;
if (!this._leadInterval) {
this._lastUpdate = Date.now();
this._leadInterval = setInterval(this._leadLoop.bind(this), 1000);
this._leadLoop();
}
}
}
};
/**
* == Functions for sending various playlist data to users ==
*/
PlaylistModule.prototype.sendPlaylist = function (users) {
var pl = this.items.toArray(true);
var perms = this.channel.modules.permissions;
var self = this;
users.forEach(function (u) {
u.socket.emit("setPlaylistMeta", self.meta);
if (!perms.canSeePlaylist(u)) {
return;
}
u.socket.emit("playlist", pl);
if (self.current) {
u.socket.emit("setCurrent", self.current.uid);
}
});
};
PlaylistModule.prototype.sendChangeMedia = function (users) {
if (!this.current || !this.current.media) {
return;
}
var update = this.current.media.getFullUpdate();
var uid = this.current.uid;
if (users === this.channel.users) {
this.channel.broadcastAll("setCurrent", uid);
this.channel.broadcastAll("changeMedia", update);
var m = this.current.media;
this.channel.logger.log("[playlist] Now playing: " + m.title +
" (" + m.type + ":" + m.id + ")");
} else {
users.forEach(function (u) {
u.socket.emit("setCurrent", uid);
u.socket.emit("changeMedia", update);
});
}
};
PlaylistModule.prototype.sendMediaUpdate = function (users) {
if (!this.current || !this.current.media) {
return;
}
var update = this.current.media.getTimeUpdate();
if (users === this.channel.users) {
this.channel.broadcastAll("mediaUpdate", update);
} else {
users.forEach(function (u) {
u.socket.emit("mediaUpdate", update);
});
}
};
/**
* == Handlers for playlist manipulation ==
*/
PlaylistModule.prototype.handleQueue = function (user, data) {
if (typeof data.id === "boolean" && data.id !== false) {
return;
}
var id = data.id;
var type = data.type;
if (data.pos !== "next" && data.pos !== "end") {
return;
}
/**
* Specifying a custom title is currently only allowed for custom media
* and raw files
*/
if (typeof data.title !== "string" || (data.type !== "cu" && data.type !== "fi")) {
data.title = false;
}
var link = util.formatLink(id, type);
var perms = this.channel.modules.permissions;
if (!perms.canAddVideo(user, data)) {
return;
}
if (data.pos === "next" && !perms.canAddNext(user)) {
return;
}
/* Certain media types require special permission to add */
if (data.type === "yp" && !perms.canAddList(user)) {
user.socket.emit("queueFail", {
msg: "You don't have permission to add playlists",
link: link
});
return;
} else if (util.isLive(type) && !perms.canAddLive(user)) {
user.socket.emit("queueFail", {
msg: "You don't have permission to add live media",
link: link
});
return;
} else if (type === "cu" && !perms.canAddCustom(user)) {
user.socket.emit("queueFail", {
msg: "You don't have permission to add custom embeds",
link: link
});
return;
} else if (type === "fi" && !perms.canAddRawFile(user)) {
user.socket.emit("queueFail", {
msg: "You don't have permission to add raw video files",
link: link
});
return;
}
var temp = data.temp || !perms.canAddNonTemp(user);
var queueby = user.getName();
var duration = undefined;
/**
* Duration can optionally be specified for a livestream.
* The UI for it only shows up for jw: queues, but it is
* accepted for any live media
*/
if (util.isLive(type) && typeof data.duration === "number") {
duration = !isNaN(data.duration) ? data.duration : undefined;
}
var limit = {
burst: 3,
sustained: 1
};
if (user.account.effectiveRank >= 2) {
limit = {
burst: 10,
sustained: 2
};
}
if (user.queueLimiter.throttle(limit)) {
user.socket.emit("queueFail", {
msg: "You are adding videos too quickly",
link: link
});
return;
}
var maxlength = 0;
if (!perms.canExceedMaxLength(user)) {
if (this.channel.modules.options) {
maxlength = this.channel.modules.options.get("maxlength");
}
}
data = {
id: data.id,
type: data.type,
pos: data.pos,
title: data.title,
link: link,
temp: temp,
shouldAddToLibrary: !temp,
queueby: queueby,
duration: duration,
maxlength: maxlength
};
if (data.type === "yp") {
this.queueYouTubePlaylist(user, data);
} else {
this.queueStandard(user, data);
}
};
PlaylistModule.prototype.queueStandard = function (user, data) {
var error = function (what) {
user.socket.emit("queueFail", {
msg: what,
link: data.link
});
};
var self = this;
this.channel.activeLock.lock();
this.semaphore.queue(function (lock) {
var lib = self.channel.modules.library;
if (lib && self.channel.is(Flags.C_REGISTERED) && !util.isLive(data.type)) {
lib.getItem(data.id, function (err, item) {
if (err && err !== "Item not in library") {
error(err+"");
self.channel.activeLock.release();
return lock.release();
}
if (item !== null) {
/* Don't re-cache data we got from the library */
data.shouldAddToLibrary = false;
self._addItem(item, data, user, function () {
lock.release();
self.channel.activeLock.release();
});
} else {
handleLookup();
}
});
} else {
handleLookup();
}
function handleLookup() {
InfoGetter.getMedia(data.id, data.type, function (err, media) {
if (err) {
error(err+"");
self.channel.activeLock.release();
return lock.release();
}
self._addItem(media, data, user, function () {
lock.release();
self.channel.activeLock.release();
});
});
}
});
};
PlaylistModule.prototype.queueYouTubePlaylist = function (user, data) {
var error = function (what) {
user.socket.emit("queueFail", {
msg: what,
link: data.link
});
};
var self = this;
this.semaphore.queue(function (lock) {
InfoGetter.getMedia(data.id, data.type, function (err, vids) {
if (err) {
error(err+"");
return lock.release();
}
if (self.dead) {
return lock.release();
}
/**
* Add videos in reverse order if queueing a playlist next.
* This is because each video gets added after the currently playing video
*/
if (data.pos === "next") {
vids = vids.reverse();
/* Special case: when the playlist is empty, add the real first video */
if (self.items.length === 0) {
vids.unshift(vids.pop());
}
}
self.channel.activeLock.lock();
vids.forEach(function (media) {
self._addItem(media, data, user);
});
self.channel.activeLock.release();
lock.release();
});
});
};
PlaylistModule.prototype.handleDelete = function (user, data) {
var self = this;
var perms = this.channel.modules.permissions;
if (!perms.canDeleteVideo(user)) {
return;
}
if (typeof data !== "number") {
return;
}
var plitem = this.items.find(data);
self.channel.activeLock.lock();
this.semaphore.queue(function (lock) {
if (self._delete(data)) {
self.channel.logger.log("[playlist] " + user.getName() + " deleted " +
plitem.media.title);
}
lock.release();
self.channel.activeLock.release();
});
};
PlaylistModule.prototype.handleSetTemp = function (user, data) {
if (!this.channel.modules.permissions.canSetTemp(user)) {
return;
}
var item = this.items.find(data.uid);
if (!item) {
return;
}
item.temp = data.temp;
this.channel.broadcastAll("setTemp", data);
if (!data.temp && this.channel.modules.library) {
this.channel.modules.library.cacheMedia(item.media);
}
};
PlaylistModule.prototype.handleMoveMedia = function (user, data) {
if (!this.channel.modules.permissions.canMoveVideo(user)) {
return;
}
var from = this.items.find(data.from);
var after = this.items.find(data.after);
if (!from || from === after) {
return;
}
var self = this;
self.channel.activeLock.lock();
self.semaphore.queue(function (lock) {
if (!self.items.remove(data.from)) {
self.channel.activeLock.release();
return lock.release();
}
if (data.after === "prepend") {
if (!self.items.prepend(from)) {
self.channel.activeLock.release();
return lock.release();
}
} else if (data.after === "append") {
if (!self.items.append(from)) {
self.channel.activeLock.release();
return lock.release();
}
} else {
if (!self.items.insertAfter(from, data.after)) {
self.channel.activeLock.release();
return lock.release();
}
}
self.channel.broadcastAll("moveVideo", data);
self.channel.logger.log("[playlist] " + user.getName() + " moved " +
from.media.title +
(after ? " after " + after.media.title : ""));
lock.release();
self.channel.activeLock.release();
});
};
PlaylistModule.prototype.handleJumpTo = function (user, data) {
if (typeof data !== "string" && typeof data !== "number") {
return;
}
if (!this.channel.modules.permissions.canSkipVideo(user)) {
return;
}
var to = this.items.find(data);
var title = "";
if (this.current) {
title = " from " + this.current.media.title;
}
if (to) {
title += " to " + to.media.title;
var old = this.current;
this.current = to;
this.startPlayback();
this.channel.logger.log("[playlist] " + user.getName() + " skipped" + title);
if (old && old.temp) {
this._delete(old.uid);
}
}
};
PlaylistModule.prototype.handlePlayNext = function (user) {
if (!this.channel.modules.permissions.canSkipVideo(user)) {
return;
}
var title = "";
if (this.current) {
title = this.current.media.title;
}
this.channel.logger.log("[playlist] " + user.getName() + " skipped" + title);
this._playNext();
};
PlaylistModule.prototype.handleClear = function (user) {
if (!this.channel.modules.permissions.canClearPlaylist(user)) {
return;
}
this.channel.logger.log("[playlist] " + user.getName() + " cleared the playlist");
this.current = null;
this.items.clear();
this.semaphore.reset();
this.meta = {
count: 0,
rawTime: 0,
time: util.formatTime(0)
};
this.channel.broadcastAll("playlist", []);
this.channel.broadcastAll("setPlaylistMeta", this.meta);
};
PlaylistModule.prototype.handleShuffle = function (user) {
if (!this.channel.modules.permissions.canShufflePlaylist(user)) {
return;
}
this.channel.logger.log("[playlist] " + user.getName() + " shuffled the playlist");
var pl = this.items.toArray(false);
this.items.clear();
this.semaphore.reset();
while (pl.length > 0) {
var i = Math.floor(Math.random() * pl.length);
var item = new PlaylistItem(pl[i].media, {
uid: this._nextuid++,
temp: pl[i].temp,
queueby: pl[i].queueby
});
this.items.append(item);
pl.splice(i, 1);
}
this.current = this.items.first;
pl = this.items.toArray(true);
var perms = this.channel.modules.permissions;
this.channel.users.forEach(function (u) {
if (perms.canSeePlaylist(u)) {
u.socket.emit("playlist", pl);
};
});
this.startPlayback();
};
/**
* == Leader stuff ==
*/
PlaylistModule.prototype.handleAssignLeader = function (user, data) {
if (!this.channel.modules.permissions.canAssignLeader(user)) {
return user.kick("Attempted assignLeader without sufficient permission");
}
var name = data.name;
if (this.leader) {
var old = this.leader;
this.leader = null;
if (old.account.effectiveRank === 1.5) {
old.account.effectiveRank = old.account.oldRank;
old.socket.emit("rank", old.account.effectiveRank);
}
this.channel.broadcastAll("setUserRank", {
name: old.getName(),
rank: old.account.effectiveRank
});
this.channel.logger.log("[mod] " + user.getName() + " removed leader from " + old.getName());
}
if (!name) {
this.channel.broadcastAll("setLeader", "");
this.channel.logger.log("[playlist] Resuming autolead");
if (this.current !== null) {
this.current.media.paused = false;
if (!this._leadInterval) {
this._lastUpdate = Date.now();
this._leadInterval = setInterval(this._leadLoop.bind(this), 1000);
this._leadLoop();
}
}
return;
}
for (var i = 0; i < this.channel.users.length; i++) {
if (this.channel.users[i].getName() === name) {
this.channel.logger.log("[playlist] Assigned leader: " + name);
this.leader = this.channel.users[i];
if (this._leadInterval) {
clearInterval(this._leadInterval);
this._leadInterval = false;
}
if (this.leader.account.effectiveRank < 1.5) {
this.leader.account.oldRank = this.leader.account.effectiveRank;
this.leader.account.effectiveRank = 1.5;
this.leader.socket.emit("rank", 1.5);
}
this.channel.broadcastAll("setLeader", name);
if (this.leader.account.effectiveRank === 1.5) {
this.channel.broadcastAll("setUserRank", {
name: name,
rank: 1.5
});
}
break;
}
}
if (this.leader === null) {
user.socket.emit("errorMsg", {
msg: "Unable to assign leader: could not find user " + name
});
return;
}
this.channel.logger.log("[mod] " + user.getName() + " assigned leader to " + data.name);
};
PlaylistModule.prototype.handleUpdate = function (user, data) {
if (this.leader !== user) {
return;
}
if (!this.current) {
return;
}
var media = this.current.media;
if (util.isLive(media.type) && media.type !== "jw") {
return;
}
if (media.id !== data.id || isNaN(data.currentTime)) {
return;
}
media.currentTime = data.currentTime;
media.paused = Boolean(data.paused);
var update = media.getTimeUpdate();
this.channel.broadcastAll("mediaUpdate", update);
};
/**
* == Internal playlist manipulation ==
*/
PlaylistModule.prototype._delete = function (uid) {
var self = this;
var perms = this.channel.modules.permissions;
var item = self.items.find(uid);
if (!item) {
return false;
}
var next = item.next || this.items.first;
var success = self.items.remove(uid);
if (success) {
self.meta.count--;
self.meta.rawTime -= item.media.seconds;
self.meta.time = util.formatTime(self.meta.rawTime);
self.channel.users.forEach(function (u) {
if (perms.canSeePlaylist(u)) {
u.socket.emit("delete", { uid: uid });
}
u.socket.emit("setPlaylistMeta", self.meta);
});
}
if (self.current === item && item === next) {
self.current = null;
} else if (self.current === item) {
self.current = next;
self.startPlayback();
}
return success;
};
PlaylistModule.prototype._addItem = function (media, data, user, cb) {
var self = this;
var allowDuplicates = false;
if (this.channel.modules.options && this.channel.modules.options.get("allow_dupes")) {
allowDuplicates = true;
}
var qfail = function (msg) {
user.socket.emit("queueFail", {
msg: msg,
link: data.link
});
if (cb) {
cb();
}
};
if (data.duration) {
media.seconds = data.duration;
media.duration = util.formatTime(media.seconds);
}
if (data.maxlength > 0 && media.seconds > data.maxlength) {
return qfail("Video exceeds the maximum length set by the channel admin: " +
data.maxlength + " seconds");
}
if (this.items.length >= MAX_ITEMS) {
return qfail("Playlist limit reached (" + MAX_ITEMS + ")");
}
var existing = this.items.findVideoId(media.id);
if (existing && !allowDuplicates && (data.pos === "end" || existing === this.current)) {
return qfail("This item is already on the playlist");
}
var usersItems = this.items.findAll(function (item) {
return item.queueby.toLowerCase() === user.getLowerName();
});
if (this.channel.modules.options &&
this.channel.modules.options.get("playlist_max_per_user") &&
usersItems.length >= this.channel.modules.options.get("playlist_max_per_user")) {
if (!this.channel.modules.permissions.canExceedMaxItemsPerUser(user)) {
return qfail("Channel limit exceeded: maximum number of videos per user");
}
}
/* Warn about blocked countries */
if (media.meta.restricted) {
user.socket.emit("queueWarn", {
msg: "Video is blocked in the following countries: " + media.meta.restricted,
link: data.link
});
}
/* Warn about high bitrate for raw files */
if (media.type === "fi" && media.meta.bitrate > 1000) {
user.socket.emit("queueWarn", {
msg: "This video has a bitrate over 1000kbps. Clients with slow " +
"connections may experience lots of buffering."
});
}
/* Warn about possibly unsupported formats */
if (media.type === "fi" && media.meta.codec &&
media.meta.codec.indexOf("/") !== -1 &&
media.meta.codec !== "mov/h264" &&
media.meta.codec !== "flv/h264") {
user.socket.emit("queueWarn", {
msg: "The codec <code>" + media.meta.codec + "</code> is not supported " +
"by all browsers, and is not supported by the flash fallback layer. " +
"This video may not play for some users."
});
}
var item = new PlaylistItem(media, {
uid: self._nextuid++,
temp: data.temp,
queueby: data.queueby
});
if (data.title && (media.type === "cu" || media.type === "fi")) {
media.setTitle(data.title);
}
var success = function () {
var packet = {
item: item.pack(),
after: item.prev ? item.prev.uid : "prepend"
};
self.meta.count++;
self.meta.rawTime += media.seconds;
self.meta.time = util.formatTime(self.meta.rawTime);
var perms = self.channel.modules.permissions;
self.channel.users.forEach(function (u) {
if (perms.canSeePlaylist(u)) {
u.socket.emit("queue", packet);
}
u.socket.emit("setPlaylistMeta", self.meta);
});
if (data.shouldAddToLibrary && !util.isLive(media.type)) {
if (self.channel.modules.library) {
self.channel.modules.library.cacheMedia(media);
}
}
if (self.items.length === 1) {
self.current = item;
self.startPlayback();
}
if (cb) {
cb();
}
};
if (data.pos === "end" || this.current == null) {
this.items.append(item);
return success();
} else {
if (this.items.insertAfter(item, this.current.uid)) {
if (existing && !allowDuplicates) {
item.temp = existing.temp;
this._delete(existing.uid);
}
return success();
} else {
return qfail("Playlist failure");
}
}
};
function isExpired(media) {
if (media.meta.expiration && media.meta.expiration < Date.now()) {
return true;
} else if (media.type === "gd") {
return !media.meta.object;
} else if (media.type === "vi") {
return !media.meta.direct;
}
}
PlaylistModule.prototype.startPlayback = function (time) {
var self = this;
if (!self.current || !self.current.media) {
return false;
}
var media = self.current.media;
media.reset();
if (self.leader != null) {
media.paused = false;
media.currentTime = time || 0;
self.channel.checkModules("onPreMediaChange", [self.current.media],
function () {
/*
* onPreMediaChange doesn't care about the callback result.
* Its purpose is to allow modification of playback data before
* users are sent a changeMedia
*/
if (!self.current || !self.current.media) {
return;
}
self.sendChangeMedia(self.channel.users);
self.channel.notifyModules("onMediaChange", [self.current.media]);
}
);
return;
}
/* Lead-in time of 3 seconds to allow clients to buffer */
time = time || (media.seconds > 0 ? -3 : 0);
media.paused = time < 0;
media.currentTime = time;
/* Module was already leading, stop the previous timer */
if (self._leadInterval) {
clearInterval(self._leadInterval);
self._leadInterval = false;
}
self.channel.checkModules("onPreMediaChange", [self.current.media],
function () {
/*
* onPreMediaChange currently doesn't care about the callback result.
* Its purpose is to allow modification of playback data before
* users are sent a changeMedia
*/
if (!self.current || !self.current.media) {
return;
}
self.sendChangeMedia(self.channel.users);
self.channel.notifyModules("onMediaChange", [self.current.media]);
/* Only start the timer if the media item is not live, i.e. has a duration */
if (media.seconds > 0) {
self._lastUpdate = Date.now();
self._leadInterval = setInterval(function() {
self._leadLoop();
}, 1000);
}
}
);
}
const UPDATE_INTERVAL = Config.get("playlist.update-interval");
PlaylistModule.prototype._leadLoop = function() {
if (this.current == null) {
return;
}
if (!this.channel || this.channel.dead) {
if (this._leadInterval) {
clearInterval(this._leadInterval);
this._leadInterval = false;
}
return;
}
var dt = (Date.now() - this._lastUpdate) / 1000.0;
var t = this.current.media.currentTime;
/* Transition from lead-in to playback */
if (t < 0 && (t + dt) >= 0) {
this.current.media.currentTime = 0;
this.current.media.paused = false;
this._counter = 0;
this._lastUpdate = Date.now();
this.sendMediaUpdate(this.channel.users);
return;
}
this.current.media.currentTime += dt;
this._lastUpdate = Date.now();
this._counter++;
/**
* Don't transition until 2 seconds after the end, to allow slightly
* off-sync clients to catch up
*/
if (this.current.media.currentTime >= this.current.media.seconds + 2) {
this._playNext();
} else if(this._counter % UPDATE_INTERVAL == 0) {
this.sendMediaUpdate(this.channel.users);
}
};
PlaylistModule.prototype.refreshGoogleDocs = function (cb) {
var self = this;
if (self.dead || !self.channel || self.channel.dead) {
return;
}
var abort = function () {
clearInterval(self._gdRefreshTimer);
self._gdRefreshTimer = false;
if (self.current) {
self.current.media.meta.object = self.current.media.meta.object || null;
self.current.media.meta.failed = true;
}
if (cb) {
cb();
}
};
if (!this.current || this.current.media.type !== "gd") {
return abort();
}
self.channel.activeLock.lock();
InfoGetter.getMedia(this.current.media.id, "gd", function (err, media) {
if (err) {
Logger.errlog.log("Google Docs autorefresh failed: " + err);
Logger.errlog.log("ID was: " + self.current.media.id);
if (self.current) {
self.current.media.meta.object = self.current.media.meta.object || null;
self.current.media.meta.failed = true;
}
if (cb) {
cb();
}
self.channel.activeLock.release();
} else {
if (!self.current || self.current.media.type !== "gd") {
self.channel.activeLock.release();
return abort();
}
self.current.media.meta = media.meta;
self.current.media.meta.expiration = Date.now() + 3600000;
self.channel.logger.log("[playlist] Auto-refreshed Google Doc video");
cb && cb();
self.channel.activeLock.release();
}
});
};
PlaylistModule.prototype._playNext = function () {
if (!this.current) {
return;
}
var next = this.current.next || this.items.first;
if (this.current.temp) {
/* The _delete handler will take care of starting the next video */
this._delete(this.current.uid);
} else if (next) {
this.current = next;
this.startPlayback();
}
};
PlaylistModule.prototype.clean = function (test) {
var self = this;
var matches = self.items.findAll(test);
matches.forEach(function (m) {
self._delete(m.uid);
});
};
/**
* == Command Handlers ==
*/
function generateTargetRegex(target) {
const flagsre = /^(-[img]+\s+)/i
var m = target.match(flagsre);
var flags = "";
if (m) {
flags = m[0].slice(1,-1);
target = target.replace(flagsre, "");
}
return new RegExp(target, flags);
}
PlaylistModule.prototype.handleClean = function (user, msg, meta) {
if (!this.channel.modules.permissions.canDeleteVideo(user)) {
return;
}
var args = msg.split(" ");
var cmd = args.shift();
var target = generateTargetRegex(args.join(" "));
var cleanfn;
if (cmd === "/clean") {
cleanfn = function (item) { return target.test(item.queueby); };
} else if (cmd === "/cleantitle") {
cleanfn = function (item) { return target.exec(item.media.title) !== null; };
}
this.clean(cleanfn);
};
/**
* == User playlist stuff ==
*/
PlaylistModule.prototype.handleListPlaylists = function (user) {
if (!user.is(Flags.U_REGISTERED)) {
return user.socket.emit("errorMsg", {
msg: "Only registered users can use the user playlist function."
});
}
db.listUserPlaylists(user.getName(), function (err, rows) {
if (err) {
user.socket.emit("errorMsg", {
msg: "Database error when attempting to fetch list of playlists"
});
return;
}
user.socket.emit("listPlaylists", rows);
});
};
PlaylistModule.prototype.handleClonePlaylist = function (user, data) {
if (!user.is(Flags.U_REGISTERED)) {
return user.socket.emit("errorMsg", {
msg: "Only registered users can use the user playlist function."
});
}
if (!this.channel.modules.permissions.canSeePlaylist(user)) {
return user.socket.emit("errorMsg", {
msg: "You are not allowed to save this playlist"
});
}
data.name = data.name.replace(/[^\u0000-\uffff]/g, "?");
var pl = this.items.toArray();
var self = this;
db.saveUserPlaylist(pl, user.getName(), data.name, function (err) {
if (err) {
user.socket.emit("errorMsg", {
msg: "Database error when saving playlist"
});
return;
}
self.handleListPlaylists(user);
});
};
PlaylistModule.prototype.handleDeletePlaylist = function (user, data) {
if (!user.is(Flags.U_REGISTERED)) {
return user.socket.emit("errorMsg", {
msg: "Only registered users can use the user playlist function."
});
}
var self = this;
db.deleteUserPlaylist(user.getName(), data.name, function (err) {
if (err) {
user.socket.emit("errorMsg", {
msg: "Database error when deleting playlist"
});
return;
}
self.handleListPlaylists(user);
});
};
PlaylistModule.prototype.handleQueuePlaylist = function (user, data) {
var perms = this.channel.modules.permissions;
if (!perms.canAddList(user)) {
return;
}
if (data.pos !== "next" && data.pos !== "end") {
return;
}
if (data.pos === "next" && !perms.canAddNext(user)) {
return;
}
var temp = data.temp || !perms.canAddNonTemp(user);
var maxlength = 0;
if (!perms.canExceedMaxLength(user)) {
if (this.channel.modules.options) {
maxlength = this.channel.modules.options.get("maxlength");
}
}
var qdata = {
temp: temp,
queueby: user.getName(),
maxlength: maxlength,
pos: data.pos
};
var self = this;
self.channel.activeLock.lock();
db.getUserPlaylist(user.getName(), data.name, function (err, pl) {
if (err) {
return user.socket.emit("errorMsg", {
msg: "Playlist load failed: " + err
});
}
try {
if (data.pos === "next") {
pl.reverse();
if (pl.length > 0 && self.meta.count === 0) {
pl.unshift(pl.pop());
}
}
/* Ancient playlists don't have full data */
if (pl.length > 0 && !pl[0].hasOwnProperty("title")) {
pl.forEach(function (item) {
self.handleQueue(user, {
id: item.id,
type: item.type,
pos: data.pos,
temp: temp
});
});
return;
}
pl.forEach(function (item) {
var m = new Media(item.id, item.title, item.seconds, item.type, item.meta);
self._addItem(m, qdata, user);
});
self.channel.activeLock.release();
} catch (e) {
Logger.errlog.log("Loading user playlist failed!");
Logger.errlog.log("PL: " + user.getName() + "-" + data.name);
Logger.errlog.log(e.stack);
user.socket.emit("queueFail", {
msg: "Internal error occurred when loading playlist.",
link: null
});
self.channel.activeLock.release();
}
});
};
module.exports = PlaylistModule;