// ==UserScript==
//
// @name Last.fm Enhanced Song Page
// @description Injects external song information to the last.fm track page
// @namespace lastfmenhancedsongpage
//
// @include http://www.last.fm/*
// @include http://www.lastfm.*/*
//
// @require https://bitbucket.org/saaj/lastfm-enhanced-song-page/raw/default/build/base.js
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js
//
// @downloadURL https://bitbucket.org/saaj/lastfm-enhanced-song-page/raw/default/build/98307.user.js
//
// @noframes
//
// @grant GM_xmlhttpRequest
// @grant GM_getResourceURL
//
// @resource progress http://cdn.last.fm/tageditor/progress_active.gif
//
// @author saaj <mail@saaj.me>
// @website https://bitbucket.org/saaj/lastfm-enhanced-song-page
// @license GPL-2.0+
// @version 0.12.0
//
// ==/UserScript==
/**
* Formats self in modern Python manner
*/
String.prototype.format = function()
{
var value = this;
for(var i = 0; i < arguments.length; i++)
{
value = value.split('{' + i + '}').join(String(arguments[i]));
}
return value;
};
/**
* Computes a Levenshtein distance of self and passed argument,
* based on qx.util.EditDistance
*/
String.prototype.distance = function(value)
{
var a = this;
var b = value;
// distance is table with a.length + 1 rows and b.length + 1 columns
var distance = [];
// posA and posB are used to iterate over a and b
var posA, posB, cost;
for(posA = 0; posA <= a.length; posA++)
{
distance[posA] = [];
distance[posA][0] = posA;
}
for(posB = 1; posB <= b.length; posB++)
{
distance[0][posB] = posB;
}
for(posA = 1; posA <= a.length; posA++)
{
for(posB = 1; posB <= b.length; posB++)
{
cost = a[posA - 1] === b[posB - 1] ? 0 : 1;
distance[posA][posB] = Math.min(
distance[posA - 1][posB ] + 1, // deletion
distance[posA ][posB - 1] + 1, // insertion
distance[posA - 1][posB - 1] + cost // substitution
);
}
}
return distance[posA - 1][posB - 1];
};
function namespace(ns)
{
var root = window;
ns.split('.').forEach(function(part)
{
if(typeof root[part] == 'undefined')
{
root[part] = {};
}
root = root[part];
});
return root;
}
namespace('lesp');
/**
* Event target. Sort of conceptual mix of qx.core.Object and SplObserver/SplSubject.
*/
lesp.Target = Base.extend({
_listeners : null,
constructor : function(supportedEvents)
{
this._listeners = {};
supportedEvents.forEach(function(name)
{
this._listeners[name] = [];
}, this);
},
attach : function(event, listener)
{
this._listeners[event].push(listener);
},
detach : function(event, listener)
{
var i = this._listeners[event].indexOf(listener);
if(i != -1)
{
this._listeners[event].splice(i, 1);
}
},
notify : function(event, data)
{
this._listeners[event].forEach(function(listener)
{
listener(this, data);
}, this);
}
});
lesp.mangleHtml = function(value)
{
// the trick allows jquery traverse html correctly
return value.replace(/<(\/?)(html|head|body|img)/ig, '<$1$2z');
};
namespace('lesp.abstract');
/**
* Abstract song.
*/
lesp.abstract.Song = lesp.Target.extend({
_baseUrl : null,
_document : null,
_pageUrl : null,
_exact : null,
_name : null,
constructor : function(match)
{
this.base(['loaded', 'notLoaded']);
this._pageUrl = this._baseUrl + match.href
.replace(this._baseUrl, '')
.replace(this._baseUrl.split(':', 2)[1], '');
this._exact = match.dist == 0;
this._name = match.name;
GM_xmlhttpRequest({
'method' : 'get',
'url' : this._pageUrl,
'onload' : this._onLoad.bind(this),
'onerror' : this.notify.bind(this, 'notLoaded')
});
},
isExact : function()
{
return this._exact;
},
getName : function()
{
return this._name;
},
_onLoad : function(response)
{
this._document = jQuery(lesp.mangleHtml(response.responseText));
this.notify('loaded');
},
getPageUrl : function()
{
return this._pageUrl;
}
});
/**
* Base manager.
*/
lesp.abstract.Manager = lesp.Target.extend({
_track : null,
_searchUrlTemplate : null,
_songUrlQuery : null,
constructor : function(artist, track)
{
this.base(['found', 'failed', 'loaded']);
this._track = track;
GM_xmlhttpRequest({
'method' : 'get',
'url' : this._searchUrlTemplate.format(encodeURIComponent(artist)),
'onload' : this._onLoad.bind(this),
'onerror' : this.notify.bind(this, 'failed')
});
},
_onLoad : function(response)
{
// be overriden
},
getTrackCandidates : function(trackAnchorCollection)
{
var trackName = this._track.toLowerCase();
return trackAnchorCollection
.map(function()
{
var item = jQuery(this);
var distance = item.text().toLowerCase().distance(trackName);
// jQuery filters nulls and concats returned arrays
return distance < 8
? {
'dist' : distance,
'href' : item.attr('href'),
'name' : item.text()
}
: null;
})
.get()
.sort(function(a, b)
{
return a.dist - b.dist;
});
},
getSongUrlQuery : function()
{
return this._songUrlQuery;
}
});
/**
* Abstract song renderer.
*/
lesp.abstract.Renderer = lesp.Target.extend({
_manager : null,
_song : null,
_title : null,
constructor : function(manager)
{
this.base(['completed', 'failed']);
manager.attach('loaded', (function(target, song)
{
this._song = song;
this.notify('completed');
}).bind(this));
manager.attach('failed', this.notify.bind(this, 'failed'));
},
getTitle : function()
{
return this._title;
},
_createMatchNotice : function()
{
if(this._song.isExact())
{
return '';
}
return [
'<p style="color: #bbb; padding-top: 1em; text-align: center;">',
'No exact match is found. Best match is {0}.'.format(this._song.getName()),
'</p>'
].join('');
},
render : function()
{
// be overriden
}
});
namespace('lesp.songfacts');
/**
* Songfacts' song page. Retrives facts and comments.
*/
lesp.songfacts.Song = lesp.abstract.Song.extend({
_baseUrl : 'http://www.songfacts.com',
getFacts : function()
{
if(!this._document)
{
throw new Error('Song document is not yet ready');
}
return this._document.find('ul.factsullist-sf li div.inner').map(function()
{
var fact = jQuery(this);
fact.find('.trigger-credits').remove();
fact.find('.creditsdiv .wrapper b').remove();
var credit = fact.find('.creditsdiv .wrapper').text().replace(':', '').trim();
fact.find('.creditsdiv').remove();
var factHtml = fact.html().trim();
return '<p>{0}</p><p style="color: #696969; margin: 0 0 1em 0;">{1}</p>'.format(factHtml, credit);
}).get();
},
getComments : function()
{
if(!this._document)
{
throw new Error('Song document is not yet ready');
}
return this._document.find('div.sfdetail-comments .comment-holder').map(function()
{
return jQuery(this).html();
}).get();
}
});
/**
* Songfacts' manager. Searches for a song of given artist.
*/
lesp.songfacts.Manager = lesp.abstract.Manager.extend({
_searchUrlTemplate : 'http://www.songfacts.com/search-artist-1.php?{0}',
_baseUrl : 'http://www.songfacts.com',
_onLoad : function(response)
{
var contents = jQuery(lesp.mangleHtml(response.responseText));
var artist = contents.find('.search-holder ul.songullist-blue li a:eq(0)');
if(artist.length)
{
var url = artist.attr('href');
if(url.indexOf(this._baseUrl) != 0)
{
url = this._baseUrl + url;
}
GM_xmlhttpRequest({
'method' : 'get',
'url' : url,
'onload' : this._onArtistSongsLoad.bind(this),
'onerror' : this.notify.bind(this, 'failed')
});
}
else
{
this.notify('failed');
}
},
_onArtistSongsLoad : function(response)
{
if(response.finalUrl.indexOf(this._searchUrlTemplate.format('')) == 0)
{
this.notify('failed');
}
else
{
var contents = jQuery(lesp.mangleHtml(response.responseText));
var candidates = this.getTrackCandidates(contents.find('ul.songullist-orange li a'));
if(candidates.length)
{
this._songUrlQuery = candidates[0].href;
var song = new lesp.songfacts.Song(candidates[0]);
song.attach('loaded', this.notify.bind(this, 'loaded', song));
song.attach('notLoaded', this.notify.bind(this, 'failed'));
this.notify('found');
}
else
{
this.notify('failed');
}
}
}
});
/**
* Songfacts' song renderer. Creates ready DOM node of facts and comment.
*/
lesp.songfacts.Renderer = lesp.abstract.Renderer.extend({
_title : 'SongFacts',
_createFacts : function()
{
var facts = this._song.getFacts().map(function(fact)
{
return '<li>{0}</li>'.format(fact);
}).join('');
return facts
? [
'<ul style="margin: 1em 2em 1em 0;">',
facts,
'</ul>'
].join('')
: '';
},
_createMenu : function()
{
var comments = this._song.getComments().length;
var pageUrl = this._song.getPageUrl();
var result = jQuery('<div style="text-align: right; padding: 5px 30px 10px 0;"/>')
.append(comments
? '<a href="#songfacts-commnets" id="songfacts-commnets" class="show-comments">' +
'Show comments</a>'
: '')
.append(comments ? '<span style="padding: 10px;">•</span>' : '')
.append(('<a href="{0}" class="external-link" target="_blank">' +
'SongFacts page</a>').format(pageUrl));
result.find('a.show-comments').click(function()
{
var anchor = jQuery(this);
anchor.parent()
.next().toggle()
.next().toggle();
anchor.text(anchor.data('shown') ? 'Show comments' : 'Hide comments');
anchor.data('shown', !anchor.data('shown'));
});
return result;
},
_createComments : function()
{
var comments = this._song.getComments().map(function(comment)
{
comment = jQuery('<div>{0}</div>'.format(comment))
.find('span:eq(1)').css('color', '#696969')
.prepend('<br/>').parent()
.append('<hr/>').parent()
.html();
return '<li>{0}</li>'.format(comment);
}).join('');
return comments
? [
'<ul style="display:none; overflow:auto; height:450px; ',
'margin-right: 30px; padding-right: 10px;">',
comments,
'</ul>'
].join('')
: '';
},
render : function()
{
return jQuery('<div/>')
.append(this._createMatchNotice())
.append(this._createFacts())
.append(this._createMenu())
.append(this._createComments());
}
});
namespace('lesp.songmeanings');
/**
* SongMeanings' manager. Searches for a song of given artist.
*/
lesp.songmeanings.Manager = lesp.abstract.Manager.extend({
_searchUrlTemplate : 'http://songmeanings.com/query/?q={0}&type=artists',
_baseUrl : 'http://songmeanings.com',
_onLoad : function(response)
{
if(response.finalUrl.indexOf(this._baseUrl + '/artist') == 0)
{
// exact match redirect
this._onArtistSongsLoad(response);
}
else if(response.finalUrl.indexOf('https://www.google') == 0)
{
// google search fallback
var contents = jQuery(lesp.mangleHtml(response.responseText));
var search = '{0}/artist'.format(this._baseUrl.replace('http://', ''));
var links = contents.find('cite').get().filter((function(element)
{
return jQuery(element).text().indexOf(search) == 0;
}).bind(this));
if(links.length)
{
GM_xmlhttpRequest({
'method' : 'get',
'url' : 'http://' + jQuery(links[0]).text(),
'onload' : this._onArtistSongsLoad.bind(this),
'onerror' : this.notify.bind(this, 'failed')
});
}
else
{
this.notify('failed');
}
}
else
{
var contents = jQuery(lesp.mangleHtml(response.responseText));
var artist = contents.find('#content-big tr.item td a:eq(0)');
if(artist.length)
{
var url = this._baseUrl + artist.attr('href')
.replace(this._baseUrl, '') // if href was already absolute w/ schema
.replace(this._baseUrl.split(':', 2)[1], ''); // if href was already abolsute w/o schema
GM_xmlhttpRequest({
'method' : 'get',
'url' : url,
'onload' : this._onArtistSongsLoad.bind(this),
'onerror' : this.notify.bind(this, 'failed')
});
}
else
{
this.notify('failed');
}
}
},
_onArtistSongsLoad : function(response)
{
var contents = jQuery(lesp.mangleHtml(response.responseText));
var candidates = this.getTrackCandidates(contents.find('#songslist td:not(.comments) a'));
if(candidates.length)
{
this._songUrlQuery = this._baseUrl + candidates[0].href
.replace(this._baseUrl, '') // if href was already absolute w/ schema
.replace(this._baseUrl.split(':', 2)[1], ''); // if href was already abolsute w/o schema
var song = new lesp.songmeanings.Song(candidates[0]);
song.attach('loaded', this.notify.bind(this, 'loaded', song));
song.attach('notLoaded', this.notify.bind(this, 'failed'));
this.notify('found');
}
else
{
this.notify('failed');
}
}
});
/**
* SongMeanings' song page. Retrives lyrics.
*/
lesp.songmeanings.Song = lesp.abstract.Song.extend({
_baseUrl : 'http://songmeanings.com',
_commentCache : null,
_commentDocument : null,
getLyrics : function()
{
if(!this._document)
{
throw new Error('Song document is not yet ready');
}
var result = this._document.find('div.lyric-box');
result.find('div').remove(); // button container
var text = result.text().replace(/�/g, "'");
return text;
},
_onLoad : function(response)
{
this._document = jQuery(lesp.mangleHtml(response.responseText));
GM_xmlhttpRequest({
'method' : 'post',
'url' : this._pageUrl,
'data' : 'command=loadComments',
'headers' : {
'X-Requested-With' : 'XMLHttpRequest',
'Content-Type' : 'application/x-www-form-urlencoded'
},
'onload' : (function(response)
{
this._commentDocument = jQuery('<div>{0}</div>'.format(response.responseText));
this.notify('loaded');
}).bind(this),
'onerror' : this.notify.bind(this, 'notLoaded')
});
},
getCommentCount : function()
{
var match = /\d+/.exec(this._document.find('#header-comments-counter').text());
return match ? Number(match[0]) : 0;
},
getFirstComments : function()
{
if(!this._commentDocument)
{
throw new Error('Comment document is not yet ready');
}
if(this._commentCache)
{
return this._commentCache;
}
return this._commentCache = this._commentDocument.find('li div.text').map(function()
{
var comment = jQuery(this);
var user = comment.find('.sign a.author:eq(0)').text();
var date = comment.find('.sign em.date:eq(0)').text();
var text = comment.contents().map(function()
{
if(this.nodeName == '#text')
{
return jQuery.trim(this.wholeText);
}
else if(this.nodeName == 'BR')
{
return '<br/>';
}
}).get().join('');
return {
'user' : user,
'date' : date,
'text' : text
};
}).get();
}
});
/**
* SongMeanings' song renderer. Creates ready DOM node for lyrics.
*/
lesp.songmeanings.Renderer = lesp.abstract.Renderer.extend({
_title : 'SongMeanings',
_createLyrics : function()
{
return jQuery('<div/>')
.append(
jQuery('<div style="margin-top: 15px" class="lyrics-snippet">')
.html(this._song.getLyrics()
.replace(/^[\s]+/g, '<br/>')
.replace(/[\s]+$/g, '<br/>')
.replace(/\n/g, '<br/>')
)
)
.html(); // doesn't include outer div
},
_createMenu : function()
{
var result = jQuery('<div/>').css({
'text-align' : 'right',
'padding' : '5px 30px 10px 0'
});
var commentsTotal = this._song.getCommentCount();
var commentPostfix = '';
if(commentsTotal)
{
var commentsLoaded = this._song.getFirstComments().length;
if(commentsTotal > commentsLoaded)
{
commentPostfix = ' ({0}/{1})'.format(commentsLoaded, commentsTotal);
}
result.append('<a href="#songmeanings-commnets" class="show-comments" ' +
'id="songmeanings-commnets">Show comments{0}</a>'.format(commentPostfix));
result.append('<span style="padding: 10px;">•</span>');
}
result.append(('<a href="{0}" class="external-link" target="_blank">' +
'SongMeanings page</a>').format(this._song.getPageUrl()));
result.find('a.show-comments').click(function()
{
var anchor = jQuery(this);
anchor.parent()
.next().toggle()
.next().toggle();
anchor.text((anchor.data('shown') ? 'Show comments' : 'Hide comments') + commentPostfix);
anchor.data('shown', !anchor.data('shown'));
});
return result;
},
_createComments : function()
{
var comments = this._song.getFirstComments().map(function(comment)
{
var template = [
'<li>',
'<div>{0}</div>',
'<span style="color: #696969">- {1} {2}</span>',
'<hr/>',
'</li>'
].join('');
return template.format(comment.text, comment.user, comment.date);
}).join('');
return comments
? [
'<ul style="display:none; overflow:auto; height:450px; ',
'margin-right: 30px; padding-right: 10px;">',
comments,
'</ul>'
].join('')
: '';
},
render : function()
{
return jQuery('<div/>')
.append(this._createMatchNotice())
.append(this._createLyrics())
.append(this._createMenu())
.append(this._createComments());
}
});
namespace('lesp.application');
/**
* Main interface renderer.
*/
lesp.application.Renderer = Base.extend({
add : function(renderer)
{
var progress = document.createElement('img');
progress.id = new Date().valueOf();
progress.src = GM_getResourceURL('progress');
progress.style.padding = '15px';
var container = jQuery('<section/>').css({
'width' : '800px',
'float' : 'left'
})
.append('<h2>{0}</h2>'.format(renderer.getTitle()))
.append(progress)
.appendTo('.wiki-section');
renderer.attach('completed', function()
{
container.find('#' + progress.id).remove();
container.append(renderer.render());
});
renderer.attach('failed', function()
{
container.find('#' + progress.id).remove();
container.append('<p>Not found</p>');
});
}
});
/**
* Determines whether the page is a track page.
*/
lesp.application.TrackPage = Base.extend({
pathname : null,
_positiveRe : /^\/music\/([^\/]+)\/([^\/]+)\/([^\/]+)$/,
_negativeRe : /^\/music.*\/\+/,
constructor : function(pathname)
{
this.pathname = pathname;
},
isTrackPage : function()
{
return !this._negativeRe.test(this.pathname) && this._positiveRe.test(this.pathname);
},
parseTrackPage : function()
{
if(!this.isTrackPage())
{
throw Error('Pathname is not of a track page');
}
var match = this._positiveRe.exec(this.pathname);
match = match.map(function(item)
{
return decodeURIComponent(item.replace(/\+/g, '%20'));
});
return {
'artist' : match[1],
'album' : match[2],
'track' : match[3]
};
}
});
/**
* Main manager.
*/
lesp.application.Application = Base.extend({
_isFirstPage : true,
_pageToRender : null,
_mainRenderer : null,
_pageChangeObserver : null,
_renderTrackPage : function(page)
{
var artist = page['artist'];
var track = page['track'];
var providerRendererList = [
new lesp.songfacts.Renderer(new lesp.songfacts.Manager(artist, track)),
new lesp.songmeanings.Renderer(new lesp.songmeanings.Manager(artist, track))
];
providerRendererList.forEach(this._mainRenderer.add, this._mainRenderer);
if('ga' in unsafeWindow)
{
unsafeWindow.ga('lesp.send', 'pageview');
}
},
_onRenderTargetPage : function()
{
if(this._pageToRender)
{
this._renderTrackPage(this._pageToRender);
this._pageToRender = null;
}
},
_onChangeLocation : function()
{
var page = new lesp.application.TrackPage(unsafeWindow.location.pathname);
if(!page.isTrackPage())
{
return;
}
var track = page.parseTrackPage();
// First page is just synchronous HTML
if(this._isFirstPage)
{
this._isFirstPage = false;
this._renderTrackPage(track);
}
else
{
this._pageToRender = track;
}
},
_setupUrlPolling : function()
{
var previousUrl = null;
setInterval(function()
{
if(previousUrl != unsafeWindow.location.href)
{
previousUrl = unsafeWindow.location.href;
this._onChangeLocation();
}
}.bind(this), 250);
},
main : function()
{
this._mainRenderer = new lesp.application.Renderer();
this._pageChangeObserver = new MutationObserver(this._onRenderTargetPage.bind(this));
this._pageChangeObserver.observe(document.querySelector('#content'), {'childList': true});
this._setupUrlPolling();
// purpose is to debug include and exclude regexes, look at usage
if('ga' in unsafeWindow)
{
unsafeWindow.ga('create', 'UA-29858354-3', 'auto', 'lesp');
}
}
});
new lesp.application.Application().main();
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.