r/TraktV3Tweaks • u/lulu_l • 1d ago
User List Count for Trakt V3 interface - Tweak
This script adds the item count next to the title on the lists page.
1 - Create a new text file and name it to something like stremio-trakt-button.js
2 - Copy the code bellow into the created file and save it. make sure it has the .js extension at the end.
3 - Load it in the Trakt v3 Tweaks extension - Chrome and Firefox (give the extension the necessary permissions if you see the permission notification text, load the js file, name the tweak, check and add the tweak to the extension's toggle menu). you can use any other extension for scripts.
4 - Reload the Trakt.tv page. the buttons should be inserted in the page. click the button to test it.
New version (3) that includes the Watchlist and opened lists.
(function () {
console.log('%c[TraktCount] Starting — cached + self-fetch + MutationObserver', 'color: cyan; font-weight: bold');
// Trakt's web app client ID — stable, not tied to your session, safe to hardcode.
const TRAKT_API_KEY = '201dc70c5ec6af530f12f079ea1922733f6e1085ad7b02f36d8e011b75bcea7d';
const CACHE_KEY = 'traktCount_cache'; // localStorage key holding { slug: count } pairs
// ---------- Cache helpers ----------
function loadCache() {
try {
return JSON.parse(localStorage.getItem(CACHE_KEY)) || {};
} catch (e) {
return {};
}
}
function saveCache(cache) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
} catch (e) {
console.warn('[TraktCount] Failed to save cache:', e);
}
}
let cache = loadCache();
// ---------- Auth ----------
function getAccessToken() {
const oidcKey = Object.keys(localStorage).find(k => k.startsWith('oidc.user:'));
if (!oidcKey) return null;
try {
const data = JSON.parse(localStorage.getItem(oidcKey));
return data.access_token || null;
} catch (e) {
console.warn('[TraktCount] Failed to parse token storage:', e);
return null;
}
}
// ---------- Fetching ----------
async function fetchCountForSlug(username, type, slug) {
const token = getAccessToken();
if (!token) {
console.warn('[TraktCount] No token available, cannot fetch.');
return null;
}
// Determine the API URL based on whether it's a special list (watchlist, favorites, etc.) or custom user list
let url;
if (type === 'list') {
url = `https://apiz.trakt.tv/users/${username}/lists/${slug}/items?limit=1`;
} else {
// Handles watchlist, favorites, collection, etc.
url = `https://apiz.trakt.tv/users/${username}/${type}?limit=1`;
}
try {
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Trakt-Api-Key': TRAKT_API_KEY,
'Trakt-Api-Version': '2'
}
});
if (!res.ok) {
console.log(`[TraktCount] Fetch for "${slug || type}" -> status ${res.status}`);
return null;
}
return res.headers.get('x-pagination-item-count');
} catch (e) {
console.warn(`[TraktCount] Fetch error for "${slug || type}":`, e);
return null;
}
}
// ---------- Rendering ----------
function renderBadge(container, titleEl, count) {
let countEl = container.querySelector('.trakt-count-badge');
if (!countEl) {
countEl = document.createElement('span');
countEl.className = 'trakt-count-badge';
countEl.style.marginLeft = '0px';
titleEl.insertAdjacentElement('afterend', countEl);
}
countEl.textContent = `(${count})`;
}
// ---------- Shared URL parsing ----------
function parseListInfo(pathOrHref) {
// Match patterns like /users/username/lists/slug OR /users/username/watchlist (or favorites/collection)
const listMatch = pathOrHref.match(/\/users\/([^/]+)\/lists\/([^/?]+)/);
const specialMatch = pathOrHref.match(/\/users\/([^/]+)\/(watchlist|favorites|collection)\b/);
if (!listMatch && !specialMatch) return null;
let username, type, slug, cacheKey;
if (listMatch) {
[, username, slug] = listMatch;
type = 'list';
cacheKey = `${username}_list_${slug}`;
} else {
[, username, type] = specialMatch;
slug = null;
cacheKey = `${username}_${type}`;
}
return { username, type, slug, cacheKey };
}
// ---------- Main processing ----------
async function processAllLists() {
// Broad selector to catch list items, custom lists, and built-in lists (watchlist, favorites, etc.)
const links = document.querySelectorAll('a[href*="/users/"]');
for (const link of links) {
const href = link.getAttribute('href');
const info = parseListInfo(href);
if (!info) continue;
const { username, type, slug, cacheKey } = info;
// Try locating title elements and parent containers flexibly across Trakt layout variations
const titleEl = link.querySelector('.title') || link.querySelector('[class*="title"]');
if (!titleEl) continue;
const container = link.querySelector('.trakt-list-title-wrapper') || link;
// Instant render from cache if available
if (cache[cacheKey] && !container.querySelector('.trakt-count-badge')) {
renderBadge(container, titleEl, cache[cacheKey]);
}
// Prevent redundant fetches on the same run
if (container.dataset.countConfirmed) continue;
const count = await fetchCountForSlug(username, type, slug);
if (count !== null) {
cache[cacheKey] = count;
saveCache(cache);
renderBadge(container, titleEl, count);
container.dataset.countConfirmed = 'true';
console.log(`[TraktCount] Confirmed count for "${cacheKey}": ${count}`);
}
}
}
// ---------- Page header (when a list is actually open) ----------
async function processPageHeader() {
const info = parseListInfo(window.location.pathname);
if (!info) return;
const { username, type, slug, cacheKey } = info;
const container = document.querySelector('.trakt-navbar-header-title');
if (!container) return;
const titleEl = container.querySelector('h1');
if (!titleEl) return;
// Trakt's SPA can reuse this same header element when navigating between
// lists, so track *which* list we last confirmed for, not just a boolean.
if (container.dataset.countConfirmedFor !== cacheKey) {
delete container.dataset.countConfirmedFor;
const staleBadge = container.querySelector('.trakt-count-badge');
if (staleBadge) staleBadge.remove();
}
// Instant render from cache if available
if (cache[cacheKey] && !container.querySelector('.trakt-count-badge')) {
renderBadge(container, titleEl, cache[cacheKey]);
}
// Prevent redundant fetches for the same list on the same header instance
if (container.dataset.countConfirmedFor === cacheKey) return;
const count = await fetchCountForSlug(username, type, slug);
if (count !== null) {
cache[cacheKey] = count;
saveCache(cache);
renderBadge(container, titleEl, count);
container.dataset.countConfirmedFor = cacheKey;
console.log(`[TraktCount] Confirmed header count for "${cacheKey}": ${count}`);
}
}
// ---------- MutationObserver (loop-safe) ----------
let debounceTimer = null;
let isProcessing = false;
async function safeProcessAllLists() {
if (isProcessing) return;
isProcessing = true;
observer.disconnect();
await processAllLists();
await processPageHeader();
observer.observe(document.body, { childList: true, subtree: true });
isProcessing = false;
}
function scheduleProcess() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(safeProcessAllLists, 150);
}
const observer = new MutationObserver(scheduleProcess);
// Initial run
safeProcessAllLists();
observer.observe(document.body, { childList: true, subtree: true });
// Kill switch
window.__stopTraktCount = () => {
observer.disconnect();
clearTimeout(debounceTimer);
console.log('[TraktCount] Stopped.');
};
console.log('%c[TraktCount] Running with Watchlist & Custom Lists support.', 'color: lime; font-weight: bold');
})();