// ==UserScript==
// @name YAKITORY 控制面板 - iLearn 影音平台自動播放、自動靜音
// @namespace https://github.com/poterpan/tampermonkey-scripts/yakitory-helper
// @version 2.2
// @description 自動點擊YAKITORY影音平台上的確認按鈕,結束時通知,自動播放下一部影片,自動靜音,以及提供控制面板
// @author PoterPan
// @match *://ilearn.fcu.edu.tw/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @grant GM_addStyle
// @homepageURL https://github.com/poterpan/tampermonkey-scripts
// @supportURL https://github.com/poterpan/tampermonkey-scripts/issues
// ==/UserScript==
(function() {
'use strict';
// ===================== 配置選項 =====================
// 設定檢查間隔(毫秒)
const checkInterval = 2000; // 每2秒檢查一次
// 影片接近結束的閾值(百分比)
const endThreshold = 95; // 當影片播放進度達到95%時發出通知
// 影片完全結束的閾值(百分比)
const completeThreshold = 99; // 當影片播放進度達到99%時視為已完成
// 通知聲音URL (公開可訪問的音效文件)
const notificationSound = 'https://cdn.pixabay.com/download/audio/2022/01/18/audio_a29a673ef4.mp3?filename=decidemp3-14575.mp3'; // 替換為您喜歡的音效
// 自動播放檢查的時間間隔(毫秒)
const autoPlayCheckInterval = 2000; // 跳轉到新頁面後,自動播放檢查的時間間隔
// ===================== 全局變量 =====================
// 狀態控制
let notificationSent = false;
let videoCompleted = false;
let lastVideoInfo = GM_getValue('lastVideoInfo', {
title: '無記錄',
progress: '0%',
timestamp: 0
});
// 功能開關
let autoClickerEnabled = GM_getValue('autoClickerEnabled', true);
let autoNextEnabled = GM_getValue('autoNextEnabled', false);
let soundEnabled = GM_getValue('soundEnabled', true);
let autoMuteEnabled = GM_getValue('autoMuteEnabled', false); // 自動靜音功能默認關閉
let isPanelExpanded = GM_getValue('isPanelExpanded', true);
// 當前影片信息
let currentVideoInfo = {
title: '未檢測到影片',
progress: '0%'
};
// ===================== 控制面板 =====================
// 添加自訂CSS樣式
GM_addStyle(`
#yakitory-control-panel {
transition: all 0.3s ease;
font-family: Arial, sans-serif;
}
#yakitory-control-panel.collapsed {
width: 40px !important;
height: 40px !important;
overflow: hidden;
border-radius: 50%;
}
#yakitory-control-panel.collapsed #yakitory-panel-content {
display: none;
}
#yakitory-toggle-panel {
font-size: 16px;
font-weight: bold;
}
#yakitory-status-icon {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 5px;
}
.yakitory-controls {
margin-bottom: 8px;
}
.yakitory-slider {
width: 100%;
margin-top: 5px;
}
`);
// 創建控制面板
function createControlPanel() {
const panelDiv = document.createElement('div');
panelDiv.id = 'yakitory-control-panel';
panelDiv.classList.add(isPanelExpanded ? 'expanded' : 'collapsed');
panelDiv.style.position = 'fixed';
panelDiv.style.top = '100px';
panelDiv.style.right = '20px';
panelDiv.style.zIndex = '9999';
panelDiv.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
panelDiv.style.border = '1px solid #ccc';
panelDiv.style.borderRadius = '5px';
panelDiv.style.padding = '10px';
panelDiv.style.width = '250px';
panelDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
panelDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<strong style="white-space: nowrap; overflow: hidden;">YAKITORY 控制</strong>
<button id="yakitory-toggle-panel" style="background: none; border: none; cursor: pointer;">${isPanelExpanded ? '_' : '+'}</button>
</div>
<div id="yakitory-panel-content" style="${isPanelExpanded ? '' : 'display: none;'}">
<div class="yakitory-controls">
<label style="display: flex; align-items: center;">
<input type="checkbox" id="yakitory-autoclicker-enabled" ${autoClickerEnabled ? 'checked' : ''}>
<span style="margin-left: 5px;">自動點擊確認</span>
</label>
</div>
<div class="yakitory-controls">
<label style="display: flex; align-items: center;">
<input type="checkbox" id="yakitory-autonext-enabled" ${autoNextEnabled ? 'checked' : ''}>
<span style="margin-left: 5px;">自動播放下一部</span>
</label>
</div>
<div class="yakitory-controls">
<label style="display: flex; align-items: center;">
<input type="checkbox" id="yakitory-sound-enabled" ${soundEnabled ? 'checked' : ''}>
<span style="margin-left: 5px;">結束通知聲音</span>
</label>
</div>
<div class="yakitory-controls">
<label style="display: flex; align-items: center;">
<input type="checkbox" id="yakitory-automute-enabled" ${autoMuteEnabled ? 'checked' : ''}>
<span style="margin-left: 5px;">自動靜音影片</span>
</label>
</div>
<div style="margin-top: 10px; font-size: 12px;">
<strong>當前影片:</strong><br><span id="yakitory-current-video" style="word-break: break-all;">-</span><br>
<strong>播放進度:</strong> <span id="yakitory-progress">-</span>
</div>
<div style="margin-top: 10px; font-size: 12px;">
<strong>上次完成:</strong><br><span id="yakitory-last-video" style="word-break: break-all;">${lastVideoInfo.title || '-'}</span>
<br><span id="yakitory-last-time">${formatTimestamp(lastVideoInfo.timestamp)}</span>
</div>
<div style="margin-top: 10px; text-align: center;">
<span id="yakitory-status-icon" style="background-color: green;"></span>
<span id="yakitory-status" style="font-size: 12px;">運行中</span>
</div>
<div style="margin-top: 6px; text-align: center;">
<button id="yakitory-test-play" style="font-size: 11px; padding: 2px 6px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;">測試自動播放</button>
</div>
</div>
`;
document.body.appendChild(panelDiv);
// 添加控制面板拖曳功能
makeDraggable(panelDiv);
// 綁定事件
document.getElementById('yakitory-toggle-panel').addEventListener('click', togglePanel);
document.getElementById('yakitory-autoclicker-enabled').addEventListener('change', function(e) {
autoClickerEnabled = e.target.checked;
GM_setValue('autoClickerEnabled', autoClickerEnabled);
updateStatusDisplay();
});
document.getElementById('yakitory-autonext-enabled').addEventListener('change', function(e) {
autoNextEnabled = e.target.checked;
GM_setValue('autoNextEnabled', autoNextEnabled);
updateStatusDisplay();
});
document.getElementById('yakitory-sound-enabled').addEventListener('change', function(e) {
soundEnabled = e.target.checked;
GM_setValue('soundEnabled', soundEnabled);
});
document.getElementById('yakitory-automute-enabled').addEventListener('change', function(e) {
autoMuteEnabled = e.target.checked;
GM_setValue('autoMuteEnabled', autoMuteEnabled);
// 如果開啟了自動靜音,立即對當前影片進行靜音處理
if (autoMuteEnabled) {
muteCurrentVideo();
}
});
// 測試自動播放按鈕
document.getElementById('yakitory-test-play').addEventListener('click', function() {
const played = autoPlayVideo();
if (!played) {
alert('未找到可播放的影片或播放失敗,請手動點擊播放按鈕');
} else {
this.textContent = '播放成功!';
setTimeout(() => {
this.textContent = '測試自動播放';
}, 2000);
}
});
// 如果啟用了自動靜音,立即嘗試靜音當前影片
if (autoMuteEnabled) {
setTimeout(muteCurrentVideo, 1000);
}
// 初始顯示
updateVideoInfoDisplay();
updateStatusDisplay();
}
// 使控制面板可拖曳
function makeDraggable(elem) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
elem.style.cursor = 'move';
elem.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// 獲取滑鼠位置
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// 當滑鼠移動時調用elementDrag函數
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// 計算新位置
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// 設置元素的新位置
elem.style.top = (elem.offsetTop - pos2) + "px";
elem.style.right = (parseInt(elem.style.right) + pos1) + "px";
}
function closeDragElement() {
// 停止移動
document.onmouseup = null;
document.onmousemove = null;
}
}
// 切換控制面板展開/收起狀態
function togglePanel() {
const panel = document.getElementById('yakitory-control-panel');
const content = document.getElementById('yakitory-panel-content');
const toggleBtn = document.getElementById('yakitory-toggle-panel');
isPanelExpanded = !isPanelExpanded;
GM_setValue('isPanelExpanded', isPanelExpanded);
if (isPanelExpanded) {
panel.classList.remove('collapsed');
panel.classList.add('expanded');
content.style.display = '';
toggleBtn.textContent = '_';
} else {
panel.classList.remove('expanded');
panel.classList.add('collapsed');
content.style.display = 'none';
toggleBtn.textContent = '+';
}
}
// 更新狀態顯示
function updateStatusDisplay() {
const statusIcon = document.getElementById('yakitory-status-icon');
const statusText = document.getElementById('yakitory-status');
if (autoClickerEnabled || autoNextEnabled || autoMuteEnabled) {
statusIcon.style.backgroundColor = 'green';
statusText.textContent = '運行中';
statusText.style.color = 'green';
} else {
statusIcon.style.backgroundColor = 'gray';
statusText.textContent = '已暫停';
statusText.style.color = 'gray';
}
}
// 更新影片信息顯示
function updateVideoInfoDisplay() {
const currentVideoElem = document.getElementById('yakitory-current-video');
const progressElem = document.getElementById('yakitory-progress');
const lastVideoElem = document.getElementById('yakitory-last-video');
const lastTimeElem = document.getElementById('yakitory-last-time');
if (currentVideoElem) currentVideoElem.textContent = currentVideoInfo.title || '-';
if (progressElem) progressElem.textContent = currentVideoInfo.progress || '-';
if (lastVideoElem) lastVideoElem.textContent = lastVideoInfo.title || '-';
if (lastTimeElem) lastTimeElem.textContent = formatTimestamp(lastVideoInfo.timestamp);
}
// 格式化時間戳
function formatTimestamp(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return `${date.getMonth()+1}/${date.getDate()} ${padZero(date.getHours())}:${padZero(date.getMinutes())}`;
}
// 數字前綴補零
function padZero(num) {
return num < 10 ? '0' + num : num;
}
// ===================== 影片播放相關函數 =====================
// 檢測目前頁面的影片標題
function detectVideoTitle() {
// 嘗試多種可能的標題選擇器
const titleSelectors = [
'.page-header-headings h1', // 頁面標題
'.activityname', // 活動名稱
'.modvideos_videojs .video-js', // 影片容器
'h1.h2', // 標題元素
'title' // 頁面標題
];
let title = '';
for (const selector of titleSelectors) {
const element = document.querySelector(selector);
if (element) {
title = element.textContent.trim();
if (title) break;
}
}
return title || '未知影片';
}
// 播放聲音
function playNotificationSound() {
if (!soundEnabled) return;
const audio = new Audio(notificationSound);
audio.volume = 0.8; // 音量 (0.0 到 1.0)
// 嘗試播放
audio.play().catch(error => {
console.log('無法播放通知聲音:', error);
});
}
// 監控影片進度並在接近結束時通知
function checkVideoProgress() {
const progressControl = document.querySelector('.vjs-progress-control');
if (!progressControl) return;
const progressHolder = progressControl.querySelector('.vjs-progress-holder');
if (!progressHolder || !progressHolder.hasAttribute('aria-valuenow')) return;
const progress = parseFloat(progressHolder.getAttribute('aria-valuenow'));
const totalTime = progressHolder.getAttribute('aria-valuetext') || '';
// 更新當前影片信息
currentVideoInfo.title = detectVideoTitle();
currentVideoInfo.progress = `${progress.toFixed(1)}%`;
updateVideoInfoDisplay();
// 檢查影片是否接近結束且尚未發送過通知
if (progress >= endThreshold && !notificationSent) {
// 發送通知
GM_notification({
title: '影片即將結束',
text: `影片已播放 ${progress.toFixed(1)}%,${totalTime}`,
timeout: 10000 // 通知顯示10秒
});
// 播放聲音提示
playNotificationSound();
// 在頁面上顯示一個通知
const notificationDiv = document.createElement('div');
notificationDiv.style.position = 'fixed';
notificationDiv.style.top = '20px';
notificationDiv.style.right = '20px';
notificationDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
notificationDiv.style.color = 'white';
notificationDiv.style.padding = '10px 20px';
notificationDiv.style.borderRadius = '5px';
notificationDiv.style.zIndex = '9999';
notificationDiv.style.fontWeight = 'bold';
notificationDiv.textContent = `影片即將結束: ${progress.toFixed(1)}%, ${totalTime}`;
document.body.appendChild(notificationDiv);
// 5秒後自動移除頁面通知
setTimeout(() => {
if (document.body.contains(notificationDiv)) {
document.body.removeChild(notificationDiv);
}
}, 5000);
// 標記已發送通知以避免重複
notificationSent = true;
}
// 檢查影片是否已完成且需要自動跳到下一個
if (progress >= completeThreshold && !videoCompleted && autoNextEnabled) {
videoCompleted = true;
// 保存最後完成的影片信息
lastVideoInfo = {
title: currentVideoInfo.title,
progress: `${progress.toFixed(1)}%`,
timestamp: Date.now()
};
GM_setValue('lastVideoInfo', lastVideoInfo);
updateVideoInfoDisplay();
// 設置跳轉標記 (為下一頁做準備)
GM_setValue('pendingAutoPlay', true);
GM_setValue('lastJumpTime', Date.now());
// 延遲1秒再點擊下一個影片按鈕,給影片一些緩衝時間
setTimeout(goToNextVideo, 1000);
}
// 如果影片已重新開始或新影片,重設通知狀態
if (progress < 50) {
notificationSent = false;
videoCompleted = false;
}
}
// 檢查影片元素狀態並自動播放
function autoPlayVideo() {
console.log("嘗試自動播放影片...");
// 首先嘗試找到影片元素
const videoElements = document.querySelectorAll('.video-js');
if (videoElements.length === 0) {
console.log("未找到影片元素");
return false;
}
let played = false;
videoElements.forEach(videoElement => {
// 檢查影片是否處於暫停或未播放狀態
if (videoElement.classList.contains('vjs-paused') ||
!videoElement.classList.contains('vjs-playing')) {
console.log("找到需要播放的影片元素:", videoElement.id);
// 如果啟用了自動靜音,先將影片靜音
if (autoMuteEnabled) {
muteVideo(videoElement);
}
// 嘗試方法1: 點擊大的播放按鈕
const bigPlayButton = videoElement.querySelector('.vjs-big-play-button');
if (bigPlayButton && window.getComputedStyle(bigPlayButton).display !== 'none') {
console.log("點擊大播放按鈕");
bigPlayButton.click();
played = true;
return;
}
// 嘗試方法2: 點擊控制欄中的播放按鈕
const playControl = videoElement.querySelector('.vjs-play-control');
if (playControl && playControl.classList.contains('vjs-paused')) {
console.log("點擊控制欄播放按鈕");
playControl.click();
played = true;
return;
}
// 嘗試方法3: 使用videojs API (如果可用)
if (window.videojs && videoElement.id) {
try {
const player = videojs(videoElement.id);
if (player && typeof player.play === 'function') {
console.log("使用videojs API播放");
player.play();
played = true;
return;
}
} catch (e) {
console.log("videojs API調用失敗:", e);
}
}
} else {
console.log("影片已經在播放中:", videoElement.id);
// 如果影片正在播放且啟用了自動靜音,確保其處於靜音狀態
if (autoMuteEnabled) {
muteVideo(videoElement);
}
played = true;
}
});
return played;
}
// 將影片靜音
function muteVideo(videoElement) {
if (!videoElement) return;
console.log("嘗試將影片靜音:", videoElement.id);
// 方法1: 使用volumePanel中的靜音按鈕
const muteButton = videoElement.querySelector('.vjs-mute-control');
if (muteButton && !muteButton.classList.contains('vjs-vol-0')) {
console.log("點擊靜音按鈕");
muteButton.click();
return true;
}
// 方法2: 使用videojs API
if (window.videojs && videoElement.id) {
try {
const player = videojs(videoElement.id);
if (player && typeof player.muted === 'function') {
console.log("使用videojs API靜音");
player.muted(true);
return true;
}
} catch (e) {
console.log("videojs API調用失敗:", e);
}
}
return false;
}
// 靜音當前頁面上的所有影片
function muteCurrentVideo() {
const videoElements = document.querySelectorAll('.video-js');
let success = false;
videoElements.forEach(videoElement => {
if (muteVideo(videoElement)) {
success = true;
}
});
return success;
}
// 檢查頁面上的影片狀態和播放情況
function checkAndPlayVideo() {
// 如果自動下一步功能開啟
if (autoNextEnabled) {
// 檢查是否剛剛跳轉到新頁面或需要自動播放
const currentTime = Date.now();
const timeSinceLastJump = currentTime - lastJumpTime;
// 如果是最近跳轉的頁面或標記為待自動播放
if (pendingAutoPlay || timeSinceLastJump < 10000) {
console.log("檢測到頁面跳轉或需要自動播放");
// 重置自動播放標記
pendingAutoPlay = false;
GM_setValue('pendingAutoPlay', false);
// 影片自動播放 (延遲2秒等待頁面完全加載)
setTimeout(() => {
if (!autoPlayVideo()) {
// 如果第一次未成功,再嘗試一次
setTimeout(autoPlayVideo, 2000);
}
}, 2000);
}
} else if (autoMuteEnabled) {
// 如果僅啟用了自動靜音但未啟用自動播放,確保影片處於靜音狀態
setTimeout(() => {
muteCurrentVideo();
}, 2000);
}
}
// 自動點擊下一個影片按鈕
function goToNextVideo() {
if (!autoNextEnabled) return;
console.log('嘗試尋找並點擊下一個活動按鈕...');
// 嘗試找到"下一個活動"按鈕並點擊
const nextActivityButton = document.querySelector('#next-activity-link');
if (nextActivityButton) {
console.log('找到下一個活動按鈕,自動點擊');
// 滾動到按鈕位置
nextActivityButton.scrollIntoView({ behavior: 'smooth' });
// 延遲500ms後點擊,確保滾動完成
setTimeout(() => {
nextActivityButton.click();
console.log('已點擊下一個活動按鈕');
}, 500);
return;
}
console.log('未找到下一個活動按鈕');
}
// 自動點擊"是的, 請繼續"按鈕
function checkAfkDialog() {
if (!autoClickerEnabled) return;
// 查找不帶hidden類的vjs-afk-container
const dialogBox = document.querySelector('.vjs-afk-container:not(.hidden)');
if (dialogBox) {
// 在對話框中查找"是的, 請繼續"按鈕
const confirmButton = dialogBox.querySelector('.vjs-afk-button:nth-child(2), button.vjs-afk-button:first-of-type');
if (confirmButton && confirmButton.textContent.includes('是的, 請繼續')) {
console.log('找到繼續觀看按鈕,自動點擊');
confirmButton.click();
}
}
}
// 設置DOM變化監聽器
function setupObserver() {
const observer = new MutationObserver(function(mutations) {
let shouldCheckPlay = false;
mutations.forEach(function(mutation) {
// 檢查新增節點
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
Array.from(mutation.addedNodes).forEach(node => {
// 監控影片確認對話框
if (node.nodeType === 1 && node.classList && node.classList.contains('vjs-afk-container')) {
if (autoClickerEnabled) {
const confirmButton = node.querySelector('button.vjs-afk-button:first-of-type');
if (confirmButton && confirmButton.textContent.includes('是的, 請繼續')) {
console.log('發現新增對話框,立即點擊');
confirmButton.click();
}
}
}
// 監控影片播放器元素加載
if (node.nodeType === 1 &&
(node.classList && node.classList.contains('video-js') ||
node.querySelector && node.querySelector('.video-js'))) {
shouldCheckPlay = true;
}
});
}
// 檢查屬性變化(hidden class被移除的情況)
if (mutation.type === 'attributes' &&
mutation.attributeName === 'class' &&
mutation.target.classList) {
// 影片確認對話框
if (mutation.target.classList.contains('vjs-afk-container') &&
!mutation.target.classList.contains('hidden')) {
if (autoClickerEnabled) {
const confirmButton = mutation.target.querySelector('button.vjs-afk-button:first-of-type');
if (confirmButton && confirmButton.textContent.includes('是的, 請繼續')) {
console.log('對話框顯示狀態變化,立即點擊');
confirmButton.click();
}
}
}
// 影片播放狀態變化
if (mutation.target.classList.contains('video-js')) {
if (autoMuteEnabled && !mutation.target.classList.contains('vjs-muted')) {
muteVideo(mutation.target);
}
shouldCheckPlay = true;
}
}
});
// 檢測到影片相關變化時檢查自動播放和靜音
if (shouldCheckPlay) {
if (autoNextEnabled || autoMuteEnabled) {
setTimeout(() => {
if (autoNextEnabled) autoPlayVideo();
if (autoMuteEnabled) muteCurrentVideo();
}, 500);
}
}
});
// 開始觀察頁面變化,特別關注class屬性的變化
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
console.log('DOM變化監聽器已設置');
}
// ===================== 初始化 =====================
// 等待頁面加載完成
function init() {
console.log('YAKITORY增強腳本初始化中...');
// 創建控制面板
createControlPanel();
// 設定定期檢查
setInterval(checkVideoProgress, checkInterval); // 檢查影片進度
setInterval(checkAfkDialog, checkInterval); // 檢查確認對話框
// 檢查當前頁面狀態
checkCurrentPage();
// 設置DOM變化監聽器
setupObserver();
// 檢查是否需要自動播放影片
setTimeout(checkAndPlayVideo, 2000);
// 添加頁面可見性變化檢測(用於處理頁面切換和跳轉)
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkAndPlayVideo();
}
});
// 頁面加載完成後再次檢查
window.addEventListener('load', function() {
setTimeout(checkAndPlayVideo, 1000);
});
console.log('YAKITORY增強腳本已啟動');
}
// 檢查當前頁面的狀況
function checkCurrentPage() {
// 檢查當前URL是否包含影片頁面特徵
const isVideoPage = window.location.href.includes('/mod/videos/view.php');
// 檢查是否有影片元素
const hasVideoElement = document.querySelector('.video-js') !== null;
// 檢查是否來自跳轉
const pendingAutoPlay = GM_getValue('pendingAutoPlay', false);
const lastJumpTime = GM_getValue('lastJumpTime', 0);
const timeSinceJump = Date.now() - lastJumpTime;
console.log(`當前頁面狀況: 影片頁面=${isVideoPage}, 有影片元素=${hasVideoElement}, 待自動播放=${pendingAutoPlay}, 跳轉時間=${timeSinceJump}ms前`);
// 重置一些狀態
if (isVideoPage && hasVideoElement) {
notificationSent = false;
videoCompleted = false;
}
}
// 當DOM加載完成後初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();