Greasy Fork is available in English.

動畫瘋下載器

取得動畫的 m3u8 網址,並可使用 PotPlayer 播放

// ==UserScript==
// @name        動畫瘋下載器
// @namespace
// @description 取得動畫的 m3u8 網址,並可使用 PotPlayer 播放
// @version     1.6.4
// @author
// @match       https://ani.gamer.com.tw/animeVideo.php?sn=*
// @connect     ani.gamer.com.tw
// @grant       none
// @namespace 
// ==/UserScript==

(function () {
    'use strict';
    //保存路徑
    const path = '%USERPROFILE%/Downloads'
    //複製模式(0:複製完整指令|1:複製URL+名稱)
    const mode = 0;

     //注入樣式到頁面中
    function injectStyles(css) {
        const style = document.createElement('style'); // 創建 <style> 元素
        style.textContent = css; // 設置樣式內容
        document.head.appendChild(style); // 將 <style> 添加到 <head>
    }

     //解析 m3u8 播放列表,並在頁面上生成按鈕供使用者複製鏈接或使用 PotPlayer 播放
    async function parsePlaylist() {
        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 Name = document.title.replace(" 線上看 - 巴哈姆特動畫瘋", "").replace(/[\/:*?"<>|]/g, '_');
        titleDisplay.textContent = '複製下載指令或使用外部播放器';

        let m3u8_url2
        // 遍歷每個 m3u8 連結,生成對應的按鈕
        for (const item of m3u8List) {
            let key = item.match(/=\d+x(\d+)/)[1]; // 提取清晰度(如 720)
            let m3u8_url = item.match(/.*chunklist.+/)[0]; // 提取 m3u8 文件的相對路徑
            m3u8_url = urlPrefix + m3u8_url; // 拼接成完整的 m3u8 URL
            m3u8_url2 = m3u8_url;

            // 創建複製鏈接的按鈕
            const copyLink = document.createElement('a');
            copyLink.classList.add('anig-tb');
            copyLink.textContent = `${key}p`;
            copyLink.title = '複製ffmpeg下載指令';
            copyLink.addEventListener('click', function () {
                let ffmpegUrl;
                if (mode==0){
                    ffmpegUrl = `ffmpeg -headers "Origin: https://ani.gamer.com.tw" -i "${m3u8_url}" -c copy "${path}/${Name}.mkv"`; // 構建 PotPlayer 協議的 URL
                }else{
                    ffmpegUrl = `${m3u8_url}@${Name}.mkv"`; // 構建 PotPlayer 協議的 URL
                }
                navigator.clipboard.writeText(ffmpegUrl); // 複製鏈接
                titleDisplay.textContent = '複製成功!'; // 提示成功
                setTimeout(() => {
                    titleDisplay.textContent = '複製下載指令或使用外部播放器'; // 恢復提示文字
                }, 500);
            });
            m3u8Container.appendChild(copyLink);
        }

        // 創建使用 MPV 播放的按鈕
        const MPVLink = document.createElement('a');
        MPVLink.classList.add('anig-tb');
        MPVLink.textContent = 'MPV';
        MPVLink.title = '使用 MPV 播放';
        MPVLink.addEventListener('click', function () {
            const MPVUrl = `${m3u8_url2} --http-header-fields="origin: https://ani.gamer.com.tw" --force-media-title="${Name}"`; // 構建 MPV 協議的 URL
            navigator.clipboard.writeText(MPVUrl);
            window.open('mpv:', '_self'); // 開啟 PotPlayer
        });
        m3u8Container.appendChild(MPVLink);

        // 創建使用 PotPlayer 播放的按鈕
        const potplayerLink = document.createElement('a');
        potplayerLink.classList.add('anig-tb');
        potplayerLink.textContent = 'PotPlayer';
        potplayerLink.title = '使用 PotPlayer 播放';
        potplayerLink.addEventListener('click', function () {
            const potplayerUrl = `${m3u8_url2} /sub="" /headers="origin: https://ani.gamer.com.tw" /current /title="${Name}"`; // 構建 PotPlayer 協議的 URL
            navigator.clipboard.writeText(potplayerUrl);
            window.open('potplayer:', '_self'); // 開啟 PotPlayer
        });
        m3u8Container.appendChild(potplayerLink);

    }

    /**
     * 獲取播放列表,並等待廣告結束
     */
    async function getPlaylist() {
        const req = `https://ani.gamer.com.tw/ajax/m3u8.php?sn=${AniVideoSn}&device=${DeviceID}`; // 構建請求 URL
        titleDisplay.textContent = '等待廣告...'; // 提示使用者等待廣告

        let retries = 0; // 重試次數計數器
        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(); // 解析播放列表並生成按鈕
        } else {
            titleDisplay.textContent = '獲取播放列表失敗'; // 提示使用者失敗
        }
    }

    /**
     * 獲取設備 ID,這是請求播放列表所需的參數
     */
    async function getDeviceId() {
        const req = 'https://ani.gamer.com.tw/ajax/getdeviceid.php'; // 請求設備 ID 的 URL
        const response = await fetch(req); // 發送請求
        const data = await response.json(); // 解析 JSON 資料
        DeviceID = data.deviceid; // 提取設備 ID
        await getPlaylist(); // 繼續獲取播放列表
    }

    // Main

    // 從 URL 中獲取動畫的編號(AniVideoSn)
    let AniVideoSn = new URLSearchParams(window.location.search).get('sn');
    // 定義全域變數
    let DeviceID; // 設備 ID
    let playlist; // 播放列表資料

    // 定義樣式
    const css =`
    .anig-ct {
        margin: 5px;
    }

    .anig-tb {
        display: inline-block;
        padding: 5px;
        background: #50b2d7;
        color: #FFF;
        margin-right: 5px;
        cursor: pointer;
    }`;

    // 創建顯示提示信息的元素
    const titleDisplay = document.createElement('div');
    titleDisplay.classList.add('anig-tb');
    titleDisplay.textContent = '載入中...'; // 初始提示文字
    // 創建一個容器來包裹提示信息
    const container = document.createElement('div');
    container.classList.add('anig-ct');
    container.appendChild(titleDisplay); // 將提示元素添加到容器中
    // 創建容器,用於放置清晰度按鈕和 PotPlayer 按鈕
    const m3u8Container = document.createElement('div');
    m3u8Container.classList.add('anig-ct');
    // 將容器添加到頁面中的指定位置
    const animeName = document.querySelector('.anime_name');
    animeName.appendChild(container); // 添加提示容器
    animeName.appendChild(m3u8Container); // 添加按鈕容器

    /**
     * 為頁面中的集數連結添加點擊事件監聽
     * 當使用者點擊不同的集數時,更新 AniVideoSn 並重新獲取播放列表
     */
    document.querySelectorAll('a[data-ani-video-sn]').forEach(link => {
        link.addEventListener('click', function () {
            AniVideoSn = this.getAttribute('data-ani-video-sn'); // 更新 AniVideoSn
            m3u8Container.innerHTML = ''; // 清空按鈕容器
            titleDisplay.textContent = '載入中...'; // 更新提示文字
            getDeviceId(); // 重新獲取設備 ID 並獲取播放列表
        });
    });

    /**
     * 新增檢查機制,監測 URL 和 AniVideoSn 是否發生變化
     * 如果發生變化,則重新獲取播放列表,確保資料的及時更新
     */
    let lastAniVideoSn = AniVideoSn; // 保存上一次的 AniVideoSn
    let lastUrl = window.location.href; // 保存上一次的 URL

    setInterval(() => {
        const currentUrl = window.location.href; // 獲取當前的 URL
        const currentAniVideoSn = new URLSearchParams(window.location.search).get('sn'); // 獲取當前的 AniVideoSn

        // 如果 URL 或 AniVideoSn 發生變化
        if (currentUrl !== lastUrl || currentAniVideoSn !== lastAniVideoSn) {
            lastUrl = currentUrl; // 更新 URL
            lastAniVideoSn = currentAniVideoSn; // 更新 AniVideoSn
            AniVideoSn = currentAniVideoSn; // 更新全域變數

            m3u8Container.innerHTML = ''; // 清空按鈕容器
            titleDisplay.textContent = '載入中...'; // 更新提示文字
            getDeviceId(); // 重新獲取設備 ID 並獲取播放列表
        }
    }, 1000); // 每秒檢查一次

    // 開始執行程式
    getDeviceId(); // 獲取設備 ID 並開始流程
    injectStyles(css); // 注入自定義樣式到頁面

})();