Last active 1753547450

Miscelleanous tweaks applied to Nyaa.si

Revision 744fbf0265aee10c41caae26322014bdd39dbb90

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