您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Send or preview an entire IMDb list to Kodi (Fenlight) with button-based action selection when configured to ask each time.
// ==UserScript== // @name IMDb List → Send to Kodi via Fenlight (Button Action Choice) // @namespace http://tampermonkey.net/ // @version 1.5 // @description Send or preview an entire IMDb list to Kodi (Fenlight) with button-based action selection when configured to ask each time. // @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 first.'); 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(err) { console.error('Kodi RPC error', err); alert('❌ Failed to contact Kodi.'); } }); } // // ─── SETTINGS PANEL ────────────────────────────────────────────────────── // function showSettings() { if (document.getElementById('kodisettings-modal')) return; const overlay = document.createElement('div'); overlay.id = 'kodisettings-modal'; Object.assign(overlay.style, { position: 'fixed', top:0, left:0, width:'100vw', height:'100vh', background:'rgba(0,0,0,0.8)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:100000 }); const panel = document.createElement('div'); panel.innerHTML = ` <h2 style="margin:0 0 16px;color:#fff">Kodi Settings</h2> <label style="display:block;margin:8px 0 4px;color:#fff">Kodi IP:</label> <input id="kodiIp" style="width:100%;padding:6px" value="${GM_getValue('kodiIp','')}"/> <label style="display:block;margin:8px 0 4px;color:#fff">Kodi Port:</label> <input id="kodiPort" style="width:100%;padding:6px" value="${GM_getValue('kodiPort','')}"/> <label style="display:block;margin:8px 0 4px;color:#fff">Kodi User:</label> <input id="kodiUser" style="width:100%;padding:6px" value="${GM_getValue('kodiUser','')}"/> <label style="display:block;margin:8px 0 4px;color:#fff">Kodi Pass:</label> <input id="kodiPass" type="password" style="width:100%;padding:6px" value="${GM_getValue('kodiPass','')}"/> <label style="display:block;margin:12px 0 4px;color:#fff">Default Action:</label> <select id="kodiAction" style="width:100%;padding:6px;box-sizing:border-box"> <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> <button id="kodiSave" style=" margin-top:16px; padding:8px 12px; background:#28a745;color:#fff; border:none;border-radius:4px; cursor:pointer; ">Save</button> <button id="kodiClose" style=" position:absolute;top:8px;right:8px; background:none;border:none;color:#fff; font-size:18px;cursor:pointer; ">✖</button> `; Object.assign(panel.style, { background:'#222', padding:'24px', borderRadius:'8px', width:'360px', position:'relative', fontFamily:'sans-serif' }); document.body.append(overlay); overlay.append(panel); panel.querySelector('#kodiAction').value = GM_getValue('kodiAction','import'); panel.querySelector('#kodiSave').onclick = () => { GM_setValue('kodiIp', panel.querySelector('#kodiIp').value); GM_setValue('kodiPort', panel.querySelector('#kodiPort').value); 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('#kodiClose').onclick = () => document.body.removeChild(overlay); } // // ─── JSON-LD SCRAPER ──────────────────────────────────────────────────── // 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; 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 to parse page 1 JSON-LD'); return null; } 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(entry => { const media_id = (entry.item.url.match(/tt\d+/)||[])[0]||''; const rawType = entry.item['@type']||''; let media_type; if (rawType==='Movie') media_type='movie'; else if (/^TV/.test(rawType)) media_type='tvshow'; else media_type=rawType.toLowerCase(); return { media_id, id_type:'imdb', media_type }; }); } // // ─── URL BUILDER (no id_type param) ───────────────────────────────────── // async function buildPluginUrl(action, limitCount) { const titleEl = document.querySelector('span.hero__primary-text[data-testid="hero__primary-text"]'); const listName = titleEl ? titleEl.textContent.trim() : prompt('List name:',''); if (!listName) { alert('❌ No list name'); return null; } let items = await gatherItems(); if (!items) return null; if (Number.isInteger(limitCount) && limitCount > 0) { items = items.slice(0, limitCount); } return 'plugin://plugin.video.fenlight/' + '?mode=personal_lists.import_external' + `&action=${encodeURIComponent(action)}` + `&item_list=${encodeURIComponent(JSON.stringify(items))}` + `&list_name=${encodeURIComponent(listName)}`; } // // ─── ACTION SELECTION MODAL ───────────────────────────────────────────── // function chooseActionButton() { return new Promise(resolve => { if (document.getElementById('action-choose-modal')) return; const overlay = document.createElement('div'); overlay.id = 'action-choose-modal'; Object.assign(overlay.style, { position:'fixed',top:0,left:0,width:'100vw',height:'100vh', background:'rgba(0,0,0,0.6)',display:'flex', alignItems:'center',justifyContent:'center',zIndex:100000 }); const panel = document.createElement('div'); panel.innerHTML = ` <h3 style="margin:0 0 12px;color:#fff">Select Action</h3> <div style="display:flex;gap:8px;justify-content:center"> <button id="act-view" style="padding:8px 12px;border:none;border-radius:4px;cursor:pointer">view</button> <button id="act-import" style="padding:8px 12px;border:none;border-radius:4px;cursor:pointer">import</button> <button id="act-import_view" style="padding:8px 12px;border:none;border-radius:4px;cursor:pointer">import_view</button> </div> `; Object.assign(panel.style, { background:'#333', padding:'24px', borderRadius:'8px', fontFamily:'sans-serif', textAlign:'center' }); overlay.append(panel); document.body.append(overlay); ['view','import','import_view'].forEach(act => { panel.querySelector(`#act-${act}`).onclick = () => { document.body.removeChild(overlay); resolve(act); }; }); }); } // // ─── HANDLERS ─────────────────────────────────────────────────────────── // async function onSendToKodi() { let action = GM_getValue('kodiAction','import'); if (action === 'ask') { action = await chooseActionButton(); if (!action) return; } const url = await buildPluginUrl(action); if (url) sendToKodi(url); } async function onShowUrl() { let input = prompt( "How many items to include from the top?\n" + "Enter a number (e.g. 4) or 'all' for entire list:", "all" ); if (input === null) return; input = input.trim().toLowerCase(); let limit = null; if (input !== 'all') { const n = parseInt(input, 10); if (isNaN(n) || n < 1) { alert('❌ Invalid number'); return; } limit = n; } const url = await buildPluginUrl('import', limit); if (!url) return; if (document.getElementById('url-preview-modal')) return; const overlay = document.createElement('div'); overlay.id = 'url-preview-modal'; Object.assign(overlay.style, { position:'fixed',top:0,left:0,width:'100vw',height:'100vh', background:'rgba(0,0,0,0.8)',display:'flex', alignItems:'center',justifyContent:'center',zIndex:100000 }); const panel = document.createElement('div'); panel.innerHTML = ` <h3 style="margin:0 0 12px;color:#fff">Preview Kodi Plugin URL</h3> <textarea readonly style=" width:100%;height:120px;padding:8px;box-sizing:border-box; font-family:monospace; ">${url}</textarea> <button id="copyBtn" style=" margin-top:12px;padding:8px 12px;background:#007bff; color:#fff;border:none;border-radius:4px;cursor:pointer; ">Copy to Clipboard</button> <button id="closePreview" style=" position:absolute;top:8px;right:8px;background:none; border:none;color:#fff;font-size:18px;cursor:pointer; ">✖</button> `; Object.assign(panel.style, { background:'#222',padding:'24px', borderRadius:'8px',width:'80%',maxWidth:'720px', position:'relative',fontFamily:'sans-serif' }); overlay.append(panel); document.body.append(overlay); panel.querySelector('#copyBtn').onclick = () => { const ta = panel.querySelector('textarea'); ta.select(); document.execCommand('copy'); alert('✅ URL copied'); }; panel.querySelector('#closePreview').onclick = () => document.body.removeChild(overlay); } // // ─── INJECT UI ────────────────────────────────────────────────────────── // function injectUI() { if (document.getElementById('kodiconnector-container')) return; const container = document.createElement('div'); container.id = 'kodiconnector-container'; Object.assign(container.style, { position:'fixed', top:'10px', right:'10px', zIndex:9999, display:'flex', gap:'8px' }); const btnShow = document.createElement('button'); btnShow.textContent = 'Show URL'; Object.assign(btnShow.style,{ padding:'8px 12px', background:'#888', color:'#fff', border:'none', borderRadius:'4px', cursor:'pointer' }); btnShow.onclick = onShowUrl; container.append(btnShow); const btnSend = document.createElement('button'); btnSend.textContent = 'Send to Kodi'; Object.assign(btnSend.style,{ padding:'8px 12px', background:'#f5c518', color:'#000', border:'none', borderRadius:'4px', cursor:'pointer' }); btnSend.onclick = onSendToKodi; container.append(btnSend); const btnSettings = document.createElement('button'); btnSettings.textContent = '⚙'; Object.assign(btnSettings.style,{ padding:'8px 12px', background:'#ccc', color:'#000', border:'none', borderRadius:'4px', cursor:'pointer' }); btnSettings.onclick = showSettings; container.append(btnSettings); document.body.append(container); } if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', injectUI); } else { injectUI(); } })();