您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在BGM.tv的追番页面添加设置按钮,可以设置番剧的播放时间并排序
// ==UserScript== // @name BGM Anime Time Set // @namespace http://tampermonkey.net/ // @version 1.2 // @description 在BGM.tv的追番页面添加设置按钮,可以设置番剧的播放时间并排序 // @author age // @match https://bgm.tv/ // @match https://bangumi.tv/ // @match https://chii.in/ // @license MIT // ==/UserScript== (function() { 'use strict'; // 常量定义 const STORAGE_KEY = 'BGM_HOME_ANIME_TIME_SET_AGE'; const SETTINGS_KEY = 'BGM_HOME_ANIME_TIME_SETTINGS_AGE'; const EXPIRATION_DAYS = 200; const WEEK_DAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; // 缓存DOM元素 let cachedContainer = null; let cachedAnimeTimeData = null; let cachedSettings = null; // 添加CSS样式 const style = document.createElement('style'); style.textContent = ` #Age-js01-button { margin-left: 5px; padding: 1px 5px; border-radius: 3px; border: none; font-size: 12px; cursor: pointer; background-color: #2e2e2e; color: #eee; } #Age-js01-button.past { background-color: #118FDD; } #Age-js01-button.future { background-color: #10C745; } #Age-js01-button.soon { background-color: #FF3333; } #Age-js01-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px; border-radius: 4px; box-shadow: 0 0 10px rgba(0,0,0,0.5); z-index: 9999; font-size: 14px; background-color: #333; color: #fff; min-width: 300px; } #Age-js01-select, #Age-js01-time { padding: 3px; border-radius: 3px; background-color: #444; color: #fff; } #Age-js01-time { margin-right: 8px; } #Age-js01-button-container { margin-top: 10px; text-align: right; } #Age-js01-save, #Age-js01-cancel, #Age-js01-clear { padding: 3px 8px; border-radius: 3px; border: none; font-size: 12px; cursor: pointer; background-color: #444; color: #fff; } #Age-js01-clear { margin-right: 8px; } #Age-js01-manager-button { margin-left: 1px; border-radius: 100px; border: none; font-size: 22px; cursor: pointer; background-color: #3f3e3f; color: #eee; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 2px 6px; width: 25px; height: 40px; } #Age-js01-manager-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px; border-radius: 4px; box-shadow: 0 0 10px rgba(0,0,0,0.5); z-index: 9999; background-color: #333; color: #fff; min-width: 300px; max-height: 80vh; overflow-y: auto; } #Age-js01-manager-modal h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 5px; } #Age-js01-manager-modal button { padding: 3px 8px; border-radius: 3px; border: none; font-size: 12px; cursor: pointer; background-color: #444; color: #fff; margin: 5px; } #Age-js01-manager-modal-content { margin: 10px 0; } #Age-js01-storage-content { background-color: #444; padding: 10px; border-radius: 3px; max-height: 200px; overflow-y: auto; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; } #Age-js01-timezone-select { padding: 3px; border-radius: 3px; background-color: #444; color: #fff; margin-left: 8px; } #Age-js01-show-style-select { padding: 3px; border-radius: 3px; background-color: #444; color: #fff; margin-left: 8px; } html[data-theme='light'] #Age-js01-button { background-color: #f5f5f5; color: #333; } html[data-theme='light'] #Age-js01-button.past { background-color: #AEEFFF; } html[data-theme='light'] #Age-js01-button.future { background-color: #AEFFB8; } html[data-theme='light'] #Age-js01-button.soon { background-color: #FFAAAA; } html[data-theme='light'] #Age-js01-dialog { background-color: white; color: #333; } html[data-theme='light'] #Age-js01-select, html[data-theme='light'] #Age-js01-time, html[data-theme='light'] #Age-js01-timezone-select, html[data-theme='light'] #Age-js01-show-style-select { background-color: #fff; color: #333; } html[data-theme='light'] #Age-js01-save, html[data-theme='light'] #Age-js01-cancel, html[data-theme='light'] #Age-js01-clear { background-color: #f5f5f5; color: #333; } html[data-theme='light'] #Age-js01-manager-button { background-color: #fff; color: #333; } html[data-theme='light'] #Age-js01-manager-modal { background-color: white; color: #333; } html[data-theme='light'] #Age-js01-manager-modal h3 { border-bottom-color: #ddd; } html[data-theme='light'] #Age-js01-manager-modal button { background-color: #f5f5f5; color: #333; } html[data-theme='light'] #Age-js01-storage-content { background-color: #f5f5f5; } `; document.head.appendChild(style); // 数据存储操作 function getAnimeTimeData() { if (cachedAnimeTimeData) return cachedAnimeTimeData; const data = localStorage.getItem(STORAGE_KEY); if (!data) return {}; try { const parsed = JSON.parse(data); // 检查并清理过期数据 const now = new Date(); const cleanedData = {}; for (const [id, entry] of Object.entries(parsed)) { if (entry.expiresAt && new Date(entry.expiresAt) > now) { cleanedData[id] = { weekDay: entry.weekDay, time: entry.time }; } } cachedAnimeTimeData = cleanedData; return cachedAnimeTimeData; } catch (e) { console.error('Failed to parse anime time data:', e); return {}; } } function setAnimeTimeData(data) { const now = new Date(); const storageData = {}; // 保留现有的过期时间,只更新修改的条目 const existingData = getAnimeTimeData(); for (const [id, entry] of Object.entries(data)) { storageData[id] = { ...entry, // 如果条目已存在且未被修改,保留原过期时间 expiresAt: existingData[id] && existingData[id].weekDay === entry.weekDay && existingData[id].time === entry.time ? existingData[id].expiresAt : new Date(now.getTime() + EXPIRATION_DAYS * 24 * 60 * 60 * 1000).toISOString() }; } localStorage.setItem(STORAGE_KEY, JSON.stringify(storageData)); cachedAnimeTimeData = data; } function getSettings() { if (cachedSettings) return cachedSettings; const settings = localStorage.getItem(SETTINGS_KEY); if (!settings) return getDefaultSettings(); try { const parsed = JSON.parse(settings); cachedSettings = validateSettings(parsed); return cachedSettings; } catch (e) { console.error('Failed to parse settings:', e); return getDefaultSettings(); } } function getDefaultSettings() { return { setShow: true, showStyleRed: 0, showStyleGreen: 0, showStyleBlue: 0 }; } function validateSettings(settings) { // 确保设置存在且是有效范围内的数字 if (typeof settings.showStyleRed !== 'number' || settings.showStyleRed < 0 || settings.showStyleRed > 3) { settings.showStyleRed = 0; } if (typeof settings.showStyleGreen !== 'number' || settings.showStyleGreen < 0 || settings.showStyleGreen > 5) { settings.showStyleGreen = 0; } if (typeof settings.showStyleBlue !== 'number' || settings.showStyleBlue < 0 || settings.showStyleBlue > 3) { settings.showStyleBlue = 0; } return settings; } function setSettings(settings) { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); cachedSettings = settings; } // 主初始化函数 function init() { cachedContainer = document.getElementById('cloumnSubjectInfo'); if (!cachedContainer) return; // 添加管理按钮 addManagerButton(); // 初始化数据 cachedAnimeTimeData = getAnimeTimeData(); cachedSettings = getSettings(); // 为每个番剧添加设置按钮 addSetButtons(); // 根据设置显示/隐藏SET按钮 toggleSetButtons(cachedSettings.setShow); // 重新排序番剧列表 sortAnimeList(); } // 添加管理按钮 function addManagerButton() { const prgManagerMode = document.getElementById('prgManagerMode'); if (!prgManagerMode || document.getElementById('Age-js01-manager-button')) return; const managerButton = document.createElement('button'); managerButton.id = 'Age-js01-manager-button'; managerButton.innerHTML = '<div style="text-align:center;line-height:2;">⏰︎</div><div style="text-align:center;line-height:0;"></div>'; managerButton.addEventListener('click', showManagerModal); prgManagerMode.appendChild(managerButton); } // 显示管理框 function showManagerModal() { const modal = document.createElement('div'); modal.id = 'Age-js01-manager-modal'; modal.innerHTML = ` <h3>时间管理</h3> <div id="Age-js01-manager-modal-content"> <div> <button id="Age-js01-auto-fetch">自动获取时间</button> <select id="Age-js01-timezone-select"> <option value="-12">UTC-12</option> <option value="-11">UTC-11</option> <option value="-10">UTC-10</option> <option value="-9">UTC-9</option> <option value="-8">UTC-8</option> <option value="-7">UTC-7</option> <option value="-6">UTC-6</option> <option value="-5">UTC-5</option> <option value="-4">UTC-4</option> <option value="-3">UTC-3</option> <option value="-2">UTC-2</option> <option value="-1">UTC-1</option> <option value="0">UTC±0</option> <option value="1">UTC+1</option> <option value="2">UTC+2</option> <option value="3">UTC+3</option> <option value="4">UTC+4</option> <option value="5">UTC+5</option> <option value="6">UTC+6</option> <option value="7">UTC+7</option> <option value="8" selected>UTC+8</option> <option value="9">UTC+9</option> <option value="10">UTC+10</option> <option value="11">UTC+11</option> <option value="12">UTC+12</option> </select> </div> <div style="margin-top: 15px;"> <h4>显示样式设置</h4> <div> <label for="Age-js01-show-style-red">红:</label> <select id="Age-js01-show-style-red"> <option value="0">1小时内即将放送</option> <option value="1">2小时内即将放送</option> <option value="2">4小时内即将放送</option> <option value="3">禁用</option> </select> </div> <div style="margin-top: 5px;"> <label for="Age-js01-show-style-green">绿:</label> <select id="Age-js01-show-style-green"> <option value="0">18小时内即将放送</option> <option value="1">24小时内即将放送</option> <option value="2">今天内即将放送</option> <option value="3">明天6点前即将放送</option> <option value="4">明天8点前即将放送</option> <option value="5">禁用</option> </select> </div> <div style="margin-top: 5px;"> <label for="Age-js01-show-style-blue">蓝:</label> <select id="Age-js01-show-style-blue"> <option value="0">18小时内已经放送</option> <option value="1">24小时内已经放送</option> <option value="2">今天内已经放送</option> <option value="3">禁用</option> </select> </div> </div> <button id="Age-js01-toggle-set">${cachedSettings.setShow ? '隐藏所有SET按钮' : '显示所有SET按钮'}</button> <div style="margin-top: 15px;"> <h4>存储内容 (${STORAGE_KEY})</h4> <div id="Age-js01-storage-content" contenteditable="true">${JSON.stringify(getFullStorageData(), null, 2)}</div> </div> </div> <div style="text-align: right;"> <button id="Age-js01-save-storage">保存修改</button> <button id="Age-js01-close-manager">关闭</button> </div> `; // 设置当前显示样式 modal.querySelector('#Age-js01-show-style-red').value = cachedSettings.showStyleRed; modal.querySelector('#Age-js01-show-style-green').value = cachedSettings.showStyleGreen; modal.querySelector('#Age-js01-show-style-blue').value = cachedSettings.showStyleBlue; document.body.appendChild(modal); // 使用事件委托处理模态框内的事件 modal.addEventListener('click', (e) => { const target = e.target; if (target.id === 'Age-js01-auto-fetch') { autoFetchSchedule(); } else if (target.id === 'Age-js01-toggle-set') { const newSettings = { ...cachedSettings, setShow: !cachedSettings.setShow }; setSettings(newSettings); toggleSetButtons(newSettings.setShow); target.textContent = newSettings.setShow ? '隐藏所有SET按钮' : '显示所有SET按钮'; } else if (target.id === 'Age-js01-save-storage') { try { const newData = JSON.parse(document.getElementById('Age-js01-storage-content').textContent); // 保留未修改条目的过期时间 const existingData = getFullStorageData(); const now = new Date(); const mergedData = {}; for (const [id, entry] of Object.entries(newData)) { mergedData[id] = { ...entry, expiresAt: existingData[id] && existingData[id].weekDay === entry.weekDay && existingData[id].time === entry.time ? existingData[id].expiresAt : new Date(now.getTime() + EXPIRATION_DAYS * 24 * 60 * 60 * 1000).toISOString() }; } localStorage.setItem(STORAGE_KEY, JSON.stringify(mergedData)); cachedAnimeTimeData = getAnimeTimeData(); // 重新加载数据 addSetButtons(); sortAnimeList(); alert('保存成功'); } catch (e) { alert('保存失败: JSON格式错误'); console.error(e); } } else if (target.id === 'Age-js01-close-manager') { const newSettings = { ...cachedSettings, showStyleRed: parseInt(modal.querySelector('#Age-js01-show-style-red').value), showStyleGreen: parseInt(modal.querySelector('#Age-js01-show-style-green').value), showStyleBlue: parseInt(modal.querySelector('#Age-js01-show-style-blue').value) }; setSettings(newSettings); addSetButtons(); sortAnimeList(); document.body.removeChild(modal); } }); } // 获取完整的存储数据(包括过期时间) function getFullStorageData() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : {}; } // 自动获取时间表 async function autoFetchSchedule() { if (!confirm('此操作将清空目前的时间表,是否继续?')) return; try { const subjectLinks = document.querySelectorAll('#cloumnSubjectInfo .infoWrapper_tv.hidden.clearit a[href^="/subject/"]'); const subjectIds = new Set(); subjectLinks.forEach(link => { const href = link.getAttribute('href'); const match = href.match(/^\/subject\/(\d+)/); if (match && match[1]) subjectIds.add(match[1]); }); if (subjectIds.size === 0) { alert('未找到任何动画条目ID'); return; } const response = await fetch('https://raw.githubusercontent.com/zhollgit/bgm-onair/main/onair.json'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); const newAnimeTimeData = {}; const timezoneOffset = parseInt(document.getElementById('Age-js01-timezone-select')?.value || 8); const now = new Date(); data.items.forEach(item => { const bangumiSite = item.sites.find(site => site.site === 'bangumi'); if (bangumiSite?.id && subjectIds.has(bangumiSite.id) && item.begin) { const beginDate = new Date(item.begin); let adjustedHours = beginDate.getUTCHours() + timezoneOffset; let adjustedDay = beginDate.getUTCDay(); if (adjustedHours >= 24) { adjustedHours -= 24; adjustedDay = (adjustedDay + 1) % 7; } else if (adjustedHours < 0) { adjustedHours += 24; adjustedDay = (adjustedDay - 1 + 7) % 7; } const time = `${adjustedHours.toString().padStart(2, '0')}:${beginDate.getUTCMinutes().toString().padStart(2, '0')}`; newAnimeTimeData[bangumiSite.id] = { weekDay: adjustedDay, time, expiresAt: new Date(now.getTime() + EXPIRATION_DAYS * 24 * 60 * 60 * 1000).toISOString() }; } }); localStorage.setItem(STORAGE_KEY, JSON.stringify(newAnimeTimeData)); cachedAnimeTimeData = getAnimeTimeData(); addSetButtons(); sortAnimeList(); const storageContent = document.getElementById('Age-js01-storage-content'); if (storageContent) storageContent.textContent = JSON.stringify(newAnimeTimeData, null, 2); alert(`成功获取 ${Object.keys(newAnimeTimeData).length} 个番剧的时间数据`); } catch (error) { console.error('自动获取时间表失败:', error); alert('自动获取时间表失败: ' + error.message); } } // 切换SET按钮的显示/隐藏 function toggleSetButtons(show) { document.querySelectorAll('#Age-js01-button').forEach(button => { const subjectId = button.getAttribute('data-subject-id'); if (!cachedAnimeTimeData[subjectId]) { button.style.display = show ? '' : 'none'; } }); } // 添加设置按钮 function addSetButtons() { const editLinks = cachedContainer.querySelectorAll('a.thickbox.l[id^="sbj_prg_"]:not([data-processed])'); editLinks.forEach(editLink => { if (editLink.textContent.trim() === '[edit]') return; const subjectId = editLink.id.split('_')[2]; editLink.setAttribute('data-processed', 'true'); const setButton = document.createElement('button'); setButton.id = 'Age-js01-button'; setButton.setAttribute('data-subject-id', subjectId); if (cachedAnimeTimeData[subjectId]) { setButton.textContent = formatTimeData(cachedAnimeTimeData[subjectId]); const timeStatus = getTimeStatus(cachedAnimeTimeData[subjectId]); if (timeStatus) setButton.classList.add(timeStatus); } else { setButton.textContent = 'SET'; if (!cachedSettings.setShow) setButton.style.display = 'none'; } setButton.addEventListener('click', () => showTimeSettingDialog(subjectId, setButton)); editLink.parentNode.insertBefore(setButton, editLink.nextSibling); }); } // 获取时间状态 function getTimeStatus(timeData) { const now = new Date(); const today = now.getDay(); const currentHours = now.getHours(); const currentMinutes = now.getMinutes(); const [hours, minutes] = timeData.time.split(':').map(Number); const targetDay = timeData.weekDay; let dayDiff = targetDay - today; if (dayDiff < -3) dayDiff += 7; else if (dayDiff > 3) dayDiff -= 7; const totalDiffHours = dayDiff * 24 + (hours - currentHours) + (minutes - currentMinutes) / 60; // 红色优先级最高 if (cachedSettings.showStyleRed !== 3) { switch (cachedSettings.showStyleRed) { case 0: if (totalDiffHours >= 0 && totalDiffHours < 1) return 'soon'; break; case 1: if (totalDiffHours >= 0 && totalDiffHours < 2) return 'soon'; break; case 2: if (totalDiffHours >= 0 && totalDiffHours < 4) return 'soon'; break; } } // 然后检查绿色条件 if (cachedSettings.showStyleGreen !== 5) { switch (cachedSettings.showStyleGreen) { case 0: if (totalDiffHours >= 0 && totalDiffHours < 18) return 'future'; break; case 1: if (totalDiffHours >= 0 && totalDiffHours < 24) return 'future'; break; case 2: if (dayDiff === 0 && totalDiffHours >= 0) return 'future'; break; case 3: if ((dayDiff === 0 && totalDiffHours >= 0) || (dayDiff === 1 && hours < 6)) return 'future'; break; case 4: if ((dayDiff === 0 && totalDiffHours >= 0) || (dayDiff === 1 && hours < 8)) return 'future'; break; } } // 最后检查蓝色条件 if (cachedSettings.showStyleBlue !== 3) { switch (cachedSettings.showStyleBlue) { case 0: if (totalDiffHours >= -18 && totalDiffHours < 0) return 'past'; break; case 1: if (totalDiffHours >= -24 && totalDiffHours < 0) return 'past'; break; case 2: if (dayDiff === 0 && totalDiffHours < 0) return 'past'; break; } } return ''; } // 显示时间设置对话框 function showTimeSettingDialog(subjectId, button) { const dialog = document.createElement('div'); dialog.id = 'Age-js01-dialog'; const weekDaySelect = document.createElement('select'); weekDaySelect.id = 'Age-js01-select'; WEEK_DAYS.forEach((day, index) => { const option = document.createElement('option'); option.value = index; option.textContent = day; weekDaySelect.appendChild(option); }); const timeInput = document.createElement('input'); timeInput.id = 'Age-js01-time'; timeInput.type = 'time'; if (cachedAnimeTimeData[subjectId]) { weekDaySelect.value = cachedAnimeTimeData[subjectId].weekDay; timeInput.value = cachedAnimeTimeData[subjectId].time; } dialog.innerHTML = ` <label>星期: </label> <select id="Age-js01-select">${WEEK_DAYS.map((day, i) => `<option value="${i}">${day}</option>`).join('')}</select> <label style="margin-left:8px;">时间: </label> <input id="Age-js01-time" type="time" ${cachedAnimeTimeData[subjectId] ? `value="${cachedAnimeTimeData[subjectId].time}"` : ''}> <div id="Age-js01-button-container"> <button id="Age-js01-save">保存</button> <button id="Age-js01-clear">清除</button> <button id="Age-js01-cancel">取消</button> </div> `; document.body.appendChild(dialog); // 事件处理 dialog.addEventListener('click', (e) => { if (e.target.id === 'Age-js01-save') { const weekDay = parseInt(dialog.querySelector('#Age-js01-select').value); const time = dialog.querySelector('#Age-js01-time').value; if (!time) { alert('请选择时间'); return; } // 获取当前存储的完整数据 const fullData = getFullStorageData(); const now = new Date(); // 更新数据 fullData[subjectId] = { weekDay, time, expiresAt: new Date(now.getTime() + EXPIRATION_DAYS * 24 * 60 * 60 * 1000).toISOString() }; // 保存更新 localStorage.setItem(STORAGE_KEY, JSON.stringify(fullData)); cachedAnimeTimeData = getAnimeTimeData(); button.textContent = formatTimeData(cachedAnimeTimeData[subjectId]); button.className = 'Age-js01-button'; const timeStatus = getTimeStatus(cachedAnimeTimeData[subjectId]); if (timeStatus) button.classList.add(timeStatus); sortAnimeList(); document.body.removeChild(dialog); } else if (e.target.id === 'Age-js01-clear') { // 获取当前存储的完整数据 const fullData = getFullStorageData(); if (fullData[subjectId]) { delete fullData[subjectId]; localStorage.setItem(STORAGE_KEY, JSON.stringify(fullData)); cachedAnimeTimeData = getAnimeTimeData(); button.textContent = 'SET'; button.className = 'Age-js01-button'; sortAnimeList(); } document.body.removeChild(dialog); } else if (e.target.id === 'Age-js01-cancel') { document.body.removeChild(dialog); } }); } // 格式化时间数据显示 function formatTimeData(timeData) { return `${WEEK_DAYS[timeData.weekDay]} ${timeData.time}`; } // 排序 function sortAnimeList() { const wrapper = cachedContainer.querySelector('.infoWrapperContainer.infoWrapper_tv.hidden.clearit'); if (!wrapper) return; const animeItems = Array.from(wrapper.querySelectorAll('.clearit.infoWrapper')); animeItems.sort((a, b) => { const aId = a.id.split('_')[1]; const bId = b.id.split('_')[1]; const aData = cachedAnimeTimeData[aId]; const bData = cachedAnimeTimeData[bId]; if (!aData && !bData) return 0; if (!aData) return 1; if (!bData) return -1; if (aData.weekDay !== bData.weekDay) return aData.weekDay - bData.weekDay; return aData.time.localeCompare(bData.time); }); // 使用文档片段减少重绘 const fragment = document.createDocumentFragment(); animeItems.forEach(item => fragment.appendChild(item)); wrapper.appendChild(fragment); } // 使用DOMContentLoaded而不是load事件,加快响应速度 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 0); } })();