IMDb List → Send to Kodi via Fenlight (Ask-each-time fixed)

Send an IMDb list to Kodi (Fenlight) as a personal list; pick a default action or have it ask you each time with buttons.

// ==UserScript==
// @name         IMDb List → Send to Kodi via Fenlight (Ask-each-time fixed)
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Send an IMDb list to Kodi (Fenlight) as a personal list; pick a default action or have it ask you each time with buttons.
// @match        https://www.imdb.com/list/ls*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

;(function() {
  'use strict';

  // ─── Kodi JSON-RPC sender ───────────────────────────────────────────────
  function sendToKodi(url) {
    const ip   = GM_getValue('kodiIp','').trim();
    const port = GM_getValue('kodiPort','').trim();
    const user = GM_getValue('kodiUser','');
    const pass = GM_getValue('kodiPass','');
    if (!ip || !port) {
      alert('⚠️ Please configure Kodi IP & port in settings.');
      return;
    }
    GM_xmlhttpRequest({
      method: 'POST',
      url:    `http://${ip}:${port}/jsonrpc`,
      headers: {
        'Content-Type':  'application/json',
        'Authorization': 'Basic ' + btoa(`${user}:${pass}`)
      },
      data: JSON.stringify({
        jsonrpc: '2.0',
        id:      1,
        method:  'Player.Open',
        params:  { item: { file: url } }
      }),
      onerror() {
        alert('❌ Failed to contact Kodi.');
      }
    });
  }

  // ─── Settings panel ─────────────────────────────────────────────────────
  function showSettings() {
    if (document.getElementById('kodisettings')) return;
    const overlay = document.createElement('div');
    overlay.id = 'kodisettings';
    Object.assign(overlay.style, {
      position:'fixed',top:0,left:0,width:'100vw',height:'100vh',
      background:'rgba(0,0,0,0.7)',display:'flex',
      alignItems:'center',justifyContent:'center',zIndex:99999
    });
    const panel = document.createElement('div');
    panel.innerHTML = `
      <h2 style="margin-bottom:16px;color:#fff">Kodi Settings</h2>
      <label style="color:#fff">Kodi IP:</label>
      <input id="kodiIp" value="${GM_getValue('kodiIp','')}" style="width:100%;margin-bottom:8px"/>
      <label style="color:#fff">Kodi Port:</label>
      <input id="kodiPort" value="${GM_getValue('kodiPort','')}" style="width:100%;margin-bottom:8px"/>
      <label style="color:#fff">Kodi User:</label>
      <input id="kodiUser" value="${GM_getValue('kodiUser','')}" style="width:100%;margin-bottom:8px"/>
      <label style="color:#fff">Kodi Pass:</label>
      <input id="kodiPass" type="password" value="${GM_getValue('kodiPass','')}" style="width:100%;margin-bottom:12px"/>
      <label style="color:#fff">Default Action:</label>
      <select id="kodiAction" style="width:100%;margin-bottom:16px">
        <option value="view">view</option>
        <option value="import">import</option>
        <option value="import_view">import_view</option>
        <option value="ask">Ask each time</option>
      </select>
      <div style="text-align:right">
        <button id="kodisave" style="margin-right:8px">Save</button>
        <button id="kodicancel">Cancel</button>
      </div>
    `;
    Object.assign(panel.style, {
      background:'#222', padding:'20px',
      borderRadius:'6px', width:'300px', boxSizing:'border-box'
    });
    overlay.append(panel);
    document.body.append(overlay);

    panel.querySelector('#kodiAction').value = GM_getValue('kodiAction','import');
    panel.querySelector('#kodisave').onclick = () => {
      GM_setValue('kodiIp',     panel.querySelector('#kodiIp').value.trim());
      GM_setValue('kodiPort',   panel.querySelector('#kodiPort').value.trim());
      GM_setValue('kodiUser',   panel.querySelector('#kodiUser').value);
      GM_setValue('kodiPass',   panel.querySelector('#kodiPass').value);
      GM_setValue('kodiAction', panel.querySelector('#kodiAction').value);
      document.body.removeChild(overlay);
      alert('✅ Settings saved');
    };
    panel.querySelector('#kodicancel').onclick = () =>
      document.body.removeChild(overlay);
  }

  // ─── Grab JSON-LD itemList ──────────────────────────────────────────────
  function extractItemList(html) {
    const re = /<script\s+type="application\/ld\+json">([\s\S]*?)<\/script>/g;
    let m;
    while ((m = re.exec(html))) {
      try {
        const j = JSON.parse(m[1]);
        if (j['@type']==='ItemList') return j.itemListElement;
      } catch {}
    }
    return [];
  }
  async function gatherItems() {
    const origin   = location.origin;
    const basePath = location.pathname.replace(/\?.*$/,'');
    const sel      = document.getElementById('listPagination');
    const total    = sel? sel.options.length : 1;

    // page 1 from DOM
    const scripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
    let page1 = null;
    for (const s of scripts) {
      try {
        const j = JSON.parse(s.textContent);
        if (j['@type']==='ItemList') { page1 = j.itemListElement; break; }
      } catch{}
    }
    if (!page1) { alert('⚠️ Failed page 1 parse'); return null; }

    // pages 2…N
    const urls = [];
    for (let p=2; p<=total; p++) urls.push(`${origin+basePath}?page=${p}`);
    const rest = await Promise.all(
      urls.map(u=>fetch(u,{credentials:'include'})
                .then(r=>r.text())
                .then(html=>extractItemList(html)))
    );

    const all = page1.concat(...rest);
    return all.map(e => {
      const id  = (e.item.url.match(/tt\d+/)||[])[0]||'';
      const t   = e.item['@type']||'';
      const mt  = t==='Movie' ? 'movie' : /^TV/.test(t) ? 'tvshow' : t.toLowerCase();
      return { media_id:id, id_type:'imdb', media_type:mt };
    });
  }

  // ─── Build Fenlight URL ─────────────────────────────────────────────────
  async function buildPluginUrl(action, limit) {
    const hero = document.querySelector('span.hero__primary-text[data-testid="hero__primary-text"]');
    const listName = hero
      ? hero.textContent.trim()
      : prompt('List name:','');
    if (!listName) return null;

    let items = await gatherItems();
    if (!items) return null;
    if (Number.isInteger(limit) && limit > 0) {
      items = items.slice(0, limit);
    }

    return 'plugin://plugin.video.fenlight/' +
           '?mode=personal_lists.import_external' +
           `&action=${encodeURIComponent(action)}` +
           `&item_list=${encodeURIComponent(JSON.stringify(items))}` +
           `&list_name=${encodeURIComponent(listName)}`;
  }

  // ─── Ask-each-time modal ─────────────────────────────────────────────────
  function chooseAction() {
    return new Promise(resolve => {
      // overlay
      const o = document.createElement('div');
      Object.assign(o.style, {
        position:'fixed',top:0,left:0,width:'100vw',height:'100vh',
        background:'rgba(0,0,0,0.7)',display:'flex',
        alignItems:'center',justifyContent:'center',zIndex:99999
      });
      // panel
      const p = document.createElement('div');
      p.innerHTML = `
        <h3 style="margin-bottom:12px;color:#fff">Choose Action:</h3>
        <button data-act="view">view</button>
        <button data-act="import">import</button>
        <button data-act="import_view">import_view</button>
      `;
      Object.assign(p.style, {
        background:'#222',padding:'20px',borderRadius:'6px',
        display:'flex',flexDirection:'column',gap:'8px',alignItems:'stretch'
      });
      o.append(p);
      document.body.append(o);

      p.querySelectorAll('button').forEach(btn => {
        btn.onclick = () => {
          const a = btn.getAttribute('data-act');
          document.body.removeChild(o);
          resolve(a);
        };
      });
    });
  }

  // ─── Handlers ────────────────────────────────────────────────────────────
  async function onSendToKodi() {
    let action = GM_getValue('kodiAction','import');
    if (action === 'ask') {
      action = await chooseAction();
      if (!action) return;
    }
    const url = await buildPluginUrl(action);
    if (url) sendToKodi(url);
  }

  async function onShowUrl() {
    let resp = prompt("Top N items? Enter number or 'all':","all");
    if (resp===null) return;
    resp = resp.trim().toLowerCase();
    let limit = null;
    if (resp!=='all') {
      const n = parseInt(resp,10);
      if (isNaN(n)||n<1) { alert('❌ Invalid'); return; }
      limit = n;
    }
    const url = await buildPluginUrl('import', limit);
    if (!url) return;

    // preview modal
    const o = document.createElement('div');
    Object.assign(o.style, {
      position:'fixed',top:0,left:0,width:'100vw',height:'100vh',
      background:'rgba(0,0,0,0.7)',display:'flex',
      alignItems:'center',justifyContent:'center',zIndex:99998
    });
    const p = document.createElement('div');
    p.innerHTML = `
      <h3 style="margin-bottom:8px;color:#fff">Preview URL:</h3>
      <textarea readonly style="width:320px;height:120px">${url}</textarea>
      <button id="copyBtn" style="margin-top:8px">Copy to Clipboard</button>
      <button id="closeBtn" style="margin-left:8px">Close</button>
    `;
    Object.assign(p.style, {
      background:'#222',padding:'16px',borderRadius:'6px',textAlign:'center'
    });
    o.append(p);
    document.body.append(o);

    p.querySelector('#copyBtn').onclick = () => {
      const ta = p.querySelector('textarea');
      ta.select(); document.execCommand('copy');
      alert('✅ Copied');
    };
    p.querySelector('#closeBtn').onclick = () => document.body.removeChild(o);
  }

  // ─── Inject UI ──────────────────────────────────────────────────────────
  function injectUI() {
    if (document.getElementById('kodicont')) return;
    const c = document.createElement('div');
    c.id = 'kodicont';
    Object.assign(c.style, {
      position:'fixed',top:'10px',right:'10px',display:'flex',gap:'8px',zIndex:9999
    });
    const b1 = document.createElement('button');
    b1.textContent = 'Show URL'; b1.onclick = onShowUrl;
    const b2 = document.createElement('button');
    b2.textContent = 'Send to Kodi'; b2.onclick = onSendToKodi;
    const b3 = document.createElement('button');
    b3.textContent = '⚙'; b3.onclick = showSettings;
    [b1,b2,b3].forEach(b=>{
      Object.assign(b.style,{
        padding:'6px 12px',border:'none',borderRadius:'4px',cursor:'pointer'
      });
    });
    c.append(b1,b2,b3);
    document.body.append(c);
  }

  if (document.readyState==='loading') {
    window.addEventListener('DOMContentLoaded', injectUI);
  } else {
    injectUI();
  }

})();