cuenation SCPlayer

Play cued mixes right from Soundcloud!

// ==UserScript==
// @name        cuenation SCPlayer
// @locale      en
// @description Play cued mixes right from Soundcloud!
// @license     http://creativecommons.org/licenses/by-sa/3.0/
// @namespace   http://cuenation.com
// @include     http://cuenation.com/?page=tracklist*.cue
// @version     0.7.3
// ==/UserScript==

/**
 * Changelog
 * v0.1         Initial version
 * v0.2         switched to SC Widget API
 * v0.3         Added some style
 * v0.4         'Now playing...' caption
 * v0.4.1       Cursor changed to pointer
 * v0.5         Added cursor, play/pause fix
 * v0.6         Hidden/visible player
 * v0.6.1       Support for private tracks with secret_token
 * v0.6.2       LOAD_PROGRESS is not supported any more
 * v0.7         Chrome support, progress bar on tracks
 * v0.7.1       Bugfix at seeking
 * v0.7.2       Update cursor if seeking in paused state
 * v0.7.3       Typo fixes + SC ID
 *
 *
 * Todo list, known issues:
 * - volume control
 * - support for various cueNation layouts
 * - shuffle/reorder tracks
 * - document and optimize code
 *
 */

var doc = document;
var forEach = Array.prototype.forEach;

var $ = null;
var SC = null;
var widget = null;

var loadedPercent = 0.0;
var loadedMs = 0;
var duration = 0;
var lastLoadedTrack = 0;
var tracks = new Array();
var currentIdx = -1;
var paused = true;

/* General class for the Tracks */
function Track(_o, _l, _e, _clickCallback) {
    this.offset = _o;
    this.element = _e;
    this.length = _l;
    var self = this;
    this._addEvent('click', function(e){
        _clickCallback(self.offset);
        e.stopPropagation();
    });
}

Track.prototype.Length = function() {
    return this.length;
};

Track.prototype.Offset = function() {
    return this.offset;
};

Track.prototype.Element = function() {
    return this.element;
};

Track.prototype._addEvent = function(eventName, callback) {
    if (this.element.addEventListener) {
        this.element.addEventListener(eventName, callback, false);
    } else {
        this.element.attachEvent(eventName, callback, false);
    }
};

/***************************/
/******** Functions */

// Init Soundcloud player widget API
function initSC1() {
    var sc = doc.createElement('script');
    sc.src = 'https://w.soundcloud.com/player/api.js';
    sc.type = 'text/javascript';
    sc.id = 'api1';
    sc.onload = function () {
        initSC2();
    };
    doc.getElementsByTagName('head')[0].appendChild(sc);
}

// Init Soundcloud general API 
function initSC2() {
    var sc = doc.createElement('script');
    sc.src = 'https://connect.soundcloud.com/sdk.js';
    sc.type = 'text/javascript';
    sc.onload = function () {
        initJQ();
    };
    doc.getElementsByTagName('head')[0].appendChild(sc);
}

// Init JQuery 
function initJQ() {
    var jq = doc.createElement('script');
    jq.src = 'http://code.jquery.com/jquery.min.js';
    jq.type = 'text/javascript';
    jq.onload = function() {
        initWidget();
    };
    doc.getElementsByTagName('head')[0].appendChild(jq);
}


// Function to retrieve the SC url from the links divs 
function getSCUrl() {
    var anchors = doc.querySelectorAll('a.clear');
    var firstUrl = '';
    var found = false;
    forEach.call(anchors, function(anchor){
        if (anchor.href.indexOf('soundcloud.com') > 0 && !found) {
            firstUrl = anchor.href;
            found = true;
        }
    });
    if (firstUrl !== ''){
        var re = new RegExp('.*url=(.*soundcloud\.com.*)', 'g');
        var arr = re.exec(firstUrl);
        var url = unescape(arr[1]);
        if (url.indexOf('/download') > 0) {
            url = url.replace('download', '');
        }
        return url;
    }else {
        return '';
    }
}

// Init the Soundcloud widget
function initWidget() {
    $ = unsafeWindow.jQuery;
    SC = unsafeWindow.SC;

    var placeholder = $('h2.title');
    var track_url = getSCUrl();
    if (track_url !== '') {
        SC.initialize({
            client_id: '24fe22b60396d3664c297e47f0f27929'
        });
        SC.get('/resolve', {url: track_url},  function(track){
            if (track.id === null || track.id === undefined) {
                addNoSC(placeholder, 'Track is removed. Player is disabled.');
            } else {
                var minimized = false;
                var soundurl = track.uri;
                var woptions = '&auto_play=false&auto_advance=false&buying=true&liking=true';
                woptions += '&download=true&sharing=true&show_artwork=false&show_comments=false';
                woptions += '&show_playcount=true&show_user=false';

                placeholder.after('<div id="box"><table id="boxCaption"><tr><td id="showhide"></td><td id="current"></td></tr></table><iframe class="ifr" width="100%" height="160" scrolling="no" frameborder="no"></iframe></div>');

                var ifr = doc.querySelector('.ifr');
                ifr.style.transition = 'height 0.5s linear 0s';
                ifr.style.height = '160px';
                ifr.src = 'https://w.soundcloud.com/player/?url=' + soundurl + woptions;
                widget = SC.Widget(ifr);

                initEvents();
                var boxCaption = doc.querySelector('#boxCaption');
                boxCaption.style.color = '#aaa';
                boxCaption.style.margin = '0px';
                boxCaption.style.cursor = 'pointer';
                boxCaption.style.backgroundColor = '#f5f5f5';
                boxCaption.style.border = '1px solid #e5e5e5';
                boxCaption.style.width = '100%';
                boxCaption.style.height = '10px';
                boxCaption.style.tableLayout = 'fixed';

                var sh = doc.querySelector('#showhide');
                sh.style.paddingRight = '4px';
                sh.style.width = '90px';
                sh.innerHTML = '<< Hide Player';

                var box = doc.querySelector('#box');
                box.style.height = '170px';
                box.style.transition = 'height 0.5s linear 0s';

                boxCaption.addEventListener('click', function(e) {
                    if (!minimized){
                        box.style.height = '10px';
                        ifr.style.height = '0px';
                        minimized = true;
                        sh.innerHTML = '>> Show Player';
                    } else {
                        box.style.height = '170px';
                        ifr.style.height = '160px';
                        minimized = false;
                        sh.innerHTML = '<< Hide Player';
                    }
                });
            }
        });
    } else {
        addNoSC(placeholder, 'No soundcloud link is found. Player is disabled.');
    }
}

function addNoSC(placeholder, msg) {
    placeholder.after('<div class="noSC">'+msg+'</div>');
    var noSC = doc.querySelector('.noSC');
    noSC.style.color = '#999';
    noSC.style.paddingLeft = '0px';
    noSC.style.paddingBottom = '0px';
    noSC.style.fontStyle = 'italic';
}

function updateCurrent (playData)
{
  var res = {};
  res = getCurrentTrack(playData);
  if (res.ix != currentIdx) {
    removeCursor(currentIdx);
    addCursor(res.ix);
    currentIdx = res.ix;
  }
  updateProgressBar(res); //res.ix, res.progress
}

// bind to SC events
function initEvents(){
    if (widget === null) {
        return;
    }

    widget.bind(SC.Widget.Events['READY'], function(){
        widget.setVolume(20);
        widget.getDuration(function(val){
            duration = val;
        });
    });

    widget.bind(SC.Widget.Events['PLAY_PROGRESS'], function(eData){
        updateCurrent(eData);
        if (lastLoadedTrack == tracks.length-1)
        {
          return;
        }
        loadedPercent = eData.loadedProgress;
        loadedMs = duration * loadedPercent;
        for (var i = lastLoadedTrack; i < tracks.length; i++) {
            if (tracks[i].Offset() <= loadedMs) {
                tracks[i].Element().style.textDecoration = 'underline';
                tracks[i].Element().style.color = 'rgba(255,102,0,1.0)';
                tracks[i].Element().style.cursor = 'pointer';
                lastLoadedTrack = i;
            }
        }
    });

    widget.bind(SC.Widget.Events['PAUSE'], function(eData){
        var res = {};
        res = getCurrentTrack(eData);
        paused = true;
        var p = doc.querySelector('.pauseBtn');
        if (p !== null){
            p.innerHTML = 'play';
        }
    });

    widget.bind(SC.Widget.Events['PLAY'], function(eData){
        var res = {};
        res = getCurrentTrack(eData);
        paused = false;
        var p = doc.querySelector('.pauseBtn');
        if (p !== null){
            p.innerHTML = 'pause';
        }
    });

    widget.bind(SC.Widget.Events['FINISH'], function(eData){
        seekTo(0);
        widget.pause();
    });
}

function removeCursor(idx) {
    if (idx < 0) {
        return;
    }
    var idxElement = tracks[idx].Element().nextSibling;
    var titleElement = tracks[idx].Element().nextSibling.nextSibling;
    if (titleElement !== null && idxElement !== null) {
        idxElement.style.backgroundColor = 'rgba(255,255,255,0.0)';
        titleElement.style.backgroundColor = 'rgba(255,255,255,0.0)';
    }
    var toRemove = doc.querySelector('.pauseBtn');
    if (null !== toRemove) {
        toRemove.parentNode.removeChild(toRemove);
    }
}

function addCursor(idx) {
    var idxElement = tracks[idx].Element().nextSibling;
    var titleElement = tracks[idx].Element().nextSibling.nextSibling;
    if (titleElement !== null && idxElement !== null) {
        idxElement.style.backgroundColor = 'rgba(255,102,0,0.3)';
        titleElement.style.backgroundColor = 'rgba(255,102,0,0.3)';
        titleElement.style.height = '1.5pt';
    }
    var span = document.createElement('span');
    span.setAttribute('class', 'pauseBtn');
    span.innerHTML = paused ? 'play' : 'pause';
    span.style.paddingLeft = '7px';
    span.style.color = 'rgba(255,102,0,1.0)';
    span.style.fontWeight = 'bold';
    span.style.cursor = 'pointer';

    var cp = doc.querySelector('#current');
    cp.style.color = 'rgba(255,102,0,1.0)';
    cp.style.paddingLeft = '4px';
    cp.style.paddingRight = '4px';
    cp.style.borderLeft = '1px solid #e5e5e5';
    cp.innerHTML = '<marquee behavior="alternate" scrollamount=1 scrolldelay=5>Now playing: ' + titleElement.innerHTML + '</marquee>';

    span.addEventListener('click', function(e){
        if (!paused) {
            widget.pause();
        } else {
            widget.play();
        }
    });

    titleElement.appendChild(span);
}

function updateProgressBar(res)
{
  //res.ix, res.progress
  var toUpdate = doc.querySelector('.pBar');
  if (null !== toUpdate) {
      var i = toUpdate.getAttribute('ix');
      if (i != res.ix)
      {
        toUpdate.parentNode.removeChild(toUpdate);
      }
      toUpdate.style.width = '' + res.progress*100 + '%';
      return;
  }
  var toAdd = doc.querySelector('.pauseBtn');
  if (null !== toAdd) {
      var span = document.createElement('span');
      span.setAttribute('class', 'pBar');
      span.setAttribute('ix', res.ix);
      span.style.backgroundColor = 'rgba(255,102,0,1.0)';
      span.style.height = '2px';
      span.style.display = 'block';
      span.style.width = '' + res.progress*100 + '%';
      toAdd.parentNode.appendChild(span);
  }
}

function getCurrentTrack(eData) {
    var currPos = eData.currentPosition;
    var currIdx = 0;
    var found = false;
    var percent = 0;

    if (currPos >= tracks[tracks.length-1].Offset()) {
        currIdx = tracks.length-1;
        percent = (currPos - tracks[tracks.length-1].Offset())/(duration - tracks[tracks.length-1].Offset());
        found = true;
    }
    for (var i = 0; i < tracks.length - 1 && !found; i++) {
        if (tracks[i].Offset() <= currPos && tracks[i+1].Offset() > currPos) {
            currIdx = i;
            percent = (currPos - tracks[i].Offset())/(tracks[i+1].Offset() - tracks[i].Offset());
            found = true;
        }
    }
    return {ix: currIdx, progress: percent};
}

// seek to a certain position in milliseconds
function seekTo(ms) {
    if (null !== widget) {
        if (ms <= loadedMs){
            widget.seekTo(ms);
            var playData = {currentPosition: ms};
            updateCurrent(playData);
        } else {
            //???
        }
    }
}

// init the 'buttons': time offsets on the page
function initButtons() {
    var times = doc.querySelectorAll('.timeindex');

    for (var i = 0; i < times.length; i++) {
        var tracktimeParts = times[i].innerHTML.replace(/^.(\s+)?/, '').replace(/(\s+)?.$/, '').split(':');
        var ms = tracktimeParts[0]*60000 + tracktimeParts[1]*1000;
        var trackLengthElement = times[i].nextSibling.nextSibling.nextSibling;
        if (trackLengthElement !== null) {
            var l = trackLengthElement.innerHTML.replace(/^.(\s+)?/, '').replace(/(\s+)?.$/, '').split(':');
            var newTrack = new Track(ms, l, times[i], seekTo);
            tracks[i] = newTrack;
        }
    }
}


// MAIN
initButtons();
initSC1();