MissAV 备份

自动发现并批量抓取MissAV所有片单和收藏,提供多种格式导出功能

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 += `![${video.title}](${video.coverUrl})\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 += `![${video.title}](${video.coverUrl})\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);
})();