Last active
February 20, 2017 13:03
-
-
Save SavageCore/2cf95795f6f26bc85ba3f41d04dd86d1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name RED Album Chronology | |
// @namespace redacted.ch | |
// @description Provides links to the next/previous albums in an artist's timeline | |
// @version 0.1.1 | |
// @include http*://*redacted.ch/torrents.php*id=* | |
// @include http*://*redacted.ch/artist.php* | |
// @grant GM_addStyle | |
// @grant GM_registerMenuCommand | |
// ==/UserScript== | |
function doArtistPage() { | |
var id = window.location.search.match(/[?&]id=(\d+)/); | |
if (id) id = pref.setReferrer(id[1]); | |
else return; | |
// Update cache | |
var albums = {}; | |
for (var type in pref.types) { | |
if (pref.types.hasOwnProperty(type)) { | |
var arr = []; | |
var links = dom.qsa('.releases_' + type + ' .group_info > strong > a:last-of-type'); | |
for (var i = 0, il = links.length; i < il; i++) { | |
var groupId = +links[i].href.split('id=')[1]; | |
if (groupId) { | |
arr.push({ | |
i: groupId, | |
n: shn.name(links[i].textContent), | |
y: shn.year(parseInt(links[i].parentNode.firstChild.textContent, 10)) | |
}); | |
} | |
} | |
if (arr.length) albums[type] = arr; | |
} | |
} | |
cache.updateArtist(id, albums); | |
} | |
function doAlbumPage() { | |
var type, artists = [], sel = 0; | |
var artBox, chronBox, lists; | |
function onLinkClick(e) { | |
if (e.target.id == 'gmac_next') { | |
e.preventDefault(); | |
artists[sel].link.classList.remove('gmac_sel'); | |
sel = (sel + 1) % artists.length; | |
artists[sel].link.classList.add('gmac_sel'); | |
updateLists(); | |
if (artists[sel].list == lists.empty) { | |
makeList(artists[sel]); | |
} | |
} else if (e.target.nodeName == 'A') { // album link | |
pref.setReferrer(artists[sel].id); | |
} | |
} | |
function onMouseMove(e) { | |
artBox.classList[e.type == 'mouseover' ? 'add' : 'remove']('gmac_hl' + pref.settings.style); | |
} | |
function updateLists() { | |
var lists = chronBox.children; | |
for (var i = lists.length; --i; ) { // skip first | |
lists[i].classList.add('hidden'); | |
} | |
artists[sel].list.classList.remove('hidden'); | |
} | |
function mkEmptyList(text, show) { | |
return dom.mk('ul', {className: 'stats nobullet' + (show ? '' : ' hidden')}, | |
dom.mk('li', null, text)); | |
} | |
artBox = dom.cl('box_artists')[0] || dom.mk('div'); | |
var artistLinks = dom.qsa('.artist_main > a, .artists_conductors > a', artBox); | |
var re = new RegExp('releases_(' + Object.keys(pref.types).join('|') + ')\\b'); | |
var row = dom.cl('edition')[0]; | |
var typeMatch = row && row.className.match(re); | |
if (typeMatch && artistLinks.length) type = typeMatch[1]; | |
else return; | |
lists = { | |
empty: mkEmptyList('\u00a0', true), | |
loading: mkEmptyList('Loading...'), | |
failed: mkEmptyList('Not Found') | |
}; | |
chronBox = dom.mk('div', {id: 'gmac_chronology', className: 'box'}, | |
dom.mk('div', {className: 'head'}, | |
dom.mk('strong', null, pref.types[type] + ' Chronology'), | |
dom.mk('a', {id: 'gmac_next', className: 'brackets hidden', | |
href: '#', title: 'Next artist'}, 'Next')), | |
lists.empty, | |
lists.loading, | |
lists.failed); | |
var sidebar = artBox.parentNode; | |
var box = dom.qsa('.box:not(#votes_ranks)', sidebar)[pref.settings.place]; | |
sidebar.insertBefore(chronBox, box && box.nextElementSibling); | |
chronBox.addEventListener('click', onLinkClick, false); | |
GM_addStyle([ | |
'#gmac_next { float: right; }', | |
'#gmac_chronology { padding: 0 !important; text-align: left; }', | |
'#gmac_chronology > .head { margin: 0; }', | |
'#gmac_chronology, #gmac_chronology > .head { width: auto; }', | |
'#gmac_chronology > ul { padding: 5px 10px; margin: 0; list-style: none outside none; }', | |
'#gmac_chronology li { margin: 0; padding: 3px 0; }', | |
'#gmac_chronology li > span { font-weight: bold; opacity: 0.95; }', | |
'#gmac_chronology > ul { word-wrap: break-word; overflow-wrap: break-word; }' | |
].join('')); | |
for (var i = 0, il = artistLinks.length; i < il; i++) { | |
artists.push({ | |
id: artistLinks[i].href.split('id=')[1], | |
link: artistLinks[i], | |
list: lists.empty | |
}); | |
if (artists[i].id == pref.settings.referrer) sel = i; | |
} | |
if (artists.length > 1) { | |
GM_addStyle([ | |
'.gmac_hl0 .gmac_sel { text-decoration: underline !important; }', | |
'.gmac_hl1 .gmac_sel { color: #000 !important; text-shadow: -1px -1px 1px #D3D3D3,', | |
'-1px 1px 1px #D3D3D3, 1px -1px 1px #D3D3D3, 1px 1px 1px #D3D3D3, 0 0 8px #FFF; }', | |
'.gmac_hl2 .gmac_sel { color: #FFF !important; text-shadow: -1px -1px 1px #808080,', | |
'-1px 1px 1px #808080, 1px -1px 1px #808080, 1px 1px 1px #808080, 0 0 8px black; }', | |
'.gmac_hl3 .gmac_sel { color: #1DE0FE !important; text-shadow: -1px -1px 2px #FE97DC,', | |
'-1px 1px 2px #FE97DC, 1px -1px 2px #FE97DC, 1px 1px 2px #FE97DC, 0 0 10px #FE97DC; }' | |
].join('')); | |
artists[sel].link.classList.add('gmac_sel'); | |
dom.id('gmac_next').classList.remove('hidden'); | |
chronBox.addEventListener('mouseover', onMouseMove, false); | |
chronBox.addEventListener('mouseout', onMouseMove, false); | |
} | |
pref.applyContext = function () { | |
makeList(artists[sel]); | |
}; | |
var makeList = function () { | |
function currentAlbumIndex(albums) { | |
for (var i = albums.length; i--; ) { | |
if (albums[i].i == groupId) return i; | |
} | |
return -1; // not found, cache entry is outdated | |
} | |
var groupId = +(window.location.search.match(/[?&]id=(\d+)/) || [])[1]; | |
return function (artist) { | |
if (artist.id == '83') return; | |
var alreadyUpdated = artist.list != lists.empty; | |
if (cache.has[artist.id]) { | |
var list = dom.mk('ul', {className: 'stats nobullet'}); | |
var albums = cache.has[artist.id][type] || []; | |
var al = albums.length; | |
var curr = currentAlbumIndex(albums); | |
var c = pref.settings.context + 1; | |
var i = Math.max(Math.min(curr + c, al) - 2*c, -1); | |
var j = Math.min(i + 2*c, al); | |
while (++i < j) { | |
var tagName = 'a'; | |
var attrs = {title: [pref.types[type], al - i, 'of', al].join(' '), dir: 'ltr'}; | |
if (i == curr) tagName = 'span'; | |
else attrs.href = 'torrents.php?id=' + albums[i].i; | |
dom.app(list, dom.mk('li', null, | |
dom.mk(tagName, attrs, albums[i].n), ' (' + shn.year(albums[i].y) + ')')); | |
} | |
if (list.children.length) { | |
artist.list = list; | |
dom.app(chronBox, list); | |
} else { | |
artist.list = lists.failed; | |
} | |
// Update cache entry if older than 4 hours OR current album not in cache | |
// (unless we just updated, to avoid a loop if the album was removed from site) | |
var age = shn.time() - cache.has[artist.id].t; | |
if (age > 4 || curr == -1 && !alreadyUpdated) { | |
if (artist.list == lists.failed) artist.list = lists.loading; | |
loadDiscog(artist); | |
} | |
} else { // cache miss | |
if (alreadyUpdated) { | |
artist.list = lists.failed; | |
} else { | |
artist.list = lists.loading; | |
loadDiscog(artist); | |
} | |
} | |
updateLists(); | |
}; | |
}(); | |
var loadDiscog = function () { | |
function request(artist) { | |
var xhr = new XMLHttpRequest(); | |
xhr.artist = artist; | |
xhr.onload = onLoad; | |
xhr.onerror = onError; | |
xhr.open('GET', 'ajax.php?action=artist&id=' + artist.id, true); | |
xhr.send(null); | |
} | |
function onError() { | |
if (this.artist.list == lists.loading) { | |
this.artist.list = lists.failed; | |
updateLists(); | |
} | |
} | |
var onLoad = function () { | |
function artistInGroup(artist, group) { | |
var extArt = group.extendedArtists || []; | |
// if artist is a main artist (1) or conductor (5) in the group: | |
for (var imp = 1; imp < 6; imp += 4) { | |
if (extArt[imp]) { | |
for (var i = extArt[imp].length; i--; ) { | |
if (extArt[imp][i].id == +artist.id) return true; | |
} | |
} | |
} | |
return false; | |
} | |
function decode(str) { | |
decoder.innerHTML = str; | |
return decoder.textContent; | |
} | |
var decoder = dom.mk('div'); | |
return function () { | |
var groups, albums = {}, prevId = 0; | |
try { | |
groups = JSON.parse(this.responseText).response.torrentgroup; | |
} catch (e) { | |
onError.call(this); | |
return; | |
} | |
for (var i = 0, il = groups.length; i < il; i++) { | |
var type = groups[i].releaseType; | |
if (type in pref.types && artistInGroup(this.artist, groups[i]) && | |
prevId != groups[i].groupId) { | |
prevId = groups[i].groupId; | |
if (!albums[type]) albums[type] = []; | |
albums[type].push({ | |
i: groups[i].groupId, | |
n: shn.name(decode(groups[i].groupName)), | |
y: shn.year(groups[i].groupYear) | |
}); | |
} | |
} | |
cache.updateArtist(this.artist.id, albums); | |
makeList(this.artist); | |
}; | |
}(); | |
// Slow down the requests if the user rapidly clicks "Next" | |
var delay = artists.length < 6 ? 1000 : 2000; | |
var timeForNext = 0; | |
return function (artist) { | |
var wait = timeForNext - Date.now(); | |
if (wait > 0) { | |
timeForNext += delay; | |
setTimeout(function () { request(artist); }, wait); | |
} else { | |
timeForNext = Date.now() + delay; | |
request(artist); | |
} | |
}; | |
}(); | |
makeList(artists[sel]); | |
} // doAlbumPage | |
var stor = { | |
get: function (key, def) { | |
var val = window.localStorage && window.localStorage.getItem(key); | |
return val ? JSON.parse(val) : typeof def != 'undefined' ? def : null; | |
}, | |
set: function (key, val) { | |
try { | |
if (window.localStorage) window.localStorage.setItem(key, JSON.stringify(val)); | |
} catch (e) { return -1; } // quota exceeded | |
}, | |
del: function (key) { | |
if (window.localStorage) window.localStorage.removeItem(key); | |
} | |
}; | |
var dom = { | |
id: function (id) { return document.getElementById(id); }, | |
qs: function (s, p) { return (p || document).querySelector(s); }, | |
qsa: function (s, p) { return (p || document).querySelectorAll(s); }, | |
cl: function (cl, p) { return (p || document).getElementsByClassName(cl); }, | |
tag: function (tag, p) { return (p || document).getElementsByTagName(tag); }, | |
txt: function (txt) { return document.createTextNode(txt); }, | |
app: function (parent, var_args) { | |
for (var i = 1, il = arguments.length; i < il; ++i) { | |
var child = arguments[i]; | |
if (typeof child == 'string') child = this.txt(child); | |
parent.appendChild(child); | |
} | |
}, | |
mk: function (tag, attr, var_args) { | |
var elem = document.createElement(tag); | |
if (attr) for (var a in attr) if (attr.hasOwnProperty(a)) elem[a] = attr[a]; | |
if (arguments.length > 2) { | |
var args = Array.prototype.slice.call(arguments, 2); | |
args.unshift(elem); | |
this.app.apply(this, args); | |
} | |
return elem; | |
} | |
}; | |
var cache = { | |
has: stor.get('gmac_cache', {}), | |
size: function () { return JSON.stringify(this.has).length; }, | |
maxSize: 999999, | |
updateArtist: function (artistId, albums) { | |
if (Object.keys(albums).length) { | |
albums.t = shn.time(); | |
this.has[artistId] = albums; | |
} else { | |
if (this.has[artistId]) delete this.has[artistId]; | |
else return; | |
} | |
if (this.size() > this.maxSize) this.purge(50); | |
this.save(); | |
}, | |
save: function () { | |
var num = 50; | |
while (num && stor.set('gmac_cache', this.has) == -1) { | |
num = num < 800 ? num * 2 : 0; | |
this.purge(num, true); | |
} | |
}, | |
purge: function (num, keepInMem) { | |
if (num) { // delete the num least recently visited artists: | |
var ids = Object.keys(this.has); | |
ids.sort(function (a, b) { | |
return cache.has[a].t - cache.has[b].t; | |
}); | |
var ok = false; | |
for (var i = Math.min(num, ids.length); i--; ) { | |
// always keep new ones (so it will work even if saving fails): | |
if (ok || shn.time() - this.has[ids[i]].t > 0) { | |
ok = true; // no need to keep checking since they're sorted by time | |
delete this.has[ids[i]]; | |
} | |
} | |
} else { // clear cache: | |
if (!keepInMem) this.has = {}; | |
stor.del('gmac_cache'); | |
} | |
} | |
}; // cache | |
var shn = { | |
time: function () { | |
return Math.floor(Date.now() / 3600000) - 395760; // hours since 2015-02-24 | |
}, | |
year: function (year) { return 2015 - year; }, | |
name: function (name) { | |
var max = 70; | |
return name.length <= max ? name : name.slice(0, max - 3) + '...'; | |
} | |
}; | |
var pref = { | |
scriptVersion: '1.1.1', | |
types: { 1: 'Album', 3: 'Soundtrack', 5: 'EP', 6: 'Anthology', 9: 'Single', 11: 'Live Album', 14: 'Bootleg', 15: 'Interview', 16: 'Mixtape', 21: 'Unknown', 22: 'Concert Recording', 23: 'Demo', }, | |
settings: { context: 2, place: 1, style: 0, referrer: '', version: '' }, | |
setReferrer: function (ref) { | |
this.settings.referrer = ref; | |
this.save(); | |
return ref; | |
}, | |
save: function () { | |
stor.set('gmac_settings', this.settings); | |
}, | |
restore: function () { | |
var set = stor.get('gmac_settings', this.settings); | |
if (this.scriptVersion == set.version) { | |
this.settings = set; | |
} else { | |
cache.purge(); | |
this.settings.version = this.scriptVersion; | |
this.save(); | |
} | |
}, | |
prompt: function () { | |
if (!dom.id('gmac_set_box')) pref.init(); | |
dom.id('gmac_set_con').value = pref.settings.context; | |
dom.id('gmac_set_pos').selectedIndex = pref.settings.place; | |
dom.id('gmac_set_hl').selectedIndex = pref.settings.style; | |
setTimeout(function () { dom.id('gmac_set_con').select(); }, 10); | |
pref.show(true); | |
}, | |
show: function (show) { | |
var toggle = show ? 'remove' : 'add'; | |
dom.id('gmac_set_bg').classList[toggle]('hidden'); | |
dom.id('gmac_set_box').classList[toggle]('hidden'); | |
}, | |
applyContext: function () {}, | |
applyChanges: function () { | |
var changes = 0; | |
var con = parseInt(dom.id('gmac_set_con').value, 10); | |
if (con != this.settings.context && con > 0 && con < 100) { | |
this.settings.context = con; | |
changes++; | |
this.applyContext(); | |
} | |
var pos = dom.id('gmac_set_pos').selectedIndex; | |
if (pos != this.settings.place) { | |
var chronBox = dom.id('gmac_chronology'); | |
if (chronBox) { | |
var sidebar = chronBox.parentNode; | |
var box = dom.qsa('.box:not(#votes_ranks):not(#gmac_chronology)', sidebar)[pos]; | |
sidebar.insertBefore(chronBox, box && box.nextElementSibling); | |
} | |
this.settings.place = pos; | |
changes++; | |
} | |
var hl = dom.id('gmac_set_hl').selectedIndex; | |
if (hl != this.settings.style) { | |
this.settings.style = hl; | |
changes++; | |
} | |
if (changes) this.save(); | |
}, | |
init: function () { | |
function makeSelRow(name, options, label) { | |
var selId = 'gmac_set_' + name; | |
var newSel = dom.mk('select', {id: selId}); | |
for (var i = 0, il = options.length; i < il; i++) { | |
dom.app(newSel, dom.mk('option', null, options[i])); | |
} | |
return dom.mk('tr', null, dom.mk('td', null, newSel), | |
dom.mk('td', null, dom.mk('label', {htmlFor: selId}, label))); | |
} | |
dom.app(document.body, | |
dom.mk('div', {id: 'gmac_set_box'}, | |
dom.mk('div', null, 'redacted.ch Album Chronology'), | |
dom.mk('div', null, 'User Settings'), | |
dom.mk('table', null, dom.mk('tbody', null, | |
dom.mk('tr', null, | |
dom.mk('td', null, | |
dom.mk('input', {id: 'gmac_set_con', type: 'text', size: 2, | |
maxLength: 2, autocomplete: 'off'})), | |
dom.mk('td', null, | |
dom.mk('label', {htmlFor: 'gmac_set_con'}, | |
'Include this many items on either side of the current album'))), | |
makeSelRow('pos', ['Cover', 'Artists', 'Add artist', 'Album Votes', 'Tags', 'Add tag'], | |
'Place the chronology after this section in the sidebar'), | |
makeSelRow('hl', ['Underline', 'Black', 'White', 'Neon'], | |
'Use this style for artist highlighting'))), | |
dom.mk('div', {id: 'gmac_set_buttons'}, | |
dom.mk('button', {type: 'button'}, 'OK'), | |
dom.mk('button', {type: 'button'}, 'Cancel'))), | |
dom.mk('div', {id: 'gmac_set_bg'})); | |
GM_addStyle([ | |
'#gmac_set_bg {', | |
'background-color: #000; opacity: 0.4; width: 100%; height: 100%;', | |
'position: fixed; left: 0; top: 0; z-index: 9000; }', | |
'#gmac_set_box {', | |
'color: #000; background-color: #F0F0F0; text-shadow: none; position: fixed;', | |
'width: 460px; top: 25%; left: 0; right: 0; margin: 0 auto; padding: 15px;', | |
'border: 4px double #777; border-radius: 12px; z-index: 9001;', | |
'font: 8pt Tahoma, Helvetica, sans-serif; box-shadow: 7px 7px 10px #333; }', | |
'#gmac_set_box > div { font-family: inherit; margin-left: 30px; }', | |
'#gmac_set_box > div:first-child {', | |
'font-weight: bold; font-size: 13pt; padding: 10px 0; }', | |
'#gmac_set_box > div + div { font-size: 11pt; padding: 0 0 20px; }', | |
'#gmac_set_box > table, #gmac_set_box tr, #gmac_set_box td {', | |
'border: none; background-color: #F0F0F0; margin: 0;', | |
'-moz-box-shadow: none; -webkit-box-shadow: none; }', | |
'#gmac_set_box td { padding: 5px; vertical-align: baseline; }', | |
'#gmac_set_box td:first-child, #gmac_set_con { text-align: right; }', | |
'#gmac_set_box input, #gmac_set_box select, #gmac_set_box option {', | |
'font-size: 8pt; background: #FFF; color: #000; }', | |
'#gmac_set_box input, #gmac_set_box select {', | |
'padding: 2px; margin: 0; width: auto; border: none !important;', | |
'outline: 1px inset #FFF !important; box-shadow: none; }', | |
'#gmac_set_box option { padding: 0; margin: 1px 2px; }', | |
'#gmac_set_buttons { float: right; margin-top: 25px; }', | |
'#gmac_set_buttons > button {', | |
'width: 78px; height: 28px; margin-left: 10px;', | |
'font-size: 8pt; text-shadow: none; text-transform: none; }' | |
].join('')); | |
dom.id('gmac_set_bg').addEventListener('click', this.handle.bgClick, false); | |
dom.id('gmac_set_buttons').addEventListener('click', this.handle.buttonClick, false); | |
dom.id('gmac_set_box').addEventListener('keydown', this.handle.boxKeydown, false); | |
dom.id('gmac_set_con').addEventListener('input', this.handle.conInput, false); | |
}, | |
handle: { | |
bgClick: function () { pref.show(false); }, | |
buttonClick: function (e) { | |
if (e.target.nodeName == 'BUTTON') { | |
if (e.target.nextSibling) pref.applyChanges(); // 'OK' | |
pref.show(false); | |
} | |
}, | |
boxKeydown: function (e) { | |
switch (e.keyCode) { | |
case 13: // Enter | |
if (e.target.nodeName == 'BUTTON') break; | |
pref.applyChanges(); | |
case 27: // Esc | |
pref.show(false); | |
break; | |
case 38: // Up arrow | |
case 40: // Down arrow | |
var inputElem = dom.id('gmac_set_con'); | |
if (e.target == inputElem) { | |
var up = e.keyCode == 38; | |
var val = parseInt(inputElem.value, 10); | |
val = val > 0 ? val : 0; // negative or NaN => 0 | |
if (up && val < 99) val++; | |
else if (!up && val > 1) val--; | |
else break; | |
inputElem.value = val; | |
} | |
} | |
}, | |
conInput: function () { | |
var notNum = /\D/g; | |
return function (e) { | |
e.target.value = e.target.value.replace(notNum, ''); | |
}; | |
}() | |
} | |
}; // pref | |
pref.restore(); | |
if (window.location.pathname == '/artist.php') doArtistPage(); | |
else doAlbumPage(); | |
if (this.GM_registerMenuCommand) { | |
GM_registerMenuCommand('redacted.ch Album Chronology: Settings', pref.prompt); | |
} else { | |
var elem = dom.qs('#gmac_chronology strong'); | |
if (elem) elem.addEventListener('click', pref.prompt, false); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment