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);
}
})();