// ==UserScript==
// @name 观影self
// @namespace liuser.betterworld.love
// @match https://movie.douban.com/subject/*
// @match https://m.douban.com/movie/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect *
// @run-at document-end
// @require https://cdn.jsdelivr.net/npm/xy-ui@1.10.7/+esm
// @require https://cdn.staticfile.org/artplayer/4.6.2/artplayer.min.js
// @require https://unpkg.com/hls.js@1.2.9/dist/hls.min.js
// @version 2.4.1
// @author liuser, collaborated with ray
// @description 不更不答复.请勿反馈. 代码归原作者.https://greasyfork.org/zh-CN/scripts/459540-%E6%88%91%E5%8F%AA%E6%83%B3%E5%A5%BD%E5%A5%BD%E8%A7%82%E5%BD%B1.
// @license MIT
// ==/UserScript==
// ==UserScript==
// @name 我只想好好观影
// @namespace liuser.betterworld.love
// @match https://movie.douban.com/subject/*
// @match https://m.douban.com/movie/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @antifeature tracking
// @connect *
// @run-at document-end
// @require https://cdn.staticfile.org/artplayer/4.6.2/artplayer.min.js
// @require https://cdn.staticfile.org/hls.js/1.4.3/hls.min.js
// @version 2.12
// @author liuser, collaborated with ray
// @description 想看就看
// @license MIT
// ==/UserScript==
(function () {
const _debug = 0;
let art = {}; //播放器
let seriesNum = 0;
let sourceSelected = false;
const { query: $, queryAll: $$, isMobile } = Artplayer.utils;
const tip = (message) => alert(message);
//获取豆瓣影片名称
const videoName = isMobile ? $(".sub-title").innerText : document.title.slice(0, -5);
// debug
const log = (function () {
if (_debug) return console.log.bind(console);
return function () { };
})();
function htmlToElement(html) {//将html转为element
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
function addScript() {//添加统计脚本
let statistic = document.createElement('script');
statistic.setAttribute("src", "https://hm.baidu.com/hm.js?f02301d8266631b0285c3e325c9a574b")
document.head.appendChild(statistic);
}
//搜索源
const searchSource = [
//自己的放前面
{ "name": "天堂官采", "searchUrl": "http://vipmv.cc/api.php/provide/vod/" },
{ "name": "M3U8官采", "searchUrl": "https://www.zycaiji.net:7788/api.php/provide/vod/" },
//以下原版
{ "name": "红牛资源", "searchUrl": "https://www.hongniuzy2.com/api.php/provide/vod/from/hnm3u8/" },
{ "name": "非凡资源", "searchUrl": "http://cj.ffzyapi.com/api.php/provide/vod/" },
{ "name": "量子资源", "searchUrl": "https://cj.lziapi.com/api.php/provide/vod/" },
{ "name": "ikun资源", "searchUrl": "https://ikunzyapi.com/api.php/provide/vod/from/ikm3u8/at/json/" },
{ "name": "光速资源", "searchUrl": "https://api.guangsuapi.com/api.php/provide/vod/from/gsm3u8/" },
{ "name": "高清资源", "searchUrl": "https://api.1080zyku.com/inc/apijson.php/" },
{ "name": "188资源", "searchUrl": "https://www.188zy.org/api.php/provide/vod/" },
{ "name": "天空资源","searchUrl":"https://m3u8.tiankongapi.com/api.php/provide/vod/from/tkm3u8/"},//有防火墙,垃圾
{ "name": "闪电资源","searchUrl":"https://sdzyapi.com/api.php/provide/vod/"},//不太好,格式经常有错
// { "name": "飞速资源", "searchUrl": "https://www.feisuzyapi.com/api.php/provide/vod/" },//经常作妖或者没有资源
// { "name": "卧龙资源", "searchUrl": "https://collect.wolongzyw.com/api.php/provide/vod/" }, 非常恶心的广告
// { "name": "8090资源", "searchUrl": "https://api.yparse.com/api/json/m3u8/" },垃圾 可能有墙
// { "name": "百度云资源", "searchUrl": "https://api.apibdzy.com/api.php/provide/vod/" },
// { "name": "酷点资源", "searchUrl": "https://kudian10.com/api.php/provide/vod/" },
// { "name": "淘片资源", "searchUrl": "https://taopianapi.com/home/cjapi/as/mc10/vod/json/" },
// { "name": "ck资源", "searchUrl": "https://ckzy.me/api.php/provide/vod/" },
// { "name": "快播资源", "searchUrl": "https://caiji.kczyapi.com/api.php/provide/vod/" },
// { "name": "海外看资源", "searchUrl": "http://api.haiwaikan.com/v1/vod/" }, // 说是屏蔽了所有中国的IP,所以如果你有外国的ip可能比较好
// { "name": "68资源", "searchUrl": "https://caiji.68zyapi.com/api.php/provide/vod/" },
// {"name":"鱼乐资源","searchUrl":"https://api.yulecj.com/api.php/provide/vod/"},//速度太慢
// {"name":"无尽资源","searchUrl":"https://api.wujinapi.me/api.php/provide/vod/"},//资源少
];
const pushSource = ()=>{
let sourceAdded = GM_getValue("sourceAdded","")
sourceAdded.split(",").forEach((item)=>{
if(item==="")return
name_url = item.split("|")
searchSource.push({
"name":name_url[0],
"searchUrl":name_url[1]
})
})
}
const SourceTop = ()=>{
let sourceAdded = prompt("请输入自定义源,名称与链接用|隔开,每项用英文逗号隔开")
GM_setValue("sourceAdded",sourceAdded)
pushSource()
}
GM_registerMenuCommand("自定义源",SourceTop)
//处理搜索到的结果:从返回结果中找到对应片子
function handleResponse(r) {
if (!r || r.list.length == 0) {
log("未搜索到结果");
return 0
}
let video, found = false;
for (let item of r.list) {
log("正在对比剧集年份和演员");
log(item)
let yearEqual = getVideoYear(item.vod_year);
let actorContain = videoActor(item.vod_actor.split(",")[0])
if (yearEqual === true|| actorContain=== true){
video = item;
found = true;
break
}
}
if (found == false) {
log("没有找到匹配剧集的影片,怎么回事哟!");
return 0
}
let playList = video.vod_play_url.split("$$$").filter(str => str.includes("m3u8"));
if (playList.length == 0) {
log("没有m3u8资源, 无法测速, 无法播放");
return 0
}
playList = playList[0].split("#");
playList = playList.map(str => {
let index = str.indexOf("$");
return { "name": str.slice(0, index), "url": str.slice(index + 1) }
});
return playList
}
//到电影网站搜索电影
const search = (url) => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: encodeURI(`${url}?ac=detail&wd=${videoName}`),
timeout: 3000,
responseType: 'json',
onload(r) {
try {
resolve(handleResponse(r.response, videoName));
} catch (e) {
log("垃圾资源,解析失败了,可能有防火墙");
log(e);
reject()
}
},
onerror: reject,
ontimeout: reject
});
});
//播放按钮
class PlayBtn {
constructor() {
const e = htmlToElement(`<button class="liu-btn play-btn">一键播放</button>`);
$(isMobile ? ".sub-original-title" : "h1").appendChild(e);
const render = async (item) => {
const playList = await search(item.searchUrl);
if (playList == 0) return;
if (e.loading) {
e.loading = false;
new UI(playList);
}
//渲染资源列表
const btn = new SourceButton({ name: item.name, playList }).element;
if(!sourceSelected){
btn.classList.add("selected")
sourceSelected = true
}
$(".sourceButtonList").appendChild(btn);
};
e.onclick = function () {
e.loading = true;
//tip("正在搜索");
searchSource.forEach(render);
setTimeout(() => {
if (e.loading == true) {
e.loading = false;
tip("未搜索到资源")
} else {
speedTest()
}
}, 3500);
};
}
}
class UI {
constructor(playList) {
document.body.appendChild(htmlToElement(
`<div class="liu-playContainer">
<button class="liu-closePlayer liu-btn">X</button>
<div class="playSpace" >
<div class="artplayer-app"></div>
<div class="series">
<div class="seletor-title">选集</div>
<div class="series-contianer"></div>
</div>
</div>
<div class="sourceButtonList"></div>
<div class="mannul">
<div class="show-series" style="color:#a3a3a3"></div>
<a class="love-support liu-btn" href="http://babelgo.cn:5230/m/1" target="_blank" style="color:#4aa150">☕赏作者喝一杯咖啡?</a>
<a class="love-support liu-btn" href="https://t.me/wzxhhgy" target="_blank" style="color:#4aa150">电报群</a>
<a class="love-support liu-btn" href="https://greasyfork.org/zh-CN/scripts/459540-%E6%88%91%E5%8F%AA%E6%83%B3%E5%A5%BD%E5%A5%BD%E8%A7%82%E5%BD%B1/feedback" target="_blank" style="color:#4aa150">👉反馈</a>
</div>
</div>`
)).querySelector(".liu-closePlayer").onclick = function () {
this.parentNode.remove();
document.body.style.overflow = 'auto';
};
document.body.style.overflow = 'hidden';
//第n集开始播放
log(playList[seriesNum].url);
initArt(playList[seriesNum].url);
new SeriesContainer(playList);
}
}
//初始化播放器
function initArt(url) {
art = new Artplayer({
container: ".artplayer-app",
url:url,
pip: true,
fullscreen: true,
fullscreenWeb: true,
screenshot: true,
hotkey: true,
airplay: true,
playbackRate: true,
controls: [{
name: "resolution",
html: "分辨率",
position: "right"
}],
customType: {
m3u8(video, url) {
// Attach the Hls instance to the Artplayer instance
if (art.hls) art.hls.destroy();
art.hls = new Hls();
art.hls.loadSource(url);
art.hls.attachMedia(video);
if (!video.src) {//兼容safari
video.src = url;
}
},
}
});
art.once('destroy', () => art.hls.destroy());
art.on("video:loadedmetadata", () => {
art.controls.resolution.innerText = art.video.videoHeight + "P";
});
log(art)
}
//影视源选择按钮
class SourceButton {
constructor(item) {
this.element = htmlToElement(`<button class="source-selector liu-btn" >${item.name}</button>`);
this.element.onclick = () => {
$(".selected")?$(".selected").classList.remove("selected"):null;
this.element.classList.add("selected")
switchUrl(item.playList[seriesNum].url);
new SeriesContainer(item.playList);
};
this.element._playList = item.playList
this.element._sourceName = item.name
}
//sources 是[{name:"..资源",playList:[{name:"第一集",url:""}]}]
}
//剧集选择器
class SeriesButton {
constructor(pNode, name, url, index) {
let selector = htmlToElement(
`<button class="series-selector liu-btn" style="color:#a3a3a3" >${name.slice(0,4)}</button>`
)
pNode.appendChild(selector).onclick = () => {
seriesNum = index;
switchUrl(url);
$(".playing")?$(".playing").classList.remove("playing"):null;
// $(".show-series").innerText = `正在播放第${index + 1}集`;
selector.classList.add("playing")
speedTest()
};
}
}
//剧集选择器的container
class SeriesContainer {
constructor(playList) {
//const e = htmlToElement(`<div class="series-select-space" style="display:flex;flex-wrap:wrap;overflow:scroll;align-content: start;"></div>`);
const e = $(".series-contianer")
e.innerHTML = ""
for (let [index, item] of playList.entries()) {
new SeriesButton(e, item.name, item.url, index);
}
seriesNum==0?$(".series-selector").classList.add("playing"):null;
}
}
function switchUrl(url) {//兼容safari
art.switchUrl(url)
if (art.video.src != url) {
art.video.src = url;
}
}
//获取电影的年份
function getVideoYear(outYear) {
const e = $(isMobile ? ".sub-original-title" : ".year");
if (!e) {
log("获取年份失败,请检查!");
return 0;
}
return e.innerText.includes(outYear);
}
//对比电影演员
function videoActor(outActor){
const e = $(isMobile?".bd":".actor")
if (!e) {
log("获取演员失败,请检查!");
return 0;
}
//log(`${outActor}:匹配结果${e.innerText.includes(outActor)}`)
return e.innerText.includes(outActor);
}
//下载
const get = (url) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: encodeURI(url),
timeout: 10000,
onload: function (r) {
resolve(r.response)
},
onerror: function (e) {
resolve("html")
},
ontimeout: function (o) {
resolve("html")
}
})
})
}
//下载m3u8的内容,返回片段列表
async function downloadM3u8(url) {
let domain = url.split("/")[0]
let baseUrl = url.split("/")[2]
let downLoadList = []
log(`正在获取index.m3u8 ${url}`)
let downloadContent = await get(url)
if (downloadContent.includes("html")) {
log(downloadContent)
log(`下载失败,被反爬虫了`)
return []
}
if (downloadContent.includes("index.m3u8")) { //如果是m3u8地址
let lines = downloadContent.split("\n")
for (let item of lines) {
if (/^[#\s]/.test(item)) continue //跳过注释和空白行
if (/^\//.test(item)) {
downLoadList = await downloadM3u8(domain + "//" + baseUrl + item)
} else if (/^(http)/.test(item)) {
downLoadList = await downloadM3u8(item)
} else {
downLoadList = await downloadM3u8(url.replace("index.m3u8", item))
}
}
} else {//如果是ts地址
let lines = downloadContent.split("\n")
for (let item of lines) {
if (/^[#\s]/.test(item)) continue//跳过注释和空白行
if (/^(http)/.test(item)) {//如果是http直链
downLoadList.push(item)
} else if (/^\//.test(item)) { //如果是绝对链接
downLoadList.push(domain + "//" + baseUrl + item)
} else {
downLoadList.push(url.replace("index.m3u8", item))
}
}
}
// log(`测试列表为${downLoadList}`)
return downLoadList
}
//对资源进行测速
function speedTest() {
// tip("脚本自动测试源的速度,随后请自行切换源进行尝试")
let sourceButtons = $$(".source-selector")
//log(sourceButtons)
sourceButtons.forEach(async (e) => {
let url = e._playList[seriesNum].url
let tsList = await downloadM3u8(url)
let downloadList = []
for (let i = 0; i < 8; i++) {
downloadList.push(tsList[Math.floor(Math.random() * tsList.length)])
}
let downloadSize = 0
let startTime = Date.now();
for (item of downloadList) {
log("正在下载" + item)
let r = await getBuffer(item)
downloadSize += r.byteLength / 1024 / 1024
}
let endTime = Date.now();
let duration = (endTime - startTime) / 1000
let speed = downloadSize / duration ? downloadSize / duration : 0
log(`速度为${speed}mb/s`)
e.innerText = e._sourceName + " " + speed.toFixed(2) + "mb/s"
let state = speed > 1 ? "fast" : "slow"
e.classList.add(`speed-${state}`)
})
}
//将GM_xmlhttpRequest改造为Promise
function getBuffer(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
timeout: 3000,
url: encodeURI(url),
responseType: "arraybuffer",
onload: function (r) {
resolve(r.response);
},
onerror: function (error) {
log("速度太慢了或无法测速")
resolve({ "byteLength": 0 })
},
ontimeout: function (out) {
log("速度太慢了或无法测速")
resolve({ "byteLength": 0 })
}
});
});
}
//按钮样式
GM_addStyle(
`
.liu-btn{
cursor:pointer;
font-size:1rem;
padding: 0.6rem 1.2rem;
border: 1px solid transparent;
}
.play-btn {
border-radius: 8px;
cursor: pointer;
font-weight: bolder;
background-color:#e8f5e9;
}
.play-btn:hover {
background-color:#c8e6c9;
}
.play-btn:active{
background-color: #81c784;
}
.source-selector{
background-color: #141414;
color: #99a2aa;
padding:0.2rem 0.5rem;
margin:0.5rem 0.875rem;
border-radius:4px;
}
.series-selector{
background-color: #141414;
border-radius:3px;
color: #99a2aa;
width:3.5rem;
height:3.5rem;
font-size:0.75rem;
line-height:3.5rem;
padding:0;
}
.playing{
border:1px solid #4caf50;
}
.selected{
border:1px solid #4caf50;
}
.liu-closePlayer{
border-radius:3px;
background-color: #141414;
float:right;
color: #99a2aa;
width:2rem;
height:2rem;
line-height:2rem;
padding:0;
margin:0.5rem 1rem;
}
.liu-closePlayer:hover{
background-color:#1f1f1f;
color:white;
}
.love-support{
margin-top:1rem;
background-color:#141414;
margin-right:1rem;
}
.love-support:hover{
background-color:#1f1f1f;
}
`
);
//剧集选择器布局
GM_addStyle(
`
.series-contianer{
display:grid;
grid-template-columns: repeat(5,1fr);
grid-column-gap:0.5rem;
grid-row-gap:0.5rem;
margin-top:1rem;
}
@media screen and (max-width: 1025px) {
.series-contianer{
display:grid;
grid-template-columns: repeat(5,1fr);
grid-column-gap:0.5rem;
grid-row-gap:0.5rem;
margin-top:1rem;
}
}
`
)
//布局
GM_addStyle(
`
:root{
font-size:16px
}
.TalionNav{
z-index:10;
}
.speed-slow{
color:#9e9e9e;
}
.speed-fast{
color:#4aa150;
}
.mannul{
margin:1rem;
font-size:1rem;
display:flex;
flex-wrap:wrap;
}
.liu-playContainer{
width:100%;
height:100%;
background-color:#1c2022;
position:fixed;
top:0;
z-index:11;
overflow:auto;
}
.video-selector{
display:flex;
flex-wrap:wrap;
margin-top:1rem;
}
.liu-selector:hover{
color:#aed0ee;
background-color:none;
}
.liu-selector{
color:black;
cursor:pointer;
padding:3px;
margin:5px;
border-radius:2px;
}
.liu-rapidPlay{
color: #007722;
}
.liu-light{
background-color:#7bed9f;
}
.artplayer-app{
height:600px;
}
.playSpace{
display: grid;
/* height:400px; */
margin:1rem;
grid-template-columns: 2fr 1fr;
grid-row-gap:0px;
grid-column-gap:1rem;
margin-top:2rem;
clear: both;
}
@media screen and (max-width: 1025px) {
.playSpace{
display: grid;
/* height:600px; */
grid-template-rows: 1fr 0.5fr;
grid-template-columns:1fr;
grid-row-gap:10px;
grid-column-gap:0px;
}
}
.seletor-title{
height:3rem;
line-height:3rem;
background-color: #141414;
color:#fafafa;
font-size:1.25rem;
padding: 0 1rem;
}
`
);
new PlayBtn();
addScript();
})();