NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Collage Extensions for Gazelle Music Trackers // @version 1.24.3 // @description Direct browsing from torrent pages; quick groups removal, custom quick Add To Collage form // @author Anakunda // @license GPL-3.0-or-later // @copyright 2020, Anakunda (https://openuserjs.org/users/Anakunda) // @namespace https://greasyfork.org/users/321857-anakunda // @match https://*/torrents.php?id=* // @match https://*/collages.php?*id=* // @match https://*/artist.php?*id=* // @grant GM_getValue // @grant GM_slinketValue // @grant GM_registerMenuCommand // @require https://openuserjs.org/src/libs/Anakunda/async-mutex.min.js // @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.js // ==/UserScript== 'use strict'; var auth = document.querySelector('input[name="auth"][value]'); if (auth != null) auth = auth.value; else { auth = document.querySelector('li#nav_logout > a'); if (auth != null && /\b(?:auth)=(\w+)\b/.test(auth.search)) auth = RegExp.$1; else throw 'Auth not found'; } let userId = document.querySelector('li#nav_userinfo > a.username'); if (userId != null) { userId = new URLSearchParams(userId.search); userId = parseInt(userId.get('id')); } function addToTorrentCollage(collageId, torrentGroupId) { if (!collageId) return Promise.reject('collage id not defined'); if (!torrentGroupId) return Promise.reject('torrent group id not defined'); return (apiKey ? queryAjaxAPI('addtocollage', { id: collageId }, { groupids: torrentGroupId }).then(function(response) { if (!response.groupsadded.includes(torrentGroupId)) return Promise.reject('Error: ' + JSON.stringify(response)); }) : queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then( collage => !collage.torrentGroupIDList.map(parseInt).includes(torrentGroupId) ? collageId : Promise.reject('already in collage') ).then(collageId => new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest, formData = new URLSearchParams({ action: 'add_torrent', collageid: collageId, groupid: torrentGroupId, url: document.location.origin.concat('/torrents.php?id=', torrentGroupId), auth: auth, }); xhr.open('POST', '/collages.php', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onreadystatechange = function() { if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; if (xhr.status >= 200 && xhr.status < 400) resolve(collageId); else reject(defaultErrorHandler(xhr)); xhr.abort(); }; xhr.onerror = function() { reject(defaultErrorHandler(xhr)) }; xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) }; xhr.send(formData); }))).then(collageId => queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then( collage => collage.torrentGroupIDList.map(groupId => parseInt(groupId)).includes(torrentGroupId) ? collage : Promise.reject('Error: not added for unknown reason') )); } function removeFromTorrentCollage(collageId, torrentGroupId, question) { if (!confirm(question)) return Promise.reject('Cancelled'); return new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest, formData = new URLSearchParams({ action: 'manage_handle', collageid: collageId, groupid: torrentGroupId, auth: auth, submit: 'Remove', }); xhr.open('POST', '/collages.php', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onreadystatechange = function() { if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr)); xhr.abort(); }; xhr.onerror = function() { reject(defaultErrorHandler(xhr)) }; xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) }; xhr.send(formData); }); } function addToArtistCollage(collageId, artistId) { if (!collageId) return Promise.reject('collage id not defined'); if (!artistId) return Promise.reject('artist id not defined'); return (/*apiKey ? queryAjaxAPI('addtocollage', { id: collageId }, { groupids: artistId }).then(function(response) { if (!response.groupsadded.includes(artistId)) return Promise.reject('Error: ' + JSON.stringify(response)); }) : */queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then( collage => !collage.artists.map(artist => parseInt(artist.id)).includes(artistId) ? collageId : Promise.reject('already in collage') ).then(collageId => new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest, formData = new URLSearchParams({ action: 'add_artist', collageid: collageId, artistid: artistId, url: document.location.origin.concat('/artist.php?id=', artistId), auth: auth, }); xhr.open('POST', '/collages.php', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onreadystatechange = function() { if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; if (xhr.status >= 200 && xhr.status < 400) resolve(collageId); else reject(defaultErrorHandler(xhr)); xhr.abort(); }; xhr.onerror = function() { reject(defaultErrorHandler(xhr)) }; xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) }; xhr.send(formData); }))).then(collageId => queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then( collage => collage.artists.map(artist => parseInt(artist.id)).includes(artistId) ? collage : Promise.reject('Error: not added for unknown reason') )); } function removeFromArtistCollage(collageId, artistId, question) { if (!confirm(question)) return Promise.reject('Cancelled'); return new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest, formData = new URLSearchParams({ action: 'manage_artists_handle', collageid: collageId, artistid: artistId, auth: auth, submit: 'Remove', }); xhr.open('POST', '/collages.php', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onreadystatechange = function() { if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr)); xhr.abort(); }; xhr.onerror = function() { reject(defaultErrorHandler(xhr)) }; xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) }; xhr.send(formData); }); } function addQuickAddForm() { if (!userId || !torrentGroupId && !artistId) return; // User id missing let ref = document.querySelector('div.sidebar'); if (ref == null) return; // Sidebar missing const addSuccess = 'Successfully added to collage.'; const alreadyInCollage = 'Error: This ' + (torrentGroupId ? 'torrent group' : artistId ? 'artist' : null) + ' is already in this collage'; new Promise(function(resolve, reject) { try { var categories = JSON.parse(GM_getValue(document.location.hostname + '-categories')); if (categories.length > 0) resolve(categories); else throw 'empty list cached'; } catch(e) { let xhr = new XMLHttpRequest; xhr.open('GET', '/collages.php', true); xhr.responseType = 'document'; xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 400) { categories = [ ]; xhr.response.querySelectorAll('tr#categories > td > label').forEach(function(label, index) { let input = xhr.response.querySelector('tr#categories > td > input#' + label.htmlFor); categories[input != null && /\[(\d+)\]/.test(input.name) ? parseInt(RegExp.$1) : index] = label.textContent.trim(); }); if (categories.length > 0) { GM_setValue(document.location.hostname + '-categories', JSON.stringify(categories)); resolve(categories); } else reject('Site categories could not be extracted'); } else reject(defaultErrorHandler(xhr)); }; xhr.onerror = function() { reject(defaultErrorHandler(xhr)) }; xhr.ontimeout = function() { reject(defaultTimeoutHandler()) }; xhr.send(); } }).then(function(categories) { const artistsIndexes = categories .map((category, index) => /^(?:Artists)$/i.test(category) ? index : -1) .filter(index => index >= 0); if (artistId && artistsIndexes.length <= 0) throw 'Artists index not found'; const isCompatibleCategory = categoryId => categoryId >= 0 && categoryId < categories.length && (torrentGroupId && !artistsIndexes.includes(categoryId) || artistId && artistsIndexes.includes(categoryId)); document.head.appendChild(document.createElement('style')).innerHTML = ` form#addtocollage optgroup { background-color: slategray; color: white; } form#addtocollage option { background-color: white; color: black; max-width: 290pt; } div.box_addtocollage > form { padding: 0px 10px; } `; let elem = document.createElement('div'); elem.className = 'box box_addtocollage'; elem.style = 'padding: 0 0 10px;'; elem.innerHTML = ` <div class="head" style="margin-bottom: 5px;"><strong>Add to Collage</strong></div> <div id="ajax_message" class="hidden center" style="padding: 7px 0px;"></div> <form id="searchcollages"> <input id="searchforcollage" placeholder="Collage search" type="text" style="max-width: 10em;"> <input id="searchforcollagebutton" value="Search" type="submit" style="max-width: 4em;"> </form> <form id="addtocollage" class="add_form" name="addtocollage"> <select name="collageid" id="matchedcollages" class="add_to_collage_select" style="width: 96%;"> <input id="opencollage-btn" value="Open collage" type="button"> <input id="addtocollage-btn" value="Add to collage" type="button"> </form> `; ref.append(elem); let ajaxMessage = document.getElementById('ajax_message'); let srchForm = document.getElementById('searchcollages'); if (srchForm == null) throw new Error('#searchcollages missing'); let searchText = document.getElementById('searchforcollage'); if (searchText == null) throw new Error('#searchforcollage missing'); let dropDown = document.getElementById('matchedcollages'); if (dropDown == null) throw new Error('#matchedcollages missing'); let doOpen = document.getElementById('opencollage-btn'); let doAdd = document.getElementById('addtocollage-btn'); if (doAdd == null) throw new Error('#addtocollage-btn missing'); let searchforcollagebutton = document.getElementById('searchforcollagebutton'); if (searchforcollagebutton != null) searchforcollagebutton.disabled = searchText.value.length <= 0; srchForm.onsubmit = searchSubmit; searchText.ondrop = evt => dataHandler(evt.currentTarget, evt.dataTransfer); searchText.onpaste = evt => dataHandler(evt.currentTarget, evt.clipboardData); searchText.oninput(function(evt) { if (searchforcollagebutton != null) searchforcollagebutton.disabled = evt.currentTarget.value.length <= 0; }); if (doOpen != null) doOpen.onclick = openCollage; doAdd.onclick = addToCollage; let initTimeCap = GM_getValue('max_preload_time', 0); // max time in ms to preload the dropdown if (initTimeCap > 0) findCollages({ userid: userId, contrib: 1 }, initTimeCap); function clearList() { while (dropDown.childElementCount > 0) dropDown.removeChild(dropDown.firstElementChild); } function findCollages(query, maxSearchTime) { return typeof query == 'object' ? new Promise(function(resolve, reject) { let start = Date.now(); searchFormEnable(false); clearList(); elem = document.createElement('option'); elem.text = 'Searching...'; dropDown.add(elem); dropDown.selectedIndex = 0; let retryCount = 0, options = [ ]; searchInternal(); function searchInternal(page) { if (maxSearchTime > 0 && Date.now() - start > maxSearchTime) { reject('Time limit exceeded'); return; } let xhr = new XMLHttpRequest, _query = new URLSearchParams(query); if (!page) page = 1; _query.set('page', page); xhr.open('GET', '/collages.php?' + _query, true); xhr.responseType = 'document'; xhr.onload = function() { if (xhr.status < 200 || xhr.status >= 400) throw defaultErrorHandler(xhr); xhr.response.querySelectorAll('table.collage_table > tbody > tr[class^="row"]').forEach(function(tr, rowNdx) { if ((ref = tr.querySelector(':scope > td:nth-of-type(1) > a')) == null) { console.warn('Page parsing error'); return; } elem = document.createElement('option'); if ((elem.category = categories.findIndex(category => category.toLowerCase() == ref.textContent.toLowerCase())) < 0 && /\b(?:cats)\[(\d+)\]/i.test(ref.search)) elem.category = parseInt(RegExp.$1); // unsafe due to site bug if ((ref = tr.querySelector(':scope > td:nth-of-type(2) > a')) == null || !/\b(?:id)=(\d+)\b/i.test(ref.search)) { console.warn(`Unknown collage id (${xhr.responseURL}/${rowNdx})`); return; } elem.value = elem.collageId = parseInt(RegExp.$1); elem.text = elem.title = ref.textContent.trim(); if ((ref = tr.querySelector(':scope > td:nth-of-type(3)')) != null) elem.size = parseInt(ref.textContent); if ((ref = tr.querySelector(':scope > td:nth-of-type(4)')) != null) elem.subscribers = parseInt(ref.textContent); if ((ref = tr.querySelector(':scope > td:nth-of-type(6) > a')) != null && /\b(?:id)=(\d+)\b/i.test(ref.search)) elem.author = parseInt(RegExp.$1); if (isCompatibleCategory(elem.category) && (elem.category != 0 || elem.author == userId)) options.push(elem); }); if (xhr.response.querySelector('div.linkbox > a.pager_next') != null) searchInternal(page + 1); else { if (!Object.keys(query).includes('order')) options.sort((a, b) => (b.size || 0) - (a.size || 0)/*a.title.localeCompare(b.title)*/); resolve(options); } }; xhr.onerror = function() { if (xhr.status == 0 && retryCount++ <= 10) setTimeout(function() { searchInternal(page) }, 200); else reject(defaultErrorHandler(xhr)); }; xhr.ontimeout = function() { reject(defaultTimeoutHandler()) }; xhr.send(); } }).then(function(options) { clearList(); categories.forEach(function(category, ndx) { let _category = options.filter(option => option.category == ndx); if (_category.length <= 0) return; elem = document.createElement('optgroup'); elem.label = category; elem.append(..._category); dropDown.add(elem); }); dropDown.selectedIndex = 0; searchFormEnable(true); return options; }).catch(function(reason) { clearList(); searchFormEnable(true); console.warn(reason); }) : Promise.reject('Invalid parameter'); } function searchFormEnable(enabled) { for (let i = 0; i < srchForm.length; ++i) srchForm[i].disabled = !enabled; } function searchSubmit(evt) { let searchTerm = searchText.value.trim(); if (searchTerm.length <= 0) return false; let query = { action: 'search', search: searchTerm, type: 'c.name', order: 'Updated', sort: 'desc', order_way: 'Descending', }; categories.map((category, index) => 'cats[' + index + ']') .filter((category, index) => isCompatibleCategory(index)) .forEach(index => { query[index] = 1 }); findCollages(query); return false; } function addToCollage(evt) { (function() { evt.currentTarget.disabled = true; if (ajaxMessage != null) ajaxMessage.classList.add('hidden'); let collageId = parseInt(dropDown.value); if (!collageId) return Promise.reject('No collage selected'); /* if (Array.from(document.querySelectorAll('table.collage_table > tbody > tr:not([class="colhead"]) > td > a')) .map(node => /\b(?:id)=(\d+)\b/i.test(node.search) && parseInt(RegExp.$1)).includes(collageId)) return Promise.reject(alreadyInCollage); */ if (torrentGroupId) return addToTorrentCollage(collageId, torrentGroupId); if (artistId) return addToArtistCollage(collageId, artistId); return Promise.reject('munknown page class'); })().then(function(collage) { if (ajaxMessage != null) { ajaxMessage.innerHTML = '<span style="color: #0A0;">' + addSuccess + '</span>'; ajaxMessage.classList.remove('hidden'); } evt.currentTarget.disabled = false; let mainColumn = document.querySelector('div.main_column'); if (mainColumn == null) return collage; let tableName = collage.collageCategoryID != 0 ? 'collages' : 'personal_collages' let tbody = mainColumn.querySelector('table#' + tableName + ' > tbody'); if (tbody == null) { tbody = document.createElement('tbody'); tbody.innerHTML = '<tr class="colhead"><td width="85%"><a href="#">↑</a> </td><td># torrents</td></tr>'; elem = document.createElement('table'); elem.id = tableName; elem.className = 'collage_table'; elem.append(tbody); mainColumn.insertBefore(elem, [ 'table#personal_collages', 'table#vote_matches', 'div.torrent_description', 'div#similar_artist_map', 'div#artist_information', ].reduce((acc, selector) => acc || document.querySelector(selector), null)); } tableName = '\xA0This ' + (artistsIndexes.includes(collage.collageCategoryID) ? 'artist' : 'album') + ' is in ' + tbody.childElementCount + ' ' + (collage.collageCategoryID != 0 ? 'collage' : 'personal collage'); if (tbody.childElementCount > 1) tableName += 's'; tbody.firstElementChild.firstElementChild.childNodes[1].data = tableName; elem = document.createElement('tr'); elem.className = 'collage_rows'; if (tbody.querySelector('tr.collage_rows.hidden') != null) elem.classList.add('hidden'); elem.innerHTML = '<td><a href="/collages.php?id=' + collage.id + '">' + collage.name + '</a></td><td class="number_column">' + collage[artistsIndexes.includes(collage.collageCategoryID) ? 'artists' : 'torrentgroups'].length + '</td>'; tbody.append(elem); return collage; }).catch(function(reason) { evt.currentTarget.disabled = false; if (ajaxMessage == null) return; ajaxMessage.innerHTML = '<span style="color: #A00;">' + reason.toString() + '</span>'; ajaxMessage.classList.remove('hidden'); }); } function openCollage(evt) { let collageId = parseInt(dropDown.value); if (collageId <= 0) return false; let win = window.open('/collages.php?id=' + collageId, '_blank'); win.focus(); } function dataHandler(target, data) { var text = data.getData('text/plain'); if (!text) return false; if (searchforcollagebutton != null) searchforcollagebutton.disabled = false; target.value = text; srchForm.onsubmit(); return false; } }); } const contextId = 'context-9b7e0e42-1e35-4518-ac5f-b6bb31cce23f'; let menu = document.createElement('menu'); menu.type = 'context'; menu.id = contextId; function contextUpdater(evt) { menu = evt.currentTarget } menu.innerHTML = '<menuitem label="Remove from this collage" icon="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAADaElEQVQ4y0XTT0xcVRQG8O+ce+fNAOUN7QiFFoHSNq2SIDQ2QGfQ0hYtSatpuvFPjK0mxtDWRF24MG7UbVeasDGmiTrWgG2NjRlaUyAsirrQoCZVF5QZhsKUP/NmKMwM993jwlG/zfdtT3J+IGYCwPE3zye+f//d6UrmMAAwkUI5/+6qilD4q7Z90x9tjyQIYAIIAHjo1ZfjMn5dNm9+LdfeOjdZQeSCCEzETMQAUBl03C/6j0wmD0Vl7kCnfFgXiQNgFT/32ndnTp84lVnK+A+8rHTubW7piNT2Xfvp5xEDFABIheLwZ6dPjR4u+odWvKxvQ0Hpcd32RrFdvD1cXV9az8Ffz4sqFdT8fNr0P76/O372hYQDbHFEtnz+4vOJJ6C7M/fmTSjoKBYrOuAgEnDqqZI5HH/97LcDPe29CytLJmCtLpV8U1dVo6+M3R6DdnCirr4vOTFhKiortEBMrVJ6ODU3eSGVOkkAUKmU++WZ50aPdu7pvp/NmwAcbQzZ6mA12VUPmbFb4hAYzCbCrK+m01ODs8mni0BOMZEqiRS++eX3kQ5365PtO3c059dLVvuai+tFyf34A3SxwBxQ9iHF6mo6fXtwNjlQBHIMKCWAMBEbSOHqb3fie0NVsUdrd+za9MWaP+6wZBZJBQO2CsSXU+mJ88nUUwZYY4AtYDUAQAQgghWB2SiCfIGZT8OfS0E7DhiAIoEAsOXfkHIrJlIWZEMk4Uv9x248+9iBWHZ12dq//mSGWMUkmsDCsNFtNbsaoY6MetkRHyj8d0KI4H7S1zc68Ehbz8pazmBmRmFjw1bpAAUJJAxRAi5AzMEat7mBAodveN6wDxRUiBAeisWun9zfFl1ZXzOcWdT+6rKJBIPqcjI9/msufze6raa1ADEk0EWI6drqNkVEx2553hX9aTQ6fnTPvo7lnGf0xgNtlpZMJODo4dT81Nup5DMA4LC6+VJTQ3ceviGBzlnfvNJc3yuWxzmTzy+YnAdd3CB/6b4fUUoPp+9NXZi9e9yA1gyw9sbszPFLyYUpl5UWgh9gULZgsVjaXAAB/F7r7ngyGpPF7i75uKll0gFcACiLYwAIgNyLD7dOer09MnuwSwYjO+MAGGWS/EFdQ2KosWU6RPQPZ+B/zuUdJApfbNw9/U5dUwJlzn8DiOOFPhubkGUAAAAASUVORK5CYII=" /><menuitem label="-" />'; function subscribeCallback(evt) { let link = menu || evt.relatedTarget || document.activeElement; if (!(link instanceof HTMLAnchorElement)) return true; let collageId = parseInt(new URLSearchParams(link.search).get('id')); if (!collageId) { console.warn('Assertion failed: no collage id', link); throw 'no id'; } let xhr = new XMLHttpRequest; xhr.open('GET', '/userhistory.php?' + new URLSearchParams({ action: 'collage_subscribe', collageid: collageId, auth: auth, }), true); xhr.onreadystatechange = function() { if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; if (xhr.status >= 200 && xhr.status < 400) { console.info('Subscribed to collage id', collageId) } else console.error(defaultErrorHandler(xhr)); xhr.abort(); }; xhr.send(); } switch (document.location.pathname) { case '/torrents.php': { var torrentGroupId = new URLSearchParams(document.location.search).get('id'), collages; if (torrentGroupId) torrentGroupId = parseInt(torrentGroupId); else break; // Unexpected URL format const searchforcollage = document.getElementById('searchforcollage'), submitButton = document.getElementById('searchforcollagebutton'); if (searchforcollage != null) { if (submitButton != null) submitButton.disabled = searchforcollage.value.length <= 0; if (typeof SearchCollage == 'function') SearchCollage = () => { const searchTerm = $('#searchforcollage').val(), personalCollages = $('#personalcollages'); ajax.get(`ajax.php?action=collages&search=${encodeURIComponent(searchTerm)}`, responseText => { const { response, status } = JSON.parse(responseText); if (status !== 'success') return; const categories = response.reduce((accumulator, item) => { const { collageCategoryName } = item; accumulator[collageCategoryName] = (accumulator[collageCategoryName] || []).concat(item); return accumulator; }, {}); personalCollages.children().remove(); Object.entries(categories).forEach(([category, collages]) => { console.log(collages); personalCollages.append(` <optgroup label="${category}"> ${collages.reduce((accumulator, { id, name }) => `${accumulator}<option value="${id}">${name}</option>` ,'')} </optgroup> `); }); }); }; function inputHandler(evt, key) { const data = evt[key].getData('text/plain').trim(); if (!data) return true; evt.currentTarget.value = data; if (submitButton != null) submitButton.disabled = false; SearchCollage(); setTimeout(function() { const add_to_collage_select = document.querySelector('select.add_to_collage_select'); if (add_to_collage_select != null && add_to_collage_select.options.length > 1) { // TODO: expand } }, 3000); return false; } searchforcollage.onpaste = evt => inputHandler(evt, 'clipboardData'); searchforcollage.ondrop = evt => inputHandler(evt, 'dataTransfer'); searchforcollage.oninput = function(evt) { if (submitButton != null) submitButton.disabled = evt.currentTarget.value.length <= 0; }; searchforcollage.onkeypress = function(evt) { if (evt.key == 'Enter' && evt.currentTarget.value.length > 0) SearchCollage(); }; } else addQuickAddForm(); try { collages = JSON.parse(window.sessionStorage.collages) } catch(e) { collages = { } } if (!collages[document.domain]) collages[document.domain] = { }; function callback(evt) { switch (evt.currentTarget.nodeName) { case 'A': if (evt.button != 0 || !evt.altKey) return true; var link = evt.currentTarget; break; case 'MENUITEM': link = menu || evt.relatedTarget || document.activeElement; break; } if (!(link instanceof HTMLAnchorElement)) return true; let collageId = parseInt(new URLSearchParams(link.search).get('id')); if (!collageId) { console.warn('Assertion failed: no collage id', link); throw 'no id'; } return removeFromTorrentCollage(collageId, torrentGroupId, 'Are you sure to remove this group from collage "' + link.textContent.trim() + '"?').then(function(status) { const tr = link.parentNode.parentNode, table = tr.parentNode.parentNode; tr.remove(); if (table.querySelectorAll('tbody > tr:not([class="colhead"])').length <= 0) table.remove(); }); } menu.children[0].onclick = callback; let subscribeCmd = document.createElement('menuitem'); subscribeCmd.label = 'Subscribe to this collage - toggle (!)'; subscribeCmd.title = 'Use with care - toggling command; on already subscribed collages performs unsubscribe'; subscribeCmd.onclick = subscribeCallback; menu.insertBefore(subscribeCmd, menu.children[1]); document.body.append(menu); document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) { if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return; let collageId = parseInt(RegExp.$1), toggle, navLinks = [ ], numberColumn = link.parentNode.parentNode.querySelector('td.number_column'); link.onclick = callback; link.oncontextmenu = contextUpdater; link.setAttribute('contextmenu', contextId); link.title = 'Use Alt + left click or context menu(FF) to remove from this collage'; if (numberColumn != null) { numberColumn.style.cursor = 'pointer'; numberColumn.onclick = loadCollage; numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing'; } if (collages[document.domain][collageId]) { expandSection(); addCollageLinks(collages[document.domain][collageId]); } function addCollageLinks(collage) { var index = collage.torrentgroups.findIndex(group => group.id == torrentGroupId); if (index < 0) { console.warn('Assertion failed: torrent', torrentGroupId, 'not found in the collage', collage); return false; } link.style.color = 'white'; link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;'; var stats = document.createElement('span'); stats.textContent = (index + 1) + '/' + collage.torrentgroups.length; stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;'; navLinks.push(stats); link.parentNode.append(stats); if (collage.torrentgroups[index - 1]) { var a = document.createElement('a'); a.href = '/torrents.php?id=' + collage.torrentgroups[index - 1].id; //a.classList.add('brackets'); a.textContent = '[\xA0<\xA0]'; a.title = getTitle(index - 1); a.style = 'color: chartreuse; margin-right: 10px;'; navLinks.push(a); link.parentNode.prepend(a); a = document.createElement('a'); a.href = '/torrents.php?id=' + collage.torrentgroups[0].id; //a.classList.add('brackets'); a.textContent = '[\xA0<<\xA0]'; a.title = getTitle(0); a.style = 'color: chartreuse; margin-right: 5px;'; navLinks.push(a); link.parentNode.prepend(a); } if (collage.torrentgroups[index + 1]) { a = document.createElement('a'); a.href = '/torrents.php?id=' + collage.torrentgroups[index + 1].id; //a.classList.add('brackets'); a.textContent = '[\xA0>\xA0]'; a.title = getTitle(index + 1); a.style = 'color: chartreuse; margin-left: 10px;'; navLinks.push(a); link.parentNode.append(a); a = document.createElement('a'); a.href = '/torrents.php?id=' + collage.torrentgroups[collage.torrentgroups.length - 1].id; //a.classList.add('brackets'); a.textContent = '[\xA0>>\xA0]'; a.title = getTitle(collage.torrentgroups.length - 1); a.style = 'color: chartreuse; margin-left: 5px;'; navLinks.push(a); link.parentNode.append(a); } return true; function getTitle(index) { if (typeof index != 'number' || index < 0 || index >= collage.torrentgroups.length) return undefined; let title = collage.torrentgroups[index].musicInfo && Array.isArray(collage.torrentgroups[index].musicInfo.artists) ? collage.torrentgroups[index].musicInfo.artists.map(artist => artist.name).join(', ') + ' - ' : ''; if (collage.torrentgroups[index].name) title += collage.torrentgroups[index].name; if (collage.torrentgroups[index].year) title += ' (' + collage.torrentgroups[index].year + ')'; return title; } } function expandSection() { if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]'); if (toggle === null || toggle.dataset.expanded) return false; toggle.dataset.expanded = true; toggle.click(); return true; } function loadCollage(evt) { evt.currentTarget.disabled = true; navLinks.forEach(a => { a.remove() }); navLinks = []; let span = document.createElement('span'); span.textContent = '[\xA0loading...\xA0]'; span.style = 'color: red; background-color: white; margin-left: 10px;'; link.parentNode.append(span); queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(function(collage) { span.remove(); cacheCollage(collage); addCollageLinks(collage); evt.currentTarget.disabled = false; }, function(reason) { span.remove(); evt.currentTarget.disabled = false; }); return false; } }); function cacheCollage(collage) { collages[document.domain][collage.id] = { id: collage.id, name: collage.name, torrentgroups: collage.torrentgroups.map(group => ({ id: group.id, musicInfo: group.musicInfo ? { artists: Array.isArray(group.musicInfo.artists) ? group.musicInfo.artists.map(artist => ({ name: artist.name })) : undefined, } : undefined, name: group.name, year: parseInt(group.year) || undefined, })), }; window.sessionStorage.collages = JSON.stringify(collages); } break; } case '/artist.php': { var artistId = parseInt(new URLSearchParams(document.location.search).get('id')); if (!artistId) break; // Unexpected URL format addQuickAddForm(); try { collages = JSON.parse(window.sessionStorage.collages) } catch(e) { collages = { } } if (!collages[document.domain]) collages[document.domain] = { }; function callback(evt) { switch (evt.currentTarget.nodeName) { case 'A': if (evt.button != 0 || !evt.altKey) return true; var link = evt.currentTarget; break; case 'MENUITEM': link = menu || evt.relatedTarget || document.activeElement; break; } if (!(link instanceof HTMLAnchorElement)) return true; let collageId = parseInt(new URLSearchParams(link.search).get('id')); if (!collageId) { console.warn('Assertion failed: no collage id', link); throw 'no id'; } return removeFromArtistCollage(collageId, artistId, 'Are you sure to remove this artist from collage "' + link.textContent.trim() + '"?').then(function(status) { const tr = link.parentNode.parentNode, table = tr.parentNode.parentNode; tr.remove(); if (table.querySelectorAll('tbody > tr:not([class="colhead"])').length <= 0) table.remove(); }); } menu.children[0].onclick = callback; let subscribeCmd = document.createElement('menuitem'); subscribeCmd.label = 'Subscribe to this collage - toggle (!)'; subscribeCmd.title = 'Use with care - toggling command; on already subscribed collages performs unsubscribe'; subscribeCmd.onclick = subscribeCallback; menu.insertBefore(subscribeCmd, menu.children[1]); document.body.append(menu); document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) { if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return; let collageId = parseInt(RegExp.$1), toggle, navLinks = [], numberColumn = link.parentNode.parentNode.querySelector('td:last-of-type'); link.onclick = callback; link.oncontextmenu = contextUpdater; link.setAttribute('contextmenu', contextId); link.title = 'Use Alt + left click or context menu(FF) to remove from this collage'; if (numberColumn != null) { numberColumn.style.cursor = 'pointer'; numberColumn.onclick = loadCollage; numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing'; } if (collages[document.domain][collageId]) { expandSection(); addCollageLinks(collages[document.domain][collageId]); } function addCollageLinks(collage) { var index = collage.artists.findIndex(artist => artist.id == artistId); if (index < 0) { console.warn('Assertion failed: torrent', torrentGroupId, 'not found in the collage', collage); return false; } link.style.color = 'white'; link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;'; var stats = document.createElement('span'); stats.textContent = `${index + 1} / ${collage.artists.length}`; stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;'; navLinks.push(stats); link.parentNode.append(stats); if (collage.artists[index - 1]) { var a = document.createElement('a'); a.href = '/artist.php?id=' + collage.artists[index - 1].id; a.textContent = '[\xA0<\xA0]'; a.title = getTitle(index - 1); a.style = 'color: chartreuse; margin-right: 10px;'; navLinks.push(a); link.parentNode.prepend(a); a = document.createElement('a'); a.href = '/artist.php?id=' + collage.artists[0].id; a.textContent = '[\xA0<<\xA0]'; a.title = getTitle(0); a.style = 'color: chartreuse; margin-right: 5px;'; navLinks.push(a); link.parentNode.prepend(a); } if (collage.artists[index + 1]) { a = document.createElement('a'); a.href = '/artist.php?id=' + collage.artists[index + 1].id; a.textContent = '[\xA0>\xA0]'; a.title = getTitle(index + 1); a.style = 'color: chartreuse; margin-left: 10px;'; navLinks.push(a); link.parentNode.append(a); a = document.createElement('a'); a.href = '/artist.php?id=' + collage.artists[collage.artists.length - 1].id; a.textContent = '[\xA0>>\xA0]'; a.title = getTitle(collage.artists.length - 1); a.style = 'color: chartreuse; margin-left: 5px;'; navLinks.push(a); link.parentNode.append(a); } return true; function getTitle(index) { console.assert(index >= 0 && index < collage.artists.length, "index >= 0 && index < collage.artists.length"); if (!(index >= 0 && index < collage.artists.length)) return undefined; return collage.artists[index] ? collage.artists[index].name : ''; } } function expandSection() { if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]'); if (toggle === null || toggle.dataset.expanded) return false; toggle.dataset.expanded = true; toggle.click(); return true; } function loadCollage(evt) { evt.currentTarget.disabled = true; navLinks.forEach(a => { a.remove() }); navLinks = [ ]; let span = document.createElement('span'); span.textContent = '[\xA0loading...\xA0]'; span.style = 'color: red; background-color: white; margin-left: 10px;'; link.parentNode.append(span); queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(function(collage) { span.remove(); cacheCollage(collage); addCollageLinks(collage); evt.currentTarget.disabled = false; }, function(reason) { span.remove(); evt.currentTarget.disabled = false; }); return false; } }); function cacheCollage(collage) { collages[document.domain][collage.id] = { id: collage.id, name: collage.name, artists: collage.artists.map(artist => ({ id: artist.id, name: artist.name, })), }; window.sessionStorage.collages = JSON.stringify(collages); } break; } case '/collages.php': { var collageId = new URLSearchParams(document.location.search).get('id'); if (collageId) collageId = parseInt(collageId); else break; // Collage id missing let category = document.querySelector('div.box_category > div.pad > a'), selectors, callback; category = category != null ? category.textContent : undefined; console.assert(category, 'category != undefined'); if (category != 'Artists') { selectors = [ 'tr.group > td[colspan] > strong > a[href^="torrents.php?id="]', 'ul.collage_images > li > a[href^="torrents.php?id="]', ]; callback = function(evt) { switch (evt.currentTarget.nodeName) { case 'A': if (evt.button != 0 || !evt.altKey) return true; var link = evt.currentTarget; break; case 'MENUITEM': link = menu || evt.relatedTarget || document.activeElement; break; } if (!(link instanceof HTMLAnchorElement)) return true; let torrentGroupId = parseInt(new URLSearchParams(link.search).get('id')); if (!torrentGroupId) { console.warn('Assertion failed: no id', link); throw 'no id'; } removeFromTorrentCollage(collageId, torrentGroupId, 'Are you sure to remove selected group from this collage?').then(function(status) { document.querySelectorAll(selectors.join(', ')).forEach(function(a) { if (parseInt(new URLSearchParams(a.search).get('id')) == torrentGroupId) switch (a.parentNode.nodeName) { case 'STRONG': a.parentNode.parentNode.parentNode.remove(); break; case 'LI': a.parentNode.remove(); break; } }); }); }; } else { selectors = [ 'table#discog_table > tbody > tr > td > a[href^="artist.php?id="]', 'ul.collage_images > li > a[href^="artist.php?id="]', ]; callback = function(evt) { switch (evt.currentTarget.nodeName) { case 'A': if (evt.button != 0 || !evt.altKey) return true; var link = evt.currentTarget; break; case 'MENUITEM': link = menu || evt.relatedTarget || document.activeElement; break; } if (!(link instanceof HTMLAnchorElement)) return true; let artistId = parseInt(new URLSearchParams(link.search).get('id')); if (!artistId) { console.warn('Assertion failed: no id', evt.currentTarget); throw 'no id'; } removeFromArtistCollage(collageId, artistId, 'Are you sure to remove selected artist from this collage?').then(function(status) { document.querySelectorAll(selectors.join(', ')).forEach(function(a) { if (parseInt(new URLSearchParams(a.search).get('id')) == artistId) switch (a.parentNode.nodeName) { case 'TD': a.parentNode.parentNode.remove(); break; case 'LI': a.parentNode.remove(); break; } }); }); }; let artistLink = document.querySelector('form.add_form[name="artist"] input#artist'); if (artistLink != null) { let ref = document.querySelector('form.add_form[name="artist"] > div.submit_div'); let searchBtn = document.createElement('input'); searchBtn.value = 'Look up'; searchBtn.type = 'button'; searchBtn.onclick = function(evt) { let xhr = new XMLHttpRequest; xhr.open('HEAD', '/artist.php?artistname=' + encodeURIComponent(artistLink.value.trim()), true); xhr.onreadystatechange = function() { if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; artistLink.value = xhr.responseURL.includes('/artist.php?id=') ? xhr.responseURL : ''; }; xhr.send(); }; ref.append(searchBtn); } } menu.children[0].onclick = callback; document.body.append(menu); function handlerInstaller(a) { a.onclick = callback; a.oncontextmenu = contextUpdater; a.setAttribute('contextmenu', contextId); } document.querySelectorAll(selectors.join(', ')).forEach(handlerInstaller); let coverart = document.getElementById('coverart'); if (coverart != null) new MutationObserver(function(mutationsList) { mutationsList.forEach(function(mutation) { if (mutation.type == 'childList') mutation.addedNodes.forEach(function(node) { if (node.nodeName != 'UL' || !node.classList.contains('collage_images')) return; node.querySelectorAll('li > a').forEach(handlerInstaller); }); }); }).observe(coverart, { childList: true }); break; } }