// ==UserScript==
// @name Auto play ads on ani.gamer.com.tw
// @name:zh-CN 动画疯自动播放广告
// @name:zh-TW 動畫瘋自動播放廣告
// @namespace ling921
// @version 0.8.0
// @description Agree to age prompt, auto skip ads when time is up, auto play next video, and register some keyboard shortcuts (see the release notes below for details)
// @description:zh-CN 自动同意年龄提示,到达时间后自动跳过广告(内置播放器广告和两种 google iframe 广告),自动播放下一集,并注册一些快捷键(详见最下方的更新日志)
// @description:zh-TW 自動同意年齡提示,到達時間後自動跳過廣告(內置播放器廣告和兩種 google iframe 廣告),自動播放下一集,並註冊一些快捷鍵(詳見最下方的更新日誌)
// @author ling921
// @match https://ani.gamer.com.tw/animeVideo.php*
// @match https://*.safeframe.googlesyndication.com/*
// @match https://imasdk.googleapis.com/*
// @icon http://gamer.com.tw/favicon.ico
// @grant none
// @run-at document-idle
// @tag video
// @tag anime
// @tag utilities
// @license MIT
// ==/UserScript==
/**
* Global variable to store video player
* @type {HTMLVideoElement}
*/
var videoPlayer;
/**
* Localization text
*/
const i18n = {
'en': {
'addEventListenerToPlayer': '🎮 Yay! Connected to the video player~',
'autoPlayNext': '⏭️ Whoosh~ Auto-jumping to next episode!',
'agreeAgePrompt': '✨ Of course I\'m old enough! *wink*',
'skipAds': '🚀 Bye bye ads~ Moving to the good stuff!',
'dismissDialog': '🎯 Poof! Dialog ad vanished!',
'dismissButtonHidden': '👀 Hmm... waiting for the dismiss button to show up...',
'dismissButtonNotFound': '🤔 Eh? Can\'t find the dismiss button anywhere...',
'skipAdButton': '⚡ Zap! Skipping this ad!',
'noPlayButton': '😱 Oh no! Can\'t find the play button...',
'noPrevButton': '⚠️ Oopsie! Previous episode button is missing...',
'noNextButton': '⚠️ Uh-oh! Next episode button is nowhere to be found...',
'noDanmuButton': '💬 Ara ara~ Danmu button is hiding...',
'noTheaterButton': '🎭 Theater mode button seems to be on vacation...',
'noFullscreenButton': '📺 The fullscreen button is playing hide and seek...',
'noVideoPlayer': '📼 Eh?! Where did the video player go?',
'pauseOrPlay': '⏯️ Boop~ Toggling play state!',
'gotoPrev': '⏮️ Time travel to previous episode!',
'gotoNext': '⏭️ Leaping to next episode~',
'toggleDanmu': '💫 Whoosh~ Danmu rain on/off!',
'toggleTheater': '🎪 Poof~ Theater mode switch!',
'toggleFullscreen': '🌟 Maximum screen power!',
'volumeUp': '🔊 Turning up the volume~',
'volumeDown': '🔉 Making things a bit quieter...',
'seekBackward': '⏪ Rewinding time~',
'seekForward': '⏩ Fast forward go brrr!',
'clickContinue': '✨ Yes yes, continue playing~',
'videoStuck': '⚠️ Video seems stuck, trying to resume...',
'resumeFailed': '😢 Oops! Failed to resume playback:',
'muteAds': '🔇 Shh~ Muting all ad videos~'
},
'zh-CN': {
'addEventListenerToPlayer': '🎮 哇!成功连接到播放器啦~',
'autoPlayNext': '⏭️ 咻咻咻~ 自动跳转下一集!',
'agreeAgePrompt': '✨ 当然已经成年啦!*眨眼*',
'skipAds': '🚀 白白啦广告君~ 马上就能看番啦!',
'dismissDialog': '🎯 啪!广告框框消失啦!',
'dismissButtonHidden': '👀 诶嘿~等待关闭按钮出现中...',
'dismissButtonNotFound': '🤔 咦?找不到关闭按钮呢...',
'skipAdButton': '⚡ 唰!跳过广告!',
'noPlayButton': '😱 呜哇!找不到播放按钮...',
'noPrevButton': '⚠️ 糟糕!上一集按钮不见了...',
'noNextButton': '⚠️ 哎呀!下一集按钮去哪了...',
'noDanmuButton': '💬 啊啦啦~ 弹幕按钮躲起来了...',
'noTheaterButton': '🎭 剧场模式按钮去度假了...',
'noFullscreenButton': '📺 全屏按钮在玩捉迷藏...',
'noVideoPlayer': '📼 诶诶?!播放器君去哪了?',
'pauseOrPlay': '⏯️ 啵~ 切换播放状态!',
'gotoPrev': '⏮️ 时光倒流到上一集!',
'gotoNext': '⏭️ 飞速跳转下一集~',
'toggleDanmu': '💫 唰~ 弹幕开关切换!',
'toggleTheater': '🎪 啪~ 剧场模式变身!',
'toggleFullscreen': '🌟 全屏模式启动!',
'volumeUp': '🔊 调大音量中~',
'volumeDown': '🔉 轻声轻声模式...',
'seekBackward': '⏪ 时光倒流中~',
'seekForward': '⏩ 快进冲鸭!',
'clickContinue': '✨ 好哒好哒,继续播放~',
'videoStuck': '⚠️ 检测到视频卡住,尝试恢复播放...',
'resumeFailed': '😢 哎呀!恢复播放失败:',
'muteAds': '🔇 嘘~ 已将广告视频静音~'
},
'zh-TW': {
'addEventListenerToPlayer': '🎮 哇!成功連接到播放器啦~',
'autoPlayNext': '⏭️ 咻咻咻~ 自動跳轉下一集!',
'agreeAgePrompt': '✨ 當然已經成年啦!*眨眼*',
'skipAds': '🚀 掰掰啦廣告君~ 馬上就能看番啦!',
'dismissDialog': '🎯 啪!廣告框框消失啦!',
'dismissButtonHidden': '👀 誒嘿~等待關閉按鈕出現中...',
'dismissButtonNotFound': '🤔 咦?找不到關閉按鈕呢...',
'skipAdButton': '⚡ 唰!跳過廣告!',
'noPlayButton': '😱 嗚哇!找不到播放按鈕...',
'noPrevButton': '⚠️ 糟糕!上一集按鈕不見了...',
'noNextButton': '⚠️ 哎呀!下一集按鈕去哪了...',
'noDanmuButton': '💬 啊啦啦~ 彈幕按鈕躲起來了...',
'noTheaterButton': '🎭 劇場模式按鈕去度假了...',
'noFullscreenButton': '📺 全螢幕按鈕在玩捉迷藏...',
'noVideoPlayer': '📼 誒誒?!播放器君去哪了?',
'pauseOrPlay': '⏯️ 啵~ 切換播放狀態!',
'gotoPrev': '⏮️ 時光倒流到上一集!',
'gotoNext': '⏭️ 飛速跳轉下一集~',
'toggleDanmu': '💫 唰~ 彈幕開關切換!',
'toggleTheater': '🎪 啪~ 劇場模式變身!',
'toggleFullscreen': '🌟 全螢幕模式啟動!',
'volumeUp': '🔊 調大音量中~',
'volumeDown': '🔉 輕聲輕聲模式...',
'seekBackward': '⏪ 時光倒流中~',
'seekForward': '⏩ 快進衝鴨!',
'clickContinue': '✨ 好啦好啦,繼續播放~',
'videoStuck': '⚠️ 檢測到視頻卡住,嘗試恢復播放...',
'resumeFailed': '😢 哎呀!恢復播放失敗:',
'muteAds': '🔇 噓~ 已將廣告視頻靜音~'
}
};
/**
* Get user language and match the most suitable translation
* @returns {string} - The language
*/
function getUserLanguage() {
const lang = navigator.language;
if (lang.startsWith("en")) return "en";
if (lang === "zh-CN") return "zh-CN";
return "zh-TW"; // Default to Traditional Chinese
}
/**
* Get localized text
* @param {string} key - The key
* @returns {string} - The text
*/
function t(key) {
const lang = getUserLanguage();
return i18n[lang][key] || i18n["zh-TW"][key];
}
(function () {
"use strict";
// Handle top level window
if (window === window.top) {
videoPlayer = document.querySelector("#ani_video_html5_api");
if (videoPlayer) {
console.log(t("addEventListenerToPlayer"));
// Auto unmute video player
videoPlayer.addEventListener("loadstart", () => {
videoPlayer.muted = false;
});
// Auto play next video
videoPlayer.addEventListener("ended", () => {
const nextButton = document.querySelector(".vjs-next-button");
if (nextButton) {
console.log(t("autoPlayNext"));
nextButton.click();
}
});
}
// Attempt to play video
attemptToPlayVideo();
// Register keyboard shortcuts
registerKeyboardShortcuts(document);
// Define observer to execute functions when DOM changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (node) {
removeInsTag(node);
});
});
agreeAgePrompt();
removeTitleAds();
handleVideoPlayerAds();
ensureShortcutTitles();
});
// Start observing the body for changes
observer.observe(document.documentElement, { childList: true, subtree: true });
}
// Handle iframe window
else {
if (window.location.href.includes("safeframe.googlesyndication.com")) {
const observer = new MutationObserver(() => {
handleIframeAds(document);
muteAllVideos(document);
});
observer.observe(document.body, { childList: true, subtree: true });
} else if (window.location.href.includes("imasdk.googleapis.com")) {
const observer = new MutationObserver(() => {
handleIframeAds2(document);
muteAllVideos(document);
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
})();
/**
* Attempt to play video
*/
function attemptToPlayVideo() {
setInterval(() => {
const playButton = document.querySelector(".vjs-play-control");
if (playButton && playButton.classList.contains("vjs-playing") && videoPlayer.readyState === 2) {
console.log(t('videoStuck'));
videoPlayer.pause();
videoPlayer.play().catch((err) => console.error(t('resumeFailed'), err));
}
}, 300);
}
/**
* Register keyboard shortcuts
* @param {Document} doc - The document
*/
function registerKeyboardShortcuts(doc) {
doc.addEventListener("keydown", (event) => {
// Ignore input fields event propagation
if (
event.target.tagName === "INPUT" ||
event.target.tagName === "TEXTAREA" ||
event.target.isContentEditable
) {
return;
}
if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
/**
* Get the document of the event target
* @type {Document}
*/
const _doc = event.target.ownerDocument || doc;
// P pause or play
if (event.key === "p") {
const playButton = _doc.querySelector(".vjs-play-control");
if (playButton) {
console.log(t("pauseOrPlay"));
playButton.click();
} else {
console.log(t("noPlayButton"));
}
}
// [ goes to previous video
else if (event.key === "[") {
const prevButton = _doc.querySelector(".vjs-pre-button");
if (prevButton) {
console.log(t("gotoPrev"));
prevButton.click();
} else {
console.log(t("noPrevButton"));
}
}
// ] goes to next video
else if (event.key === "]") {
const nextButton = _doc.querySelector(".vjs-next-button");
if (nextButton) {
console.log(t("gotoNext"));
nextButton.click();
} else {
console.log(t("noNextButton"));
}
}
// D enable or disable danmu
else if (event.key === "d") {
const danmuButton = _doc.querySelector(
".vjs-danmu-button .vjs-menu-button"
);
if (danmuButton) {
console.log(t("toggleDanmu"));
danmuButton.click();
} else {
console.log(t("noDanmuButton"));
}
}
// T enter or exit theater mode
else if (event.key === "t") {
const theaterButton = _doc.querySelector(".vjs-indent-button");
if (theaterButton) {
console.log(t("toggleTheater"));
theaterButton.click();
} else {
console.log(t("noTheaterButton"));
}
}
// F enter or exit fullscreen
else if (event.key === "f") {
const fullscreenButton = _doc.querySelector(".vjs-fullscreen-control");
if (fullscreenButton) {
console.log(t("toggleFullscreen"));
fullscreenButton.click();
} else {
console.log(t("noFullscreenButton"));
}
}
// Video player control
else if (!event.target.closest("video-js")) {
const dispatchEvent = (eventType) => {
videoPlayer.dispatchEvent(
new KeyboardEvent(eventType, {
key: event.key,
code: event.code,
keyCode: event.keyCode,
which: event.which,
bubbles: true,
cancelable: true,
composed: true,
isTrusted: true,
})
);
};
// ↑ video volume up
if (event.key === "ArrowUp") {
if (videoPlayer) {
if (videoPlayer.volume < 1) {
event.preventDefault();
console.log(t("volumeUp"));
dispatchEvent("keydown");
}
} else {
console.log(t("noVideoPlayer"));
}
}
// ↓ video volume down
else if (event.key === "ArrowDown") {
if (videoPlayer) {
if (videoPlayer.volume > 0) {
event.preventDefault();
console.log(t("volumeDown"));
dispatchEvent("keydown");
}
} else {
console.log(t("noVideoPlayer"));
}
}
// ← video backward
else if (event.key === "ArrowLeft") {
if (videoPlayer) {
if (videoPlayer.currentTime > 0) {
event.preventDefault();
console.log(t("seekBackward"));
dispatchEvent("keydown");
}
} else {
console.log(t("noVideoPlayer"));
}
}
// → video forward
else if (event.key === "ArrowRight") {
if (videoPlayer) {
if (videoPlayer.currentTime < videoPlayer.duration) {
event.preventDefault();
console.log(t("seekForward"));
dispatchEvent("keydown");
}
} else {
console.log(t("noVideoPlayer"));
}
}
}
}
});
}
/**
* Agree to age prompt
*/
function agreeAgePrompt() {
const agePrompt = document.querySelector("button.choose-btn-agree#adult");
if (agePrompt) {
agePrompt.click();
console.log(t("agreeAgePrompt"));
}
}
/**
* Remove <ins> tag
* @param {Node} node - The node
*/
function removeInsTag(node) {
if (
node instanceof Element &&
node.tagName === "INS" &&
node.parentNode === document.documentElement
) {
node.remove();
}
}
/**
* Remove ads in title
*/
function removeTitleAds() {
const titleAds = document.querySelectorAll('[id^="div-gpt-ad-"]');
titleAds.forEach((ad) => {
ad.remove();
});
}
/**
* Handle ads in video player
*/
function handleVideoPlayerAds() {
const skipButton = document.querySelector("#adSkipButton");
if (skipButton) {
if (skipButton.classList.contains("enable")) {
console.log(t("skipAds"));
skipButton.click();
} else {
videoPlayer.muted = true;
}
}
const skipButton2 = document.querySelector(".nativeAD-skip-button.enable");
if (skipButton2 && !skipButton2.classList.contains("vjs-hidden")) {
console.log(t("skipAds"));
skipButton2.click();
}
}
/**
* Ensure shortcut titles
*/
function ensureShortcutTitles() {
/**
* Ensure title ends with text
* @param {Element|null} element - The element
* @param {string} text - The text
*/
function ensureTitleEndsWith(element, text) {
if (!element) {
return;
}
const title = element.getAttribute("title");
if (!title) {
element.setAttribute("title", text);
} else if (!title.endsWith(text)) {
element.setAttribute("title", title + " " + text);
}
}
// Play button
ensureTitleEndsWith(document.querySelector(".vjs-play-control"), "(P)");
// Previous button
ensureTitleEndsWith(document.querySelector(".vjs-pre-button"), "([)");
// Next button
ensureTitleEndsWith(document.querySelector(".vjs-next-button"), "(])");
// Danmu button
ensureTitleEndsWith(document.querySelector(".vjs-danmu-button"), "(D)");
// Theater button
ensureTitleEndsWith(document.querySelector(".vjs-indent-button"), "(T)");
// Fullscreen button
ensureTitleEndsWith(document.querySelector(".vjs-fullscreen-control"), "(F)");
}
/**
* Handle ads in iframe
* @param {Document} doc - The iframe document
*/
function handleIframeAds(doc) {
// Handle continue button
const resumeButton =
doc.querySelector(".rewardResumebutton") ||
doc.querySelector("#resume_video_button");
if (resumeButton) {
console.log(t("clickContinue"));
resumeButton.click();
}
// Handle ad dismiss button (1)
const adsCountDown = doc.querySelector("#count-down-text");
if (adsCountDown) {
const dismissDialog = () => {
const dismissButton = doc.querySelector("#card #dismiss-button-element");
if (dismissButton) {
if (dismissButton.style.display !== "none") {
console.log(t("dismissDialog"));
dismissButton.click();
} else {
console.log(t("dismissButtonHidden"));
}
} else {
console.log(t("dismissButtonNotFound"));
}
};
if (adsCountDown.offsetParent === null) {
dismissDialog();
} else if (adsCountDown.textContent === "1 秒後即可獲得獎勵") {
setTimeout(dismissDialog, 1000);
}
}
// Handle ad dismiss button (2)
const countDown = doc.querySelector("#count_down");
if (countDown && countDown.textContent === "0 秒後可獲獎勵") {
// Handle continue button
const resumeButton =
doc.querySelector(".rewardResumebutton") ||
doc.querySelector("#resume_video_button");
if (resumeButton) {
console.log(t("clickContinue"));
resumeButton.click();
}
const closeButton = doc.querySelector("#close_button");
if (closeButton) {
console.log(t("dismissDialog"));
closeButton.click();
}
}
// Handle skip ad button
const skipButton = doc.querySelector(".videoAdUiSkipButton");
if (skipButton && !skipButton.classList.contains("videoAdUiHidden")) {
console.log(t("skipAds"));
skipButton.click();
}
}
/**
* Handle ads in iframe
* @param {Document} doc - The iframe document
*/
function handleIframeAds2(doc) {
const skipButton = doc.querySelector('[aria-label="Skip Ad"]');
if (skipButton) {
if (skipButton.textContent === "Skip Ad") {
console.log(t("skipAdButton"));
skipButton.click();
} else {
videoPlayer.muted = true;
}
}
}
/**
* Mute all videos in document
* @param {Document} doc - The document
*/
function muteAllVideos(doc) {
const videos = doc.querySelectorAll('video');
if (videos.length > 0) {
videos.forEach(video => {
video.muted = true;
});
console.log(t('muteAds'));
}
}
// Release notes
// 2024-12-29 version 0.8.0
// - 再次優化廣告跳過邏輯
// - 新增廣告自動靜音
// - 新增視頻卡住時自動恢復播放
// 2024-12-29 version 0.7.0
// - 優化 safeframe.googlesyndication.com 的廣告跳過邏輯
// 2024-12-23 version 0.6.0
// - 新增日誌本地化支援
// - 修改日誌描述文本
// 2024-12-18 version 0.5.0
// - 新增自動播放下一集
// - 完善頁面快速鍵相關按鈕的 title 屬性
// 2024-12-16 version 0.4.0
// - 規範版本號
// 2024-12-16 version 0.3
// - 註冊快速鍵 ↑ ↓ ← → 分別控制音量、時間軸
// - 註冊快捷鍵 D 控制彈幕
// 2024-12-15 version 0.2
// - 新增標籤 video, anime, utilities
// 2024-12-14 version 0.1
// - 自動同意年齡確認
// - 廣告倒計時結束結束自動跳過廣告
// - 播放廣告時靜音,播放影片時取消靜音
// - 註冊快捷鍵 [ 和 ] 分別跳到上一個和下一個視頻
// - 註冊快速鍵 P 暫停或播放
// - 註冊快速鍵 T 進入或退出劇院模式
// - 註冊快速鍵 F 進入或退出全螢幕