ProQuest Document & Video Downloader

下载 ProQuest 的 PDF 文档和视频

// ==UserScript==
// @name         ProQuest Document & Video Downloader
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  下载 ProQuest 的 PDF 文档和视频
// @author       pocai
// @match        https://www.proquest.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @run-at       document-end
// @connect      *
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    GM_addStyle(`
        .download-all-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            z-index: 9999;
        }
        .download-video-all-btn {
            position: fixed;
            top: 70px;
            right: 20px;
            background-color: #2196F3;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            z-index: 9999;
        }
        .single-download-btn, .single-video-btn {
            background-color: #2196F3;
            color: white;
            padding: 5px 10px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 5px;
        }
        .download-status {
            color: #666;
            margin-left: 10px;
            font-size: 12px;
        }
    `);

    // 获取视频的 m3u8 地址
    async function getM3U8Url(videoPageUrl) {
        try {
            const response = await fetch(videoPageUrl);
            const html = await response.text();

            // 从页面提取参数
            const specMatch = html.match(/const spec = ({.*?});/);
            if (!specMatch) throw new Error('未找到视频参数');

            const spec = JSON.parse(specMatch[1]);
            const params = {
                identifier: spec.videoTitleId,
                TOTP: `account_id=${spec.accountId}&app_id=${spec.appId}&object_id=${spec.objectId}&token=${spec.token}&usage_group_id=${spec.usageGroupId}`
            };

            // 发送 GraphQL 请求获取 m3u8 地址
            const graphqlResponse = await fetch('https://video.alexanderstreet.com/api/graphql', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    query: `query ($identifier: String, $TOTP: String) {
                        readMediaObject(identifier: $identifier, TOTP: $TOTP) {
                            content {
                                ... on Video {
                                    media {
                                        file
                                    }
                                }
                            }
                        }
                    }`,
                    variables: params
                })
            });

            const data = await graphqlResponse.json();
            const mediaList = data.data.readMediaObject.content.media;
            if (!mediaList || !mediaList.length) throw new Error('未找到媒体文件');

            return mediaList[0].file;
        } catch (error) {
            console.error('获取M3U8地址失败:', error);
            throw error;
        }
    }

    // 保存 m3u8 文件
    async function saveM3U8File(url, fileName) {
        try {
            const response = await fetch(url, {
                headers: {
                    'Accept': '*/*',
                    'Origin': 'https://www.proquest.com',
                    'Referer': 'https://www.proquest.com/'
                }
            });
            const content = await response.text();

            const blob = new Blob([content], { type: 'application/x-mpegURL' });
            const downloadUrl = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = downloadUrl;
            a.download = fileName;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(downloadUrl);

            return true;
        } catch (error) {
            console.error('保存M3U8文件失败:', error);
            throw error;
        }
    }

    // 添加视频下载按钮
    function addVideoDownloadButtons() {
        const resultItems = document.querySelectorAll('li.resultItem');
        resultItems.forEach(item => {
            const videoContainer = item.querySelector('.videoThumbnailContainer');
            if (!videoContainer || item.querySelector('.single-video-btn')) return;

            const titleElement = item.querySelector('.truncatedResultsTitle');
            let fileName = 'video.m3u8';
            if (titleElement) {
                fileName = titleElement.textContent.trim()
                    .replace(/[/\\?%*:|"<>]/g, '-')
                    .replace(/\s+/g, '_')
                    + '.m3u8';
            }

            const buttonContainer = document.createElement('div');
            buttonContainer.style.display = 'flex';
            buttonContainer.style.alignItems = 'center';

            const downloadBtn = document.createElement('button');
            downloadBtn.className = 'single-video-btn';
            downloadBtn.textContent = '下载视频';

            const status = document.createElement('span');
            status.className = 'download-status';

            buttonContainer.appendChild(downloadBtn);
            buttonContainer.appendChild(status);
            item.appendChild(buttonContainer);

            downloadBtn.onclick = async () => {
                try {
                    status.textContent = '获取视频地址...';
                    const videoLink = item.querySelector('a[href*="docview"]').href;
                    const m3u8Url = await getM3U8Url(videoLink);

                    status.textContent = '保存M3U8文件...';
                    await saveM3U8File(m3u8Url, fileName);

                    // 自动打开下载器
                    window.open(`https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({
                        m3u8: m3u8Url,
                        referer: location.href
                    })}`, '_blank');

                    status.textContent = '下载完成';
                    status.style.color = '#4CAF50';
                } catch (error) {
                    status.textContent = '下载失败: ' + error.message;
                    status.style.color = '#f44336';
                }
            };
        });
    }

    // 添加批量视频下载按钮
    function addBatchVideoDownloadButton() {
        if (document.querySelector('.download-video-all-btn')) return;

        const batchButton = document.createElement('button');
        batchButton.className = 'download-video-all-btn';
        batchButton.textContent = '批量下载视频';
        document.body.appendChild(batchButton);

        batchButton.onclick = async () => {
            const resultItems = document.querySelectorAll('li.resultItem');
            let delay = 0;

            for (const item of resultItems) {
                const videoContainer = item.querySelector('.videoThumbnailContainer');
                if (!videoContainer) continue;

                const status = item.querySelector('.download-status') || document.createElement('span');
                status.className = 'download-status';
                item.appendChild(status);

                const titleElement = item.querySelector('.truncatedResultsTitle');
                let fileName = `video_${Date.now()}.m3u8`;
                if (titleElement) {
                    fileName = titleElement.textContent.trim()
                        .replace(/[/\\?%*:|"<>]/g, '-')
                        .replace(/\s+/g, '_')
                        + '.m3u8';
                }

                setTimeout(async () => {
                    try {
                        status.textContent = '获取视频地址...';
                        const videoLink = item.querySelector('a[href*="docview"]').href;
                        const m3u8Url = await getM3U8Url(videoLink);

                        status.textContent = '保存M3U8文件...';
                        await saveM3U8File(m3u8Url, fileName);

                        status.textContent = '下载完成';
                        status.style.color = '#4CAF50';
                    } catch (error) {
                        status.textContent = '下载失败: ' + error.message;
                        status.style.color = '#f44336';
                    }
                }, delay);

                delay += 3000; // 每个下载间隔3秒
            }
        };
    }

    // 保留原有的 PDF 下载功能代码...
     // 下载单个文档
    async function downloadDocument(downloadUrl, statusElement, fileName) {
        try {
            if (statusElement) {
                statusElement.textContent = '准备下载...';
            }

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: downloadUrl,
                    responseType: 'blob',
                    onload: function(response) {
                        if (response.status === 200) {
                            const blob = new Blob([response.response], { type: 'application/pdf' });
                            const url = window.URL.createObjectURL(blob);
                            const a = document.createElement('a');
                            a.href = url;
                            a.download = fileName;
                            document.body.appendChild(a);
                            a.click();
                            window.URL.revokeObjectURL(url);
                            a.remove();

                            if (statusElement) {
                                statusElement.textContent = '下载完成';
                                statusElement.style.color = '#4CAF50';
                            }
                            resolve();
                        } else {
                            if (statusElement) {
                                statusElement.textContent = '下载失败: ' + response.status;
                                statusElement.style.color = '#f44336';
                            }
                            reject(new Error('下载失败: ' + response.status));
                        }
                    },
                    onerror: function(error) {
                        console.error('下载请求失败:', error);
                        if (statusElement) {
                            statusElement.textContent = '下载失败: 网络错误';
                            statusElement.style.color = '#f44336';
                        }
                        reject(error);
                    }
                });
            });
        } catch (error) {
            console.error('下载过程出错:', error);
            if (statusElement) {
                statusElement.textContent = '下载失败: ' + error.message;
                statusElement.style.color = '#f44336';
            }
            throw error;
        }
    }

    // 添加下载按钮到每个搜索结果
    function addDownloadButtons() {
        const resultItems = document.querySelectorAll('li.resultItem');
        resultItems.forEach(item => {
            // 检查是否已经添加了下载按钮

            if (item.querySelector('.single-download-btn')) {
                return;
            }

            const pdfLink = item.querySelector('a[href*="fulltextPDF"]');

            if (!pdfLink) return;
            const titleElement = item.querySelector('.truncatedResultsTitle');
            let fileName = 'document.pdf';
            if (titleElement) {
                // 获取标题文本,去除首尾空白,并替换不允许作为文件名的字符
                fileName = titleElement.textContent.trim()
                    .replace(/[/\\?%*:|"<>]/g, '-') // 替换不允许的字符为连字符
                    .replace(/\s+/g, '_') // 将空白字符替换为下划线
                    + '.pdf'; // 添加 .pdf 后缀
            }


            const buttonContainer = document.createElement('div');
            buttonContainer.style.display = 'flex';
            buttonContainer.style.alignItems = 'center';

            const downloadBtn = document.createElement('button');
            downloadBtn.className = 'single-download-btn';
            downloadBtn.textContent = '下载文档';

            const status = document.createElement('span');
            status.className = 'download-status';

            buttonContainer.appendChild(downloadBtn);
            buttonContainer.appendChild(status);
            item.appendChild(buttonContainer);

            downloadBtn.onclick = async () => {
                try {
                    const pdfPageUrl = pdfLink.href;
                    console.log("pdfPageUrl", pdfPageUrl)
                    const response = await fetch(pdfPageUrl);
                    const html = await response.text();
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(html, 'text/html');
                    const downloadLink = doc.querySelector('a.pdf-download[download="ProQuestDocument.pdf"]');
                    if (downloadLink) {
                        await downloadDocument(downloadLink.href, status, fileName);
                    } else {
                        throw new Error('找不到PDF下载链接');
                    }
                } catch (error) {
                    console.error('单个文档下载失败:', error);
                    status.textContent = '下载失败: ' + error.message;
                    status.style.color = '#f44336';
                }
            };
        });
    }

    // 添加批量下载按钮
    function addBatchDownloadButton() {
        if (document.querySelector('.download-all-btn')) {
            return;
        }

        const batchButton = document.createElement('button');
        batchButton.className = 'download-all-btn';
        batchButton.textContent = '批量下载文档';
        document.body.appendChild(batchButton);

        batchButton.onclick = async () => {
            const resultItems = document.querySelectorAll('li.resultItem');
            let delay = 0;

            for (const item of resultItems) {
                const pdfLink = item.querySelector('a[href*="fulltextPDF"]');
                console.log(pdfLink)
                if (!pdfLink) continue;

                const status = item.querySelector('.download-status') || document.createElement('span');
                status.className = 'download-status';
                item.appendChild(status);

                const titleElement = item.querySelector('.truncatedResultsTitle');
                let fileName = 'document.pdf';
                if (titleElement) {
                    // 获取标题文本,去除首尾空白,并替换不允许作为文件名的字符
                    fileName = titleElement.textContent.trim()
                        .replace(/[/\\?%*:|"<>]/g, '-') // 替换不允许的字符为连字符
                        .replace(/\s+/g, '_') // 将空白字符替换为下划线
                        + '.pdf'; // 添加 .pdf 后缀
                }

                setTimeout(async () => {
                    try {
                        const pdfPageUrl = pdfLink.href;
                        const response = await fetch(pdfPageUrl);
                        const html = await response.text();
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(html, 'text/html');
                        const downloadLink = doc.querySelector('a.pdf-download[download="ProQuestDocument.pdf"]');
                        if (downloadLink) {
                            await downloadDocument(downloadLink.href, status, fileName);
                        } else {
                            throw new Error('找不到PDF下载链接');
                        }
                    } catch (error) {
                        console.error('批量下载过程中出错:', error);
                        status.textContent = '下载失败: ' + error.message;
                        status.style.color = '#f44336';
                    }
                }, delay);

                delay += 2000; // 每个下载间隔2秒
            }
        };
    }

    // 监听页面变化
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                addDownloadButtons();
                addVideoDownloadButtons();
                addBatchDownloadButton();
                addBatchVideoDownloadButton();
            }
        }
    });

    // 初始化
    function initialize() {
        addDownloadButtons();
        addVideoDownloadButtons();
        addBatchDownloadButton();
        addBatchVideoDownloadButton();
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // 页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();