¡Hola, Invitado! (Iniciar sesiónRegístrate)
Hora: 11 Jan 2026, 00:38

[Userscript] Botón de descarga de Torrent en ficha de Fansub

22 Dec 2025, 04:45 (Este mensaje fue modificado por última vez en: 22 Dec 2025 16:30 por Johange)
Mensaje: #1

[Userscript] Botón de descarga de Torrent en ficha de Fansub

Hola, pues navegando por el foro, me encontré de que no hay manera de filtrar los torrents de un fansub específico a menos que te sepas el nombre del torrent que buscas. Y, además, que para descargar dicho torrent, hay que entrar a la ficha de este. Podrán llamarme perezoso, pero siempre me ha gustado ahorrarme la mayor cantidad de clicks posibles. jajajaja

Pero bien, al grano. Lo que busco con este script es evitarme todo eso, y usar el foro donde sí puedes filtrar los grupos. Lo que hace el script es tomar la página del fansub en el foro, y enlazarlo directamente con su torrent mediante un pequeño botón. Me ha costado mucho hacerlo porque no entiendo para nada programación. :v
Pero creo que el resultado fue algo muy decente, no sé si estará del todo bien, pero me funciona, y espero que a ustedes también.


Muestra:
Mostrar
chrome-tv6t4-Df-TMw

Instalación:
  1. Instala Tampermonkey para Chrome
  2. Instala UFTorrentButton
  3. Y listo
    Ya puedes descargar los torrents de tu fansub favorito en http://foro.unionfansub.com/fansubs.php

    Debería funcionar en Anime, Hentai, Manga, Música, Doramas, etc...


Para los que sí son programadores:
Este script lo hice con Chatgpt, así que no me interesa ningún crédito, puedes tomarlo como una idea, modificarlo, mejorarlo, rehacerlo y publicarlo con toda confianza. Solo me pareció algo útil y quise compartirlo con todos. Espero que les haya gustado. Y, sobretodo, espero que no afecte de ninguna manera al foro o a su seguridad, es lo que menos quiero, así que si es causante de algo de eso, por favor, eliminarlo.

Mucho texto:
Mostrar
De hecho, este script se me ocurrió porque estaba buscando la manera de descargar varios mangas de X grupo, pero son demasiados, y para hacerlo hay que entrar a cada ficha, y es superdemorado eso, así que acá estamos. Ohnoes" title="Ohnoes




Actualización:
- Actualizado a versión 2.0.0 gracias a @Kumicho
22 Dec 2025, 11:09
Mensaje: #2

RE: [Userscript] Botón de descarga de Torrent en ficha de Fansub

Messirve
22 Dec 2025, 13:59
Mensaje: #3

RE: [Userscript] Botón de descarga de Torrent en ficha de Fansub

Nunca uso ChatGPT, porque no es demasiado bueno escribiendo código.

Código:
// ==UserScript==
// @name UFTorrentButton
// @namespace uf-torrent-fansubs
// @version 2.0.0
// @description Agrega botón "Torrent" por ficha en fansubs.php sin modificar el layout.
// @match http://foro.unionfansub.com/fansubs.php*
// @match https://foro.unionfansub.com/fansubs.php*
// @[url=http://foro.unionfansub.com/member.php?action=profile&uid=41112]grant[/url] none
// @run-at document-idle
// @downloadURL https://update.greasyfork.org/scripts/559772/UFTorrentButton.user.js
// @updateURL https://update.greasyfork.org/scripts/559772/UFTorrentButton.meta.js
// ==/UserScript==

(function () {
'use strict';

const CACHE_KEY = 'UF_TORRENT_ID_CACHE_v5';
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
const MAX_RETRIES = 2;

let memoryCache = null;

/* -------------------- CACHE (Optimizada) -------------------- */

function loadCache() {
if (memoryCache) return memoryCache;

try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) return (memoryCache = {});

const now = Date.now();
const data = JSON.parse(raw);
const valid = {};

for (const tid in data) {
const entry = data[tid];
if (entry.expiry > now) {
valid[tid] = entry.torrentId;
}
}

// Solo guardar si hay diferencia
if (Object.keys(valid).length !== Object.keys(data).length) {
saveCache(valid);
}

return (memoryCache = valid);
} catch {
return (memoryCache = {});
}
}

function saveCache(cache) {
const now = Date.now();
const out = {};

for (const tid in cache) {
out[tid] = {
torrentId: cache[tid],
expiry: now + CACHE_TTL
};
}

localStorage.setItem(CACHE_KEY, JSON.stringify(out));
memoryCache = cache;
}

function updateCache(tid, torrentId) {
const cache = loadCache();
cache[tid] = torrentId;
saveCache(cache);
}

/* -------------------- UTILIDADES (Mejoradas) -------------------- */

function extractTid(url) {
// Más rápido que regex cuando sea posible
try {
const urlObj = new URL(url, location.href);
return urlObj.searchParams.get('tid');
} catch {
const m = url.match(/[?&]tid=(\d+)/);
return m ? m[1] : null;
}
}

