// ==UserScript==
// @name YouTube 多重播放器
// @namespace http://tampermonkey.net/
// @version 4.1
// @match https://www.youtube.com/
// @match https://www.youtube.com/feed/subscriptions
// @match https://www.youtube.com/feed/history
// @match https://www.youtube.com/playlist?list=WL
// @match https://www.youtube.com/playlist?list=LL
// @grant none
// @license MIT
// @description 以新分頁或新視窗,同時播放複數Youtube影片。
// ==/UserScript==
(function(){
const STORAGE_POS='ytMulti_btnPos';
const STORAGE_LIST1='ytMulti_videoList1';
const STORAGE_LIST2='ytMulti_videoList2';
const STORAGE_MODE='ytMulti_openMode';
const STORAGE_CURRENT='ytMulti_currentList';
let currentList=localStorage.getItem(STORAGE_CURRENT)||'list1';
const panel=document.createElement('div');
panel.id='ytMulti_panel';
panel.style.cssText="position:fixed;background:rgba(0,0,0,0.6);color:#fff;padding:5px;border-radius:4px;z-index:9999;display:flex;align-items:center;cursor:move;gap:5px;";
document.body.appendChild(panel);
const savedPos=JSON.parse(localStorage.getItem(STORAGE_POS)||'null');
if(savedPos){panel.style.top=savedPos.top;panel.style.left=savedPos.left;panel.style.right='auto';}
panel.addEventListener('mousedown',e=>{
e.preventDefault();
let startX=e.clientX,startY=e.clientY;
const rect=panel.getBoundingClientRect();
function onMove(ev){panel.style.top=rect.top+ev.clientY-startY+'px';panel.style.left=rect.left+ev.clientX-startX+'px';}
function onUp(){
localStorage.setItem(STORAGE_POS,JSON.stringify({top:panel.style.top,left:panel.style.left}));
window.removeEventListener('mousemove',onMove);
window.removeEventListener('mouseup',onUp);
}
window.addEventListener('mousemove',onMove);
window.addEventListener('mouseup',onUp);
});
const playBtn=document.createElement('button');
playBtn.textContent='▶ 多重播放';
panel.appendChild(playBtn);
const modeBtn=document.createElement('button');
modeBtn.textContent=localStorage.getItem(STORAGE_MODE)==='tab'?'分頁':'視窗';
panel.appendChild(modeBtn);
const listBtn=document.createElement('button');
listBtn.textContent='切換清單';
panel.appendChild(listBtn);
panel.addEventListener('dragover',e=>e.preventDefault());
panel.addEventListener('drop',e=>{
e.preventDefault();
const data=e.dataTransfer.getData('text/uri-list')||e.dataTransfer.getData('text/plain');
const vid=parseYouTubeID(data);
if(!vid) return;
const storageKey=currentList==='list1'?STORAGE_LIST1:STORAGE_LIST2;
const ids=JSON.parse(localStorage.getItem(storageKey)||'[]');
if(!ids.includes(vid)){
ids.push(vid);
localStorage.setItem(storageKey,JSON.stringify(ids));
}
});
modeBtn.addEventListener('click',()=>{
const mode=localStorage.getItem(STORAGE_MODE)==='tab'?'window':'tab';
localStorage.setItem(STORAGE_MODE,mode);
modeBtn.textContent=mode==='tab'?'分頁':'視窗';
});
listBtn.addEventListener('click',()=>{
currentList=currentList==='list1'?'list2':'list1';
localStorage.setItem(STORAGE_CURRENT,currentList);
});
playBtn.addEventListener('click',()=>{
const storageKey=currentList==='list1'?STORAGE_LIST1:STORAGE_LIST2;
const ids=JSON.parse(localStorage.getItem(storageKey)||'[]');
if(!ids.length) return alert('尚無影片清單');
const html=makeBlobPage(ids,currentList);
const blobUrl=URL.createObjectURL(new Blob([html],{type:'text/html'}));
const mode=localStorage.getItem(STORAGE_MODE);
mode==='tab'?window.open(blobUrl,'_blank'):window.open(blobUrl,'_blank','width=800,height=600');
});
function parseYouTubeID(url){
const m=url.match(/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/);
return m?m[1]:null;
}
function makeBlobPage(ids,listKey){
const listJson=JSON.stringify(ids);
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>多重播放</title><style>
body{margin:0;padding:0;background:#000;overflow:hidden;}
.container{position:absolute;top:0;left:0;width:100vw;height:100vh;display:flex;flex-wrap:wrap;align-content:flex-start;}
.video-wrapper{position:relative;overflow:hidden;will-change:transform;}
.video-wrapper iframe{width:100%;height:100%;border:none;transform:scale(0.999);}
.remove-btn{
position:absolute;top:6px;right:6px;
width:20px;height:20px;
background:#ff4444;
border-radius:3px;
display:none;
cursor:pointer;
z-index:9999;
box-shadow:0 0 3px rgba(0,0,0,0.3);
}
.remove-btn::after{
content:'×';
color:white;
font-size:16px;
position:absolute;
top:50%;left:50%;
transform:translate(-50%,-50%);
}
.video-wrapper:hover .remove-btn{display:block;}
</style></head><body><div class="container"></div><script>
const ASPECT_RATIO=16/9;
const ids=${listJson};
const listKey=${JSON.stringify(listKey)};
const container=document.querySelector('.container');
function calculateLayout(){
const W=container.offsetWidth;
const H=container.offsetHeight;
const n=ids.length;
if(n===0)return{cols:0,rows:0,itemWidth:0,itemHeight:0};
let bestCols=1;
let bestRows=1;
let bestItemWidth=0;
let bestItemHeight=0;
let bestScore=0;
for(let cols=1;cols<=Math.min(n,12);cols++){
const rows=Math.ceil(n/cols);
let itemWidth=W/cols;
let itemHeight=itemWidth/ASPECT_RATIO;
if(rows*itemHeight>H){
itemHeight=H/rows;
itemWidth=itemHeight*ASPECT_RATIO;
}
const usedWidth=cols*itemWidth;
const usedHeight=rows*itemHeight;
const areaScore=usedWidth*usedHeight;
const penalty=(W-usedWidth)*0.1+(H-usedHeight)*0.2;
const totalScore=areaScore-penalty;
if(totalScore>bestScore){
bestScore=totalScore;
bestCols=cols;
bestRows=rows;
bestItemWidth=itemWidth;
bestItemHeight=itemHeight;
}
}
return{
cols:bestCols,
rows:bestRows,
itemWidth:bestItemWidth,
itemHeight:bestItemHeight
};
}
function updateLayout(){
const{cols,rows,itemWidth,itemHeight}=calculateLayout();
container.style.fontSize='0';
Array.from(container.children).forEach((wrap,index)=>{
const col=index%cols;
const row=Math.floor(index/cols);
wrap.style.width=itemWidth+'px';
wrap.style.height=itemHeight+'px';
wrap.style.transform=\`translate(\${col*itemWidth}px,\${row*itemHeight}px)\`;
});
}
function createVideo(id,idx){
const wrap=document.createElement('div');
wrap.className='video-wrapper';
wrap.style.display='inline-block';
wrap.style.position='absolute';
const ifr=document.createElement('iframe');
ifr.src='https://www.youtube.com/embed/'+id+'?autoplay=1&playsinline=1';
ifr.allow='autoplay; encrypted-media; fullscreen';
const delBtn=document.createElement('div');
delBtn.className='remove-btn';
delBtn.onclick=()=>{
const storageKey=listKey==='list1'?'ytMulti_videoList1':'ytMulti_videoList2';
const stored=JSON.parse(localStorage.getItem(storageKey)||'[]');
stored.splice(stored.indexOf(id),1);
localStorage.setItem(storageKey,JSON.stringify(stored));
wrap.remove();
updateLayout();
};
wrap.append(ifr,delBtn);
return wrap;
}
ids.forEach((id,idx)=>container.appendChild(createVideo(id,idx)));
updateLayout();
window.addEventListener('resize',updateLayout);
setInterval(updateLayout,500);
<\/script></body></html>`;
}
})();