cuenation SCPlayer

Play cued mixes right from Soundcloud!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();