// ==UserScript==
// @name MissAV 备份
// @namespace http://tampermonkey.net/
// @version 5.9
// @description 自动发现并批量抓取MissAV所有片单和收藏,提供多种格式导出功能
// @author Gemini
// @match *://missav.*/*
// @match *://missav.ai/*
// @match *://missav.ws/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect *
// ==/UserScript==
(function() {
'use strict';
// 核心函数和变量
const state = {
allPlaylists: [],
savedVideos: [],
isScraping: false
};
let container, statusDiv, scrapePlaylistsBtn, scrapeSavedBtn, exportHtmlBtn, exportMdBtn, exportJsonBtn, resultsDiv, videoListDiv, logOutputDiv, header, bodyContainer;
// 检查并插入UI的函数
function insertUI() {
container = document.createElement('div');
container.id = 'missav-manager-container';
container.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
background-color: #1a202c;
color: #e2e8f0;
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: 'Inter', sans-serif;
width: 300px;
max-height: 80vh;
overflow-y: auto;
`;
header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
cursor: grab;
`;
const title = document.createElement('h3');
title.id = 'missav-title';
title.textContent = '播放列表管理器';
title.style.cssText = 'font-size: 1.125rem; font-weight: bold; margin: 0;';
header.appendChild(title);
bodyContainer = document.createElement('div');
bodyContainer.id = 'missav-body';
bodyContainer.style.cssText = 'display: flex; flex-direction: column; gap: 0.75rem;';
statusDiv = document.createElement('div');
statusDiv.id = 'missav-status';
statusDiv.style.cssText = 'padding: 0.5rem; border-radius: 0.25rem; text-align: center; background-color: #4a5568;';
statusDiv.textContent = '等待操作...';
const scrapeBtnGroup = document.createElement('div');
scrapeBtnGroup.style.cssText = 'display: flex; flex-direction: column; gap: 0.5rem;';
scrapePlaylistsBtn = createScrapeButton('批量抓取所有片单①', '#3182ce', '#2c5282');
scrapeSavedBtn = createScrapeButton('抓取我的收藏视频②', '#48bb78', '#38a169');
scrapeBtnGroup.appendChild(scrapePlaylistsBtn);
scrapeBtnGroup.appendChild(scrapeSavedBtn);
const exportBtnGroup = document.createElement('div');
exportBtnGroup.style.cssText = 'display: flex; flex-direction: row; gap: 0.5rem;';
exportHtmlBtn = createExportButton('导出为 HTML (推荐)③', 'html', '#38a169', '#2f855a');
exportMdBtn = createExportButton('导出为 Markdown', 'md', '#667eea', '#5a67d8');
exportJsonBtn = createExportButton('导出为 JSON', 'json', '#e53e3e', '#c53030');
exportBtnGroup.appendChild(exportHtmlBtn);
exportBtnGroup.appendChild(exportMdBtn);
exportBtnGroup.appendChild(exportJsonBtn);
resultsDiv = document.createElement('div');
resultsDiv.id = 'missav-results';
resultsDiv.style.cssText = 'margin-top: 1rem;';
logOutputDiv = document.createElement('div');
logOutputDiv.id = 'missav-log';
logOutputDiv.style.cssText = 'background-color: #2d3748; padding: 0.5rem; border-radius: 0.25rem; max-height: 150px; overflow-y: auto; font-size: 0.8rem; margin-top: 0.5rem; white-space: pre-wrap; word-wrap: break-word;';
logOutputDiv.textContent = '日志输出:';
videoListDiv = document.createElement('div');
videoListDiv.id = 'missav-video-list';
videoListDiv.style.cssText = 'padding: 0.5rem; background-color: #2d3748; border-radius: 0.25rem; max-height: 200px; overflow-y: auto;';
videoListDiv.textContent = '抓取结果将显示在这里。';
bodyContainer.appendChild(scrapeBtnGroup);
bodyContainer.appendChild(exportBtnGroup);
bodyContainer.appendChild(statusDiv);
bodyContainer.appendChild(logOutputDiv);
bodyContainer.appendChild(resultsDiv);
resultsDiv.appendChild(videoListDiv);
container.appendChild(header);
container.appendChild(bodyContainer);
document.body.appendChild(container);
scrapePlaylistsBtn.addEventListener('click', scrapeAllPlaylists);
scrapeSavedBtn.addEventListener('click', scrapeSavedVideos);
exportHtmlBtn.addEventListener('click', () => exportData('html'));
exportMdBtn.addEventListener('click', () => exportData('md'));
exportJsonBtn.addEventListener('click', () => exportData('json'));
header.addEventListener('click', toggleUI);
header.addEventListener('mousedown', startDrag);
logMessage('UI 已成功加载。');
}
// 辅助函数,用于创建抓取按钮
function createScrapeButton(text, bgColor, hoverColor) {
const btn = document.createElement('button');
btn.textContent = text;
btn.style.cssText = 'width: 100%; padding: 0.75rem; border-radius: 0.25rem; color: white; font-weight: bold; cursor: pointer; transition: background-color 0.2s ease-in-out;';
btn.style.backgroundColor = bgColor;
btn.onmouseover = () => btn.style.backgroundColor = hoverColor;
btn.onmouseout = () => btn.style.backgroundColor = bgColor;
return btn;
}
// 辅助函数,用于创建导出按钮
function createExportButton(text, format, bgColor, hoverColor) {
const btn = document.createElement('button');
btn.textContent = text;
btn.style.cssText = `flex: 1; padding: 0.75rem 0; border-radius: 0.25rem; background-color: ${bgColor}; color: white; font-weight: bold; cursor: pointer; transition: background-color 0.2s ease-in-out;`;
btn.onmouseover = () => btn.style.backgroundColor = hoverColor;
btn.onmouseout = () => btn.style.backgroundColor = bgColor;
btn.disabled = true;
btn.dataset.format = format;
return btn;
}
// 核心函数
function logMessage(message, isError = false) {
if (!logOutputDiv) {
console.error('日志容器未找到。');
return;
}
const time = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${time}] ${message}`;
logEntry.style.color = isError ? '#f56565' : '#e2e8f0';
logOutputDiv.appendChild(logEntry);
logOutputDiv.scrollTop = logOutputDiv.scrollHeight;
}
function setStatus(message, isError = false) {
if (statusDiv) {
statusDiv.textContent = message;
statusDiv.style.backgroundColor = isError ? '#c53030' : '#4a5568';
}
logMessage(message, isError);
}
// 更新UI以显示分类后的数据和封面图
function updateUI() {
const allData = [...state.allPlaylists, ...state.savedVideos.length ? [{ playlistTitle: '我的收藏', videos: state.savedVideos }] : []];
if (allData.length > 0) {
videoListDiv.innerHTML = '';
allData.forEach(playlist => {
const playlistTitleEl = document.createElement('h4');
playlistTitleEl.textContent = playlist.playlistTitle;
playlistTitleEl.style.cssText = 'font-weight: bold; margin-top: 1rem; border-bottom: 1px solid #4a5568; padding-bottom: 0.25rem;';
videoListDiv.appendChild(playlistTitleEl);
const ul = document.createElement('ul');
ul.style.cssText = 'list-style-type: none; padding: 0;';
playlist.videos.forEach(video => {
const li = document.createElement('li');
li.style.cssText = 'display: flex; align-items: center; padding: 0.5rem; border-bottom: 1px solid #4a5568;';
if (video.coverUrl) {
const img = document.createElement('img');
img.src = video.coverUrl;
img.style.cssText = 'width: 50px; height: 50px; object-fit: cover; margin-right: 0.75rem; border-radius: 0.25rem;';
li.appendChild(img);
}
const link = document.createElement('a');
link.href = video.url;
link.target = '_blank';
link.style.cssText = 'color: #63b3ed; text-decoration: none;';
link.textContent = video.title;
li.appendChild(link);
ul.appendChild(li);
});
videoListDiv.appendChild(ul);
});
exportHtmlBtn.disabled = false;
exportMdBtn.disabled = false;
exportJsonBtn.disabled = false;
} else {
videoListDiv.textContent = '未找到任何视频。';
exportHtmlBtn.disabled = true;
exportMdBtn.disabled = true;
exportJsonBtn.disabled = true;
}
logMessage('UI已更新。');
}
let wasDragged = false;
function toggleUI(event) {
if (wasDragged) {
wasDragged = false;
return;
}
const isHidden = bodyContainer.style.display === 'none';
if (isHidden) {
bodyContainer.style.display = 'flex';
container.style.width = '300px';
header.querySelector('h3').textContent = '播放列表管理器';
} else {
bodyContainer.style.display = 'none';
container.style.width = 'fit-content';
header.querySelector('h3').textContent = '▼';
}
}
let isDragging = false;
let offsetX, offsetY;
function startDrag(e) {
e.preventDefault();
if (e.target.tagName.toLowerCase() === 'a' || e.target.tagName.toLowerCase() === 'button') {
return;
}
isDragging = true;
wasDragged = false;
const rect = container.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
header.style.cursor = 'grabbing';
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
wasDragged = true;
const newX = e.clientX - offsetX;
const newY = e.clientY - offsetY;
container.style.left = `${newX}px`;
container.style.top = `${newY}px`;
container.style.right = 'auto';
container.style.bottom = 'auto';
}
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
header.style.cursor = 'grab';
}
// 检查是否为无意义的标签标题
function isJunkTitle(title) {
if (!title) return true;
const junkTitles = [
'CHINESE-SUBTITLE', 'NEW', 'RELEASE', 'UNCENSORED-LEAK',
'TODAY-HOT', 'WEEKLY-HOT', 'MONTHLY-HOT', 'SIRO', 'LUXU',
'GANA', 'MAAN', 'SCUTE', 'ARA', 'FC2', 'HEYZO', 'TOKYOHOT',
'1pondo', 'CARIBBEANCOM', 'CARIBBEANCOMPR', '10musume',
'PACOPACOMAMA', 'GACHINCO', 'XXXAV', 'MARRIEDSLASH',
'NAUGHTY4610', 'NAUGHTY0930', 'MADOU', 'TWAV', 'FURUKE',
'BOKD', 'DASS', 'BTIS', 'OTLD', 'MIAD', 'ZEX', 'DAZD', 'ACZD',
'HSM', 'CNY', 'DM'
];
const normalizedTitle = title.toUpperCase().trim();
return junkTitles.some(junk => normalizedTitle.startsWith(junk));
}
// 提取视频信息的通用函数,现在也包含封面图
function extractVideosFromDoc(doc, baseUrl) {
const videos = [];
const seenUrls = new Set();
const videoCards = doc.querySelectorAll('.thumbnail.group');
if (videoCards.length === 0) {
return videos;
}
videoCards.forEach(card => {
const linkElement = card.querySelector('a[href]');
if (!linkElement || !linkElement.href.includes('missav')) {
return;
}
const imgElement = linkElement.querySelector('img');
const url = linkElement.href.startsWith('http') ? linkElement.href : new URL(linkElement.href, baseUrl).href;
let coverUrl = '';
if (imgElement) {
// 优先从 data-src 或 data-original 获取,解决懒加载问题
coverUrl = imgElement.getAttribute('data-src') || imgElement.getAttribute('data-original') || imgElement.src;
}
let title = '';
let code = '';
try {
const pathname = new URL(url).pathname;
const pathSegments = pathname.split('/').filter(Boolean);
if (pathSegments.length > 0) {
code = pathSegments[pathSegments.length - 1];
}
} catch (e) {
return;
}
if (imgElement && imgElement.alt) {
title = imgElement.alt.trim();
}
if (!title || title.length < 5) {
if (code) {
title = code.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
} else {
return;
}
}
if (isJunkTitle(title)) {
logMessage(`跳过垃圾标签: ${title}`);
return;
}
if (code && !title.toUpperCase().includes(code.toUpperCase())) {
title = `${code.toUpperCase()} ${title}`;
}
if (title && url && !seenUrls.has(url)) {
videos.push({ title, url, coverUrl });
seenUrls.add(url);
}
});
return videos;
}
// 自动发现所有播放列表并获取标题(新版,支持分页)
async function findPlaylists() {
setStatus('正在发现所有片单...');
let allPlaylists = [];
let page = 1;
const maxPages = 50;
let consecutiveEmptyPages = 0;
const baseUrl = 'https://missav.ai/cn/playlists';
while (page <= maxPages) {
const playlistsUrl = `${baseUrl}?page=${page}`;
logMessage(`正在发现片单,第 ${page} 页...`);
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: playlistsUrl,
onload: resolve,
onerror: reject,
withCredentials: true
});
});
if (response.status !== 200) {
logMessage(`网络请求失败,状态码: ${response.status},已达到抓取终点。`, true);
break;
}
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const playlistCards = doc.querySelectorAll('a[href*="/playlists/"]');
const foundPlaylists = [];
const seenUrls = new Set();
playlistCards.forEach(card => {
const url = new URL(card.href, playlistsUrl).href;
if (url.includes('/playlists/') && url.split('/').length > 5 && !seenUrls.has(url)) {
const titleElement = card.querySelector('p.text-base.font-medium.text-nord6.truncate');
const title = titleElement ? titleElement.textContent.trim() : '未知标题';
if (!allPlaylists.some(p => p.url === url)) {
foundPlaylists.push({
title: title,
url: url
});
seenUrls.add(url);
}
}
});
if (foundPlaylists.length > 0) {
logMessage(`第 ${page} 页找到 ${foundPlaylists.length} 个片单。`);
allPlaylists.push(...foundPlaylists);
consecutiveEmptyPages = 0;
page++;
} else {
consecutiveEmptyPages++;
logMessage(`第 ${page} 页没有找到片单。连续空页数:${consecutiveEmptyPages}`);
if (consecutiveEmptyPages >= 2) {
logMessage('连续两页未找到片单,停止抓取。');
break;
}
page++;
}
} catch (error) {
setStatus(`发现片单失败: ${error.message}`, true);
break;
}
}
const uniquePlaylists = Array.from(new Map(allPlaylists.map(p => [p.url, p])).values());
logMessage(`已发现 ${uniquePlaylists.length} 个不重复的片单。`);
return uniquePlaylists;
}
// 改进后的单播放列表抓取函数
async function scrapeSingleList(list) {
const { title, url } = list;
const baseUrl = url.split('?')[0];
let page = 1;
const maxPages = 50;
let consecutiveEmptyPages = 0;
let listVideos = [];
logMessage(`--- 开始抓取:${title} ---`);
while(page <= maxPages) {
const pageUrl = `${baseUrl}?page=${page}`;
logMessage(`正在抓取 ${title},第 ${page} 页...`);
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: pageUrl,
onload: resolve,
onerror: reject,
withCredentials: true
});
});
if (response.status !== 200) {
logMessage(`网络请求失败,状态码: ${response.status},已达到抓取终点。`, true);
break;
}
const parser = new DOMParser();
const pageDoc = parser.parseFromString(response.responseText, 'text/html');
const videosFound = extractVideosFromDoc(pageDoc, pageUrl);
if (videosFound.length > 0) {
logMessage(`第 ${page} 页找到 ${videosFound.length} 个视频。`);
listVideos.push(...videosFound);
page++;
consecutiveEmptyPages = 0;
} else {
consecutiveEmptyPages++;
logMessage(`第 ${page} 页没有找到视频。连续空页数:${consecutiveEmptyPages}`);
if (consecutiveEmptyPages >= 2) {
logMessage('连续两页未找到视频,停止抓取。');
break;
}
page++;
}
} catch (error) {
logMessage(`抓取页面 ${pageUrl} 失败: ${error.message}`, true);
break;
}
}
return {
title: title,
videos: listVideos
};
}
// 批量抓取所有播放列表
async function scrapeAllPlaylists() {
if (state.isScraping) {
setStatus('正在抓取中,请稍候...', false);
return;
}
state.isScraping = true;
state.allPlaylists = [];
const playlists = await findPlaylists();
if (playlists.length === 0) {
setStatus('未发现任何片单。', true);
state.isScraping = false;
return;
}
let totalVideosFound = 0;
for (const playlist of playlists) {
setStatus(`正在抓取片单: ${playlist.title}`);
const result = await scrapeSingleList(playlist);
if (result.videos.length > 0) {
state.allPlaylists.push({
playlistTitle: result.title,
videos: result.videos
});
totalVideosFound += result.videos.length;
}
updateUI();
await new Promise(resolve => setTimeout(resolve, 2000));
}
setStatus(`所有片单抓取完成!总共找到 ${totalVideosFound} 个视频。`, false);
state.isScraping = false;
}
// 抓取我的收藏视频
async function scrapeSavedVideos() {
if (state.isScraping) {
setStatus('正在抓取中,请稍候...', false);
return;
}
state.isScraping = true;
state.savedVideos = [];
setStatus('正在抓取收藏视频...');
const savedResult = await scrapeSingleList({
title: '我的收藏',
url: 'https://missav.ai/cn/saved'
});
if (savedResult.videos.length > 0) {
state.savedVideos = savedResult.videos;
setStatus(`收藏视频抓取完成!总共找到 ${state.savedVideos.length} 个视频。`);
} else {
setStatus('未在收藏列表中找到任何视频。');
}
updateUI();
state.isScraping = false;
}
// 导出数据并支持多种格式
function exportData(format) {
if (state.allPlaylists.length === 0 && state.savedVideos.length === 0) {
setStatus('没有可以导出的数据。', true);
return;
}
let data = '';
let fileName = '';
let mimeType = '';
if (format === 'html') {
fileName = 'missav_collection.html';
mimeType = 'text/html';
data = generateHtmlData(state.allPlaylists, state.savedVideos);
} else if (format === 'md') {
fileName = 'missav_collection.md';
mimeType = 'text/markdown';
data = generateMarkdownData(state.allPlaylists, state.savedVideos);
} else if (format === 'json') {
fileName = 'missav_collection.json';
mimeType = 'application/json';
data = JSON.stringify({
playlists: state.allPlaylists,
savedVideos: state.savedVideos
}, null, 2);
}
const blob = new Blob([data], { type: mimeType });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setStatus(`数据已导出为 ${format.toUpperCase()} 格式!`, false);
}
// 生成带目录和排版的 Markdown 数据
function generateMarkdownData(playlists, savedVideos) {
let md = '# MissAV 数据导出\n\n';
if (playlists.length > 0) {
md += '## 所有片单\n\n';
md += '### 目录\n\n';
playlists.forEach(playlist => {
const sanitizedTitle = playlist.playlistTitle.toLowerCase().replace(/[\s\W]+/g, '-');
md += `* [${playlist.playlistTitle}](#${sanitizedTitle})\n`;
});
md += '\n---\n\n';
playlists.forEach(playlist => {
md += `## ${playlist.playlistTitle}\n\n`;
md += '影片数量:' + playlist.videos.length + '\n\n';
playlist.videos.forEach(video => {
md += `### ${video.title}\n`;
md += `\n`;
md += `**链接**: [${video.url}](${video.url})\n\n`;
});
md += '\n---\n\n';
});
}
if (savedVideos.length > 0) {
md += '## 我的收藏\n\n';
md += '影片数量:' + savedVideos.length + '\n\n';
savedVideos.forEach(video => {
md += `### ${video.title}\n`;
md += `\n`;
md += `**链接**: [${video.url}](${video.url})\n\n`;
});
}
return md;
}
// 生成美观的 HTML 数据(新增目录和美化)
function generateHtmlData(playlists, savedVideos) {
const hasPlaylists = playlists.length > 0;
const hasSavedVideos = savedVideos.length > 0;
let html = `
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MissAV 收藏与片单</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 2rem; background-color: #f0f2f5; color: #1a202c; }
h1 { color: #2d3748; text-align: center; margin-bottom: 2rem; }
main { display: flex; gap: 2rem; }
.content { flex: 1; margin-left: 200px; }
.toc-container {
position: fixed;
top: 2rem;
left: 2rem;
width: 180px;
max-height: calc(100vh - 4rem);
overflow-y: auto;
background-color: #fff;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 1rem;
border-left: 4px solid #4299e1;
z-index: 100;
}
.toc-container h3 { margin-top: 0; color: #2b6cb0; }
.toc-list { list-style: none; padding: 0; margin: 0; }
.toc-list a { display: block; padding: 0.5rem; color: #4a5568; text-decoration: none; border-radius: 0.25rem; transition: background-color 0.2s; }
.toc-list a:hover { background-color: #e2e8f0; }
.playlist-section { background-color: #fff; border-radius: 0.5rem; box-shadow: 0 4px 10px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 2rem; }
.playlist-title { color: #4a5568; margin-top: 0; margin-bottom: 1rem; font-size: 1.5rem; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.5rem; }
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
.video-card { background-color: #f7fafc; border-radius: 0.5rem; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: transform 0.2s, box-shadow 0.2s; text-align: center; }
.video-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
.video-cover { width: 100%; height: auto; display: block; }
.video-info { padding: 1rem; }
.video-title { margin: 0; font-size: 0.9rem; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.video-link { display: block; margin-top: 0.5rem; color: #4299e1; text-decoration: none; font-size: 0.8rem; }
.video-link:hover { text-decoration: underline; }
.toggle-btn {
position: fixed;
top: 2rem;
right: 2rem;
background-color: #2d3748;
color: #fff;
border: none;
padding: 10px 15px;
border-radius: 0.5rem;
cursor: pointer;
z-index: 101;
transition: background-color 0.2s;
}
.toggle-btn:hover {
background-color: #4a5568;
}
.saved-videos-container .video-card {
background-color: #fff;
}
.hidden { display: none; }
</style>
</head>
<body>
<h1>MissAV 收藏与片单</h1>
${hasPlaylists && hasSavedVideos ? `<button id="toggle-view" class="toggle-btn">切换到我的收藏</button>` : ''}
<main>
${hasPlaylists ? `
<div id="toc-container" class="toc-container">
<h3>片单目录</h3>
<ul class="toc-list">
${playlists.map(p => {
const sanitizedTitle = p.playlistTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '');
return `<li><a href="#${sanitizedTitle}">${p.playlistTitle}</a></li>`;
}).join('')}
</ul>
</div>
` : ''}
<div id="content" class="content">
<div id="playlists-view" ${!hasPlaylists ? 'class="hidden"' : ''}>
${playlists.map(p => `
<div class="playlist-section" id="${p.playlistTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '')}">
<h2 class="playlist-title">${p.playlistTitle}</h2>
<div class="video-grid">
${p.videos.map(v => `
<div class="video-card">
<a href="${v.url}" target="_blank" class="video-link">
<img src="${v.coverUrl}" alt="${v.title}" class="video-cover">
<div class="video-info">
<p class="video-title" title="${v.title}">${v.title}</p>
</div>
</a>
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
<div id="saved-view" ${hasPlaylists ? 'class="hidden"' : ''}>
<div class="playlist-section saved-videos-container">
<h2 class="playlist-title">我的收藏</h2>
<div class="video-grid">
${savedVideos.map(v => `
<div class="video-card">
<a href="${v.url}" target="_blank" class="video-link">
<img src="${v.coverUrl}" alt="${v.title}" class="video-cover">
<div class="video-info">
<p class="video-title" title="${v.title}">${v.title}</p>
</div>
</a>
</div>
`).join('')}
</div>
</div>
</div>
</div>
</main>
<script>
const toggleBtn = document.getElementById('toggle-view');
const playlistsView = document.getElementById('playlists-view');
const savedView = document.getElementById('saved-view');
const tocContainer = document.getElementById('toc-container');
const content = document.getElementById('content');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const isPlaylistsHidden = playlistsView.classList.contains('hidden');
if (isPlaylistsHidden) {
playlistsView.classList.remove('hidden');
savedView.classList.add('hidden');
if(tocContainer) tocContainer.classList.remove('hidden');
if(content) content.style.marginLeft = '200px';
toggleBtn.textContent = '切换到我的收藏';
} else {
playlistsView.classList.add('hidden');
savedView.classList.remove('hidden');
if(tocContainer) tocContainer.classList.add('hidden');
if(content) content.style.marginLeft = '2rem';
toggleBtn.textContent = '切换到所有片单';
}
});
}
</script>
</body>
</html>
`;
return html;
}
window.addEventListener('load', insertUI);
})();