Skip to content

Instantly share code, notes, and snippets.

@SavageCore
Last active February 20, 2017 13:03
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 SavageCore/2cf95795f6f26bc85ba3f41d04dd86d1 to your computer and use it in GitHub Desktop.
Save SavageCore/2cf95795f6f26bc85ba3f41d04dd86d1 to your computer and use it in GitHub Desktop.
// ==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