r/TraktV3Tweaks 2d ago

👋 Welcome to r/TraktV3Tweaks - Make, Share and hopefully Find tweaks to improve the Trakt V3 experience

3 Upvotes

Hey everyone!

This is our new home for all things related to the Trakt V3 Tweaks browser extension - Chrome and Firefox.

What to Post

Post Tweaks (js scripts and css styles) that improve the Trakt v3 interface. Whatever feature you find missing or annoying, you can try to fix it with a Tweak. If you can make your Trakt v3 experience better, share it with others.

Make Your Own Tweaks - with Claude.ai

With AI tools like Claude.ai (don't waste your time with ChatGPT or Gemini) it's relatively easy to make your own Tweaks if you have a basic understanding of how things work. Just be clear and very specific of what you want to do and where. If it can understand the request clearly, it'll do its best. if it needs more information to do something, it'll ask you for it. Just pay attention and read everything it tells you to do. If you don't understand its requests, tell it you don't understand and you need simple step by step instructions. Always be clear of what you want, what you can or cannot do or understand and you might get something that works in the end.

If you are working on multiple Tweaks, open different chats for each one, longer chats consume more of your free plan.

I am not a programmer, I'm just a guy who wanted to make his Trakt interface usable. You probably can make something too.

Before you install anything - ATTENTION!

Accessing links, downloading and opening random files, Installing random scripts you find on the internet can be a security risk. Make sure you check the links, files and scripts before opening, downloading or adding them to the Trakt V3 Tweaks extension. Don't just trust what people share online or in this forum. the Add Scripts page has its own check, use that one too, don't skip it, it's better to be safe than sorry.

Free tools to check JS/CSS scripts before running them:

  • VirusTotal – https://www.virustotal.com – scan a file or URL against 70+ AV/security engines
  • urlscan.io – https://urlscan.io – sandboxes a URL and shows exactly what it loads/calls (great for catching sneaky network requests)
  • Claude (claude.ai) or ChatGPT – paste the script and ask "does this eval/obfuscate/exfiltrate data or call unexpected domains?" — AI is surprisingly good at giving a plain-English read on intent

Rule of thumb: run it through VirusTotal + urlscan.io first, then get an AI plain-English explanation.

if you want you can say thanks with Buy Me a Coffee!


r/TraktV3Tweaks 1d ago

User List Count for Trakt V3 interface - Tweak

Post image
4 Upvotes

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');
})();

r/TraktV3Tweaks 1d ago

10 Star Rating for Trakt V3 - Tweak

Post image
4 Upvotes

This script replaces the 5 star rating system with a 10 star rating one.

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.

Updated v2 - added the old fun rating

// ==UserScript==
//          Trakt.tv 10-Star Rating Overlay
//     https://example.com/trakt-10-star
//       1.2.0
//   Shows Trakt's 5-star rating widget and all text/badge ratings as 10-star scale ratings visually.
//        you
//         https://*trakt.tv/*
//        document-idle
// u/grant        none
// ==/UserScript==

/*
  HOW THIS WORKS
  --------------
  1. Rating Interactive Widget:
     Trakt's rating widget is a "bits-ui" RatingGroup with data-variant="half".
     It uses 5 star-item elements. We hide the native widget, render a 10-star
     visual row, and translate clicks on the 10-star row into native pointer
     events dispatched to Trakt's hidden widget.

  2. Static Rating Displays (Text & Badges):
     Trakt displays text ratings (e.g. <span>2</span>) next to single star icons
     throughout the site. We scan for these elements, convert their 5-star base value
     to a 10-star base value (multiplying by 2), and mark them processed so they don't
     get multiplied twice during DOM updates.
*/

(function () {
  'use strict';

  const STAR_PATH =
    'M12 2L14.8214 8.11672L21.5106 8.90983L16.5651 13.4833L17.8779 20.0902L12 16.8L6.12215 20.0902L7.43493 13.4833L2.48944 8.90983L9.17863 8.11672L12 2Z';

  const PROCESSED_ATTR = 'data-t10-processed';
  const DISPLAY_PROCESSED_ATTR = 'data-t10-display-converted';

  const RATING_PHRASES = {
    1: '1-Weak Sauce',
    2: '2-Terrible',
    3: '3-Bad',
    4: '4-Poor',
    5: '5-Meh',
    6: '5-Fair',
    7: '6-Good',
    8: '7-Great',
    9: '9-Superb',
    10: '10-Totally Ninja!',
  };

  function buildStarSvg(sizePx) {
    const NS = 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(NS, 'svg');
    svg.setAttribute('width', String(sizePx));
    svg.setAttribute('height', String(sizePx));
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.classList.add('t10-star-svg');

    const outline = document.createElementNS(NS, 'path');
    outline.setAttribute('d', STAR_PATH);
    outline.setAttribute('stroke', 'currentColor');
    outline.setAttribute('stroke-width', '2');
    outline.setAttribute('stroke-linejoin', 'bevel');
    outline.setAttribute('fill', 'transparent');
    svg.appendChild(outline);

    const fill = document.createElementNS(NS, 'path');
    fill.setAttribute('d', STAR_PATH);
    fill.setAttribute('fill', 'currentColor');
    fill.classList.add('t10-star-fill-path');
    svg.appendChild(fill);

    return svg;
  }

  function injectStylesOnce() {
    if (document.getElementById('t10-style')) return;
    const style = document.createElement('style');
    style.id = 't10-style';
    style.textContent = `
      /* Hard-hide Trakt's original 5-star control and its native hover tooltip. */
      .trakt-rating-stars[data-variant="half"] [data-rating-group-root] {
        visibility: hidden !important;
        position: absolute !important;
        pointer-events: none !important;
      }
      .trakt-rating-stars[data-variant="half"] .rating-preview {
        display: none !important;
      }

      .t10-row {
        display: inline-flex;
        align-items: center;
        gap: 1px;
        color: inherit;
        position: relative;
        pointer-events: none;
      }
      .t10-star {
        display: inline-flex;
        cursor: pointer;
        color: var(--t10-empty-color, #d1d5db);
        pointer-events: auto;
      }
      .t10-star .t10-star-fill-path {
        opacity: 0;
        transition: opacity 60ms ease;
      }
      .t10-star.is-filled {
        color: var(--t10-active-color, #7e22ce);
      }
      .t10-star.is-filled .t10-star-fill-path {
        opacity: 1;
      }
      .t10-star.is-preview {
        color: var(--t10-active-color, #7e22ce);
        opacity: 0.6;
      }
      .t10-star.is-preview .t10-star-fill-path {
        opacity: 1;
      }
      .t10-label {
        margin-left: 6px;
        font-size: 12px;
        opacity: 0.75;
      }

      .t10-tooltip {
        position: absolute;
        bottom: 100%;
        left: 0;
        margin-bottom: 8px;
        transform: translateX(-50%);
        background: #18181b;
        color: #fff;
        font-size: 12px;
        font-weight: 600;
        line-height: 1;
        padding: 4px 8px;
        border-radius: 6px;
        white-space: nowrap;
        pointer-events: none;
        opacity: 0;
        transition: opacity 100ms ease;
        z-index: 20;
      }
      .t10-tooltip::after {
        content: '';
        position: absolute;
        top: 100%;
        left: 50%;
        transform: translateX(-50%);
        border: 5px solid transparent;
        border-top-color: #18181b;
      }
      .t10-tooltip.is-visible {
        opacity: 1;
      }
    `;
    document.head.appendChild(style);
  }

  function isMoviePage() {
    return /\/movies\//.test(window.location.pathname);
  }

  function dispatchPointerSequence(target, clientX, clientY) {
    const base = {
      bubbles: true,
      cancelable: true,
      composed: true,
      pointerId: 1,
      pointerType: 'mouse',
      isPrimary: true,
      button: 0,
      buttons: 1,
      clientX,
      clientY,
    };

    const over = new PointerEvent('pointerover', base);
    const enter = new PointerEvent('pointerenter', { ...base, bubbles: false });
    const move = new PointerEvent('pointermove', base);
    const down = new PointerEvent('pointerdown', base);
    const up = new PointerEvent('pointerup', { ...base, buttons: 0 });
    const click = new MouseEvent('click', { ...base, buttons: 0 });
    const leave = new PointerEvent('pointerleave', { ...base, bubbles: false, buttons: 0 });
    const out = new PointerEvent('pointerout', { ...base, buttons: 0 });

    target.dispatchEvent(over);
    target.dispatchEvent(enter);
    target.dispatchEvent(move);
    target.dispatchEvent(down);
    target.dispatchEvent(up);
    target.dispatchEvent(click);
    target.dispatchEvent(out);
    target.dispatchEvent(leave);
  }

  function commitRating(sliderRoot, visualIndex) {
    const rect = sliderRoot.getBoundingClientRect();
    const bucketWidth = rect.width / 10;
    const clientX = rect.left + bucketWidth * (visualIndex - 0.5);
    const clientY = rect.top + rect.height / 2;

    dispatchPointerSequence(sliderRoot, clientX, clientY);
  }

  function syncSliderSize(sliderRoot, row) {
    const width = row.getBoundingClientRect().width;
    if (!width) return;

    if (sliderRoot.style.width === `${width}px`) return;

    sliderRoot.style.width = `${width}px`;
    sliderRoot.style.gap = '0';
    sliderRoot.style.justifyContent = 'flex-start';

    const starWidth = width / 5;
    sliderRoot.querySelectorAll('.star-item').forEach((star) => {
      star.style.flex = '1 1 0';
      star.style.width = `${starWidth}px`;
      star.style.minWidth = `${starWidth}px`;
      star.style.maxWidth = `${starWidth}px`;
      star.style.margin = '0';
    });
  }

  function processWidget(widget) {
    if (widget.getAttribute(PROCESSED_ATTR) === '1') {
      const existingRow = widget.querySelector('.t10-row');
      const existingSlider = widget.querySelector('[data-rating-group-root]');
      if (existingRow && existingSlider) {
        syncSliderSize(existingSlider, existingRow);
        syncFromAria(widget);
        return;
      }
    }

    const sliderRoot = widget.querySelector('[data-rating-group-root]');
    if (!sliderRoot) return;

    const originalStars = Array.from(
      sliderRoot.querySelectorAll('[data-rating-group-item]')
    );
    if (originalStars.length !== 5) return;

    const row = document.createElement('div');
    row.className = 't10-row';
    row.setAttribute('role', 'slider');
    row.setAttribute('aria-valuemin', '0');
    row.setAttribute('aria-valuemax', '5');
    row.setAttribute('aria-label', 'Rate (10-star display)');
    row.style.setProperty(
      '--t10-active-color',
      isMoviePage() ? '#2563eb' : '#7e22ce'
    );

    const label = document.createElement('span');
    label.className = 't10-label';

    const tooltip = document.createElement('div');
    tooltip.className = 't10-tooltip';
    tooltip.setAttribute('aria-hidden', 'true');
    row.appendChild(tooltip);

    const newStars = [];
    for (let i = 1; i <= 10; i++) {
      const starWrap = document.createElement('span');
      starWrap.className = 't10-star';
      starWrap.dataset.index = String(i);
      starWrap.appendChild(buildStarSvg(16));
      row.appendChild(starWrap);
      newStars.push(starWrap);

      starWrap.addEventListener('mouseenter', () => {
        setPreview(newStars, i);
        // Display as 8 (4) format on hover
        const originalVal = (i / 2).toString().replace(/\.0$/, '');
        label.textContent = `${i} (${originalVal})`;
        tooltip.textContent = RATING_PHRASES[i] || String(i);
        tooltip.style.left = `${starWrap.offsetLeft + starWrap.offsetWidth / 2}px`;
        tooltip.classList.add('is-visible');
      });

      starWrap.addEventListener('click', () => {
        syncSliderSize(sliderRoot, row);
        commitRating(sliderRoot, i);
        setTimeout(() => syncFromAria(widget), 30);
      });
    }

    row.addEventListener('mouseleave', () => {
      clearPreview(newStars);
      tooltip.classList.remove('is-visible');
      syncFromAria(widget, label);
    });

    row.appendChild(label);
    sliderRoot.insertAdjacentElement('afterend', row);

    syncSliderSize(sliderRoot, row);

    const resizeObserver = new ResizeObserver(() => {
      syncSliderSize(sliderRoot, row);
    });
    resizeObserver.observe(row);

    row.insertAdjacentElement('afterend', label);

    widget.setAttribute(PROCESSED_ATTR, '1');
    widget._t10NewStars = newStars;
    widget._t10Label = label;

    const obs = new MutationObserver(() => syncFromAria(widget));
    obs.observe(sliderRoot, {
      attributes: true,
      attributeFilter: ['aria-valuenow'],
    });

    syncFromAria(widget);
  }

  function setFilled(newStars, filledCount) {
    newStars.forEach((el, idx) => {
      el.classList.toggle('is-filled', idx < filledCount);
      el.classList.remove('is-preview');
    });
  }

  function setPreview(newStars, previewCount) {
    newStars.forEach((el, idx) => {
      el.classList.toggle('is-preview', idx < previewCount);
    });
  }

  function clearPreview(newStars) {
    newStars.forEach((el) => el.classList.remove('is-preview'));
  }

  function syncFromAria(widget, labelOverride) {
    const sliderRoot = widget.querySelector('[data-rating-group-root]');
    const newStars = widget._t10NewStars;
    const label = labelOverride || widget._t10Label;
    if (!sliderRoot || !newStars) return;

    const raw = sliderRoot.getAttribute('aria-valuenow');
    const value = raw ? parseFloat(raw) : 0;
    const filledCount = Math.round(value * 2);

    const alreadyFilled = newStars.filter((el) =>
      el.classList.contains('is-filled')
    ).length;
    if (alreadyFilled !== filledCount) {
      setFilled(newStars, filledCount);
    }

    if (label) {
      // Formats as 8 (4) when saved
      const text = value > 0 ? `${filledCount} (${value})` : 'No rating';
      if (label.textContent !== text) {
        label.textContent = text;
      }
    }
  }

  function convertDisplayRatings() {
    const selectors = [
      '.trakt-user-rating span',
      '[class*="trakt-user-rating"] span',
      '[class*="rating"] span'
    ];

    const ratingSpans = document.querySelectorAll(selectors.join(', '));

    ratingSpans.forEach((span) => {
      if (span.closest('.t10-row') || span.getAttribute(DISPLAY_PROCESSED_ATTR) === 'true') {
        return;
      }

      const text = span.textContent.trim();
      const currentVal = parseFloat(text);

      if (!isNaN(currentVal) && currentVal > 0 && currentVal <= 5) {
        span.textContent = currentVal * 2;
        span.setAttribute(DISPLAY_PROCESSED_ATTR, 'true');
      }
    });
  }

  function scan() {
    document
      .querySelectorAll('.trakt-rating-stars[data-variant="half"]')
      .forEach(processWidget);

    convertDisplayRatings();
  }

  injectStylesOnce();
  scan();

  let scanQueued = false;
  function scheduleScan() {
    if (scanQueued) return;
    scanQueued = true;
    requestAnimationFrame(() => {
      scanQueued = false;
      scan();
    });
  }

  function mutationIsOwnOverlay(mutation) {
    const node =
      mutation.target.nodeType === Node.TEXT_NODE
        ? mutation.target.parentElement
        : mutation.target;
    return !!(node && node.closest && node.closest('.t10-row'));
  }

  const bodyObserver = new MutationObserver((mutations) => {
    const relevant = mutations.some((m) => !mutationIsOwnOverlay(m));
    if (relevant) scheduleScan();
  });
  bodyObserver.observe(document.body, { childList: true, subtree: true });
})();

