Last active 1753547450

Miscelleanous tweaks applied to Nyaa.si

Revision 594286636fdd90e4bb14c67a5f64c8706a0db179

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