Viu More

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

当前为 2021-08-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

(function() {
    'use strict';

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

    const seasonName = location.href.split('/')[4];
    const cookie = 'b13b2e6a06a230f8b2'; // f7da2aac5e3df01adc


    const notificationAvailable = "Notification" in window;
    addStyle();
    const msgBox = document.createElement('div');
    msgBox.id = 'msg-box';
    document.querySelector('#page-wrap').append(msgBox);

    const observer = new MutationObserver(getSeasonEposideList);
    observer.observe(document.querySelector('#outer-container'), {
        attributes: false,
        childList: true,
        subtree: false
    });


    function getSeasonEposideList(){
        if(typeof document.querySelector('#page-wrap') == "undefined"){
            return;
        }
        observer.disconnect();
        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(totalCount !== res.episodes.length){
                    // 只显示一集,说明是最后一集
                    // 这里一定是总集数和显示集数不一致才会被调用的
                    for(let i=0;i<res.episodes.length;i++){
                        episodeList.push({episodeNum:res.episodes[i].episodeNum,productId:res.episodes[i].productId});
                    }
                    if(res.episodes.length === 1){
                        isAsc = false;
                    }else{
                        isAsc = res.episodes[0].episodeNum < res.episodes[1].episodeNum;
                    }
                    setTimeout(()=>updateUiEpisodeList(res.episodes), 1000);
                }
            }
        });
    }

    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, episodeList[index].episodeNum);
            });
            item.append(div);
        });

        // 添加因过期而未能显示的列表
        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));
                if(i==0){
                    listBox.insertBefore(div, listBox.firstChild);
                    prevDiv = div;
                }else{
                    listBox.insertBefore(div, prevDiv);
                    prevDiv = div;
                }
            }
        }else{
            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));
                listBox.append(div);
            }
        }
    }

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

    function getSubtitleWithProductId(id, episodeNum){
        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'},
            data: JSON.stringify({"callerReferenceNo":getTimeStamp(new Date()),"productId":id,"contentId":id,
                                  "contentType":"Vod","mode":"prod","PIN":"password","cookie":cookie,"deviceId":"U5e83045b551442088","deviceType":"ANDROID_WEB","format":"HLS"}),
            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":
                        GM_setClipboard(res.asset[0]);
                        showMsg("MPD文件的url已复制成功",1);
                        GM_xmlhttpRequest({
                            method:'GET',
                            url: res.asset[0],
                            onload:r=>{
                                r = r.responseXML;
                                if(typeof r.childNodes[0].childNodes[1].childNodes !='undefined'){
                                    r = r.childNodes[0].childNodes[1].childNodes;
                                    r.forEach(item=>{
                                        if(item.tagName == 'AdaptationSet' && ("text/vtt" ==item.getAttribute('mimeType'))){
                                            subtitles.push(item.getAttribute('lang'));
                                        }
                                    });
                                    if(subtitles.length>0){
                                        downloadSubtitles(id,episodeNum);
                                    }
                                }else{
                                    showMsg('该视频没有字幕',1);
                                }
                            }
                        });
                        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 langList = {yue:"TRD", eng:"GBR"};
    const langName = {yue:"zh", eng:"en"};
    function downloadSubtitles(id,episodeNum){
        subtitles.forEach(item=>{
            GM_download({
                // https://static.viu.tv/subtitle/202104211351468/202104211351468-TRD.srt
                url: `https://static.viu.tv/subtitle/${id}/${id}-${langList[item]}.srt`,
                name:`${sn(episodeNum,2)}.${langName[item]}.srt`,
                onerror:e=>showMsg(`${sn(episodeNum,2)}.${langName[item]}.srt\nhttps://static.viu.tv/subtitle/${id}/${id}-${langList[item]}.srt下载失败`,0)
            })
        });
    }

    function showMsg(msg,type){
        if(!notificationAvailable){
            alert(msg);
        }else{
            msgBox.innerText = msg;
            msgBox.className=type?'showing':'err';
            setTimeout(()=>{msgBox.className='';}, 3000);
        }
    }

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

    function addStyle(){
        GM_addStyle(`.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;
        }
        `);
    }
})();