// ==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;
}
`);
}
})();