您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Scrapes NHK programs/episodes into Excel sheets, tracks watched status, auto-caches.
// ==UserScript== // @name NHK School Program & Episode Tracker (Multi-Sheet with Auto-Cache) // @namespace http://tampermonkey.net/ // @version 1.2 // @description Scrapes NHK programs/episodes into Excel sheets, tracks watched status, auto-caches. // @author iniquitousx // @match https://www.nhk.or.jp/school/program/ // @match https://www.nhk.or.jp/school/*/*/ // @match https://www.nhk.or.jp/school/*/*/*/ // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect www.nhk.or.jp // @license MIT // ==/UserScript== (function() { 'use strict'; const CACHED_EXCEL_DATA_KEY = 'nhkSchoolExcelData_v1_2'; // Version bump for potential cache structure changes const PROGRAM_OVERVIEW_SHEET_NAME = "Program Overview"; let currentExcelData = {}; // --- Helper Functions --- async function fetchData(url) { /* ... (same as before) ... */ return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { if (response.status >= 200 && response.status < 300) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); resolve(doc); } else { reject(new Error(`Failed to fetch ${url}: Status ${response.status}`)); } }, onerror: function(error) { reject(new Error(`Network error fetching ${url}: ${error}`)); } }); }); } function getRubyText(element) { /* ... (same as before) ... */ if (!element) return ''; const rbElement = element.querySelector('rb'); if (rbElement) return rbElement.textContent.trim(); let text = element.textContent.trim(); const rtElement = element.querySelector('rt'); if (rtElement) { text = text.replace(rtElement.textContent.trim(), ''); } return text.trim().replace(/\s+/g, ' '); } function cacheCurrentExcelData() { if (Object.keys(currentExcelData).length > 0) { GM_setValue(CACHED_EXCEL_DATA_KEY, JSON.stringify(currentExcelData)); console.log("NHK Tracker: Excel data cached automatically."); } else { GM_deleteValue(CACHED_EXCEL_DATA_KEY); console.log("NHK Tracker: Excel data cache cleared."); } } function loadExcelDataFromCache() { const cachedJson = GM_getValue(CACHED_EXCEL_DATA_KEY, null); if (cachedJson) { try { currentExcelData = JSON.parse(cachedJson); console.log("NHK Tracker: Data loaded from cache.", Object.keys(currentExcelData).length, "sheets."); return true; } catch (e) { console.error("NHK Tracker: Error parsing cached data:", e); GM_deleteValue(CACHED_EXCEL_DATA_KEY); // Clear corrupted cache currentExcelData = {}; return false; } } currentExcelData = {}; return false; } function sanitizeSheetName(name) { /* ... (same as before) ... */ return name.replace(/[:\\/?*[\]]/g, '').substring(0, 31); } // --- UI Elements and General Functions --- const statusDivGlobal = document.createElement('div'); statusDivGlobal.id = 'nhk-tracker-global-status'; const statusDivOnProgramPage = document.createElement('div'); // Defined here for broader access statusDivOnProgramPage.id = 'nhk-tracker-program-page-status'; function displayStatus(message, isError = false, onProgramPage = false) { const targetDiv = onProgramPage ? statusDivOnProgramPage : statusDivGlobal; if(document.getElementById(targetDiv.id)) { // Only update if the div is on the page targetDiv.textContent = message; targetDiv.style.color = isError ? 'red' : 'black'; } console.log("NHK Tracker Status:", message); } // --- UI Styling --- GM_addStyle(`/* ... (same full CSS as before) ... */ #nhk-tracker-controls-container { position: fixed; top: 10px; right: 10px; z-index: 10000; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: .25rem; padding: 15px; box-shadow: 0 .5rem 1rem rgba(0,0,0,.15); width: 320px; font-family: sans-serif; font-size: 14px; } .nhk-tracker-btn { background-color: #007bff; color: white; border: none; padding: 8px 12px; margin: 8px 0; cursor: pointer; border-radius: 4px; font-size: 14px; display: block; width: 100%; text-align: center; box-sizing: border-box; } .nhk-tracker-btn:hover { background-color: #0056b3; } .nhk-tracker-btn-success { background-color: #28a745; } .nhk-tracker-btn-success:hover { background-color: #1e7e34; } .nhk-tracker-btn-info { background-color: #17a2b8; } .nhk-tracker-btn-info:hover { background-color: #117a8b; } #nhk-tracker-global-status, #nhk-tracker-program-page-status { margin-top: 10px; padding: 8px; background-color: #e9ecef; border: 1px solid #ced4da; font-size: 13px; min-height: 20px; word-wrap: break-word; } #excelFileInput { display: none; } .nhk-episode-watched-toggle { padding: 3px 6px; font-size: 11px; min-width: 90px; margin-left: 8px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; } .nhk-episode-watched-toggle.watched-true { background-color: #28a745; color: white; } .nhk-episode-watched-toggle.watched-false { background-color: #dc3545; color: white; } .nhk-episode-watched-toggle.not-tracked { background-color: #ffc107; color: black; } .nhk-scan-prompt-div { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; padding: 10px; margin: 10px 0; border-radius: 4px; text-align: center; font-size: 13px; } .programList .itemList { display: flex; align-items: center; } .programList .itemList > a { flex-grow: 1; } `); // --- Excel Handling --- function handleExcelFileLoad(file) { const isOnProgramPage = !!document.getElementById('nhk-tracker-program-page-status'); displayStatus('Reading Excel file...', false, isOnProgramPage); // ... (rest of handleExcelFileLoad - ENSURE IT CALLS cacheCurrentExcelData() after successful load) const reader = new FileReader(); reader.onload = function(e) { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); let newExcelData = {}; workbook.SheetNames.forEach(sheetName => { const worksheet = workbook.Sheets[sheetName]; newExcelData[sheetName] = XLSX.utils.sheet_to_json(worksheet); }); currentExcelData = newExcelData; cacheCurrentExcelData(); // <<<< SAVE TO CACHE AFTER LOAD displayStatus(`Excel loaded. ${Object.keys(currentExcelData).length} sheets.`, false, isOnProgramPage); if (window.location.pathname.match(/^\/school\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\/($|onair\/$)/)) { addEpisodeButtonsToPage(); } } catch (err) { displayStatus(`Error processing Excel: ${err.message}`, true, isOnProgramPage); } }; reader.readAsArrayBuffer(file); } function saveToExcel() { /* ... (same as before, this is an explicit action, doesn't need to re-cache) ... */ const isOnProgramPage = !!document.getElementById('nhk-tracker-program-page-status'); if (Object.keys(currentExcelData).length === 0) { alert("No data to save."); displayStatus("No data to save.", true, isOnProgramPage); return; } const workbook = XLSX.utils.book_new(); for (const sheetName in currentExcelData) { if (currentExcelData.hasOwnProperty(sheetName) && currentExcelData[sheetName].length > 0) { const worksheet = XLSX.utils.json_to_sheet(currentExcelData[sheetName]); if (sheetName === PROGRAM_OVERVIEW_SHEET_NAME) { worksheet['!cols'] = [ { wch: 25 }, { wch: 40 }, { wch: 60 } ]; } else { worksheet['!cols'] = [ { wch: 40 }, { wch: 60 }, { wch: 10 } ]; } XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); } else if (currentExcelData.hasOwnProperty(sheetName)) { const emptyWs = XLSX.utils.json_to_sheet([]); XLSX.utils.book_append_sheet(workbook, emptyWs, sheetName); } } XLSX.writeFile(workbook, "nhk_school_tracker.xlsx"); displayStatus("Data exported to 'nhk_school_tracker.xlsx'.", false, isOnProgramPage); } // --- Logic for Program Overview Page (/school/program/) --- function scanProgramOverview() { displayStatus('Scanning program overview...'); let programOverviewData = []; // ... (rest of scanProgramOverview as before) ... const gradeListSection = document.getElementById('gradeList'); if (!gradeListSection) { displayStatus('Error: Main program section (id="gradeList") not found.', true); return; } const categoryBlocks = gradeListSection.querySelectorAll('div.listWrap[id]'); if (categoryBlocks.length === 0) { displayStatus('No category blocks found.', true); return; } categoryBlocks.forEach(block => { const categoryHeader = block.querySelector('h3'); const categoryRubyElement = categoryHeader ? categoryHeader.querySelector('ruby') : null; const categoryName = categoryRubyElement ? getRubyText(categoryRubyElement) : 'Unknown Category'; const programItems = block.querySelectorAll('div.itemKyouka'); programItems.forEach(item => { const anchorElement = item.querySelector('a'); if (!anchorElement) return; const titleDiv = anchorElement.querySelector('div.title'); const programName = titleDiv ? titleDiv.textContent.trim().replace(/\s+/g, ' ') : 'Unknown Program'; let programLink = anchorElement.getAttribute('href'); if (programLink) { try { programLink = new URL(programLink, window.location.href).href; } catch (e) { programLink = anchorElement.getAttribute('href'); } } else { programLink = '#'; } if (programName !== 'Unknown Program') { programOverviewData.push({ "Category (Subject)": categoryName, "Program Name": programName, "Program Link": programLink }); } }); }); if (programOverviewData.length > 0) { currentExcelData[PROGRAM_OVERVIEW_SHEET_NAME] = programOverviewData; cacheCurrentExcelData(); // <<<< SAVE TO CACHE displayStatus(`Program overview updated: ${programOverviewData.length} programs.`); } else { displayStatus('No programs found in overview scan.', true); } } // --- Logic for Individual Program Pages --- async function scanEpisodesForCurrentProgram() { const isOnProgramPage = true; // ... (rest of scanEpisodesForCurrentProgram as before - ENSURE IT CALLS cacheCurrentExcelData() on success) const programNameElement = document.querySelector('#programHeader h2, .program-header__title, .program-detail__title'); const programNameUnsanitized = programNameElement ? getRubyText(programNameElement) : "Unknown Program"; if (programNameUnsanitized === "Unknown Program") { displayStatus("Could not determine program name from this page.", true, isOnProgramPage); return; } const programName = sanitizeSheetName(programNameUnsanitized); // Use sanitized for sheet name, but display original if needed const programSheetName = `${programName} Episodes`; const currentPageUrl = window.location.href; displayStatus(`Scanning episodes for "${programNameUnsanitized}"...`, false, isOnProgramPage); let existingEpisodesInSheet = (currentExcelData[programSheetName] || []).reduce((acc, ep) => { if(ep["Episode Link"]) acc[ep["Episode Link"]] = ep["Watched"]; return acc; }, {}); let newEpisodeListData = []; try { let docToScrapeEpisodesFrom = document; if (!currentPageUrl.includes('/onair/') && !document.querySelector('div.programList div.itemList a')) { const navHeader = document.querySelector('nav.nav-header'); let episodeListPageUrl = null; if (navHeader) { const links = navHeader.querySelectorAll('a'); for(const link of links){ if(link.getAttribute('href') && (link.getAttribute('href').includes('/onair/') || link.textContent.includes('放送リスト'))) { episodeListPageUrl = new URL(link.getAttribute('href'), currentPageUrl).href; break; } } } if (!episodeListPageUrl) { const commonLink = document.querySelector('.onairLink a[href*="/onair/"], a.cbtn.school[href*="/onair/"]'); if (commonLink) episodeListPageUrl = new URL(commonLink.getAttribute('href'), currentPageUrl).href; } if (episodeListPageUrl && episodeListPageUrl !== currentPageUrl) { displayStatus(`Fetching episode list from ${episodeListPageUrl}...`, false, isOnProgramPage); docToScrapeEpisodesFrom = await fetchData(episodeListPageUrl); } else { displayStatus(`Attempting to scrape episodes from current page: ${programNameUnsanitized}`, false, isOnProgramPage); } } const episodeItems = docToScrapeEpisodesFrom.querySelectorAll('div.programList div.itemList a'); if (episodeItems.length === 0) { displayStatus(`No episode items found for "${programNameUnsanitized}" with the current selectors.`, true, isOnProgramPage); } episodeItems.forEach(item => { const subTitleElement = item.querySelector('div.subTitle'); const onairDateElement = item.querySelector('div.onair'); const episodeTitle = (subTitleElement ? subTitleElement.textContent.trim() : (onairDateElement ? onairDateElement.textContent.trim() : 'Unknown Episode')).replace(/\s+/g, ' '); let episodeLink = item.getAttribute('href'); if (episodeLink) { try { episodeLink = new URL(episodeLink, docToScrapeEpisodesFrom.baseURI || currentPageUrl).href; } catch(e) { console.warn("Invalid episode link:", item.getAttribute('href')); return; } } else { return; } const watchedStatus = existingEpisodesInSheet[episodeLink] || "No"; newEpisodeListData.push({ "Episode Title": episodeTitle, "Episode Link": episodeLink, "Watched": watchedStatus }); }); currentExcelData[programSheetName] = newEpisodeListData; cacheCurrentExcelData(); // <<<< SAVE TO CACHE displayStatus(`"${programSheetName}" sheet updated: ${newEpisodeListData.length} episodes.`, false, isOnProgramPage); addEpisodeButtonsToPage(); } catch (err) { displayStatus(`Error scanning episodes for "${programNameUnsanitized}": ${err.message}`, true, isOnProgramPage); console.error(err); } } function addEpisodeButtonsToPage() { const isOnProgramPage = true; // ... (rest of addEpisodeButtonsToPage - ENSURE button.onclick CALLS cacheCurrentExcelData()) const programNameElement = document.querySelector('#programHeader h2, .program-header__title, .program-detail__title'); let programNameUnsanitized = programNameElement ? getRubyText(programNameElement) : null; if (!programNameUnsanitized) { const pathParts = window.location.pathname.split('/').filter(Boolean); if (pathParts.length >= 3 && pathParts[0] === 'school') { let derivedName = pathParts[pathParts.length - (pathParts.includes('onair') ? 2 : 1)]; const overview = currentExcelData[PROGRAM_OVERVIEW_SHEET_NAME] || []; const progEntry = overview.find(p => p["Program Link"] && p["Program Link"].includes(`/${pathParts[1]}/${derivedName}/`)); programNameUnsanitized = progEntry ? progEntry["Program Name"] : derivedName; } if (!programNameUnsanitized) { console.warn("NHK Tracker: Could not determine program name for button addition."); return; } } programNameUnsanitized = programNameUnsanitized.replace(/\s+/g, ' '); const programName = sanitizeSheetName(programNameUnsanitized); const programSheetName = `${programName} Episodes`; const episodeListItems = document.querySelectorAll('div.programList div.itemList'); if (episodeListItems.length === 0) { if(document.querySelector('div.programList')){ // Only show prompt if programList div exists but no items displayStatus(`No episode items found on page for "${programNameUnsanitized}" to add buttons to.`, false, isOnProgramPage); } return; } const existingScanPrompt = document.querySelector('.nhk-scan-prompt-div'); if (existingScanPrompt) existingScanPrompt.remove(); if ((!currentExcelData[programSheetName] || currentExcelData[programSheetName].length === 0) && episodeListItems.length > 0) { const programListDiv = document.querySelector('div.programList'); if (programListDiv) { const scanPromptDiv = document.createElement('div'); scanPromptDiv.className = 'nhk-scan-prompt-div'; scanPromptDiv.innerHTML = `Episode data for "<strong>${programNameUnsanitized}</strong>" is not yet tracked. <br>`; const scanNowButton = document.createElement('button'); scanNowButton.textContent = 'Scan Episodes Now'; scanNowButton.className = 'nhk-tracker-btn nhk-tracker-btn-info'; scanNowButton.style.width = 'auto'; scanNowButton.style.display = 'inline-block'; scanNowButton.style.padding = '5px 10px'; scanNowButton.onclick = async () => { await scanEpisodesForCurrentProgram(); scanPromptDiv.remove(); }; scanPromptDiv.appendChild(scanNowButton); programListDiv.parentNode.insertBefore(scanPromptDiv, programListDiv); } return; } episodeListItems.forEach(itemDiv => { const anchor = itemDiv.querySelector('a'); if (!anchor) return; let episodeLink = anchor.getAttribute('href'); if (episodeLink) { try { episodeLink = new URL(episodeLink, window.location.href).href; } catch(e) { return; } } else { return; } const oldButton = itemDiv.querySelector('.nhk-episode-watched-toggle'); if (oldButton) oldButton.remove(); const episodeDataEntry = (currentExcelData[programSheetName] || []).find(ep => ep["Episode Link"] === episodeLink); let isWatched = false; let buttonText = "Mark Watched"; let buttonClass = "watched-false"; if (episodeDataEntry) { isWatched = (episodeDataEntry["Watched"] === "Yes"); buttonText = isWatched ? "Watched" : "Mark Watched"; buttonClass = isWatched ? "watched-true" : "watched-false"; } else { buttonText = "Not Tracked"; buttonClass = "not-tracked"; } const button = document.createElement('button'); button.className = `nhk-episode-watched-toggle ${buttonClass}`; button.textContent = buttonText; if (episodeDataEntry) { button.onclick = () => { if (!currentExcelData[programSheetName]) return; let epEntry = currentExcelData[programSheetName].find(ep => ep["Episode Link"] === episodeLink); if (epEntry) { epEntry["Watched"] = (epEntry["Watched"] === "Yes") ? "No" : "Yes"; isWatched = (epEntry["Watched"] === "Yes"); button.textContent = isWatched ? "Watched" : "Mark Watched"; button.classList.toggle('watched-true', isWatched); button.classList.toggle('watched-false', !isWatched); cacheCurrentExcelData(); // <<<< SAVE TO CACHE displayStatus(`Episode status updated. Save Excel to persist.`, false, isOnProgramPage); } }; } else { button.disabled = true; button.title = "Episode not in tracker. Scan episodes."; } const detailDiv = itemDiv.querySelector('div.detail'); if(detailDiv) detailDiv.appendChild(button); else itemDiv.appendChild(button); }); } // --- Page Specific UI Initialization --- function initPageControls() { const controlsContainerId = 'nhk-tracker-controls-container'; let isOnProgramPage = window.location.pathname.match(/^\/school\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\/($|onair\/$)/); if (document.getElementById(controlsContainerId)) { if (isOnProgramPage) { addEpisodeButtonsToPage(); } return; } // ... (rest of initPageControls as before, ensuring displayStatus targets correctly) const controlsContainer = document.createElement('div'); controlsContainer.id = controlsContainerId; const title = document.createElement('h4'); title.textContent = 'NHK Tracker'; title.style.textAlign = 'center'; title.style.marginBottom = '10px'; title.style.marginTop = '0'; const excelFileInput = document.createElement('input'); excelFileInput.type = 'file'; excelFileInput.id = 'excelFileInput'; excelFileInput.accept = ".xlsx, .xls"; excelFileInput.onchange = (event) => { const file = event.target.files[0]; if (file) { if (Object.keys(currentExcelData).length > 0) { if (!confirm("Loading this Excel will replace any currently cached data. Continue?")) { event.target.value = null; return; } } handleExcelFileLoad(file); // This now calls cacheCurrentExcelData } event.target.value = null; }; const loadExcelButton = document.createElement('button'); loadExcelButton.textContent = 'Load Data (Excel)'; loadExcelButton.className = 'nhk-tracker-btn'; loadExcelButton.onclick = () => excelFileInput.click(); const saveExcelButton = document.createElement('button'); saveExcelButton.textContent = 'Save Data (Excel)'; saveExcelButton.className = 'nhk-tracker-btn nhk-tracker-btn-success'; saveExcelButton.onclick = saveToExcel; controlsContainer.appendChild(title); controlsContainer.appendChild(loadExcelButton); controlsContainer.appendChild(excelFileInput); controlsContainer.appendChild(saveExcelButton); let statusDivToUse = statusDivGlobal; if (window.location.pathname === '/school/program/') { const scanOverviewButton = document.createElement('button'); scanOverviewButton.textContent = 'Scan/Update Program List'; scanOverviewButton.className = 'nhk-tracker-btn nhk-tracker-btn-info'; scanOverviewButton.onclick = scanProgramOverview; // This now calls cacheCurrentExcelData controlsContainer.appendChild(scanOverviewButton); } else if (isOnProgramPage) { const scanEpisodesButton = document.createElement('button'); scanEpisodesButton.textContent = 'Scan/Update Episodes (This Program)'; scanEpisodesButton.className = 'nhk-tracker-btn nhk-tracker-btn-info'; scanEpisodesButton.onclick = scanEpisodesForCurrentProgram; // This now calls cacheCurrentExcelData controlsContainer.appendChild(scanEpisodesButton); statusDivOnProgramPage.id = 'nhk-tracker-program-page-status'; statusDivToUse = statusDivOnProgramPage; } controlsContainer.appendChild(statusDivToUse); document.body.appendChild(controlsContainer); console.log("NHK Tracker: initPageControls - On program page. Will attempt to add episode buttons after a short delay."); setTimeout(() => { console.log("NHK Tracker: initPageControls - setTimeout fired. Calling addEpisodeButtonsToPage."); addEpisodeButtonsToPage(); }, 1000); } // --- Main Execution --- loadExcelDataFromCache(); // Load any previously saved data at the very start initPageControls(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址