From 90c7a594567d4b33bd0923f461af0f623fafe4db Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Wed, 3 Aug 2011 08:36:50 +0100 Subject: [PATCH] move Javascript code to its own directory; add a Makefile to invoke the Closure Javascript Compiler to merge all our code into a single minified JS file --- server/djrandom/frontend/static/js/djr.min.js | 17 ++ .../djrandom/frontend/static/js/djr/Makefile | 16 ++ .../frontend/static/js/djr/backend.js | 101 ++++++- .../djrandom/frontend/static/js/djr/player.js | 254 +++--------------- .../frontend/static/js/djr/playlist.js | 236 ++++++++++++++++ server/djrandom/frontend/templates/_base.html | 2 +- 6 files changed, 396 insertions(+), 230 deletions(-) create mode 100644 server/djrandom/frontend/static/js/djr.min.js create mode 100644 server/djrandom/frontend/static/js/djr/Makefile create mode 100644 server/djrandom/frontend/static/js/djr/playlist.js diff --git a/server/djrandom/frontend/static/js/djr.min.js b/server/djrandom/frontend/static/js/djr.min.js new file mode 100644 index 0000000..a4a8370 --- /dev/null +++ b/server/djrandom/frontend/static/js/djr.min.js @@ -0,0 +1,17 @@ +djr={};var CHARS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");djr.generateRandomId=function(){var a=[],b=CHARS,c,d=b.length;for(c=0;c<40;c++)a[c]=b[0|Math.random()*d];return a.join("")};djr.Backend=function(){};djr.Backend.prototype.search=function(a,b,c){$.ajax({url:"/json/search",data:{q:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.moreLikeThese=function(a,b,c){$.ajax({url:"/json/morelikethese",data:{h:a.join(",")},dataType:"json",type:"POST",context:c||this,success:function(a){b(a.results)}})}; +djr.Backend.prototype.getPlaylist=function(a,b,c){$.ajax({url:"/json/playlist/get/"+a,dataType:"json",type:"GET",context:c||this,success:function(c){b(new djr.PlaylistChunk(c.songs,a))}})};djr.Backend.prototype.savePlaylist=function(a,b){$.ajax({url:"/json/playlist/save",data:{uuid:a,h:b.join(",")},type:"POST"})};djr.Backend.prototype.streamPlaylist=function(a,b,c,d){$.ajax({url:"/json/playlist/stream",data:{uuid:a,stream:b?"y":"n"},dataType:"json",type:"POST",context:d||this,success:function(a){c(a)}})}; +djr.Backend.prototype.getHtmlForSongs=function(a,b,c){$.ajax({url:"/fragment/songs",data:{h:a.join(",")},dataType:"html",type:"POST",context:c||this,success:b})};djr.Backend.prototype.nowPlaying=function(a,b){$.ajax({url:"/json/playing",data:{cur:a,prev:b.join(",")},type:"POST"})};djr.PlaylistChunk=function(a,b){this.songs=a||[];this.title=b};djr.PlaylistChunk.prototype.hasSong=function(a){return this.songs.indexOf(a)>=0};djr.PlaylistChunk.prototype.removeSong=function(a){this.songs=$.grep(this.songs,function(b){return b!=a})};djr.controlButtons=function(a){return'<div class="ctlbox" style="display:none"><a id="'+a+'_remove" class="ctl_btn ctl_remove"> </a></div>'}; +djr.PlaylistChunk.prototype.wrapHtml=function(a,b){return'<div id="chunk_'+a+'" class="chunk"><div class="chunk_ctl_wrap">'+djr.controlButtons("chunk_ctl_"+a)+'<a class="chunk_title">'+this.title+'</a></div><div class="chunk_inner">'+(b||"")+"</div></div>"};djr.Playlist=function(a){this.uuid=a||djr.generateRandomId();this.chunks=[];this.song_map={};this.chunk_map={};this.next_chunk_id=0}; +djr.Playlist.prototype.allSongs=function(){var a=[],b,c;for(b=0;b<this.chunks.length;b++)for(c=0;c<this.chunk_map[this.chunks[b]].songs.length;c++)a.push(this.chunk_map[this.chunks[b]].songs[c]);return a};djr.Playlist.prototype.createUniqueChunk=function(a,b){var c=[],d;for(d=0;d<a.length;d++)this.song_map[a[d]]==null&&c.push(a[d]);return c.length>0?new djr.PlaylistChunk(c,b):null}; +djr.Playlist.prototype.addChunk=function(a){djr.debug("adding chunk to playlist "+this.uuid);var b,c=this.next_chunk_id++;for(b=0;b<a.songs.length;b++)this.song_map[a.songs[b]]=c;this.chunk_map[c]=a;this.chunks.push(c);return c};djr.Playlist.prototype.getChunkSongs=function(a){return this.chunk_map[a].songs}; +djr.Playlist.prototype.removeChunk=function(a){djr.debug("removing chunk "+a);var b=this.chunk_map[a].songs,c;for(c=0;c<b.length;c++)delete this.song_map[b[c]];delete this.chunk_map[a];this.chunks=$.grep(this.chunks,function(b){return b!=a})};djr.Playlist.prototype.removeSong=function(a){djr.debug("removing song "+a);var b=this.song_map[a];this.chunk_map[b].removeSong(a);delete this.song_map[a];return this.chunk_map[b].songs.length==0?(this.removeChunk(b),b):-1}; +djr.Playlist.prototype.merge=function(){var a=[],b;for(b=0;b<this.chunks.length;b++)a.push(this.chunk_map[this.chunks[b]].title);a=a.join(" + ");b=new djr.Playlist;b.uuid=this.uuid;b.addChunk(new djr.PlaylistChunk(this.allSongs(),a));return b};djr.Playlist.prototype.getNextSong=function(a){var b=this.song_map[a],c=this.chunk_map[b].songs,b=this.chunks.indexOf(b),a=c.indexOf(a)+1;a>=c.length&&(a=0,b++,b>=this.chunks.length&&(b=0));return this.chunk_map[this.chunks[b]].songs[a]};djr.Player=function(a,b){this.backend=a;this.player=$(b);this.playlist=new djr.Playlist;this.old_songs=[];this.cur_song=null;this.player.jPlayer({swfPath:"/static/js",ready:function(){djr.debug("player ready")}});this.player.bind($.jPlayer.event.ended+".djr",function(){djr.state.player.nextSong()});this.player.bind($.jPlayer.event.error+".djr",function(){djr.state.player.reportError()})};djr.Player.prototype.hideAllChunks=function(){$(".chunk .chunk_inner").hide()}; +djr.Player.prototype.removeChunk=function(a){this.playlist.removeChunk(a);this.savePlaylist();$("#chunk_"+a).remove()};djr.Player.prototype.removeSong=function(a){$("#song_"+a).remove();a=this.playlist.removeSong(a);this.savePlaylist();a>0&&$("#chunk_"+a).remove()};djr.Player.prototype.savePlaylist=function(){this.backend.savePlaylist(this.playlist.uuid,this.playlist.allSongs())};djr.Player.prototype.clearPlaylist=function(){this.playlist=new djr.Playlist;$("#playlistDiv").empty()}; +djr.Player.prototype.mergePlaylistChunks=function(){this.playlist=this.playlist.merge();var a=[];$(".chunk .chunk_inner").each(function(){a.push($(this).html())});$("#playlistDiv").empty();var b=this.playlist.chunks[0];this.setChunkHtml(this.playlist.chunk_map[b],b,a.join(""))};djr.Player.prototype.search=function(a){var b=this;this.backend.search(a,function(c){var d=[];$.each(c,function(a,b){d.push(b.sha1)});d.length==0?djr.debug("No results found."):b.createChunk(d,a)})}; +djr.Player.prototype.extendCurrentPlaylist=function(){var a=this;this.backend.moreLikeThese(this.playlist.allSongs(),function(b){a.createChunk(b,"suggestions")})};djr.Player.prototype.createChunk=function(a,b){var c=this.playlist.createUniqueChunk(a,b);if(c){this.playlist.chunks.length>1&&this.mergePlaylistChunks();var d=this.playlist.addChunk(c);this.savePlaylist();this.backend.getHtmlForSongs(a,function(a){this.hideAllChunks();this.setChunkHtml(c,d,a)},this)}else djr.debug("All the results are already in the playlist")}; +djr.Player.prototype.setChunkHtml=function(a,b,c){a=a.wrapHtml(b,c);$("#playlistDiv").append(a);var d=this,e=$("#chunk_"+b);e.find(".song_a").click(function(){d.play($(this).attr("id").substr(5))});e.find(".album_a").click(function(){d.search('(album:"'+$(this).text()+'")')});e.find(".chunk_title").click(function(){e.find(".chunk_inner").toggle()});e.hover(function(){$(this).find(".chunk_ctl_wrap .ctlbox").show()},function(){$(this).find(".chunk_ctl_wrap .ctlbox").hide()});e.find(".chunk_ctl_wrap .ctlbox .ctl_remove").click(function(){djr.debug("removing chunk "+ +b);d.removeChunk(b)});e.find(".chunk_inner .song").hover(function(){$(this).find(".ctlbox").show()},function(){$(this).find(".ctlbox").hide()});e.find(".chunk_inner .ctlbox .ctl_remove").click(function(){var a=$(this).parent().parent().attr("id").substr(5);d.removeSong(a)})}; +djr.Player.prototype.play=function(a){djr.debug("play "+a);this.cur_song&&(this.old_songs.push(this.cur_song),this.old_songs.length>5&&this.old_songs.shift());this.cur_song=a;$(".song").removeClass("playing");$("#song_"+a).addClass("playing");this.player.jPlayer("setMedia",{mp3:"/dl/"+a}).jPlayer("play");var b=$("#song_"+a+" .artist").text(),c=$("#song_"+a+" .album").text();$("#jp_playlist_1").html($("#song_"+a+" .title").text()+"<br>"+b+"<br><small>"+c+"</small>");b="/album_image/"+escape(b)+"/"+ +escape(c);$("#albumart_fs").attr("src",b);$("#albumart_fs").fullBg();$("#albumart_fs").show();this.backend.nowPlaying(a,this.old_songs)};djr.Player.prototype.nextSong=function(){this.play(this.playlist.getNextSong(this.cur_song))};djr.Player.prototype.streamCurrentPlaylist=function(){};djr.state={backend:null,player:null}; +djr.init=function(){djr.state.backend=new djr.Backend;djr.state.player=new djr.Player(djr.state.backend,"#djr_player");$("#playlistClearBtn").click(function(){djr.state.player.clearPlaylist()});$("#playlistStreamBtn").click(function(){djr.state.player.streamCurrentPlaylist()});$("#playlistExtendBtn").click(function(){djr.state.player.extendCurrentPlaylist()});$("#albumart_fs").load(function(){$(this).fullBg();$(this).show()})};djr.player=function(){return djr.state.player}; +djr.debug=function(a){$("#debug").append(a+"<br>")}; diff --git a/server/djrandom/frontend/static/js/djr/Makefile b/server/djrandom/frontend/static/js/djr/Makefile new file mode 100644 index 0000000..fc30223 --- /dev/null +++ b/server/djrandom/frontend/static/js/djr/Makefile @@ -0,0 +1,16 @@ + +JSCOMPILER = java -jar ~/src/jscompiler/compiler.jar + +SOURCES = \ + djr.js \ + backend.js \ + playlist.js \ + player.js + +TARGET = ../djr.min.js + +all: $(TARGET) + +../djr.min.js: $(SOURCES) + $(JSCOMPILER) $(SOURCES:%=--js=%) --js_output_file=$@ + diff --git a/server/djrandom/frontend/static/js/djr/backend.js b/server/djrandom/frontend/static/js/djr/backend.js index 3586149..5c0f45c 100644 --- a/server/djrandom/frontend/static/js/djr/backend.js +++ b/server/djrandom/frontend/static/js/djr/backend.js @@ -1,13 +1,34 @@ // backend.js -// Backend API stub. - +/** + * Backend API. + * + * This object provides simple wrappers for the most common AJAX calls + * to the backend. + * + * Note: methods that take a 'callback' parameter also accept a + * context parameter 'ctx', in case your callbacks need to use 'this' + * (also, I suck at Javascript OOP, so it's entirely possible there's + * an easier way to do it)... + * + * @constructor + * @this {Backend} + */ djr.Backend = function() { - // Nothing yet. + // No state yet. }; -// Search. +/** + * Search. + * + * It will call 'callback(results)' where results is an array of song + * objects that will have the 'sha1' and 'score' attributes. + * + * @param {string} query The search query. + * @param {function} callback Callback function. + * @param {Object} ctx Callback context. + */ djr.Backend.prototype.search = function(query, callback, ctx) { $.ajax({url: '/json/search', data: {'q': query}, @@ -20,7 +41,13 @@ djr.Backend.prototype.search = function(query, callback, ctx) { }); }; -// More like these. +/** + * Return results similar to the provided songs. + * + * @param {Array[string]} hashes SHA1 hashes of the input songs. + * @param {function} callback Callback function. + * @param {Object} ctx Callback context. + */ djr.Backend.prototype.moreLikeThese = function(uuids, callback, ctx) { $.ajax({url: '/json/morelikethese', data: {'h': uuids.join(',')}, @@ -33,9 +60,16 @@ djr.Backend.prototype.moreLikeThese = function(uuids, callback, ctx) { }); }; -// Get information about a specific song. - -// Read a playlist, calls callback(Playlist). +/** + * Get the songs in a playlist. + * + * It will invoke callback(songs) where 'songs' is an array of SHA1 + * song hashes. + * + * @param {string} uuid Playlist identifier. + * @param {function} callback Callback function. + * @param {Object} ctx Callback context. + */ djr.Backend.prototype.getPlaylist = function(uuid, callback, ctx) { $.ajax({url: '/json/playlist/get/' + uuid, dataType: 'json', @@ -47,7 +81,15 @@ djr.Backend.prototype.getPlaylist = function(uuid, callback, ctx) { }); }; -// Save a playlist (song array). +/** + * Update contents of a playlist. + * + * Will create the playlist if necessary. You're supposed to generate + * the unique playlist identifier on the caller side. + * + * @param {string} uuid Playlist identifier. + * @param {Array[string]} songs SHA1 hashes of the songs. + */ djr.Backend.prototype.savePlaylist = function(uuid, songs) { $.ajax({url: '/json/playlist/save', data: {uuid: uuid, @@ -56,7 +98,19 @@ djr.Backend.prototype.savePlaylist = function(uuid, songs) { }); }; -// Stream a playlist. +/** + * Enable/disable streaming for a playlist. + * + * Invokes callback() with a single parameter: an object that has a + * boolean 'stream' attribute, and an optional 'url' attribute + * containing the actual stream url (only present if streaming is + * enabled). + * + * @param {string} uuid Playlist identifier. + * @param {bool} streaming Enable streaming for this playlist. + * @param {function} callback Callback function. + * @param {Object} ctx Callback context. + */ djr.Backend.prototype.streamPlaylist = function(uuid, streaming, callback, ctx) { $.ajax({url: '/json/playlist/stream', data: {uuid: uuid, @@ -70,23 +124,42 @@ djr.Backend.prototype.streamPlaylist = function(uuid, streaming, callback, ctx) }); }; -// Return the HTML fragment for an array of songs. +/** + * Return the HTML fragment to render a list of songs. + * + * The callback function will be called with the resulting HTML string + * as its first argument. + * + * @param {Array[string]} songs SHA1 hashes of the songs. + * @param {function} callback Callback function. + * @param {Object} ctx Callback context. + */ djr.Backend.prototype.getHtmlForSongs = function(songs, callback, ctx) { $.ajax({url: '/fragment/songs', data: {'h': songs.join(',')}, dataType: 'html', type: 'POST', context: ctx || this, - success: callback, + success: callback }); }; -// Ping a song that is currently playing. +/** + * Report that a new song is playing. + * + * Together with the hash of the current song, you can also send a + * short list of the most recent songs that have been played. + * + * @param {string} song SHA1 hash of the song that is about to start + * playing. + * @param {Array[string]} old_songs SHA1 hashes of the previously + * played songs, in order from the oldest to the newest. + */ djr.Backend.prototype.nowPlaying = function(song, old_songs) { $.ajax({url: '/json/playing', data: {'cur': song, 'prev': old_songs.join(',')}, - type: 'POST', + type: 'POST' }); }; diff --git a/server/djrandom/frontend/static/js/djr/player.js b/server/djrandom/frontend/static/js/djr/player.js index 91286e6..2bdfa05 100644 --- a/server/djrandom/frontend/static/js/djr/player.js +++ b/server/djrandom/frontend/static/js/djr/player.js @@ -1,159 +1,6 @@ // player.js -// A Playlist chunk (basically, an array of songs). - -djr.PlaylistChunk = function(songs, title) { - this.songs = songs || []; - this.title = title; -}; - -djr.PlaylistChunk.prototype.hasSong = function(sha1) { - return (this.songs.indexOf(sha1) >= 0); -}; - -djr.PlaylistChunk.prototype.removeSong = function(sha1) { - this.songs = $.grep(this.songs, function(a) { return a != sha1; }); -}; - -// HTML template for control buttons, generated from JS. -djr.controlButtons = function(id_base) { - return ('<div class=\"ctlbox\" style=\"display:none\">' - + '<a id=\"' + id_base + '_remove\" class="ctl_btn ctl_remove"> </a>' - + '</div>'); -}; - -djr.PlaylistChunk.prototype.wrapHtml = function(chunk_id, songs_html) { - return ('<div id=\"chunk_' + chunk_id + '\" class=\"chunk\">' - + '<div class=\"chunk_ctl_wrap\">' - + djr.controlButtons('chunk_ctl_' + chunk_id) - + '<a class=\"chunk_title\">' + this.title + '</a>' - + '</div>' - + '<div class=\"chunk_inner\">' - + (songs_html || '') + '</div></div>'); -}; - - -// Playlist. - -djr.Playlist = function(uuid) { - this.uuid = uuid || djr.generateRandomId(); - this.chunks = []; - this.song_map = {}; - this.chunk_map = {}; - this.next_chunk_id = 0; -}; - -// Return an array with all songs, in order. -djr.Playlist.prototype.allSongs = function() { - var songs = [], i, j; - for (i = 0; i < this.chunks.length; i++) { - for (j = 0; j < this.chunk_map[this.chunks[i]].songs.length; j++) { - songs.push(this.chunk_map[this.chunks[i]].songs[j]); - } - } - return songs; -}; - -// Creates a new chunk with only unique songs. -djr.Playlist.prototype.createUniqueChunk = function(songs, title) { - var unique = [], i; - for (i = 0; i < songs.length; i++) { - if (this.song_map[songs[i]] != null) { - continue; - } - unique.push(songs[i]); - } - if (unique.length > 0) { - return new djr.PlaylistChunk(unique, title); - } else { - return null; - } -}; - -// Add a new chunk (only adds unique songs). -// Returns the chunk ID, or -1 if no songs were added. -djr.Playlist.prototype.addChunk = function(playlist_chunk) { - djr.debug('adding chunk to playlist ' + this.uuid); - var i, chunk_id = this.next_chunk_id++; - for (i = 0; i < playlist_chunk.songs.length; i++) { - var song = playlist_chunk.songs[i]; - this.song_map[song] = chunk_id; - } - this.chunk_map[chunk_id] = playlist_chunk; - this.chunks.push(chunk_id); - return chunk_id; -}; - -// Return the songs for a specific chunk. -djr.Playlist.prototype.getChunkSongs = function(chunk_id) { - return this.chunk_map[chunk_id].songs; -}; - -// Remove a chunk (by ID). -djr.Playlist.prototype.removeChunk = function(chunk_id) { - djr.debug('removing chunk ' + chunk_id); - var songs = this.chunk_map[chunk_id].songs, i; - for (i = 0; i < songs.length; i++) { - delete this.song_map[songs[i]]; - } - delete this.chunk_map[chunk_id]; - this.chunks = $.grep(this.chunks, function(cid) { return cid != chunk_id; }); -}; - -// Remove a song. -// Returns the chunk ID if after removing the song, the chunk was left -// completely empty, or -1 otherwise. -djr.Playlist.prototype.removeSong = function(song) { - djr.debug('removing song ' + song); - var chunk_id = this.song_map[song]; - this.chunk_map[chunk_id].removeSong(song); - delete this.song_map[song]; - if (this.chunk_map[chunk_id].songs.length == 0) { - this.removeChunk(chunk_id); - return chunk_id; - } else { - return -1; - } -}; - -// Return a new Playlist with all the current chunks merged. -djr.Playlist.prototype.merge = function() { - var new_title_parts = [], i; - for (i = 0; i < this.chunks.length; i++) { - new_title_parts.push(this.chunk_map[this.chunks[i]].title); - } - var new_title = new_title_parts.join(' + '); - var new_playlist = new djr.Playlist(); - new_playlist.uuid = this.uuid; - new_playlist.addChunk( - new djr.PlaylistChunk(this.allSongs(), new_title)); - return new_playlist; -}; - -// Find the next song. -djr.Playlist.prototype.getNextSong = function(song) { - var cur_chunk = this.song_map[song]; - var chunk_songs = this.chunk_map[cur_chunk].songs; - - var chunk_idx = this.chunks.indexOf(cur_chunk); - var idx = chunk_songs.indexOf(song) + 1; - - if (idx >= chunk_songs.length) { - idx = 0; - chunk_idx++; - if (chunk_idx >= this.chunks.length) { - chunk_idx = 0; - } - } - - return this.chunk_map[this.chunks[chunk_idx]].songs[idx]; -}; - - - - - // Player djr.Player = function(backend, selector) { @@ -168,7 +15,7 @@ djr.Player = function(backend, selector) { swfPath: '/static/js', ready: function() { djr.debug('player ready'); - }, + } }); this.player.bind($.jPlayer.event.ended + '.djr', function () {djr.state.player.nextSong(); }); @@ -247,31 +94,47 @@ djr.Player.prototype.search = function(query) { } // Create a chunk of unique new songs. - var new_chunk = player.playlist.createUniqueChunk(songs, query); - if (!new_chunk) { - djr.debug('All the results are already in the playlist.'); - return; - } - songs = new_chunk.songs; - - // Ok, now, if we have more than 1 chunk in the playlist, we - // merge them. - if (player.playlist.chunks.length > 1) { - player.mergePlaylistChunks(); - } + player.createChunk(songs, query); + }); +}; - // Now add the new chunks with the search results. - var chunk_id = player.playlist.addChunk(new_chunk); - djr.debug('search: new chunk ' + chunk_id + ', ' + songs.length + ' songs'); - player.savePlaylist(); +// Extend the current playlist with suggestions. +djr.Player.prototype.extendCurrentPlaylist = function() { + var player = this; + var cur_songs = this.playlist.allSongs(); - player.backend.getHtmlForSongs(songs, function(songs_html) { - var chunk_div = 'chunk_' + chunk_id; - player.hideAllChunks(); - player.setChunkHtml(new_chunk, chunk_id, songs_html); - }); + this.backend.moreLikeThese(cur_songs, function(results) { + player.createChunk(results, 'suggestions'); }); +}; + +djr.Player.prototype.createChunk = function(songs, chunk_title) { + // Create the new chunk, with unique songs. + var chunk = this.playlist.createUniqueChunk(songs, chunk_title); + if (!chunk) { + djr.debug('All the results are already in the playlist'); + return; + } + + // If there's more than one chunk in the playlist, merge (that is, + // replace the current playlist with a new, merged one). + if (this.playlist.chunks.length > 1) { + this.mergePlaylistChunks(); + } + + // Add the new chunk. + var chunk_id = this.playlist.addChunk(chunk); + // Save the current playlist. + this.savePlaylist(); + + // Load the HTML fragment for the chunk and display it. Also, + // minimize all the other chunks. + this.backend.getHtmlForSongs(songs, function(songs_html) { + var chunk_div = 'chunk_' + chunk_id; + this.hideAllChunks(); + this.setChunkHtml(chunk, chunk_id, songs_html); + }, this); }; // Set the HTML contents of a chunk object, and the related event hooks. @@ -366,41 +229,10 @@ djr.Player.prototype.streamCurrentPlaylist = function(enable) { // Nothing for now. }; -// Extend the current playlist with suggestions. -djr.Player.prototype.extendCurrentPlaylist = function() { - var player = this; - var cur_songs = this.playlist.allSongs(); - - this.backend.moreLikeThese(cur_songs, function(results) { - var new_chunk = player.playlist.createUniqueChunk(results, 'suggestions'); - if (!new_chunk) { - djr.debug('All the results are already in the playlist.'); - return; - } - - // Ok, now, if we have more than 1 chunk in the playlist, we - // merge them. - if (player.playlist.chunks.length > 1) { - player.mergePlaylistChunks(); - } - - // Add the new chunk with the suggestions. - var chunk_id = player.playlist.addChunk(new_chunk); - var songs = new_chunk.songs; - player.savePlaylist(); - - player.backend.getHtmlForSongs(songs, function(songs_html) { - var chunk_div = 'chunk_' + chunk_id; - player.hideAllChunks(); - player.setChunkHtml(new_chunk, chunk_id, songs_html); - }); - }); -}; - djr.state = { backend: null, - player: null, + player: null }; djr.init = function () { @@ -441,14 +273,6 @@ djr.debug = function(msg) { /** - -// Empty the current playlist. -djr.Player.prototype.clearPlaylist = function() { - this.curPlaylist = Array(); - this.curPlaylistId = null; - this.curIdx = -1; -}; - // An error has occurred in the player. djr.Player.prototype.reportError = function(event) { switch(event.jPlayer.error.type) { diff --git a/server/djrandom/frontend/static/js/djr/playlist.js b/server/djrandom/frontend/static/js/djr/playlist.js new file mode 100644 index 0000000..cc23bd3 --- /dev/null +++ b/server/djrandom/frontend/static/js/djr/playlist.js @@ -0,0 +1,236 @@ +// playlist.js + + +/** + * A Playlist chunk (basically, an array of songs). + * + * A chunk is the lowest level unit of a playlist, it's just an + * ordered list of songs, with a title. + * + * @constructor + * @param {Array[string]} songs SHA1 hashes of the songs. + * @param {string} title Chunk title (optional). + */ +djr.PlaylistChunk = function(songs, title) { + this.songs = songs || []; + this.title = title; +}; + +/** + * Check if the playlist contains a specific song. + * + * @param {string} sha1 Song SHA1 hash. + * @return {bool} True if the song is in this chunk. + */ +djr.PlaylistChunk.prototype.hasSong = function(sha1) { + return (this.songs.indexOf(sha1) >= 0); +}; + +/** + * Remove a song from the playlist (if present). + * + * @param {string} sha1 Song SHA1 hash. + */ +djr.PlaylistChunk.prototype.removeSong = function(sha1) { + this.songs = $.grep(this.songs, function(a) { return a != sha1; }); +}; + +// HTML template for control buttons, generated from JS. +djr.controlButtons = function(id_base) { + return ('<div class=\"ctlbox\" style=\"display:none\">' + + '<a id=\"' + id_base + '_remove\" class="ctl_btn ctl_remove"> </a>' + + '</div>'); +}; + +/** + * Generate the HTML chunk for the playlist. + * + * This method needs a bit of context: when we need to create a new + * chunk on-the-fly, the HTML fargment returned by the backend search + * must be wrapped by the chunk <div>. We do it here in this + * function, since there's not much to it anyway, and we can save a + * round-trip to the server when creating a chunk with existing data + * (as it happens when merging after search, for example). + * + * @param {string} chunk_id The chunk ID. + * @param {string} songs_html The HTML fragment returned by the backend. + */ +djr.PlaylistChunk.prototype.wrapHtml = function(chunk_id, songs_html) { + return ('<div id=\"chunk_' + chunk_id + '\" class=\"chunk\">' + + '<div class=\"chunk_ctl_wrap\">' + + djr.controlButtons('chunk_ctl_' + chunk_id) + + '<a class=\"chunk_title\">' + this.title + '</a>' + + '</div>' + + '<div class=\"chunk_inner\">' + + (songs_html || '') + '</div></div>'); +}; + + + +// Playlist. + +djr.Playlist = function(uuid) { + this.uuid = uuid || djr.generateRandomId(); + this.chunks = []; + this.song_map = {}; + this.chunk_map = {}; + this.next_chunk_id = 0; +}; + +/** + * Return an array with all songs in the playlist. + * + * Songs are returned in order. + * + * @return {Array[string]} SHA1 hashes of all songs. + */ +djr.Playlist.prototype.allSongs = function() { + var songs = [], i, j; + for (i = 0; i < this.chunks.length; i++) { + for (j = 0; j < this.chunk_map[this.chunks[i]].songs.length; j++) { + songs.push(this.chunk_map[this.chunks[i]].songs[j]); + } + } + return songs; +}; + +/** + * Creates a new chunk with only unique songs. + * + * This method creates a new chunk that enforces the constraint that a + * playlist should only contain unique songs. The newly created chunk + * is just returned, but not added to the playlist. + * + * @param {Array[string]} songs SHA1 hashes of the songs. + * @param {string} title Title of the new chunk. + * @return {PlaylistChunk} Newly created chunk with unique songs, or + * null if the chunk would have been empty. + */ +djr.Playlist.prototype.createUniqueChunk = function(songs, title) { + var unique = [], i; + for (i = 0; i < songs.length; i++) { + if (this.song_map[songs[i]] != null) { + continue; + } + unique.push(songs[i]); + } + if (unique.length > 0) { + return new djr.PlaylistChunk(unique, title); + } else { + return null; + } +}; + +/** + * Add a new chunk (only adds unique songs). + * + * Also assigns a new unique chunk ID (an integer at the moment). + * + * @param {PlaylistChunk} playlist_chunk Chunk to add. + * @return {number} The chunk ID, or -1 if no songs were added. + */ +djr.Playlist.prototype.addChunk = function(playlist_chunk) { + djr.debug('adding chunk to playlist ' + this.uuid); + var i, chunk_id = this.next_chunk_id++; + for (i = 0; i < playlist_chunk.songs.length; i++) { + var song = playlist_chunk.songs[i]; + this.song_map[song] = chunk_id; + } + this.chunk_map[chunk_id] = playlist_chunk; + this.chunks.push(chunk_id); + return chunk_id; +}; + +/** + * Return the songs for a specific chunk. + * + * @param {number} chunk_id ID of the chunk. + * @return {Array[string]} SHA1 hashes of the songs. + */ +djr.Playlist.prototype.getChunkSongs = function(chunk_id) { + return this.chunk_map[chunk_id].songs; +}; + +/** + * Remove a chunk. + * + * @param {number} chunk_id ID of the chunk to remove. + */ +djr.Playlist.prototype.removeChunk = function(chunk_id) { + djr.debug('removing chunk ' + chunk_id); + var songs = this.chunk_map[chunk_id].songs, i; + for (i = 0; i < songs.length; i++) { + delete this.song_map[songs[i]]; + } + delete this.chunk_map[chunk_id]; + this.chunks = $.grep(this.chunks, function(cid) { return cid != chunk_id; }); +}; + +/** + * Remove a song. + * + * @param {string} song SHA1 hash of the song. + * @return {number} Returns the chunk ID if after removing the song, + * the chunk was left completely empty, or -1 otherwise. + */ +djr.Playlist.prototype.removeSong = function(song) { + djr.debug('removing song ' + song); + var chunk_id = this.song_map[song]; + this.chunk_map[chunk_id].removeSong(song); + delete this.song_map[song]; + if (this.chunk_map[chunk_id].songs.length == 0) { + this.removeChunk(chunk_id); + return chunk_id; + } else { + return -1; + } +}; + +/** + * Merge all chunks into a new playlist. + * + * The new Playlist will contain a single chunk, with all the + * playlists' songs in it, and with a title that created combining the + * titles of all the existing chunks. + * + * The UUID of the new playlist will be the same as the current one. + * + * @return {Playlist} The new playlist. + */ +djr.Playlist.prototype.merge = function() { + var new_title_parts = [], i; + for (i = 0; i < this.chunks.length; i++) { + new_title_parts.push(this.chunk_map[this.chunks[i]].title); + } + var new_title = new_title_parts.join(' + '); + var new_playlist = new djr.Playlist(); + new_playlist.uuid = this.uuid; + new_playlist.addChunk( + new djr.PlaylistChunk(this.allSongs(), new_title)); + return new_playlist; +}; + +/** + * Find the next song. + * + * @param {string} song SHA1 hash of a song. + * @return {string} SHA1 hash of the song that comes after the + * specified one. + */ +djr.Playlist.prototype.getNextSong = function(song) { + var cur_chunk = this.song_map[song]; + var chunk_songs = this.chunk_map[cur_chunk].songs; + + var chunk_idx = this.chunks.indexOf(cur_chunk); + var idx = chunk_songs.indexOf(song) + 1; + + if (idx >= chunk_songs.length) { + idx = 0; + chunk_idx++; + if (chunk_idx >= this.chunks.length) { + chunk_idx = 0; + } + } + + return this.chunk_map[this.chunks[chunk_idx]].songs[idx]; +}; diff --git a/server/djrandom/frontend/templates/_base.html b/server/djrandom/frontend/templates/_base.html index c12b36a..256f6ee 100644 --- a/server/djrandom/frontend/templates/_base.html +++ b/server/djrandom/frontend/templates/_base.html @@ -17,7 +17,7 @@ <script type="text/javascript" src="/static/js/jquery.fullbg.min.js"></script> <script type="text/javascript" - src="/static/player.js?v=5"></script> + src="/static/js/djr.min.js"></script> {% block head %}{% endblock %} </head> <body> -- GitLab