// ==UserScript==
// @name Bandcamp TagPlayer
// @namespace https://github.com/SLLABslevap/bandcamp-tagplayer
// @description A genre/tag audio player for bandcamp.com (like Last.fm Player for Last.fm).
// @version 0.1.2
// @include http*://bandcamp.com/tagplayer
// @include http*://bandcamp.com/tagplayer?*
// @include http*://bandcamp.com/tagplayer/*
// @include http*://bandcamp.com/tags
// @include http*://bandcamp.com/tags/
// @include http*://bandcamp.com/tag/*
// @icon https://github.com/SLLABslevap/bandcamp-tagplayer/raw/master/icon/icon.png
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @downloadURL https://update.greasyfork.icu/scripts/7786/Bandcamp%20TagPlayer.user.js
// @updateURL https://update.greasyfork.icu/scripts/7786/Bandcamp%20TagPlayer.meta.js
// ==/UserScript==
//Global var declarations/definitions
var tpName = "Bandcamp TagPlayer";
var tpVersion = "0.1.2 pre-alpha post-WIP development";
var tagplayerURL = window.location.protocol + "//bandcamp.com/tagplayer/?";
var tagViaUrl="";
var tagViaUrlForDisplay="";
//Global var declarations/definitions - part 2
var bandcampTagUrl = window.location.protocol + "//bandcamp.com/tag/";
var currentTagForDisplayUNSAFE; //Human-readable version of the tag as parsed from the url, eg. "rock & roll" or "1337h4x0r"
var currentTagForDisplaySafe; //Human-readable version of the tag, html escaped, eg. "rock & roll" or "<b>1337h4x0r</b>"
var currentTag; //Bandcamp-readable version of the tag eg. "rock-roll" or "-b-1337h4x0r-b-"
var firstRun = true;
var isTrackLoading = false;
var nextPage;
var currPage;
var albumList;
var preferPopular = (localStorage.getItem("GM_BCTP_preferPopular") != "false" ? true : false); //false: prefer newness over popularity tracks when loading tracks, true: the opposite
var consecutiveErrorCount=0; //For preventing error loops when loading tracks fails consecutively because of timeout / connection error
//On the "/tags" page insert option to play tags in the TagPlayer
if (/^https?\:\/\/(www\.)?bandcamp\.com\/(tags)\/?$/.test(document.location)){
insertDnDTagLoader();
//On every "/tag/..." page insert a link for playing that particular tag in the TagPlayer
} else if (/^https?\:\/\/(www\.)?bandcamp\.com\/(tag\/([^\/]+))\/?$/.test(document.location)){
try {
var h1 = document.getElementById("content").getElementsByTagName("h1")[0];
var a = document.createElement("a");
a.href = tagplayerURL + encodeURIComponent(h1.textContent);
a.setAttribute("class","tp_playA");
a.innerHTML = "\u25B6";
GM_addStyle(".tp_playA{font-size:0.7em;margin-left:10px}.tp_playA:hover{text-decoration:none !important;color:#8BC9FF !important;}")
h1.appendChild(a);
} catch (ex){
//console.log("[BCTP]\t"+"Couldn't insert TagPlayer links."); //DEBUG
displayMsgAlt("Couldn't insert TagPlayer links. If this problem persists, please report it to developer of Bandcamp TagPlayer.",true);
}
//On every other page listed as an @include (other than "/tags" or "tag/...") load TagPlayer
// currently that only means bandcamp.com/tagplayer
// since BCTP currently uses the CSS of the page it's loaded on, it cannot be loaded on any page, because the styles might be screwed up
} else if (/^https?\:\/\/(www\.)?bandcamp\.com\/tagplayer(\/.*)?|(\?.*)?$/i.test(document.location)){
//Cover original bandcamp.com/tagplayer (404) page, display loading message
initLoaderOverLay();
var n = (document.location.href).match(/^https?\:\/\/(?:www\.)?bandcamp\.com\/(?:[^\/\&\#\?]+)(\/|\?+)([^?].*)/);
//If format is "http://bandcamp.com/tagplayer/sometag" redirect to "http://bandcamp.com/tagplayer/?sometag"
//Or if format is "http://bandcamp.com/tagplayer?sometag" redirect to "http://bandcamp.com/tagplayer/?sometag"
if (n){
document.location.href = tagplayerURL + n[2];
//Else continue
} else {
//Check if there is a tag passed to the player via url, eg. "http://bandcamp.com/tagplayer/?rock"
// if there is (eg. "rock"), save it in tagViaUrl
var m = (document.location.href).match(/^https?\:\/\/(?:www\.)?bandcamp\.com\/(?:[^\/\&\#\?]*?)\/\?*(.*)/);
//console.log("[BCTP]\t"+"Tag via URL regex: " + m); //DEBUG
if (m!==null) {
if (m[1])
tagViaUrl = decodeURIComponent(m[1]);
//console.log("[BCTP]\t"+"tagViaUrl " + tagViaUrl) //DEBUG
}
//Add player overlay
initPlayer();
//Load tag, load first random track, remove loader overlay
// if there is a tag passed via url, load it
if (tagViaUrl) {
loadTag(tagViaUrl);
// else load the TagPlayer landing page (withoutt loading a tag)
} else {
setTagDisplay(false, "", "");
removeLoaderOverLay();
firstRun = false;
}
}
}
//Loads an overlay that covers up the original bandcamp.com/tagplayer 404 page
// this will be hidden (replaced by the player overlay) as soon as the player is loaded
function initLoaderOverLay(){
var d = document.createElement("div");
d.id = "tagPlayerOverlay";
d.setAttribute("style","display:block;width:100%;height:100%;position:fixed;left:0px;top:0px;background-color:#fff;z-index:999999;");
var l = document.createElement("p");
l.id="tagPlayerOverlayTxt"
l.setAttribute("style","width:100%;text-align:center;position:fixed;margin-top:50px;color:#888;font-size:1.7em");
l.innerHTML = "Loading player...Please wait!"
d.appendChild(l);
document.body.appendChild(d);
var s = document.createElement("script");
s.id = "tpLoaderScr";
s.innerHTML = 'var tp_u=true;var tp_s=setTimeout(function(){clearInterval(tp_t);var tpt = document.getElementById("tagPlayerOverlayTxt"); if (!tpt) return; tpt.innerHTML = "It\'s taking more than 10 seconds to load this tag. You should probably consider reloading this page.";},10000);var tp_t = setInterval(function (){var tpd=document.getElementById("tpLoaderDots");if(!tpd){clearInterval(tp_t);return;};var tpop = parseFloat(tpd.style.opacity); if (tpop>=1){tp_u=false;} else if (tpop<=0.2){tp_u=true} tpop = (tp_u ? (tpop+0.1) : (tpop-0.1)); tpd.style.opacity = tpop;},60);';
document.body.appendChild(s);
}
function updateLoaderOverLay(msg){
var tpTxt = document.getElementById("tagPlayerOverlayTxt");
if (tpTxt){
tpTxt.innerHTML = msg;
}
}
function removeLoaderOverLay(){
try {
var ts = document.getElementById("tpLoaderScr");
var to = document.getElementById("tagPlayerOverlay");
if (ts) document.body.removeChild(ts);
if (to) document.body.removeChild(to);
} catch (ex){
//console.log("[BCTP]\t" + "Couldn't remove overlay."); //DEBUG
}
}
//Inserts option on the /tags/ page to open tags in TagPlayer
function insertDnDTagLoader(){
try {
var wDiv = document.createElement("div");
wDiv.innerHTML = '';
document.getElementById("content").parentNode.insertBefore(wDiv,document.getElementById("content") );
var pArr = [];
var h1 = document.getElementsByTagName("h1");
for (var i=0;i
If this problem persists, please report it to developer of Bandcamp TagPlayer.",true);
}
}
//Event handler for the click event on the tag links on the /tags/ page.
//If event was fired on a tag link, it opens the tag in TagPlayer
function tagToPlayer(e){
var target = e.target || e.srcElement
if (target.tagName.toLowerCase() == "a" && /^\s*tag(\s+.*?)?/.test(target.className)){
e.preventDefault();
window.location.href = tagplayerURL + target.textContent;
}
}
//Loads tag in TagPlayer.
// tag - actual tag to load in TagPlayer, eg.
// tagForDisplay - optional text to display instead of tag
function loadTag(tag, tagForDisplay){
setCurrPlayingTag("loading...")
setTrackLoading(true); //loading indicator will be stopped in expandalbumlist->loadtrack
if (tagForDisplay){
currentTagForDisplayUNSAFE = tagForDisplay;
currentTagForDisplaySafe = escapeHtml(tagForDisplay);
} else {
currentTagForDisplayUNSAFE = tag;
currentTagForDisplaySafe = escapeHtml(tag);
}
currentTag = makeCompatible(tag);
//console.log("[BCTP]\t"+"loadTag\n" + tag + "\n" + currentTag); //DEBUG
nextPage = 1;
currPage = 1;
clearAlbumList()
expandAlbumList(true);
setCurrPlayingTag(currentTagForDisplaySafe,currentTag)
setTagDisplay(false,currentTag,currentTagForDisplayUNSAFE);
//console.log("[BCTP]\t"+"loadTag\n" + "end"); //DEBUG
}
//Fetches more albums from the next page of "bandcamp.com/tag/sometag" for the current tag,
// then adds the albums to the album pool (albumList),
// from which the next random track will be selected from
function expandAlbumList(loadTrack){
currPage = nextPage;
//console.log("[BCTP]\t expandAlbumList URL: "+bandcampTagUrl + "" + currentTag + "?page=" + nextPage + (preferPopular==false ? "&sort_field=date" : "&sort_field=pop")); //DEBUG
GM_xmlhttpRequest({
method: "GET",
url: bandcampTagUrl + "" + currentTag + "?page=" + nextPage + (preferPopular==false ? "&sort_field=date" : "&sort_field=pop"),
timeout: 30000,
onload: function(response) {
var d = document.createElement("div");
d.innerHTML = response.responseText;
var itemList = d.getElementsByClassName("item_list");
if (itemList.length<=0){ //no more tag pages, stop expanding albumlist
return;
}
var albumA = itemList[0].getElementsByTagName("a"); // album links for this page
if (albumA.length<=0){ //no albums found for this tag, stop script
if (nextPage == 1 || albumList.length <= 0) {//first run, theoretically always true if parent if is true
setCurrPlayingTag("nothing");
setTrackLoading(false);
setTagDisplay(false, "","");
if (firstRun){
removeLoaderOverLay();
firstRun = false;
}
displayMsg("Couldn't find any albums for the tag \"" + currentTagForDisplaySafe + "\".",true)
}
return;
}
addToAlbumList(albumA)
//console.log("[BCTP]\t"+albumList); //DEBUG
++nextPage;
//if loadTrack==true, load the next track
// used when loading a new tag and
// filling albumList for the first time for this tag
if (loadTrack)
loadNext();
d.innerHTML = "";
}
});
}
//Clears albumList, indicates that temporarily there are no tracks to load next by disabling the next button
function clearAlbumList(){
albumList = [];
var nb = document.getElementById("tp_pi_nextbtn");
if (nb){
nb.style.opacity = "0.4";
nb.style.cursor = "default";
nb.style.background = "#111";
}
}
//Adds links in hrefArr to albumList, enables next button
function addToAlbumList(hrefArr){
if (hrefArr.length<=0)
return;
for (var i=0;i=50){
displayMsgAlt("Loading tracks failed multiple times. The script will now stop. If the problem persists, please report it to the developer.",true);
//console.log("[BCTP]\t"+"Loading next track failed. The script will now stop."); //DEBUG
if (firstRun){
updateLoaderOverLay("There was an error. The script stopped. If the problem persists, please report it to the developer of " +tpName+ ".")
}
setCurrPlayingTag("nothing");
setTrackLoading(false);
setTagDisplay(false, "","");
consecutiveErrorCount=0;
return;
}
if (albumList.length<=0)
return;
setTrackLoading(true);
var validityCheckTag = currentTag;
var r = getRandomInt(0, albumList.length-1);
var currentAlbumURL = albumList[r];
//console.log("[BCTP]\t"+"loadNext request " + currentAlbumURL); //DEBUG
GM_xmlhttpRequest({
method: "GET",
url: currentAlbumURL,
timeout: 7000,
onerror: function(response){
++consecutiveErrorCount;
displayMsgAlt("Loading track failed. Trying again...",true);
//console.log("[BCTP]\t"+"Loading track failed. Trying again..."); //DEBUG
loadNext();
},
ontimeout: function(response){
++consecutiveErrorCount;
displayMsgAlt("Loading track failed, probably because of slow connection/no connection. Trying again...",true);
//console.log("[BCTP]\t"+"Loading track failed (timeout). Trying again..."); //DEBUG
loadNext();
},
onload: function(response) {
try {
//console.log("[BCTP]\t"+"loadNext request response"); //DEBUG
var d = response.responseText;
var albumImg = d.match(/\/)[1];
var albumTitle = d.match(/\" + albumArtist + "";
pAlbum.innerHTML = "" + albumTitle + "";
pTrack.innerHTML = "" + trackTitle + "";
pImg.src = albumImg;
pAudio.src = trackURL;
pAudio.load();
document.head.getElementsByTagName("title")[0].innerHTML = trackTitle + " | " + albumArtist + " ["+tpName+"]";
pPlayer.style.display = "block";
}
//Loads TagPlayer UI
function initPlayer(){
//Minified HTML code for the player overlay, including a base64 encoded image for audio control
var playerHtml = '
Random indie band name of the day: "+getEdgyIndieBandName()+"
";
var contentWrapper = document.createElement("div");
contentWrapper.setAttribute("style","text-align:center;position:fixed;width:500px;height:300px;left:50%;top:50%;margin:-151px 0 0 -251px;z-index:999999;border:#ccc;text-align:right;");
exitDiv.appendChild(exitSpan);
contentWrapper.appendChild(exitDiv);
contentWrapper.appendChild(contentDiv);
aboutMainWrapper.appendChild(overlay);
aboutMainWrapper.appendChild(contentWrapper);
document.body.appendChild(aboutMainWrapper);
overlay.addEventListener("click",function(){
document.getElementById("tp_aboutWrapper").style.display = "none";
});
exitSpan.addEventListener("click",function(){
document.getElementById("tp_aboutWrapper").style.display = "none";
});
}
});
}
//Display messages using the id=tp_msg_wrapper element (only on tagplayer pages)
function displayMsg(txt,isError){
document.getElementById("tp_msg_txt").innerHTML = (isError ? "Error: " : "Info: ") + txt;
document.getElementById("tp_msg_wrapper").style.display = "block";
}
//Stand alone version of displayMsg, it displays messages using
// the id=tp_msg_wrapper element inserted into the current page
// (can be used on any page, its style might be affected by the page's CSS though)
function displayMsgAlt(txt,isError){
if (!document.getElementById("tp_msg_wrapper")){
var msgDiv = document.createElement("div");
msgDiv.innerHTML = '
X
';
document.body.appendChild(msgDiv);
document.getElementById("tp_msg_exit").addEventListener("click",function(e){
document.getElementById("tp_msg_wrapper").style.display = "none";
});
}
document.getElementById("tp_msg_txt").innerHTML = (isError ? "Error: " : "Info: ") + txt;
document.getElementById("tp_msg_wrapper").style.display = "block";
}
//Updates URL to match the currently playing tag
// so that the page can be bookmarked
// tag = bandcamp-readable tag
// disptagUnsafe = human-readable tag as parsed directly from the url (uri decoded), html not escaped
function setTagDisplay(isLoading, tag, disptagUnsafe){
if (isLoading){
} else {
window.history.pushState("BCTP", "", tagplayerURL + (disptagUnsafe));
}
}
//Updates the "currently plaing" section
function setCurrPlayingTag(tag, alttag){
var c = document.getElementById("tp_currplaying");
c.innerHTML = "" + tag + "" ;
}
//Updates page title with an indicator to the player's state (playing/stopped)
function setTitlePlaying(isPlaying){
var title = document.head.getElementsByTagName("title")[0];
if (!isPlaying){
title.innerHTML = title.innerHTML.replace("\u25B6 ", "");
} else {
if (title.innerHTML.indexOf("\u25B6")<0){
title.innerHTML = "\u25B6 " + title.innerHTML;
}
}
}
function setTrackLoading(isLoading){
if (isLoading){
isTrackLoading = true;
document.getElementById("tp_loading").style.display = "inline";
} else {
document.getElementById("tp_loading").style.display = "none";
isTrackLoading = false;
}
}
//Transforms tag string into bandcamp compatible format
// eg. "rock & roll" --> "rock-roll"
function makeCompatible(tag){
tag = tag.replace(/\&\;/g,"&");
tag = tag.replace(/[^\.\(\)a-zA-Z0-9]/g,"-")
tag = tag.replace(/\-+/g,"-");
return tag;
}
//Demoing my poor coding skills by generating shitty band names using a shitty limited dictionary
function getEdgyIndieBandName(){
var bn_b1 = "genomic,dimply,unawakened,premethodical,spermous,smoothbore,gestureless,suffuse,happy,wreckful,unobscene,prompt,future,lyophobic,sicklier,groomed,weer,mismatched,patchy,bettering,analytical,naturalist,disarming,hoodwink,buttonholed,pickling,leprose".split(",");
var bn_b2 = "robbery,chez,childbirth,anear,barrette,opuscule,bat,rabbitry,hamal,ideogram,future,bantamweight,legislate,spirit,subject,twig,embouchure,viking,landholding,conductor,doom,quirt,stimulation,nagana,moonlight,pinhead,prompt,drift ,cystine,paederast,cheddar,atelier,death".split(",");
var bn_r = getRandomInt(0,3);
var bn_a = [];
var bn_i;
for (bn_i=0;bn_i1 && getRandomInt(0,1) == 0){
bn_a[bn_a.length-2] = bn_b2[getRandomInt(0,(bn_b2.length)-1)];
bn_a[bn_a.length-2] = (bn_a[bn_a.length-2])[0].toUpperCase() + bn_a[bn_a.length-2].slice(1);
}
return bn_a.join(" ");
}
//http://www.janoszen.com/2012/04/16/proper-xss-protection-in-javascript-php-and-smarty/
function escapeHtml(unsafe) {
return (unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'"));
}
//https://stackoverflow.com/questions/566203/changing-css-values-with-javascript#11081100
function css(selector, property, value) {
for (var i=0; i