Last active 1753547450

Miscelleanous tweaks applied to Nyaa.si

Revision 0b187b9456e9ec4ee0ba9f7e1a9b717355e9364c

nyaa-shit.user.js Raw
1// ==UserScript==
2// @name Nyaa - Personal Tweaks
3// @namespace github.com/Decicus
4// @match https://nyaa.si/*
5// @grant GM_setClipboard
6// @version 1.3.1
7// @author Decicus
8// @description Adds some extra functionality to Nyaa.
9// ==/UserScript==
10
11let copyAmount = 5;
12const multipliers = {
13 Bytes: 1,
14 KiB: 1024,
15 MiB: 1024 * 1024,
16 GiB: 1024 * 1024 * 1024,
17 TiB: 1024 * 1024 * 1024 * 1024,
18};
19
20const sizesRegex = /(Bytes|KiB|MiB|GiB|TiB)/g;
21
22function 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
30function getLinks()
31{
32 return getElements('download');
33}
34
35function 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 */
43function 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 console.log(totalBytes);
64 }
65
66 const multiplierKeys = Object.keys(multipliers).reverse();
67 let totalSize = `${totalBytes} Bytes`;
68 for (const key of multiplierKeys)
69 {
70 if (totalBytes === 0) {
71 break;
72 }
73
74 const divideBy = multipliers[key];
75 const result = totalBytes / divideBy;
76 if (result > 1) {
77 totalSize = `${result.toFixed(2)} ${key}`;
78 break;
79 }
80 }
81
82 const torrentList = document.querySelector('.torrent-list tbody');
83 if (!torrentList) {
84 return;
85 }
86
87 const html = `<tr class="default" id="all-torrents-size">
88 <td class="text-center">
89 <a href="#all-torrents-size" title="Total size">
90 💽
91 </a>
92 </td>
93 <td colspan="2">
94 <a href="#all-torrents-size">Total size</a>
95 </td>
96 <td class="text-center"><!-- Download --></td>
97 <td class="text-center">${totalSize}</td>
98 <td class="text-center"><!-- Timestamp --></td>
99 <td class="text-center"><!-- Seeders --></td>
100 <td class="text-center"><!-- Leechers --></td>
101 <td class="text-center"><!-- Completed --></td>
102 </tr>`;
103
104 torrentList.insertAdjacentHTML('beforeend', html);
105}
106
107/**
108 * Handler for copying download links (torrents)
109 */
110function copyLinks(amount)
111{
112 let links = getLinks();
113 if (amount && typeof amount === 'number') {
114 amount = amount > links.length ? links.length : amount;
115 links = links.slice(0, amount);
116 }
117
118 const list = links.join('\n');
119 GM_setClipboard(list);
120}
121
122/**
123 * Handler for copying magnets
124 */
125function copyMagnets(amount)
126{
127 let magnets = getMagnets();
128 if (amount && typeof amount === 'number') {
129 amount = amount > magnets.length ? magnets.length : amount;
130 magnets = magnets.slice(0, amount);
131 }
132
133 const list = magnets.join('\n');
134 GM_setClipboard(list);
135}
136
137/**
138 * Changes the download links for torrents/magnets to trigger a copy,
139 * instead of default handling (download for torrents, torrent *client* for magnets).
140 */
141function copyOnClick(ev)
142{
143 ev.preventDefault();
144
145 const target = ev.target;
146 const isAnchor = target.nodeName === 'A';
147 const parent = isAnchor ? target : target.parentElement;
148 GM_setClipboard(parent.href + '\n');
149
150 const checkIcon = 'fa-check';
151 const originalIcon = parent.href.includes('/download/') ? 'fa-download' : 'fa-magnet';
152
153 const iconElement = !target.classList.contains('fa') ? target.querySelector('.fa') : target;
154 console.log(iconElement);
155
156 if (iconElement) {
157 iconElement.classList.replace(originalIcon, checkIcon);
158
159 setTimeout(
160 function() {
161 iconElement.classList.replace(checkIcon, originalIcon);
162 },
163 1500
164 );
165 }
166}
167
168/**
169 * Changes said magnet/torrent links to use the `copyOnClick` handler.
170 */
171function changeToCopy()
172{
173 const links = document.querySelectorAll('.fa-magnet, .fa-download');
174
175 for (const link of links)
176 {
177 link.parentElement.addEventListener('click', copyOnClick);
178 }
179}
180
181/**
182 * List of regex replacements so that I can copy titles as directory names, with my personally preferred naming scheme.
183 */
184const titleReplacements = [
185 {
186 comment: 'Remove quoted series/movie title from the end',
187 search: / \"[A-z0-9-!\s]+\"$/,
188 replacement: '',
189 },
190 {
191 comment: 'Move release group to the end',
192 search: /^\[([-\w ]+)\] (.+)$/,
193 replacement: '$2 [$1]',
194 },
195 {
196 comment: 'Colon space to space dash space',
197 search: /: /g,
198 replacement: ' - ',
199 },
200 {
201 comment: 'Season naming so Sonarr doesnt get confused because its dumb',
202 search: / \(Season (\d+)\)/g,
203 replacement: ' - S$1',
204 },
205 {
206 comment: 'No slashes, please',
207 search: /\//g,
208 replacement: '-',
209 },
210 {
211 comment: 'Closing => Opening bracket spacing between each "tag"',
212 search: /\]\[/g,
213 replacement: '] [',
214 },
215 {
216 comment: 'Final replacement: Replace 2+ spaces with one space',
217 search: /\s{2,}/g,
218 replacement: ' ',
219 },
220];
221
222/**
223 * Uses `titleReplacements` to "fix" torrent name when "Copy torrent name" button is clicked.
224 */
225function fixTitle(input)
226{
227 let finalString = input.trim();
228 for (const replace of titleReplacements)
229 {
230 const { comment, search, replacement } = replace;
231
232 const match = finalString.match(search);
233 if (match === null) {
234 continue;
235 }
236
237 console.log(`\nFound: ${search}\nComment: ${comment}\nMatches:\n- ${match.join('\n- ')}`);
238 finalString = finalString.replace(search, replacement || '');
239 }
240
241 // Deal with any excess whitespace
242 finalString = finalString.trim();
243 console.log(`Original input: ${input}\nFinal string: ${finalString}`);
244
245 return finalString;
246}
247
248/**
249 * Adds copy button for torrent titles on torrent pages
250 * Implements `fixTitle()`
251 */
252function titleHandler()
253{
254 const title = document.querySelector('h3.panel-title');
255
256 if (!title) {
257 return;
258 }
259
260 const titleParent = title.parentElement;
261
262 const buttonParent = document.createElement('div');
263 buttonParent.setAttribute('class', 'btn-group pull-right');
264
265 const button = document.createElement('button');
266 button.textContent = '📋';
267 button.setAttribute('class', 'btn btn-primary btn-sm');
268 button.addEventListener('click', function() {
269 const origText = button.textContent;
270
271 button.textContent = '✔';
272 button.setAttribute('disabled', '1');
273
274 const fixedTitle = fixTitle(title.textContent.trim());
275 GM_setClipboard(fixedTitle);
276
277 setTimeout(
278 () => {
279 button.textContent = origText;
280 button.removeAttribute('disabled');
281 }, 1500
282 );
283 });
284
285 titleParent.classList.add('clearfix');
286 buttonParent.insertAdjacentElement('afterbegin', button);
287 titleParent.insertAdjacentElement('afterbegin', buttonParent);
288}
289
290function bbcodeConverter(html) {
291 html = html.replace(/<pre(.*?)>(.*?)<\/pre>/gmi, "[code]$2[/code]");
292 html = html.replace(/<h[1-7](.*?)>(.*?)<\/h[1-7]>/, "\n[h]$2[/h]\n");
293 //paragraph handling:
294 //- if a paragraph opens on the same line as another one closes, insert an extra blank line
295 //- opening tag becomes two line breaks
296 //- closing tags are just removed
297 // html += html.replace(/<\/p><p/<\/p>\n<p/gi;
298 // html += html.replace(/<p[^>]*>/\n\n/gi;
299 // html += html.replace(/<\/p>//gi;
300
301 html = html.replace(/<\/p>\n<p>/gi, "\n\n\n");
302
303 html = html.replace(/<br(.*?)>/gi, "\n");
304 html = html.replace(/<textarea(.*?)>(.*?)<\/textarea>/gmi, "\[code]$2\[\/code]");
305 html = html.replace(/<b>/gi, "[b]");
306 html = html.replace(/<i>/gi, "[i]");
307 html = html.replace(/<u>/gi, "[u]");
308 html = html.replace(/<\/b>/gi, "[/b]");
309 html = html.replace(/<\/i>/gi, "[/i]");
310 html = html.replace(/<\/u>/gi, "[/u]");
311 html = html.replace(/<em>/gi, "[b]");
312 html = html.replace(/<\/em>/gi, "[/b]");
313 html = html.replace(/<strong>/gi, "[b]");
314 html = html.replace(/<\/strong>/gi, "[/b]");
315 html = html.replace(/<cite>/gi, "[i]");
316 html = html.replace(/<\/cite>/gi, "[/i]");
317 html = html.replace(/<font color="(.*?)">(.*?)<\/font>/gmi, "[color=$1]$2[/color]");
318 html = html.replace(/<font color=(.*?)>(.*?)<\/font>/gmi, "[color=$1]$2[/color]");
319 html = html.replace(/<link(.*?)>/gi, "");
320 html = html.replace(/<li(.*?)>(.*?)<\/li>/gi, "[*]$2");
321 html = html.replace(/<ul(.*?)>/gi, "[list]");
322 html = html.replace(/<\/ul>/gi, "[/list]");
323 html = html.replace(/<div>/gi, "\n");
324 html = html.replace(/<\/div>/gi, "\n");
325 html = html.replace(/<td(.*?)>/gi, " ");
326 html = html.replace(/<tr(.*?)>/gi, "\n");
327
328 html = html.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, "[img]$2[/img]");
329 html = html.replace(/<a(.*?)href="(.*?)"(.*?)>(.*?)<\/a>/gi, "[url=$2]$4[/url]");
330
331 html = html.replace(/<head>(.*?)<\/head>/gmi, "");
332 html = html.replace(/<object>(.*?)<\/object>/gmi, "");
333 html = html.replace(/<script(.*?)>(.*?)<\/script>/gmi, "");
334 html = html.replace(/<style(.*?)>(.*?)<\/style>/gmi, "");
335 html = html.replace(/<title>(.*?)<\/title>/gmi, "");
336 html = html.replace(/<!--(.*?)-->/gmi, "\n");
337
338 html = html.replace(/\/\//gi, "/");
339 html = html.replace(/http:\//gi, "http://");
340
341 html = html.replace(/<(?:[^>'"]*|(['"]).*?\1)*>/gmi, "");
342 html = html.replace(/\r\r/gi, "");
343 html = html.replace(/\[img]\//gi, "[img]");
344 html = html.replace(/\[url=\//gi, "[url=");
345
346 html = html.replace(/(\S)\n/gi, "$1 ");
347
348 return html;
349}
350
351function descriptionHandler()
352{
353 const descriptionElem = document.querySelector('#torrent-description');
354 if (!descriptionElem) {
355 return;
356 }
357
358 console.log(bbcodeConverter(descriptionElem.innerHTML));
359
360 const button = document.createElement('button');
361 button.textContent = '📋 Copy description as BBCode';
362 button.setAttribute('class', 'btn btn-primary');
363 button.addEventListener('click', function() {
364 const origText = button.textContent;
365
366 button.textContent = '✔ Copied!';
367 button.setAttribute('disabled', '1');
368
369 const descriptionBBcode = bbcodeConverter(descriptionElem.innerHTML).trim();
370 GM_setClipboard(descriptionBBcode);
371
372 setTimeout(
373 () => {
374 button.textContent = origText;
375 button.removeAttribute('disabled');
376 }, 1500
377 );
378 });
379
380 const panelHeading = document.createElement('div');
381 panelHeading.setAttribute('class', 'panel-heading');
382 panelHeading.insertAdjacentElement('beforeend', button);
383
384 descriptionElem.insertAdjacentElement('beforebegin', panelHeading);
385}
386
387/**
388 * Adds a clipboard icon (using Font Awesome 4) to each row that is marked for "mass copy".
389 */
390function addClipboardIconToRow()
391{
392 const rows = document.querySelectorAll('.torrent-list > tbody > tr > td[colspan="2"]');
393
394 let currentAmount = 0;
395
396 for (const row of rows)
397 {
398 currentAmount++;
399 const iconExists = row.querySelector('a.clipboard-icon');
400 if (iconExists) {
401 iconExists.remove();
402 }
403
404 // Check if this row is past the point of "don't receive icon"
405 if (currentAmount > copyAmount) {
406 // `continue`, not `break` - If we break, then when we lower `copyAmount`, there will be extra rows with clipboard icon.
407 continue;
408 }
409
410 row.insertAdjacentHTML('afterbegin', '<a href="#" class="comments clipboard-icon" style="margin-left: 0.5em;"><i class="fa fa-clipboard"></i></a>');
411 }
412}
413
414/**
415 * Adds buttons and number input fields above torrent table, for "mass copying" links/magnets.
416 */
417function torrentListViewHandler()
418{
419 const parentElement = document.querySelector('.table-responsive');
420
421 if (!parentElement) {
422 return;
423 }
424
425 addClipboardIconToRow();
426
427 /**
428 * Start: Copy all links/magnets
429 */
430 const element = document.createElement('button');
431 element.innerHTML = '📋 Copy all download links <i class="fa fa-fw fa-download"></i>';
432 element.setAttribute('class', 'btn btn-default');
433 element.addEventListener('click', function() {
434 const origText = element.innerHTML;
435
436 element.innerHTML = '✔';
437 element.setAttribute('disabled', '1');
438 copyLinks();
439
440 setTimeout(() => {
441 element.innerHTML = origText;
442 element.removeAttribute('disabled');
443 }, 1500);
444 });
445
446 const magnetCopy = document.createElement('button');
447 magnetCopy.innerHTML = '📋 Copy all magnets <i class="fa fa-fw fa-magnet"></i>';
448 magnetCopy.setAttribute('class', 'btn btn-default');
449 magnetCopy.addEventListener('click', function() {
450 const origText = magnetCopy.innerHTML;
451
452 magnetCopy.innerHTML = '✔';
453 magnetCopy.setAttribute('disabled', '1');
454 copyMagnets();
455
456 setTimeout(() => {
457 magnetCopy.innerHTML = origText;
458 magnetCopy.removeAttribute('disabled');
459 }, 1500);
460 });
461
462 /**
463 * End: Copy all links/magnets
464 */
465
466 /**
467 * Start: Copy X links/magnets
468 */
469 const linkAmount = (getLinks()).length;
470 copyAmount = linkAmount < 5 ? linkAmount : 5;
471 const amountSelect = document.createElement('input');
472 amountSelect.setAttribute('type', 'number');
473 amountSelect.setAttribute('min', '2');
474 amountSelect.setAttribute('max', linkAmount);
475 amountSelect.setAttribute('value', copyAmount);
476 amountSelect.setAttribute('class', 'pull-right');
477
478 const amountCopyLinks = document.createElement('button');
479 amountCopyLinks.innerHTML = `📋 Copy ${copyAmount} download links <i class="fa fa-fw fa-download"></i>`;
480 amountCopyLinks.setAttribute('class', 'btn btn-default pull-right');
481 amountCopyLinks.addEventListener('click', function() {
482 const origText = amountCopyLinks.innerHTML;
483
484 amountCopyLinks.innerHTML = '✔';
485 amountCopyLinks.setAttribute('disabled', '1');
486 copyLinks(copyAmount);
487
488 setTimeout(() => {
489 amountCopyLinks.innerHTML = origText;
490 amountCopyLinks.removeAttribute('disabled');
491 }, 1500);
492 });
493
494 const amountCopyMagnets = document.createElement('button');
495 amountCopyMagnets.innerHTML = `📋 Copy ${copyAmount} magnets <i class="fa fa-fw fa-magnet"></i>`;
496 amountCopyMagnets.setAttribute('class', 'btn btn-default pull-right');
497 amountCopyMagnets.addEventListener('click', function() {
498 const origText = amountCopyMagnets.innerHTML;
499
500 amountCopyMagnets.innerHTML = '✔';
501 amountCopyMagnets.setAttribute('disabled', '1');
502 copyMagnets(copyAmount);
503
504 setTimeout(() => {
505 amountCopyMagnets.innerHTML = origText;
506 amountCopyMagnets.removeAttribute('disabled');
507 }, 1500);
508 });
509
510 amountSelect.addEventListener('change', function() {
511 copyAmount = parseInt(amountSelect.value, 10);
512 amountCopyLinks.innerHTML = `📋 Copy ${copyAmount} download links <i class="fa fa-fw fa-download"></i>`;
513 amountCopyMagnets.innerHTML = `📋 Copy ${copyAmount} magnets <i class="fa fa-fw fa-magnet"></i>`;
514
515 addClipboardIconToRow();
516 });
517 /**
518 * End: Copy X links/magnets
519 */
520
521 /**
522 * Append all these buttons and inputs to the DOM.
523 */
524 parentElement.insertAdjacentElement('beforebegin', element);
525 parentElement.insertAdjacentElement('beforebegin', magnetCopy);
526
527 parentElement.insertAdjacentElement('beforebegin', amountSelect);
528 parentElement.insertAdjacentElement('beforebegin', amountCopyMagnets);
529 parentElement.insertAdjacentElement('beforebegin', amountCopyLinks);
530}
531
532function init() {
533 changeToCopy();
534 titleHandler();
535 descriptionHandler();
536 torrentListViewHandler();
537 calculateTotalSize();
538}
539
540init();