Skip to content

Instantly share code, notes, and snippets.

@Xorboo
Last active March 22, 2026 14:53
Show Gist options
  • Select an option

  • Save Xorboo/4bb4b9a33fd3379ccf8cdc57ad2fa6c5 to your computer and use it in GitHub Desktop.

Select an option

Save Xorboo/4bb4b9a33fd3379ccf8cdc57ad2fa6c5 to your computer and use it in GitHub Desktop.
Trakt.tv torrent icon
// ==UserScript==
// @name Trakt.tv quick torrent search
// @namespace http://tampermonkey.net/
// @version 2.3
// @description Quickly open torrent search for the episode/movie. Works on Progress, Watchlist, and Discover pages.
// @author Xorboo
// @match https://trakt.tv/*
// @match https://www.trakt.tv/*
// @match https://app.trakt.tv/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- URL builder ---
function buildSearchURL(title, season, episode) {
const cleanTitle = title
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '+')
.trim();
if (season != null && episode != null) {
const s = season.toString().padStart(2, '0');
const e = episode.toString().padStart(2, '0');
return `https://1337x.to/search/${cleanTitle}+s${s}e${e}/1/`;
}
return `https://1337x.to/search/${cleanTitle}/1/`;
}
// --- Info extraction ---
function extractCardInfo(card) {
// Primary: footer title element (progress/watchlist cards)
let title = card.querySelector('.trakt-card-title')?.textContent.trim();
// Fallback: popup menu button label attr (discover cards have no footer title)
// label="Pop up menu for "Jarhead"" → extract the quoted name
if (!title) {
const menuBtn = card.querySelector('.trakt-popup-menu-button');
if (menuBtn) {
const label = menuBtn.getAttribute('label') || '';
const match = label.match(/^Pop up menu for "(.+)"$/i);
if (match) title = match[1].trim();
}
}
if (!title) return null;
// Episode info from subtitle (e.g. "S1 • E8 - Episode Title")
const subtitleEl = card.querySelector('.trakt-card-subtitle');
let season = null, episode = null;
if (subtitleEl) {
const match = subtitleEl.textContent.match(/S(\d+)\s*[·]\s*E(\d+)/i);
if (match) {
season = parseInt(match[1]);
episode = parseInt(match[2]);
}
}
return { title, season, episode };
}
// --- Button factory ---
function createButton(url) {
const btn = document.createElement('a');
btn.className = 'trakt-torrent-search-btn';
btn.href = url;
btn.target = '_blank';
btn.rel = 'noopener noreferrer';
btn.title = 'Search torrent';
btn.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
cursor: pointer;
color: #aa3333;
text-decoration: none;
flex-shrink: 0;
border-radius: 4px;
transition: background 0.2s ease, color 0.2s ease;
`;
btn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg" style="pointer-events:none">
<path d="M12 3v13M6 11l6 6 6-6" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 20h16" stroke="currentColor" stroke-width="2"
stroke-linecap="round"/>
</svg>
`;
btn.addEventListener('mouseenter', () => {
btn.style.background = '#aa3333';
btn.style.color = '#ffffff';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = 'transparent';
btn.style.color = '#aa3333';
});
return btn;
}
// --- Card handlers ---
function handleFooterActionCard(card) {
const info = extractCardInfo(card);
if (!info) return false;
const footerAction = card.querySelector('.trakt-card-footer-action');
if (!footerAction) return false;
const url = buildSearchURL(info.title, info.season, info.episode);
const btn = createButton(url);
footerAction.style.cssText = 'display:flex; align-items:center; gap:4px;';
footerAction.insertBefore(btn, footerAction.firstChild);
return true;
}
function handleDiscoverCard(card) {
const info = extractCardInfo(card);
if (!info) return false;
const tagBar = card.querySelector('.trakt-card-footer-tag .trakt-tag-bar');
if (!tagBar) return false;
const url = buildSearchURL(info.title, info.season, info.episode);
tagBar.appendChild(createButton(url));
return true;
}
function handleSummaryCard(card) {
const info = extractCardInfo(card);
if (!info) return false;
const bottomBar = card.querySelector('.trakt-summary-card-bottom-bar');
if (!bottomBar) return false;
const url = buildSearchURL(info.title, info.season, info.episode);
const btn = createButton(url);
btn.style.marginLeft = '8px';
bottomBar.appendChild(btn);
return true;
}
// --- Card type detection & dispatch ---
function processCard(card) {
if (card.dataset.traktTorrentDone) return;
if (card.querySelector('.trakt-torrent-search-btn')) {
card.dataset.traktTorrentDone = '1';
return;
}
let success = false;
if (card.classList.contains('trakt-summary-card')) {
success = handleSummaryCard(card);
} else if (card.querySelector('.trakt-card-footer-tag .trakt-tag-bar')) {
success = handleDiscoverCard(card);
} else if (card.querySelector('.trakt-card-footer-action')) {
success = handleFooterActionCard(card);
}
if (success) {
card.dataset.traktTorrentDone = '1';
}
}
function processAllCards() {
document.querySelectorAll('.trakt-card').forEach(processCard);
}
// --- SPA navigation detection ---
// SvelteKit uses history.pushState for navigation — no page reload fires.
// We patch pushState/replaceState and listen to popstate to detect URL changes,
// then wait briefly for Svelte to render the new page content before scanning.
let navigationTimer = null;
function onNavigate() {
// Debounce: Svelte may call pushState multiple times during a single navigation.
// We also need to wait for the new DOM to render before scanning.
clearTimeout(navigationTimer);
navigationTimer = setTimeout(processAllCards, 300);
}
function patchHistoryMethod(method) {
const original = history[method];
history[method] = function (...args) {
original.apply(this, args);
onNavigate();
};
}
patchHistoryMethod('pushState');
patchHistoryMethod('replaceState');
window.addEventListener('popstate', onNavigate);
// --- MutationObserver ---
// Catches cards added dynamically (infinite scroll, lazy sections, etc.)
// Debounced scan — batches rapid mutations into a single processAllCards call
let scanTimer = null;
function scheduleScan() {
clearTimeout(scanTimer);
scanTimer = setTimeout(processAllCards, 100);
}
function initObserver() {
const observer = new MutationObserver(scheduleScan);
observer.observe(document.body, { childList: true, subtree: true });
}
// --- Init ---
function init() {
processAllCards();
initObserver();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment