在 YouTube 视频页标题下、频道栏上方插入播放列表按钮,支持桌面和移动端;Shorts 自动使用悬浮备用位置;新增“全清单”按钮
// ==UserScript==
// @name YouTube Playlists Buttons In Video Header
// @namespace c0d3r
// @license MIT
// @version 1.2.0
// @description 在 YouTube 视频页标题下、频道栏上方插入播放列表按钮,支持桌面和移动端;Shorts 自动使用悬浮备用位置;新增“全清单”按钮
// @author c0d3r + ChatGPT
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function () {
'use strict';
const INLINE_ID = 'yavp-inline-bar';
const FLOAT_ID = 'yavp-float-bar';
const STYLE_ID = 'yavp-inline-style';
const STORAGE_PREFIX = 'yavp_inline_';
const playlists = [
{ label: '全部', title: 'All Uploads', prefix: 'UU' },
{ label: '热门', title: 'Popular Uploads', prefix: 'PU' },
{ label: '视频', title: 'All Videos', prefix: 'UULF' },
{ label: '视频热', title: 'Popular Videos', prefix: 'UULP' },
{ label: 'Shorts', title: 'All Shorts', prefix: 'UUSH' },
{ label: '短热', title: 'Popular Shorts', prefix: 'UUPS' },
{ label: '直播', title: 'All Streams', prefix: 'UULV' },
{ label: '直播热', title: 'Popular Streams', prefix: 'UUPV' },
{ label: '会员', title: 'Members-Only Videos', prefix: 'UUMO' }
];
const options = {
playNext: getValue('playNext', true),
newTabs: getValue('newTabs', false)
};
let lastKey = '';
function getValue(key, fallback) {
try {
if (typeof GM_getValue === 'function') {
return GM_getValue(key, fallback);
}
} catch (error) {}
try {
const raw = localStorage.getItem(STORAGE_PREFIX + key);
return raw === null ? fallback : JSON.parse(raw);
} catch (error) {
return fallback;
}
}
function setValue(key, value) {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(key, value);
}
} catch (error) {}
try {
localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
} catch (error) {}
}
function isWatchPage() {
return location.pathname === '/watch';
}
function isShortsPage() {
return location.pathname.startsWith('/shorts/');
}
function isVideoPage() {
return isWatchPage() || isShortsPage();
}
function getChannelId() {
const meta = document.querySelector('meta[itemprop="channelId"][content]');
if (meta && /^UC[\w-]+$/.test(meta.content)) {
return meta.content.trim();
}
const link = document.querySelector(
'a[href*="/channel/UC"], a[href^="/channel/UC"], a[href*="youtube.com/channel/UC"]'
);
if (link) {
const match = link.href.match(/\/channel\/(UC[\w-]+)/);
if (match) {
return match[1];
}
}
const details =
window.ytInitialPlayerResponse?.videoDetails ||
window.__INITIAL_PLAYER_RESPONSE__?.videoDetails;
if (details && /^UC[\w-]+$/.test(details.channelId || '')) {
return details.channelId;
}
return '';
}
function getChannelBasePath() {
const selectors = [
'#owner a[href]',
'ytd-watch-metadata #owner a[href]',
'ytm-slim-owner-renderer a[href]',
'ytm-channel-bar-renderer a[href]'
];
for (const selector of selectors) {
const link = document.querySelector(selector);
const href = link && link.getAttribute('href');
if (!href) {
continue;
}
try {
const url = new URL(href, location.origin);
let path = url.pathname.replace(/\/+$/, '');
path = path.replace(
/\/(featured|videos|shorts|streams|playlists|community|about)$/i,
''
);
if (/^(\/@[^/]+|\/channel\/UC[\w-]+|\/c\/[^/]+|\/user\/[^/]+)$/i.test(path)) {
return path;
}
} catch (error) {}
}
const channelId = getChannelId();
return channelId ? '/channel/' + channelId : '';
}
function buildPlaylistUrl(prefix, channelId) {
const pureId = channelId.startsWith('UC') ? channelId.slice(2) : channelId;
let url = location.origin + '/playlist?list=' + prefix + pureId;
if ((options.playNext && prefix !== 'UUMO') || prefix === 'PU') {
url += '&playnext=1';
}
return url;
}
function buildChannelPlaylistsUrl() {
const base = getChannelBasePath();
return base ? location.origin + base + '/playlists' : '';
}
function openUrl(url) {
if (!url) {
return;
}
if (options.newTabs) {
window.open(url, '_blank', 'noopener');
return;
}
location.assign(url);
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) {
return;
}
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${INLINE_ID} {
width: 100%;
margin: 8px 0 12px 0;
}
#${FLOAT_ID} {
position: fixed;
left: 12px;
right: 12px;
bottom: calc(env(safe-area-inset-bottom, 0px) + 12px);
z-index: 2147483647;
padding: 10px;
border-radius: 16px;
background: var(--yt-spec-base-background, #fff);
border: 1px solid rgba(127, 127, 127, 0.18);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
html[dark] #${FLOAT_ID} {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
#${FLOAT_ID}.is-hidden {
display: none !important;
}
#${INLINE_ID} .yavp-row,
#${FLOAT_ID} .yavp-row {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 2px 0;
scrollbar-width: thin;
}
#${INLINE_ID} .yavp-row::-webkit-scrollbar,
#${FLOAT_ID} .yavp-row::-webkit-scrollbar {
height: 6px;
}
#${INLINE_ID} .yavp-row::-webkit-scrollbar-thumb,
#${FLOAT_ID} .yavp-row::-webkit-scrollbar-thumb {
background: rgba(127, 127, 127, 0.35);
border-radius: 999px;
}
#${INLINE_ID} button,
#${FLOAT_ID} button {
flex: 0 0 auto;
appearance: none;
border: 1px solid var(--yt-spec-10-percent-layer, rgba(0, 0, 0, 0.12));
background: var(--yt-spec-badge-chip-background, rgba(0, 0, 0, 0.05));
color: var(--yt-spec-text-primary, #0f0f0f);
border-radius: 999px;
padding: 8px 12px;
margin: 0;
min-height: 32px;
line-height: 1;
font: 500 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
cursor: pointer;
}
#${INLINE_ID} button:hover,
#${FLOAT_ID} button:hover {
filter: brightness(0.97);
}
#${INLINE_ID} button:active,
#${FLOAT_ID} button:active {
transform: translateY(1px);
}
#${INLINE_ID} .yavp-toggle[data-on="1"],
#${FLOAT_ID} .yavp-toggle[data-on="1"] {
background: var(--yt-spec-call-to-action, #065fd4);
color: var(--yt-spec-text-primary-inverse, #fff);
border-color: transparent;
font-weight: 700;
}
`;
(document.head || document.documentElement).appendChild(style);
}
function removeBars() {
document.getElementById(INLINE_ID)?.remove();
document.getElementById(FLOAT_ID)?.remove();
}
function updateFloatVisibility() {
const floatBar = document.getElementById(FLOAT_ID);
if (!floatBar) {
return;
}
floatBar.classList.toggle('is-hidden', !!document.fullscreenElement);
}
function getInlineMount() {
if (!isWatchPage()) {
return null;
}
if (location.hostname === 'www.youtube.com') {
const topRow = document.querySelector('ytd-watch-metadata #top-row');
if (topRow && topRow.parentElement) {
return {
parent: topRow.parentElement,
before: topRow
};
}
const owner = document.querySelector('ytd-watch-metadata #owner');
if (owner && owner.parentElement) {
return {
parent: owner.parentElement,
before: owner
};
}
}
if (location.hostname === 'm.youtube.com') {
const owner = document.querySelector('ytm-slim-owner-renderer, ytm-channel-bar-renderer');
if (owner && owner.parentElement) {
return {
parent: owner.parentElement,
before: owner
};
}
const meta = document.querySelector('ytm-slim-video-metadata-section-renderer, ytm-watch-metadata');
if (meta && meta.parentElement) {
return {
parent: meta.parentElement,
before: meta.nextSibling || null
};
}
}
return null;
}
function createButton(text, title, handler, isToggle, isOn) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = text;
button.title = title || text;
if (isToggle) {
button.className = 'yavp-toggle';
button.dataset.on = isOn ? '1' : '0';
}
button.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
handler();
});
return button;
}
function buildBar(mode, channelId) {
const root = document.createElement('div');
root.id = mode === 'inline' ? INLINE_ID : FLOAT_ID;
const row = document.createElement('div');
row.className = 'yavp-row';
playlists.forEach(function (item) {
row.appendChild(
createButton(item.label, item.title, function () {
openUrl(buildPlaylistUrl(item.prefix, channelId));
}, false, false)
);
});
const allPlaylistsUrl = buildChannelPlaylistsUrl();
if (allPlaylistsUrl) {
row.appendChild(
createButton('全清单', '当前频道全部播放清单', function () {
openUrl(allPlaylistsUrl);
}, false, false)
);
}
row.appendChild(
createButton('自动播', '切换 playnext=1', function () {
options.playNext = !options.playNext;
setValue('playNext', options.playNext);
render(true);
}, true, options.playNext)
);
row.appendChild(
createButton('新标签', '新标签页打开', function () {
options.newTabs = !options.newTabs;
setValue('newTabs', options.newTabs);
render(true);
}, true, options.newTabs)
);
root.appendChild(row);
return root;
}
function render(force) {
if (!document.body) {
return;
}
if (!isVideoPage()) {
removeBars();
lastKey = '';
return;
}
const channelId = getChannelId();
if (!channelId) {
removeBars();
lastKey = '';
return;
}
ensureStyles();
const mount = getInlineMount();
const mode = mount ? 'inline' : 'float';
const currentKey = [
location.href,
channelId,
mode,
options.playNext,
options.newTabs
].join('|');
const existing = document.getElementById(mode === 'inline' ? INLINE_ID : FLOAT_ID);
if (!force && existing && existing.isConnected && lastKey === currentKey) {
updateFloatVisibility();
return;
}
removeBars();
const bar = buildBar(mode, channelId);
if (mount) {
mount.parent.insertBefore(bar, mount.before);
} else {
document.body.appendChild(bar);
}
lastKey = currentKey;
updateFloatVisibility();
}
function scheduleRender() {
window.setTimeout(render, 50);
window.setTimeout(render, 300);
window.setTimeout(render, 900);
window.setTimeout(render, 1800);
}
window.addEventListener('yt-navigate-finish', scheduleRender, true);
window.addEventListener('yt-page-data-updated', scheduleRender, true);
window.addEventListener('popstate', scheduleRender, true);
document.addEventListener('fullscreenchange', updateFloatVisibility, true);
const rawPushState = history.pushState;
history.pushState = function () {
const result = rawPushState.apply(this, arguments);
scheduleRender();
return result;
};
const rawReplaceState = history.replaceState;
history.replaceState = function () {
const result = rawReplaceState.apply(this, arguments);
scheduleRender();
return result;
};
setInterval(render, 1500);
scheduleRender();
})();