您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();