Skip to content

Instantly share code, notes, and snippets.

@Kuju29
Last active April 11, 2025 18:05
Show Gist options
  • Save Kuju29/a34cf4e362e7342991b763433b8529ca to your computer and use it in GitHub Desktop.
Save Kuju29/a34cf4e362e7342991b763433b8529ca to your computer and use it in GitHub Desktop.
เล่นวีดีโอถัดไปเฉพาะคลิปสั้นเท่านั้น
// ==UserScript==
// @name YouTube Auto Play (Short Videos, views, same language, with Endscreen fallback)
// @version 2.0.2
// @description เล่นเฉพาะวิดีโอสั้น
// @match *://www.youtube.com/*
// @updateURL https://gist.githubusercontent.com/Kuju29/a34cf4e362e7342991b763433b8529ca/raw/YouTube%2520Auto%2520Play%2520Short%2520Videos.user.js
// @downloadURL https://gist.githubusercontent.com/Kuju29/a34cf4e362e7342991b763433b8529ca/raw/YouTube%2520Auto%2520Play%2520Short%2520Videos.user.js
// @grant GM_registerMenuCommand
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const settingsKey = "youtubeAutoPlaySettings";
const defaultSettings = {
maxDuration: 600,
minViews: 1000000,
maxAgeYears: 99,
detectLanguage: true,
removewhanreload: false,
};
function loadSettings() {
const saved = localStorage.getItem(settingsKey);
if (saved) {
try {
const parsed = JSON.parse(saved);
Object.assign(defaultSettings, parsed);
console.log("Loaded settings:", defaultSettings);
} catch(e) {
console.error("Failed to parse settings:", e);
}
}
}
function createSettingsUI() {
if (document.getElementById('settings-panel')) return;
const container = document.createElement('div');
container.id = 'settings-panel';
Object.assign(container.style, {
position: 'fixed',
top: '0',
right: '0',
width: '300px',
background: 'linear-gradient(135deg, rgb(24 24 25) 0%, rgb(84 27 141) 100%)',
color: '#ffffff',
borderRadius: '8px 0 0 8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.3)',
padding: '20px',
fontFamily: 'sans-serif',
fontSize: '14px',
transition: 'transform 0.4s ease, opacity 0.4s ease',
opacity: '0.95',
zIndex: 9999,
});
document.body.appendChild(container);
container.innerHTML = `
<div style="text-align:right;"><button id="close-settings">X</button></div>
<h3 style="margin: 0 0 10px 0;">Settings</h3>
<div style="margin-bottom:5px;">
<label>Max Duration (sec): </label>
<input type="number" id="setting-maxDuration" value="${defaultSettings.maxDuration}" style="width:100%;" />
</div>
<div style="margin-bottom:5px;">
<label>Min Views: </label>
<input type="number" id="setting-minViews" value="${defaultSettings.minViews}" style="width:100%;" />
</div>
<div style="margin-bottom:5px;">
<label>Max Age (years): </label>
<input type="number" id="setting-maxAgeYears" value="${defaultSettings.maxAgeYears}" style="width:100%;" />
</div>
<div style="margin-bottom:5px;">
<label>
<input type="checkbox" id="setting-detectLanguage" ${defaultSettings.detectLanguage ? 'checked' : ''} />
Detect Language
</label>
</div>
<div style="margin-bottom:5px;">
<label>
<input type="checkbox" id="setting-removewhanreload" ${defaultSettings.removewhanreload ? 'checked' : ''} />
Remove When Reload
</label>
</div>
<div style="margin-top: 10px; text-align: right;">
<button id="save-settings">Save</button>
</div>
`;
document.body.appendChild(container);
document.getElementById('close-settings').addEventListener('click', function() {
container.style.display = 'none';
});
document.getElementById('save-settings').addEventListener('click', function() {
defaultSettings.maxDuration = parseInt(document.getElementById('setting-maxDuration').value, 10);
defaultSettings.minViews = parseInt(document.getElementById('setting-minViews').value, 10);
defaultSettings.maxAgeYears = parseInt(document.getElementById('setting-maxAgeYears').value, 10);
defaultSettings.detectLanguage = document.getElementById('setting-detectLanguage').checked;
defaultSettings.removewhanreload = document.getElementById('setting-removewhanreload').checked;
localStorage.setItem(settingsKey, JSON.stringify(defaultSettings));
container.style.display = 'none';
console.log("Settings saved:", defaultSettings);
});
}
function showSettingsUI() {
loadSettings();
if (!document.getElementById('settings-panel')) {
createSettingsUI();
} else {
document.getElementById('settings-panel').style.display = 'block';
}
}
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand("Settings", showSettingsUI);
}
let hasTriedShowMoreForCurrentClip = false;
if (typeof unsafeWindow !== 'undefined') {
unsafeWindow.onblur = null;
unsafeWindow.blurred = false;
unsafeWindow.document.hasFocus = () => true;
unsafeWindow.window.onFocus = () => true;
} else {
window.onblur = null;
document.hasFocus = () => true;
window.onFocus = () => true;
}
["hidden", "mozHidden", "msHidden", "webkitHidden"].forEach(prop_name => {
Object.defineProperty(document, prop_name, { value: false });
});
Object.defineProperty(document, "visibilityState", { get: () => "visible" });
Object.defineProperty(document, "webkitVisibilityState", { get: () => "visible" });
if (typeof unsafeWindow !== 'undefined') {
unsafeWindow.document.onvisibilitychange = undefined;
} else {
document.onvisibilitychange = undefined;
}
const blurWhitelist = [
HTMLInputElement,
HTMLAnchorElement,
HTMLSpanElement,
HTMLParagraphElement,
];
const hoverBlacklist = [
HTMLIFrameElement,
HTMLHtmlElement,
HTMLBodyElement,
HTMLHeadElement,
HTMLFrameSetElement,
HTMLFrameElement
];
var event_handler = (event) => {
if (event.type === 'blur' &&
(blurWhitelist.some(type => event.target instanceof type) ||
(event.target.classList && event.target.classList.contains('ql-editor')))) {
return;
}
if (['mouseleave', 'mouseout'].includes(event.type) &&
!hoverBlacklist.some(type => event.target instanceof type)) {
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
};
[
"visibilitychange",
"webkitvisibilitychange",
"blur",
"hasFocus",
"mouseleave",
"mouseout",
"mozvisibilitychange",
"msvisibilitychange"
].forEach(event_name => {
window.addEventListener(event_name, event_handler, true);
document.addEventListener(event_name, event_handler, true);
});
let playedVideoIds = [];
try {
const stored = sessionStorage.getItem("playedVideoIds");
if (stored) {
playedVideoIds = JSON.parse(stored);
}
} catch (e) {
console.error("Error parsing playedVideoIds from localStorage:", e);
}
function savePlayedVideoIds() {
sessionStorage.setItem("playedVideoIds", JSON.stringify(playedVideoIds));
}
function getVideoIdFromUrl(url) {
const match = url.match(/[?&]v=([^&]+)/);
return match ? match[1] : null;
}
function parseDuration(durationText) {
if (!durationText) return 0;
const parts = durationText.split(':').map(Number).reverse();
let seconds = 0;
for (let i = 0; i < parts.length; i++) {
seconds += parts[i] * Math.pow(60, i);
}
return seconds;
}
function detectLanguage(text) {
if (!defaultSettings.detectLanguage) return 'unknown';
const thaiRegex = /[\u0E00-\u0E7F]/;
const laoRegex = /[\u0E80-\u0EFF]/;
const hangulRegex = /[\uAC00-\uD7AF]/;
const cjkRegex = /[\u4E00-\u9FFF]/;
const jpHiraganaKatakana = /[\u3040-\u30FF]/;
const txt = String(text || "");
if (thaiRegex.test(txt)) {
return 'thai';
} else if (laoRegex.test(txt)) {
return 'lao';
} else if (hangulRegex.test(txt)) {
return 'korean';
} else if (jpHiraganaKatakana.test(txt)) {
return 'japanese';
} else if (cjkRegex.test(txt)) {
return 'chinese';
}
return 'unknown';
}
function getCurrentVideoLanguage() {
const titleEl = document.querySelector('h1.title.ytd-watch-metadata, h1.title.style-scope.ytd-video-primary-info-renderer');
if (!titleEl) return 'unknown';
return detectLanguage(titleEl.textContent.trim());
}
function shuffleArray(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function pickRandom(arr) {
if (!Array.isArray(arr) || arr.length === 0) return null;
const shuffled = shuffleArray([...arr]);
return shuffled[0];
}
function fallbackToNextButton() {
console.log("ลอง fallback ด้วยการกดปุ่ม 'ถัดไป'...");
const nextBtn = document.querySelector('.ytp-next-button[aria-disabled="false"]');
if (!nextBtn) {
console.log("ไม่พบปุ่ม 'ถัดไป' หรือปุ่มถูก disable");
return;
}
nextBtn.click();
}
function parseViews(text) {
let clean = text.toLowerCase().replace(/,/g, '').replace(/\s+/g, '').replace(/views?/, '');
const regex = /^([\d\.]+)(k|m|b|||)?/i;
const match = clean.match(regex);
if (!match) return 0;
let num = parseFloat(match[1]);
const unit = match[2] ? match[2].toLowerCase() : '';
if (unit === 'k' || unit === 'พัน') num *= 1e3;
else if (unit === 'm' || unit === 'ล้าน') num *= 1e6;
else if (unit === 'b' || unit === 'พันล้าน') num *= 1e9;
return Math.round(num);
}
function parseUploadAge(ageText) {
ageText = ageText.toLowerCase().trim();
let match = ageText.match(/(\d+)\s*(year|years|)/);
if (match) return parseInt(match[1], 10);
match = ageText.match(/(\d+)\s*(month|months|)/);
if (match) return Math.floor(parseInt(match[1], 10) / 12);
match = ageText.match(/(\d+)\s*(day|days|)/);
if (match) return Math.floor(parseInt(match[1], 10) / 365);
match = ageText.match(/(\d+)\s*(hour|hours|)/);
if (match) return Math.floor(parseInt(match[1], 10) / (365*24));
match = ageText.match(/(\d+)\s*(minute|minutes|)/);
if (match) return Math.floor(parseInt(match[1], 10) / (365*24*60));
return 0;
}
function getVideoInfo(item) {
const title = item.querySelector('#video-title')?.getAttribute('title')?.trim() ||
item.querySelector('#video-title')?.textContent?.trim() || '';
const channel = item.querySelector('#channel-name #text')?.textContent?.trim() || '';
const viewsText = item.querySelectorAll('#metadata-line span.inline-metadata-item')[0]?.textContent || '';
const views = parseViews(viewsText);
const ageText = item.querySelectorAll('#metadata-line span.inline-metadata-item')[1]?.textContent || '';
const age = parseUploadAge(ageText);
const durationText = item.querySelector('ytd-thumbnail-overlay-time-status-renderer .badge-shape-wiz__text')?.textContent?.trim() || '0:00';
const duration = durationText.split(':').reverse().reduce((acc, v, i) => acc + parseInt(v, 10) * Math.pow(60, i), 0);
return { title, channel, views, age, duration };
}
function getEndscreenData(item) {
const url = item.getAttribute('href') || '';
const title = item.querySelector('.ytp-videowall-still-info-title')?.textContent.trim() || '';
const author = item.querySelector('.ytp-videowall-still-info-author')?.textContent.trim() || '';
const durText = item.querySelector('.ytp-videowall-still-info-duration')?.textContent.trim() || '0:00';
const [channel, viewsStr] = author.split('•').map(s => s.trim());
const views = parseViews(viewsStr || '');
const duration = durText.split(':').reverse().reduce((acc, v, i) => acc + parseInt(v, 10) * Math.pow(60, i), 0);
return { url, title, channel: channel || '', views, duration };
}
function pickVideoFromEndscreen() {
console.log("ลองเลือกวิดีโอจาก Endscreen...");
const endscreenSelector = '.html5-endscreen .ytp-videowall-still';
const endscreenVideos = document.querySelectorAll(endscreenSelector);
if (!endscreenVideos || endscreenVideos.length === 0) {
console.log("ไม่พบวิดีโอใน Endscreen หรือยังไม่โหลด => ตัดไปกด Next button");
fallbackToNextButton();
return;
}
let candidates = [];
endscreenVideos.forEach(video => {
if (!video.closest('.html5-endscreen')) return;
const data = getEndscreenData(video);
if (!data) {
console.log("ไม่สามารถดึงข้อมูลจาก endscreen video:", video);
return;
}
if (data.duration < defaultSettings.maxDuration && data.views >= defaultSettings.minViews) {
candidates.push({
duration: data.duration,
views: data.views,
age: 0,
lang: detectLanguage(data.title),
title: data.title,
uploader: data.channel,
element: video
});
}
});
console.log("รายละเอียดวีดีโอก่อนสุ่ม (Endscreen):", candidates);
if (candidates.length === 0) {
console.log("ไม่พบวิดีโอใน Endscreen ที่เหมาะสม => ตัดไปกด Next button");
fallbackToNextButton();
return;
}
const picked = pickRandom(candidates);
if (picked) {
console.log("เลือกวิดีโอจาก Endscreen แบบสุ่ม:", picked.element.href);
picked.element.click();
} else {
console.log("สุ่มไม่เจอวิดีโอใน Endscreen => ตัดไปกด Next button");
fallbackToNextButton();
}
}
function mainAutoPlay() {
console.log("== [mainAutoPlay] เริ่มทำงาน ==");
const autoplayBtn = document.querySelector('.ytp-autonav-toggle-button-container .ytp-autonav-toggle-button');
if (!autoplayBtn) {
console.log("ไม่พบปุ่ม Autoplay หรือยังโหลดไม่เสร็จ");
return;
}
const isAutoplayOff = autoplayBtn.getAttribute('aria-checked') === 'false';
if (!isAutoplayOff) {
console.log("Autoplay ยังเปิดอยู่ ไม่ทำงาน");
return;
}
if (window.location.href.includes('list=')) {
console.log("เป็น Playlist (มี list=) ไม่ทำงาน");
return;
}
const videoItems = Array.from(document.querySelectorAll('ytd-compact-video-renderer'));
if (videoItems.length === 0) {
console.log("ไม่พบวิดีโอถัดไปใน sidebar => ลอง Endscreen...");
pickVideoFromEndscreen();
return;
}
const videoData = videoItems.map(item => {
const info = getVideoInfo(item);
if (!info.title) return null;
const videoUrl = item.querySelector('a#thumbnail')?.getAttribute('href') || "";
const videoId = getVideoIdFromUrl(videoUrl);
const lang = detectLanguage(info.title);
const age = info.age || 0;
return {
duration: info.duration,
views: info.views,
age: age,
lang: lang,
title: info.title,
videoId: videoId,
element: item.querySelector('a#thumbnail')
};
}).filter(Boolean);
const currentLang = getCurrentVideoLanguage();
const candidates = videoData.filter(v =>
v.duration < defaultSettings.maxDuration &&
v.views >= defaultSettings.minViews &&
v.lang === currentLang &&
v.videoId &&
!playedVideoIds.includes(v.videoId) &&
v.age <= defaultSettings.maxAgeYears
);
console.log("รายการวีดีโอก่อนกรอง:", videoData);
if (candidates.length === 0) {
console.log("ไม่มีวิดีโอที่ผ่านเงื่อนไขการกรอง => ลอง Endscreen...");
pickVideoFromEndscreen();
return;
}
const picked = pickRandom(candidates);
if (!picked) {
console.log("สุ่มไม่เจอ? => ลอง Endscreen...");
pickVideoFromEndscreen();
return;
}
console.log("รายการวีดีโอหลังกรอง:", candidates);
console.log("เล่นวิดีโอถัดไป:", picked.element.href);
playedVideoIds.push(picked.videoId);
savePlayedVideoIds();
picked.element.click();
}
let lastUrl = location.href;
function shouldRunScript() {
if (window.location.hostname !== 'www.youtube.com') {
return false;
}
if (!window.location.href.includes('watch?')) {
return false;
}
if (window.location.href.includes('&list=')) {
return false;
}
return true;
}
function init() {
let hasTriedShowMoreForCurrentClip = false;
if (!shouldRunScript()) {
console.log('ไม่ใช่หน้า watch หรือมี &list= => ไม่ทำงาน');
return;
}
console.log('[init] สคริปต์กำลังทำงาน...');
loadSettings();
const currentId = getVideoIdFromUrl(window.location.href);
if (currentId && !playedVideoIds.includes(currentId)) {
playedVideoIds.push(currentId);
savePlayedVideoIds();
}
const video = document.querySelector('video');
if (!video) {
console.log("ไม่พบ video element");
return;
}
if (!video.dataset.autoPlayEventAdded) {
video.dataset.autoPlayEventAdded = "true";
video.addEventListener('ended', () => {
console.log("วิดีโอจบ => เรียก mainAutoPlay");
mainAutoPlay();
});
}
}
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
console.log("🔄 URL เปลี่ยนจาก:", lastUrl, "เป็น:", location.href);
lastUrl = location.href;
setTimeout(() => {
if (shouldRunScript()) {
init();
} else {
console.log("ไม่ใช่หน้า watch หรือมี &list= => ไม่ทำงาน");
}
}, 1500);
}
});
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
} else {
document.addEventListener('DOMContentLoaded', () => {
observer.observe(document.body, { childList: true, subtree: true });
});
}
function checkAndInit() {
setTimeout(init, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAndInit);
} else {
checkAndInit();
}
if (defaultSettings.removewhanreload) {
window.addEventListener("beforeunload", () => {
sessionStorage.removeItem("playedVideoIds");
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment