// ==UserScript==
// @name Extract DownloadURL
// @version 0.2.4.20201027
// @description mp4もしくはm3u8の動画URLを取得します。
// @namespace https://greasyfork.org/users/2671
// @include *
// @grant GM.xmlHttpRequest
// @noframes
// @run-at document-idle
// ==/UserScript==
const rules = [
{
"reg":/tver\.jp\/\w+\/f?\d+\/?$/
,"cssTitle":"div.inner>h1, div.inner>p>span.summary, div.inner>p>span>span.tv"
,"isBrightcove":true
}
,{
"reg":/gyao\.yahoo\.co\.jp\/(episode|player|title|p)\/.+/
,"cssTitle":"h1.video-title"
,"isBrightcove":true
}
,{
"reg":/cu\.ntv\.co\.jp\/.+?/
,"isBrightcove":true
}
,{
"reg":/tbs\.co\.jp\/muryou-douga\/.+?\/\d+\/.+/
,"isBrightcove":true
}
,{
"reg":/video\.tv-tokyo\.co\.jp\/.+?\/.+?\/\d+\.html$/
,"isBrightcove":true
}
,{
"reg":/dizm\.mbs\.jp\/title\/\?/
,"isBrightcove":true
}
,{
"reg":/ytv\.co\.jp\/mydo\/.+/
,"isBrightcove":true
}
,{
"reg":/ktv-smart\.jp\/.+?\/.+?\/index\.php\?key=\d+/
,"isBrightcove":true
}
,{
"reg":/jshow\.tv\/.+/
,"cssIflame":"div#player-embed>iframe"
,"isBrightcove":false
}
];
const userAgents = {
"ipadSafari":"Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
};
const css = {};
css.panel = "div#download_panel";
css.container = css.panel + ">div";
css.topcontent = "div#download_panel_topcontent";
css.bottomcontent = "div#download_panel_bottomcontent";
css.btn = "div.download_panel_btn";
css.brd = "div.download_panel_border";
css.m3u8btn = css.topcontent + ">div.download_panel_color_m3u8";
css.mp4btn = css.topcontent + ">div.download_panel_color_mp4";
css.errorbtn = css.topcontent + ">div.download_panel_color_error";
css.list = "div.download_panel_list";
css.m3u8list = css.bottomcontent + ">div.download_panel_color_m3u8";
css.mp4list = css.bottomcontent + ">div.download_panel_color_mp4";
css.errorlist = css.bottomcontent + ">div.download_panel_color_error";
const createNode = (tag, html, attrObj, node, type) => {
if (!html) html = "";
if (!attrObj) attrObj = {};
tag = document.createElement(tag);
tag.innerHTML = html;
Object.entries(attrObj).forEach((ary) => {
tag.setAttribute(ary[0], ary[1]);
});
if (!node) return tag;
switch (type) {
case undefined:
return node.appendChild(tag);
case "before":
return node.parentNode.insertBefore(tag, node);
case "after":
return node.parentNode.insertBefore(tag, node.nextElementSibling);
};
return (node) ? node.appendChild(tag) : tag;
};
const addStyle = (ary) => {
ary.forEach((css) => {createNode("style", css, {"type":"text/css"}, document.getElementsByTagName("head")[0])});
};
const XmlHttpRequest = class {
static get(url, headers = {}) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method:"GET"
,url:url
,headers:headers
,onload:(response) => {
if (response.status==200) {
resolve(response.responseText);
} else {
reject(url + "からのURL取得に失敗しました。");
}
}
});
});
}
static redirect(url, headers = {}) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method:"GET"
,url:url
,headers:headers
,onload:(response) => {
if (!/40\d/.test(response.status)) {
resolve(response.finalUrl);
} else {
reject(url + "からのURL取得に失敗しました。");
}
}
});
});
}
static check(url, headers = {}) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method:"HEAD"
,url:url
,headers:headers
,onload:(response) => {resolve(response.status==200)}
});
});
}
};
const CreateURL = class {
constructor() {
this.url = location.href;
this.rule = rules.find((obj) => {return obj.reg.test(this.url)}) || {};
this.isBrightcove = this.rule.isBrightcove || false;
let title = document.querySelector("head>title");
this.title = (title) ? title.textContent.trim() : "title";
if (this.rule.cssTitle) {
let nodes = Array.from(document.querySelectorAll(this.rule.cssTitle));
if (nodes.length!=0) this.title = nodes.map((node) => {return node.textContent}).join(" ");
};
}
createBrightcoveObj() {
let video = document.querySelector("video");
if (!video) return null;
return [video, video.parentNode].map((node) => {
let pubId = node.getAttribute("data-account") || "";
let videoId = node.getAttribute("data-video-id") || "";
let playerId = node.getAttribute("data-player") || "";
return (/^\d+$/.test(pubId) && /^\d+$/.test(videoId) && /^\w+$/.test(playerId)) ? {"pubId":pubId, "videoId":videoId, "playerId":playerId} : null;
}).find((obj) => {return obj!=null});
}
searchBrightcoveNode() {
let obj = this.createBrightcoveObj();
if (obj) return Promise.resolve(obj);
return new Promise((resolve, reject) => {
console.log("Extract DownloadURL:MutationObserver開始");
new MutationObserver((mutations, ins) => {
let obj = this.createBrightcoveObj();
if (!obj) return null;
console.log("Extract DownloadURL:MutationObserver切断");
ins.disconnect();
resolve(obj);
}).observe(document.getElementsByTagName("body")[0], {childList:true, subtree:true, attributes:true});
});
}
async getKey(obj) {
const url = "https://players.brightcove.net/" + obj.pubId + "/" + obj.playerId + "_default/index.min.js";
let html = await XmlHttpRequest.get(url).catch((mes) => {return ""});
return /policyKey:"(.+?)"/.test(html) ? RegExp.$1 : null;
}
async isGyaoDrm() {
let json = await XmlHttpRequest.get("https://gyao.yahoo.co.jp/dam/v1/videos/" + this.url.replace(/^http.+\/([\w\-]+)\/?$/, "$1") + "?fields=drm").catch(() => {return "{}"});
json = JSON.parse(json);
return json["drm"] || false;
}
async isDrm() {
let bool = false;
if (this.url.indexOf("gyao.yahoo.co.jp") > -1) bool = await this.isGyaoDrm();
return bool;
}
async getURLByBrightcove(obj, policykey) {
if (policykey) {
let json = await XmlHttpRequest.get("https://edge.api.brightcove.com/playback/v1/accounts/" + obj.pubId + "/videos/" + obj.videoId, {"Accept":"application/json;pk=" + policykey, "Origin":this.url.replace(/^(https?:\/\/[^\/]+).*$/, "$1")});
json = JSON.parse(json);
let isDrm = await this.isDrm();
return {"url":json["sources"].map((obj) => {return obj.src}), "title":this.title, "drm":isDrm};
} else {
return Promise.resolve();
};
}
async brightcove(obj) {
let policykey = await this.getKey(obj);
let url = "http://c.brightcove.com/services/mobile/streaming/index/master.m3u8?videoId=" + obj.videoId + "&pubId=" + obj.pubId;
return (policykey) ? this.getURLByBrightcove(obj, policykey) : XmlHttpRequest.check(url).then((bool) => {return bool ? Promise.resolve({"url":url, "title":this.title}) : Promise.resolve()});
}
async fujitv() {
let id = null;
if (this.url.indexOf("tver.jp")>-1) {
id = document.documentElement.innerHTML.match(/ser=(\w+)&/);
id = id[1];
} else {
id = this.url.replace(/^http.+\/(\w+)\/$/, "$1");
};
let urls = `https://i.fod.fujitv.co.jp/abr/pc_html5/${id}.m3u8`;
return {"url":urls, "title":this.title};
}
async jshowtv() {
let html = await XmlHttpRequest.get(this.url).catch(() => {return ""});
let doc = new DOMParser().parseFromString(html, "text/html");
let iframe = doc.querySelector(this.rule.cssIflame);
if (!iframe) return null;
this.url = iframe.src.replace(/^.+?loadloadingfreevideo\.top\/.+?index\.html\?id=(.+)$/, "https://lb.loadloadingfreevideo.top/hls/$1/$1");
return {"url":this.url, "title":this.title};
}
execute() {
if (document.documentElement.innerHTML.indexOf("i.fod.fujitv.co.jp")>-1) {
console.log("Extract DownloadURL:fujitv");
return this.fujitv();
} else if (/jshow\.tv/.test(this.url)) {
console.log("Extract DownloadURL:jshow.tv系");
return this.jshowtv();
} else if (this.isBrightcove) {
console.log("Extract DownloadURL:brightcove");
return this.searchBrightcoveNode()
.then((obj) => {return this.brightcove(obj)});
} else {
console.log("Extract DownloadURL:該当無し");
return Promise.resolve();
};
}
};
const Panel = class {
static create() {
let panel = createNode("div", null, {"id":"download_panel"}, document.querySelector("body"));
let container = createNode("div", null, null, panel);
let topcontent = createNode("div", null, {"id":"download_panel_topcontent"}, container);
createNode("div", null, {"class":"download_panel_btn download_panel_border download_panel_color_m3u8"}, topcontent);
createNode("div", null, {"class":"download_panel_btn download_panel_border download_panel_color_mp4"}, topcontent);
createNode("div", null, {"class":"download_panel_btn download_panel_border download_panel_color_error"}, topcontent);
let bottomcontent = createNode("div", null, {"id":"download_panel_bottomcontent"}, container);
createNode("div", null, {"class":"download_panel_list download_panel_border download_panel_color_m3u8"}, bottomcontent);
createNode("div", null, {"class":"download_panel_list download_panel_border download_panel_color_mp4"}, bottomcontent);
createNode("div", null, {"class":"download_panel_list download_panel_border download_panel_color_error"}, bottomcontent);
}
static update(title, urls, isDrm) {
if (!Array.isArray(urls)) urls = [urls];
urls = urls.filter((url) => {
if (!url) return false;
if (/^https/.test(url)) return true;
return !urls.includes(url.replace(/^http:/, "https:"));
});
let m3u8Ary = [];
let mp4Ary = [];
if (!document.getElementById("download_panel")) this.create();
let m3u8list = document.querySelector(css.m3u8list);
let mp4list = document.querySelector(css.mp4list);
title = title.replace(/\|/g, " ");
urls.forEach((url) => {
let ext = url.replace(/^.+\.(\w+)(\?.+)?$/, "$1");
ext = (/^\w+$/.test(ext)) ? ext.toLowerCase() : "etc";
let ism3u8 = ext=="m3u8";
let ismpd = ext=="mpd";
let list = (ism3u8 || ismpd) ? m3u8list : mp4list;
let extContainer = list.querySelector("div.download_panel_container_" + ext);
if (!extContainer) {
extContainer = createNode("div", null, {"class":"download_panel_container_" + ext}, list);
createNode("div", (ext=="etc") ? null : ext, {"class":"download_panel_container_ext"}, extContainer);
createNode("div", null, {"class":"download_panel_container_list"}, extContainer);
};
let div = createNode("div", null, null, list.querySelector("div.download_panel_container_" + ext + ">div.download_panel_container_list"));
if (!ism3u8 && !ismpd) {
mp4Ary.push(url);
createNode("a", title, {"href":url}, div);
} else {
m3u8Ary.push(url);
if (isDrm) {
createNode("a", "SAMPLE-AES:" + title, {"href":url, "class":"sampleaes"}, div);
} else {
createNode("a", title, {"href":url}, div);
}
if (ismpd) return null;
if (isDrm) return null;
div.addEventListener("mouseover", ((url) => {
let callback = async (ev) => {
ev.currentTarget.removeEventListener("mouseover", callback);
let m3u8 = await XmlHttpRequest.get(url);
let list = [];
let urls = [...document.querySelectorAll("div.download_panel_addm3u8>a")].map((node) => {return node.href.replace(/^https/, "http")});
let streamInf = m3u8.match(/^#EXT-X-STREAM-INF:.+[\r\n]+.+$/gm) || [];
let media = m3u8.match(/^#EXT-X-MEDIA:.+$/gm) || [];
let iFrameStreamInf = m3u8.match(/^#EXT-X-I-FRAME-STREAM-INF:.+$/gm) || [];
streamInf.forEach((str) => {
let [inf, m3u8] = str.replace(/^#.+?:/, "").split(/[\r\n]+/);
inf = inf.split(",").filter((str) => {return /^(bandwidth|resolution|audio)/i.test(str)}).join(" ");
if (/audio=/i.test(inf)) inf = "Video Only " + inf;
list.push({"inf":inf, "m3u8":m3u8});
});
media.forEach((str) => {
let [inf, m3u8] = str.replace(/^#.+?:/, "").split(",").filter((str) => {return /^(group-id|uri)/i.test(str)});
m3u8 = m3u8.replace(/^uri="(.+)"$/i, "$1");
inf = (/audio/i.test(inf)) ? "Audio Only " + inf : "Subtitles " + inf;
list.push({"inf":inf, "m3u8":m3u8});
});
iFrameStreamInf.forEach((str) => {
str = str.replace(/^#.+?:/, "").split(",").filter((str) => {return /^(bandwidth|resolution|uri)/i.test(str)});
let m3u8 = str.pop();
let inf = "I-FRAME " + str.join(" ")
m3u8 = m3u8.replace(/^uri="(.+)"$/i, "$1");
list.push({"inf":inf, "m3u8":m3u8});
});
list.forEach((obj) => {
obj.m3u8 = new URL(obj.m3u8, url).href;
let addURL = createNode("div", null, {"class":"download_panel_addm3u8"}, div, "after");
if (urls.includes(obj.m3u8.replace(/^https/, "http"))) {
createNode("span", obj.inf, null , addURL);
} else {
XmlHttpRequest.get(obj.m3u8)
.then((m3u8) => {
createNode("a", obj.inf, {"href":obj.m3u8}, addURL);
});
};
});
};
return callback;
})(url));
};
});
let m3u8btn = document.querySelector(css.m3u8btn);
let mp4btn = document.querySelector(css.mp4btn);
if (m3u8Ary.length!=0) m3u8btn.textContent = m3u8Ary.length;
if (mp4Ary.length!=0) mp4btn.textContent = mp4Ary.length;
return (m3u8Ary.length==0 && mp4Ary.length==0);
}
static error(message) {
if (!message) message = "URL取得に失敗しました。";
if (!document.getElementById("download_panel")) this.create();
let list = document.querySelector(css.errorlist);
let btn = document.querySelector(css.errorbtn);
btn.textContent = 1;
createNode("div", message, null, list);
return false;
}
};
(() => {
addStyle([
//表示位置
css.panel + " {position:fixed; top:10px; right:10px; z-index:1000000;}"
//レイアウト
, css.panel + " {display:flex; flex-direction:column;}"
, css.topcontent + " {display:flex; flex-direction:row; justify-content:flex-end}"
, css.list + ">div {display:flex; flex-direction:row;}"
//文字列センタリング
, css.btn + "," + css.list + " div.download_panel_container_ext {display:flex; justify-content:center; align-items: center;}"
//表示制御
, css.panel + ":hover " + css.list + ":not(:empty) {display:block;}"
//色
, "div.download_panel_color_m3u8 {background-color:rgba(224, 255, 255, 0.5); color:black;}"
, "div.download_panel_color_mp4 {background-color:rgba(255, 192, 203, 0.5); color:black;}"
, "div.download_panel_color_error {background-color:rgba(255, 0, 0, 0.5); color:white;}"
, "div.download_panel_color_m3u8 a.sampleaes {color:red!important;}"
, "div.download_panel_addm3u8>a {color:orange;}"
, "div.download_panel_container_list>div:not(.download_panel_addm3u8)>* {color:black;}"
//文字列
, css.list + " a {text-decoration:none;}"
, css.bottomcontent + " * {font-size:small;}"
, css.btn + " {font-size:large;}"
//ボーダー
, css.brd + " {border-style:solid; border-width:1px; border-color:rgb(191, 191, 191); border-radius:10px;}"
//非表示
, css.btn + ":empty {display:none;}"
, css.list + " {display:none;}"
//その他
, css.btn + " {position:relative; width:50px; height:50px; cursor:pointer;}"
, css.list + " {padding:10px 30px; white-space:nowrap; overflow-x:auto; overflow-y:auto; max-width:" + window.innerWidth*0.7 + "px; min-height:50px; max-height:" + window.innerHeight*0.4 + "px;}"
, css.list + " div.download_panel_container_ext {width:70px;}"
, css.list + " div.download_panel_container_list {overflow-x:hidden; text-align:left; padding:10px;}"
]);
new CreateURL().execute()
.then((obj) => {
if (!obj) return Promise.resolve();
Panel.update(obj.title, obj.url, obj.drm)
})
.catch((message) => {return Panel.error(message)})
})();