function extractTorrentId(html) {
// Regex más rápido para el caso común
let m = html.match(/torrent=(\d+)/i);
if (m) return m[1];

m = html.match(/id=(\d+)/i);
if (m) return m[1];

// Fallback a DOMParser solo si es necesario
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const links = doc.querySelectorAll('a[href*="torrent.unionfansub.com"]');

for (const a of links) {
const href = a.href || a.getAttribute('href') || '';
m = href.match(/torrent=(\d+)/i);
if (m) return m[1];
m = href.match(/id=(\d+)/i);
if (m) return m[1];
}
} catch {
// Silently fail
}

return null;
}

async function fetchTorrentId(threadUrl, tid, retry = 0) {
try {
const res = await fetch(threadUrl, {
credentials: 'include',
cache: 'no-store' // Evitar caché del navegador
});

if (!res.ok) {
if (retry < MAX_RETRIES) {
await new Promise(r => setTimeout(r, 300 * (retry + 1)));
return fetchTorrentId(threadUrl, tid, retry + 1);
}
return null;
}

const html = await res.text();
const id = extractTorrentId(html);

if (id && tid) updateCache(tid, id);
return id;
} catch {
if (retry < MAX_RETRIES) {
await new Promise(r => setTimeout(r, 300 * (retry + 1)));
return fetchTorrentId(threadUrl, tid, retry + 1);
}
return null;
}
}

/* -------------------- BOTÓN (Mejor UX) -------------------- */

function createButton(threadUrl) {
const btn = document.createElement('button');
btn.className = 'uf-torrent-btn';
btn.textContent = 'Torrent';
btn.title = 'Descargar torrent';
btn.setAttribute('type', 'button');

btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();

if (btn.disabled) return;

const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = '...';

try {
const tid = extractTid(threadUrl);
const cache = loadCache();
let torrentId = tid ? cache[tid] : null;

if (!torrentId) {
torrentId = await fetchTorrentId(threadUrl, tid);
}

if (torrentId) {
window.open(
`https://torrent.unionfansub.com/download.php?torrent=${torrentId}`,
'_blank',
'noopener,noreferrer'
);
} else {
alert('No se encontró enlace de torrent en este hilo.');
}
} catch (error) {
console.error('[UF Torrent] Error:', error);
alert('Error al obtener el torrent.');
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
});

return btn;
}

/* -------------------- DOM (Optimizada) -------------------- */

function processHolder(holder) {
if (holder.dataset.torrentProcessed) return;
holder.dataset.torrentProcessed = 'true';

const link = holder.querySelector('a.threadlink[href*="showthread.php"]');
if (!link || holder.querySelector('.uf-torrent-btn')) return;

const threadUrl = new URL(link.href, location.href).href;
holder.appendChild(createButton(threadUrl));
}

function scanInitial() {
// Usar querySelectorAll una sola vez
const holders = document.querySelectorAll('span.threadholder:not([data-torrent-processed])');
holders.forEach(processHolder);
}

function observeMutations() {
const observer = new MutationObserver((mutations) => {
// Procesar en batch para mejor performance
const holdersToProcess = new Set();

for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;

if (node.matches?.('span.threadholder')) {
holdersToProcess.add(node);
}

const holders = node.querySelectorAll?.('span.threadholder:not([data-torrent-processed])');
holders?.forEach(h => holdersToProcess.add(h));
}
}

holdersToProcess.forEach(processHolder);
});

observer.observe(document.body, {
childList: true,
subtree: true
});
}

/* -------------------- ESTILOS (Mejorados) -------------------- */

function injectStyles() {
if (document.getElementById('uf-torrent-styles')) return;

const css = `
span.threadholder {
position: relative !important;
}

.uf-torrent-btn {
position: absolute;
top: 6px;
right: 6px;
padding: 4px 8px;
border-radius: 7px;
border: 1px solid rgba(0,0,0,.25);
background: rgba(255,255,255,.92);
cursor: pointer;
font-size: 12px;
z-index: 999;
line-height: 1;
transition: all 0.15s ease;
user-select: none;
}

.uf-torrent-btn:hover {
background: rgba(255,255,255,.95);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

.uf-torrent-btn:active {
transform: translateY(1px);
}

.uf-torrent-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
`;

const style = document.createElement('style');
style.id = 'uf-torrent-styles';
style.textContent = css;
document.head.appendChild(style);
}

/* -------------------- INICIO -------------------- */

function start() {
injectStyles();
scanInitial();
observeMutations();
}

// Iniciar inmediatamente si el DOM está listo
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
setTimeout(start, 0); // Usar setTimeout para no bloquear
}
})();

Larga vida a MS y los chinos. Aunque admito no haber comprobado si el código funciona.

Se supone que para el servidor no hay diferencia, pero a nivel local es evidente que es bastante mejor.
  • La nueva versión es más rápida porque solo procesa las fichas nuevas, no toda la página.
  • Evita trabajo repetido marcando cada ficha como procesada.
  • Usa un sistema de caché más limpio y eficiente.
  • El botón funciona mejor y evita clics dobles.
  • El código es más estable y gasta menos recursos del navegador.
22 Dec 2025, 16:31
Mensaje: #4

RE: [Userscript] Botón de descarga de Torrent en ficha de Fansub

Muchas gracias por las mejoras, Kumicho. Ya actualicé el script con las nuevas modificaciones.


Usuario(s) navegando en este tema: 1 invitado(s)