// ex. http://www.mixcloud.com/superbreak/sunday-drift-04-superbreak/
trackrows = htmldoc.querySelectorAll('div.cloudcast-tracklist>div.track-row[ng-repeat="section in juno.sections"]');
} else {
// - tracklist NOT sourced from jd ex. http://www.mixcloud.com/acidpauli/weisse-baren-im-schwarzen-schaf/
//
// ex. http://www.mixcloud.com/acidpauli/weisse-baren-im-schwarzen-schaf/
trackrows = htmldoc.querySelectorAll('div.cloudcast-tracklist>div.track-row');
}
console.log(trackrows.length + ' tracks found');
// collect tracklist information into rls.tracklist
for (t = 0; t < trackrows.length; t += 1) {
trk = new Track();
trk.number = trackrows[t].querySelector('span.track-number').textContent.replace(/[.]/, '').trim();
trk.title = trackrows[t].querySelector('span.chapter-name, span.track-song-name-link, a.track-song-name-link').textContent.tidyline();
trk.artist = trackrows[t].querySelector('span.artist-name-link, a.artist-name-link').textContent.tidyline();
if (tracktimecodes.length === trackrows.length) {
// timecodes to all tracks available (native, not from junodownload case)
trk.time = (tracktimecodes[t] * 1000).millisecToString(); // hh:mm:ss
} else if (trackrows[t].querySelector('a[ng-href*="?timein="]') !== null ) {
// fall back to get if from the track node "?timein=" argument (dynamically generated tracklist use case, for all except for "Unknown" tracks)
// ex.: http://www.junodownload.com/charts/mixcloud/falentinvreigeist/kyodai-at-attitude-club-paristokyo-dec-2012-dj-set/30160370?timein=1557
trk.time = trackrows[t].querySelector('a[ng-href]').attributes['ng-href'].value.match(/timein=(\d+)/)[1];
trk.time = (Number(trk.time) * 1000).millisecToString();
}
// append to tracklist array
rls.tracklist.push(trk);
}
rls.tracks = rls.tracklist.length.toString();
// return Release object with collected information
rls.normalizeProfile(); // HEURISTICS on title, artist, (uploaded) by, label, catalog#
rls.normalizeTimecodes();
return rls;
};
// ======================================
// soundcloud.com release data collection
// ======================================
// last updated 7 december 2013
Release.prototype.get_soundcloud = function getRelease() {
// page to parse and new Release object to collect data in
var htmldoc = window.top.document, rlsInfo = htmldoc,
rls = new Release(), rlsDescription = new Section(),
i, trackrows, t, trk;
// RELEASE INFO BLOCK below image (optional) - usually found with label/artist track previews
rlsInfo = htmldoc.querySelectorAll('dt.listenInfo__releaseTitle, dd.listenInfo__releaseData');
for (i = 0; i < rlsInfo.length / 2; i += 1) {
if (rlsInfo[i * 2].textContent.match(/Released by/i) !== null) { rls.label = rlsInfo[i * 2 + 1].textContent.trim().toInitials(); }
if (rlsInfo[i * 2].textContent.match(/catalog/i) !== null) { rls.catalog = rlsInfo[i * 2 + 1].textContent.trim(); }
if (rlsInfo[i * 2].textContent.match(/date/i) !== null) { rls.released = rlsInfo[i * 2 + 1].textContent.trim().tidydate(); }
// no example found with other info fields so far...
}
// MAIN CONTENT HEADER
rlsInfo = htmldoc.getElementById('content');
// title - LONG dash(es) replaced by regular dash(es) if present
rls.title = rlsInfo.getElementsByClassName('soundTitle__title')[0].textContent.tidyline().replace(/\u2013/g, '-');
// uploader username
rls.by = rlsInfo.querySelector('a.soundTitle__username').textContent.tidyline();
// duration (track or set)
//TODO: find an alternative way to get the duration, now fails, soundcloud source changed and generated on the fly somehow it seems...
if (rlsInfo.querySelector('div.timeIndicator__total') !== null) {
//rls.duration = rlsInfo.querySelector('div.timeIndicator__total').textContent.trim().replace(/\./, ':');
} else {
rls.duration = "0:00";
}
try {
// format + download // free download & external download/buy link detection
if (rlsInfo.getElementsByClassName('listenContent')[0].querySelector('button.sc-button-download') !== null) {
rls.format = 'Free download [' + htmldoc.URL.tidyurl(true) + '/download]';
} else if (rlsInfo.querySelector('div.sc-button-group>a.soundActions__purchaseLink') !== null) {
rls.format = rlsInfo.querySelector('div.sc-button-group>a.soundActions__purchaseLink').title.trim() + ' [' + rlsInfo.querySelector('div.sc-button-group>a.soundActions__purchaseLink').href.tidyurl() + ']';
}
} catch(e) {}
// default label to soundcloud.com if not set
if (rls.label === '') { rls.label = 'soundcloud.com'; }
// source-specific release info - order matters for txt layout
// source url
rls.soundcloud = htmldoc.URL.tidyurl();
// date uploaded - set to .released date if empty
rls.uploaded = rlsInfo.querySelector('time.relativeTime').title.replace(/ \d\d\:\d\d$/, '').replace(/^Posted on /i, '').trim();
if (rls.released === '') {
rls.released = rls.uploaded;
rls.uploaded = '';
}
// description (optional)
var descriptionDiv;
if (rlsInfo.querySelector('div.listenDetails__description') !== null) {
rlsDescription.title = 'Description';
if (rlsInfo.querySelector('a.truncatedUserText__toggleLink') !== null) {
// expandable text => get the long version - we MUST expand to get the text with format
if (rlsInfo.querySelector('a.truncatedUserText__toggleLink').textContent === 'Read full description') {
rlsInfo.querySelector('a.truncatedUserText__toggleLink').click();
}
descriptionDiv = rlsInfo.querySelector('div.userText__expanded');
} else {
// standard text
descriptionDiv = rlsInfo.querySelector('div.listenDetails__description');
}
// unfuck links in description text - side effect: FIXES THE HTML SOURCE PAGE TOO.
descriptionDiv.expandLinks();
rlsDescription.content = descriptionDiv.innerText.trim();
if (rlsDescription.content !== '') { rls.description.push(rlsDescription); }
}
// tags (optional)
rlsInfo = rlsInfo.querySelectorAll('div.sc-tag-group>a');
rls.tags = '';
for (i = 0; i < rlsInfo.length; i += 1) {
rls.tags += ((rls.tags === '') ? '' : ', ') + rlsInfo[i].textContent.trim();
}
// SINGLE TRACK MIXES/LIVES TRACKLIST
// TODO: detect tracklist in description and feed it to tracklist[]
// SOUNDCLOUD SET => GET TRACKS TOTAL & TRACKLIST
// note: for more than artist, title and title url, each title info would need to be loaded/queried for duration, comment...
if (htmldoc.URL.split('?')[0].match(/\/sets\//) !== null) {
// Set duration
if (htmldoc.querySelectorAll('h3.trackListTitle>Strong') !== null) {
rls.duration = htmldoc.querySelectorAll('h3.trackListTitle>Strong')[1].textContent.trim().replace(/\./, ':');
}
// tracks details
trackrows = htmldoc.querySelectorAll('div.soundBadge__content');
for (t = 0; t < trackrows.length; t += 1) {
trk = new Track();
trk.title = trackrows[t].querySelector('a.soundTitle__title').textContent.tidyline().replace(/\u2013/g, '-');
// extract track number from title, if present
if (trk.title.match(/^\d+[ \.\-]+/) !== null) {
trk.number = trk.title.match(/^\d+/)[0];
trk.title = trk.title.replace(/^\d+[ \.\-]+/, '');
} else {
trk.number = (t + 1).toString();
}
// normalize title CAPS
if (trk.title.toUpperCase() === trk.title || trk.title.toLowerCase() === trk.title) { trk.title = trk.title.toInitials(); }
// .title='artist - title' => .artist & .title
if (trk.title.split(' - ').length > 1) {
trk.artist = trk.title.split(' - ')[0];
trk.title = trk.title.split(' - ')[1];
}
// track URL - unused for now
trk.url = trackrows[t].querySelector('a.soundTitle__title').href.tidyurl();
// append to tracklist array
rls.tracklist.push(trk);
}
rls.tracks = t.toString();
}
// HEURISTICS on title, artist, (uploaded) by, label, catalog#
rls.normalizeProfile();
// return Release object with collected information
return rls;
};
// ==================================================================================================================
// SITE-SPECIFIC SUPPORT FUNCTIONS
// this section needs amending to add support to other discographic release pages with identification of source
// and call to the appropriate getRelease_[source]() data collection function
// ==================================================================================================================
function releaseTXT_DetectNavChange_soundcloud() {
// document URL changed by soundcloud script => automatically trigger release text box re-set according to new page/track/set
// div id=content first child
is tagged by releaseTXT_main() with current URL & sound title on first SC page visit
// TODO? integrate back into releaseTXT_main()
var htmldoc = window.top.document,
pageURL = (htmldoc.querySelector('div#content>div').attributes['nav-url'] === undefined) ? '' : htmldoc.querySelector('div#content>div').attributes['nav-url'].value;
if (pageURL !== htmldoc.URL) {
console.log('url change: "' + pageURL + '" => "' + htmldoc.URL);
// handing over to releaseTXT_main => we don't want to trigger url change detection on the current page's every content div change event anymore
htmldoc.querySelector('div#content').removeEventListener('DOMNodeRemoved', releaseTXT_DetectNavChange_soundcloud, false);
setTimeout(releaseTXT_main('url-change'), 500); // let's give page div content some time to change unattended
}
}
// ==================================================================================================================
// SITE-SPECIFIC MAIN FUNCTIONS
// this section needs amending to add support to other discographic release pages with identification of source
// and call to the appropriate getRelease_[source]() data collection function
// ==================================================================================================================
function releaseTXT_main(mode) {
// main function called by the 'release:txt' button.
var htmldoc = window.top.document,
txtbox = htmldoc.getElementById('releaseTXT_txtbox'),
releaseTXT = 'page loading...',
pageTitle = '',
rls = new Release();
// set UI text box to 'loading...' state
txtbox.style.backgroundColor = '#FFD700'; // light red-orange wait state
txtbox.value = releaseTXT;
// collect release data text version from current page
switch (htmldoc.domain.parentDomain()) {
case 'bandcamp.com':
releaseTXT = rls.get_bandcamp().TXT();
break;
case 'beatport.com':
// UI is included in all pages because beatport implements dynamic content replacement navigation.
// TODO: implement dynamic url change detection (user just needs to press "release:txt" to refresh the txt until then)
if (htmldoc.URL.tidyurl().match(/^(www|mixes)\.beatport\.com\/(charts|mix|release)\//i) === null) {
console.log('fill text box: not a release @ ' + htmldoc.URL);
pageTitle = 'none';
releaseTXT = 'not a release';
txtbox.style.backgroundColor = '#d5d5d5'; // light grey
} else {
releaseTXT = rls.get_beatport().TXT();
}
break;
case 'discogs.com':
releaseTXT = rls.get_discogs().TXT();
break;
case 'junodownload.com':
releaseTXT = rls.get_junodownload().TXT();
break;
case 'mixcloud.com':
// UI is included in all pages because mixcloud 2014 switched to dynamic content replacement navigation.
// TODO: implement dynamic url change detection (user just needs to press "release:txt" to refresh the txt until then)
if (htmldoc.URL.tidyurl().match(/^www\.mixcloud\.com$/i) !== null ||
htmldoc.URL.tidyurl().match(/^www\.mixcloud\.com\/[\w\d\-]+\/$/i) !== null ||
htmldoc.URL.tidyurl().match(/^www\.mixcloud\.com\/[\w\d\-]+\/(favorites|followers|following|listens|playlists|uploads)\//i) !== null ||
htmldoc.URL.tidyurl().match(/^www\.mixcloud\.com\/(ads|artist|categories|competitions|dashboard|developers|groups|jobs|media|myaccount|partners|player|projects|tag|terms|track|tracklist|upload)\//i) !== null) {
console.log('fill text box: not a cloudcast @ ' + htmldoc.URL);
pageTitle = 'none';
releaseTXT = 'not a cloudcast';
txtbox.style.backgroundColor = '#d5d5d5'; // light grey
} else {
// If tracklist parent node
has a "ng-init" attribute,
// the
tracklist container is populated dynamically after the initial page load
// => we run this script again until required tracklist info has been loaded.
if (htmldoc.querySelector('div[ng-controller=CloudcastHeaderCtrl]').attributes['ng-init'] !== undefined) {
if (htmldoc.querySelectorAll('div#fb-root>div').length === 0 && htmldoc.querySelectorAll('div.cloudcast-tracklist>div.track-row').length === 0) {
// first run => set trigger to run releaseTXT_main() again when
tag gets updated with content
htmldoc.querySelector('div#fb-root').addEventListener('DOMNodeInserted', releaseTXT_main, false);
console.log('cloudcast with dynamically loaded tracklist: detected');
releaseTXT = "loading tracklist...";
} else if (htmldoc.querySelectorAll('div#fb-root>div').length === 1) {
// interim re-run, no change to Event listeners
console.log('cloudcast with dynamically loaded tracklist: loading');
releaseTXT = "loading tracklist...";
} else if (htmldoc.querySelectorAll('div#fb-root>div').length === 2) {
//
is populated with all 2 child
tags, we can expect tracklist to be fully loaded.
// remove event listener from
tag
htmldoc.querySelector('#fb-root').removeEventListener('DOMNodeInserted', releaseTXT_main, false);
// security: listen in case tracklist unexpectedly expands anyway after that
htmldoc.querySelector('div[ng-controller=CloudcastHeaderCtrl]').addEventListener('DOMNodeInserted', releaseTXT_main, false);
console.log('cloudcast with dynamically loaded tracklist: complete (' + htmldoc.querySelectorAll('div#fb-root>div').length.toString() + 'x div#fb-root>div => ok, remove event listener)');
// go ahead acquiring cloudcast profile/tracklist
releaseTXT = rls.get_mixcloud().TXT();
}
} else {
// all needed info is up already => go ahead acquiring cloudcast profile/tracklist
console.log('cloudcast with initial tracklist: go ahead');
releaseTXT = rls.get_mixcloud().TXT();
}
}
break;
case 'soundcloud.com':
// UI is included in all pages because of sc's new design dynamic content replacement navigation.
if (htmldoc.URL.tidyurl().match(/^soundcloud\.com\/[\w\d\-]+\/sets\/|^soundcloud\.com\/[\w\d\-]+\/[\w\d\-]+/i) !== null &&
htmldoc.URL.tidyurl().match(/^soundcloud\.com\/[\w\d\-]+\/(apps|comments|favorites|following|followers|groups|likes|stats|tracks)[\/]?/i) === null &&
htmldoc.URL.tidyurl().match(/^soundcloud\.com\/(101|apps|creativecommons|creators|explore|groups|jobs|messages|people|pages|premium|search|settings|sounds|stream|tags|tour|tracks|upload|you)\//i) === null) {
if (htmldoc.querySelector('div#content>div') === null) {
setTimeout(releaseTXT_main, 500); // execute this _main() again until the page has the required title & html data
} else if (htmldoc.querySelector('span.soundTitle__title') === null) {
setTimeout(releaseTXT_main, 500); // execute this _main() again until the page has the required title & html data
} else if (htmldoc.querySelector('div#content>div').attributes['nav-title'] !== undefined) {
if (mode === 'url-change' && htmldoc.querySelector('span.soundTitle__title').textContent === htmldoc.querySelector('div#content>div').attributes['nav-title'].value) {
setTimeout(releaseTXT_main, 500); // execute this _main() again until the page has the required title & html data
} else {
console.log('fill text box: track or set @ ' + htmldoc.URL);
pageTitle = htmldoc.querySelector('span.soundTitle__title').textContent;
releaseTXT = rls.get_soundcloud().TXT();
}
} else {
console.log('fill text box: track or set @ ' + htmldoc.URL);
pageTitle = htmldoc.querySelector('span.soundTitle__title').textContent;
releaseTXT = rls.get_soundcloud().TXT();
}
} else {
// not a track or set target page
if (htmldoc.querySelector('div#content>div') === null) {
console.log('waiting for content div: ' + htmldoc.URL);
setTimeout(releaseTXT_main, 500); // execute this _main() again until the page has the required title & html data
} else {
console.log('fill text box: not a release @ ' + htmldoc.URL);
pageTitle = 'none';
releaseTXT = 'not a track or set';
txtbox.style.backgroundColor = '#d7d7d7'; // light grey
}
}
if (pageTitle !== '') {
// set custom attribute flags & EventListener used in dynamic url change detection/handling
htmldoc.querySelector('div#content>div').setAttribute('nav-url', htmldoc.URL);
htmldoc.querySelector('div#content>div').setAttribute('nav-title', pageTitle); // TODO? do we need to filter/escape some pageTitle characters ?
htmldoc.querySelector('div#content').addEventListener('DOMNodeRemoved', releaseTXT_DetectNavChange_soundcloud, false);
}
break;
default:
if (htmldoc.querySelector('head>meta[content*=".bandcamp.com/"]') !== null) {
// bandcamp.com rebranded domain page has in
// note: we get here only if user added rebranded domain to this script's Settings>User includes (CH/TM)
releaseTXT = rls.get_bandcamp().TXT();
} else {
// else, we're not supposed to be here...
releaseTXT = 'ERROR, unexpected source page domain: ' + htmldoc.domain.replace(/^(www|\w+)\./, '');
}
}
// fill text box with formatted release information text
txtbox.value = releaseTXT;
if (releaseTXT.match(/^(page loading\.\.\.|not a release|not a track or set|not a cloudcast)$/i) === null) {
txtbox.style.backgroundColor = '#FFFFFF';
}
// TODO: add lightweight error management in case getReleaseData_...() fails
//txtbox.value = 'Could not collect the data for this release !! Click the + button for more...\n\n' +
// 'Please report faulty URL below to userscripts.org/scripts/discuss/156420 :\n\n' + htmldoc.URL + '\n\n' +
// 'Your help improving this script is appreciated.' ;
}
// ==================================================================================================================
// INITIALIZE
// ==================================================================================================================
function releaseTXT_init() {
// insert UI into the site's source page
var htmldoc = window.top.document;
switch (htmldoc.domain.parentDomain()) {
case 'bandcamp.com':
releaseTXT_buildUI();
break;
case 'beatport.com':
releaseTXT_buildUI('margin-top: 69px; '); // move UI to below the page's menu+player overlay
break;
case 'discogs.com':
releaseTXT_buildUI('background-color: #d7d7d7; ');
break;
case 'junodownload.com':
releaseTXT_buildUI('background-color: #252525; ');
break;
case 'mixcloud.com':
releaseTXT_buildUI('background-color: #25292b; ');
break;
case 'soundcloud.com':
// TODO: detect pages of old (classic) design and skip UI insert & _main() call + remove related @excludes...
releaseTXT_buildUI('background-color: #333; ');
break;
default:
releaseTXT_buildUI();
}
// get release text for current page
releaseTXT_main('init');
}
/* FIREFOX/GREASEMONKEY: script wrapper to prevent init() execution for each embedded Ad frame
added security check on title, as ads frames typically have a but no title */
if (window.top === window.self && window.self.document.title !== '') {
releaseTXT_init();
}