nyaa-shit.user.js
· 19 KiB · JavaScript
Raw
// ==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 = `<tr class="default" id="all-torrents-size">
<td class="text-center">
<a href="#all-torrents-size" title="Total size">
💽
</a>
</td>
<td colspan="2">
<a href="#all-torrents-size">Total size</a>
</td>
<td class="text-center">
<!-- These elements purely exist to avoid any issues with extensions or userscripts that would normally expect valid torrent/magnet URIs -->
<!-- In my case I only found an issue with NyaaBlue from SeaDex -->
<a href="${sampleTorrent}" style="display: none;"><!-- Download torrent --></a>
<a href="${sampleMagnet}" style="display: none;"><!-- Magnet URI --></a>
</td>
<td class="text-center">${totalSize}</td>
<td class="text-center"><!-- Timestamp --></td>
<td class="text-center"><!-- Seeders --></td>
<td class="text-center"><!-- Leechers --></td>
<td class="text-center"><!-- Completed --></td>
</tr>`;
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(.*?)>(.*?)<\/pre>/gmi, "[code]$2[/code]");
html = html.replace(/<h[1-7](.*?)>(.*?)<\/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><p/<\/p>\n<p/gi;
// html += html.replace(/<p[^>]*>/\n\n/gi;
// html += html.replace(/<\/p>//gi;
html = html.replace(/<\/p>\n<p>/gi, "\n\n\n");
html = html.replace(/<br(.*?)>/gi, "\n");
html = html.replace(/<textarea(.*?)>(.*?)<\/textarea>/gmi, "\[code]$2\[\/code]");
html = html.replace(/<b>/gi, "[b]");
html = html.replace(/<i>/gi, "[i]");
html = html.replace(/<u>/gi, "[u]");
html = html.replace(/<\/b>/gi, "[/b]");
html = html.replace(/<\/i>/gi, "[/i]");
html = html.replace(/<\/u>/gi, "[/u]");
html = html.replace(/<em>/gi, "[b]");
html = html.replace(/<\/em>/gi, "[/b]");
html = html.replace(/<strong>/gi, "[b]");
html = html.replace(/<\/strong>/gi, "[/b]");
html = html.replace(/<cite>/gi, "[i]");
html = html.replace(/<\/cite>/gi, "[/i]");
html = html.replace(/<font color="(.*?)">(.*?)<\/font>/gmi, "[color=$1]$2[/color]");
html = html.replace(/<font color=(.*?)>(.*?)<\/font>/gmi, "[color=$1]$2[/color]");
html = html.replace(/<link(.*?)>/gi, "");
html = html.replace(/<li(.*?)>(.*?)<\/li>/gi, "[*]$2");
html = html.replace(/<ul(.*?)>/gi, "[list]");
html = html.replace(/<\/ul>/gi, "[/list]");
html = html.replace(/<div>/gi, "\n");
html = html.replace(/<\/div>/gi, "\n");
html = html.replace(/<td(.*?)>/gi, " ");
html = html.replace(/<tr(.*?)>/gi, "\n");
html = html.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, "[img]$2[/img]");
html = html.replace(/<a(.*?)href="(.*?)"(.*?)>(.*?)<\/a>/gi, "[url=$2]$4[/url]");
html = html.replace(/<head>(.*?)<\/head>/gmi, "");
html = html.replace(/<object>(.*?)<\/object>/gmi, "");
html = html.replace(/<script(.*?)>(.*?)<\/script>/gmi, "");
html = html.replace(/<style(.*?)>(.*?)<\/style>/gmi, "");
html = html.replace(/<title>(.*?)<\/title>/gmi, "");
html = html.replace(/<!--(.*?)-->/gmi, "\n");
html = html.replace(/\/\//gi, "/");
html = html.replace(/http:\//gi, "http://");
html = html.replace(/<(?:[^>'"]*|(['"]).*?\1)*>/gmi, "");
html = html.replace(/\r\r/gi, "");
html = html.replace(/\[img]\//gi, "[img]");
html = html.replace(/\[url=\//gi, "[url=");
html = html.replace(/(\S)\n/gi, "$1 ");
return html;
}
function descriptionHandler()
{
const descriptionElem = document.querySelector('#torrent-description');
if (!descriptionElem) {
return;
}
console.log(bbcodeConverter(descriptionElem.innerHTML));
const button = document.createElement('button');
button.textContent = '📋 Copy description as BBCode';
button.setAttribute('class', 'btn btn-primary');
button.addEventListener('click', function() {
const origText = button.textContent;
button.textContent = '✔ Copied!';
button.setAttribute('disabled', '1');
const descriptionBBcode = bbcodeConverter(descriptionElem.innerHTML).trim();
GM_setClipboard(descriptionBBcode);
setTimeout(
() => {
button.textContent = origText;
button.removeAttribute('disabled');
}, 1500
);
});
const panelHeading = document.createElement('div');
panelHeading.setAttribute('class', 'panel-heading');
panelHeading.insertAdjacentElement('beforeend', button);
descriptionElem.insertAdjacentElement('beforebegin', panelHeading);
}
/**
* Adds a clipboard icon (using Font Awesome 4) to each row that is marked for "mass copy".
*/
function addClipboardIconToRow()
{
const rows = document.querySelectorAll('.torrent-list > tbody > tr > td[colspan="2"]');
let currentAmount = 0;
for (const row of rows)
{
currentAmount++;
const iconExists = row.querySelector('a.clipboard-icon');
if (iconExists) {
iconExists.remove();
}
// Check if this row is past the point of "don't receive icon"
if (currentAmount > copyAmount) {
// `continue`, not `break` - If we break, then when we lower `copyAmount`, there will be extra rows with clipboard icon.
continue;
}
row.insertAdjacentHTML('afterbegin', '<a href="#" class="comments clipboard-icon" style="margin-left: 0.5em;"><i class="fa fa-clipboard"></i></a>');
}
}
/**
* Adds buttons and number input fields above torrent table, for "mass copying" links/magnets.
*/
function torrentListViewHandler()
{
const parentElement = document.querySelector('.table-responsive');
if (!parentElement) {
return;
}
addClipboardIconToRow();
/**
* Start: Copy all links/magnets
*/
const element = document.createElement('button');
element.innerHTML = '📋 Copy all download links <i class="fa fa-fw fa-download"></i>';
element.setAttribute('class', 'btn btn-default');
element.addEventListener('click', function() {
const origText = element.innerHTML;
element.innerHTML = '✔';
element.setAttribute('disabled', '1');
copyLinks();
setTimeout(() => {
element.innerHTML = origText;
element.removeAttribute('disabled');
}, 1500);
});
const magnetCopy = document.createElement('button');
magnetCopy.innerHTML = '📋 Copy all magnets <i class="fa fa-fw fa-magnet"></i>';
magnetCopy.setAttribute('class', 'btn btn-default');
magnetCopy.addEventListener('click', function() {
const origText = magnetCopy.innerHTML;
magnetCopy.innerHTML = '✔';
magnetCopy.setAttribute('disabled', '1');
copyMagnets();
setTimeout(() => {
magnetCopy.innerHTML = origText;
magnetCopy.removeAttribute('disabled');
}, 1500);
});
/**
* End: Copy all links/magnets
*/
/**
* Start: Copy X links/magnets
*/
const linkAmount = (getLinks()).length;
copyAmount = linkAmount < 5 ? linkAmount : 5;
const amountSelect = document.createElement('input');
amountSelect.setAttribute('type', 'number');
amountSelect.setAttribute('min', '2');
amountSelect.setAttribute('max', linkAmount);
amountSelect.setAttribute('value', copyAmount);
amountSelect.setAttribute('class', 'pull-right');
const amountCopyLinks = document.createElement('button');
amountCopyLinks.innerHTML = `📋 Copy ${copyAmount} download links <i class="fa fa-fw fa-download"></i>`;
amountCopyLinks.setAttribute('class', 'btn btn-default pull-right');
amountCopyLinks.addEventListener('click', function() {
const origText = amountCopyLinks.innerHTML;
amountCopyLinks.innerHTML = '✔';
amountCopyLinks.setAttribute('disabled', '1');
copyLinks(copyAmount);
setTimeout(() => {
amountCopyLinks.innerHTML = origText;
amountCopyLinks.removeAttribute('disabled');
}, 1500);
});
const amountCopyMagnets = document.createElement('button');
amountCopyMagnets.innerHTML = `📋 Copy ${copyAmount} magnets <i class="fa fa-fw fa-magnet"></i>`;
amountCopyMagnets.setAttribute('class', 'btn btn-default pull-right');
amountCopyMagnets.addEventListener('click', function() {
const origText = amountCopyMagnets.innerHTML;
amountCopyMagnets.innerHTML = '✔';
amountCopyMagnets.setAttribute('disabled', '1');
copyMagnets(copyAmount);
setTimeout(() => {
amountCopyMagnets.innerHTML = origText;
amountCopyMagnets.removeAttribute('disabled');
}, 1500);
});
amountSelect.addEventListener('change', function() {
copyAmount = parseInt(amountSelect.value, 10);
amountCopyLinks.innerHTML = `📋 Copy ${copyAmount} download links <i class="fa fa-fw fa-download"></i>`;
amountCopyMagnets.innerHTML = `📋 Copy ${copyAmount} magnets <i class="fa fa-fw fa-magnet"></i>`;
addClipboardIconToRow();
});
/**
* End: Copy X links/magnets
*/
/**
* Append all these buttons and inputs to the DOM.
*/
parentElement.insertAdjacentElement('beforebegin', element);
parentElement.insertAdjacentElement('beforebegin', magnetCopy);
parentElement.insertAdjacentElement('beforebegin', amountSelect);
parentElement.insertAdjacentElement('beforebegin', amountCopyMagnets);
parentElement.insertAdjacentElement('beforebegin', amountCopyLinks);
}
/**
* This previously changed some `.container` classes to `.container-fluid`,
* but since that breaks other userscripts, such as NyaaBlue from SeaDex,
* I figured it'd be better to simply override the CSS width that .container usually applies.
*/
function widerContainers()
{
const styleDoc = document.createElement('style');
styleDoc.setAttribute('type', 'text/css');
styleDoc.textContent = `
.container {
width: auto !important;
}
`;
document.head.insertAdjacentElement('beforeend', styleDoc);
}
function init() {
changeToCopy();
titleHandler();
descriptionHandler();
copyFilenameHandler();
torrentListViewHandler();
calculateTotalSize();
widerContainers();
}
init();
1 | // ==UserScript== |
2 | // @name Nyaa - Personal Tweaks |
3 | // @namespace github.com/Decicus |
4 | // @match https://nyaa.si/* |
5 | // @grant GM_setClipboard |
6 | // @version 1.5.0 |
7 | // @author Decicus |
8 | // @description Adds some extra functionality to Nyaa. |
9 | // ==/UserScript== |
10 | |
11 | let copyAmount = 5; |
12 | const multipliers = { |
13 | Bytes: 1, |
14 | KiB: 1024, |
15 | MiB: 1024 * 1024, |
16 | GiB: 1024 * 1024 * 1024, |
17 | TiB: 1024 * 1024 * 1024 * 1024, |
18 | }; |
19 | |
20 | const sizesRegex = /(Bytes|KiB|MiB|GiB|TiB)/g; |
21 | |
22 | function getElements(linkType = 'download') |
23 | { |
24 | const elements = document.querySelectorAll(`.fa-${linkType}`); |
25 | return Array.from(elements) |
26 | .map(x => x.parentElement.href) |
27 | .filter(x => x && x.trim() !== ''); |
28 | } |
29 | |
30 | function getLinks() |
31 | { |
32 | return getElements('download'); |
33 | } |
34 | |
35 | function getMagnets() |
36 | { |
37 | return getElements('magnet'); |
38 | } |
39 | |
40 | /** |
41 | * 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 |
42 | */ |
43 | function calculateTotalSize() |
44 | { |
45 | const timestamps = document.querySelectorAll('.torrent-list td[data-timestamp]'); |
46 | let totalBytes = 0; |
47 | for (const timestamp of timestamps) |
48 | { |
49 | const sizeCell = timestamp.previousElementSibling; |
50 | const size = sizeCell.textContent.trim(); |
51 | let suffix = size.match(sizesRegex); |
52 | if (!suffix || suffix.length < 1) { |
53 | console.error('Could not find size in element', sizeCell); |
54 | continue; |
55 | } |
56 | |
57 | suffix = suffix[0]; |
58 | const multiplier = multipliers[suffix]; |
59 | const sizeNumber = parseFloat(size.replace(` ${suffix}`, '').trim()); |
60 | const torrentBytes = sizeNumber * multiplier; |
61 | |
62 | totalBytes += torrentBytes; |
63 | } |
64 | |
65 | const multiplierKeys = Object.keys(multipliers).reverse(); |
66 | let totalSize = `${totalBytes} Bytes`; |
67 | for (const key of multiplierKeys) |
68 | { |
69 | if (totalBytes === 0) { |
70 | break; |
71 | } |
72 | |
73 | const divideBy = multipliers[key]; |
74 | const result = totalBytes / divideBy; |
75 | if (result > 1) { |
76 | totalSize = `${result.toFixed(2)} ${key}`; |
77 | break; |
78 | } |
79 | } |
80 | |
81 | const torrentList = document.querySelector('.torrent-list tbody'); |
82 | if (!torrentList) { |
83 | return; |
84 | } |
85 | |
86 | const sampleTorrent = "https://nyaa.si/download/222409.torrent"; |
87 | 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"; |
88 | |
89 | const html = `<tr class="default" id="all-torrents-size"> |
90 | <td class="text-center"> |
91 | <a href="#all-torrents-size" title="Total size"> |
92 | 💽 |
93 | </a> |
94 | </td> |
95 | <td colspan="2"> |
96 | <a href="#all-torrents-size">Total size</a> |
97 | </td> |
98 | <td class="text-center"> |
99 | <!-- These elements purely exist to avoid any issues with extensions or userscripts that would normally expect valid torrent/magnet URIs --> |
100 | <!-- In my case I only found an issue with NyaaBlue from SeaDex --> |
101 | <a href="${sampleTorrent}" style="display: none;"><!-- Download torrent --></a> |
102 | <a href="${sampleMagnet}" style="display: none;"><!-- Magnet URI --></a> |
103 | </td> |
104 | <td class="text-center">${totalSize}</td> |
105 | <td class="text-center"><!-- Timestamp --></td> |
106 | <td class="text-center"><!-- Seeders --></td> |
107 | <td class="text-center"><!-- Leechers --></td> |
108 | <td class="text-center"><!-- Completed --></td> |
109 | </tr>`; |
110 | |
111 | torrentList.insertAdjacentHTML('beforeend', html); |
112 | } |
113 | |
114 | /** |
115 | * Handler for copying download links (torrents) |
116 | */ |
117 | function copyLinks(amount) |
118 | { |
119 | let links = getLinks(); |
120 | if (amount && typeof amount === 'number') { |
121 | amount = amount > links.length ? links.length : amount; |
122 | links = links.slice(0, amount); |
123 | } |
124 | |
125 | const list = links.join('\n'); |
126 | GM_setClipboard(list); |
127 | } |
128 | |
129 | /** |
130 | * Handler for copying magnets |
131 | */ |
132 | function copyMagnets(amount) |
133 | { |
134 | let magnets = getMagnets(); |
135 | if (amount && typeof amount === 'number') { |
136 | amount = amount > magnets.length ? magnets.length : amount; |
137 | magnets = magnets.slice(0, amount); |
138 | } |
139 | |
140 | const list = magnets.join('\n'); |
141 | GM_setClipboard(list); |
142 | } |
143 | |
144 | /** |
145 | * Changes the download links for torrents/magnets to trigger a copy, |
146 | * instead of default handling (download for torrents, torrent *client* for magnets). |
147 | */ |
148 | function copyOnClick(ev) |
149 | { |
150 | ev.preventDefault(); |
151 | |
152 | const target = ev.target; |
153 | const isAnchor = target.nodeName === 'A'; |
154 | const parent = isAnchor ? target : target.parentElement; |
155 | GM_setClipboard(parent.href + '\n'); |
156 | |
157 | const checkIcon = 'fa-check'; |
158 | const originalIcon = parent.href.includes('/download/') ? 'fa-download' : 'fa-magnet'; |
159 | |
160 | const iconElement = !target.classList.contains('fa') ? target.querySelector('.fa') : target; |
161 | console.log(iconElement); |
162 | |
163 | if (iconElement) { |
164 | iconElement.classList.replace(originalIcon, checkIcon); |
165 | |
166 | setTimeout( |
167 | function() { |
168 | iconElement.classList.replace(checkIcon, originalIcon); |
169 | }, |
170 | 1500 |
171 | ); |
172 | } |
173 | } |
174 | |
175 | /** |
176 | * Changes said magnet/torrent links to use the `copyOnClick` handler. |
177 | */ |
178 | function changeToCopy() |
179 | { |
180 | const links = document.querySelectorAll('.fa-magnet, .fa-download'); |
181 | |
182 | for (const link of links) |
183 | { |
184 | link.parentElement.addEventListener('click', copyOnClick); |
185 | } |
186 | } |
187 | |
188 | /** |
189 | * List of regex replacements so that I can copy titles as directory names, with my personally preferred naming scheme. |
190 | */ |
191 | const titleReplacements = [ |
192 | { |
193 | comment: 'Remove quoted series/movie title from the end', |
194 | search: / \"[A-z0-9-!\s]+\"$/, |
195 | replacement: '', |
196 | }, |
197 | // { |
198 | // comment: 'Move release group to the end', |
199 | // search: /^\[([-\w ]+)\] (.+)$/, |
200 | // replacement: '$2 [$1]', |
201 | // }, |
202 | { |
203 | comment: 'Colon space to space dash space', |
204 | search: /: /g, |
205 | replacement: ' - ', |
206 | }, |
207 | { |
208 | comment: 'Ensure seasons have two numbers', |
209 | search: / \(Season (\d)\)/g, |
210 | replacement: ' (Season 0$1)', |
211 | }, |
212 | { |
213 | comment: 'Season naming so Sonarr doesnt get confused because its dumb', |
214 | search: / \(Season (\d+)\)/g, |
215 | replacement: ' - S$1', |
216 | }, |
217 | { |
218 | comment: 'No slashes, please', |
219 | search: /\//g, |
220 | replacement: '-', |
221 | }, |
222 | { |
223 | comment: 'Closing => Opening bracket spacing between each "tag"', |
224 | search: /\]\[/g, |
225 | replacement: '] [', |
226 | }, |
227 | { |
228 | comment: 'Final replacement: Replace 2+ spaces with one space', |
229 | search: /\s{2,}/g, |
230 | replacement: ' ', |
231 | }, |
232 | ]; |
233 | |
234 | /** |
235 | * Uses `titleReplacements` to "fix" torrent name when "Copy torrent name" button is clicked. |
236 | */ |
237 | function fixTitle(input) |
238 | { |
239 | let finalString = input.trim(); |
240 | for (const replace of titleReplacements) |
241 | { |
242 | const { comment, search, replacement } = replace; |
243 | |
244 | const match = finalString.match(search); |
245 | if (match === null) { |
246 | continue; |
247 | } |
248 | |
249 | console.log(`\nFound: ${search}\nComment: ${comment}\nMatches:\n- ${match.join('\n- ')}`); |
250 | finalString = finalString.replace(search, replacement || ''); |
251 | } |
252 | |
253 | // Deal with any excess whitespace |
254 | finalString = finalString.trim(); |
255 | console.log(`Original input: ${input}\nFinal string: ${finalString}`); |
256 | |
257 | return finalString; |
258 | } |
259 | |
260 | /** |
261 | * Adds copy button for torrent titles on torrent pages |
262 | * Implements `fixTitle()` |
263 | */ |
264 | function titleHandler() |
265 | { |
266 | const title = document.querySelector('h3.panel-title'); |
267 | |
268 | if (!title) { |
269 | return; |
270 | } |
271 | |
272 | const titleParent = title.parentElement; |
273 | |
274 | const buttonParent = document.createElement('div'); |
275 | buttonParent.setAttribute('class', 'btn-group pull-right'); |
276 | |
277 | const button = document.createElement('button'); |
278 | button.textContent = '📋'; |
279 | button.setAttribute('class', 'btn btn-primary btn-sm'); |
280 | button.addEventListener('click', function() { |
281 | const origText = button.textContent; |
282 | |
283 | button.textContent = '✔'; |
284 | button.setAttribute('disabled', '1'); |
285 | |
286 | const fixedTitle = fixTitle(title.textContent.trim()); |
287 | GM_setClipboard(fixedTitle); |
288 | |
289 | setTimeout( |
290 | () => { |
291 | button.textContent = origText; |
292 | button.removeAttribute('disabled'); |
293 | }, 1500 |
294 | ); |
295 | }); |
296 | |
297 | titleParent.classList.add('clearfix'); |
298 | buttonParent.insertAdjacentElement('afterbegin', button); |
299 | titleParent.insertAdjacentElement('afterbegin', buttonParent); |
300 | } |
301 | |
302 | /** |
303 | * Used by `copyFilenameHandler()` to do the actual copying |
304 | */ |
305 | function copyFilenameTrigger(ev) |
306 | { |
307 | const { target } = ev; |
308 | |
309 | // Trust no one, not even yourself |
310 | if (!target) { |
311 | return; |
312 | } |
313 | |
314 | // We clone it here because I want to remove the file size that's normally at the end |
315 | // If I simply do a `remove()` on the usual element, it will actually remove it from the DOM. |
316 | const parent = target.parentElement.cloneNode(true); |
317 | const fileSize = parent.querySelector('.file-size'); |
318 | |
319 | if (fileSize) { |
320 | fileSize.remove(); |
321 | } |
322 | |
323 | const filename = parent.textContent.trim(); |
324 | |
325 | target.classList.remove('fa-file'); |
326 | target.classList.add('fa-check'); |
327 | |
328 | GM_setClipboard(filename); |
329 | |
330 | setTimeout(() => { |
331 | target.classList.remove('fa-check'); |
332 | target.classList.add('fa-file'); |
333 | }, 1500); |
334 | } |
335 | |
336 | /** |
337 | * Clicking the file icon in front of each file will allow you to copy the filename |
338 | */ |
339 | async function copyFilenameHandler() |
340 | { |
341 | const fileList = document.querySelector('.torrent-file-list'); |
342 | if (!fileList) { |
343 | return; |
344 | } |
345 | |
346 | const fileIcons = fileList.querySelectorAll('.fa-file'); |
347 | |
348 | for (const icon of fileIcons) |
349 | { |
350 | icon.addEventListener('click', copyFilenameTrigger); |
351 | } |
352 | } |
353 | |
354 | function bbcodeConverter(html) { |
355 | html = html.replace(/<pre(.*?)>(.*?)<\/pre>/gmi, "[code]$2[/code]"); |
356 | html = html.replace(/<h[1-7](.*?)>(.*?)<\/h[1-7]>/, "\n[h]$2[/h]\n"); |
357 | //paragraph handling: |
358 | //- if a paragraph opens on the same line as another one closes, insert an extra blank line |
359 | //- opening tag becomes two line breaks |
360 | //- closing tags are just removed |
361 | // html += html.replace(/<\/p><p/<\/p>\n<p/gi; |
362 | // html += html.replace(/<p[^>]*>/\n\n/gi; |
363 | // html += html.replace(/<\/p>//gi; |
364 | |
365 | html = html.replace(/<\/p>\n<p>/gi, "\n\n\n"); |
366 | |
367 | html = html.replace(/<br(.*?)>/gi, "\n"); |
368 | html = html.replace(/<textarea(.*?)>(.*?)<\/textarea>/gmi, "\[code]$2\[\/code]"); |
369 | html = html.replace(/<b>/gi, "[b]"); |
370 | html = html.replace(/<i>/gi, "[i]"); |
371 | html = html.replace(/<u>/gi, "[u]"); |
372 | html = html.replace(/<\/b>/gi, "[/b]"); |
373 | html = html.replace(/<\/i>/gi, "[/i]"); |
374 | html = html.replace(/<\/u>/gi, "[/u]"); |
375 | html = html.replace(/<em>/gi, "[b]"); |
376 | html = html.replace(/<\/em>/gi, "[/b]"); |
377 | html = html.replace(/<strong>/gi, "[b]"); |
378 | html = html.replace(/<\/strong>/gi, "[/b]"); |
379 | html = html.replace(/<cite>/gi, "[i]"); |
380 | html = html.replace(/<\/cite>/gi, "[/i]"); |
381 | html = html.replace(/<font color="(.*?)">(.*?)<\/font>/gmi, "[color=$1]$2[/color]"); |
382 | html = html.replace(/<font color=(.*?)>(.*?)<\/font>/gmi, "[color=$1]$2[/color]"); |
383 | html = html.replace(/<link(.*?)>/gi, ""); |
384 | html = html.replace(/<li(.*?)>(.*?)<\/li>/gi, "[*]$2"); |
385 | html = html.replace(/<ul(.*?)>/gi, "[list]"); |
386 | html = html.replace(/<\/ul>/gi, "[/list]"); |
387 | html = html.replace(/<div>/gi, "\n"); |
388 | html = html.replace(/<\/div>/gi, "\n"); |
389 | html = html.replace(/<td(.*?)>/gi, " "); |
390 | html = html.replace(/<tr(.*?)>/gi, "\n"); |
391 | |
392 | html = html.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, "[img]$2[/img]"); |
393 | html = html.replace(/<a(.*?)href="(.*?)"(.*?)>(.*?)<\/a>/gi, "[url=$2]$4[/url]"); |
394 | |
395 | html = html.replace(/<head>(.*?)<\/head>/gmi, ""); |
396 | html = html.replace(/<object>(.*?)<\/object>/gmi, ""); |
397 | html = html.replace(/<script(.*?)>(.*?)<\/script>/gmi, ""); |
398 | html = html.replace(/<style(.*?)>(.*?)<\/style>/gmi, ""); |
399 | html = html.replace(/<title>(.*?)<\/title>/gmi, ""); |
400 | html = html.replace(/<!--(.*?)-->/gmi, "\n"); |
401 | |
402 | html = html.replace(/\/\//gi, "/"); |
403 | html = html.replace(/http:\//gi, "http://"); |
404 | |
405 | html = html.replace(/<(?:[^>'"]*|(['"]).*?\1)*>/gmi, ""); |
406 | html = html.replace(/\r\r/gi, ""); |
407 | html = html.replace(/\[img]\//gi, "[img]"); |
408 | html = html.replace(/\[url=\//gi, "[url="); |
409 | |
410 | html = html.replace(/(\S)\n/gi, "$1 "); |
411 | |
412 | return html; |
413 | } |
414 | |
415 | function descriptionHandler() |
416 | { |
417 | const descriptionElem = document.querySelector('#torrent-description'); |
418 | if (!descriptionElem) { |
419 | return; |
420 | } |
421 | |
422 | console.log(bbcodeConverter(descriptionElem.innerHTML)); |
423 | |
424 | const button = document.createElement('button'); |
425 | button.textContent = '📋 Copy description as BBCode'; |
426 | button.setAttribute('class', 'btn btn-primary'); |
427 | button.addEventListener('click', function() { |
428 | const origText = button.textContent; |
429 | |
430 | button.textContent = '✔ Copied!'; |
431 | button.setAttribute('disabled', '1'); |
432 | |
433 | const descriptionBBcode = bbcodeConverter(descriptionElem.innerHTML).trim(); |
434 | GM_setClipboard(descriptionBBcode); |
435 | |
436 | setTimeout( |
437 | () => { |
438 | button.textContent = origText; |
439 | button.removeAttribute('disabled'); |
440 | }, 1500 |
441 | ); |
442 | }); |
443 | |
444 | const panelHeading = document.createElement('div'); |
445 | panelHeading.setAttribute('class', 'panel-heading'); |
446 | panelHeading.insertAdjacentElement('beforeend', button); |
447 | |
448 | descriptionElem.insertAdjacentElement('beforebegin', panelHeading); |
449 | } |
450 | |
451 | /** |
452 | * Adds a clipboard icon (using Font Awesome 4) to each row that is marked for "mass copy". |
453 | */ |
454 | function addClipboardIconToRow() |
455 | { |
456 | const rows = document.querySelectorAll('.torrent-list > tbody > tr > td[colspan="2"]'); |
457 | |
458 | let currentAmount = 0; |
459 | |
460 | for (const row of rows) |
461 | { |
462 | currentAmount++; |
463 | const iconExists = row.querySelector('a.clipboard-icon'); |
464 | if (iconExists) { |
465 | iconExists.remove(); |
466 | } |
467 | |
468 | // Check if this row is past the point of "don't receive icon" |
469 | if (currentAmount > copyAmount) { |
470 | // `continue`, not `break` - If we break, then when we lower `copyAmount`, there will be extra rows with clipboard icon. |
471 | continue; |
472 | } |
473 | |
474 | row.insertAdjacentHTML('afterbegin', '<a href="#" class="comments clipboard-icon" style="margin-left: 0.5em;"><i class="fa fa-clipboard"></i></a>'); |
475 | } |
476 | } |
477 | |
478 | /** |
479 | * Adds buttons and number input fields above torrent table, for "mass copying" links/magnets. |
480 | */ |
481 | function torrentListViewHandler() |
482 | { |
483 | const parentElement = document.querySelector('.table-responsive'); |
484 | |
485 | if (!parentElement) { |
486 | return; |
487 | } |
488 | |
489 | addClipboardIconToRow(); |
490 | |
491 | /** |
492 | * Start: Copy all links/magnets |
493 | */ |
494 | const element = document.createElement('button'); |
495 | element.innerHTML = '📋 Copy all download links <i class="fa fa-fw fa-download"></i>'; |
496 | element.setAttribute('class', 'btn btn-default'); |
497 | element.addEventListener('click', function() { |
498 | const origText = element.innerHTML; |
499 | |
500 | element.innerHTML = '✔'; |
501 | element.setAttribute('disabled', '1'); |
502 | copyLinks(); |
503 | |
504 | setTimeout(() => { |
505 | element.innerHTML = origText; |
506 | element.removeAttribute('disabled'); |
507 | }, 1500); |
508 | }); |
509 | |
510 | const magnetCopy = document.createElement('button'); |
511 | magnetCopy.innerHTML = '📋 Copy all magnets <i class="fa fa-fw fa-magnet"></i>'; |
512 | magnetCopy.setAttribute('class', 'btn btn-default'); |
513 | magnetCopy.addEventListener('click', function() { |
514 | const origText = magnetCopy.innerHTML; |
515 | |
516 | magnetCopy.innerHTML = '✔'; |
517 | magnetCopy.setAttribute('disabled', '1'); |
518 | copyMagnets(); |
519 | |
520 | setTimeout(() => { |
521 | magnetCopy.innerHTML = origText; |
522 | magnetCopy.removeAttribute('disabled'); |
523 | }, 1500); |
524 | }); |
525 | |
526 | /** |
527 | * End: Copy all links/magnets |
528 | */ |
529 | |
530 | /** |
531 | * Start: Copy X links/magnets |
532 | */ |
533 | const linkAmount = (getLinks()).length; |
534 | copyAmount = linkAmount < 5 ? linkAmount : 5; |
535 | const amountSelect = document.createElement('input'); |
536 | amountSelect.setAttribute('type', 'number'); |
537 | amountSelect.setAttribute('min', '2'); |
538 | amountSelect.setAttribute('max', linkAmount); |
539 | amountSelect.setAttribute('value', copyAmount); |
540 | amountSelect.setAttribute('class', 'pull-right'); |
541 | |
542 | const amountCopyLinks = document.createElement('button'); |
543 | amountCopyLinks.innerHTML = `📋 Copy ${copyAmount} download links <i class="fa fa-fw fa-download"></i>`; |
544 | amountCopyLinks.setAttribute('class', 'btn btn-default pull-right'); |
545 | amountCopyLinks.addEventListener('click', function() { |
546 | const origText = amountCopyLinks.innerHTML; |
547 | |
548 | amountCopyLinks.innerHTML = '✔'; |
549 | amountCopyLinks.setAttribute('disabled', '1'); |
550 | copyLinks(copyAmount); |
551 | |
552 | setTimeout(() => { |
553 | amountCopyLinks.innerHTML = origText; |
554 | amountCopyLinks.removeAttribute('disabled'); |
555 | }, 1500); |
556 | }); |
557 | |
558 | const amountCopyMagnets = document.createElement('button'); |
559 | amountCopyMagnets.innerHTML = `📋 Copy ${copyAmount} magnets <i class="fa fa-fw fa-magnet"></i>`; |
560 | amountCopyMagnets.setAttribute('class', 'btn btn-default pull-right'); |
561 | amountCopyMagnets.addEventListener('click', function() { |
562 | const origText = amountCopyMagnets.innerHTML; |
563 | |
564 | amountCopyMagnets.innerHTML = '✔'; |
565 | amountCopyMagnets.setAttribute('disabled', '1'); |
566 | copyMagnets(copyAmount); |
567 | |
568 | setTimeout(() => { |
569 | amountCopyMagnets.innerHTML = origText; |
570 | amountCopyMagnets.removeAttribute('disabled'); |
571 | }, 1500); |
572 | }); |
573 | |
574 | amountSelect.addEventListener('change', function() { |
575 | copyAmount = parseInt(amountSelect.value, 10); |
576 | amountCopyLinks.innerHTML = `📋 Copy ${copyAmount} download links <i class="fa fa-fw fa-download"></i>`; |
577 | amountCopyMagnets.innerHTML = `📋 Copy ${copyAmount} magnets <i class="fa fa-fw fa-magnet"></i>`; |
578 | |
579 | addClipboardIconToRow(); |
580 | }); |
581 | /** |
582 | * End: Copy X links/magnets |
583 | */ |
584 | |
585 | /** |
586 | * Append all these buttons and inputs to the DOM. |
587 | */ |
588 | parentElement.insertAdjacentElement('beforebegin', element); |
589 | parentElement.insertAdjacentElement('beforebegin', magnetCopy); |
590 | |
591 | parentElement.insertAdjacentElement('beforebegin', amountSelect); |
592 | parentElement.insertAdjacentElement('beforebegin', amountCopyMagnets); |
593 | parentElement.insertAdjacentElement('beforebegin', amountCopyLinks); |
594 | } |
595 | |
596 | /** |
597 | * This previously changed some `.container` classes to `.container-fluid`, |
598 | * but since that breaks other userscripts, such as NyaaBlue from SeaDex, |
599 | * I figured it'd be better to simply override the CSS width that .container usually applies. |
600 | */ |
601 | function widerContainers() |
602 | { |
603 | const styleDoc = document.createElement('style'); |
604 | styleDoc.setAttribute('type', 'text/css'); |
605 | |
606 | styleDoc.textContent = ` |
607 | .container { |
608 | width: auto !important; |
609 | } |
610 | `; |
611 | |
612 | document.head.insertAdjacentElement('beforeend', styleDoc); |
613 | } |
614 | |
615 | function init() { |
616 | changeToCopy(); |
617 | titleHandler(); |
618 | descriptionHandler(); |
619 | copyFilenameHandler(); |
620 | torrentListViewHandler(); |
621 | calculateTotalSize(); |
622 | widerContainers(); |
623 | } |
624 | |
625 | init(); |