From 98e903e0a0db5e0ab7be46010a0bf7c282b5da15 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Fri, 31 Jan 2025 17:29:52 +0700 Subject: [PATCH 01/19] fix: Incorrectly removed ad elements, attempt to fix issue #5 --- scripts/Auto-Skip-YouTube-Ads/script.user.js | 9 +++++---- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 11 ++++++----- watch.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 0ebf0e7..0912e44 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.0 +// @version 6.0.1 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -140,9 +140,9 @@ function removeAdElements() { ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], // Sponsored ad video items on home page. - ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], + // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], - ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], + // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], // Ad videos on YouTube Short. ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], @@ -159,7 +159,7 @@ function removeAdElements() { for (const adSelector of adSelectors) { const adEl = document.querySelector(adSelector[0]) if (adEl === null) continue - const neededEl = document.querySelector(adSelector[1]) + const neededEl = adEl.querySelector(adSelector[1]) if (neededEl === null) continue adEl.remove() } @@ -171,4 +171,5 @@ window.setInterval(skipAd, 500) window.setInterval(removeAdElements, 1000) addCss() +removeAdElements() skipAd() diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index a58de55..0d73a35 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.0 +// @version 6.0.1 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -157,9 +157,9 @@ function removeAdElements(): void { ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], // Sponsored ad video items on home page. - ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], + // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], - ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], + // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], // Ad videos on YouTube Short. ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], @@ -174,9 +174,9 @@ function removeAdElements(): void { ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { - const adEl = document.querySelector(adSelector[0]) + const adEl = document.querySelector(adSelector[0]) if (adEl === null) continue - const neededEl = document.querySelector(adSelector[1]) + const neededEl = adEl.querySelector(adSelector[1]) if (neededEl === null) continue adEl.remove() } @@ -188,4 +188,5 @@ window.setInterval(skipAd, 500) window.setInterval(removeAdElements, 1000) addCss() +removeAdElements() skipAd() diff --git a/watch.ts b/watch.ts index 0d9678e..bde981c 100644 --- a/watch.ts +++ b/watch.ts @@ -17,7 +17,7 @@ async function handleWatch(path: string, stat: Stats): Promise { const dirPath: string = dirname(path).replace(/\\/g, '/') const code: string = readFileSync(path, 'utf-8') - const matches = code.match(/\/\/ ==UserScript==\n.+?\n\/\/ ==\/UserScript==/s) + const matches = code.match(/\/\/ ==UserScript==\n.+?\n\/\/ ==\/UserScript==/su) if (matches === null) return const meta: string = matches[0] From 681c64b3b0914ce4b45fed399768392ffa06d25e Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sat, 1 Feb 2025 10:21:29 +0700 Subject: [PATCH 02/19] docs: Tetr-io Improvements --- scripts/Tetr-io-Improvements/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 scripts/Tetr-io-Improvements/README.md diff --git a/scripts/Tetr-io-Improvements/README.md b/scripts/Tetr-io-Improvements/README.md new file mode 100644 index 0000000..dfba14b --- /dev/null +++ b/scripts/Tetr-io-Improvements/README.md @@ -0,0 +1,14 @@ +# Tetr.io Improvements + +## Introducion + +This userscript makes your [tetr.io][1] playing experience better. + +## Features + +- Remove smooth motion effects, making navigation faster. +- Remove ads. +- Press the `Home` key each time to automatically press the login button, enter Tetra League, start finding matches. +- Press the `End` key to press the next button when the match ends. + +[1]: https://tetr.io From 4d3988451d845dd3314522b6ec6131221f8e5f2f Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 2 Feb 2025 07:41:28 +0700 Subject: [PATCH 03/19] improvement: Disable some unnecessary CSS --- scripts/Auto-Skip-YouTube-Ads/README.md | 4 ++++ scripts/Auto-Skip-YouTube-Ads/script.user.js | 22 +++++++++++--------- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 22 +++++++++++--------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index ec84c6f..bbb4e43 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -8,6 +8,10 @@ Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát h ## 📑 Changelog +### 6.0.2 - 2025-02-02 + +- Disable some unnecessary CSS. + ### 6.0.0 - 2025-01-29 _Happy Lunar New Year!_ diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 0912e44..e6881c9 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.1 +// @version 6.0.2 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -45,8 +45,8 @@ // ==/UserScript== function skipAd() { - const isYouTubeShort = checkIsYouTubeShort() - if (isYouTubeShort) return + const isYouTubeShorts = checkIsYouTubeShorts() + if (isYouTubeShorts) return const hasAd = checkHasAd() if (!hasAd) return @@ -77,7 +77,7 @@ function checkHasAd() { return false } -function checkIsYouTubeShort() { +function checkIsYouTubeShorts() { return location.pathname.startsWith('/shorts/') } @@ -107,8 +107,10 @@ function addCss() { // Masthead ad on home page. '#masthead-ad', - 'ytd-ad-slot-renderer', - '.ytp-suggested-action', + // Sponsored ad video items on home page. + // 'ytd-ad-slot-renderer', + + // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', // Featured product ad banner at the bottom left of the video. @@ -145,16 +147,16 @@ function removeAdElements() { // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] // Ad blocker warning dialog. - ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], + // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], // Survey dialog on home page, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], + // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], // Survey to rate suggested content, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] + // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { const adEl = document.querySelector(adSelector[0]) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index 0d73a35..7c996df 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.1 +// @version 6.0.2 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -62,8 +62,8 @@ interface YouTubeVideoData { } function skipAd(): void { - const isYouTubeShort: boolean = checkIsYouTubeShort() - if (isYouTubeShort) return + const isYouTubeShorts: boolean = checkIsYouTubeShorts() + if (isYouTubeShorts) return const hasAd: boolean = checkHasAd() if (!hasAd) return @@ -94,7 +94,7 @@ function checkHasAd(): boolean { return false } -function checkIsYouTubeShort(): boolean { +function checkIsYouTubeShorts(): boolean { return location.pathname.startsWith('/shorts/') } @@ -124,8 +124,10 @@ function addCss(): void { // Masthead ad on home page. '#masthead-ad', - 'ytd-ad-slot-renderer', - '.ytp-suggested-action', + // Sponsored ad video items on home page. + // 'ytd-ad-slot-renderer', + + // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', // Featured product ad banner at the bottom left of the video. @@ -162,16 +164,16 @@ function removeAdElements(): void { // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] // Ad blocker warning dialog. - ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], + // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], // Survey dialog on home page, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], + // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], // Survey to rate suggested content, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] + // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { const adEl = document.querySelector(adSelector[0]) From d4d67abc07e01dc35b2ec327195566e520706c61 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Mon, 10 Feb 2025 20:32:58 +0700 Subject: [PATCH 04/19] Remove `ad-showing` class when there is an ad --- scripts/Auto-Skip-YouTube-Ads/script.user.js | 19 +++++++-------- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 25 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index e6881c9..b47c3de 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.2 +// @version 6.0.3 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -48,12 +48,14 @@ function skipAd() { const isYouTubeShorts = checkIsYouTubeShorts() if (isYouTubeShorts) return - const hasAd = checkHasAd() - if (!hasAd) return + const ad = getInterruptiveAd() + if (ad === null) return const player = getYouTubePlayer() if (player === null) return + ad.classList.remove('ad-showing') + const videoData = player.getVideoData() const videoId = videoData.video_id const startTime = Math.floor(player.getCurrentTime()) @@ -62,19 +64,16 @@ function skipAd() { console.log('Ad skipped!', videoId, startTime, videoData.title) } -/** - * Check if there are any ads interrupting the video. - */ -function checkHasAd() { +function getInterruptiveAd() { // This element appears when a video ad appears. const adShowing = document.querySelector('.ad-showing') - if (adShowing !== null) return true + if (adShowing !== null) return adShowing // Timed pie countdown ad. const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container') - if (pieCountdown !== null) return true + if (pieCountdown !== null) return pieCountdown - return false + return null } function checkIsYouTubeShorts() { diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index 7c996df..b934c94 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.2 +// @version 6.0.3 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -65,12 +65,14 @@ function skipAd(): void { const isYouTubeShorts: boolean = checkIsYouTubeShorts() if (isYouTubeShorts) return - const hasAd: boolean = checkHasAd() - if (!hasAd) return + const ad: HTMLElement | null = getInterruptiveAd() + if (ad === null) return const player: YouTubePlayer | null = getYouTubePlayer() if (player === null) return + ad.classList.remove('ad-showing') + const videoData: YouTubeVideoData = player.getVideoData() const videoId: string = videoData.video_id const startTime: number = Math.floor(player.getCurrentTime()) @@ -79,19 +81,18 @@ function skipAd(): void { console.log('Ad skipped!', videoId, startTime, videoData.title) } -/** - * Check if there are any ads interrupting the video. - */ -function checkHasAd(): boolean { +function getInterruptiveAd(): HTMLElement | null { // This element appears when a video ad appears. - const adShowing = document.querySelector('.ad-showing') - if (adShowing !== null) return true + const adShowing = document.querySelector('.ad-showing') + if (adShowing !== null) return adShowing // Timed pie countdown ad. - const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container') - if (pieCountdown !== null) return true + const pieCountdown = document.querySelector( + '.ytp-ad-timed-pie-countdown-container' + ) + if (pieCountdown !== null) return pieCountdown - return false + return null } function checkIsYouTubeShorts(): boolean { From ee1063d9b642f594ed900379352e11196b41408b Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 16 Feb 2025 00:36:29 +0700 Subject: [PATCH 05/19] Version 6 had many bug reports, reverting to the previous version 5 (#6). --- .resources/global.d.ts | 4 + scripts/Auto-Skip-YouTube-Ads/README.md | 378 ++++++++++--------- scripts/Auto-Skip-YouTube-Ads/script.user.js | 352 +++++++++++++---- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 376 +++++++++++++----- 4 files changed, 757 insertions(+), 353 deletions(-) diff --git a/.resources/global.d.ts b/.resources/global.d.ts index 8b51306..f3fbb53 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -53,5 +53,9 @@ interface YtdPlayerElement extends HTMLElement { loadVideoWithPlayerVars(options: { videoId: string; start?: number }): void } +interface YouTubeMoviePlayerElement extends HTMLElement { + loadVideoByPlayerVars(options: { videoId: string; start?: number }): void +} + declare type TrustedTypePolicyFactory = import('trusted-types/lib').TrustedTypePolicyFactory declare const trustedTypes: TrustedTypePolicyFactory diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index bbb4e43..27aa020 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -1,186 +1,192 @@ -## 📰 Introduction - -Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. - -立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 - -Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. - -## 📑 Changelog - -### 6.0.2 - 2025-02-02 - -- Disable some unnecessary CSS. - -### 6.0.0 - 2025-01-29 - -_Happy Lunar New Year!_ - -- Completely rewritten way to skip ads, more efficient, not detected by YouTube ad blocker warning. -- New way to skip ads is temporarily not working on YouTube Music. - -### 5.3.0 - 2025-01-23 - -- Supports older browser versions. - -### 5.2.0 - 2025-01-21 - -- Support for **YouTube mobile** version 🎉 -- Revisit fix for issue [#2]: Fully resolved the problem where videos couldn't be paused on mobile. The previous fix was incomplete. - -### 5.1.3 - 2025-01-18 - -- Fix ad skipping issue. - -### 5.1.2 - 2025-01-17 - -- Fix issue [#2] where video can't be paused on mobile. - -### 5.1.1 - 2024-12-27 - -- Hide the survey to rate suggested content, located at bottom right. - -### 5.1.0 - 2024-12-26 - -- Skip pie countdown ads 🎉 - -### 5.0.0 - 2024-12-25 - -_Merry Christmas!_ - -- **No need to reload the page** when there is no way to skip the ad anymore 🤯 -- Configuration removed, no longer needed. - -### 4.8.2 - 2024-12-21 - -- Fix timestamp loss when reloading. - -### 4.8.1 - 2024-12-03 - -- Hide survey dialog on home page. - -### 4.8.0 - 2024-11-26 - -- The current video's timestamp will be preserved when the page is reloaded ([#267857]). - -### 4.7.4 - 2024-11-20 - -- Improved ad skipping. - -### 4.7.0 - 2024-10-26 - -- Add option "Don't reload while the user is busy" in Tampermonkey's menu to avoid reloading page when user is busy doing something, like reading comments, entering text. Enabled by default. - -### 4.6.2 - 2024-10-13 - -- Improve hiding of ad banners. - -### 4.6.0 - 2024-10-07 - -- Support skipping ads on **YouTube Music** (PR [#1]). - -### 4.5.2 - 2024-09-30 - -- Fix Shorts reload infinitely ([#258626], [#259545], [#261679]). - -### 4.5.0 - 2024-09-26 - -- Add option to enable/disable "Reload the page when there is no other way to skip ads" feature in Tampermonkey's menu. Enabled by default.\ - ![Screenshot-001] - -### 4.4.0 - 2024-08-30 - -- Automatically reload web page when ad blocker warnings appear. - -### 4.3.13 - 2024-08-26 - -- Fixed bug where video could not be paused using pause/play key on keyboard or media controls ([#257424]). -- Improve the performance. - -### 4.3.9 - 2024-08-21 - -- Fix `@match` invalid syntax ([#256841]). - -### 4.3.8 - 2024-08-20 - -- Fix the issue of removing ad videos in Shorts. - -### 4.3.6 - 2024-08-07 - -- Fix bug where video rewinds a segment after skipping an ad ([#254113]). - -### 4.3.4 - 2024-08-02 - -- Improve the performance. - -### 4.2.1 - 2024-07-30 - -- Fixed video automatically replay when ended. - -### 4.2.0 - 2024-07-30 - -- Videos will now no longer occasionally pause due to ad blocker use. -- Faster ad video skipping speed. - -### 4.1.0 - 2024-07-10 - -- No need to reload the page when the ad blocker warning dialog appears. - -### 4.0.0 - 2024-07-09 - -- The page will now reload if an ad blocker warning dialog appears. Because YouTube now pauses the video at first if an ad blocker is detected. -- Write to the Console every time skip an ad video, etc. Purpose to help debug. To open the Console, press `Ctrl+Shift+J`. - -### 3.1.2 - 2024-07-06 - -- Playing video after clicking dismiss the ad blocker warning popup. - -### 3.1.1 - 2024-07-04 - -- Add a few CSS that hides the ads. - -### 3.1.0 - 2024-07-02 - -- Skip ads faster when the tab is active. -- Fixed bug when set time to end of ad video without the video duration being available. -- Change icon. - -### 3.0.2 - 2024-06-28 - -- Rewriting to only use `setInterval` simplifies things, and fix some bugs. - -### 2.1.3 - 2024-06-21 - -- Fix `popupContainer` not found error. - -### 2.1.0 - 2024-06-20 - -- Auto close YouTube's ad blocker warning popup. - -### 2.0.1 - 2024-06-19 - -- Improved skip ad button detection. -- Fall back to `setInterval` when `MutationObserver` is not supported. - -### 2.0.0 - 2024-06-18 - -- Rewrite the entire code, use `MutationObserver` instead of `setInterval`. - -### 1.0.0 - 2024-06-17 - -- Stable release. - -## 💳 Credits - -Youtube icons created by Ruslan Babkin - Flaticon. - -[#2]: https://github.com/tientq64/userscripts/issues/2 -[#267857]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/267857 -[#258626]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/258626 -[#259545]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/259545 -[#261679]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/261679 -[#257424]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/257424 -[#256841]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/256841 -[#254113]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/254113 -[#1]: https://github.com/tientq64/userscripts/pull/1 -[Screenshot-001]: https://cdn.jsdelivr.net/gh/tientq64/userscripts/scripts/Auto-Skip-YouTube-Ads/assets/screenshot-001.png +## 📰 Introduction + +Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. + +立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 + +Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. + +## 📑 Changelog + +### 7.0.0 - 2025-02-15 + +- Version 6 had many bug reports, reverting to the previous version 5 ([#6], [#279168]). + +### 6.0.2 - 2025-02-02 + +- Disable some unnecessary CSS. + +### 6.0.0 - 2025-01-29 + +_Happy Lunar New Year!_ + +- Completely rewritten way to skip ads, more efficient, not detected by YouTube ad blocker warning. +- New way to skip ads is temporarily not working on YouTube Music. + +### 5.3.0 - 2025-01-23 + +- Supports older browser versions. + +### 5.2.0 - 2025-01-21 + +- Support for **YouTube mobile** version 🎉 +- Revisit fix for issue [#2]: Fully resolved the problem where videos couldn't be paused on mobile. The previous fix was incomplete. + +### 5.1.3 - 2025-01-18 + +- Fix ad skipping issue. + +### 5.1.2 - 2025-01-17 + +- Fix issue [#2] where video can't be paused on mobile. + +### 5.1.1 - 2024-12-27 + +- Hide the survey to rate suggested content, located at bottom right. + +### 5.1.0 - 2024-12-26 + +- Skip pie countdown ads 🎉 + +### 5.0.0 - 2024-12-25 + +_Merry Christmas!_ + +- **No need to reload the page** when there is no way to skip the ad anymore 🤯 +- Configuration removed, no longer needed. + +### 4.8.2 - 2024-12-21 + +- Fix timestamp loss when reloading. + +### 4.8.1 - 2024-12-03 + +- Hide survey dialog on home page. + +### 4.8.0 - 2024-11-26 + +- The current video's timestamp will be preserved when the page is reloaded ([#267857]). + +### 4.7.4 - 2024-11-20 + +- Improved ad skipping. + +### 4.7.0 - 2024-10-26 + +- Add option "Don't reload while the user is busy" in Tampermonkey's menu to avoid reloading page when user is busy doing something, like reading comments, entering text. Enabled by default. + +### 4.6.2 - 2024-10-13 + +- Improve hiding of ad banners. + +### 4.6.0 - 2024-10-07 + +- Support skipping ads on **YouTube Music** (PR [#1]). + +### 4.5.2 - 2024-09-30 + +- Fix Shorts reload infinitely ([#258626], [#259545], [#261679]). + +### 4.5.0 - 2024-09-26 + +- Add option to enable/disable "Reload the page when there is no other way to skip ads" feature in Tampermonkey's menu. Enabled by default.\ + ![Screenshot-001] + +### 4.4.0 - 2024-08-30 + +- Automatically reload web page when ad blocker warnings appear. + +### 4.3.13 - 2024-08-26 + +- Fixed bug where video could not be paused using pause/play key on keyboard or media controls ([#257424]). +- Improve the performance. + +### 4.3.9 - 2024-08-21 + +- Fix `@match` invalid syntax ([#256841]). + +### 4.3.8 - 2024-08-20 + +- Fix the issue of removing ad videos in Shorts. + +### 4.3.6 - 2024-08-07 + +- Fix bug where video rewinds a segment after skipping an ad ([#254113]). + +### 4.3.4 - 2024-08-02 + +- Improve the performance. + +### 4.2.1 - 2024-07-30 + +- Fixed video automatically replay when ended. + +### 4.2.0 - 2024-07-30 + +- Videos will now no longer occasionally pause due to ad blocker use. +- Faster ad video skipping speed. + +### 4.1.0 - 2024-07-10 + +- No need to reload the page when the ad blocker warning dialog appears. + +### 4.0.0 - 2024-07-09 + +- The page will now reload if an ad blocker warning dialog appears. Because YouTube now pauses the video at first if an ad blocker is detected. +- Write to the Console every time skip an ad video, etc. Purpose to help debug. To open the Console, press `Ctrl+Shift+J`. + +### 3.1.2 - 2024-07-06 + +- Playing video after clicking dismiss the ad blocker warning popup. + +### 3.1.1 - 2024-07-04 + +- Add a few CSS that hides the ads. + +### 3.1.0 - 2024-07-02 + +- Skip ads faster when the tab is active. +- Fixed bug when set time to end of ad video without the video duration being available. +- Change icon. + +### 3.0.2 - 2024-06-28 + +- Rewriting to only use `setInterval` simplifies things, and fix some bugs. + +### 2.1.3 - 2024-06-21 + +- Fix `popupContainer` not found error. + +### 2.1.0 - 2024-06-20 + +- Auto close YouTube's ad blocker warning popup. + +### 2.0.1 - 2024-06-19 + +- Improved skip ad button detection. +- Fall back to `setInterval` when `MutationObserver` is not supported. + +### 2.0.0 - 2024-06-18 + +- Rewrite the entire code, use `MutationObserver` instead of `setInterval`. + +### 1.0.0 - 2024-06-17 + +- Stable release. + +## 💳 Credits + +Youtube icons created by Ruslan Babkin - Flaticon. + +[#6]: https://github.com/tientq64/userscripts/issues/6 +[#2]: https://github.com/tientq64/userscripts/issues/2 +[#1]: https://github.com/tientq64/userscripts/pull/1 +[#279168]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279168 +[#267857]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/267857 +[#261679]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/261679 +[#259545]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/259545 +[#258626]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/258626 +[#257424]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/257424 +[#256841]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/256841 +[#254113]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/254113 +[Screenshot-001]: https://cdn.jsdelivr.net/gh/tientq64/userscripts/scripts/Auto-Skip-YouTube-Ads/assets/screenshot-001.png diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index b47c3de..feebd61 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -1,38 +1,35 @@ // ==UserScript== // @name Auto Skip YouTube Ads -// @name:ar تخطي إعلانات YouTube تلقائيًا +// @name:ar التخطي التلقائي لإعلانات YouTube // @name:es Saltar Automáticamente Anuncios De YouTube -// @name:fr Ignorer Automatiquement Les Publicités YouTube // @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें // @name:id Lewati Otomatis Iklan YouTube // @name:ja YouTube 広告を自動スキップ // @name:ko YouTube 광고 자동 건너뛰기 -// @name:nl YouTube-Advertenties Automatisch Overslaan // @name:pt-BR Pular Automaticamente Anúncios Do YouTube // @name:ru Автоматический Пропуск Рекламы На YouTube // @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.3 -// @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. -// @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. -// @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. -// @description:fr Ignorez automatiquement et instantanément les publicités YouTube. Non détecté par les avertissements du bloqueur de publicités YouTube. -// @description:hi YouTube विज्ञापनों को स्वचालित रूप से तुरंत छोड़ दें। YouTube विज्ञापन अवरोधक चेतावनियों द्वारा पता नहीं लगाया गया। -// @description:id Lewati iklan YouTube secara otomatis secara instan. Tidak terdeteksi oleh peringatan pemblokir iklan YouTube. -// @description:ja YouTube 広告を即座に自動的にスキップします。YouTube 広告ブロッカーの警告には検出されません。 -// @description:ko YouTube 광고를 즉시 자동으로 건너뜁니다. YouTube 광고 차단 경고에 감지되지 않습니다. -// @description:nl Sla YouTube-advertenties direct automatisch over. Ongemerkt door YouTube-adblockerwaarschuwingen. -// @description:pt-BR Pule anúncios do YouTube instantaneamente. Não detectado pelos avisos do bloqueador de anúncios do YouTube. -// @description:ru Автоматически пропускать рекламу YouTube мгновенно. Не обнаруживается предупреждениями блокировщиков рекламы YouTube. -// @description:vi Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. -// @description:zh-CN 立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 -// @description:zh-TW 立即自動跳過 YouTube 廣告。 YouTube 廣告攔截器警告未被偵測到。 +// @version 7.0.0 +// @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up. +// @description:ar تخطي إعلانات YouTube تلقائيًا في الحال. إزالة النافذة المنبثقة لتحذير مانع الإعلانات. +// @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios. +// @description:hi YouTube विज्ञापनों को लगभग तुरंत ही स्वचालित रूप से छोड़ दें। विज्ञापन अवरोधक चेतावनी पॉप-अप हटाएँ। +// @description:id Lewati iklan YouTube secara otomatis hampir seketika. Hapus pop-up peringatan pemblokir iklan. +// @description:ja YouTube 広告をほぼ瞬時に自動的にスキップします。広告ブロッカーの警告ポップアップを削除します。 +// @description:ko YouTube 광고를 거의 즉시 자동으로 건너뜁니다. 광고 차단 경고 팝업을 제거합니다. +// @description:pt-BR Pule automaticamente os anúncios do YouTube quase instantaneamente. Remova o pop-up de aviso do bloqueador de anúncios. +// @description:ru Автоматически пропускайте рекламу YouTube почти мгновенно. Уберите всплывающее предупреждение о блокировщике рекламы. +// @description:vi Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo. +// @description:zh-CN 几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。 +// @description:zh-TW 幾乎立即自動跳過 YouTube 廣告。刪除廣告攔截器警告彈出視窗。 // @author tientq64 // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @match https://music.youtube.com/* // @grant none // @license MIT // @compatible firefox @@ -44,95 +41,253 @@ // @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Auto-Skip-YouTube-Ads // ==/UserScript== +/** + * Skip ads. Remove ad elements. + */ function skipAd() { - const isYouTubeShorts = checkIsYouTubeShorts() - if (isYouTubeShorts) return + removeAdElements() - const ad = getInterruptiveAd() - if (ad === null) return + video = null + fineScrubber = document.querySelector('.ytp-fine-scrubbing') - const player = getYouTubePlayer() - if (player === null) return + // Check if the current URL is a YouTube Shorts URL and exit the function if true. + if (window.location.pathname.startsWith('/shorts/')) return - ad.classList.remove('ad-showing') + const moviePlayer = document.querySelector('#movie_player') - const videoData = player.getVideoData() - const videoId = videoData.video_id - const startTime = Math.floor(player.getCurrentTime()) - player.loadVideoById(videoId, startTime) + if (moviePlayer) { + hasAd = moviePlayer.classList.contains('ad-showing') + video = moviePlayer.querySelector('video.html5-main-video') + } - console.log('Ad skipped!', videoId, startTime, videoData.title) -} + if (hasAd) { + const skipButton = document.querySelector(` + .ytp-skip-ad-button, + .ytp-ad-skip-button, + .ytp-ad-skip-button-modern, + .ytp-ad-survey-answer-button + `) + // Click the skip ad button if available. + if (skipButton) { + skipButton.click() + skipButton.remove() + } + // Otherwise, fast forward to the end of the ad video. + // Use `9999` instead of `video.duration` to avoid errors when `duration` is not a number. + else if (video && video.src) { + video.currentTime = 9999 + } + } -function getInterruptiveAd() { - // This element appears when a video ad appears. - const adShowing = document.querySelector('.ad-showing') - if (adShowing !== null) return adShowing + if (video) { + video.addEventListener('pause', handleVideoPause) + video.addEventListener('pointerup', allowPauseVideo) + video.addEventListener('timeupdate', handleVideoTimeUpdate) + } // Timed pie countdown ad. const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container') - if (pieCountdown !== null) return pieCountdown + if (pieCountdown) { + pieCountdown.remove() + replaceCurrentVideo() + } + + // Handle when ad blocker warning appears inside video player. + const adBlockerWarningInner = document.querySelector( + '.yt-playability-error-supported-renderers' + ) + if (adBlockerWarningInner) { + adBlockerWarningInner.remove() + document.addEventListener('yt-navigate-finish', handleYouTubeNavigateFinish) + replaceCurrentVideo() + } + + // Video play/pause button. + const playButton = document.querySelector( + 'button.ytp-play-button, button.player-control-play-pause-icon' + ) + if (playButton) { + playButton.addEventListener('click', allowPauseVideo) + } +} + +function queryHasSelector(selector, hasSelector, element = document) { + const el = element.querySelector(selector) + if (el === null) return null + const hasEl = el.querySelector(hasSelector) + if (hasEl === null) return null + return el +} + +function queryHasSelectorAll(selector, hasSelector, element = document) { + const els = element.querySelectorAll(selector) + const result = [] + for (const el of els) { + const hasEl = el.querySelector(hasSelector) + if (hasEl === null) continue + result.push(el) + } + return result +} - return null +function getCurrentVideoId() { + const params = new URLSearchParams(location.search) + const videoId = params.get('v') + return videoId } -function checkIsYouTubeShorts() { - return location.pathname.startsWith('/shorts/') +/** + * Check if the user is focused on the input. + */ +function checkEnteringInput() { + if (document.activeElement === null) { + return false + } + return document.activeElement.matches('input, textarea, select') } /** - * Finds and returns the current YouTube video player. - * - * @returns The current YouTube video player, or `null` if not found. + * Temporarily allows the video to be paused, for a short period of time. */ -function getYouTubePlayer() { - let player - if (isYouTubeMobile) { - const playerEl = document.querySelector('#movie_player') - player = playerEl +function allowPauseVideo() { + pausedByUser = true + window.clearTimeout(allowPauseVideoTimeoutId) + allowPauseVideoTimeoutId = window.setTimeout(disallowPauseVideo, 500) +} + +/** + * Pausing the video is not allowed. The purpose is to prevent video from being paused, + * against the behavior of pausing video when YouTube ad blocking warning dialog appears. + * Unless certain conditions, such as pausing by user, etc. + */ +function disallowPauseVideo() { + pausedByUser = false + window.clearTimeout(allowPauseVideoTimeoutId) +} + +function handleWindowBlur() { + isTabBlurred = true +} + +function handleWindowFocus() { + isTabBlurred = false +} + +/** + * Handle when video is paused. If certain conditions are not met, it will continue + * playing. Returning early in this function means the video should be paused as it should + * be. + */ +function handleVideoPause() { + if (isYouTubeMusic) return + + // If it was stopped by the user, it's ok, let the video pause as it should, and exit the function. + if (pausedByUser) { + disallowPauseVideo() + return + } + + // The video will pause normally if the tab is not focused. This is to allow for pausing the video via the media controller (of the browser or operating system), etc. + // Note: While this also gives YouTube the opportunity to pause videos to annoy users, it's an acceptable trade-off. + if (document.hidden) return + if (isTabBlurred) return + + if (fineScrubber && fineScrubber.style.display !== 'none') return + if (video === null) return + if (video.duration - video.currentTime < 0.1) return + + // This is YouTube's disruptive behavior towards users, so the video should continue to play as normal. + video.play() +} + +function handleVideoTimeUpdate() { + if (hasAd || video === null) return + currentVideoTime = video.currentTime +} + +/** + * Handle both keyboard press or release events. + */ +function handleWindowKeyDownAndKeyUp(event) { + if (isYouTubeMusic) return + if (checkEnteringInput()) return + const code = event.code + if (event.type === 'keydown') { + if (code === 'KeyK' || code === 'MediaPlayPause') { + allowPauseVideo() + } } else { - const playerEl = document.querySelector('#ytd-player') - if (playerEl === null) return null - player = playerEl.getPlayer() + if (code === 'Space') { + allowPauseVideo() + } + } +} + +function handleYouTubeNavigateFinish() { + currentVideoTime = 0 + replaceCurrentVideo() +} + +async function replaceCurrentVideo() { + const start = Math.floor(currentVideoTime) + for (let i = 0; i < 16; i++) { + await waitFor(500) + const videoId = getCurrentVideoId() + if (!videoId || !video || video.src) continue + if (isYouTubeMobile) { + const player = document.querySelector('#movie_player') + if (!player) continue + player.loadVideoByPlayerVars({ videoId, start }) + } else { + const player = document.querySelector('#ytd-player') + if (!player) continue + player.loadVideoWithPlayerVars({ videoId, start }) + } } - return player } +function waitFor(millis) { + return new Promise((resolve) => { + window.setTimeout(resolve, millis) + }) +} + +/** + * Add CSS hides some ad elements on the page. + */ function addCss() { - const adsSelectors = [ + const hideCssSelector = [ // Ad banner in the upper right corner, above the video playlist. '#player-ads', // Masthead ad on home page. '#masthead-ad', - // Sponsored ad video items on home page. - // 'ytd-ad-slot-renderer', + 'ytd-ad-slot-renderer', - // '.ytp-suggested-action', - '.yt-mealbar-promo-renderer', + // Ad blocker warning inside the player. + 'yt-playability-error-supported-renderers#error-screen', - // Featured product ad banner at the bottom left of the video. - '.ytp-featured-product', - - // Products shelf ad banner below the video description. - 'ytd-merch-shelf-renderer', + '.ytp-suggested-action', + '.yt-mealbar-promo-renderer', // YouTube Music Premium trial promotion dialog, bottom left corner. 'ytmusic-mealbar-promo-renderer', // YouTube Music Premium trial promotion banner on home page. 'ytmusic-statement-banner-renderer' - ] - const adsSelector = adsSelectors.join(',') - const css = `${adsSelector} { display: none !important; }` + ].join(',') + const css = ` + #ytd-player { visibility: visible !important; } + ${hideCssSelector} { display: none !important; } + ` const style = document.createElement('style') style.textContent = css document.head.appendChild(style) } /** - * Remove ad elements using JavaScript because these selectors require the use of the CSS + * Remove ad elements using javascript because these selectors require the use of the CSS * `:has` selector which is not supported in older browser versions. */ function removeAdElements() { @@ -140,16 +295,19 @@ function removeAdElements() { // Ad banner in the upper right corner, above the video playlist. ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], + // Temporarily comment out this selector to fix issue [#265124](https://greasyfork.org/en/scripts/498197-auto-skip-youtube-ads/discussions/265124). + // ['#panels', 'ytd-ads-engagement-panel-content-renderer'], + // Sponsored ad video items on home page. // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], // Ad blocker warning dialog. - // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], + ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'] // Survey dialog on home page, located at bottom right. // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], @@ -158,19 +316,63 @@ function removeAdElements() { // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { - const adEl = document.querySelector(adSelector[0]) - if (adEl === null) continue - const neededEl = adEl.querySelector(adSelector[1]) - if (neededEl === null) continue - adEl.remove() + const adEls = queryHasSelectorAll(adSelector[0], adSelector[1]) + for (const adEl of adEls) { + adEl.remove() + } } } +/** + * Is it YouTube mobile version. + */ const isYouTubeMobile = location.hostname === 'm.youtube.com' -window.setInterval(skipAd, 500) -window.setInterval(removeAdElements, 1000) +/** + * Is the current page YouTube Music. + */ +const isYouTubeMusic = location.hostname === 'music.youtube.com' + +/** + * Current video element. + */ +let video = null + +let fineScrubber = null +let hasAd = false +let currentVideoTime = 0 + +/** + * Is the video paused by the user, not paused by YouTube's ad blocker warning dialog. + */ +let pausedByUser = false + +/** + * Is the current tab blurred. + */ +let isTabBlurred = false + +let allowPauseVideoTimeoutId = 0 + +// Observe DOM changes to detect ads. +if (window.MutationObserver) { + const observer = new MutationObserver(skipAd) + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'src'], + childList: true, + subtree: true + }) +} +// If DOM observation is not supported. Detect ads every 500ms (2 times per second). +else { + window.setInterval(skipAd, 500) +} + +window.addEventListener('blur', handleWindowBlur) +window.addEventListener('focus', handleWindowFocus) +window.addEventListener('keydown', handleWindowKeyDownAndKeyUp) +window.addEventListener('keyup', handleWindowKeyDownAndKeyUp) addCss() -removeAdElements() skipAd() diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index b934c94..22c5f94 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -1,38 +1,35 @@ // ==UserScript== // @name Auto Skip YouTube Ads -// @name:ar تخطي إعلانات YouTube تلقائيًا +// @name:ar التخطي التلقائي لإعلانات YouTube // @name:es Saltar Automáticamente Anuncios De YouTube -// @name:fr Ignorer Automatiquement Les Publicités YouTube // @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें // @name:id Lewati Otomatis Iklan YouTube // @name:ja YouTube 広告を自動スキップ // @name:ko YouTube 광고 자동 건너뛰기 -// @name:nl YouTube-Advertenties Automatisch Overslaan // @name:pt-BR Pular Automaticamente Anúncios Do YouTube // @name:ru Автоматический Пропуск Рекламы На YouTube // @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.3 -// @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. -// @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. -// @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. -// @description:fr Ignorez automatiquement et instantanément les publicités YouTube. Non détecté par les avertissements du bloqueur de publicités YouTube. -// @description:hi YouTube विज्ञापनों को स्वचालित रूप से तुरंत छोड़ दें। YouTube विज्ञापन अवरोधक चेतावनियों द्वारा पता नहीं लगाया गया। -// @description:id Lewati iklan YouTube secara otomatis secara instan. Tidak terdeteksi oleh peringatan pemblokir iklan YouTube. -// @description:ja YouTube 広告を即座に自動的にスキップします。YouTube 広告ブロッカーの警告には検出されません。 -// @description:ko YouTube 광고를 즉시 자동으로 건너뜁니다. YouTube 광고 차단 경고에 감지되지 않습니다. -// @description:nl Sla YouTube-advertenties direct automatisch over. Ongemerkt door YouTube-adblockerwaarschuwingen. -// @description:pt-BR Pule anúncios do YouTube instantaneamente. Não detectado pelos avisos do bloqueador de anúncios do YouTube. -// @description:ru Автоматически пропускать рекламу YouTube мгновенно. Не обнаруживается предупреждениями блокировщиков рекламы YouTube. -// @description:vi Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. -// @description:zh-CN 立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 -// @description:zh-TW 立即自動跳過 YouTube 廣告。 YouTube 廣告攔截器警告未被偵測到。 +// @version 7.0.0 +// @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up. +// @description:ar تخطي إعلانات YouTube تلقائيًا في الحال. إزالة النافذة المنبثقة لتحذير مانع الإعلانات. +// @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios. +// @description:hi YouTube विज्ञापनों को लगभग तुरंत ही स्वचालित रूप से छोड़ दें। विज्ञापन अवरोधक चेतावनी पॉप-अप हटाएँ। +// @description:id Lewati iklan YouTube secara otomatis hampir seketika. Hapus pop-up peringatan pemblokir iklan. +// @description:ja YouTube 広告をほぼ瞬時に自動的にスキップします。広告ブロッカーの警告ポップアップを削除します。 +// @description:ko YouTube 광고를 거의 즉시 자동으로 건너뜁니다. 광고 차단 경고 팝업을 제거합니다. +// @description:pt-BR Pule automaticamente os anúncios do YouTube quase instantaneamente. Remova o pop-up de aviso do bloqueador de anúncios. +// @description:ru Автоматически пропускайте рекламу YouTube почти мгновенно. Уберите всплывающее предупреждение о блокировщике рекламы. +// @description:vi Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo. +// @description:zh-CN 几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。 +// @description:zh-TW 幾乎立即自動跳過 YouTube 廣告。刪除廣告攔截器警告彈出視窗。 // @author tientq64 // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @match https://music.youtube.com/* // @grant none // @license MIT // @compatible firefox @@ -43,115 +40,263 @@ // @noframes // ==/UserScript== -interface YtdPlayerElement extends HTMLElement { - getPlayer: () => YouTubePlayer -} - /** - * YouTube video player. + * Skip ads. Remove ad elements. */ -interface YouTubePlayer { - getVideoData: () => YouTubeVideoData - getCurrentTime: () => number - loadVideoById: (videoId: string, startTime?: number) => void -} - -interface YouTubeVideoData { - title: string - video_id: string -} - function skipAd(): void { - const isYouTubeShorts: boolean = checkIsYouTubeShorts() - if (isYouTubeShorts) return + removeAdElements() - const ad: HTMLElement | null = getInterruptiveAd() - if (ad === null) return + video = null + fineScrubber = document.querySelector('.ytp-fine-scrubbing') - const player: YouTubePlayer | null = getYouTubePlayer() - if (player === null) return + // Check if the current URL is a YouTube Shorts URL and exit the function if true. + if (window.location.pathname.startsWith('/shorts/')) return - ad.classList.remove('ad-showing') + const moviePlayer = document.querySelector('#movie_player') - const videoData: YouTubeVideoData = player.getVideoData() - const videoId: string = videoData.video_id - const startTime: number = Math.floor(player.getCurrentTime()) - player.loadVideoById(videoId, startTime) + if (moviePlayer) { + hasAd = moviePlayer.classList.contains('ad-showing') + video = moviePlayer.querySelector('video.html5-main-video') + } - console.log('Ad skipped!', videoId, startTime, videoData.title) -} + if (hasAd) { + const skipButton = document.querySelector(` + .ytp-skip-ad-button, + .ytp-ad-skip-button, + .ytp-ad-skip-button-modern, + .ytp-ad-survey-answer-button + `) + // Click the skip ad button if available. + if (skipButton) { + skipButton.click() + skipButton.remove() + } + // Otherwise, fast forward to the end of the ad video. + // Use `9999` instead of `video.duration` to avoid errors when `duration` is not a number. + else if (video && video.src) { + video.currentTime = 9999 + } + } -function getInterruptiveAd(): HTMLElement | null { - // This element appears when a video ad appears. - const adShowing = document.querySelector('.ad-showing') - if (adShowing !== null) return adShowing + if (video) { + video.addEventListener('pause', handleVideoPause) + video.addEventListener('pointerup', allowPauseVideo) + video.addEventListener('timeupdate', handleVideoTimeUpdate) + } // Timed pie countdown ad. const pieCountdown = document.querySelector( '.ytp-ad-timed-pie-countdown-container' ) - if (pieCountdown !== null) return pieCountdown + if (pieCountdown) { + pieCountdown.remove() + replaceCurrentVideo() + } - return null + // Handle when ad blocker warning appears inside video player. + const adBlockerWarningInner = document.querySelector( + '.yt-playability-error-supported-renderers' + ) + if (adBlockerWarningInner) { + adBlockerWarningInner.remove() + document.addEventListener('yt-navigate-finish', handleYouTubeNavigateFinish) + replaceCurrentVideo() + } + + // Video play/pause button. + const playButton = document.querySelector( + 'button.ytp-play-button, button.player-control-play-pause-icon' + ) + if (playButton) { + playButton.addEventListener('click', allowPauseVideo) + } +} + +function queryHasSelector( + selector: string, + hasSelector: string, + element: Document | Element = document +): T | null { + const el = element.querySelector(selector) + if (el === null) return null + const hasEl = el.querySelector(hasSelector) + if (hasEl === null) return null + return el +} + +function queryHasSelectorAll( + selector: string, + hasSelector: string, + element: Document | Element = document +): T[] { + const els = element.querySelectorAll(selector) + const result: T[] = [] + for (const el of els) { + const hasEl = el.querySelector(hasSelector) + if (hasEl === null) continue + result.push(el) + } + return result } -function checkIsYouTubeShorts(): boolean { - return location.pathname.startsWith('/shorts/') +function getCurrentVideoId(): string | null { + const params = new URLSearchParams(location.search) + const videoId: string | null = params.get('v') + return videoId } /** - * Finds and returns the current YouTube video player. - * - * @returns The current YouTube video player, or `null` if not found. + * Check if the user is focused on the input. */ -function getYouTubePlayer(): YouTubePlayer | null { - let player: YouTubePlayer | null - if (isYouTubeMobile) { - const playerEl: unknown = document.querySelector('#movie_player') - player = playerEl as YouTubePlayer +function checkEnteringInput(): boolean { + if (document.activeElement === null) { + return false + } + return document.activeElement.matches('input, textarea, select') +} + +/** + * Temporarily allows the video to be paused, for a short period of time. + */ +function allowPauseVideo(): void { + pausedByUser = true + window.clearTimeout(allowPauseVideoTimeoutId) + allowPauseVideoTimeoutId = window.setTimeout(disallowPauseVideo, 500) +} + +/** + * Pausing the video is not allowed. The purpose is to prevent video from being paused, + * against the behavior of pausing video when YouTube ad blocking warning dialog appears. + * Unless certain conditions, such as pausing by user, etc. + */ +function disallowPauseVideo(): void { + pausedByUser = false + window.clearTimeout(allowPauseVideoTimeoutId) +} + +function handleWindowBlur(): void { + isTabBlurred = true +} + +function handleWindowFocus(): void { + isTabBlurred = false +} + +/** + * Handle when video is paused. If certain conditions are not met, it will continue + * playing. Returning early in this function means the video should be paused as it should + * be. + */ +function handleVideoPause(): void { + if (isYouTubeMusic) return + + // If it was stopped by the user, it's ok, let the video pause as it should, and exit the function. + if (pausedByUser) { + disallowPauseVideo() + return + } + + // The video will pause normally if the tab is not focused. This is to allow for pausing the video via the media controller (of the browser or operating system), etc. + // Note: While this also gives YouTube the opportunity to pause videos to annoy users, it's an acceptable trade-off. + if (document.hidden) return + if (isTabBlurred) return + + if (fineScrubber && fineScrubber.style.display !== 'none') return + if (video === null) return + if (video.duration - video.currentTime < 0.1) return + + // This is YouTube's disruptive behavior towards users, so the video should continue to play as normal. + video.play() +} + +function handleVideoTimeUpdate(): void { + if (hasAd || video === null) return + currentVideoTime = video.currentTime +} + +/** + * Handle both keyboard press or release events. + */ +function handleWindowKeyDownAndKeyUp(event: KeyboardEvent): void { + if (isYouTubeMusic) return + if (checkEnteringInput()) return + const code: string = event.code + if (event.type === 'keydown') { + if (code === 'KeyK' || code === 'MediaPlayPause') { + allowPauseVideo() + } } else { - const playerEl = document.querySelector('#ytd-player') - if (playerEl === null) return null - player = playerEl.getPlayer() + if (code === 'Space') { + allowPauseVideo() + } + } +} + +function handleYouTubeNavigateFinish(): void { + currentVideoTime = 0 + replaceCurrentVideo() +} + +async function replaceCurrentVideo(): Promise { + const start: number = Math.floor(currentVideoTime) + for (let i = 0; i < 16; i++) { + await waitFor(500) + const videoId: string | null = getCurrentVideoId() + if (!videoId || !video || video.src) continue + if (isYouTubeMobile) { + const player = document.querySelector('#movie_player') + if (!player) continue + player.loadVideoByPlayerVars({ videoId, start }) + } else { + const player = document.querySelector('#ytd-player') + if (!player) continue + player.loadVideoWithPlayerVars({ videoId, start }) + } } - return player } +function waitFor(millis: number): Promise { + return new Promise((resolve) => { + window.setTimeout(resolve, millis) + }) +} + +/** + * Add CSS hides some ad elements on the page. + */ function addCss(): void { - const adsSelectors: string[] = [ + const hideCssSelector: string = [ // Ad banner in the upper right corner, above the video playlist. '#player-ads', // Masthead ad on home page. '#masthead-ad', - // Sponsored ad video items on home page. - // 'ytd-ad-slot-renderer', - - // '.ytp-suggested-action', - '.yt-mealbar-promo-renderer', + 'ytd-ad-slot-renderer', - // Featured product ad banner at the bottom left of the video. - '.ytp-featured-product', + // Ad blocker warning inside the player. + 'yt-playability-error-supported-renderers#error-screen', - // Products shelf ad banner below the video description. - 'ytd-merch-shelf-renderer', + '.ytp-suggested-action', + '.yt-mealbar-promo-renderer', // YouTube Music Premium trial promotion dialog, bottom left corner. 'ytmusic-mealbar-promo-renderer', // YouTube Music Premium trial promotion banner on home page. 'ytmusic-statement-banner-renderer' - ] - const adsSelector: string = adsSelectors.join(',') - const css: string = `${adsSelector} { display: none !important; }` - const style = document.createElement('style') + ].join(',') + const css: string = ` + #ytd-player { visibility: visible !important; } + ${hideCssSelector} { display: none !important; } + ` + const style: HTMLStyleElement = document.createElement('style') style.textContent = css document.head.appendChild(style) } /** - * Remove ad elements using JavaScript because these selectors require the use of the CSS + * Remove ad elements using javascript because these selectors require the use of the CSS * `:has` selector which is not supported in older browser versions. */ function removeAdElements(): void { @@ -159,16 +304,19 @@ function removeAdElements(): void { // Ad banner in the upper right corner, above the video playlist. ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], + // Temporarily comment out this selector to fix issue [#265124](https://greasyfork.org/en/scripts/498197-auto-skip-youtube-ads/discussions/265124). + // ['#panels', 'ytd-ads-engagement-panel-content-renderer'], + // Sponsored ad video items on home page. // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], // Ad blocker warning dialog. - // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], + ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'] // Survey dialog on home page, located at bottom right. // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], @@ -177,19 +325,63 @@ function removeAdElements(): void { // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { - const adEl = document.querySelector(adSelector[0]) - if (adEl === null) continue - const neededEl = adEl.querySelector(adSelector[1]) - if (neededEl === null) continue - adEl.remove() + const adEls = queryHasSelectorAll(adSelector[0], adSelector[1]) + for (const adEl of adEls) { + adEl.remove() + } } } +/** + * Is it YouTube mobile version. + */ const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com' -window.setInterval(skipAd, 500) -window.setInterval(removeAdElements, 1000) +/** + * Is the current page YouTube Music. + */ +const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com' + +/** + * Current video element. + */ +let video: HTMLVideoElement | null = null + +let fineScrubber: HTMLDivElement | null = null +let hasAd: boolean = false +let currentVideoTime: number = 0 + +/** + * Is the video paused by the user, not paused by YouTube's ad blocker warning dialog. + */ +let pausedByUser: boolean = false + +/** + * Is the current tab blurred. + */ +let isTabBlurred: boolean = false + +let allowPauseVideoTimeoutId: number = 0 + +// Observe DOM changes to detect ads. +if (window.MutationObserver) { + const observer: MutationObserver = new MutationObserver(skipAd) + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'src'], + childList: true, + subtree: true + }) +} +// If DOM observation is not supported. Detect ads every 500ms (2 times per second). +else { + window.setInterval(skipAd, 500) +} + +window.addEventListener('blur', handleWindowBlur) +window.addEventListener('focus', handleWindowFocus) +window.addEventListener('keydown', handleWindowKeyDownAndKeyUp) +window.addEventListener('keyup', handleWindowKeyDownAndKeyUp) addCss() -removeAdElements() skipAd() From 6100c884a80e628d332170fbf4c1b8d8bfeb7991 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 16 Feb 2025 01:25:25 +0700 Subject: [PATCH 06/19] Bug fix for YouTube Mobile --- .resources/global.d.ts | 16 +++++ scripts/Auto-Skip-YouTube-Ads/script.user.js | 45 +++++++------- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 63 +++++++------------- 3 files changed, 56 insertions(+), 68 deletions(-) diff --git a/.resources/global.d.ts b/.resources/global.d.ts index 8b51306..1b72fe7 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -51,6 +51,22 @@ declare const unsafeWindow: UnsafeWindow interface YtdPlayerElement extends HTMLElement { loadVideoWithPlayerVars(options: { videoId: string; start?: number }): void + getPlayer: () => YouTubePlayer +} + +interface YouTubeMoviePlayerElement extends HTMLElement, YouTubePlayer { + loadVideoByPlayerVars(options: { videoId: string; start?: number }): void +} + +interface YouTubePlayer { + getVideoData: () => YouTubeVideoData + getCurrentTime: () => number + loadVideoById: (videoId: string, startTime?: number) => void +} + +interface YouTubeVideoData { + title: string + video_id: string } declare type TrustedTypePolicyFactory = import('trusted-types/lib').TrustedTypePolicyFactory diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index b47c3de..08a9b3b 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.3 +// @version 6.0.4 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -51,17 +51,30 @@ function skipAd() { const ad = getInterruptiveAd() if (ad === null) return - const player = getYouTubePlayer() - if (player === null) return + let playerEl + let player + if (isYouTubeMobile) { + playerEl = document.querySelector('#movie_player') + player = playerEl + } else { + playerEl = document.querySelector('#ytd-player') + player = playerEl && playerEl.getPlayer() + } + if (playerEl === null || player === null) return - ad.classList.remove('ad-showing') + // ad.classList.remove('ad-showing') const videoData = player.getVideoData() const videoId = videoData.video_id - const startTime = Math.floor(player.getCurrentTime()) - player.loadVideoById(videoId, startTime) + const start = Math.floor(player.getCurrentTime()) - console.log('Ad skipped!', videoId, startTime, videoData.title) + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) + } else { + playerEl.loadVideoByPlayerVars({ videoId, start }) + } + + console.log('Ad skipped!', videoId, start, videoData.title) } function getInterruptiveAd() { @@ -80,24 +93,6 @@ function checkIsYouTubeShorts() { return location.pathname.startsWith('/shorts/') } -/** - * Finds and returns the current YouTube video player. - * - * @returns The current YouTube video player, or `null` if not found. - */ -function getYouTubePlayer() { - let player - if (isYouTubeMobile) { - const playerEl = document.querySelector('#movie_player') - player = playerEl - } else { - const playerEl = document.querySelector('#ytd-player') - if (playerEl === null) return null - player = playerEl.getPlayer() - } - return player -} - function addCss() { const adsSelectors = [ // Ad banner in the upper right corner, above the video playlist. diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index b934c94..a947404 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.3 +// @version 6.0.4 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -43,24 +43,6 @@ // @noframes // ==/UserScript== -interface YtdPlayerElement extends HTMLElement { - getPlayer: () => YouTubePlayer -} - -/** - * YouTube video player. - */ -interface YouTubePlayer { - getVideoData: () => YouTubeVideoData - getCurrentTime: () => number - loadVideoById: (videoId: string, startTime?: number) => void -} - -interface YouTubeVideoData { - title: string - video_id: string -} - function skipAd(): void { const isYouTubeShorts: boolean = checkIsYouTubeShorts() if (isYouTubeShorts) return @@ -68,17 +50,30 @@ function skipAd(): void { const ad: HTMLElement | null = getInterruptiveAd() if (ad === null) return - const player: YouTubePlayer | null = getYouTubePlayer() - if (player === null) return + let playerEl: YtdPlayerElement | YouTubeMoviePlayerElement | null + let player: YouTubePlayer | YouTubeMoviePlayerElement | null + if (isYouTubeMobile) { + playerEl = document.querySelector('#movie_player') + player = playerEl + } else { + playerEl = document.querySelector('#ytd-player') + player = playerEl && playerEl.getPlayer() + } + if (playerEl === null || player === null) return - ad.classList.remove('ad-showing') + // ad.classList.remove('ad-showing') const videoData: YouTubeVideoData = player.getVideoData() const videoId: string = videoData.video_id - const startTime: number = Math.floor(player.getCurrentTime()) - player.loadVideoById(videoId, startTime) + const start: number = Math.floor(player.getCurrentTime()) - console.log('Ad skipped!', videoId, startTime, videoData.title) + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) + } else { + playerEl.loadVideoByPlayerVars({ videoId, start }) + } + + console.log('Ad skipped!', videoId, start, videoData.title) } function getInterruptiveAd(): HTMLElement | null { @@ -99,24 +94,6 @@ function checkIsYouTubeShorts(): boolean { return location.pathname.startsWith('/shorts/') } -/** - * Finds and returns the current YouTube video player. - * - * @returns The current YouTube video player, or `null` if not found. - */ -function getYouTubePlayer(): YouTubePlayer | null { - let player: YouTubePlayer | null - if (isYouTubeMobile) { - const playerEl: unknown = document.querySelector('#movie_player') - player = playerEl as YouTubePlayer - } else { - const playerEl = document.querySelector('#ytd-player') - if (playerEl === null) return null - player = playerEl.getPlayer() - } - return player -} - function addCss(): void { const adsSelectors: string[] = [ // Ad banner in the upper right corner, above the video playlist. From 5c572b80af60065b85a094d51987e5bd5f18c671 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Mon, 17 Feb 2025 10:44:02 +0700 Subject: [PATCH 07/19] Remove unused `@types/trusted-types` lib --- .resources/global.d.ts | 3 --- package.json | 1 - pnpm-lock.yaml | 8 -------- 3 files changed, 12 deletions(-) diff --git a/.resources/global.d.ts b/.resources/global.d.ts index f3fbb53..50fff57 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -56,6 +56,3 @@ interface YtdPlayerElement extends HTMLElement { interface YouTubeMoviePlayerElement extends HTMLElement { loadVideoByPlayerVars(options: { videoId: string; start?: number }): void } - -declare type TrustedTypePolicyFactory = import('trusted-types/lib').TrustedTypePolicyFactory -declare const trustedTypes: TrustedTypePolicyFactory diff --git a/package.json b/package.json index 375ed01..d0ea677 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@types/node": "^20.14.13", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@types/trusted-types": "2.0.7", "concurrently": "^8.2.2", "eslint": "8.57.0", "eslint-plugin-react": "^7.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df8e21c..87cf79a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,6 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 - '@types/trusted-types': - specifier: 2.0.7 - version: 2.0.7 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -357,9 +354,6 @@ packages: '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1906,8 +1900,6 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@types/trusted-types@2.0.7': {} - '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)': From f60104f600d96883370b508b272b55d8a408e3c8 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Thu, 20 Feb 2025 08:59:55 +0700 Subject: [PATCH 08/19] Remove some CSS --- scripts/Auto-Skip-YouTube-Ads/script.user.js | 6 +++--- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index feebd61..d7a2006 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -12,7 +12,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.0.0 +// @version 7.0.1 // @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up. // @description:ar تخطي إعلانات YouTube تلقائيًا في الحال. إزالة النافذة المنبثقة لتحذير مانع الإعلانات. // @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios. @@ -263,12 +263,12 @@ function addCss() { // Masthead ad on home page. '#masthead-ad', - 'ytd-ad-slot-renderer', + // 'ytd-ad-slot-renderer', // Ad blocker warning inside the player. 'yt-playability-error-supported-renderers#error-screen', - '.ytp-suggested-action', + // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', // YouTube Music Premium trial promotion dialog, bottom left corner. diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index 22c5f94..f38a06c 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -12,7 +12,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.0.0 +// @version 7.0.1 // @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up. // @description:ar تخطي إعلانات YouTube تلقائيًا في الحال. إزالة النافذة المنبثقة لتحذير مانع الإعلانات. // @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios. @@ -272,12 +272,12 @@ function addCss(): void { // Masthead ad on home page. '#masthead-ad', - 'ytd-ad-slot-renderer', + // 'ytd-ad-slot-renderer', // Ad blocker warning inside the player. 'yt-playability-error-supported-renderers#error-screen', - '.ytp-suggested-action', + // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', // YouTube Music Premium trial promotion dialog, bottom left corner. From c29dd507459b68c7a8f8130441e588895a81cbca Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sat, 22 Feb 2025 12:28:47 +0700 Subject: [PATCH 09/19] Improve --- .editorconfig | 11 +++ .resources/global.d.ts | 3 + .vscode/settings.json | 3 +- README.md | 96 ++++++++++++++++++++- package.json | 2 +- scripts/Tetr-io-Improvements/script.user.js | 10 +-- scripts/Tetr-io-Improvements/script.user.ts | 10 +-- tailwind.config.js => tailwind.config.ts | 6 +- watch.ts | 37 ++++---- 9 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 .editorconfig rename tailwind.config.js => tailwind.config.ts (50%) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b311d28 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root=true + +[*] +end_of_line=lf +charset=utf-8 + +[*.{ts,tsx,js,json}] +indent_style=tab +indent_size=4 +trim_trailing_whitespace=true +insert_final_newline=true diff --git a/.resources/global.d.ts b/.resources/global.d.ts index ab9c2c9..311cf31 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -38,6 +38,7 @@ type GMOpenInTabOptions = { } declare function GM_openInTab(url: string, options?: GMOpenInTabOptions): void +declare type ReactElement = import('react').ReactElement declare type ChangeEvent = import('react').ChangeEvent declare type Immer = import('immer').Immer @@ -61,12 +62,14 @@ interface YouTubeMoviePlayerElement extends HTMLElement, YouTubePlayer { interface YouTubePlayer { getVideoData: () => YouTubeVideoData getCurrentTime: () => number + getDuration: () => number loadVideoById: (videoId: string, startTime?: number) => void } interface YouTubeVideoData { title: string video_id: string + isLive: boolean } interface YouTubeMoviePlayerElement extends HTMLElement { diff --git a/.vscode/settings.json b/.vscode/settings.json index a67c02d..8c48c71 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,6 +19,7 @@ }, "files.readonlyInclude": { "scripts/*/{dev,script}.user.js": true, - ".resources/tailwind.min.css": true + ".resources/tailwind.min.css": true, + "pnpm-lock.yaml": true } } diff --git a/README.md b/README.md index 1ff9d57..bba59aa 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,82 @@

