// ==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);
}
})();
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.