Skip to content

Instantly share code, notes, and snippets.

@hinell
Last active July 7, 2023 18:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hinell/747e1a937dd07ad027d359993e5327c1 to your computer and use it in GitHub Desktop.
Save hinell/747e1a937dd07ad027d359993e5327c1 to your computer and use it in GitHub Desktop.
/********************
Name : SC.Tracks snippet
Version : 0.4.5
Created.......: Jan 9, 2019
Last-Modified : July 07, 2023
Description :
note: Created date actually is much ealier
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
**********************/
var text = [
"Little Earth (Preview)",
'DarkBright'
];
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 NodeList items
**/
var NodeListX = class extends Object {
constructor(nodes){
this.nodes = Array.from(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(nodeOrNodes){
if(nodeOrNodes instanceof Element) this.pushOne(nodeOrNodes);
if(nodeOrNodes instanceof NodeList) this.pushOne(nodeOrNodes);
}
pushOne(node){
var isNode = node instanceof Element;
if (!isNode) { throw new TypeError(`Invalid argument: Element is expected`) }
return this.nodes.push(node);
}
pushAll(nodes){
var isNodes = nodes instanceof NodeList;
if (!isNode) { throw new TypeError(`Invalid argument: Element is expected`) }
isNodes.forEach((node) => this.nodes.push(node));
return this.nodes.length
}
remove(){
this.nodes.forEach(node => node.remove());
this.nodes = []; // resetting array
}
}
/**
* Class that helps to handle EventTarget-s items specifically
**/
var EventTargetsList = class {
push(eventTarget){
if (!(eventTarget instanceof EventTarget)) {
throw new TypeError(`Invalid argument type: EventTarget instance is expected!`);
}
super.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 Observer = class {
constructor({ fn, frequency }){
if (!fn) throw new Error(`Invalid arg: fn is required`);
this.fn = fn
this.fr = frequency
}
cancel() { throw new Error(`This API is deprecated!`); }
observe() { throw new Error(`This API is deprecated!`); }
start(){
if(this.interval || this.running) {return};
this.running = true;
this.interval = setInterval(function(){
this.fn();
}.bind(this),this.fr);
return this
}
stop(){
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 = class {
constructor(selector){
this.selector = selector;
}
}
var Track = class {
// Element's selectors
static playCSSSelector = `.sc-button-play`;
static stateCSSSelector = ``;
constructor(e){
this.e = e;
this.mouseClickEvent = new MouseEvent('click', {bubble: true, cancelable: true, composed: false});
}
play(){ this.toggle(); return this }
toggle(){
let selector = this.constructor.playCSSSelector;
this.e.querySelector(selector)
.dispatchEvent(this.mouseClickEvent)
return this
}
}
// I'm tasked with fetching (downloading) tracks
// and removing excessive ones which may pollute memory.
// 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,opt){
this.update(el);
this.current = this.nodes[0];
this.currentIndex = 0; // current checked node index of the tracks
this.totalTracksFetched = this.nodes.length;
// maximum nodes to load & to enumerate over
this.searchTreshhold = opt.searchTreshhold || 1024 * 2;
this.direction = 'wn' // searching direction: up or wn (down);
// 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;
this.subSelectorsToRemove =
Array.isArray(opt.subSelectorsToRemove) ? opt.subSelectorsToRemove : []
// Removing extr tracks
this.removedTracks= 0;
// this.extraTrackRemoveObject = new Observer(this.truncateToLimit.bind(this), 2000);
this.check = function(){
this._match();
if(this.found) {
this.onFound && this.onFound();
console.clear();
console.log('IT IS FOUND :) ',this.nodes.length);
console.log(this.current);
this.observers.forEach(o => o.cancel());
return
}
// if(this._hasNext()) return this._walk();
if(this._hasNext()) {
this.onRunning && this.onRunning(this);
this._walk();
} else
// FINALLY: NOTHING FOUND
if(this._isExceedingTreshhold()){
console.clear();
console.log('NOTHING FOUND :( )',this.nodes.length);
this.onBad && this.onBad(this);
// window.scroll(0,0);
this.observers.forEach(o => o.cancel());
return
} else {
this._fetch();
}
if(this._isExceedingLimit()) {
this.truncateToLimit();
}
}.bind(this);
this.fetchCb = function(){
if (!this.current) {
throw new Error(`Fetching failed: something went wrong, no current element is found, checkout the code, Alex!`);
} else
if (!this.current.nextElementSibling) {
this._fetch();
}
// this._fetch();
}.bind(this);
// fetching triggers soundcloud tracks dowload
this.fetching = new Observer({
frequency: 100*4,
fn: this.fetchCb
});
// 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({
frequency: 100*1.5,
fn: this.check
});
// MAIN LOOOP with observers
this.observers = [
this.checking
// , this.fetching
// , this.extraTrackRemoveObject
]
}
Tracks.prototype.truncateToLimit = function(){
let tracksToRemove = [];
tracksToRemove = [].slice.call(tracks.nodes, 0, this.tracksLimit);
tracksToRemove.forEach(e => {
if(this.onTrackRemoval) {
this.onTrackRemoval(e);
}
e.remove();
e = null;
});
this.removedTracks += tracksToRemove.length;
console.log(`Total tracks removed: %d`, this.removedTracks);
return tracksToRemove
}
// 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);
}
Tracks.prototype._isExceedingTreshhold= function(){
return this.totalTracksFetched > this.searchTreshhold;
}
Tracks.prototype._isExceedingLimit = function() {
let v = this.tracksLimit
&& this.nodes.length > this.tracksLimit
&& this.currentIndex > this.tracksLimit
return v
}
// 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(){
this.previous = this.current;
this.previous.style.display == 'none' && (this.current.style.display = '');
switch (this.direction){
case 'up': this.current = this.current.previousElementSibling; break;
case 'wn': this.current = this.current.nextElementSibling;
}
// Mark currently traversed element by red border
if(this.hide) {
this.previous.style.display = 'none';
} else {
this.previous.style.border = 'none'
this.previous.removeAttribute('style');
}
this.current.style.border = 'inset .6em #ff5500';
// Remove specified sub elements (saving memory);
if (this.subSelectorsToRemove.length !== 0) {
this.subSelectorsToRemove.forEach((sel) => {
nodeToRemove = this.current.querySelector(sel);
if (!(nodeToRemove === null || nodeToRemove === void 0)) {
nodeToRemove.remove();
}
});
}
this.totalTracksFetched++;
this.currentIndex = [].slice.call(this.nodes).indexOf(this.current);
}
// Entry method. That's where algorithm gets started
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, abortTransitionWhen }){
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;
this.abortTransitionWhen = abortTransitionWhen;
this.listener = function (e) {
const alreadyAssigned = this.nodes.has(e.target);
const containsStopWord = this.constructor.LINK_STOP_WORDS.test(e.target.title);
const haveExcludedClasses = this.constructor.LINK_STOP_CLASSES.some((className) => {
return e.target.classList.contains(className);
});
const isNotEligible =
alreadyAssigned
// || containsStopWord
|| haveExcludedClasses;
if (isNotEligible) { return }
if (this.isTransitionAllowed !== undefined
&& this.abortTransitionWhen()) {
return
}
e.stopPropagation();
let allowTransition = false;
if (this.popup.isOpen) { return }
allowTransition = this.popup.open(this.message.generate(e.currentTarget.href));
if (!allowTransition) {
e.preventDefault();
};
}.bind(this);
}
toggle() { this.enabled = !this.enabled }
/**
* Add listener to provided node
**/
add(node) {
node.addEventListener(`click`, this.listener);
return this.nodes.add(node);
}
/**
* Remove node from links set
**/
rem(node){
node.removeEventListener(`click`, this.listener);
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 }
// RESET & STOP BUTTON
var tracks;
// Previously path the /stream changed to /stream
var streamingPath = "/feed" // /stream - previously
var tracksClassName = '.lazyLoadingList__list.sc-list-nostyle.sc-clearfix';
var initTrackSearch = function(){
// Guard from accident relocation
document.querySelector('div.header__middle').style='display: none';
var panel = document.querySelector('.'+'header__right sc-clearfix'.split(' ').join('.'));
// elementType, id, style
var 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');
// 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 === streamingPath){
var tracklist = document.querySelector(tracksClassName);
if(tracks) {
tracks.update(tracklist);
console.log('Elements update interval cleared!');
} else {
contentElement = document.getElementById(`content`);
subSelectorsToRemove = [
`.commentForm`
, `.sound__waveform`
, `.sc-button-share`
, `.sound__artwork`
];
// Tracks iterator
tracks = new Tracks(tracklist, {
tracksLimit : 512
,nodeHide : false // Hide nodes
,subSelectorsToRemove
});
// waveforms = new Waveforms();
// waveforms.hide();
}
// INITIALIZING CLICK PAGE TRANSITION GUARD
var confirmationDialog = new ConfirmationPopup();
var transitionGuard = new PageTransitionGuard({
popup : confirmationDialog,
abortTransitionWhen: () => tracks.running
});
// Add links to the interceptor so every link clicked gets confirmation window shown
transitionGuard.addMany(document.querySelectorAll(`a`));
transitionGuard.addMany(document.querySelectorAll(`.soundTitle__tagContainer`));
let nodeToRemove;
// tracks.onFetch = interceptor.addMany.bind(interceptor);
// tracks.onTrackRemoval = interceptor.rem.bind(interceptor);
tracks.onFetch = function(nodes){
nodes.forEach((node) => {
transitionGuard.addMany(node.querySelectorAll(`a`));
transitionGuard.addMany(node.querySelectorAll(`.soundTitle__tagContainer`));
});
}
tracks.onTrackRemoval = function(node){
transitionGuard.rem(node);
}
// Specify whether to alert when traverser reaches boundary
// at which clicking on play songs would reload page
var alertOnReachingBoundary = true;
tracks.onRunning = function(){
stopButton.label('...' + tracks.totalTracksFetched);
stopButton.style('cursor: wait');
// play every 10th track
// if(!(tracks.totalTracksFetched % 10)) {
// try {
// var t = new Track(this.current);
// t.toggle();
// }
// catch (e) {
// console.log('Whooops. Play is failed!', e);
// }
// }
if (this.totalTracksFetched >= 1200 && alertOnReachingBoundary) {
alertOnReachingBoundary = false;
this.toggle();
alert(`Play song now and later use [ctrl + shift + <right array>] in order to go through songs!`);
}
}
tracks.onBad = function(tracks){
stopButton.label('BAD :('+tracks.totalTracksFetched);
}
tracks.onFound = function(){
stopButton.label(`SUCCESS ${this.totalTracksFetched}:`);
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.totalTracksFetched);
stopButton.style('');
}
textsearchselector = '.soundTitle__usernameTitleContainer'
;tracks.find(text,textsearchselector,'wn');
// ;tracks.toggle();
// If location not streamingPath then remove control UI elements
} else {
let button = buttons[0];
let out = []
for (let buttonIndx = 0; buttonIndx < buttons; buttonIndx++) {
button = buttons[buttonIndx]
if(document.getElementById(button.el.id)) {
// button.el.style = 'display: none'); // hide
button.el.remove() // remove element entirely
}
}
tracks && tracks.update(undefined);
tracks = null;
document.querySelector('div.header__middle').style='display:';
}
}
// ENTRY POINT
if (window.location.pathname !== streamingPath) {
const stream = `https://soundcloud.com/feed`;
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) };
}
// ex:expandtab
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment