class Timer { constructor(callback, interval) { this.callback = callback; this.interval = interval; this.timerId = null; this.isRunning = false; } start() { if (!this.isRunning) { this.isRunning = true; this.timerId = setInterval(() => { this.callback(); }, this.interval); } } stop() { if (this.isRunning) { clearInterval(this.timerId); this.timerId = null; this.isRunning = false; } } getIsRunning() { return this.isRunning; } } const actions = { initPlayer: function (player, data) { player.init(); player.addTrack(data["tracks"]); player.updateCurrentQueuePos(data['current_queue_position']); player.isDJ = data['is_dj']; player.is_paused = data['is_paused']; player.current_position = data['current_position']; player.repeat = data['repeat_mode']; player.channelName = data["channel_name"]; data["users"].forEach(user => { player.addUser(user); }) $("#sortable").sortable("option", "disabled", !player.isDJ); }, playerUpdate: function (player, data) { player.last_update = data["last_update"]; player.is_connected = data["is_connected"]; player.current_position = data["last_position"]; }, trackUpdate: function (player, data) { var track = player.updateCurrentQueuePos(data['current_queue_position']); player.is_paused = data['is_paused']; if (track?.track_id != data["track_id"]) { player.send({ "op": "initPlayer" }) } }, addTrack: function (player, data) { player.addTrack(data["tracks"]); var tracks = data["tracks"]; if (tracks.length == 1) { var msg = `Added ${tracks[0]['info']['title']} songs into the queue.` } else { var msg = `Added ${tracks.length} into the queue.` } player.showToast(data["requester_id"], msg) }, getTracks: function (player, data) { var tracks = data["tracks"]; if (tracks != undefined) { const resultList = $("#search-result-list"); resultList.empty(); player.searchList = tracks; for (var i in tracks) { var track = new Track(tracks[i]); resultList.append(`
  • ${track.title}

    ${track.author}

    ${player.msToReadableTime(track.length)}

  • `) } } $("#search-result-list").fadeIn(200); $("#search-loader").fadeOut(200); }, playerClose: function (player, data) { player.init(); }, updateGuild: function (player, data) { const user = data["user"]; player.channelName = data["channel_name"]; if (user["user_id"] == player.userId) { if (data['is_joined']) { player.send({ "op": "initPlayer" }); } else { player.init(); } } if (data['is_joined']) { player.addUser(user); if (user["user_id"] != player.userId) { player.showToast(user["user_id"], "Joined your channel!"); } } else { if (player.users.hasOwnProperty(user["user_id"])) { player.showToast(user["user_id"], "Left your channel!"); delete player.users[user['user_id']]; player.updateUser(); } } }, updatePause: function (player, data) { player.is_paused = data['pause']; var msg = ""; if (data["pause"]) { msg = "Paused the player." } else { msg = "Resumed the player." } player.showToast(data["requester_id"], msg) }, updatePosition: function (player, data) { player.current_position = data["position"]; }, swapTrack: function (player, data) { var index1 = player.current_queue_position + data['position2']["index"]; var index2 = player.current_queue_position + data['position1']["index"]; var track1 = player.queue[index1]; var track2 = player.queue[index2]; if (track1?.track_id != data['position1']["track_id"] || track2?.track_id != data['position2']["track_id"]) { return player.send({ "op": "initPlayer" }); } player.queue[index1] = player.queue.splice(index2, 1, player.queue[index1])[0]; player.initSortable(); player.showToast(data["requester_id"], `${track1.title} and ${track2.title} are swapped`) }, moveTrack: function (player, data) { let position = player.current_queue_position + data["position"]["index"]; let newPosition = player.current_queue_position + data["newPosition"]["index"]; let element = player.queue.splice(position, 1)[0]; if (element?.track_id != data["position"]["track_id"]) { return player.send({ "op": "initPlayer" }); } player.queue.splice(newPosition, 0, element); const $ul = $('#sortable'); const $li = $ul.children().eq(position); $li.detach(); $ul.children().eq(newPosition).before($li); player.showToast(data["requester_id"], `Moved ${element.title} to ${newPosition}`) }, shuffleTrack: function (player, data) { var tracks = data["tracks"]; if (tracks != undefined) { player.queue = []; tracks.forEach(rawTrack => { var track = new Track(rawTrack); player.queue.push(track); }); player.initSortable(); player.showToast(data["requester_id"], "The queue is shuffled.") } }, repeatTrack: function (player, data) { player.repeat = data["repeatMode"]; }, removeTrack: function (player, data) { const positions = data["positions"]; const trackIds = data["track_ids"]; var removeTracks = [] var newQueue = []; player.queue.forEach((track, index) => { if (positions.includes(index)) { if (trackIds.includes(track.track_id)) { if (index < player.current_queue_position) { player.current_queue_position -= 1; } removeTracks.push(track); } else { return player.send({ "op": "initPlayer" }) } } else { newQueue.push(track); } }) player.queue = newQueue; player.initSortable(); if (removeTracks.length == 1) { var msg = `Removed ${removeTracks[0].title} from the queue.`; } else { var msg = `Removed ${removeTracks.length} tracks from the queue.`; } player.showToast(data["requester_id"], msg); }, errorMsg: function (player, data) { var level = data["level"]; player.showToast(level, data["msg"]); } } class Track { constructor(object) { this.title = object["info"]["title"]; this.author = object["info"]["author"]; this.imageUrl = object["thumbnail"]; this.length = object["info"]["length"]; this.track_id = object["track_id"]; this.uri = object["info"]["uri"]; } } class Player { constructor(userId) { this.socket = new Socket(`http://${window.location.hostname}:${window.location.port}`); this.socket.connect(this); this.socket.addMessageListener((msg) => this.handleMessage(msg)); this.timer = new Timer(() => this.updateTime(), 1000); this.userId = parseInt(userId); this.isDJ = false; this.date = new Date(); this.queue = []; this.users = {}; this.searchList = []; this.repeat = "off"; this.currentTrack = null; this.current_queue_position = 0; this.current_position = 0; this.is_paused = false; this.volume = 100; this.last_update = 0; this.is_connected = true; this.channelName = ""; } handleMessage(data) { const op = data["op"]; const validActions = Object.keys(actions); if (validActions.includes(op)) { actions[op](this, data); } else { console.log(`Invalid action: ${op}`) } return this.updateInfo() } init() { this.queue = [] this.currentTrack = null; this.users = []; $('#sortable').empty(); this.updateInfo(); } initSortable() { $('#sortable').empty(); for (var i in this.queue) { var track = this.queue[i]; $("#sortable").append(`
  • ${track.title}

    ${track.author}

    ${this.msToReadableTime(track.length)}

  • `) } this.updateCurrentQueuePos(); } updateCurrentQueuePos(pos) { if (pos != undefined) { this.current_queue_position = pos - 1; } this.currentTrack = this.queue[this.current_queue_position]; $('#sortable li div').removeClass('active'); const li = $(`#sortable li:eq(${this.current_queue_position})`); li.find('div').addClass('active'); const queue = $('.queue-list') if (queue.prop('scrollHeight') > queue.prop('clientHeight')) { queue.animate({ scrollTop: li.position().top - queue.position().top }, 'slow'); } return this.currentTrack; } addUser(user) { this.users[user['user_id']] = { avatar_url: user["avatar_url"], name: user["name"] }; this.updateUser(); } updateUser() { $("#users-container").empty(); for (const [id, user] of Object.entries(this.users)) { $("#users-container").append(`
  • ${user.name}

  • `) }; } addTrack(tracks) { for (var i in tracks) { var track = new Track(tracks[i]); this.queue.push(track); $("#sortable").append(`
  • ${track.title}

    ${track.author}

    ${this.msToReadableTime(track.length)}

  • `) } } moveTrack(target, to) { const c = this.current_queue_position; let element = this.queue.splice(target, 1)[0]; this.queue.splice(to, 0, element); const $ul = $('#sortable'); const $li = $ul.children().eq(position); $li.detach(); $ul.children().eq(to).before($li); if (target > c && to <= c) { this.current_queue_position += 1; } else if (target < c && to >= c) { this.current_queue_position -= 1; } else if (target == c) { this.current_queue_position = to; } this.send({ "op": "moveTrack", "position": target, "newPosition": to }) } removeTrack(position, track) { var rawTrack = this.queue[position]; if (position == this.current_queue_position) { return this.showToast("error", "You are not allow to remove playing track!"); } if (rawTrack.track_id == track.track_id) { this.send({"op": "removeTrack", "position": position, "track_id": track.track_id}); } else { this.showToast("error", "Track not found!"); } } togglePause() { this.send({ "op": "updatePause", "pause": !this.is_paused }); } skipTo(index = 1) { this.send({ "op": "skipTo", "index": index }); } backTo(index = 1) { this.send({ "op": "backTo", "index": index }); } seekTo(tempPosition) { if (this.currentTrack == undefined) { return; } var position = tempPosition / 500 * this.currentTrack.length; this.send({ "op": "updatePosition", "position": position }); } shuffle() { if ((this.queue.length - this.current_queue_position) > 3) { this.send({ "op": "shuffleTrack" }); } else { this.showToast("info", "Add more songs to the queue before shuffling."); } } repeatMode() { this.send({ "op": "repeatTrack" }) } send(payload) { var json = JSON.stringify(payload) this.socket.send(json); } isPlaying() { return (this.currentTrack != undefined && this.is_connected); } msToReadableTime(ms) { let totalSeconds = Math.floor(ms / 1000); let hours = Math.floor(totalSeconds / 3600); let minutes = Math.floor((totalSeconds % 3600) / 60); let seconds = totalSeconds % 60; minutes = (minutes < 10) ? "0" + minutes : minutes; seconds = (seconds < 10) ? "0" + seconds : seconds; let timeString = ""; if (hours > 0) { timeString += hours + ":" + minutes + ":" + seconds; } else { timeString += minutes + ":" + seconds; } return timeString; } showToast(userId, msg) { if (userId == "info") { var user = { avatar_url: "/static/img/info.png", name: "Info" } } else if (userId == "error") { var user = { avatar_url: "/static/img/error.png", name: "Error" } } else { var user = this.users[userId]; } if (user != null) { var $element = $(`
    user-icon

    ${user['name']}

    ${msg}

    `) $(".toastContrainer").append($element) setTimeout(function () { $element.fadeOut(500, function () { $(this).remove(); }); }, 6000); } } updateTime() { if (this.currentTrack == undefined) { return this.timer.stop(); } if (this.current_position >= this.currentTrack?.length) { return this.timer.stop(); } this.current_position += 1000; $("#position").text(this.msToReadableTime(this.current_position)); var time = (this.current_position / this.currentTrack?.length) * 500; $("#seek-bar").val(time); } updateInfo() { var currentTrack = this.currentTrack; if (currentTrack == undefined) { $("#title").text(""); $("#author").text(""); $("#position").text("00:00"); $("#length").text("00:00"); $("#image").removeAttr('src'); $("#largeImage").removeAttr('src'); } else { $("#title").text(currentTrack.title); $("#author").text(currentTrack.author); $("#length").text(this.msToReadableTime(currentTrack.length)); $("#image").attr("src", currentTrack.imageUrl); $("#largeImage").attr("src", currentTrack.imageUrl); } $("#channel-name").text((this.channelName == "") ? "Not Found" : this.channelName); var play_pause_btn = $("#play-pause-button"); var repeat_btn = $("#repeat-button"); if (this.is_paused || currentTrack == undefined) { play_pause_btn.removeClass('fa-pause').addClass('fa-play'); if (this.timer.getIsRunning()) { this.timer.stop(); } } else { play_pause_btn.removeClass('fa-play').addClass('fa-pause'); this.timer.start(); } repeat_btn.removeClass("fa-repeat-1").addClass("fa-repeat") if (this.repeat == "off") { repeat_btn.css('color', ''); } else if (this.repeat == "track") { repeat_btn.css('color', '#fff'); } else { repeat_btn.removeClass("fa-repeat").addClass("fa-repeat-1"); } } }