// ==UserScript==
// @name 海角社区-VIP视频观看,支持PC与手机端
// @namespace http://tampermonkey.net/
// @version 2.2.2
// @description 最新解析接口,快速稳定,可观看钻石视频
// @author chuvh360
// @license MIT
// @match https://haijiao.com/*
// @match https://tools.thatwind.com/tool*
// @match https://*/post/details*
// @icon https://hjbc30.top/images/common/project/favicon.ico
// @run-at document-start
// @grant unsafeWindow
// @grant GM_download
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @connect *
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://unpkg.com/layui@2.8.18/dist/layui.js
// @resource layuicss https://unpkg.com/layui@2.8.18/dist/css/layui.css
// @charset UTF-8
// @license MIT
// ==/UserScript==
(function () {
'use strict';
var $ = unsafeWindow.jQuery;
GM_addStyle (GM_getResourceText ("layuicss") );
let magicVideoHost = "https://ip.hjcfcf.com";
let baseUrl = "https://1256161784-g530cbmrf5-gz.scf.tencentcs.com";
let checkStep = 50;
function decode(s) {
return atob(atob(atob(s)));
}
function encode(s) {
return btoa(btoa(btoa(s)));
}
function jencode(s) {
return encode(JSON.stringify(s, `utf-8`));
}
async function setV(k, v) {
if (IsIOS()) {
window.localStorage.setItem(k, v);
} else {
await GM_setValue(k, v);
}
}
async function getV(k) {
if (IsIOS()) {
return window.localStorage.getItem(k) || "";
} else {
return await GM_getValue(k,null) || "";
}
}
async function delV(k) {
if (IsIOS()) {
return window.localStorage.removeItem(k) || "";
} else {
return await GM_deleteValue(k) || "";
}
}
async function play(blob_url,videoUrl) {
console.log("url=" + blob_url)
let playpos = ''
if (IsPhone()) {
if(IsIOS){
$('.sell-btn').empty()
$('.sell-btn').append(`
<video id="video" controls width="100%" >
<source src="${videoUrl}" type="application/x-mpegURL">
</video>
`)
}else{
playpos = '.sell-btn'
let videoInfo = document.querySelector(playpos)
videoInfo.innerHTML = '<video id="video" controls autoplay width="100%"></video>'
var video = document.getElementById('video');
var hls = new Hls();
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
hls.loadSource(blob_url);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log("manifest loaded, found " + data.levels.length + " quality level");
});
});
}
}
else {
playpos = '.sell-btn'
window.dp = new DPlayer({
element: document.querySelector(playpos),
autoplay: false,
theme: '#FADFA3',
loop: true,
lang: 'zh',
screenshot: true,
hotkey: true,
preload: 'auto',
video: {
url: blob_url,
type: 'hls'
}
})
}
}
async function videoParse(preview_url,code,topicId,preview_m3u8_content,videoLength) {
if(code == ''){
layer.msg('请输入邀请码', {icon: 2})
return
}
layer.msg('开始解析,请稍后', {icon: 1})
// 增加验证
let preview_m3u8_data = parseM3u8Content(preview_m3u8_content);
let key = preview_m3u8_data['key'];
console.log("key",key);
if(key.indexOf("http") == -1){
let preview_url_split = preview_url.split("/")
preview_url_split[preview_url_split.length-1] = key;
preview_m3u8_data['key'] = preview_url_split.join("/");
}
let firstTsUrl = replaceMagicHost(preview_m3u8_data['ts_files'][0],magicVideoHost);
let lastShardNum = videoLength - 1;
let realShardNum = await getLastShardNum(firstTsUrl, lastShardNum);
let totalShardCount = realShardNum + 1;
let parse_url = baseUrl + "/parse_plus" + "?" + "code=" + code + "&topicId=" + topicId + "&videoLength=" + videoLength + "&shardNum=" + totalShardCount + "&key=" + encodeURIComponent(preview_m3u8_data["key"]) + "&iv=" + preview_m3u8_data["iv"] + "&m=" + preview_m3u8_data["method"] + "&tsUrl=" + encodeURIComponent(preview_m3u8_data['ts_files'][0]) ;
try {
console.log("parse_url",parse_url)
let result = await httpGet(parse_url);
console.log("result",result);
await processParseResult(JSON.parse(result),code);
} catch (error) {
console.error("Error",error)
}
}
async function processParseResult(res,code){
// console.log("res",JSON.stringify(res));
if(res.code == 1001){
layer.msg('无效的邀请码', {icon: 2})
delV("hj_invit_code")
return
}else if(res.code == 200){
setV("hj_invit_code",code)
layer.msg('解析成功,请点击播放', {icon: 1})
let videoUrl = baseUrl + "/video/" + res.data.videoId;
console.log("videoUrl", videoUrl)
let m3u8_content = await httpGet(videoUrl);
console.log("m3u8_content", m3u8_content);
var blob = new Blob([m3u8_content], { type: 'text/plain' });
let blob_url = URL.createObjectURL(blob)
console.log("blob_url", blob_url)
play(blob_url,videoUrl)
}else{
layer.msg('解析失败,请重试', {icon: 2})
return;
}
}
function parseM3u8Content(content) {
let result = {};
let splitLines = content.split("\n");
let urls = [];
for (let line of splitLines) {
if (line.startsWith("#EXT-X-KEY")) {
let keyItems = line.split(",");
for (let item of keyItems) {
let value = item.split("=", 2)[1];
// console.log("value",value);
if (item.includes("METHOD")) {
result['method'] = value;
continue;
}
if (item.includes("URI")) {
result['key'] = value.replace(new RegExp("\"","gm"),"");
continue;
}
if (item.includes("IV")) {
result['iv'] = value;
}
}
}
if (line.startsWith("https://") || line.startsWith("http://")) {
urls.push(line);
}
}
result['ts_files'] = urls;
return result;
}
function replaceMagicHost(url, magicHost) {
if(url.endsWith(".ts")){
return url;
}
let tsUrlPath = new URL(url).pathname;
console.log(tsUrlPath);
return magicHost + tsUrlPath;
}
async function isShardTsUrlOk(firstTsUrl, shardNum) {
let tsUrl = firstTsUrl.replace("i0.ts", `i${shardNum}.ts`);
console.log("check tsUrl", tsUrl);
if(IsPhone()){
const response = await fetch(tsUrl, { method: 'HEAD', mode: 'cors'});
console.log("phone check response", response);
return response.ok;
}else{
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "HEAD",
url: tsUrl,
onload: function(response) {
console.log("check response", response);
resolve(response.status === 200);
},
onerror: function(error) {
console.error(error)
reject(false);
}
});
});
}
}
async function httpGet(url) {
if(IsPhone()){
const response = await fetch(url);
let res = await response.text();
return res;
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
console.log("get response", response);
resolve(response.responseText);
},
onerror: function(error) {
console.error("Error:", error);
reject(error);
}
});
});
}
async function getStepCheckShardNum(firstTsUrl, lastShardNum, step) {
let stepCheckShard = lastShardNum;
while (true) {
console.log("stepCheckShard:", stepCheckShard);
let ok = false;
try{
ok = await isShardTsUrlOk(firstTsUrl, stepCheckShard)
}catch(error){
console.error(error)
ok = false;
}
console.log("getStepCheckShardNum isShardTsUrlOk",ok)
if (!ok) {
stepCheckShard -= step;
} else {
break;
}
}
return stepCheckShard;
}
async function binarySearchShardNum(firstTsUrl, start, end) {
while (true) {
let checkShard = start + Math.floor((end - start) / 2);
console.log("binarySearchShardNum:", checkShard);
let ok = false;
try{
ok = await isShardTsUrlOk(firstTsUrl, checkShard)
}catch(error){
console.error(error)
ok = false;
}
console.log("binarySearchShardNum isShardTsUrlOk",ok)
if (ok) {
start = checkShard;
} else {
end = checkShard;
}
console.log("start",start)
console.log("end",end)
if (start + 1 === end) {
return start;
}
}
}
async function getLastShardNum(firstTsUrl, lastShardNum) {
let stepCheckShard = await getStepCheckShardNum(firstTsUrl, lastShardNum, parseInt(checkStep));
if (stepCheckShard < lastShardNum) {
let shardNum = await binarySearchShardNum(firstTsUrl, stepCheckShard, stepCheckShard + parseInt(checkStep));
return shardNum;
} else {
return lastShardNum;
}
}
function replace_exist_img(body) {
let content = body.content;
let attachments = body.attachments;
let all_img = {};
let has_video = -1;
for (var i = 0; i < attachments.length; i++) {
var atta = attachments[i];
if (atta.category === 'images') {
all_img[atta.id] = atta.remoteUrl;
}
if (atta.category === 'video') {
has_video = i;
return [body, undefined, has_video];
}
}
let re_img = /<img src=\"https:\/\/[\w\.\/]+?\/images\/.*?\" data-id=\"(\d+)\".*?\/>/g;
for (let e of content.matchAll(re_img)) {
let id = parseInt(e[1]);
if (id in all_img) {
delete all_img[id];
}
}
body.content = content;
return [body, all_img, has_video];
}
async function replace_m3u8(body, has_video) {
// await delV("hj_invit_code")
let topicId = body.topicId;
let attachments = body.attachments;
let vidx = has_video;
if (vidx < 0) {
return [body, undefined];
}
if (body.sale === null || body.sale.money_type == 0) {
return [body, attachments[vidx]];
}
let url = attachments[vidx].remoteUrl;
let videoLength = attachments[vidx].video_time_length;
console.log("topicId", topicId)
console.log("url", url)
console.log("videoLength", videoLength)
if(url.indexOf("/api/address") > -1){
url = "https://" + window.location.hostname + url;
}
console.log("url",url);
let preview_m3u8_content = await httpGet(url);
// console.log("preview_m3u8_content",preview_m3u8_content);
if(preview_m3u8_content.indexOf("IV=") >= 0){
const hj_invit_code = await getV("hj_invit_code");
console.log("hj_invit_code",hj_invit_code);
if(hj_invit_code){
console.log("start videoParse");
videoParse(url,hj_invit_code,topicId,preview_m3u8_content,videoLength);
}else{
console.log("start get qqQun");
let contact_url = baseUrl + "/contact";
let qQun = await httpGet(contact_url);
console.log("qq群:",qQun)
layer.prompt({
formType: 0,
value: '',
title: '加QQ群:【' + qQun + '】获取邀请码',
}, function(value, index, elem){
videoParse(url,value,topicId,preview_m3u8_content,videoLength);
layer.close(index);
});
}
}
return [body, attachments[vidx]];
}
function remove_vip(body) {
body.node.vipLimit = 0;
let attachments = body.attachments;
let image_urls = [];
let video_urls = ``;
let has_video = -1;
for (var i = 0; i < attachments.length; i++) {
var atta = attachments[i];
if (atta.category === 'images') {
image_urls.push(`<img src="${atta.remoteUrl}" data-id="${atta.id}"/>`)
}
if (atta.category === 'video') {
has_video = i;
}
}
console.log("has_video",has_video);
let images = image_urls.join();
if (has_video >= 0) {
console.log("replace_m3u8 in remove_vip");
let [nbody, v] = replace_m3u8(body, has_video);
body = nbody;
video_urls = `<video src="${v.remoteUrl}" data-id="${v.id}"/></video>`
}
let content = body.content.replace(/\[[图片视频]+\]?/, ``);
content = body.content.replace(/此处内容售价.*?您还没有购买,请购买后查看!/, ``);
content = '<html><head></head><body>' + content + '<br/>' + images + '<br/>' + video_urls + '<br/></body></html>';
body.content = content;
return body;
}
function modify_data(data) {
let body = data;
if(typeof data === 'string'){
body = JSON.parse(decode(data));
}
// console.log("body",body);
if (body.node.vipLimit != 0) {
body = remove_vip(body);
return jencode(body);
}
let [nbody, rest_img, has_video] = replace_exist_img(body);
body = nbody;
// console.log("has_video",has_video);
if (body.content.includes(`[/sell]`)) {
return jencode(body);
}
if ('sale' in body && body.sale !== null) {
body.sale.is_buy = true;
body.sale.buy_index = parseInt(Math.random() * (5000 - 1000 + 1) + 1000, 10);
}
if (has_video >= 0) {
console.log("replace_m3u8 in modify_data");
let [nbody, v] = replace_m3u8(body, has_video);
return jencode(nbody);
}
let img_elements = []
for (const [id, src] of Object.entries(rest_img)) {
img_elements.push(`<img src="${src}" data-id="${id}"/>`);
}
let selled_img = `[sell]` + img_elements.join() + `[/sell]`;
let ncontent = body.content.replace(/<span class=\"sell-btn\".*<\/span>/, selled_img);
body.content = ncontent;
return jencode(body);
}
function IsPhone() {
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
navigator.userAgent.toLowerCase()
);
}
function IsIOS(){
return /(iphone|ipad|ipod|ios)/i.test(navigator.userAgent.toLowerCase());
}
const originOpen = XMLHttpRequest.prototype.open;
const re_topic = /\/api\/topic\/\d+/;
XMLHttpRequest.prototype.open = function (_, url) {
// 拦截topic
if (re_topic.test(url)) {
const xhr = this;
const getter = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
"response"
).get;
Object.defineProperty(xhr, "responseText", {
get: () => {
let result = getter.call(xhr);
try {
let res = JSON.parse(result, `utf-8`);
console.log("res",res)
// 这里修改data
res.data = modify_data(res.data)
return JSON.stringify(res, `utf-8`);
} catch (e) {
console.log('发生异常! 解析失败!');
console.log(e);
return result;
}
},
});
}
originOpen.apply(this, arguments);
};
})();