您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds discovery and export tools to Learning on Screen / Box of Broadcasts public pages. DOES NOT bypass login or fetch protected files. Use to export metadata, bulk-open listing pages, and search public archives for titles.
// ==UserScript== // @name BoB-For-Free (Discovery Helper) — Legal Discovery Tools // @namespace https://example.local // @version 1.0 // @description Adds discovery and export tools to Learning on Screen / Box of Broadcasts public pages. DOES NOT bypass login or fetch protected files. Use to export metadata, bulk-open listing pages, and search public archives for titles. // @author ChatGPT // @match https://learningonscreen.ac.uk/ondemand/* // @grant GM_setClipboard // @grant none // ==/UserScript== (function () { 'use strict'; /* --- CONFIG --- */ const TOOLBAR_ID = 'bob-discovery-toolbar'; const BUTTON_STYLE = 'margin:4px;padding:6px 8px;border-radius:4px;border:1px solid #888;background:#fff;cursor:pointer;'; /* --------------- */ function createToolbar() { if (document.getElementById(TOOLBAR_ID)) return; const bar = document.createElement('div'); bar.id = TOOLBAR_ID; bar.style = 'position:fixed;right:12px;top:80px;z-index:9999;padding:8px;border:1px solid rgba(0,0,0,0.12);background:rgba(255,255,255,0.97);box-shadow:0 2px 8px rgba(0,0,0,0.08);font-family:Arial,Helvetica,sans-serif;font-size:13px;max-width:320px;'; bar.innerHTML = `<strong style="display:block;margin-bottom:6px;">BoB Discovery Helper</strong>`; document.body.appendChild(bar); addButton(bar, 'Export visible metadata (CSV)', exportCSV); addButton(bar, 'Copy metadata to clipboard (TSV)', copyTSV); addButton(bar, 'Bulk-open result pages (tab per item)', bulkOpen); addButton(bar, 'Search Archive.org for titles', searchArchiveOrg); addButton(bar, 'Search Google Video for titles', searchGoogleVideo); addButton(bar, 'Refresh detected items', () => { scanItems(true); alert('Refreshed.'); }); const hint = document.createElement('div'); hint.style = 'margin-top:8px;font-size:12px;color:#444'; hint.innerText = 'Works only with metadata visible on public listing pages. Does not access protected video files.'; bar.appendChild(hint); } function addButton(container, text, handler) { const btn = document.createElement('button'); btn.type = 'button'; btn.style = BUTTON_STYLE; btn.textContent = text; btn.addEventListener('click', handler); container.appendChild(btn); } // ---- Item scanning ---- let cachedItems = null; function scanItems(force = false) { if (cachedItems && !force) return cachedItems; // Try to detect common structures on BoB listing/search pages. // We only read text visible on the page (title, broadcaster, date, duration, page link). const items = []; // Many BoB listing pages use li.search-result or div.programme; try multiple selectors. const selectors = [ 'li.search-result', // older pattern '.search-result', // generic '.programme', // possible pattern '.card', // fallback '.result' // fallback ]; let nodes = []; for (const s of selectors) { const found = Array.from(document.querySelectorAll(s)); if (found.length) { nodes = found; break; } } // Generic fallback: try list items with links and dates if (!nodes.length) { nodes = Array.from(document.querySelectorAll('a[href*="/ondemand/"], article, li')).slice(0, 200); } nodes.forEach((node) => { try { // Title let title = ''; const titleEl = node.querySelector('h3, h2, .title, .programme-title, a'); if (titleEl) title = titleEl.textContent.trim(); // Link (page) let link = ''; const a = node.querySelector('a[href*="/ondemand/"]'); if (a) link = a.href; // Broadcaster / channel let broadcaster = ''; const bEl = node.querySelector('.broadcaster, .channel, .station, .publisher'); if (bEl) broadcaster = bEl.textContent.trim(); // Date / Air date let date = ''; const dateEl = Array.from(node.querySelectorAll('time, .date, .broadcast-date, .air-date')).find(Boolean); if (dateEl) date = dateEl.textContent.trim(); // Duration let duration = ''; const durEl = node.querySelector('.duration, .length, .running-time'); if (durEl) duration = durEl.textContent.trim(); // summary or description let summary = ''; const sumEl = node.querySelector('.summary, .description, .synopsis, p'); if (sumEl) summary = sumEl.textContent.trim().replace(/\s+/g,' ').slice(0,400); if (!title && !link) return; items.push({ title, broadcaster, date, duration, summary, link }); } catch (e) { // ignore node if parsing fails } }); // Deduplicate by link or title const unique = []; const seen = new Set(); for (const it of items) { const key = (it.link || it.title).toLowerCase(); if (!seen.has(key)) { seen.add(key); unique.push(it); } } cachedItems = unique; return unique; } // ---- Actions ---- function exportCSV() { const items = scanItems(); if (!items.length) { alert('No items detected on this page. Try on a search/listing page.'); return; } const rows = [['Title','Broadcaster','Date','Duration','Link','Summary']]; items.forEach(it => rows.push([escapeCSV(it.title), escapeCSV(it.broadcaster), escapeCSV(it.date), escapeCSV(it.duration), it.link, escapeCSV(it.summary)])); const csv = rows.map(r => r.join(',')).join('\n'); downloadText(csv, `bob_metadata_${safeFilename(document.title)}.csv`, 'text/csv;charset=utf-8;'); } function copyTSV() { const items = scanItems(); if (!items.length) { alert('No items detected on this page.'); return; } const lines = items.map(it => [it.title, it.broadcaster, it.date, it.duration, it.link, it.summary].join('\t')); const tsv = lines.join('\n'); // Try to use GM_setClipboard if available, otherwise fallback to navigator.clipboard if (typeof GM_setClipboard === 'function') { GM_setClipboard(tsv); alert('Metadata copied to clipboard (TSV).'); } else if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(tsv).then(()=> alert('Metadata copied to clipboard (TSV).')).catch(()=> alert('Clipboard failed — copy manually.')); } else { prompt('Copy the metadata below (TSV):', tsv); } } function bulkOpen() { const items = scanItems(); if (!items.length) { alert('No items detected to open.'); return; } const filtered = items.filter(it => it.link); if (!filtered.length) { alert('No item links found to open.'); return; } const confirmCount = Math.min(filtered.length, 25); if (!confirm(`Open ${confirmCount} pages in new tabs? (This will open up to 25 tabs)`)) return; for (let i = 0; i < Math.min(filtered.length, 25); i++) { window.open(filtered[i].link, '_blank'); } } function searchArchiveOrg() { const items = scanItems(); if (!items.length) { alert('No items detected.'); return; } // Build a search query for archive.org for each title; open first 6 in new tabs const queries = items.map(it => encodeURIComponent(it.title + ' ' + (it.broadcaster||'')).replace(/%20%20/g,'%20')).filter(Boolean).slice(0,6); if (!queries.length) return alert('No suitable titles to search.'); queries.forEach(q => window.open('https://archive.org/search.php?query=' + q, '_blank')); } function searchGoogleVideo() { const items = scanItems(); if (!items.length) { alert('No items detected.'); return; } const queries = items.map(it => encodeURIComponent(it.title + ' ' + (it.broadcaster||'')).replace(/%20%20/g,'%20')).filter(Boolean).slice(0,6); queries.forEach(q => window.open('https://www.google.com/search?q=' + q + '&tbm=vid', '_blank')); } // ---- Helpers ---- function escapeCSV(s) { if (s == null) return ''; s = String(s).replace(/"/g,'""'); if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s}"`; return s; } function downloadText(text, filename, mime='text/plain') { const blob = new Blob([text], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } function safeFilename(s) { return (s || 'export').replace(/[^a-z0-9\-_\.]/ig,'_').slice(0,120); } // add toolbar after DOM loaded function tryInit() { createToolbar(); scanItems(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { tryInit(); } else { window.addEventListener('DOMContentLoaded', tryInit); } // optional: refresh cache on navigation or ajax updates new MutationObserver(() => { cachedItems = null; }).observe(document.body, { childList: true, subtree: true }); // Provide keyboard shortcut (press "D" while not in an input to copy TSV) window.addEventListener('keydown', (e) => { if (e.key === 'D' && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) { e.preventDefault(); copyTSV(); } }); })();