Viu More

显示已过期的集数,尝试提供下载功能

目前為 2024-03-21 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Viu More
// @namespace    http://tampermonkey.net/
// @version      0.4.3
// @description  显示已过期的集数,尝试提供下载功能
// @author       cw2012
// @match        https://viu.tv/encore/*
// @icon         https://www.viu.com/ott/hk/v1/images/web_loading_icon.gif
// @require      https://cdn.jsdelivr.net/npm/[email protected]
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_setClipboard
// @connect      viu.tv
// @connect      api.viu.now.com
// @connect      now.com
// @connect      nowe.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    let totalCount, isAsc, seasonTitle, subtitles=[],clips = [];

    let seasonName;
    const cookie = 'b13b2e6a06a230f8b2'; // f7da2aac5e3df01adc

    addStyle();
    const msgBox = document.createElement('div');
    msgBox.id = 'msg-box';
    document.querySelector('#page-wrap').append(msgBox);
    init();

    function init(){
        const btn = document.createElement('div');
        btn.innerText = '解 析';
        btn.className = 'floating-btn';
        btn.addEventListener('click', ev=>{
            getSeasonEposideList();
        });
        document.body.append(btn);
    }

    function getSeasonEposideList(){
        seasonName = location.href.split('/')[4];
        subtitles=[];
        if(typeof document.querySelector('#page-wrap') == "undefined"){
            showMsg('请在页面加载完成后重试',0);
            return;
        }
        GM_xmlhttpRequest({
            method:'GET',
            url: `https://api.viu.tv/production/programmes/${seasonName}`,
            responseType: 'json',
            onerror:e=>{showMsg(`请求发生错误:${e}`,0)},
            onload:res=>{
                res = res.response.programme;
                totalCount = res.programmeMeta.totalEpisodeNo;
                seasonTitle = res.programmeMeta.seriesTitle;
                // 预告片
                if(res.clips.length > 0 && res.episodes.length === 0){
                    for(let item of res.clips){
                        clips.push({
                            productId: item.productId,
                            episodeNum:seasonTitle,
                            episodeNameU3: '预告片',
                            productSubtitle: ''
                        });
                    }
                    updateUiEpisodeList(clips);
                }
                else if(totalCount !== res.episodes.length){
                    // 只显示一集,说明是最后一集
                    // 这里一定是总集数和显示集数不一致才会被调用的
                    if(res.episodes.length > 1){
                        isAsc = res.episodes[0].episodeNum < res.episodes[1].episodeNum;
                    }else{
                        isAsc = false;
                    }
                    setTimeout(()=>{
                        updateUiEpisodeList(res.episodes);
                        showOutdatedEpisodeList(res.episodes);}
                               , 1000);
                }else{
                    setTimeout(()=>updateUiEpisodeList(res.episodes), 2000);
                }
            }
        });
    }

    function updateUiEpisodeList(list){
        // 先操作已显示的列表
        const listBox = document.querySelector('.Episodes');
        const shownEpisode = listBox.querySelectorAll('.VideoItem.undefined');
        shownEpisode.forEach((item, index)=>{
            let div = document.createElement('div');
            div.className = 'floating-div';
            div.innerText = '下载字幕,并复制MPD文件的url';
            div.addEventListener('click', ev=>{
                window.event? window.event.cancelBubble = true : ev.stopPropagation();
                const productId = list[index].productId;
                getSubtitleWithProductId(productId, list[index].episodeNum + ' ' + list[index].episodeNameU3, list[index].productSubtitle);
            });
            item.append(div);
        });

        setTimeout(()=>{
            const li = document.createElement('li');
            li.innerText = `共${totalCount}集`;
            li.className = 'react-tabs__tab-list';
            document.querySelector('.react-tabs__tab-list').append(li);
        },500);
    }

    function showOutdatedEpisodeList(list){
        const listBox = document.querySelector('.Episodes');
        // 添加因过期而未能显示的列表
        let len2add, firstProductId,firstEpisodeNum;
        // 正片
        if(isAsc){
            len2add = list[0].episodeNum>15?15:(list[0].episodeNum-1);
            firstProductId = parseInt(list[0].productId);
            firstEpisodeNum = list[0].episodeNum;
            let prevDiv;
            for(let i=0;i<len2add;i++){
                const div = createOutdateEpisode(firstProductId-(i+1), firstEpisodeNum - (i+1), list[0].productSubtitle);
                if(i==0){
                    listBox.insertBefore(div, listBox.firstChild);
                    prevDiv = div;
                }else{
                    listBox.insertBefore(div, prevDiv);
                    prevDiv = div;
                }
            }
        }else{
            if(list.length === 0) {
                return;
            }
            len2add = list[list.length-1].episodeNum>15?15:(list[list.length -1].episodeNum-1);
            firstProductId = parseInt(list[list.length-1].productId);
            firstEpisodeNum = list[list.length-1].episodeNum;
            for(let i=0;i<len2add;i++){
                const div = createOutdateEpisode(firstProductId-(i+1), firstEpisodeNum - (i+1), list[0].productSubtitle);
                listBox.append(div);
            }
        }
    }

    function createOutdateEpisode(id, num, subtitleList){
        let div = document.createElement('div');
        div.className = 'VideoItem outdated_episode'; //
        div.innerText =`下载第${sn(num,2)}集字幕并复制MPD文件的url`;
        div.addEventListener('click',ev=>{
            getSubtitleWithProductId(id, num, subtitleList);
        });
        return div;
    }

    function getSubtitleWithProductId(id, episodeNum, subtitleList){
        subtitleList = subtitleList.split(',');
        subtitles=[];
        subtitleList.forEach(item=>{
            if(item == 'Chinese'){
                subtitles.push('TRD');
            }else if(item == 'English'){
                subtitles.push('GBR');
            }
        });
        GM_xmlhttpRequest({
            method:'POST',
            url: 'https://api.viu.now.com/p8/3/getVodURL',
            headers: {
                'accept':'*/*',
                'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 Edg/92.0.902.62'},
            // {"callerReferenceNo":"20240321224650","productId":"202403201906015","contentId":"202403201906015","contentType":"Vod","deviceType":"ANDROID_WEB"}
            data: JSON.stringify({"callerReferenceNo":getTimeStamp(new Date()),"productId":id,"contentId":id,
                                  "contentType":"Vod","mode":"prod","deviceType":"ANDROID_WEB"}),
            onerror:e=>{showMsg('获取字幕时出错:'+e,0)},
            onload:res=>{
                res = JSON.parse(res.responseText);
                switch(res.responseCode){
                    case "MISSING_INPUT":
                        showMsg('输入的参数不对',0);
                        break;
                    case "GEO_CHECK_FAIL":
                        showMsg('IP不是香港的',0);
                        break;
                    case "INTERNAL_ERROR":
                        showMsg("发生错误,该视频可能已经永久下架了",0);
                        break;
                    case "SUCCESS":
                        var videoUrl = res.asset[0];
                        var m3u8Index = videoUrl.indexOf('.m3u8');
                        GM_setClipboard(`N_m3u8DL-RE.exe -mt --use-shaka-packager --tmp-dir "Downloads" --mux-after-done "format=mp4" --save-name "${episodeNum}" "${videoUrl}"`);
                        showMsg(`${m3u8Index > -1?'m3u8':'MPD'}文件的url已复制成功`,1);
                        downloadSubtitles(id,episodeNum, videoUrl, m3u8Index);
                        break;
                }
            },
            ontimeout:e=>showMsg('呵呵,超时了',0)
        });
    }

    function getTimeStamp(date){
        const timeZone = date.getTimezoneOffset() / 60;
        date.setTime(date.getTime() - timeZone * 3600 * 1000);
        return date.toISOString().replaceAll(/[-T:Z.]/g,'').substr(0,14);
    }

    const langName = {TRD:"zh", GBR:"en"};
    function downloadSubtitles(id,episodeNum, videoUrl, m3u8Index){
        subtitles.forEach(item=>{
            const url = m3u8Index > -1? videoUrl.substr(0,m3u8Index) + `-${item}.srt`:`https://static.viu.tv/subtitle/${id}/${id}-${item}.srt`;
            GM_download({
                url: url,
                headers:{origin:'https://viu.tv',referer:'https://viu.tv/'},
                name:`${sn(episodeNum,2)}.${langName[item]}.srt`,
                onerror:e=>showMsg(`${sn(episodeNum,2)}.${langName[item]}.srt  下载失败\n${url}`,0)
            })
        });
    }

    function showMsg(msg,type){
        msgBox.innerText = msg;
        msgBox.className=type?'showing':'err';
        setTimeout(()=>{msgBox.className='';},type==0?4000: 2500);
        console.log(msg);
    }

    function sn(num,length){
        return num.toString().padStart(length, '0');
    }

    function addStyle(){
        GM_addStyle(`
        .floating-btn{
        position:fixed;
        background: #0a7deb;
        text-align: center;
        color:white;
        font-size:1.5em;
        cursor: pointer;
        border-radius:10px;
        border:solid #0a7deb 1px;
        padding:6px;
        top:50%;
        right:20px;
        box-shadow:#0a7deb 2px 2px 6px, #0a7deb 6px 6px 19px;
        }
        .floating-div{
        position:relative;
        background: #0a7deb;
        text-align: center;
        color:white;
        cursor: pointer;
        border-radius:10px;
        border:solid #0a7deb 1px;
        padding:6px;
        display:none;
        }
        .VideoItem.undefined:hover .floating-div{
        display:block;
        }
        #msg-box{
        transition:all 0.5s ease-in-out;
        font-size:15px;
        position:fixed;
        right:30px;
        top:10px;
        background: #0a7deb;
        color:white;
        border-radius:7px;
        padding:10px;
        opacity:0;
        box-shadow:#0a7deb 2px 2px 6px, #0a7deb 6px 6px 19px;
        }
        #msg-box.showing{
        opacity:1;
        top:130px;
        }
        #msg-box.err{
        background:red;
        box-shadow:red 2px 2px 6px, red 6px 6px 19px;
        opacity:1;
        top:130px;
        }
        .VideoItem.outdated_episode{
        text-align: center;
        background:#0a7deb;
        border-radius:10px;
        border:solid #0a7deb 1px ;
        padding:6px;
        margin:10px;
        color:white;
            cursor: pointer;
        }
        `);
    }
})();