NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name player // @namespace cuzi // @oujs:author cuzi // @description Play bandcamp music. // @homepageURL https://openuserjs.org/scripts/cuzi/player // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAQMAAABtzGvEAAAABlBMVEUAAAAclZU8CPpPAAAAAXRSTlMAQObYZgAAAFZJREFUeF6N0DEKAzEMBMBAinxbT/NT9gkuVRg7kCFwqS7bTCVW0uOPPOvDK2hsnELQ2DiFoLFxCkFj4xSC+UMwYGBhYkDRwsRAXfdsBHW9r5HvJ27yBmrWa3qFBFkKAAAAAElFTkSuQmCC // @version 4 // @license GPL-3.0 // @include * // @require http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js // @require https://raw.githubusercontent.com/cvzi/bcplayer/master/BCPlayerLib.js // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant unsafeWindow // ==/UserScript== "use strict"; /* This program can save a playlist of bandcamp songs from different albums and play them on any bandcamp.com page. This program is under development, and at the moment has a very simple interface and only a limited functionality. There are two necessary steps to add a song to the playlist: 1st: Visit the album page and click on the [+] next to the play button of the song (this will add the song to your "library"). 2nd: Open the player by clicking the symbol in the top right corner. In the lower view (which is your "library") click on the [+]. Now the song is enqueued and should appear in the upper view (which is your "playlist"). Start playback by pressing the play button next to a song in the upper view. For a fullscreen player open https://bandcamp.com/player */ var config = { "refreshInterval" : 30, // seconds "titlePrefix" : String.fromCharCode(9835), "fullScreenURL" : /^https?:\/\/bandcamp\.com\/player\/?$/, "ButtonOpenPlayer" : "⧉" }; /* how is stuff saved: bands identified by id (band_id) A band at the moment is very simple: { id:123, name:"the band name", albums:[id0,id1]} tracks identified by id albums identified by id (tralbum_id) Constraints: If a track is saved, its album and its band are saved aswell. If an album has no tracks saved, it must be deleted. If a band has no albums saved, it must be deleted. */ GM_registerMenuCommand("player - Reset everything", function() { if(!confirm("Really reset everything? You'll lose all your songs in the library.")) return; // JSON: GM_setValue("tracks","{}"); GM_setValue("albums","{}"); GM_setValue("bands","{}"); GM_setValue("playlist","[]"); GM_setValue("playlist_index","-1"); // No JSON GM_setValue("libversion",Number.MIN_SAFE_INTEGER); GM_setValue("playeropen",false); GM_setValue("requestClose",false); GM_setValue("lastPosition",false); alert("Reset."); document.location.reload(); }); GM_registerMenuCommand("player - Repair", function() { if(!confirm("Attempt to repair the player?\n\nThis will empty your current playlist but NOT your library.\n\nPlease close all other bandcamp.com tabs and windows before you run this command.")) return; GM_setValue("playeropen","false"); GM_setValue("playlist_index","-1"); GM_setValue("libversion",Number.MIN_SAFE_INTEGER); GM_setValue("requestClose",false); GM_setValue("lastPosition",false); alert("Repair finished."); document.location.reload(); }); var to_resizeTimeout = true; var iv_refreshInterval = false; function fullscreenPlayer(ev) { if(!document.getElementById("playerX")) { return; } var mw = $("#playerX"); // Stop animation of open button (it will be invisible anyway) $(".buttonopenplayer").removeClass("playingspinner"); var w = $(window).width()-10; var h = $(window).height(); var lh = h*0.7; var ph = h - lh - mw.find(".status").height(); mw.css({ 'position' : 'fixed', 'top' : '0px', 'left' : '0px', 'width' : w, 'height' : h }); mw.find(".library").css('height',lh); mw.find(".playlist").css('height',ph); mw.css("visibility",""); if(to_resizeTimeout === true) { to_resizeTimeout = false; $(window).resize(function(ev) { if(to_resizeTimeout === false) { to_resizeTimeout = window.setTimeout(function() { to_resizeTimeout = false; fullscreenPlayer(); },1000); } }); } } function maximizePlayer(ev,Lib,pos) { // This function makes sure that only one player is open a the same time. // It requests any other player to close (by calling hidePlayer() in the other window) and waits (5 seconds) for confirmation before opening the player in this window. // Start animations here (because the "Open player" blutton is animated) $(".buttonopenplayer").addClass("playingspinner"); if(ev && ev.data) { Lib = ev.data; } if(document.getElementById("playerX")) { // Toggle visibility if("hidden" == document.getElementById("playerX").style.visibility) { document.getElementById("playerX").style.visibility = ""; }else { document.getElementById("playerX").style.visibility = "hidden"; } return; } if(GM_getValue("playeropen",false)) { // Player is open in another window GM_setValue("requestClose",true); var checkCounter = 0; var iv_waitForOtherPlayer = window.setInterval(function() { if(GM_getValue("playeropen",false)) { checkCounter++; if(checkCounter > 16) { // after 5 seconds console.log("5 seconds rule"); // Waited long enough window.clearInterval(iv_waitForOtherPlayer); GM_setValue("playeropen",false) GM_setValue("requestClose",false); maximizePlayer(ev,Lib,pos) } return; } window.clearInterval(iv_waitForOtherPlayer); maximizePlayer(null,Lib,parseFloat(GM_getValue("lastPosition",false))); },300); } else { // open Player var pl; GM_setValue("playeropen",true); GM_setValue("requestClose",false); GM_setValue("lastPosition",false); $(window).on('unload', function(){ if(document.getElementById("playerX")) { GM_setValue("playeropen",false); } }); var iv_waitForRequestClose = window.setInterval(function() { if(GM_getValue("requestClose",false)) { window.clearInterval(iv_waitForRequestClose); var position = hidePlayer(pl); GM_setValue("lastPosition",""+position); GM_setValue("playeropen",false); } },1000); pl = showPlayer(Lib,pos); document.title = config.titlePrefix+ " - " + document.title; } } function hidePlayer(pl) { // Close the player and remove it from DOM // If the player was playing the return value is the last position in seconds var pos = false; if(pl) { pl.pause(); pos = pl.getTime(); pl.getMainWindow().remove(); pl = null; } document.title = document.title.substr(4); // Stop any animations $(".buttonopenplayer").removeClass("playingspinner"); if(iv_refreshInterval !== false) { window.clearInterval(iv_refreshInterval); iv_refreshInterval = false; } return pos; } function showMenu(options,entries) { var $div = $("<div></div>").addClass("dropdownmenu").css({ "position" : "absolute", "top" : options.position.top, "left" : options.position.left }).appendTo("#playerX"); var $ul = $("<ul></ul>").appendTo($div); for(var i = 0; i < entries.length; i++) { $("<li></li>").appendTo($ul).html(entries[i].title).click((function(i) { return function(ev) { if(options.beforeClick) options.beforeClick.call(this,ev); if(entries[i].click) entries[i].click.call(this,ev); if(options.afterClick) options.afterClick.call(this,ev); }; })(i)); } return $div; } function showPlayer(Lib,pos) { // Initialize, customize and then open the player var libraryTrackMenu = function(ev) { var $this = $(this); var pl = ev.data; // Player object var $tr = $this.parent(); var tid = parseInt($tr.find("td.ctrl").data("tid"),10); var pos = $this.position(); pos.top += $this.height(); $("#playerX .library .dropdownmenu").remove(); //$("#playerX .library").css("overflow","hidden"); var $menu = showMenu({ "position" : pos, "beforeClick" : function(ev) { }, "afterClick" : function(ev) { //$("#playerX .library").css("overflow","auto"); $menu.remove(); } }, [ { "title" : "Play", "click" : function(ev) { pl.addToPlaylist(tid,0); pl.play(0); } }, { "title" : "Add to playlist (at the top)", "click" : function(ev) { pl.addToPlaylist(tid,0); } }, { "title" : "Add to playlist (at the end)", "click" : function(ev) { pl.addToPlaylist(tid,-1); } }, { "title" : "Show Album", "click" : function(ev) { var track = pl.getLibrary().getTrackById(tid); pl.getMainWindow().find(".searchfield").val("album:"+track.album.title).trigger("keyup"); } }, { "title" : "Show Artist", "click" : function(ev) { var track = pl.getLibrary().getTrackById(tid); pl.getMainWindow().find(".searchfield").val("artist:"+track.band.title).trigger("keyup"); } }, { "title" : "Delete from library", "click" : function(ev) { pl.getLibrary().removeTrack(tid,function(ok) { if(ok) { pl.refresh(1); } else { alert("Could not delete song from library."); } }); } }, { "title" : "Edit", "click" : function(ev) { alert("Not here yet"); } } ]); }; var pl = new BCPlayer(Lib,"playerX"); GM_addStyle("\ .playingspinner {color:#cb1; }\ /*.playingspinner {width: 20px; display:inline-block; text-align:center; align-content:center;\ -webkit-animation: rotation 2s infinite linear;\ animation: rotation 10s infinite linear }\ @-webkit-keyframes rotation {\ from {-webkit-transform: rotate(0deg) }\ to {-webkit-transform: rotate(359deg) }\ }\ @keyframes rotation {\ from {transform: rotate(0deg) }\ to {transform: rotate(359deg) }\ }\ */\ #playerX {z-index: 15; position:absolute; left: 10px; top: 50px; width:600px; height: auto;\ border:5px solid #333; background: linear-gradient(to right, white, silver); font-size:smaller;}\ #playerX .library { height:300px; overflow:auto; }\ #playerX .playlist { height:268px; overflow:auto; }\ #playerX .status { height:28px; background: #333; color: white; }\ #playerX .enqueue {\ display:inline-block; width : 15px; height : 16px; text-align : center; \ color : black; background:#fff; border: 1px solid #d9d9d9; \ font-weight : 800; font-size : 14px; cursor : pointer;\ }\ #playerX .seperator h1 { display:inline-block; color:#777; }\ #playerX .playlist .ctrl { display:inline-block; width:40px; }\ #playerX .searchfield {margin-left:15px; color:#777; width:240px; background:none; border:none;}\ #playerX .searchfield .default {color:#999;}\ #playerX audio {vertical-align:middle;}\ #playerX .playlist .box { display:inline-block; width : 15px; height : 16px; text-align : center; \ color : black; background:#fff; border: 1px solid #d9d9d9; \ font-weight : 800; font-size : 14px; cursor : pointer; }\ #playerX img.thumb {max-height:24px}\ #playerX li {margin:2px 0px}\ #playerX .dropdownmenu {background:#bbb; }\ #playerX .dropdownmenu ul {list-style:none; padding:0px; }\ #playerX #tableheadcopy {width:100%; background: linear-gradient(to right, white, silver); }\ "); var $mw = pl.getMainWindow().appendTo(document.body); $mw.find(".seperator_status_playlist").append("<h1>playlist</h1>"); $mw.find(".seperator_playlist_library").append("<h1>library</h1> "+pl.getLibrary().totalTracks()+" songs "); $mw.find(".searchfield").val("Search...").data("default","Search...").appendTo( $mw.find(".seperator_playlist_library")).focus(function() { var $this = $(this); if($this.val() == $this.data("default")) { $this.removeClass("default"); $this.val(""); } }).blur(function() { var $this = $(this); if($this.val() === "") { $this.addClass("default"); $this.val($this.data("default")); } }); var pad2 = function(i) { if(i < 10 && i > -10) { return "0"+parseInt(i,10) } return ""+parseInt(i,10); }; var currentSorting = false; var currentDir = 1; var sortTwoDir = function(sid,str,desc) { return function(ev) { var $this = $(this); var pl = ev.data; // Player object if(currentSorting == sid && currentDir == 1) { pl.setSorting("desc:"+str); currentDir = -1; } else { pl.setSorting("asc:"+str); currentDir = 1; } currentSorting = sid; pl.refresh(1); } }; var sortFourDir = function(sid,str1,desc1,str2,desc2) { return function(ev) { var $this = $(this); var pl = ev.data; // Player object if(currentSorting == sid && currentDir == 0) { pl.setSorting("asc:"+str1); currentDir = 1; } else if(currentSorting == sid && currentDir == 1) { pl.setSorting("desc:"+str1); currentDir = 2; } else if(currentSorting == sid && currentDir == 2) { pl.setSorting("asc:"+str2); currentDir = 3; } else if(currentSorting == sid && currentDir == 3) { pl.setSorting("desc:"+str2); currentDir = 0; } else { pl.setSorting("asc:"+str1); currentDir = 1; } currentSorting = sid; pl.refresh(1); } }; pl.setColumns([ { title: "❏", get: function(track) { if(track.album.thumb) { return '<img onclick="PopupImage.show_inner(this.dataset.cover,800,800,this)" src="'+track.album.thumb+'" class="thumb" alt="cover" title="Open cover" data-cover="'+track.album.cover+'">'; } else { var symbols = ["127912","127917","9825","127881","128145","128049","127932","128588","10048","128018","128051"]; var i = 0; track.album.title.split("").map(function(v) {i += v.charCodeAt(0)}); i = (parseInt(i*0.1,10) % (symbols.length+1))-1; return '<span style="font-size:20px;">&#'+symbols[i]+';</span>'; } } }, { title: "Name", key: "title", fieldclick : libraryTrackMenu, headerclick: sortTwoDir(1,"title","Sort by song") // TODO use string for sorting description }, { title: "#", get: function(track) { return track.track_num+"/"+track.album.totaltracks; }, headerclick: sortTwoDir(2,"track_num","Sort by track") // TODO use string for sorting description }, { title: "Album", get: function(track) { return '<a target="_blank" href="'+track.album.url+'">'+track.album.title+'</a>'; }, headerclick: sortFourDir(3,"album.title","Sort by album","band.title,album.title,track_num","Sort by artist and album") }, { title: "Artist", key: "band.title", headerclick: sortTwoDir(4,"band.title","Sort by artist") }, { title: "Duration", get: function(track) { return pad2(Math.floor(track.duration / 60))+":"+pad2(Math.floor(track.duration % 60)); }, headerclick: sortTwoDir(5,"duration","Sort by length") } ]); pl.refresh(); // Start playing because we just closed a playing player in another window if(pos && "number" == typeof pos) { pl.playAt(pos); } // Refresh every 30 seconds if(iv_refreshInterval === false) { iv_refreshInterval = window.setInterval(function() { pl.refresh(0,true); },1000*config.refreshInterval); } return pl; } function invertRGB(s) { var c = s.match(/(\d+), (\d+), (\d+)/); c.shift(); c = c.map(function(v) {return 255-parseInt(v,10);}); return "rgb("+c[0]+","+c[1]+","+c[2]+")"; } function showPlayerButton(Lib) { // Show a small icon in the right top corner or on the left hand side of the bandcamp menu var b; if(document.getElementById("user-nav")) { b = $('<li class="menubar-item"><a><span class="buttonopenplayer">'+config.ButtonOpenPlayer+'</span></a></li>').attr("title","Open player").css({ "cursor":"pointer" }).click(Lib,maximizePlayer).dblclick(fullscreenPlayer).appendTo($("#user-nav")); window.setTimeout(function() { b.appendTo($("#user-nav")); },1000); } else { b = $('<div class="buttonopenplayer">'+config.ButtonOpenPlayer+'</div>').attr("title","Open player").css({ "position":"absolute", "top":"0px", "right":"5px", "cursor":"pointer", "z-index":10 }).click(Lib,maximizePlayer).dblclick(fullscreenPlayer).appendTo(document.body); try { // On bandcamp.example.com pages this (sometimes) throws a security exception probably because of something cross domain b.css("color",invertRGB(window.getComputedStyle(document.body).backgroundColor)); } catch(e) { b.css("color","black"); } } } (function() { if(!unsafeWindow.TralbumData && !document.location.href.match(config.fullScreenURL)) return; var result = initBCLibrary(); if(result.buttons) { showPlayerButton(result.library); } else if(document.location.href.match(config.fullScreenURL)) { showPlayerButton(result.library); maximizePlayer(null,result.library); fullscreenPlayer(); window.setTimeout(fullscreenPlayer,1000); // This might happen if the player has to wait for another window window.setTimeout(fullscreenPlayer,3000); } })();