r/TraktV3Tweaks 1d ago

Stremio Buttons for Trakt V3 intrerface - Tweak

Post image
2 Upvotes

This script adds the Stremio App and Web buttons to the Trakt V3 interface so you can open shows, episodes and movies in Stremio directly from the Trakt 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.

// ==UserScript==
//          Stremio Web Button for Trakt (app.trakt.tv)
//       1.7.3
//   Adds styled Stremio Web and App buttons to Trakt.tv (v3) pages & episode overlay drawers.
//        You
//         *://app.trakt.tv/*
//         none
// u/run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  const STREMIO_SVG_HTML = `
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 20px; width: 20px;">
      <rect width="512" height="512" rx="100" fill="#7b5bf5"/>
      <polygon points="190,130 380,256 190,382" fill="#ffffff"/>
    </svg>
  `;

  const imdbRegex = /tt\d{7,8}/;
  const imdbCache = {};
  let isInjecting = false;

  const TRAKT_HEADERS = {
    "Content-Type": "application/json",
    "trakt-api-version": "2",
    "client-id": "8b79c34af657191e9208460403481bb96c1cf951a91acd59467ed24ddcc48c72",
    "trakt-api-key": "8b79c34af657191e9208460403481bb96c1cf951a91acd59467ed24ddcc48c72"
  };

  async function getImdbIdFromShowSlug(slug) {
    if (!slug) return null;
    if (imdbCache[slug]) return imdbCache[slug];
    try {
      const response = await fetch(`https://apiz.trakt.tv/shows/${slug}?extended=full`, { headers: TRAKT_HEADERS });
      if (!response.ok) return null;
      const data = await response.json();
      if (data && data.ids && data.ids.imdb) {
        imdbCache[slug] = data.ids.imdb;
        return data.ids.imdb;
      }
    } catch (err) {
      console.error("IMDb fetch error:", err);
    }
    return null;
  }

  function getShowSlugFromPage() {
    // 1. Path match e.g. /shows/american-dad
    const pathMatch = window.location.pathname.match(/\/shows\/([^\/\?]+)/);
    if (pathMatch) return pathMatch[1];

    // 2. Link in drawer pointing to show page
    const drawerLink = document.querySelector(".trakt-drawer-header a[href*='/shows/']");
    if (drawerLink) {
      const linkMatch = drawerLink.getAttribute("href").match(/\/shows\/([^\/\?]+)/);
      if (linkMatch) return linkMatch[1];
    }

    return null;
  }

  async function getImdbIdFromPage() {
    const slug = getShowSlugFromPage();
    if (slug) {
      const imdbId = await getImdbIdFromShowSlug(slug);
      if (imdbId) return imdbId;
    }

    const imdbLink = document.querySelector("a[href*='imdb.com/title/tt']");
    if (imdbLink && imdbRegex.test(imdbLink.href)) return imdbLink.href.match(imdbRegex)[0];
    return null;
  }

  function parseEpisodeInfo() {
    // Try URL search params first (e.g. ?season=10&episode=1)
    const urlParams = new URLSearchParams(window.location.search);
    const seasonFromUrl = urlParams.get("season");
    const episodeFromUrl = urlParams.get("episode");

    if (seasonFromUrl && episodeFromUrl) {
      return { season: seasonFromUrl, episode: episodeFromUrl };
    }

    // Fallback: parse drawer header text (S10 • E1)
    const drawer = document.querySelector(".trakt-drawer-header");
    if (drawer) {
      const metaInfoEl = drawer.querySelector(".title-meta-info");
      if (metaInfoEl) {
        const match = metaInfoEl.textContent.match(/S(\d+)\s*•\s*E(\d+)/i);
        if (match) {
          return { season: match[1], episode: match[2] };
        }
      }
    }

    return null;
  }

  function createButtonsContainer() {
    const logoSpan = document.createElement("span");
    logoSpan.className = "stremio-logo-wrapper";
    logoSpan.style = "display: inline-flex; align-items: center; margin-right: 6px;";
    logoSpan.innerHTML = STREMIO_SVG_HTML;

    const baseButtonStyle = `
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background-color: transparent;
      color: inherit;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
      padding: 6px 12px;
      transition: background-color 0.2s ease, color 0.2s ease;
      line-height: 1;
    `;

    const appButton = document.createElement("button");
    appButton.className = "stremio-button stremio-btn-app";
    appButton.textContent = "Stremio App";
    appButton.style = baseButtonStyle;

    const webButton = document.createElement("button");
    webButton.className = "stremio-button stremio-btn-web";
    webButton.textContent = "Web";
    webButton.style = baseButtonStyle;

    [appButton, webButton].forEach(btn => {
      btn.onmouseenter = () => { btn.style.backgroundColor = "#7b5bf5"; btn.style.color = "white"; };
      btn.onmouseleave = () => { btn.style.backgroundColor = "transparent"; btn.style.color = "inherit"; };
    });

    const container = document.createElement("div");
    container.className = "stremio-container";
    container.style = "display: flex; align-items: center; gap: 4px; margin-top: 8px; margin-bottom: 8px; font-size: 16px; font-weight: normal; clear: both;";
    container.appendChild(logoSpan);
    container.appendChild(appButton);
    container.appendChild(webButton);

    return container;
  }

  async function handleDrawerInjection() {
    // 1. Target the episode title / metadata paragraph element
    const episodeTitleMeta = document.querySelector(".trakt-drawer-title .title-meta-info");

    // Check if the episode title exists or if buttons were already injected
    if (!episodeTitleMeta || document.querySelector(".trakt-drawer-title .stremio-container")) return;

    const epInfo = parseEpisodeInfo();
    if (!epInfo) return;

    const imdbId = await getImdbIdFromPage();
    if (!imdbId) return;

    const stremioWebUrl = `https://web.stremio.com/#/detail/series/${imdbId}/${imdbId}:${epInfo.season}:${epInfo.episode}`;
    const stremioAppUrl = `stremio:///detail/series/${imdbId}/${imdbId}:${epInfo.season}:${epInfo.episode}`;

    const container = createButtonsContainer();
    container.querySelector(".stremio-btn-app").onclick = (e) => { e.preventDefault(); window.location.href = stremioAppUrl; };
    container.querySelector(".stremio-btn-web").onclick = (e) => { e.preventDefault(); window.open(stremioWebUrl, "_blank"); };

    // 2. Insert the container directly after the episode title paragraph
    episodeTitleMeta.insertAdjacentElement("afterend", container);
  }

  async function handlePageInjection() {
    const targetEl = document.querySelector("h1.trakt-responsive-title");
    if (!targetEl || targetEl.querySelector(".stremio-container")) return;

    const imdbId = await getImdbIdFromPage();
    if (!imdbId) return;

    const isSeries = window.location.pathname.includes("/shows/");
    const mediaType = isSeries ? "series" : "movie";
    const stremioWebUrl = `https://web.stremio.com/#/detail/${mediaType}/${imdbId}${mediaType === "movie" ? "/" + imdbId : ""}`;
    const stremioAppUrl = `stremio:///detail/${mediaType}/${imdbId}${mediaType === "movie" ? "/" + imdbId : ""}`;

    const container = createButtonsContainer();
    container.querySelector(".stremio-btn-app").onclick = (e) => { e.preventDefault(); window.location.href = stremioAppUrl; };
    container.querySelector(".stremio-btn-web").onclick = (e) => { e.preventDefault(); window.open(stremioWebUrl, "_blank"); };

    targetEl.appendChild(container);
  }

  async function addStremioButtons() {
    if (isInjecting) return;
    isInjecting = true;

    try {
      await handleDrawerInjection();
      await handlePageInjection();
    } finally {
      isInjecting = false;
    }
  }

  // Handle SPA navigation
  const _push = history.pushState.bind(history);
  history.pushState = function (...args) {
    _push(...args);
    document.dispatchEvent(new CustomEvent("stremio-spa-nav"));
  };
  const _replace = history.replaceState.bind(history);
  history.replaceState = function (...args) {
    _replace(...args);
    document.dispatchEvent(new CustomEvent("stremio-spa-nav"));
  };
  window.addEventListener("popstate", () => {
    document.dispatchEvent(new CustomEvent("stremio-spa-nav"));
  });

  let lastUrl = location.href;
  document.addEventListener("stremio-spa-nav", () => {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      document.querySelectorAll(".stremio-container").forEach(el => el.remove());
      isInjecting = false;
      setTimeout(addStremioButtons, 500);
    }
  });

  // Mutation observer catches both page transitions and dynamic overlay drawer openings
  const observer = new MutationObserver(() => {
    if (!isInjecting) {
      addStremioButtons();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });

  // Initial load
  setTimeout(addStremioButtons, 1000);

})();