動畫瘋下載器

取得動畫的 m3u8 網址,並可使用 PotPlayer、MPV 播放,ffmpeg下載

// ==UserScript==
// @name        動畫瘋下載器
// @namespace
// @description 取得動畫的 m3u8 網址,並可使用 PotPlayer、MPV 播放,ffmpeg下載
// @version     1.7.4
// @author      XPRAMT
// @match       https://ani.gamer.com.tw/animeVideo.php?sn=*
// @connect     ani.gamer.com.tw
// @grant       none
// @namespace 
// ==/UserScript==
(function () {
    'use strict';
    ///////////全域變數///////////
    const mode = 0; //複製模式(0:複製完整指令|1:複製URL+名稱)
    const timeOut = 600;
    let m3u8_url = '';
    let Name = '';
    // 讀取已儲存的路徑,若無則使用預設
    let downloadPath = localStorage.getItem('anig_download_path') || '%USERPROFILE%/Downloads';
    // 從 URL 中獲取動畫的編號(AniVideoSn)
    let AniVideoSn = new URLSearchParams(window.location.search).get('sn');
    ////////////////////////////
    // 注入樣式到頁面中
    function injectStyles() {
        // 定義樣式
        const css =`
			.anig-ct {
				margin:5px;
				margin-left:17px;
			}
			.anig-tb {
				display: inline-block;
				padding: 5px;
				background: #50b2d7;
				color: #FFF;
				margin-right: 5px;
				cursor: pointer;
		}`;
        const style = document.createElement('style'); // 創建 <style> 元素
       style.textContent = css; // 設置樣式內容
        document.head.appendChild(style); // 將 <style> 添加到 <head>
    }
    //初始化InfoContainer
    function initInfoContainer() {
        //資訊顯示框
        infoDisplay.classList.add('anig-tb');
        infoDisplay.title = '打開Greasy Fork頁面'
        infoDisplay.addEventListener('click', function () {
            window.open('https://greasyfork.org/zh-TW/scripts/451695-動畫瘋下載器', '_blank');
        })
        infoContainer.appendChild(infoDisplay);
        // 輸入框
        const input = document.createElement('input');
        input.type = 'text';
        input.value = downloadPath;
        input.placeholder = '下載路徑';
        input.style.cssText = `
            margin-right: 5px;
            padding: 3px;
            background-color: #000;  /* 黑底 */
            color: #fff;             /* 白字 */
            border: None;
            display: none;           /* 預設隱藏 */
        `;
        // 儲存按鈕
        const path_btn = document.createElement('buttonSave');
        let def_text = '下載路徑'
        path_btn.textContent = def_text; // 初始文字
        path_btn.title = '修改下載路徑'
        path_btn.classList.add('anig-tb');
        let inputVisible = false; // 狀態旗標
        path_btn.addEventListener('click', () => {
            if (!inputVisible) {
                // 顯示輸入框,按鈕顯示 "儲存"
                input.size = input.value.length || 10;
                input.style.display = 'inline';
                path_btn.textContent = '儲存';
                input.focus();
                inputVisible = true;
            } else {
                // 儲存資料,隱藏輸入框,按鈕顯示 "修改路徑"
                downloadPath = input.value.trim() || downloadPath;
                localStorage.setItem('anig_download_path', downloadPath);
                input.style.display = 'none';
                path_btn.textContent = def_text;
                inputVisible = false;
            }
        });
        infoContainer.appendChild(path_btn);
        infoContainer.appendChild(input);
    }
    // 獲取設備 ID,這是請求播放列表所需的參數
    async function getDeviceId() {
        // 清空容器
        m3u8Container.innerHTML = '';
        playContainer.innerHTML = '';
        infoDisplay.textContent = '載入中...';
        const req = 'https://ani.gamer.com.tw/ajax/getdeviceid.php'; // 請求設備 ID 的 URL
        const response = await fetch(req); // 發送請求
        const data = await response.json(); // 解析 JSON 資料
        let DeviceID = data.deviceid; // 提取設備 ID
        getPlaylist(DeviceID); // 繼續獲取播放列表
    }
    // 獲取播放列表,並等待廣告結束
    async function getPlaylist(DeviceID) {
        const req = `https://ani.gamer.com.tw/ajax/m3u8.php?sn=${AniVideoSn}&device=${DeviceID}`; // 構建請求 URL
        infoDisplay.textContent = '等待廣告...'; // 提示使用者等待廣告
        let retries = 0; // 重試次數計數器
        let playlist;
        const maxRetries = 20; // 最多嘗試次數,防止無限循環
        // 循環請求播放列表,直到獲取到有效的播放地址或達到最大重試次數
        while (retries < maxRetries) {
            const response = await fetch(req); // 發送請求
            playlist = await response.json(); // 解析 JSON 資料
            // 如果獲取到有效的播放地址(不包含廣告)
            if (playlist.src && playlist.src.includes('https')) {
                break; // 跳出循環
            }
            await new Promise(resolve => setTimeout(resolve, 3000)); // 等待 3 秒再重試
            retries++; // 增加重試次數
        }
        // 判斷是否成功獲取播放列表
        if (playlist.src && playlist.src.includes('https')) {
            await parsePlaylist(playlist); // 解析播放列表並生成按鈕
        } else {
            infoDisplay.textContent = '獲取播放列表失敗'; // 提示使用者失敗
        }
    }
    // 解析 m3u8 播放列表,並在頁面上生成按鈕供使用者複製鏈接或使用 PotPlayer 播放
    async function parsePlaylist(playlist) {
        const req = playlist.src; // 獲取播放列表的 URL
        const response = await fetch(req); // 請求播放列表
        const text = await response.text(); // 獲取回應的文字內容
        const urlPrefix = req.replace(/playlist.+/, ''); // 提取 URL 前綴
        const m3u8List = text.match(/=\d+x\d+\n.+/g); // 匹配所有清晰度的 m3u8 連結
        // 生成動畫名稱,作為文件名使用
        const fullwidthMap = {
            '<': '<','>': '>',':': ':','"': '"','/': '/',
            '\\': '\','|': '|','?': '?','!': '!','*': '*'
        };
        Name = document.title.replace(" 線上看 - 巴哈姆特動畫瘋", "");
        Name = Name.replace(/[\/\\<>:"*|?!]/g, char => {
            return fullwidthMap[char] || '_';
        });
        // 遍歷每個 m3u8 連結,生成對應的按鈕
        for (const item of m3u8List) {
            let key = item.match(/=\d+x(\d+)/)[1]; // 提取清晰度(如 720)
            let url = item.match(/.*chunklist.+/)[0]; // 提取 m3u8 文件的相對路徑
            url = urlPrefix + url; // 拼接成完整的 m3u8 URL
            // 創建複製鏈接的按鈕
            const copyLink = document.createElement('a');
            copyLink.classList.add('anig-tb');
            copyLink.textContent = `${key}p`;
            copyLink.title = `切換解析度為${key}p`;
            // 被點擊時
            copyLink.addEventListener('click', function () {
                m3u8_url = url; // 更新URL
                m3u8Container.querySelectorAll('a.anig-tb').forEach(el => {
                    el.textContent = el.textContent.replace(/✅\s*$/, '');
                });
                copyLink.textContent = `${key}p✅`;
            });
            m3u8Container.appendChild(copyLink);
        }
        // 主動點擊最後一個項目
        const allBtns = m3u8Container.querySelectorAll('a.anig-tb');
        if (allBtns.length > 0) {
            allBtns[allBtns.length - 1].click();
        }
        infoDisplay.textContent = '使用說明'; //${Name}
        initPlayContainer()
    }
    //初始化playContainer
    function initPlayContainer() {
        // 創建使用 MPV 播放的按鈕
        const MPVLink = document.createElement('a');
        MPVLink.classList.add('anig-tb');
        MPVLink.textContent = 'MPV';
        MPVLink.title = `使用MPV播放: ${Name}`;
        MPVLink.addEventListener('click', function () {
            const MPVUrl = `${m3u8_url} --http-header-fields="origin: https://ani.gamer.com.tw" --force-media-title="${Name}"`; // 構建 MPV 協議的 URL
            navigator.clipboard.writeText(MPVUrl);
            window.open('mpv:', '_self'); // 開啟 PotPlayer
        });
        playContainer.appendChild(MPVLink);
        // 創建使用 PotPlayer 播放的按鈕
        const potplayerLink = document.createElement('a');
        potplayerLink.classList.add('anig-tb');
        potplayerLink.textContent = 'PotPlayer';
        potplayerLink.title = `使用PotPlayer播放: ${Name}`;
        potplayerLink.addEventListener('click', function () {
            const potplayerUrl = `${m3u8_url} /sub="" /headers="origin: https://ani.gamer.com.tw" /current /title="${Name}"`; // 構建 PotPlayer 協議的 URL
            navigator.clipboard.writeText(potplayerUrl);
            window.open('potplayer:', '_self'); // 開啟 PotPlayer
        });
        playContainer.appendChild(potplayerLink);
        // 創建使用 ffmpeg 下載的按鈕
        const ffmpegLink = document.createElement('a');
        ffmpegLink.classList.add('anig-tb');
        ffmpegLink.textContent = 'ffmpeg';
        ffmpegLink.title = `使用ffmpeg下載: ${Name}`;
        ffmpegLink.addEventListener('click', function () {
            let PSdownloadPath = downloadPath.replace(/%([^%]+)%/g, '$Env:$1');
            const ffmpegDlUrl = `& ffmpeg -headers "Origin: https://ani.gamer.com.tw" -i "${m3u8_url}" -c copy "${PSdownloadPath}/${Name}.mkv";`; // 構建 PotPlayer 協議的 URL
            navigator.clipboard.writeText(ffmpegDlUrl);
            window.open('ffmpeg:', '_self'); // 開啟 ffmpeg
        });
        playContainer.appendChild(ffmpegLink);
        // 創建複製 ffmpeg 指令的按鈕
        const CopyffmpegLink = document.createElement('a');
        CopyffmpegLink.classList.add('anig-tb');
        let def_Copy_text = 'ffmpeg 🗍'
        CopyffmpegLink.textContent = def_Copy_text;
        CopyffmpegLink.title = `複製ffmpeg指令: ${Name}`;
        CopyffmpegLink.addEventListener('click', function () {
            let ffmpegUrl
            if (mode==0){
                ffmpegUrl = `ffmpeg -headers "Origin: https://ani.gamer.com.tw" -i "${m3u8_url}" -c copy "${downloadPath}/${Name}.mkv" && exit`; // 構建 PotPlayer 協議的 URL
            }else{
                ffmpegUrl = `${m3u8_url}@${Name}.mkv"`; // 構建 PotPlayer 協議的 URL
            }
            navigator.clipboard.writeText(ffmpegUrl);
            CopyffmpegLink.textContent = 'ffmpeg ✅';
            setTimeout(() => {
                CopyffmpegLink.textContent = def_Copy_text; // 恢復提示文字
            }, timeOut);
        });
        playContainer.appendChild(CopyffmpegLink);
    }
    ////////////Main////////////
    // 注入自定義樣式到頁面
    injectStyles();
    // 將自訂容器添加到頁面中的指定位置
    const Downloader = document.createElement('div');
    document.querySelector('.videoname').appendChild(Downloader);
    // 創建顯示提示信息的元素
    const infoDisplay = document.createElement('div');
    const infoContainer = document.createElement('div');
    infoContainer.classList.add('anig-ct');
    initInfoContainer();
    Downloader.appendChild(infoContainer);
    // 創建容器,用於放置清晰度
    const m3u8Container = document.createElement('div');
    m3u8Container.classList.add('anig-ct');
    Downloader.appendChild(m3u8Container);
    // 創建容器,用於放置播放下載按鈕
    const playContainer = document.createElement('div');
    playContainer.classList.add('anig-ct');
    Downloader.appendChild(playContainer);
    // 為頁面中的集數連結添加點擊事件監聽
    // 當使用者點擊不同的集數時,更新 AniVideoSn 並重新獲取播放列表
    let lastAniVideoSn = AniVideoSn; // 保存上一次的 AniVideoSn
    document.querySelectorAll('a[data-ani-video-sn]').forEach(link => {
        link.addEventListener('click', function () {
            let it = setInterval(() => {
                AniVideoSn = new URLSearchParams(window.location.search).get('sn'); // 獲取當前的 AniVideoSn
                // 如果 URL 或 AniVideoSn 發生變化
                if (AniVideoSn != lastAniVideoSn) {
                    lastAniVideoSn = AniVideoSn; // 更新 AniVideoSn
                    getDeviceId(); // 重新獲取設備 ID 並獲取播放列表
                    clearInterval(it);
                }
            }, 100);
        });
    });
    // 開始執行程式
    getDeviceId(); // 獲取設備 ID 並開始流程
})();