Last active
April 11, 2025 18:05
-
-
Save Kuju29/a34cf4e362e7342991b763433b8529ca to your computer and use it in GitHub Desktop.
เล่นวีดีโอถัดไปเฉพาะคลิปสั้นเท่านั้น
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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