Skip to content

Instantly share code, notes, and snippets.

@hinell hinell/SC.Tracks.js
Created Oct 10, 2019

Embed
What would you like to do?
/********************
Name : SC.Tracks snippet
Version : 0.1.3
Last-Modified : 10/10/19
Description :
The programm walks over tracks:
Tracks.nodes = [
1track
2track <---- tracks.current
3track
4track
5track
]
History:
0.1.3
* Updated Accidental Click Page Transition Guard
0.1.2
* Added confirmation dialog when accidentally clicking relocation
**********************/
text = [
`London Blue Sky`
, `Adam Johnson - Anex`
, `I Wish I Knew`
, `In Orbit`
, `Leaving Without Saying Goodbye`
, `Hourglass by WÜST & INGRID`
, `20märz2019`
, `The trilling wire inside the blood`
, `Wangova - Dreamer`
, `Gentoo`
, `Remember This Summer`
// , `Shaun Allaway`
// , `irritated`
// , `centering`
// , `Storm Thoughts`
// , `Mattias spelar modulsynt VIII`
// , `Dave Smith (Sequential) OB6`
// , `fire in the cathedral`
// , `Aeternum I`
// , `SB410`
// , `The Outer Edge`
// , `Luullahan jotta on lysti olla`
// , `Evolutions`
// , `Distillery Road`
// , `Mildred`
]
console.clear()
// Utils
HTMLElement.prototype.$ = function(){return this.querySelector.apply(this,arguments) };
HTMLElement.prototype.$$= function(){return this.querySelectorAll.apply(this,arguments)};
String.prototype.contains = function(str){ return new RegExp(str).test(this) }
Array.prototype.forEach =
Array.prototype.each = function (fn ,this_){
if(this.length <=0 || fn == undefined) return;
let i = 0
if (this_) fn = fn.bind(this_);
while (i < this.length) {
fn(this[i],i++)
}
};
Set.prototype.toArray = function(){
let arr = [], iterator = this.values(), current;
while(!(current = iterator.next() ).done) arr.push(current.value) ;
return arr
};
Error.prototype.throw = function(){
throw this
}
NodeList.prototype.addEventListener = function (name, handler){
[].slice.call(this).forEach(el => el.addEventListener(name, handler))
}
NodeList.prototype.removeEventListener = function (name, handler){
[].slice.call(this).forEach(el => el.removeEventListener(name, handler))
}
/**
* Class that helps to handle EventTarget items
**/
var NodeListX = class {
constructor(nodes){
this.nodes = [].slice.call(nodes);
}
filter(fn, that){ return new NodeListX(this.nodes.filter(fn, that)) }
addEventListener(name, handler){
if (!this.nodes.length) { return }
this.nodes.forEach(el => el.addEventListener(name, handler))
}
removeEventListener(name, handler){
if (!this.nodes.length) { return }
this.nodes.forEach(el => el.removeEventListener(name, handler))
}
concat(nodelistx) {
return new NodeListX(this.nodes.concat(nodelistx.nodes))
}
push(eventTarget){
if (!(eventTarget instanceof EventTarget)) {
throw new TypeError(`Invalid argument type: EventTarget instance is expected!`)
}
this.nodes.push(eventTarget)
}
}
// Major classes
// Class which runs certain function "fn" at fixed interval of time
var Observer = function(fn,frequency){
this.fn = fn
this.fr = frequency
}
Observer.prototype.observe =
Observer.prototype.start = function (){
if(this.interval || this.running) {return};
this.running = true;
this.interval = setInterval(function(){
this.fn()
}.bind(this),this.fr)
return this
}
Observer.prototype.disconnect =
Observer.prototype.stop =
Observer.prototype.cancel = function(){
if(this.running && this.interval) {
clearInterval(this.interval);
return this.running = this.interval = false;
}
return this.running
}
var Button = class {
constructor (tag, id, style) {
this.el = document.getElementById(id) || document.createElement(tag || 'span')
this.el.id = id;
this.el.style = style || '';
this.defaultStyle = style;
}
insertInto (el){ el.appendChild(el) }
listen(event,fn){ this.el.addEventListener(event, fn) }
label(str) { this.el.innerText = str || ''}
style(st){
this.el.style = this.defaultStyle+st;
}
}
var SCButton = Button;
var Waveforms = function(selector){ this.selector = selector }
Waveforms.prototype.hide = function(){
if (this.hidden) {return}
this.hidden = true
style = document.querySelector('style').sheet;
style.insertRule((this.selector || '.waveform')+' {display: none}',style.cssRules.length)
}
var Track = class {
constructor(e){
this.e = e
this.mouseClickEvent = new MouseEvent('click', {bubble: true, cancelable: true, composed: false})
}
play(){ this.toggle(); return this }
toggle(){
this.e.querySelector(Track.playClassName)
.dispatchEvent(this.mouseClickEvent)
return this
}
}
Track.playClassName = '.sound__header .sc-button-play';
// Events listeners:
// tracks.onRunning - Called when searching is running
// tracks.onStop - Called when searching is paused/stopped
// tracks.onBad
// tracks.onFound
// tracks.onFetch(Array<node>) - Called when tracks are fetched. Provided with an argument of fetched trackss
// tracks.onTrackRemoval(node) - Called when track is removed from the list. Provided with a node to be removed
var Tracks = function(el,waveformselector,opt){
this.update(el);
this.current = this.nodes[0];
this.currentIndex = 0; // current checked node index of the tracks
// this.maxnodes = opt.nodeslimit == void 0 ? 127 : opt.nodeslimit // maximum nodes to load & to enumerate over
this.maxnodes = opt.nodeslimit || 512;
this.direction = 'wn' // searching direction: up or wn (down)
this.waveforms = new Waveforms(waveformselector);
// Tracks removal config
this.remove = opt.nodeRemove == void 0 ? false : opt.nodeRemove
this.hide = opt.nodeHide == void 0 ? true : opt.nodeHide
this.tracksLImit = opt.tracksLimit || 200;
// Removing extr tracks
this.removedTracks= 0;
this.extraTrackRemoveObject = new Observer(function(){
itemsCount = this.maxTracks;
if (this.tracksLImit
&& this.nodes.length > this.tracksLImit
&& this.currentIndex > this.tracksLImit - 1
) {
let tracksToRemove = [];
tracksToRemove = [].slice.call(tracks.nodes, 0, this.tracksLImit)
tracksToRemove.forEach(e => {
e.parentElement.removeChild(e)
});
this.removedTracks += tracksToRemove.length;
console.log(`Total tracks removed: %d`, this.removedTracks);
}
}.bind(this), 2000)
// fetching triggers soundcloud tracks dowload
this.fetching = new Observer(function(){
// console.clear()
// console.log('FETCHING IN <= NODES',this.fetchedtimes,this.nodes.length)
// console.log(this.current)
this.fetchedtimes ? this.fetchedtimes++ : (this.fetchedtimes = 1);
this.maxnodes && this.fetchedtimes >= (this.maxnodes) && this.fetching.cancel()
this.nodes.length >= this.maxnodes && this.fetching.cancel();
this.current.nextElementSibling || this._fetch()
// play last track everytime we get the tracks feed
/*let t = this.nodes[this.nodes.length - 1];
if(t) {
new Track(t).toggle()
}*/
// this._fetch()
}.bind(this),100*4)
// Most important part.
// Checking method iterates over fetched tracks
// and checks match for the track we have provided by Tracks.find() method
this.checking = new Observer(this.check = function(){
this._match()
if(this.found) {
// once found let's start to play
/*
try {
new Track(this.current).toggle()
} catch (e){
console.log('Whooops. Play is failed!', e)
}
*/
this.onFound && this.onFound();
console.clear()
console.log('IT IS FOUND :) ',this.nodes.length);
console.log(this.current)
this.checking.cancel();
this.fetching.cancel()
return
}
// if(this._hasNext()) return this._walk();
if(this._hasNext()) {
this.onRunning && this.onRunning(this);
this._walk();
}
// else { this.direction == 'wn' && this.nodes.length < this.maxnodes && this._fetch()};
// FINALLY: NOTHING FOUND
if(!this._hasNext() && this.nodes.length >= this.maxnodes){
console.clear()
console.log('NOTHING FOUND :( )',this.nodes.length)
this.onBad && this.onBad(this);
window.scroll(0,0)
this.checking.cancel();
this.fetching.cancel()
return
}
}.bind(this),100*1.5);
// MAIN LOOOP with observers
this.observers = [
this.checking
, this.fetching
, this.extraTrackRemoveObject
]
}
// Resets nodes to the specified element's children
Tracks.prototype.update = function(el){
this.el = el;
if(!el) {
this.nodes = [];
return
}
this.className = '.'+el.className.split(/\s/g).join('.')
this.nodes = el.children;
}
Tracks.prototype._fetch = function(limit){
let lastPosition = window.scrollY;
window.scroll(0,9999999)
// window.scroll(0,lastPosition);
this.onFetch && setTimeout(() => {
let newlyAddedNodes = [].slice.call(this.nodes, this.currentIndex);
this.onFetch(newlyAddedNodes)
}, 1000);
}
Tracks.prototype._match = function(selector){
this.remove && (this.checked = this.checked == void 0 ? 0 : ++this.checked)
let textelem = this.current.querySelector(this.selector || selector);
textelem || new Error('Invalid selector: the target for string search hasn\'t been found! ').throw()
if(this.str instanceof Array) {
return this.found = this.str.some(searchString => new RegExp(searchString).test(textelem.innerText) )
}
this.found = textelem.innerText.contains(this.str)
}
// True if a sibling of the current node exists
Tracks.prototype._hasNext= function(){
switch (this.direction){
case 'up': return this.current.previousElementSibling;
case 'wn': return this.current.nextElementSibling;
}
}
// Walking next
Tracks.prototype._walk = function(){
let previous;
this.current.style.display == 'none' && (this.current.style.display = '')
switch (this.direction){
case 'up': this.current = this.current.previousElementSibling; break;
case 'wn':
if(this.remove) {
previous = this.current;
this.current = this.current.nextElementSibling;
previous.parentElement.removeChild(previous)
if(this.onTrackRemoval) {
this.onTrackRemoval(this.current)
}
}
if(this.hide) {
this.current.style.display = 'none';
this.current = this.current.nextElementSibling;
}
if(!this.remove && !this.hide) {
this.current.style.border = 'none'
this.current.removeAttribute('style')
this.current = this.current.nextElementSibling
this.current.style.border = 'inset .6em #ff5500';
}
}
this.currentIndex = [].slice.call(this.nodes).indexOf(this.current);
}
Tracks.prototype.find = function(searchstring,selector,direction = 'wn'){
this.str = searchstring || this.str || new Error('Invalid argument: string required!').throw()
this.selector = selector || this.selector || new Error('Invalid argument: selector for string search required!').throw()
this.direction= direction || this.direction|| 'wn';
console.log('SEARCHING ',this.direction == 'wn' ? 'DOWN' : 'UP')
}
Tracks.prototype.pause =
Tracks.prototype.stop = function(){
if(!this.el) { return }
this.running = false
this.observers.forEach(e => e.stop() );
this.onStop && this.onStop(this);
}
Tracks.prototype.start = function(){
if(!this.el){
this.running = false;
return
}
if(this.nodes.length !== this.el.children.length){
this.pause()
this.reset()
}
this.running = true
this.observers.forEach(e => e.start() );
}
Tracks.prototype.toggle = function(){
if(!this.el) { return }
this.observers.every(o => o.running)
? this.pause()
: this.start()
}
// RESET SEARCH. Tip: uset it only after pausing
Tracks.prototype.reset = function(){
this.current = this.nodes[this.currentIndex = 0];
}
Tracks.prototype.show = function(){
[].slice.call(this.nodes).each(e => e.style.display = '')
}
/**
* Confirmation Popup.
* Every accidental click must be confirmed before page transition starts.
**/
var ConfirmationPopup = class {
constructor() {
this.isOpen = false;
this.defaultMessage = `No message is left here!`;
}
open(message){
let result;
if (this.isOpen) {
return
}
this.isOpen = true;
result = confirm(message || this.defaultMessage);
setTimeout(function(){
this.isOpen = false;
}.bind(this))
return result
}
}
/**
* Mouse Link Click event interceptor.
* This class provides specific event listener to every nodes
* and keeps all nodes accountable
**/
var PageTransitionGuard = class {
static LINK_STOP_WORDS = /Play|Pause|Stop|Continue|Repeat/g
static LINK_STOP_CLASSES = [
`compactTrackList__moreLink`
, `soundTitle__title`
, `header__userNavActivitiesButton`
];
static MessageTemplate = class {
generate (link = `<UnspecifiedLink>`){
return `You are currently in search mode. Are you sure you want to go to ${link}?
(press cancel to stay here)`
}
}
constructor({ popup }){
if (!(popup instanceof ConfirmationPopup)) {
throw new TypeError(`Invalid argument: ConfirmationPopup instance is expected!`)
}
this.popup = popup;
this.nodes = new Set()
this.message = new this.constructor.MessageTemplate();
this.enabled = true;
}
listener(e) {
e.stopPropagation();
let allowTransition = false;
if (this.popup.isOpen) { return }
allowTransition = this.popup.open(this.message.generate(e.currentTarget.href));
if (!allowTransition) {
e.preventDefault();
}
}
toggle() { this.enabled = !this.enabled }
/**
* Add listener to provided node
**/
add(node){
const alreadyAssigned = this.nodes.has(node);
const containsStopWord = this.constructor.LINK_STOP_WORDS.test(node.title);
const haveExcludedClasses = this.constructor.LINK_STOP_CLASSES.some((className) => {
return node.classList.contains(className)
})
const isNotEligible = alreadyAssigned || containsStopWord || haveExcludedClasses;
if (isNotEligible) {
debugger
return
}
node.addEventListener(`click`, this.listener.bind(this))
return this.nodes.add(node)
}
/**
* Remove node from links set
**/
rem(node){
return this.nodes.delete(node)
}
addMany(collection){
Array.prototype.forEach.call(collection, this.add.bind(this))
}
remMany(collection){
Array.prototype.forEach.call(collection, this.rem.bind(this))
}
}
// Initializing Buttons
if(tracks instanceof Tracks) { tracks.pause(); tracks = undefined }
var tracks,
// RESET & STOP BUTTON
resetButton, stopButton;
resetButton = new SCButton('span', 'snippetResetButtonTrack', 'cursor: pointer');
resetButton.el.innerText = "R";
resetButton.el.title = "Reset button. Press to reset search to 0";
resetButton.listen('click', () => {
tracks.pause();
tracks.reset();
stopButton.label('PROCEED: ' + tracks.currentIndex);
stopButton.el.style = '';
})
stopButton = new SCButton('span', 'trackFinderStopButton', 'width: 9em; cursor: pointer');
stopButton.el.innerText = 'START SEARCH';
stopButton.el.onclick = () => tracks.toggle();
stopButton.el.title = 'Start/Pause button'
nextButton = new SCButton('span', 'trackPassOverButton', 'cursor: pointer')
nextButton.el.innerText = 'N';
nextButton.el.title = 'Proceed to the next track';
nextButton.el.onclick = function(){
let next = tracks.current.nextElementSibling;
if(!next) { return }
tracks.current = next;
tracks.start();
}
currentTrackButton = new SCButton('span', 'snippetShowCurrentTruck', 'cursor: pointer;' )
currentTrackButton.el.innerText = 'CR';
currentTrackButton.el.title = 'Scroll current track into view'
currentTrackButton.el.onclick = function(){
tracks.current.scrollIntoView();
tracks.current.style.border = 'inset .6em #ff5500';
setTimeout(() => tracks.current.style.border = '', 5000 )
};
[resetButton, stopButton, nextButton, currentTrackButton]
.forEach(b => b.el.className = 'header__link header__proUpsell');
var tracksClassName = '.lazyLoadingList__list.sc-list-nostyle.sc-clearfix';
var initTrackSearch = function(){
debugger
// Guard from accident relocation
document.querySelector('div.header__middle').style='display: none';
var panel = document.querySelector('.'+'header__right sc-clearfix'.split(' ').join('.'))
// INITIALIZING BUTTONS
var buttons = [nextButton, resetButton, stopButton, currentTrackButton];
buttons.reverse().forEach(function(button){
let buttonEl = document.getElementById(button.el.id)
if(!buttonEl) {
panel.insertAdjacentElement('afterbegin', button.el);
}
})
// resetButton.el.style = stopButton.el.style = ''; // display
if(document.location.pathname === '/stream'){
var tracklist = document.querySelector(tracksClassName);
if(tracks) {
tracks.update(tracklist);
console.log('Elements update interval cleared!')
} else {
tracks = new Tracks(tracklist,'.waveform',{
nodeslimit : 256 * 3
,nodeHide : false // Hide nodes
});
tracks.waveforms.hide();
}
// INITIALIZING CLICK PAGE TRANSITION GUARD
var confirmationDialog = new ConfirmationPopup();
var transitionGuard = new PageTransitionGuard({ popup: confirmationDialog });
// Add links to the interceptor so every link clicked gets confirmation window shown
transitionGuard.addMany(document.querySelectorAll(`a`));
transitionGuard.addMany(document.querySelectorAll(`.soundTitle__tagContainer`));
// tracks.onFetch = interceptor.addMany.bind(interceptor);
// tracks.onTrackRemoval = interceptor.rem.bind(interceptor);
tracks.onFetch = function(nodes){
Array.prototype.forEach.call(nodes, (node) => {
transitionGuard.addMany(node.querySelectorAll(`a`))
transitionGuard.addMany(node.querySelectorAll(`.soundTitle__tagContainer`));
})
}
tracks.onTrackRemoval = function(node){
Array.prototype.forEach.call(nodes, (node) => {
transitionGuard.addMany(node.querySelectorAll(`a`))
transitionGuard.addMany(node.querySelectorAll(`.soundTitle__tagContainer`));
})
debugger
}
tracks.onRunning = function(){
stopButton.label('...' + tracks.currentIndex);
stopButton.style('cursor: wait')
}
tracks.onBad = function(tracks){
stopButton.label('BAD :('+tracks.currentIndex)
}
tracks.onFound = function(){
stopButton.label(`SUCCESS ${this.currentIndex}:`);
var removeHighlight = function(){
this.current.style.border = ''
}
// Highlight found node
currentTrackButton.el.onclick()
this.current.addEventListener('mouseover', removeHighlight)
setTimeout(function(){
this.current.removeEventListener('mouseover', removeHighlight)
}.bind(this))
// window.scroll(0,this.foundnode.offsetTop)
// window.scroll(0,this.current.getClientRects()[0].top+window.pageYOffset);
}
tracks.onStop = function(){
stopButton.label('CONTINUE: ' + tracks.currentIndex);
stopButton.style('')
}
textsearchselector = '.soundTitle__usernameTitleContainer'
;tracks.find(text,textsearchselector,'wn')
// ;tracks.toggle()
} else {
;
(resetButtonOk) && (resetButton.el.style = 'display: none'); // hide
(stopButtonOk) && (stopButton.el.style = 'display: none');
tracks && tracks.update(undefined);
document.querySelector('div.header__middle').style='display:';
}
}
if (window.location.pathname !== `/stream`) {
const stream = `https://soundcloud.com/stream`;
window.location.assign(stream)
alert(`You are about to go to the right page.\n Don't forget to run scripts!`)
} else {
var tracksFindingInterval;
if(tracksFindingInterval){
clearInterval(tracksFindingInterval)
};
// tracksFindingInterval = setInterval(initTrackSearch, 5000);
initTrackSearch();
;test = function(str, str2){ return new RegExp(str).test(str2 || str) };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.