-The userscript collection I wrote makes life easier and better. I use the browser extension [TamperMonkey](https://www.tampermonkey.net) to run and manage userscripts. You need to install it first to use userscript. +The userscript collection I wrote makes life easier and better. I use the browser extension [TamperMonkey][1] to run and manage userscripts. You need to install it first to use userscript. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionDaily installsTotal installs
+ Auto Skip YouTube Ads + + Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. + + + + +
+ YouTube Shorts To Normal Video + + Instantly redirect YouTube Shorts videos to normal video view, allowing you to save, download, choose quality, etc. + + + + +
+ No Fullscreen Dropdown + + Real fullscreen instead of fullscreen dropdown, very annoying when playing games. Useful for Microsoft Edge. Press Shift+F11 to toggle fullscreen. + + + + +
+ Tetr.io Improvements + + Provides improvements for Tetr.io game. + + + + +
## 📖 Usage ### Install on GreasyFork (Recommended) -Visit a list of some of my userscripts on the GreasyFork website [here](https://greasyfork.org/en/users/1306283-tientq64). +Visit a list of some of my userscripts on the GreasyFork website [here][2]. ### Install on GitHub @@ -27,22 +96,26 @@ Report bugs [here](https://github.com/tientq64/userscripts/issues). > This section is for developers. If you are a user, you can skip this section. Clone this repository: + ```cmd git clone https://github.com/tientq64/userscripts.git cd userscripts ``` Install `pnpm` if not installed: + ```cmd npm i -g pnpm ``` Install dependencies: + ```cmd pnpm install ``` Start: + ```cmd pnpm run watch ``` @@ -51,6 +124,25 @@ The `script.user.js` file is compiled from the `script.user.ts` file, so do not While developing, instead of having to reinstall the `script.user.js` file every time it changes, installing the `dev.user.js` file solves that problem. But note, if you change in the metadata block, you have to reinstall the `dev.user.js` file. +Built-in Tailwind CSS integration, just declare in the metadata block and use: + +```tsx +// ==UserScript== +// @resource TAILWINDCSS +// @grant GM_addStyle +// ==/UserScript== + +const tailwindCss: string = GM_getResourceText('TAILWINDCSS') +GM_addStyle(tailwindCss) + +const el: ReactElement =
+``` + +Common type definitions are written in [`.resources/global.d.ts`](.resources/global.d.ts) file. + ## ⚖️ License All scripts are licensed under the [MIT](./LICENSE) license. + +[1]: https://www.tampermonkey.net +[2]: https://greasyfork.org/en/users/1306283-tientq64 diff --git a/package.json b/package.json index d0ea677..d68521d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "homepage": "https://greasyfork.org/users/1306283-tientq64", "bugs": { - "email": "tientq@gmail.com", + "email": "tientq64@gmail.com", "url": "https://github.com/tientq64/userscripts/issues" }, "license": "MIT", diff --git a/scripts/Tetr-io-Improvements/script.user.js b/scripts/Tetr-io-Improvements/script.user.js index 2311e29..86df13e 100644 --- a/scripts/Tetr-io-Improvements/script.user.js +++ b/scripts/Tetr-io-Improvements/script.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Tetr.io Improvements // @namespace https://github.com/tientq64/userscripts -// @version 0.1.0 +// @version 1.0.0 // @description Provides improvements for Tetr.io game. // @author tientq64 // @icon https://www.google.com/s2/favicons?sz=64&domain=tetr.io @@ -69,8 +69,8 @@ var TetrIOImprovements window.addEventListener('keydown', handleWindowKeyDown) GM_addStyle(` - * { - transition: none !important; - } -`) + * { + transition: none !important; + } + `) })(TetrIOImprovements || (TetrIOImprovements = {})) diff --git a/scripts/Tetr-io-Improvements/script.user.ts b/scripts/Tetr-io-Improvements/script.user.ts index fee1ad1..b4c4e36 100644 --- a/scripts/Tetr-io-Improvements/script.user.ts +++ b/scripts/Tetr-io-Improvements/script.user.ts @@ -1,7 +1,7 @@ // ==UserScript== // @name Tetr.io Improvements // @namespace https://github.com/tientq64/userscripts -// @version 0.1.0 +// @version 1.0.0 // @description Provides improvements for Tetr.io game. // @author tientq64 // @icon https://www.google.com/s2/favicons?sz=64&domain=tetr.io @@ -72,8 +72,8 @@ namespace TetrIOImprovements { window.addEventListener('keydown', handleWindowKeyDown) GM_addStyle(` - * { - transition: none !important; - } -`) + * { + transition: none !important; + } + `) } diff --git a/tailwind.config.js b/tailwind.config.ts similarity index 50% rename from tailwind.config.js rename to tailwind.config.ts index e6d7780..2d8ca75 100644 --- a/tailwind.config.js +++ b/tailwind.config.ts @@ -1,5 +1,7 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { +/** + * @type {import('tailwindcss').Config} + */ +export default { content: ['**/*.tsx'], theme: { extend: {} diff --git a/watch.ts b/watch.ts index bde981c..1b98a81 100644 --- a/watch.ts +++ b/watch.ts @@ -9,28 +9,31 @@ function joinPath(...paths: string[]): string { return join(...paths).replace(/\\/g, '/') } -async function handleWatch(path: string, stat: Stats): Promise { +const endMetaTagRegex: RegExp = /^\/\/ ==\/UserScript==$/m +const tailwindcssMetaRegex: RegExp = /^\/\/ @resource {2,}TAILWINDCSS$/m +const watcherBlobs: string[] = ['scripts/*/script.user.{ts,tsx}', 'scripts/*/tracking.ts'] + +async function handleWatch(filePath: string, stat: Stats): Promise { if (!stat.isFile()) return - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dirPath: string = dirname(filePath).replace(/\\/g, '/') + const prodPath: string = joinPath(dirPath, 'script.user.js') + const devPath: string = joinPath(dirPath, 'dev.user.js') + const tsconfig: any = JSON.parse(readFileSync('tsconfig.json', 'utf-8')) - const dirPath: string = dirname(path).replace(/\\/g, '/') - const code: string = readFileSync(path, 'utf-8') + const userscriptCode: string = readFileSync(filePath, 'utf-8') - const matches = code.match(/\/\/ ==UserScript==\n.+?\n\/\/ ==\/UserScript==/su) + const matches = userscriptCode.match(/\/\/ ==UserScript==\n.+?\n\/\/ ==\/UserScript==/su) if (matches === null) return const meta: string = matches[0] - const ts: string = code.replace(meta, '') + const ts: string = userscriptCode.replace(meta, '') - const prodPath: string = joinPath(dirPath, 'script.user.js') - const devPath: string = joinPath(dirPath, 'dev.user.js') - - const metaPadLength: number = meta.match(/^\/\/ @([a-z:]+ {2,})/m)?.[1].length || 13 + const alignmentSpaces: number = meta.match(/^\/\/ @([a-z:]+ {2,})/m)?.[1].length || 13 const bothMeta: string = meta.replace( endMetaTagRegex, - `// @${'homepage'.padEnd(metaPadLength)}https://github.com/tientq64/userscripts/tree/main/${encodeURI(dirPath)}\n$&` + `// @${'homepage'.padEnd(alignmentSpaces)}https://github.com/tientq64/userscripts/tree/main/${encodeURI(dirPath)}\n$&` ) const prettierConfig = await resolveConfig('.prettierrc') @@ -49,26 +52,22 @@ async function handleWatch(path: string, stat: Stats): Promise { useTabs: false, parser: 'typescript' }) - const prodCode: string = `${prodMeta}\n\n${prodJs}` - writeFileSync(prodPath, prodCode) + const prodUserscriptCode: string = `${prodMeta}\n\n${prodJs}` + writeFileSync(prodPath, prodUserscriptCode) const devMeta: string = bothMeta - .replace(/^\/\/ @name(:[a-zA-Z-]+)? .+(? { const paths: string[] = sync(watcherBlobs) for (const path of paths) { From 163904cd2bb7fbf361fbe1a764583735907bfbe1 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 23 Feb 2025 18:52:03 +0700 Subject: [PATCH 10/19] - Experiment. - Fixed the issue where the chapters was not displayed on the right side of the video. --- README.md | 2 +- scripts/Auto-Skip-YouTube-Ads/README.md | 5 + scripts/Auto-Skip-YouTube-Ads/script.user.js | 373 +++++------------- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 385 +++++-------------- 4 files changed, 213 insertions(+), 552 deletions(-) diff --git a/README.md b/README.md index bba59aa..306634f 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Start: pnpm run watch ``` -The `script.user.js` file is compiled from the `script.user.ts` file, so do not modify it. +The `script.user.js` file is compiled from the `script.user.ts` or `script.user.tsx` file, so do not modify it. While developing, instead of having to reinstall the `script.user.js` file every time it changes, installing the `dev.user.js` file solves that problem. But note, if you change in the metadata block, you have to reinstall the `dev.user.js` file. diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index 27aa020..9ba9372 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -8,6 +8,11 @@ Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát h ## 📑 Changelog +### 7.1.0 - 2025-02-23 + +- Experiment. +- Fixed the issue where the chapters was not displayed on the right side of the video. + ### 7.0.0 - 2025-02-15 - Version 6 had many bug reports, reverting to the previous version 5 ([#6], [#279168]). diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index d7a2006..940776c 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -1,35 +1,38 @@ // ==UserScript== // @name Auto Skip YouTube Ads -// @name:ar التخطي التلقائي لإعلانات YouTube +// @name:ar تخطي إعلانات YouTube تلقائيًا // @name:es Saltar Automáticamente Anuncios De YouTube +// @name:fr Ignorer Automatiquement Les Publicités YouTube // @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें // @name:id Lewati Otomatis Iklan YouTube // @name:ja YouTube 広告を自動スキップ // @name:ko YouTube 광고 자동 건너뛰기 +// @name:nl YouTube-Advertenties Automatisch Overslaan // @name:pt-BR Pular Automaticamente Anúncios Do YouTube // @name:ru Автоматический Пропуск Рекламы На YouTube // @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.0.1 -// @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up. -// @description:ar تخطي إعلانات YouTube تلقائيًا في الحال. إزالة النافذة المنبثقة لتحذير مانع الإعلانات. -// @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios. -// @description:hi YouTube विज्ञापनों को लगभग तुरंत ही स्वचालित रूप से छोड़ दें। विज्ञापन अवरोधक चेतावनी पॉप-अप हटाएँ। -// @description:id Lewati iklan YouTube secara otomatis hampir seketika. Hapus pop-up peringatan pemblokir iklan. -// @description:ja YouTube 広告をほぼ瞬時に自動的にスキップします。広告ブロッカーの警告ポップアップを削除します。 -// @description:ko YouTube 광고를 거의 즉시 자동으로 건너뜁니다. 광고 차단 경고 팝업을 제거합니다. -// @description:pt-BR Pule automaticamente os anúncios do YouTube quase instantaneamente. Remova o pop-up de aviso do bloqueador de anúncios. -// @description:ru Автоматически пропускайте рекламу YouTube почти мгновенно. Уберите всплывающее предупреждение о блокировщике рекламы. -// @description:vi Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo. -// @description:zh-CN 几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。 -// @description:zh-TW 幾乎立即自動跳過 YouTube 廣告。刪除廣告攔截器警告彈出視窗。 +// @version 7.1.0 +// @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. +// @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. +// @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. +// @description:fr Ignorez automatiquement et instantanément les publicités YouTube. Non détecté par les avertissements du bloqueur de publicités YouTube. +// @description:hi YouTube विज्ञापनों को स्वचालित रूप से तुरंत छोड़ दें। YouTube विज्ञापन अवरोधक चेतावनियों द्वारा पता नहीं लगाया गया। +// @description:id Lewati iklan YouTube secara otomatis secara instan. Tidak terdeteksi oleh peringatan pemblokir iklan YouTube. +// @description:ja YouTube 広告を即座に自動的にスキップします。YouTube 広告ブロッカーの警告には検出されません。 +// @description:ko YouTube 광고를 즉시 자동으로 건너뜁니다. YouTube 광고 차단 경고에 감지되지 않습니다. +// @description:nl Sla YouTube-advertenties direct automatisch over. Ongemerkt door YouTube-adblockerwaarschuwingen. +// @description:pt-BR Pule anúncios do YouTube instantaneamente. Não detectado pelos avisos do bloqueador de anúncios do YouTube. +// @description:ru Автоматически пропускать рекламу YouTube мгновенно. Не обнаруживается предупреждениями блокировщиков рекламы YouTube. +// @description:vi Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. +// @description:zh-CN 立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 +// @description:zh-TW 立即自動跳過 YouTube 廣告。 YouTube 廣告攔截器警告未被偵測到。 // @author tientq64 // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* -// @match https://music.youtube.com/* // @grant none // @license MIT // @compatible firefox @@ -41,273 +44,145 @@ // @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Auto-Skip-YouTube-Ads // ==/UserScript== -/** - * Skip ads. Remove ad elements. - */ function skipAd() { - removeAdElements() - - video = null - fineScrubber = document.querySelector('.ytp-fine-scrubbing') - - // Check if the current URL is a YouTube Shorts URL and exit the function if true. - if (window.location.pathname.startsWith('/shorts/')) return - - const moviePlayer = document.querySelector('#movie_player') + const isYouTubeShorts = checkIsYouTubeShorts() + if (isYouTubeShorts) return - if (moviePlayer) { - hasAd = moviePlayer.classList.contains('ad-showing') - video = moviePlayer.querySelector('video.html5-main-video') - } - - if (hasAd) { - const skipButton = document.querySelector(` - .ytp-skip-ad-button, - .ytp-ad-skip-button, - .ytp-ad-skip-button-modern, - .ytp-ad-survey-answer-button - `) - // Click the skip ad button if available. - if (skipButton) { - skipButton.click() - skipButton.remove() - } - // Otherwise, fast forward to the end of the ad video. - // Use `9999` instead of `video.duration` to avoid errors when `duration` is not a number. - else if (video && video.src) { - video.currentTime = 9999 - } - } - - if (video) { - video.addEventListener('pause', handleVideoPause) - video.addEventListener('pointerup', allowPauseVideo) - video.addEventListener('timeupdate', handleVideoTimeUpdate) - } + // This element appears when a video ad appears. + const adShowing = document.querySelector('.ad-showing') // Timed pie countdown ad. const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container') - if (pieCountdown) { - pieCountdown.remove() - replaceCurrentVideo() - } - // Handle when ad blocker warning appears inside video player. - const adBlockerWarningInner = document.querySelector( - '.yt-playability-error-supported-renderers' - ) - if (adBlockerWarningInner) { - adBlockerWarningInner.remove() - document.addEventListener('yt-navigate-finish', handleYouTubeNavigateFinish) - replaceCurrentVideo() - } + // Survey questions in video player. + const surveyQuestions = document.querySelector('.ytp-ad-survey-questions') - // Video play/pause button. - const playButton = document.querySelector( - 'button.ytp-play-button, button.player-control-play-pause-icon' - ) - if (playButton) { - playButton.addEventListener('click', allowPauseVideo) - } -} + if (adShowing === null && pieCountdown === null && surveyQuestions === null) return -function queryHasSelector(selector, hasSelector, element = document) { - const el = element.querySelector(selector) - if (el === null) return null - const hasEl = el.querySelector(hasSelector) - if (hasEl === null) return null - return el -} - -function queryHasSelectorAll(selector, hasSelector, element = document) { - const els = element.querySelectorAll(selector) - const result = [] - for (const el of els) { - const hasEl = el.querySelector(hasSelector) - if (hasEl === null) continue - result.push(el) + let playerEl + let player + if (isYouTubeMobile) { + playerEl = document.querySelector('#movie_player') + player = playerEl + } else { + playerEl = document.querySelector('#ytd-player') + player = playerEl && playerEl.getPlayer() } - return result -} -function getCurrentVideoId() { - const params = new URLSearchParams(location.search) - const videoId = params.get('v') - return videoId -} - -/** - * Check if the user is focused on the input. - */ -function checkEnteringInput() { - if (document.activeElement === null) { - return false + if (playerEl === null || player === null) { + console.log({ + message: 'Player not found', + timeStamp: getCurrentTimeString() + }) + return } - return document.activeElement.matches('input, textarea, select') -} -/** - * Temporarily allows the video to be paused, for a short period of time. - */ -function allowPauseVideo() { - pausedByUser = true - window.clearTimeout(allowPauseVideoTimeoutId) - allowPauseVideoTimeoutId = window.setTimeout(disallowPauseVideo, 500) -} + // ad.classList.remove('ad-showing') -/** - * Pausing the video is not allowed. The purpose is to prevent video from being paused, - * against the behavior of pausing video when YouTube ad blocking warning dialog appears. - * Unless certain conditions, such as pausing by user, etc. - */ -function disallowPauseVideo() { - pausedByUser = false - window.clearTimeout(allowPauseVideoTimeoutId) -} + if (pieCountdown === null && surveyQuestions === null) { + const adVideo = document.querySelector('#ytd-player video.html5-main-video') -function handleWindowBlur() { - isTabBlurred = true -} + console.table({ + message: 'Ad video', + video: adVideo !== null, + src: adVideo?.src, + paused: adVideo?.paused, + currentTime: adVideo?.currentTime, + duration: adVideo?.duration, + timeStamp: getCurrentTimeString() + }) -function handleWindowFocus() { - isTabBlurred = false -} + if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) return -/** - * Handle when video is paused. If certain conditions are not met, it will continue - * playing. Returning early in this function means the video should be paused as it should - * be. - */ -function handleVideoPause() { - if (isYouTubeMusic) return + adVideo.muted = true + adVideo.pause() - // If it was stopped by the user, it's ok, let the video pause as it should, and exit the function. - if (pausedByUser) { - disallowPauseVideo() - return + console.log({ + message: 'Paused ad video', + timeStamp: getCurrentTimeString() + }) } - // The video will pause normally if the tab is not focused. This is to allow for pausing the video via the media controller (of the browser or operating system), etc. - // Note: While this also gives YouTube the opportunity to pause videos to annoy users, it's an acceptable trade-off. - if (document.hidden) return - if (isTabBlurred) return - - if (fineScrubber && fineScrubber.style.display !== 'none') return - if (video === null) return - if (video.duration - video.currentTime < 0.1) return - - // This is YouTube's disruptive behavior towards users, so the video should continue to play as normal. - video.play() -} - -function handleVideoTimeUpdate() { - if (hasAd || video === null) return - currentVideoTime = video.currentTime -} + const videoData = player.getVideoData() + const videoId = videoData.video_id + const start = Math.floor(player.getCurrentTime()) -/** - * Handle both keyboard press or release events. - */ -function handleWindowKeyDownAndKeyUp(event) { - if (isYouTubeMusic) return - if (checkEnteringInput()) return - const code = event.code - if (event.type === 'keydown') { - if (code === 'KeyK' || code === 'MediaPlayPause') { - allowPauseVideo() - } + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) } else { - if (code === 'Space') { - allowPauseVideo() - } + playerEl.loadVideoByPlayerVars({ videoId, start }) } -} -function handleYouTubeNavigateFinish() { - currentVideoTime = 0 - replaceCurrentVideo() + console.table({ + message: 'Ad skipped!', + videoId, + start, + title: videoData.title, + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) } -async function replaceCurrentVideo() { - const start = Math.floor(currentVideoTime) - for (let i = 0; i < 16; i++) { - await waitFor(500) - const videoId = getCurrentVideoId() - if (!videoId || !video || video.src) continue - if (isYouTubeMobile) { - const player = document.querySelector('#movie_player') - if (!player) continue - player.loadVideoByPlayerVars({ videoId, start }) - } else { - const player = document.querySelector('#ytd-player') - if (!player) continue - player.loadVideoWithPlayerVars({ videoId, start }) - } - } +function checkIsYouTubeShorts() { + return location.pathname.startsWith('/shorts/') } -function waitFor(millis) { - return new Promise((resolve) => { - window.setTimeout(resolve, millis) - }) +function getCurrentTimeString() { + return new Date().toTimeString().split(' ', 1)[0] } -/** - * Add CSS hides some ad elements on the page. - */ function addCss() { - const hideCssSelector = [ + const adsSelectors = [ // Ad banner in the upper right corner, above the video playlist. '#player-ads', + '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]', // Masthead ad on home page. '#masthead-ad', + // Sponsored ad video items on home page. // 'ytd-ad-slot-renderer', - // Ad blocker warning inside the player. - 'yt-playability-error-supported-renderers#error-screen', - // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', + // Featured product ad banner at the bottom left of the video. + '.ytp-featured-product', + + // Products shelf ad banner below the video description. + 'ytd-merch-shelf-renderer', + // YouTube Music Premium trial promotion dialog, bottom left corner. 'ytmusic-mealbar-promo-renderer', // YouTube Music Premium trial promotion banner on home page. 'ytmusic-statement-banner-renderer' - ].join(',') - const css = ` - #ytd-player { visibility: visible !important; } - ${hideCssSelector} { display: none !important; } - ` + ] + const adsSelector = adsSelectors.join(',') + const css = `${adsSelector} { display: none !important; }` const style = document.createElement('style') style.textContent = css document.head.appendChild(style) } /** - * Remove ad elements using javascript because these selectors require the use of the CSS + * Remove ad elements using JavaScript because these selectors require the use of the CSS * `:has` selector which is not supported in older browser versions. */ function removeAdElements() { const adSelectors = [ - // Ad banner in the upper right corner, above the video playlist. - ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], - - // Temporarily comment out this selector to fix issue [#265124](https://greasyfork.org/en/scripts/498197-auto-skip-youtube-ads/discussions/265124). - // ['#panels', 'ytd-ads-engagement-panel-content-renderer'], - // Sponsored ad video items on home page. // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], - // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], + // Ad videos on YouTube Shorts. + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] // Ad blocker warning dialog. - ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'] + // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], // Survey dialog on home page, located at bottom right. // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], @@ -316,63 +191,19 @@ function removeAdElements() { // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { - const adEls = queryHasSelectorAll(adSelector[0], adSelector[1]) - for (const adEl of adEls) { - adEl.remove() - } + const adEl = document.querySelector(adSelector[0]) + if (adEl === null) continue + const neededEl = adEl.querySelector(adSelector[1]) + if (neededEl === null) continue + adEl.remove() } } -/** - * Is it YouTube mobile version. - */ const isYouTubeMobile = location.hostname === 'm.youtube.com' -/** - * Is the current page YouTube Music. - */ -const isYouTubeMusic = location.hostname === 'music.youtube.com' - -/** - * Current video element. - */ -let video = null - -let fineScrubber = null -let hasAd = false -let currentVideoTime = 0 - -/** - * Is the video paused by the user, not paused by YouTube's ad blocker warning dialog. - */ -let pausedByUser = false - -/** - * Is the current tab blurred. - */ -let isTabBlurred = false - -let allowPauseVideoTimeoutId = 0 - -// Observe DOM changes to detect ads. -if (window.MutationObserver) { - const observer = new MutationObserver(skipAd) - observer.observe(document.body, { - attributes: true, - attributeFilter: ['class', 'src'], - childList: true, - subtree: true - }) -} -// If DOM observation is not supported. Detect ads every 500ms (2 times per second). -else { - window.setInterval(skipAd, 500) -} - -window.addEventListener('blur', handleWindowBlur) -window.addEventListener('focus', handleWindowFocus) -window.addEventListener('keydown', handleWindowKeyDownAndKeyUp) -window.addEventListener('keyup', handleWindowKeyDownAndKeyUp) +window.setInterval(skipAd, 500) +window.setInterval(removeAdElements, 1000) addCss() +removeAdElements() skipAd() diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index f38a06c..a6f10f9 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -1,35 +1,38 @@ // ==UserScript== // @name Auto Skip YouTube Ads -// @name:ar التخطي التلقائي لإعلانات YouTube +// @name:ar تخطي إعلانات YouTube تلقائيًا // @name:es Saltar Automáticamente Anuncios De YouTube +// @name:fr Ignorer Automatiquement Les Publicités YouTube // @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें // @name:id Lewati Otomatis Iklan YouTube // @name:ja YouTube 広告を自動スキップ // @name:ko YouTube 광고 자동 건너뛰기 +// @name:nl YouTube-Advertenties Automatisch Overslaan // @name:pt-BR Pular Automaticamente Anúncios Do YouTube // @name:ru Автоматический Пропуск Рекламы На YouTube // @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.0.1 -// @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up. -// @description:ar تخطي إعلانات YouTube تلقائيًا في الحال. إزالة النافذة المنبثقة لتحذير مانع الإعلانات. -// @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios. -// @description:hi YouTube विज्ञापनों को लगभग तुरंत ही स्वचालित रूप से छोड़ दें। विज्ञापन अवरोधक चेतावनी पॉप-अप हटाएँ। -// @description:id Lewati iklan YouTube secara otomatis hampir seketika. Hapus pop-up peringatan pemblokir iklan. -// @description:ja YouTube 広告をほぼ瞬時に自動的にスキップします。広告ブロッカーの警告ポップアップを削除します。 -// @description:ko YouTube 광고를 거의 즉시 자동으로 건너뜁니다. 광고 차단 경고 팝업을 제거합니다. -// @description:pt-BR Pule automaticamente os anúncios do YouTube quase instantaneamente. Remova o pop-up de aviso do bloqueador de anúncios. -// @description:ru Автоматически пропускайте рекламу YouTube почти мгновенно. Уберите всплывающее предупреждение о блокировщике рекламы. -// @description:vi Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo. -// @description:zh-CN 几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。 -// @description:zh-TW 幾乎立即自動跳過 YouTube 廣告。刪除廣告攔截器警告彈出視窗。 +// @version 7.1.0 +// @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. +// @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. +// @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. +// @description:fr Ignorez automatiquement et instantanément les publicités YouTube. Non détecté par les avertissements du bloqueur de publicités YouTube. +// @description:hi YouTube विज्ञापनों को स्वचालित रूप से तुरंत छोड़ दें। YouTube विज्ञापन अवरोधक चेतावनियों द्वारा पता नहीं लगाया गया। +// @description:id Lewati iklan YouTube secara otomatis secara instan. Tidak terdeteksi oleh peringatan pemblokir iklan YouTube. +// @description:ja YouTube 広告を即座に自動的にスキップします。YouTube 広告ブロッカーの警告には検出されません。 +// @description:ko YouTube 광고를 즉시 자동으로 건너뜁니다. YouTube 광고 차단 경고에 감지되지 않습니다. +// @description:nl Sla YouTube-advertenties direct automatisch over. Ongemerkt door YouTube-adblockerwaarschuwingen. +// @description:pt-BR Pule anúncios do YouTube instantaneamente. Não detectado pelos avisos do bloqueador de anúncios do YouTube. +// @description:ru Автоматически пропускать рекламу YouTube мгновенно. Не обнаруживается предупреждениями блокировщиков рекламы YouTube. +// @description:vi Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. +// @description:zh-CN 立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 +// @description:zh-TW 立即自動跳過 YouTube 廣告。 YouTube 廣告攔截器警告未被偵測到。 // @author tientq64 // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* -// @match https://music.youtube.com/* // @grant none // @license MIT // @compatible firefox @@ -40,283 +43,149 @@ // @noframes // ==/UserScript== -/** - * Skip ads. Remove ad elements. - */ function skipAd(): void { - removeAdElements() - - video = null - fineScrubber = document.querySelector('.ytp-fine-scrubbing') - - // Check if the current URL is a YouTube Shorts URL and exit the function if true. - if (window.location.pathname.startsWith('/shorts/')) return - - const moviePlayer = document.querySelector('#movie_player') + const isYouTubeShorts: boolean = checkIsYouTubeShorts() + if (isYouTubeShorts) return - if (moviePlayer) { - hasAd = moviePlayer.classList.contains('ad-showing') - video = moviePlayer.querySelector('video.html5-main-video') - } - - if (hasAd) { - const skipButton = document.querySelector(` - .ytp-skip-ad-button, - .ytp-ad-skip-button, - .ytp-ad-skip-button-modern, - .ytp-ad-survey-answer-button - `) - // Click the skip ad button if available. - if (skipButton) { - skipButton.click() - skipButton.remove() - } - // Otherwise, fast forward to the end of the ad video. - // Use `9999` instead of `video.duration` to avoid errors when `duration` is not a number. - else if (video && video.src) { - video.currentTime = 9999 - } - } - - if (video) { - video.addEventListener('pause', handleVideoPause) - video.addEventListener('pointerup', allowPauseVideo) - video.addEventListener('timeupdate', handleVideoTimeUpdate) - } + // This element appears when a video ad appears. + const adShowing = document.querySelector('.ad-showing') // Timed pie countdown ad. const pieCountdown = document.querySelector( '.ytp-ad-timed-pie-countdown-container' ) - if (pieCountdown) { - pieCountdown.remove() - replaceCurrentVideo() - } - // Handle when ad blocker warning appears inside video player. - const adBlockerWarningInner = document.querySelector( - '.yt-playability-error-supported-renderers' - ) - if (adBlockerWarningInner) { - adBlockerWarningInner.remove() - document.addEventListener('yt-navigate-finish', handleYouTubeNavigateFinish) - replaceCurrentVideo() - } + // Survey questions in video player. + const surveyQuestions = document.querySelector('.ytp-ad-survey-questions') - // Video play/pause button. - const playButton = document.querySelector( - 'button.ytp-play-button, button.player-control-play-pause-icon' - ) - if (playButton) { - playButton.addEventListener('click', allowPauseVideo) - } -} - -function queryHasSelector( - selector: string, - hasSelector: string, - element: Document | Element = document -): T | null { - const el = element.querySelector(selector) - if (el === null) return null - const hasEl = el.querySelector(hasSelector) - if (hasEl === null) return null - return el -} + if (adShowing === null && pieCountdown === null && surveyQuestions === null) return -function queryHasSelectorAll( - selector: string, - hasSelector: string, - element: Document | Element = document -): T[] { - const els = element.querySelectorAll(selector) - const result: T[] = [] - for (const el of els) { - const hasEl = el.querySelector(hasSelector) - if (hasEl === null) continue - result.push(el) + let playerEl: YtdPlayerElement | YouTubeMoviePlayerElement | null + let player: YouTubePlayer | YouTubeMoviePlayerElement | null + if (isYouTubeMobile) { + playerEl = document.querySelector('#movie_player') + player = playerEl + } else { + playerEl = document.querySelector('#ytd-player') + player = playerEl && playerEl.getPlayer() } - return result -} - -function getCurrentVideoId(): string | null { - const params = new URLSearchParams(location.search) - const videoId: string | null = params.get('v') - return videoId -} -/** - * Check if the user is focused on the input. - */ -function checkEnteringInput(): boolean { - if (document.activeElement === null) { - return false + if (playerEl === null || player === null) { + console.log({ + message: 'Player not found', + timeStamp: getCurrentTimeString() + }) + return } - return document.activeElement.matches('input, textarea, select') -} -/** - * Temporarily allows the video to be paused, for a short period of time. - */ -function allowPauseVideo(): void { - pausedByUser = true - window.clearTimeout(allowPauseVideoTimeoutId) - allowPauseVideoTimeoutId = window.setTimeout(disallowPauseVideo, 500) -} + // ad.classList.remove('ad-showing') -/** - * Pausing the video is not allowed. The purpose is to prevent video from being paused, - * against the behavior of pausing video when YouTube ad blocking warning dialog appears. - * Unless certain conditions, such as pausing by user, etc. - */ -function disallowPauseVideo(): void { - pausedByUser = false - window.clearTimeout(allowPauseVideoTimeoutId) -} + if (pieCountdown === null && surveyQuestions === null) { + const adVideo = document.querySelector( + '#ytd-player video.html5-main-video' + ) -function handleWindowBlur(): void { - isTabBlurred = true -} + console.table({ + message: 'Ad video', + video: adVideo !== null, + src: adVideo?.src, + paused: adVideo?.paused, + currentTime: adVideo?.currentTime, + duration: adVideo?.duration, + timeStamp: getCurrentTimeString() + }) -function handleWindowFocus(): void { - isTabBlurred = false -} + if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) return -/** - * Handle when video is paused. If certain conditions are not met, it will continue - * playing. Returning early in this function means the video should be paused as it should - * be. - */ -function handleVideoPause(): void { - if (isYouTubeMusic) return + adVideo.muted = true + adVideo.pause() - // If it was stopped by the user, it's ok, let the video pause as it should, and exit the function. - if (pausedByUser) { - disallowPauseVideo() - return + console.log({ + message: 'Paused ad video', + timeStamp: getCurrentTimeString() + }) } - // The video will pause normally if the tab is not focused. This is to allow for pausing the video via the media controller (of the browser or operating system), etc. - // Note: While this also gives YouTube the opportunity to pause videos to annoy users, it's an acceptable trade-off. - if (document.hidden) return - if (isTabBlurred) return - - if (fineScrubber && fineScrubber.style.display !== 'none') return - if (video === null) return - if (video.duration - video.currentTime < 0.1) return + const videoData: YouTubeVideoData = player.getVideoData() + const videoId: string = videoData.video_id + const start: number = Math.floor(player.getCurrentTime()) - // This is YouTube's disruptive behavior towards users, so the video should continue to play as normal. - video.play() -} - -function handleVideoTimeUpdate(): void { - if (hasAd || video === null) return - currentVideoTime = video.currentTime -} - -/** - * Handle both keyboard press or release events. - */ -function handleWindowKeyDownAndKeyUp(event: KeyboardEvent): void { - if (isYouTubeMusic) return - if (checkEnteringInput()) return - const code: string = event.code - if (event.type === 'keydown') { - if (code === 'KeyK' || code === 'MediaPlayPause') { - allowPauseVideo() - } + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) } else { - if (code === 'Space') { - allowPauseVideo() - } + playerEl.loadVideoByPlayerVars({ videoId, start }) } -} -function handleYouTubeNavigateFinish(): void { - currentVideoTime = 0 - replaceCurrentVideo() + console.table({ + message: 'Ad skipped!', + videoId, + start, + title: videoData.title, + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) } -async function replaceCurrentVideo(): Promise { - const start: number = Math.floor(currentVideoTime) - for (let i = 0; i < 16; i++) { - await waitFor(500) - const videoId: string | null = getCurrentVideoId() - if (!videoId || !video || video.src) continue - if (isYouTubeMobile) { - const player = document.querySelector('#movie_player') - if (!player) continue - player.loadVideoByPlayerVars({ videoId, start }) - } else { - const player = document.querySelector('#ytd-player') - if (!player) continue - player.loadVideoWithPlayerVars({ videoId, start }) - } - } +function checkIsYouTubeShorts(): boolean { + return location.pathname.startsWith('/shorts/') } -function waitFor(millis: number): Promise { - return new Promise((resolve) => { - window.setTimeout(resolve, millis) - }) +function getCurrentTimeString(): string { + return new Date().toTimeString().split(' ', 1)[0] } -/** - * Add CSS hides some ad elements on the page. - */ function addCss(): void { - const hideCssSelector: string = [ + const adsSelectors: string[] = [ // Ad banner in the upper right corner, above the video playlist. '#player-ads', + '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]', // Masthead ad on home page. '#masthead-ad', + // Sponsored ad video items on home page. // 'ytd-ad-slot-renderer', - // Ad blocker warning inside the player. - 'yt-playability-error-supported-renderers#error-screen', - // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', + // Featured product ad banner at the bottom left of the video. + '.ytp-featured-product', + + // Products shelf ad banner below the video description. + 'ytd-merch-shelf-renderer', + // YouTube Music Premium trial promotion dialog, bottom left corner. 'ytmusic-mealbar-promo-renderer', // YouTube Music Premium trial promotion banner on home page. 'ytmusic-statement-banner-renderer' - ].join(',') - const css: string = ` - #ytd-player { visibility: visible !important; } - ${hideCssSelector} { display: none !important; } - ` - const style: HTMLStyleElement = document.createElement('style') + ] + const adsSelector: string = adsSelectors.join(',') + const css: string = `${adsSelector} { display: none !important; }` + const style = document.createElement('style') style.textContent = css document.head.appendChild(style) } /** - * Remove ad elements using javascript because these selectors require the use of the CSS + * Remove ad elements using JavaScript because these selectors require the use of the CSS * `:has` selector which is not supported in older browser versions. */ function removeAdElements(): void { const adSelectors: [string, string][] = [ - // Ad banner in the upper right corner, above the video playlist. - ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], - - // Temporarily comment out this selector to fix issue [#265124](https://greasyfork.org/en/scripts/498197-auto-skip-youtube-ads/discussions/265124). - // ['#panels', 'ytd-ads-engagement-panel-content-renderer'], - // Sponsored ad video items on home page. // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], - // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], + // Ad videos on YouTube Shorts. + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] // Ad blocker warning dialog. - ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'] + // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], // Survey dialog on home page, located at bottom right. // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], @@ -325,63 +194,19 @@ function removeAdElements(): void { // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { - const adEls = queryHasSelectorAll(adSelector[0], adSelector[1]) - for (const adEl of adEls) { - adEl.remove() - } + const adEl = document.querySelector(adSelector[0]) + if (adEl === null) continue + const neededEl = adEl.querySelector(adSelector[1]) + if (neededEl === null) continue + adEl.remove() } } -/** - * Is it YouTube mobile version. - */ const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com' -/** - * Is the current page YouTube Music. - */ -const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com' - -/** - * Current video element. - */ -let video: HTMLVideoElement | null = null - -let fineScrubber: HTMLDivElement | null = null -let hasAd: boolean = false -let currentVideoTime: number = 0 - -/** - * Is the video paused by the user, not paused by YouTube's ad blocker warning dialog. - */ -let pausedByUser: boolean = false - -/** - * Is the current tab blurred. - */ -let isTabBlurred: boolean = false - -let allowPauseVideoTimeoutId: number = 0 - -// Observe DOM changes to detect ads. -if (window.MutationObserver) { - const observer: MutationObserver = new MutationObserver(skipAd) - observer.observe(document.body, { - attributes: true, - attributeFilter: ['class', 'src'], - childList: true, - subtree: true - }) -} -// If DOM observation is not supported. Detect ads every 500ms (2 times per second). -else { - window.setInterval(skipAd, 500) -} - -window.addEventListener('blur', handleWindowBlur) -window.addEventListener('focus', handleWindowFocus) -window.addEventListener('keydown', handleWindowKeyDownAndKeyUp) -window.addEventListener('keyup', handleWindowKeyDownAndKeyUp) +window.setInterval(skipAd, 500) +window.setInterval(removeAdElements, 1000) addCss() +removeAdElements() skipAd() From 032ed6137841285f94f543935edbf759ac64445c Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 23 Feb 2025 19:02:55 +0700 Subject: [PATCH 11/19] Update changelog --- scripts/Auto-Skip-YouTube-Ads/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index 9ba9372..882d30d 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -11,7 +11,7 @@ Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát h ### 7.1.0 - 2025-02-23 - Experiment. -- Fixed the issue where the chapters was not displayed on the right side of the video. +- Fixed the issue where the chapters was not displayed on the right side of the video ([#10], [#279783]). ### 7.0.0 - 2025-02-15 @@ -183,9 +183,11 @@ _Merry Christmas!_ Youtube icons created by Ruslan Babkin - Flaticon. +[#10]: https://github.com/tientq64/userscripts/issues/10 [#6]: https://github.com/tientq64/userscripts/issues/6 [#2]: https://github.com/tientq64/userscripts/issues/2 [#1]: https://github.com/tientq64/userscripts/pull/1 +[#279783]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279783 [#279168]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279168 [#267857]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/267857 [#261679]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/261679 From d8b8ae3c7c518df2b7dedae9e208bd9d0cf25c5e Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 23 Feb 2025 23:49:27 +0700 Subject: [PATCH 12/19] Exclude `studio.youtube.com` --- scripts/Auto-Skip-YouTube-Ads/script.user.js | 3 ++- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 940776c..1aec2ca 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.1.0 +// @version 7.1.1 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -33,6 +33,7 @@ // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @exclude https://studio.youtube.com/* // @grant none // @license MIT // @compatible firefox diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index a6f10f9..5b1cd54 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.1.0 +// @version 7.1.1 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -33,6 +33,7 @@ // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @exclude https://studio.youtube.com/* // @grant none // @license MIT // @compatible firefox From c1fa36b2b47d8ff2ee0ad26c989337a5f9c7bea2 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 23 Feb 2025 23:51:17 +0700 Subject: [PATCH 13/19] Include YouTube Mobile, exclude YouTube Studio --- .../script.user.js | 74 ++++++++++--------- .../script.user.ts | 72 +++++++++--------- 2 files changed, 75 insertions(+), 71 deletions(-) diff --git a/scripts/YouTube-Shorts-To-Normal-Video/script.user.js b/scripts/YouTube-Shorts-To-Normal-Video/script.user.js index 364ce78..bf803a8 100644 --- a/scripts/YouTube-Shorts-To-Normal-Video/script.user.js +++ b/scripts/YouTube-Shorts-To-Normal-Video/script.user.js @@ -1,47 +1,49 @@ // ==UserScript== -// @name YouTube Shorts To Normal Video -// @name:vi YouTube Shorts Thành Video Bình Thường -// @name:zh-CN YouTube Shorts 转普通视频 -// @name:zh-TW YouTube Shorts 轉普通視頻 -// @name:ja YouTube ショートから通常の動画へ -// @name:ko YouTube Shorts를 일반 비디오로 -// @name:es Cortos De YouTube A Video Normal -// @name:pt-BR YouTube Shorts Para Vídeo Normal -// @name:ru YouTube Shorts В Обычное Видео -// @name:id YouTube Shorts Ke Video Normal -// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक -// @name:nl YouTube Shorts Naar Normale Video -// @name:fr Short YouTube Vers Vidéo Normale -// @name:de YouTube-Shorts Zum Normalen Video -// @name:it Da YouTube Shorts A Video Normale -// @name:tr YouTube Shorts'tan Normal Videoya -// @name:th YouTube Shorts สู่วิดีโอปกติ -// @namespace https://github.com/tientq64/userscripts -// @version 1.0.2 +// @name YouTube Shorts To Normal Video +// @name:de YouTube-Shorts Zum Normalen Video +// @name:es Cortos De YouTube A Video Normal +// @name:fr Short YouTube Vers Vidéo Normale +// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक +// @name:id YouTube Shorts Ke Video Normal +// @name:it Da YouTube Shorts A Video Normale +// @name:ja YouTube ショートから通常の動画へ +// @name:ko YouTube Shorts를 일반 비디오로 +// @name:nl YouTube Shorts Naar Normale Video +// @name:pt-BR YouTube Shorts Para Vídeo Normal +// @name:ru YouTube Shorts В Обычное Видео +// @name:th YouTube Shorts สู่วิดีโอปกติ +// @name:tr YouTube Shorts'tan Normal Videoya +// @name:vi YouTube Shorts Thành Video Bình Thường +// @name:zh-CN YouTube Shorts 转普通视频 +// @name:zh-TW YouTube Shorts 轉普通視頻 +// @namespace https://github.com/tientq64/userscripts +// @version 1.1.0 // @description Instantly redirect YouTube Shorts videos to normal video view, allowing you to save, download, choose quality, etc. -// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. -// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 -// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. +// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. +// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। +// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. +// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. // @description:ja YouTube Shorts 動画を通常の動画ビューに即座にリダイレクトし、保存、ダウンロード、品質の選択などを行うことができます。 // @description:ko YouTube Shorts 동영상을 일반 동영상 보기로 즉시 리디렉션하여 저장, 다운로드, 품질 선택 등이 가능합니다. -// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. // @description:pt-BR Redirecione instantaneamente os vídeos do YouTube Shorts para a visualização normal, permitindo que você salve, baixe, escolha a qualidade, etc. // @description:ru Мгновенно перенаправляет видеоролики YouTube Shorts в обычный режим просмотра, позволяя сохранять, загружать, выбирать качество и т. д. -// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. -// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। -// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. -// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. -// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. -// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. -// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. // @description:th เปลี่ยนเส้นทางวิดีโอ YouTube Shorts ไปยังมุมมองวิดีโอปกติทันที ช่วยให้คุณบันทึก ดาวน์โหลด เลือกคุณภาพ ฯลฯ ได้ -// @author tientq64 -// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png -// @match https://www.youtube.com/* -// @license MIT -// @grant none +// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. +// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 +// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png +// @match https://www.youtube.com/* +// @match https://m.youtube.com/* +// @exclude https://studio.youtube.com/* +// @license MIT +// @grant none // @noframes -// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/YouTube-Shorts-To-Normal-Video +// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/YouTube-Shorts-To-Normal-Video // ==/UserScript== function redirect() { diff --git a/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts b/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts index 5ffa3a5..b267168 100644 --- a/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts +++ b/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts @@ -1,45 +1,47 @@ // ==UserScript== -// @name YouTube Shorts To Normal Video -// @name:vi YouTube Shorts Thành Video Bình Thường -// @name:zh-CN YouTube Shorts 转普通视频 -// @name:zh-TW YouTube Shorts 轉普通視頻 -// @name:ja YouTube ショートから通常の動画へ -// @name:ko YouTube Shorts를 일반 비디오로 -// @name:es Cortos De YouTube A Video Normal -// @name:pt-BR YouTube Shorts Para Vídeo Normal -// @name:ru YouTube Shorts В Обычное Видео -// @name:id YouTube Shorts Ke Video Normal -// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक -// @name:nl YouTube Shorts Naar Normale Video -// @name:fr Short YouTube Vers Vidéo Normale -// @name:de YouTube-Shorts Zum Normalen Video -// @name:it Da YouTube Shorts A Video Normale -// @name:tr YouTube Shorts'tan Normal Videoya -// @name:th YouTube Shorts สู่วิดีโอปกติ -// @namespace https://github.com/tientq64/userscripts -// @version 1.0.2 +// @name YouTube Shorts To Normal Video +// @name:de YouTube-Shorts Zum Normalen Video +// @name:es Cortos De YouTube A Video Normal +// @name:fr Short YouTube Vers Vidéo Normale +// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक +// @name:id YouTube Shorts Ke Video Normal +// @name:it Da YouTube Shorts A Video Normale +// @name:ja YouTube ショートから通常の動画へ +// @name:ko YouTube Shorts를 일반 비디오로 +// @name:nl YouTube Shorts Naar Normale Video +// @name:pt-BR YouTube Shorts Para Vídeo Normal +// @name:ru YouTube Shorts В Обычное Видео +// @name:th YouTube Shorts สู่วิดีโอปกติ +// @name:tr YouTube Shorts'tan Normal Videoya +// @name:vi YouTube Shorts Thành Video Bình Thường +// @name:zh-CN YouTube Shorts 转普通视频 +// @name:zh-TW YouTube Shorts 轉普通視頻 +// @namespace https://github.com/tientq64/userscripts +// @version 1.1.0 // @description Instantly redirect YouTube Shorts videos to normal video view, allowing you to save, download, choose quality, etc. -// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. -// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 -// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. +// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. +// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। +// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. +// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. // @description:ja YouTube Shorts 動画を通常の動画ビューに即座にリダイレクトし、保存、ダウンロード、品質の選択などを行うことができます。 // @description:ko YouTube Shorts 동영상을 일반 동영상 보기로 즉시 리디렉션하여 저장, 다운로드, 품질 선택 등이 가능합니다. -// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. // @description:pt-BR Redirecione instantaneamente os vídeos do YouTube Shorts para a visualização normal, permitindo que você salve, baixe, escolha a qualidade, etc. // @description:ru Мгновенно перенаправляет видеоролики YouTube Shorts в обычный режим просмотра, позволяя сохранять, загружать, выбирать качество и т. д. -// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. -// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। -// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. -// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. -// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. -// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. -// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. // @description:th เปลี่ยนเส้นทางวิดีโอ YouTube Shorts ไปยังมุมมองวิดีโอปกติทันที ช่วยให้คุณบันทึก ดาวน์โหลด เลือกคุณภาพ ฯลฯ ได้ -// @author tientq64 -// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png -// @match https://www.youtube.com/* -// @license MIT -// @grant none +// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. +// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 +// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png +// @match https://www.youtube.com/* +// @match https://m.youtube.com/* +// @exclude https://studio.youtube.com/* +// @license MIT +// @grant none // @noframes // ==/UserScript== From 36867624005ffd97d45c9061ab7f150d9faaa747 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Sun, 9 Mar 2025 13:01:19 +0700 Subject: [PATCH 14/19] - YouTube Music support is back ([#291321], [#12]). --- scripts/Auto-Skip-YouTube-Ads/README.md | 6 +++++ scripts/Auto-Skip-YouTube-Ads/script.user.js | 26 +++++++++++++++----- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 24 +++++++++++++----- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index 882d30d..a2db888 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -8,6 +8,10 @@ Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát h ## 📑 Changelog +### 7.2.0 - 2025-03-09 + +- YouTube Music support is back ([#291321], [#12]). + ### 7.1.0 - 2025-02-23 - Experiment. @@ -183,10 +187,12 @@ _Merry Christmas!_ Youtube icons created by Ruslan Babkin - Flaticon. +[#12]: https://github.com/tientq64/userscripts/issues/12 [#10]: https://github.com/tientq64/userscripts/issues/10 [#6]: https://github.com/tientq64/userscripts/issues/6 [#2]: https://github.com/tientq64/userscripts/issues/2 [#1]: https://github.com/tientq64/userscripts/pull/1 +[#291321]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/291321 [#279783]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279783 [#279168]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279168 [#267857]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/267857 diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 1aec2ca..1d5d466 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.1.1 +// @version 7.2.0 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -33,6 +33,7 @@ // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @match https://music.youtube.com/* // @exclude https://studio.youtube.com/* // @grant none // @license MIT @@ -46,8 +47,7 @@ // ==/UserScript== function skipAd() { - const isYouTubeShorts = checkIsYouTubeShorts() - if (isYouTubeShorts) return + if (checkIsYouTubeShorts()) return // This element appears when a video ad appears. const adShowing = document.querySelector('.ad-showing') @@ -62,7 +62,7 @@ function skipAd() { let playerEl let player - if (isYouTubeMobile) { + if (isYouTubeMobile || isYouTubeMusic) { playerEl = document.querySelector('#movie_player') player = playerEl } else { @@ -81,7 +81,9 @@ function skipAd() { // ad.classList.remove('ad-showing') if (pieCountdown === null && surveyQuestions === null) { - const adVideo = document.querySelector('#ytd-player video.html5-main-video') + const adVideo = document.querySelector( + '#ytd-player video.html5-main-video, #song-video video.html5-main-video' + ) console.table({ message: 'Ad video', @@ -201,9 +203,21 @@ function removeAdElements() { } const isYouTubeMobile = location.hostname === 'm.youtube.com' +const isYouTubeMusic = location.hostname === 'music.youtube.com' window.setInterval(skipAd, 500) -window.setInterval(removeAdElements, 1000) + +if (!isYouTubeMusic) { + window.setInterval(removeAdElements, 1000) +} + +// const observer = new MutationObserver(skipAd) +// observer.observe(document.body, { +// attributes: true, +// attributeFilter: ['class'], +// childList: true, +// subtree: true +// }) addCss() removeAdElements() diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index 5b1cd54..cbe603e 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.1.1 +// @version 7.2.0 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -33,6 +33,7 @@ // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @match https://music.youtube.com/* // @exclude https://studio.youtube.com/* // @grant none // @license MIT @@ -45,8 +46,7 @@ // ==/UserScript== function skipAd(): void { - const isYouTubeShorts: boolean = checkIsYouTubeShorts() - if (isYouTubeShorts) return + if (checkIsYouTubeShorts()) return // This element appears when a video ad appears. const adShowing = document.querySelector('.ad-showing') @@ -63,7 +63,7 @@ function skipAd(): void { let playerEl: YtdPlayerElement | YouTubeMoviePlayerElement | null let player: YouTubePlayer | YouTubeMoviePlayerElement | null - if (isYouTubeMobile) { + if (isYouTubeMobile || isYouTubeMusic) { playerEl = document.querySelector('#movie_player') player = playerEl } else { @@ -83,7 +83,7 @@ function skipAd(): void { if (pieCountdown === null && surveyQuestions === null) { const adVideo = document.querySelector( - '#ytd-player video.html5-main-video' + '#ytd-player video.html5-main-video, #song-video video.html5-main-video' ) console.table({ @@ -204,9 +204,21 @@ function removeAdElements(): void { } const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com' +const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com' window.setInterval(skipAd, 500) -window.setInterval(removeAdElements, 1000) + +if (!isYouTubeMusic) { + window.setInterval(removeAdElements, 1000) +} + +// const observer = new MutationObserver(skipAd) +// observer.observe(document.body, { +// attributes: true, +// attributeFilter: ['class'], +// childList: true, +// subtree: true +// }) addCss() removeAdElements() From cafdd166a23349b03a7def79ddeff6a8f6196ffa Mon Sep 17 00:00:00 2001 From: tientq64 Date: Mon, 10 Mar 2025 10:24:09 +0700 Subject: [PATCH 15/19] Try a different skip method for ads on YouTube Music --- scripts/Auto-Skip-YouTube-Ads/script.user.js | 71 ++++++++++++-------- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 71 ++++++++++++-------- 2 files changed, 84 insertions(+), 58 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 1d5d466..aa6c431 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.2.0 +// @version 7.2.1 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -80,8 +80,10 @@ function skipAd() { // ad.classList.remove('ad-showing') + let adVideo = null + if (pieCountdown === null && surveyQuestions === null) { - const adVideo = document.querySelector( + adVideo = document.querySelector( '#ytd-player video.html5-main-video, #song-video video.html5-main-video' ) @@ -97,35 +99,44 @@ function skipAd() { if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) return - adVideo.muted = true - adVideo.pause() - console.log({ - message: 'Paused ad video', + message: 'Ad video has finished loading', timeStamp: getCurrentTimeString() }) } - const videoData = player.getVideoData() - const videoId = videoData.video_id - const start = Math.floor(player.getCurrentTime()) + if (isYouTubeMusic && adVideo !== null) { + adVideo.currentTime = adVideo.duration - if ('loadVideoWithPlayerVars' in playerEl) { - playerEl.loadVideoWithPlayerVars({ videoId, start }) + console.table({ + message: 'Ad skipped', + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) } else { - playerEl.loadVideoByPlayerVars({ videoId, start }) - } + const videoData = player.getVideoData() + const videoId = videoData.video_id + const start = Math.floor(player.getCurrentTime()) - console.table({ - message: 'Ad skipped!', - videoId, - start, - title: videoData.title, - timeStamp: getCurrentTimeString(), - adShowing: adShowing !== null, - pieCountdown: pieCountdown !== null, - surveyQuestions: surveyQuestions !== null - }) + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) + } else { + playerEl.loadVideoByPlayerVars({ videoId, start }) + } + + console.table({ + message: 'Ad skipped', + videoId, + start, + title: videoData.title, + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) + } } function checkIsYouTubeShorts() { @@ -204,13 +215,19 @@ function removeAdElements() { const isYouTubeMobile = location.hostname === 'm.youtube.com' const isYouTubeMusic = location.hostname === 'music.youtube.com' +const isYouTubeDesktop = !isYouTubeMobile && !isYouTubeMusic +const isYouTubeVideo = isYouTubeMobile || isYouTubeDesktop -window.setInterval(skipAd, 500) +addCss() -if (!isYouTubeMusic) { +if (isYouTubeVideo) { window.setInterval(removeAdElements, 1000) + removeAdElements() } +window.setInterval(skipAd, 500) +skipAd() + // const observer = new MutationObserver(skipAd) // observer.observe(document.body, { // attributes: true, @@ -218,7 +235,3 @@ if (!isYouTubeMusic) { // childList: true, // subtree: true // }) - -addCss() -removeAdElements() -skipAd() diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index cbe603e..3d1dcf9 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.2.0 +// @version 7.2.1 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -81,8 +81,10 @@ function skipAd(): void { // ad.classList.remove('ad-showing') + let adVideo: HTMLVideoElement | null = null + if (pieCountdown === null && surveyQuestions === null) { - const adVideo = document.querySelector( + adVideo = document.querySelector( '#ytd-player video.html5-main-video, #song-video video.html5-main-video' ) @@ -98,35 +100,44 @@ function skipAd(): void { if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) return - adVideo.muted = true - adVideo.pause() - console.log({ - message: 'Paused ad video', + message: 'Ad video has finished loading', timeStamp: getCurrentTimeString() }) } - const videoData: YouTubeVideoData = player.getVideoData() - const videoId: string = videoData.video_id - const start: number = Math.floor(player.getCurrentTime()) + if (isYouTubeMusic && adVideo !== null) { + adVideo.currentTime = adVideo.duration - if ('loadVideoWithPlayerVars' in playerEl) { - playerEl.loadVideoWithPlayerVars({ videoId, start }) + console.table({ + message: 'Ad skipped', + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) } else { - playerEl.loadVideoByPlayerVars({ videoId, start }) - } + const videoData: YouTubeVideoData = player.getVideoData() + const videoId: string = videoData.video_id + const start: number = Math.floor(player.getCurrentTime()) - console.table({ - message: 'Ad skipped!', - videoId, - start, - title: videoData.title, - timeStamp: getCurrentTimeString(), - adShowing: adShowing !== null, - pieCountdown: pieCountdown !== null, - surveyQuestions: surveyQuestions !== null - }) + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) + } else { + playerEl.loadVideoByPlayerVars({ videoId, start }) + } + + console.table({ + message: 'Ad skipped', + videoId, + start, + title: videoData.title, + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) + } } function checkIsYouTubeShorts(): boolean { @@ -205,13 +216,19 @@ function removeAdElements(): void { const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com' const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com' +const isYouTubeDesktop: boolean = !isYouTubeMobile && !isYouTubeMusic +const isYouTubeVideo: boolean = isYouTubeMobile || isYouTubeDesktop -window.setInterval(skipAd, 500) +addCss() -if (!isYouTubeMusic) { +if (isYouTubeVideo) { window.setInterval(removeAdElements, 1000) + removeAdElements() } +window.setInterval(skipAd, 500) +skipAd() + // const observer = new MutationObserver(skipAd) // observer.observe(document.body, { // attributes: true, @@ -219,7 +236,3 @@ if (!isYouTubeMusic) { // childList: true, // subtree: true // }) - -addCss() -removeAdElements() -skipAd() From 743ac294357f7ef24d6cfe5a1ae9cca8a7a85623 Mon Sep 17 00:00:00 2001 From: tientq64 Date: Mon, 10 Mar 2025 10:26:19 +0700 Subject: [PATCH 16/19] New userscript: Web-Page-Minimap --- scripts/Web-Page-Minimap/README.md | 5 + scripts/Web-Page-Minimap/script.user.js | 176 +++++++++++++++++++++++ scripts/Web-Page-Minimap/script.user.ts | 180 ++++++++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 scripts/Web-Page-Minimap/README.md create mode 100644 scripts/Web-Page-Minimap/script.user.js create mode 100644 scripts/Web-Page-Minimap/script.user.ts diff --git a/scripts/Web-Page-Minimap/README.md b/scripts/Web-Page-Minimap/README.md new file mode 100644 index 0000000..15871aa --- /dev/null +++ b/scripts/Web-Page-Minimap/README.md @@ -0,0 +1,5 @@ +# Web Page Minimap + +## Credits + +Content icons created by DinosoftLabs - Flaticon diff --git a/scripts/Web-Page-Minimap/script.user.js b/scripts/Web-Page-Minimap/script.user.js new file mode 100644 index 0000000..58a0983 --- /dev/null +++ b/scripts/Web-Page-Minimap/script.user.js @@ -0,0 +1,176 @@ +// ==UserScript== +// @name Web Page Minimap +// @namespace https://github.com/tientq64/userscripts +// @version 0.1.0 +// @description A minimap for the current web page. +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/718/718994.png +// @match *://*/* +// @run-at document-idle +// @grant GM_addStyle +// @grant GM_getValue +// @grant GM_setValue +// @license MIT +// @noframes +// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Web-Page-Minimap +// ==/UserScript== + +const namesp = '__Web-Page-Minimap__userscript__' + +const scroller = document.createElement('div') +scroller.className = namesp +document.documentElement.appendChild(scroller) + +const note = document.createElement('div') +note.className = `${namesp}-note` +note.textContent = 'Ctrl+M to show/hide' +note.addEventListener('click', () => setIsShow(false)) +scroller.appendChild(note) + +const canvas = document.createElement('canvas') +canvas.className = `${namesp}-canvas` +scroller.appendChild(canvas) + +const pageWidth = 180 +const factor = pageWidth / (innerWidth - pageWidth) + +let isShow = GM_getValue('isShow', true) + +function setIsShow(value) { + isShow = value + GM_setValue('isShow', isShow) + if (isShow) { + scroller.removeAttribute('hidden') + render() + } else { + scroller.setAttribute('hidden', '') + } +} + +function render(loop) { + if (loop) { + setTimeout(render, loop, loop) + // requestAnimationFrame(render) + } + if (!isShow) return + + const pageHeight = Math.round(document.documentElement.scrollHeight * factor) + canvas.width = pageWidth + canvas.height = pageHeight + + const ctx = canvas.getContext('2d') + if (ctx === null) return + + ctx.imageSmoothingEnabled = false + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, pageWidth, pageHeight) + ctx.scale(factor, factor) + + const htmlRect = document.documentElement.getBoundingClientRect() + ctx.translate(-htmlRect.x, -htmlRect.y) + + const draw = ({ el, style }) => { + if (el.classList.contains(namesp)) return + + const isVisible = el.checkVisibility({ + opacityProperty: true, + visibilityProperty: true + }) + if (!isVisible) return + + const name = el.nodeName + + ctx.fillStyle = style.backgroundColor + + const rects = el.getClientRects() + for (const rect of rects) { + const x = Math.round(rect.x) + const y = Math.round(rect.y) + const width = Math.round(rect.width) + const height = Math.round(rect.height) + + ctx.fillRect(x, y, width, height) + + switch (name) { + case 'IMG': + case 'SVG': + case 'CANVAS': + case 'VIDEO': + { + const img = el + try { + ctx.drawImage(img, x, y, width, height) + } catch { + // + } + } + break + } + } + + const subEls = Array.from(el.children, (subEl) => ({ + el: subEl, + style: getComputedStyle(subEl) + })) + + subEls.sort((subElA, subElB) => { + const zA = subElA.style.zIndex === 'auto' ? 0 : Number(subElA.style.zIndex) + const zB = subElB.style.zIndex === 'auto' ? 0 : Number(subElB.style.zIndex) + return zA - zB + }) + + for (const subEl of subEls) { + draw(subEl) + } + } + + draw({ + el: document.documentElement, + style: getComputedStyle(document.documentElement) + }) +} + +window.addEventListener('keydown', (event) => { + if (event.repeat) return + if (document.activeElement?.matches('input,textarea,select')) return + + const { ctrlKey: ctrl, shiftKey: shift, altKey: alt, code } = event + + if (!ctrl && !shift && alt && code === 'KeyM') { + setIsShow(!isShow) + } +}) + +GM_addStyle(` + .${namesp} { + position: fixed; + top: 50%; + right: 0; + max-height: calc(100% - 16px); + padding: 8px; + overflow: auto; + transform: translate(0, -50%); + scrollbar-width: thin; + z-index: 99999; + } + .${namesp}-note { + font: 13px sans-serif; + line-height: 1.3; + text-align: center; + cursor: pointer; + } + .${namesp}-canvas { + border-radius: 8px; + box-shadow: 0 4px 8px #3338; + image-rendering: pixelated; + } +`) + +setIsShow(isShow) + +setTimeout(render, 1000) +setTimeout(render, 2000) +setTimeout(render, 3000) +setTimeout(render, 4000) + +render(5000) diff --git a/scripts/Web-Page-Minimap/script.user.ts b/scripts/Web-Page-Minimap/script.user.ts new file mode 100644 index 0000000..ad8a268 --- /dev/null +++ b/scripts/Web-Page-Minimap/script.user.ts @@ -0,0 +1,180 @@ +// ==UserScript== +// @name Web Page Minimap +// @namespace https://github.com/tientq64/userscripts +// @version 0.1.0 +// @description A minimap for the current web page. +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/718/718994.png +// @match *://*/* +// @run-at document-idle +// @grant GM_addStyle +// @grant GM_getValue +// @grant GM_setValue +// @license MIT +// @noframes +// ==/UserScript== + +interface El { + el: Element + style: CSSStyleDeclaration +} + +const namesp: string = '__Web-Page-Minimap__userscript__' + +const scroller: HTMLElement = document.createElement('div') +scroller.className = namesp +document.documentElement.appendChild(scroller) + +const note: HTMLDivElement = document.createElement('div') +note.className = `${namesp}-note` +note.textContent = 'Ctrl+M to show/hide' +note.addEventListener('click', () => setIsShow(false)) +scroller.appendChild(note) + +const canvas: HTMLCanvasElement = document.createElement('canvas') +canvas.className = `${namesp}-canvas` +scroller.appendChild(canvas) + +const pageWidth: number = 180 +const factor: number = pageWidth / (innerWidth - pageWidth) + +let isShow: boolean = GM_getValue('isShow', true) + +function setIsShow(value: boolean): void { + isShow = value + GM_setValue('isShow', isShow) + if (isShow) { + scroller.removeAttribute('hidden') + render() + } else { + scroller.setAttribute('hidden', '') + } +} + +function render(loop?: number): void { + if (loop) { + setTimeout(render, loop, loop) + // requestAnimationFrame(render) + } + if (!isShow) return + + const pageHeight: number = Math.round(document.documentElement.scrollHeight * factor) + canvas.width = pageWidth + canvas.height = pageHeight + + const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d') + if (ctx === null) return + + ctx.imageSmoothingEnabled = false + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, pageWidth, pageHeight) + ctx.scale(factor, factor) + + const htmlRect: DOMRect = document.documentElement.getBoundingClientRect() + ctx.translate(-htmlRect.x, -htmlRect.y) + + const draw = ({ el, style }: El): void => { + if (el.classList.contains(namesp)) return + + const isVisible: boolean = el.checkVisibility({ + opacityProperty: true, + visibilityProperty: true + }) + if (!isVisible) return + + const name: string = el.nodeName + + ctx.fillStyle = style.backgroundColor + + const rects: DOMRectList = el.getClientRects() + for (const rect of rects) { + const x: number = Math.round(rect.x) + const y: number = Math.round(rect.y) + const width: number = Math.round(rect.width) + const height: number = Math.round(rect.height) + + ctx.fillRect(x, y, width, height) + + switch (name) { + case 'IMG': + case 'SVG': + case 'CANVAS': + case 'VIDEO': + { + const img = el as CanvasImageSource + try { + ctx.drawImage(img, x, y, width, height) + } catch { + // + } + } + break + } + } + + const subEls = Array.from(el.children, (subEl) => ({ + el: subEl, + style: getComputedStyle(subEl) + })) + + subEls.sort((subElA, subElB) => { + const zA: number = subElA.style.zIndex === 'auto' ? 0 : Number(subElA.style.zIndex) + const zB: number = subElB.style.zIndex === 'auto' ? 0 : Number(subElB.style.zIndex) + return zA - zB + }) + + for (const subEl of subEls) { + draw(subEl) + } + } + + draw({ + el: document.documentElement, + style: getComputedStyle(document.documentElement) + }) +} + +window.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.repeat) return + if (document.activeElement?.matches('input,textarea,select')) return + + const { ctrlKey: ctrl, shiftKey: shift, altKey: alt, code } = event + + if (!ctrl && !shift && alt && code === 'KeyM') { + setIsShow(!isShow) + } +}) + +GM_addStyle(` + .${namesp} { + position: fixed; + top: 50%; + right: 0; + max-height: calc(100% - 16px); + padding: 8px; + overflow: auto; + transform: translate(0, -50%); + scrollbar-width: thin; + z-index: 99999; + } + .${namesp}-note { + font: 13px sans-serif; + line-height: 1.3; + text-align: center; + cursor: pointer; + } + .${namesp}-canvas { + border-radius: 8px; + box-shadow: 0 4px 8px #3338; + image-rendering: pixelated; + } +`) + +setIsShow(isShow) + +setTimeout(render, 1000) +setTimeout(render, 2000) +setTimeout(render, 3000) +setTimeout(render, 4000) + +render(5000) From c582f64ac023e983e71dd6febeae76c5eb8db12e Mon Sep 17 00:00:00 2001 From: Tran Quang Tien Date: Sun, 30 Mar 2025 19:48:03 +0700 Subject: [PATCH 17/19] Remove duplicate interface `YouTubeMoviePlayerElement` --- .resources/global.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.resources/global.d.ts b/.resources/global.d.ts index 311cf31..728102d 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -71,7 +71,3 @@ interface YouTubeVideoData { video_id: string isLive: boolean } - -interface YouTubeMoviePlayerElement extends HTMLElement { - loadVideoByPlayerVars(options: { videoId: string; start?: number }): void -} From 3dc9fd38da4bb39caeba5be09b59cc0c6d658d0c Mon Sep 17 00:00:00 2001 From: tientq64 Date: Fri, 4 Apr 2025 09:27:35 +0700 Subject: [PATCH 18/19] Fix variables --- scripts/Auto-Skip-YouTube-Ads/script.user.js | 7 ++++--- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index aa6c431..15713aa 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.2.1 +// @version 7.2.2 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -214,9 +214,10 @@ function removeAdElements() { } const isYouTubeMobile = location.hostname === 'm.youtube.com' +const isYouTubeDesktop = !isYouTubeMobile + const isYouTubeMusic = location.hostname === 'music.youtube.com' -const isYouTubeDesktop = !isYouTubeMobile && !isYouTubeMusic -const isYouTubeVideo = isYouTubeMobile || isYouTubeDesktop +const isYouTubeVideo = !isYouTubeMusic addCss() diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index 3d1dcf9..f5e9054 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.2.1 +// @version 7.2.2 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -215,9 +215,10 @@ function removeAdElements(): void { } const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com' +const isYouTubeDesktop: boolean = !isYouTubeMobile + const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com' -const isYouTubeDesktop: boolean = !isYouTubeMobile && !isYouTubeMusic -const isYouTubeVideo: boolean = isYouTubeMobile || isYouTubeDesktop +const isYouTubeVideo: boolean = !isYouTubeMusic addCss() From f5c085ad02b7947c5c3a251d7ba2254aa029064d Mon Sep 17 00:00:00 2001 From: tientq64 Date: Fri, 20 Jun 2025 01:27:08 +0700 Subject: [PATCH 19/19] - Mute video player while skipping ads ([#15]). - Automatically re-enable subtitles after skipping ads. --- .resources/global.d.ts | 4 + .resources/tailwind.min.css | 2 +- scripts/Auto-Skip-YouTube-Ads/README.md | 6 + scripts/Auto-Skip-YouTube-Ads/script.user.js | 17 +- scripts/Auto-Skip-YouTube-Ads/script.user.ts | 17 +- scripts/Diep-io-Improvements/script.user.js | 295 ++-------------- scripts/Diep-io-Improvements/script.user.tsx | 278 ++------------- scripts/Diep-io-Improvements/types.d.ts | 40 +-- scripts/Mien-AI-Music/script.user.js | 340 ++++++++++-------- scripts/Mien-AI-Music/script.user.ts | 343 +++++++++++-------- 10 files changed, 493 insertions(+), 849 deletions(-) diff --git a/.resources/global.d.ts b/.resources/global.d.ts index 728102d..d212b7a 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -39,6 +39,7 @@ type GMOpenInTabOptions = { declare function GM_openInTab(url: string, options?: GMOpenInTabOptions): void declare type ReactElement = import('react').ReactElement +declare type ReactNode = import('react').ReactNode declare type ChangeEvent = import('react').ChangeEvent declare type Immer = import('immer').Immer @@ -57,6 +58,9 @@ interface YtdPlayerElement extends HTMLElement { interface YouTubeMoviePlayerElement extends HTMLElement, YouTubePlayer { loadVideoByPlayerVars(options: { videoId: string; start?: number }): void + toggleSubtitles(): void + toggleSubtitlesOn(): void + isSubtitlesOn(): boolean } interface YouTubePlayer { diff --git a/.resources/tailwind.min.css b/.resources/tailwind.min.css index 33ce450..3596759 100644 --- a/.resources/tailwind.min.css +++ b/.resources/tailwind.min.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-7{bottom:1.75rem}.left-0{left:0}.left-72{left:18rem}.z-\[999\]{z-index:999}.inline-block{display:inline-block}.flex{display:flex}.h-4{height:1rem}.h-full{height:100%}.w-60{width:15rem}.min-w-0{min-width:0}.flex-1{flex:1 1 0%}.cursor-text{cursor:text}.flex-col{flex-direction:column}.items-center{align-items:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.bg-gray-800\/90{background-color:#1f2937e6}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-sm{font-size:.875rem;line-height:1.25rem}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-rose-400{--tw-text-opacity:1;color:rgb(251 113 133/var(--tw-text-opacity))}.text-sky-400{--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity))}.text-transparent{color:#0000}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.caret-white{caret-color:#fff}.outline{outline-style:solid}.selection\:bg-blue-700 ::-moz-selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:bg-blue-700 ::selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:text-transparent ::-moz-selection{color:#0000}.selection\:text-transparent ::selection{color:#0000}.selection\:bg-blue-700::-moz-selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:bg-blue-700::selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:text-transparent::-moz-selection{color:#0000}.selection\:text-transparent::selection{color:#0000}.hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.pointer-events-none{pointer-events:none}.absolute{position:absolute}.inset-0{inset:0}.z-\[999\]{z-index:999} \ No newline at end of file diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index a2db888..6bfe5bf 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -8,6 +8,11 @@ Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát h ## 📑 Changelog +### 7.3.0 - 2025-06-20 + +- Mute video player while skipping ads ([#15]). +- Automatically re-enable subtitles after skipping ads. + ### 7.2.0 - 2025-03-09 - YouTube Music support is back ([#291321], [#12]). @@ -187,6 +192,7 @@ _Merry Christmas!_ Youtube icons created by Ruslan Babkin - Flaticon. +[#15]: https://github.com/tientq64/userscripts/issues/15 [#12]: https://github.com/tientq64/userscripts/issues/12 [#10]: https://github.com/tientq64/userscripts/issues/10 [#6]: https://github.com/tientq64/userscripts/issues/6 diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 15713aa..49c85d0 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.2.2 +// @version 7.3.0 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -60,10 +60,12 @@ function skipAd() { if (adShowing === null && pieCountdown === null && surveyQuestions === null) return + const moviePlayerEl = document.querySelector('#movie_player') let playerEl let player + if (isYouTubeMobile || isYouTubeMusic) { - playerEl = document.querySelector('#movie_player') + playerEl = moviePlayerEl player = playerEl } else { playerEl = document.querySelector('#ytd-player') @@ -97,7 +99,12 @@ function skipAd() { timeStamp: getCurrentTimeString() }) - if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) return + if (adVideo !== null) { + adVideo.muted = true + } + if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) { + return + } console.log({ message: 'Ad video has finished loading', @@ -120,6 +127,10 @@ function skipAd() { const videoId = videoData.video_id const start = Math.floor(player.getCurrentTime()) + if (moviePlayerEl !== null && moviePlayerEl.isSubtitlesOn()) { + window.setTimeout(moviePlayerEl.toggleSubtitlesOn, 1000) + } + if ('loadVideoWithPlayerVars' in playerEl) { playerEl.loadVideoWithPlayerVars({ videoId, start }) } else { diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index f5e9054..a88d35c 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 7.2.2 +// @version 7.3.0 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -61,10 +61,12 @@ function skipAd(): void { if (adShowing === null && pieCountdown === null && surveyQuestions === null) return + const moviePlayerEl = document.querySelector('#movie_player') let playerEl: YtdPlayerElement | YouTubeMoviePlayerElement | null let player: YouTubePlayer | YouTubeMoviePlayerElement | null + if (isYouTubeMobile || isYouTubeMusic) { - playerEl = document.querySelector('#movie_player') + playerEl = moviePlayerEl player = playerEl } else { playerEl = document.querySelector('#ytd-player') @@ -98,7 +100,12 @@ function skipAd(): void { timeStamp: getCurrentTimeString() }) - if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) return + if (adVideo !== null) { + adVideo.muted = true + } + if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) { + return + } console.log({ message: 'Ad video has finished loading', @@ -121,6 +128,10 @@ function skipAd(): void { const videoId: string = videoData.video_id const start: number = Math.floor(player.getCurrentTime()) + if (moviePlayerEl !== null && moviePlayerEl.isSubtitlesOn()) { + window.setTimeout(moviePlayerEl.toggleSubtitlesOn, 1000) + } + if ('loadVideoWithPlayerVars' in playerEl) { playerEl.loadVideoWithPlayerVars({ videoId, start }) } else { diff --git a/scripts/Diep-io-Improvements/script.user.js b/scripts/Diep-io-Improvements/script.user.js index 4b1d888..cdd8cc3 100644 --- a/scripts/Diep-io-Improvements/script.user.js +++ b/scripts/Diep-io-Improvements/script.user.js @@ -8,7 +8,6 @@ // @match https://diep.io/* // @require https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js // @require https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js -// @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js // @resource TAILWINDCSS https://raw.githubusercontent.com/tientq64/userscripts/main/.resources/tailwind.min.css // @grant GM_getResourceText // @grant GM_addStyle @@ -22,280 +21,50 @@ var diepIO ;(function (diepIO) { - const preventDefault = Event.prototype.preventDefault - Event.prototype.preventDefault = () => {} + const seconds = 1000 + const minutes = 60 * seconds const css = ` ${GM_getResourceText('TAILWINDCSS')} - :focus { - outline: none; - } - #canvas { - cursor: crosshair !important; + body { + color: unset; } ` GM_addStyle(css) - const { useState, useEffect, useRef } = React - const { countBy, some } = _ + let grantRewardTimeoutId = undefined function App() { - const [stats] = useState([ - { - key: 1, - color: '#eeb690' - }, - { - key: 2, - color: '#ec6cf0' - }, - { - key: 3, - color: '#9466ea' - }, - { - key: 4, - color: '#6c96f0' - }, - { - key: 5, - color: '#f0d96c' - }, - { - key: 6, - color: '#f06c6c' - }, - { - key: 7, - color: '#98f06c' - }, - { - key: 8, - color: '#6cf0ec' - } - ]) - - const [presets] = useState([ - { - text: '010-7757-6', - upgrade: '456782456784567847586884756457475' - }, - { - text: '000-7777-5', - upgrade: '456788456784567847586664756457475' - }, - { - text: '000-7757-7', - upgrade: '456788456784567847586884756457475' - }, - { - text: '020-5757-7', - upgrade: '456788825678856784758264756457475' - } - ]) - - const [upgrade, setUpgrade] = useState('') - const [isUpgradeShown, setIsUpgradeShown] = useState(false) - const [statsStr, setStatsStr] = useState('') - const upgradeInputRef = useRef(null) - - const getStatBarSlotBackgroundColor = (stat, slotIndex) => { - return slotIndex < countBy(upgrade)[stat.key] - ? stat.color - : slotIndex < 7 - ? '#111827' - : '#030712' - } - - const handleUpgradeChange = (event) => { - const { value } = event.target - if (/[^1-8]/.test(value) || some(countBy(value), (v) => v > 10)) { - const selectionStart = event.target.selectionStart - 1 - setTimeout(() => { - event.target.selectionStart = selectionStart - event.target.selectionEnd = selectionStart - }) - return - } - setUpgrade(value) - } - - const handleUpgradeKeyDown = (event) => { - if (/^((Digit|Numpad)[1-8]|Arrow(Up|Down|Left|Right))$/.test(event.code)) { - event.stopPropagation() - } - if (event.code === 'Enter' || event.code === 'NumpadEnter') { - event.stopPropagation() - input.execute(`game_stats_build ${upgrade}`) - setIsUpgradeShown(false) - } - } - - const handlePresetClick = (preset) => { - setUpgrade(preset.upgrade) - upgradeInputRef.current.focus() - } - - const handleGlobalKeyDown = (event) => { - if (event.repeat || event.ctrlKey || event.shiftKey || event.metaKey) return - if (document.activeElement.localName === 'd-base') return - - switch (event.code) { - case 'KeyT': - setIsUpgradeShown((prev) => !prev) - break - - case 'KeyG': - input.grantReward() - break - } - } - - const handleGlobalContextMenu = (event) => { - if (['input', 'textarea'].includes(event.target.localName)) return - preventDefault.call(event) - } + return React.createElement('div', null) + } - const updateUI = () => { - const newStatStr = ui.__playerAttributes.attributes - .map((attr) => attr.slotsFilled) - .join('/') - if (newStatStr !== statsStr) { - setStatsStr(newStatStr) + function removeEls(delay) { + window.setTimeout(() => { + const els = document.querySelectorAll( + '#ad-holders, #aa-video-holder, #diep-io_300x250, iframe' + ) + for (const el of els) { + el.remove() } - } - - useEffect(() => { - window.addEventListener('keydown', handleGlobalKeyDown) - window.addEventListener('contextmenu', handleGlobalContextMenu) - setInterval(updateUI, 1000) - }, []) - - return React.createElement( - 'div', - { className: 'h-full' }, - isUpgradeShown && - React.createElement( - 'div', - { - className: - 'flex absolute bottom-7 left-72 p-4 gap-4 rounded-lg bg-gray-800/90 pointer-events-auto' - }, - React.createElement( - 'div', - { className: 'flex flex-col gap-3' }, - React.createElement( - 'div', - { className: 'flex-1 w-60' }, - stats.map((stat) => - React.createElement( - 'div', - { key: stat.key, className: 'flex items-center gap-2' }, - React.createElement( - 'div', - { className: 'font-mono text-sm' }, - stat.key - ), - React.createElement( - 'div', - { - className: - 'flex-1 flex gap-1 rounded-lg overflow-hidden' - }, - Array(10) - .fill(0) - .map((_, slotIndex) => - React.createElement('div', { - key: slotIndex, - className: 'flex-1 h-4', - style: { - backgroundColor: - getStatBarSlotBackgroundColor( - stat, - slotIndex - ) - } - }) - ) - ) - ) - ) - ), - React.createElement( - 'div', - { className: 'relative flex font-mono' }, - React.createElement('input', { - ref: upgradeInputRef, - className: - 'min-w-0 px-2 py-1 rounded-lg bg-gray-900 text-transparent caret-white selection:bg-blue-700 selection:text-transparent cursor-text', - autoFocus: true, - maxLength: 33, - size: 31, - value: upgrade, - onChange: handleUpgradeChange, - onKeyDown: handleUpgradeKeyDown - }), - React.createElement( - 'div', - { className: 'absolute inset-0 px-2 py-1 pointer-events-none' }, - Array(33) - .fill(undefined) - .map((_, i) => - React.createElement( - 'div', - { - key: i, - className: `inline-block ${i <= 13 ? 'text-gray-400' : i <= 26 ? 'text-sky-400' : i <= 27 ? 'text-yellow-400' : 'text-rose-400'}` - }, - upgrade[i] ?? '\xb7' - ) - ) - ) - ) - ), - React.createElement( - 'div', - { className: 'flex flex-col gap-1' }, - React.createElement('div', { className: 'px-2' }, 'Presets'), - React.createElement( - 'div', - { className: 'flex-1 flex flex-col overflow-x-hidden' }, - presets.map((preset) => - React.createElement( - 'button', - { - key: preset.upgrade, - className: 'px-2 rounded font-sans hover:bg-gray-600', - onClick: () => handlePresetClick(preset) - }, - preset.text - ) - ) - ) - ) - ), - ui && - React.createElement( - 'div', - { className: 'absolute bottom-7 left-0 flex flex-col' }, - ui.__playerAttributes.attributes - .slice() - .reverse() - .map((attr, i) => { - const stat = stats[i] - return React.createElement( - 'div', - { - key: stat.key, - className: 'px-1 text-black', - style: { background: `${stat.color}cc` } - }, - attr.slotsFilled || '\xa0' - ) - }) - ) - ) + }, delay) } + removeEls() + removeEls(5000) + removeEls(10000) + removeEls(15000) + + const gameOverContinueEl = document.querySelector('#game-over-continue') + gameOverContinueEl?.addEventListener('click', () => { + if (grantRewardTimeoutId !== undefined) return + + input.grantReward() + grantRewardTimeoutId = window.setTimeout( + () => { + grantRewardTimeoutId = undefined + }, + 2 * minutes + 10 * seconds + ) + }) const rootEl = document.createElement('div') rootEl.className = 'absolute inset-0 pointer-events-none z-[999]' diff --git a/scripts/Diep-io-Improvements/script.user.tsx b/scripts/Diep-io-Improvements/script.user.tsx index 6ae2ef2..a44569b 100644 --- a/scripts/Diep-io-Improvements/script.user.tsx +++ b/scripts/Diep-io-Improvements/script.user.tsx @@ -8,7 +8,6 @@ // @match https://diep.io/* // @require https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js // @require https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js -// @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js // @resource TAILWINDCSS // @grant GM_getResourceText // @grant GM_addStyle @@ -20,261 +19,50 @@ // ==/UserScript== namespace diepIO { - type Stat = { - key: number - color: string - } - - type Preset = { - text: string - upgrade: string - } - - const preventDefault = Event.prototype.preventDefault - Event.prototype.preventDefault = () => {} + const seconds: number = 1000 + const minutes: number = 60 * seconds const css: string = ` ${GM_getResourceText('TAILWINDCSS')} - :focus { - outline: none; - } - #canvas { - cursor: crosshair !important; + body { + color: unset; } ` GM_addStyle(css) - const { useState, useEffect, useRef } = React - const { countBy, some } = _ - - function App() { - const [stats] = useState([ - { - key: 1, - color: '#eeb690' - }, - { - key: 2, - color: '#ec6cf0' - }, - { - key: 3, - color: '#9466ea' - }, - { - key: 4, - color: '#6c96f0' - }, - { - key: 5, - color: '#f0d96c' - }, - { - key: 6, - color: '#f06c6c' - }, - { - key: 7, - color: '#98f06c' - }, - { - key: 8, - color: '#6cf0ec' - } - ]) - - const [presets] = useState([ - { - text: '010-7757-6', - upgrade: '456782456784567847586884756457475' - }, - { - text: '000-7777-5', - upgrade: '456788456784567847586664756457475' - }, - { - text: '000-7757-7', - upgrade: '456788456784567847586884756457475' - }, - { - text: '020-5757-7', - upgrade: '456788825678856784758264756457475' - } - ]) - - const [upgrade, setUpgrade] = useState('') - const [isUpgradeShown, setIsUpgradeShown] = useState(false) - const [statsStr, setStatsStr] = useState('') - const upgradeInputRef = useRef(null) - - const getStatBarSlotBackgroundColor = (stat: Stat, slotIndex: number): string => { - return slotIndex < countBy(upgrade)[stat.key] - ? stat.color - : slotIndex < 7 - ? '#111827' - : '#030712' - } - - const handleUpgradeChange = (event: ChangeEvent): void => { - const { value } = event.target - if (/[^1-8]/.test(value) || some(countBy(value), (v) => v > 10)) { - const selectionStart = event.target.selectionStart - 1 - setTimeout(() => { - event.target.selectionStart = selectionStart - event.target.selectionEnd = selectionStart - }) - return - } - setUpgrade(value) - } - - const handleUpgradeKeyDown = (event): void => { - if (/^((Digit|Numpad)[1-8]|Arrow(Up|Down|Left|Right))$/.test(event.code)) { - event.stopPropagation() - } - if (event.code === 'Enter' || event.code === 'NumpadEnter') { - event.stopPropagation() - input.execute(`game_stats_build ${upgrade}`) - setIsUpgradeShown(false) - } - } - - const handlePresetClick = (preset: Preset): void => { - setUpgrade(preset.upgrade) - upgradeInputRef.current.focus() - } - - const handleGlobalKeyDown = (event): void => { - if (event.repeat || event.ctrlKey || event.shiftKey || event.metaKey) return - if (document.activeElement.localName === 'd-base') return - - switch (event.code) { - case 'KeyT': - setIsUpgradeShown((prev) => !prev) - break + let grantRewardTimeoutId: number | undefined = undefined - case 'KeyG': - input.grantReward() - break - } - } - - const handleGlobalContextMenu = (event): void => { - if (['input', 'textarea'].includes(event.target.localName)) return - preventDefault.call(event) - } + function App(): ReactNode { + return
+ } - const updateUI = (): void => { - const newStatStr: string = ui.__playerAttributes.attributes - .map((attr) => attr.slotsFilled) - .join('/') - if (newStatStr !== statsStr) { - setStatsStr(newStatStr) + function removeEls(delay?: number): void { + window.setTimeout(() => { + const els = document.querySelectorAll( + '#ad-holders, #aa-video-holder, #diep-io_300x250, iframe' + ) + for (const el of els) { + el.remove() } - } - - useEffect(() => { - window.addEventListener('keydown', handleGlobalKeyDown) - window.addEventListener('contextmenu', handleGlobalContextMenu) - setInterval(updateUI, 1000) - }, []) - - return ( -
- {isUpgradeShown && ( -
- {/* Cột trái */} -
- {/* Các thanh chỉ số */} -
- {stats.map((stat) => ( -
-
{stat.key}
-
- {Array(10) - .fill(0) - .map((_, slotIndex) => ( -
- ))} -
-
- ))} -
- {/* Ô nhập */} -
- -
- {Array(33) - .fill(undefined) - .map((_, i) => ( -
- {upgrade[i] ?? '\xb7'} -
- ))} -
-
-
- {/* Cột phải */} -
-
Presets
-
- {presets.map((preset) => ( - - ))} -
-
-
- )} - {ui && ( -
- {ui.__playerAttributes.attributes - .slice() - .reverse() - .map((attr, i) => { - const stat: Stat = stats[i] - return ( -
- {attr.slotsFilled || '\xa0'} -
- ) - })} -
- )} -
- ) + }, delay) } + removeEls() + removeEls(5000) + removeEls(10000) + removeEls(15000) + + const gameOverContinueEl = document.querySelector('#game-over-continue') + gameOverContinueEl?.addEventListener('click', () => { + if (grantRewardTimeoutId !== undefined) return + + input.grantReward() + grantRewardTimeoutId = window.setTimeout( + () => { + grantRewardTimeoutId = undefined + }, + 2 * minutes + 10 * seconds + ) + }) const rootEl = document.createElement('div') rootEl.className = 'absolute inset-0 pointer-events-none z-[999]' diff --git a/scripts/Diep-io-Improvements/types.d.ts b/scripts/Diep-io-Improvements/types.d.ts index a36b7e5..ec88683 100644 --- a/scripts/Diep-io-Improvements/types.d.ts +++ b/scripts/Diep-io-Improvements/types.d.ts @@ -1,43 +1,7 @@ namespace diepIO { - declare const input: { - execute(command: string): void + interface Input { grantReward(): void - mouse(x: number, y: number): void - key_down(keyCode: number): void - key_up(keyCode: number): void } - declare const ui: { - screen: 'home' | 'game' | 'stats' - game: UIGame | null - __playerAttributes: UIPlayerAttributes - } - - type UIGame = { - selectedGameMode: UIGameMode - } - - type UIPlayerAttributes = { - pointsLeft: number - attributes: UIAttribute[] - } - - type UIAttribute = { - index: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 - name: UIAttributeName - slotsFilled: number - slotsWanted: number - totalSlots: number - } - - type UIAttributeName = - | 'Movement Speed' - | 'Bullet Damage' - | 'Bullet Penetration' - | 'Bullet Speed' - | 'Body Damage' - | 'Max Health' - | 'Health Regen' - - type UIGameMode = 'ffa' | 'teams' | '4teams' | 'event' | 'maze' | 'sandbox' + declare const input: Input } diff --git a/scripts/Mien-AI-Music/script.user.js b/scripts/Mien-AI-Music/script.user.js index ac390c7..bed56f8 100644 --- a/scripts/Mien-AI-Music/script.user.js +++ b/scripts/Mien-AI-Music/script.user.js @@ -14,18 +14,22 @@ // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle +// @grant window.onurlchange // @license MIT // @noframes // @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Mien-AI-Music // ==/UserScript== -;(async function () { - const simpleUrl = (location.hostname + location.pathname) - .replace(/^www\./, '') - .replace(/\/$/, '') +var MienAIMusic +;(function (MienAIMusic) { + const cleanupCbs = new Set() + + function cleanup(cb) { + cleanupCbs.add(cb) + } let accountEmails = [] - let accountEmailsInput = await GM_getValue('accountEmails', '') + let accountEmailsInput = GM_getValue('accountEmails', '') async function wait(ms) { return new Promise((resolve) => { @@ -59,52 +63,7 @@ el?.click() } - async function switchAccount(accountEmail) { - await GM_setValue('accountEmail', accountEmail) - clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') - await wait(600) - clickIfContentIncludes('button:has(> span)', 'Sign In') - await wait(200) - click('.cl-socialButtonsIconButton__google') - } - - async function inputEmails() { - while (accountEmailsInput === '') { - accountEmailsInput = - prompt( - 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' - ) ?? '' - accountEmailsInput = accountEmailsInput.trim() - if (accountEmailsInput === '') continue - GM_setValue('accountEmails', accountEmailsInput) - } - accountEmails = accountEmailsInput.split(/, */) - } - - async function renderAccountEmails() { - await wait(2000) - const siblingEl = document.querySelector('a[href="/search"]') - const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) - ?.textContent?.replace('@gmail.com', '') - .toLowerCase() - for (const accountEmail of accountEmails.slice().reverse()) { - const el = document.createElement('button') - el.style.paddingLeft = '40px' - el.style.textAlign = 'left' - el.textContent = accountEmail - if (accountEmail === emailName) { - el.style.color = 'limegreen' - el.style.paddingLeft = '26px' - el.textContent = `> ${accountEmail}` - } - el.addEventListener('click', () => { - switchAccount(accountEmail) - }) - siblingEl.after(el) - } - } - - function css(strs, ...vals) { + function embedCode(strs, ...vals) { let result = '' for (let i = 0; i < strs.length - 1; i++) { result += strs[i] + String(vals[i]) @@ -112,115 +71,202 @@ result += strs.at(-1) return result } + const css = embedCode - await inputEmails() - - window.addEventListener('keydown', async (event) => { - if (event.repeat) return - if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return - if (document.activeElement.matches('input, textarea, select')) return - - const audio = document.querySelector('audio#active-audio-play') - - const { code } = event - switch (code) { - case 'F1': - case 'F2': - case 'F3': - case 'F4': - case 'F5': - case 'F6': - { - event.preventDefault() - const accountIndex = Number(code.slice(-1)) - 1 - const accountEmail = accountEmails[accountIndex] - switchAccount(accountEmail) + let oldSimpleUrl = '' + + async function handleUrlChange() { + const simpleUrl = (location.hostname + location.pathname) + .replace(/^www\./, '') + .replace(/\/$/, '') + if (simpleUrl === oldSimpleUrl) return + + oldSimpleUrl = simpleUrl + + cleanupCbs.forEach((cb) => { + cb() + }) + cleanupCbs.clear() + + async function switchAccount(accountEmail) { + GM_setValue('accountEmail', accountEmail) + clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') + await wait(2000) + clickIfContentIncludes('button:has(> span)', 'Sign In') + await wait(1000) + click('.cl-socialButtonsIconButton__google') + } + + async function inputEmails() { + while (accountEmailsInput === '') { + accountEmailsInput = + prompt( + 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' + ) ?? '' + accountEmailsInput = accountEmailsInput.trim() + if (accountEmailsInput === '') continue + GM_setValue('accountEmails', accountEmailsInput) + } + accountEmails = accountEmailsInput.split(/, */) + } + + async function renderAccountEmails() { + if (!simpleUrl.startsWith('suno.com')) return + if (document.body.dataset.renderedAccountEmails !== undefined) return + + document.body.dataset.renderedAccountEmails = '1' + await wait(2000) + const siblingEl = document.querySelector('a[href="/search"]') + if (siblingEl === null) { + document.body.dataset.renderedAccountEmails = undefined + return + } + const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) + ?.textContent?.replace('@gmail.com', '') + .toLowerCase() + for (const accountEmail of accountEmails.slice().reverse()) { + const el = document.createElement('button') + el.style.paddingLeft = '40px' + el.style.textAlign = 'left' + el.textContent = accountEmail + if (accountEmail === emailName) { + el.style.color = 'limegreen' + el.style.paddingLeft = '26px' + el.textContent = `> ${accountEmail}` } - break - - case 'Backquote': - case 'Digit1': - case 'Digit2': - case 'Digit3': - case 'Digit4': - case 'Digit5': - case 'Digit6': - case 'Digit7': - case 'Digit8': - case 'Digit9': - case 'Digit0': - if (audio) { - if (audio.src) { - let part = 0 - if (code.startsWith('Digit')) { - part = Number(code.slice(-1)) + el.addEventListener('click', () => { + switchAccount(accountEmail) + }) + siblingEl.after(el) + } + } + + await inputEmails() + + function handleWindowKeyDown(event) { + if (event.repeat) return + if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return + if (document.activeElement.matches('input, textarea, select')) return + + const audio = document.querySelector('audio#active-audio-play') + + const { code } = event + switch (code) { + case 'F1': + case 'F2': + case 'F3': + case 'F4': + case 'F5': + case 'F6': + { + event.preventDefault() + const accountIndex = Number(code.slice(-1)) - 1 + const accountEmail = accountEmails[accountIndex] + switchAccount(accountEmail) + } + break + + case 'Backquote': + case 'Digit1': + case 'Digit2': + case 'Digit3': + case 'Digit4': + case 'Digit5': + case 'Digit6': + case 'Digit7': + case 'Digit8': + case 'Digit9': + case 'Digit0': + if (audio) { + if (audio.src) { + let part = 0 + if (code.startsWith('Digit')) { + part = Number(code.slice(-1)) + } + audio.currentTime = audio.duration * (part / 10) } - audio.currentTime = audio.duration * (part / 10) } - } - break - - case 'ArrowLeft': - case 'ArrowRight': - if (audio) { - if (audio.src) { - const amount = code === 'ArrowLeft' ? -5 : 5 - audio.currentTime += amount + break + + case 'ArrowLeft': + case 'ArrowRight': + if (audio) { + if (audio.src) { + const amount = code === 'ArrowLeft' ? -5 : 5 + audio.currentTime += amount + } } - } - break + break + } } - }) + window.addEventListener('keydown', handleWindowKeyDown) + cleanup(() => { + window.removeEventListener('keydown', handleWindowKeyDown) + }) - if (simpleUrl === 'suno.com') { - location.pathname = '/me' - } - // - else if (simpleUrl === 'suno.com/me') { - await wait(2000) - clickIfContentIncludes('button', 'Liked') - document.activeElement.blur() - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/create') { - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/search') { - renderAccountEmails() - } - // - else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { - await wait(500) - click('.cl-socialButtonsIconButton__google') - } - // - else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { - const accountEmail = await GM_getValue('accountEmail') - if (typeof accountEmail === 'string') { - await GM_deleteValue('accountEmail') - const emailText = `${accountEmail}@gmail.com` - await wait(600) - click(`[data-identifier="${emailText}"]`) - } - } - // - else if (simpleUrl.startsWith('google.com/search')) { - const els = document.querySelectorAll('[data-lyricid] > div > :nth-child(2) > div') - for (const el of els) { - el.style.marginBottom = '-8px' - const br = document.createElement('br') - el.after(br) + if (simpleUrl.startsWith('suno.com')) { + if (simpleUrl === 'suno.com') { + location.pathname = '/me' + } // + else { + if (simpleUrl === 'suno.com/me') { + await wait(2000) + clickIfContentIncludes('button.bg-transparent', 'Liked') + document.activeElement.blur() + } + renderAccountEmails() + } + } // + else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { + await wait(500) + click('.cl-socialButtonsIconButton__google') + } // + else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { + const accountEmail = await GM_getValue('accountEmail') + if (typeof accountEmail === 'string') { + GM_deleteValue('accountEmail') + const emailText = `${accountEmail}@gmail.com` + await wait(600) + click(`[data-identifier="${emailText}"]`) + } + } // + else if (simpleUrl.startsWith('google.com/search')) { + const els = document.querySelectorAll('[data-lyricid] > div > :nth-child(2) > div') + for (const el of els) { + el.style.marginBottom = '-8px' + const br = document.createElement('br') + el.after(br) + } } } GM_addStyle(css` - body { - font-family: sans-serif; + *, + :before, + :after { + backdrop-filter: none !important; + box-shadow: none !important; + } + body, + .font-sans, + [role='menuitem'] { + font-family: 'IBM Plex Sans', sans-serif !important; + } + .font-serif { + font-family: 'Palatino Linotype', serif; + font-weight: 700; + } + .sticky { + position: unset !important; } .css-1w1f2eu { white-space: normal !important; } + .w-split-bar + :has(.bg-vinylBlack-darker) { + width: 320px !important; + } `) -})() + + window.addEventListener('urlchange', handleUrlChange) + handleUrlChange() +})(MienAIMusic || (MienAIMusic = {})) diff --git a/scripts/Mien-AI-Music/script.user.ts b/scripts/Mien-AI-Music/script.user.ts index 602949b..444622f 100644 --- a/scripts/Mien-AI-Music/script.user.ts +++ b/scripts/Mien-AI-Music/script.user.ts @@ -14,17 +14,20 @@ // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle +// @grant window.onurlchange // @license MIT // @noframes // ==/UserScript== -;(async function () { - const simpleUrl: string = (location.hostname + location.pathname) - .replace(/^www\./, '') - .replace(/\/$/, '') +namespace MienAIMusic { + const cleanupCbs = new Set<() => unknown>() + + function cleanup(cb: () => unknown): void { + cleanupCbs.add(cb) + } let accountEmails: string[] = [] - let accountEmailsInput = await GM_getValue('accountEmails', '') + let accountEmailsInput = GM_getValue('accountEmails', '') async function wait(ms: number): Promise { return new Promise((resolve) => { @@ -66,52 +69,7 @@ el?.click() } - async function switchAccount(accountEmail: string): Promise { - await GM_setValue('accountEmail', accountEmail) - clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') - await wait(600) - clickIfContentIncludes('button:has(> span)', 'Sign In') - await wait(200) - click('.cl-socialButtonsIconButton__google') - } - - async function inputEmails(): Promise { - while (accountEmailsInput === '') { - accountEmailsInput = - prompt( - 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' - ) ?? '' - accountEmailsInput = accountEmailsInput.trim() - if (accountEmailsInput === '') continue - GM_setValue('accountEmails', accountEmailsInput) - } - accountEmails = accountEmailsInput.split(/, */) - } - - async function renderAccountEmails(): Promise { - await wait(2000) - const siblingEl = document.querySelector('a[href="/search"]')! - const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) - ?.textContent?.replace('@gmail.com', '') - .toLowerCase() - for (const accountEmail of accountEmails.slice().reverse()) { - const el = document.createElement('button') - el.style.paddingLeft = '40px' - el.style.textAlign = 'left' - el.textContent = accountEmail - if (accountEmail === emailName) { - el.style.color = 'limegreen' - el.style.paddingLeft = '26px' - el.textContent = `> ${accountEmail}` - } - el.addEventListener('click', (): void => { - switchAccount(accountEmail) - }) - siblingEl.after(el) - } - } - - function css(strs: TemplateStringsArray, ...vals: unknown[]): string { + function embedCode(strs: TemplateStringsArray, ...vals: unknown[]): string { let result: string = '' for (let i = 0; i < strs.length - 1; i++) { result += strs[i] + String(vals[i]) @@ -119,117 +77,204 @@ result += strs.at(-1) return result } + const css = embedCode - await inputEmails() - - window.addEventListener('keydown', async (event) => { - if (event.repeat) return - if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return - if (document.activeElement!.matches('input, textarea, select')) return - - const audio = document.querySelector('audio#active-audio-play') - - const { code } = event - switch (code) { - case 'F1': - case 'F2': - case 'F3': - case 'F4': - case 'F5': - case 'F6': - { - event.preventDefault() - const accountIndex: number = Number(code.slice(-1)) - 1 - const accountEmail: string = accountEmails[accountIndex] - switchAccount(accountEmail) + let oldSimpleUrl: string = '' + + async function handleUrlChange(): Promise { + const simpleUrl: string = (location.hostname + location.pathname) + .replace(/^www\./, '') + .replace(/\/$/, '') + if (simpleUrl === oldSimpleUrl) return + + oldSimpleUrl = simpleUrl + + cleanupCbs.forEach((cb) => { + cb() + }) + cleanupCbs.clear() + + async function switchAccount(accountEmail: string): Promise { + GM_setValue('accountEmail', accountEmail) + clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') + await wait(2000) + clickIfContentIncludes('button:has(> span)', 'Sign In') + await wait(1000) + click('.cl-socialButtonsIconButton__google') + } + + async function inputEmails(): Promise { + while (accountEmailsInput === '') { + accountEmailsInput = + prompt( + 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' + ) ?? '' + accountEmailsInput = accountEmailsInput.trim() + if (accountEmailsInput === '') continue + GM_setValue('accountEmails', accountEmailsInput) + } + accountEmails = accountEmailsInput.split(/, */) + } + + async function renderAccountEmails(): Promise { + if (!simpleUrl.startsWith('suno.com')) return + if (document.body.dataset.renderedAccountEmails !== undefined) return + + document.body.dataset.renderedAccountEmails = '1' + await wait(2000) + const siblingEl = document.querySelector('a[href="/search"]') + if (siblingEl === null) { + document.body.dataset.renderedAccountEmails = undefined + return + } + const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) + ?.textContent?.replace('@gmail.com', '') + .toLowerCase() + for (const accountEmail of accountEmails.slice().reverse()) { + const el = document.createElement('button') + el.style.paddingLeft = '40px' + el.style.textAlign = 'left' + el.textContent = accountEmail + if (accountEmail === emailName) { + el.style.color = 'limegreen' + el.style.paddingLeft = '26px' + el.textContent = `> ${accountEmail}` } - break - - case 'Backquote': - case 'Digit1': - case 'Digit2': - case 'Digit3': - case 'Digit4': - case 'Digit5': - case 'Digit6': - case 'Digit7': - case 'Digit8': - case 'Digit9': - case 'Digit0': - if (audio) { - if (audio.src) { - let part = 0 - if (code.startsWith('Digit')) { - part = Number(code.slice(-1)) + el.addEventListener('click', () => { + switchAccount(accountEmail) + }) + siblingEl.after(el) + } + } + + await inputEmails() + + function handleWindowKeyDown(event: KeyboardEvent): void { + if (event.repeat) return + if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return + if (document.activeElement!.matches('input, textarea, select')) return + + const audio = document.querySelector('audio#active-audio-play') + + const { code } = event + switch (code) { + case 'F1': + case 'F2': + case 'F3': + case 'F4': + case 'F5': + case 'F6': + { + event.preventDefault() + const accountIndex: number = Number(code.slice(-1)) - 1 + const accountEmail: string = accountEmails[accountIndex] + switchAccount(accountEmail) + } + break + + case 'Backquote': + case 'Digit1': + case 'Digit2': + case 'Digit3': + case 'Digit4': + case 'Digit5': + case 'Digit6': + case 'Digit7': + case 'Digit8': + case 'Digit9': + case 'Digit0': + if (audio) { + if (audio.src) { + let part = 0 + if (code.startsWith('Digit')) { + part = Number(code.slice(-1)) + } + audio.currentTime = audio.duration * (part / 10) } - audio.currentTime = audio.duration * (part / 10) } - } - break - - case 'ArrowLeft': - case 'ArrowRight': - if (audio) { - if (audio.src) { - const amount = code === 'ArrowLeft' ? -5 : 5 - audio.currentTime += amount + break + + case 'ArrowLeft': + case 'ArrowRight': + if (audio) { + if (audio.src) { + const amount = code === 'ArrowLeft' ? -5 : 5 + audio.currentTime += amount + } } - } - break + break + } } - }) + window.addEventListener('keydown', handleWindowKeyDown) + cleanup(() => { + window.removeEventListener('keydown', handleWindowKeyDown) + }) - if (simpleUrl === 'suno.com') { - location.pathname = '/me' - } - // - else if (simpleUrl === 'suno.com/me') { - await wait(2000) - clickIfContentIncludes('button', 'Liked') - ;(document.activeElement as HTMLElement).blur() - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/create') { - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/search') { - renderAccountEmails() - } - // - else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { - await wait(500) - click('.cl-socialButtonsIconButton__google') - } - // - else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { - const accountEmail = await GM_getValue('accountEmail') - if (typeof accountEmail === 'string') { - await GM_deleteValue('accountEmail') - const emailText: string = `${accountEmail}@gmail.com` - await wait(600) - click(`[data-identifier="${emailText}"]`) - } - } - // - else if (simpleUrl.startsWith('google.com/search')) { - const els = document.querySelectorAll( - '[data-lyricid] > div > :nth-child(2) > div' - ) - for (const el of els) { - el.style.marginBottom = '-8px' - const br: HTMLBRElement = document.createElement('br') - el.after(br) + if (simpleUrl.startsWith('suno.com')) { + if (simpleUrl === 'suno.com') { + location.pathname = '/me' + } // + else { + if (simpleUrl === 'suno.com/me') { + await wait(2000) + clickIfContentIncludes('button.bg-transparent', 'Liked') + ;(document.activeElement as HTMLElement).blur() + } + renderAccountEmails() + } + } // + else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { + await wait(500) + click('.cl-socialButtonsIconButton__google') + } // + else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { + const accountEmail = await GM_getValue('accountEmail') + if (typeof accountEmail === 'string') { + GM_deleteValue('accountEmail') + const emailText: string = `${accountEmail}@gmail.com` + await wait(600) + click(`[data-identifier="${emailText}"]`) + } + } // + else if (simpleUrl.startsWith('google.com/search')) { + const els = document.querySelectorAll( + '[data-lyricid] > div > :nth-child(2) > div' + ) + for (const el of els) { + el.style.marginBottom = '-8px' + const br: HTMLBRElement = document.createElement('br') + el.after(br) + } } } GM_addStyle(css` - body { - font-family: sans-serif; + *, + :before, + :after { + backdrop-filter: none !important; + box-shadow: none !important; + } + body, + .font-sans, + [role='menuitem'] { + font-family: 'IBM Plex Sans', sans-serif !important; + } + .font-serif { + font-family: 'Palatino Linotype', serif; + font-weight: 700; + } + .sticky { + position: unset !important; } .css-1w1f2eu { white-space: normal !important; } + .w-split-bar + :has(.bg-vinylBlack-darker) { + width: 320px !important; + } `) -})() + + window.addEventListener('urlchange', handleUrlChange) + handleUrlChange() +}