// ==UserScript==
// @name Nyaa - Personal Tweaks
// @namespace github.com/Decicus
// @match https://nyaa.si/*
// @grant GM_setClipboard
// @version 1.5.0
// @author Decicus
// @description Adds some extra functionality to Nyaa.
// ==/UserScript==
let copyAmount = 5;
const multipliers = {
Bytes: 1,
KiB: 1024,
MiB: 1024 * 1024,
GiB: 1024 * 1024 * 1024,
TiB: 1024 * 1024 * 1024 * 1024,
};
const sizesRegex = /(Bytes|KiB|MiB|GiB|TiB)/g;
function getElements(linkType = 'download')
{
const elements = document.querySelectorAll(`.fa-${linkType}`);
return Array.from(elements)
.map(x => x.parentElement.href)
.filter(x => x && x.trim() !== '');
}
function getLinks()
{
return getElements('download');
}
function getMagnets()
{
return getElements('magnet');
}
/**
* Calculate the approximate total size of all torrents listed in the table and append it as an information row at the bottom of the table
*/
function calculateTotalSize()
{
const timestamps = document.querySelectorAll('.torrent-list td[data-timestamp]');
let totalBytes = 0;
for (const timestamp of timestamps)
{
const sizeCell = timestamp.previousElementSibling;
const size = sizeCell.textContent.trim();
let suffix = size.match(sizesRegex);
if (!suffix || suffix.length < 1) {
console.error('Could not find size in element', sizeCell);
continue;
}
suffix = suffix[0];
const multiplier = multipliers[suffix];
const sizeNumber = parseFloat(size.replace(` ${suffix}`, '').trim());
const torrentBytes = sizeNumber * multiplier;
totalBytes += torrentBytes;
}
const multiplierKeys = Object.keys(multipliers).reverse();
let totalSize = `${totalBytes} Bytes`;
for (const key of multiplierKeys)
{
if (totalBytes === 0) {
break;
}
const divideBy = multipliers[key];
const result = totalBytes / divideBy;
if (result > 1) {
totalSize = `${result.toFixed(2)} ${key}`;
break;
}
}
const torrentList = document.querySelector('.torrent-list tbody');
if (!torrentList) {
return;
}
const sampleTorrent = "https://nyaa.si/download/222409.torrent";
const sampleMagnet = "magnet:?xt=urn:btih:db0ffe8174317b0b0ee4beb7b54f558bb9089746&dn=%5Beoy%5D%20dark%20dragoon%20-%2001.txt&tr=http%3A%2F%2Fnyaa.tracker.wf%3A7777%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce";
const html = `
💽
|
Total size
|
|
${totalSize} |
|
|
|
|
`;
torrentList.insertAdjacentHTML('beforeend', html);
}
/**
* Handler for copying download links (torrents)
*/
function copyLinks(amount)
{
let links = getLinks();
if (amount && typeof amount === 'number') {
amount = amount > links.length ? links.length : amount;
links = links.slice(0, amount);
}
const list = links.join('\n');
GM_setClipboard(list);
}
/**
* Handler for copying magnets
*/
function copyMagnets(amount)
{
let magnets = getMagnets();
if (amount && typeof amount === 'number') {
amount = amount > magnets.length ? magnets.length : amount;
magnets = magnets.slice(0, amount);
}
const list = magnets.join('\n');
GM_setClipboard(list);
}
/**
* Changes the download links for torrents/magnets to trigger a copy,
* instead of default handling (download for torrents, torrent *client* for magnets).
*/
function copyOnClick(ev)
{
ev.preventDefault();
const target = ev.target;
const isAnchor = target.nodeName === 'A';
const parent = isAnchor ? target : target.parentElement;
GM_setClipboard(parent.href + '\n');
const checkIcon = 'fa-check';
const originalIcon = parent.href.includes('/download/') ? 'fa-download' : 'fa-magnet';
const iconElement = !target.classList.contains('fa') ? target.querySelector('.fa') : target;
console.log(iconElement);
if (iconElement) {
iconElement.classList.replace(originalIcon, checkIcon);
setTimeout(
function() {
iconElement.classList.replace(checkIcon, originalIcon);
},
1500
);
}
}
/**
* Changes said magnet/torrent links to use the `copyOnClick` handler.
*/
function changeToCopy()
{
const links = document.querySelectorAll('.fa-magnet, .fa-download');
for (const link of links)
{
link.parentElement.addEventListener('click', copyOnClick);
}
}
/**
* List of regex replacements so that I can copy titles as directory names, with my personally preferred naming scheme.
*/
const titleReplacements = [
{
comment: 'Remove quoted series/movie title from the end',
search: / \"[A-z0-9-!\s]+\"$/,
replacement: '',
},
// {
// comment: 'Move release group to the end',
// search: /^\[([-\w ]+)\] (.+)$/,
// replacement: '$2 [$1]',
// },
{
comment: 'Colon space to space dash space',
search: /: /g,
replacement: ' - ',
},
{
comment: 'Ensure seasons have two numbers',
search: / \(Season (\d)\)/g,
replacement: ' (Season 0$1)',
},
{
comment: 'Season naming so Sonarr doesnt get confused because its dumb',
search: / \(Season (\d+)\)/g,
replacement: ' - S$1',
},
{
comment: 'No slashes, please',
search: /\//g,
replacement: '-',
},
{
comment: 'Closing => Opening bracket spacing between each "tag"',
search: /\]\[/g,
replacement: '] [',
},
{
comment: 'Final replacement: Replace 2+ spaces with one space',
search: /\s{2,}/g,
replacement: ' ',
},
];
/**
* Uses `titleReplacements` to "fix" torrent name when "Copy torrent name" button is clicked.
*/
function fixTitle(input)
{
let finalString = input.trim();
for (const replace of titleReplacements)
{
const { comment, search, replacement } = replace;
const match = finalString.match(search);
if (match === null) {
continue;
}
console.log(`\nFound: ${search}\nComment: ${comment}\nMatches:\n- ${match.join('\n- ')}`);
finalString = finalString.replace(search, replacement || '');
}
// Deal with any excess whitespace
finalString = finalString.trim();
console.log(`Original input: ${input}\nFinal string: ${finalString}`);
return finalString;
}
/**
* Adds copy button for torrent titles on torrent pages
* Implements `fixTitle()`
*/
function titleHandler()
{
const title = document.querySelector('h3.panel-title');
if (!title) {
return;
}
const titleParent = title.parentElement;
const buttonParent = document.createElement('div');
buttonParent.setAttribute('class', 'btn-group pull-right');
const button = document.createElement('button');
button.textContent = '📋';
button.setAttribute('class', 'btn btn-primary btn-sm');
button.addEventListener('click', function() {
const origText = button.textContent;
button.textContent = '✔';
button.setAttribute('disabled', '1');
const fixedTitle = fixTitle(title.textContent.trim());
GM_setClipboard(fixedTitle);
setTimeout(
() => {
button.textContent = origText;
button.removeAttribute('disabled');
}, 1500
);
});
titleParent.classList.add('clearfix');
buttonParent.insertAdjacentElement('afterbegin', button);
titleParent.insertAdjacentElement('afterbegin', buttonParent);
}
/**
* Used by `copyFilenameHandler()` to do the actual copying
*/
function copyFilenameTrigger(ev)
{
const { target } = ev;
// Trust no one, not even yourself
if (!target) {
return;
}
// We clone it here because I want to remove the file size that's normally at the end
// If I simply do a `remove()` on the usual element, it will actually remove it from the DOM.
const parent = target.parentElement.cloneNode(true);
const fileSize = parent.querySelector('.file-size');
if (fileSize) {
fileSize.remove();
}
const filename = parent.textContent.trim();
target.classList.remove('fa-file');
target.classList.add('fa-check');
GM_setClipboard(filename);
setTimeout(() => {
target.classList.remove('fa-check');
target.classList.add('fa-file');
}, 1500);
}
/**
* Clicking the file icon in front of each file will allow you to copy the filename
*/
async function copyFilenameHandler()
{
const fileList = document.querySelector('.torrent-file-list');
if (!fileList) {
return;
}
const fileIcons = fileList.querySelectorAll('.fa-file');
for (const icon of fileIcons)
{
icon.addEventListener('click', copyFilenameTrigger);
}
}
function bbcodeConverter(html) {
html = html.replace(/(.*?)<\/pre>/gmi, "[code]$2[/code]");
html = html.replace(/(.*?)<\/h[1-7]>/, "\n[h]$2[/h]\n");
//paragraph handling:
//- if a paragraph opens on the same line as another one closes, insert an extra blank line
//- opening tag becomes two line breaks
//- closing tags are just removed
// html += html.replace(/<\/p>\n
]*>/\n\n/gi;
// html += html.replace(/<\/p>//gi;
html = html.replace(/<\/p>\n
/gi, "\n\n\n");
html = html.replace(/
/gi, "\n");
html = html.replace(/