// ==UserScript==
// @name 我只想好好观影
// @namespace liuser.betterworld.love
// @match https://movie.douban.com/subject/*
// @match https://m.douban.com/movie/*
// @exclude https://movie.douban.com/subject/*/episode/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect *
// @run-at document-end
// @require https://cdn.jsdelivr.net/npm/xy-ui@1.10.7/+esm
// @require https://cdn.staticfile.net/mux.js/6.3.0/mux.min.js
// @require https://cdn.staticfile.net/shaka-player/4.7.6/shaka-player.compiled.min.js
// @require https://cdn.staticfile.net/artplayer/5.1.1/artplayer.min.js
// @version 4.1
// @author liuser, modify by ray
// @description 想看就看
// @license MIT
// ==/UserScript==
// ver4.0 新增魔都云,修正可能出现的重复添加播放按钮
// ver3.9 修正播放列表的样式,以匹配长片名
// ver3.8 新增木耳、极速、豪华云,对空格分隔的片名进行处理~并校正名字二次搜索资源
// ver3.7 更新暴风云、量子、樱花、新浪、索尼、无尽、鱼乐云
// ver3.6 新增U酷云,更新非凡云API
// ver3.4 fix UI bug: 集数过多时撑大播放列表;新增飘花、樱花2个资源搜索
// ver3.3 过滤掉量子云的电影解说;新增暴风云、快帆云、索尼云、天空云4个资源搜索;更新淘片云API地址
(function () {
const skBuffSize = GM_getValue('buffSize', 80);
const _debug = 0;
const isSafari = !self.chrome && navigator.userAgent.includes('Safari');
let art; //播放器
let seriesNum = 0;
const {query: $, queryAll: $$, isMobile} = Artplayer.utils;
const tip = (message) => XyMessage.info(message);
const noopFn = function() {};
const log = _debug ? console.log.bind(console) : noopFn;
const sleep = ms => new Promise(resolve => { setTimeout(resolve, ms) });
//豆瓣影片名及其年份
let vName = isMobile ? $(".sub-title").innerText : document.title.slice(0, -5);
let videoYear = $(isMobile ? ".sub-original-title" : ".year").innerText.slice(1, -1);
//将html转为element
function htmlToElement(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
//搜索源
const searchSource = [
{ name: "非凡云", searchUrl: "http://api.ffzyapi.com/api.php/provide/vod/" }, // www.ffzy.tv
{ name: "量子云", searchUrl: "http://23.224.101.30/api.php/provide/vod/" },
{ name: "神马云", searchUrl: "https://api.1080zyku.com/inc/apijson.php" },
{ name: "木耳云", searchUrl: "https://www.heimuer.tv/api.php/provide/vod/"},
{ name: "豪华云", searchUrl: "https://hhzyapi.com/api.php/provide/vod/"},
{ name: "极速云", searchUrl: "https://8.218.111.47/api.php/provide/vod/"},
{ name: "飞速云", searchUrl: "https://www.feisuzyapi.com/api.php/provide/vod/" },
{ name: "艾昆云", searchUrl: "https://ikunzyapi.com/api.php/provide/vod/from/ikm3u8/at/json/" },
{ name: "U酷云", searchUrl: "https://api.ukuapi.com/api.php/provide/vod/" },
{ name: "光速云", searchUrl: "https://api.guangsuapi.com/api.php/provide/vod/from/gsm3u8/" },
// { name: "红牛云", searchUrl: "http://www.hongniuzy2.com/api.php/provide/vod/" },
{ name: "暴风云", searchUrl: "https://app.bfzyapi.com/api.php/provide/vod/"},
// { name: "快车云", searchUrl: "https://caiji.kczyapi.com/api.php/provide/vod/from/kcm3u8/"},
{ name: "新浪云", searchUrl: "https://api.xinlangapi.com/xinlangapi.php/provide/vod/"},
{ name: "魔都云", searchUrl: "https://caiji.moduapi.cc/api.php/provide/vod/"},//须用hls.js解码播放 ?ac=list
// { name: "快帆云", searchUrl: "https://api.kuaifan.tv/api.php/provide/vod/"},
{ name: "索尼云", searchUrl: "https://suonizy.com/api.php/provide/vod/"},
{ name: "淘片云", searchUrl: "https://taopianapi.com/cjapi/mc/vod/json/m3u8.html" },
{ name: "樱花云", searchUrl: "https://m3u8.apiyhzy.com/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://api.apibdzy.com/api.php/provide/vod/" },
// { name: "酷点云", searchUrl: "https://kudian10.com/api.php/provide/vod/" },
{ name: "卧龙云", searchUrl: "https://collect.wolongzyw.com/api.php/provide/vod/" }, //非常恶心的广告
// { name: "ck云", searchUrl: "https://ckzy.me/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.ylzy.me/api.php/provide/vod/" },
{ name:"无尽云", searchUrl:"https://api.wujinapi.me/api.php/provide/vod/" }
];
//处理搜索到的结果:从返回结果中找到对应片子
function handleResponse(r) {
if (!r?.list?.length) {
log("未搜索到结果\n", r);
return 0
}
log("正在对比剧集年份");
const video = r.list.find(k => k.type_name != '电影解说' && k.vod_year == videoYear && k.vod_play_url);
if (!video) {
log("没有找到匹配剧集的影片,怎么回事哟!");
return 0
}
const a = video.vod_play_url.split("$$$").filter(str => str.includes(".m3u8"));
if (!a.length) {
log("没有m3u8资源,无法播放");
return 0
}
return a[0].split("#").map(s => {
let index = s.indexOf("$");
return { name: s.slice(0,index), url: s.slice(index + 1)}
});
}
//到电影网站搜索电影
const search = (url) => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: encodeURI(`${url}?ac=detail&wd=${vName}`),
timeout: 3000,
responseType: 'json',
onload(r) {
resolve(handleResponse(r.response));
},
onerror() {resolve(0)},
ontimeout() {resolve(0)}
});
});
//播放按钮
function playBtn() {
const e = htmlToElement(`一键播放`);
const eInfo = htmlToElement(`重设片名和年份`);
$(isMobile ? ".sub-original-title" : "h1").appendChild(e);
e.after(eInfo);
eInfo.onclick = function() {
const s = prompt(
'纠正片名和年份,二者用 | 号隔开。可以只输入片名,如片名有上下集~试着删掉空格及其后的字',
`${vName}|${videoYear}`);
if (s) {
([vName, videoYear = videoYear] = s.split('|'));
document.title = vName;
}
};
e.onclick = async function() {
// 二次搜索资源控制2变量
const secName = vName.includes(' ') ? vName.replace(' ', vName.includes(' 第') ?'':':') : null;
const sources = secName ? [] : null;
const render = async (item) => {
const playList = await search(item.searchUrl);
if (playList == 0) {
log(item.name +"获取或解析失败,可能有防火墙");
if (secName && !sources.includes(item)) sources.push(item);
return;
}
if (e.loading) {
e.loading = false;
new UI(playList);
}
//渲染资源列表
const btn = new SourceButton({ name: item.name, playList }).element;
$(".sourceButtonList").appendChild(btn);
};
e.loading = true;
tip("正在获取影视URL");
await Promise.allSettled(searchSource.map(render));
if (sources?.length) {
vName = secName;
await sleep(5000); // 防IP被封
await Promise.allSettled(sources.map(render));
}
if (!$('body > .liu-playContainer')) {
e.loading = !1;
tip("未能获取影视URL");
}
};
}
//影视源选择按钮
class SourceButton {
constructor(item) {
this.element = htmlToElement(`${item.name}`);
this.element.onclick = () => {
const list = item.playList[seriesNum];
if (!list) return;
const time = art.currentTime;
time && art.once("video:loadedmetadata", async () => {
await sleep(100);
if (art.duration > time) art.currentTime = time;
});
art.switchUrl(list.url);
$(".series-select-space").remove();
new SeriesContainer(item.playList);
};
}
//sources 是[{name:"..资源",playList:[{name:"第一集",url:""}]}]
}
//剧集选择器
class SeriesButton {
constructor(pNode, name, url, index) {
const e = pNode.appendChild(htmlToElement(
`${name}`
));
e.onclick = function() {
if (this.matches('.play')) return;
seriesNum = index;
art.switchUrl(url);
$('.play', this.parentNode)?.classList.remove('play');
this.classList.add('play');
};
if (seriesNum == index) e.classList.add('play');
}
}
//剧集选择器的container
class SeriesContainer {
constructor(playList) {
const e = htmlToElement(`
`);
for (let [index, item] of playList.entries()) {
new SeriesButton(e, item.name, item.url, index);
}
$(".playSpace").appendChild(e);
$(".next-series").hidden = playList.length < 2;
}
}
class UI {
constructor(playList) {
const e = document.body.appendChild(htmlToElement(
`
关闭界面
❤️支持开发者
不要相信视频中的广告!!!默认播放第一个搜索到的资源,若无法播放请切换其他资源。 部分影片选集后会出现卡顿,点击播放按钮或拖动一下进度条即可恢复。
下一集
`
));
$(".liu-closePlayer",e).onclick = function() {
art.destroy();
this.parentNode.remove();
document.body.style.overflow = 'auto';
};
document.body.style.overflow = 'hidden';
$(".next-series",e).onclick = function() {
$('.play + xy-button',e).click();
};
log(playList[seriesNum].url);
initArt(playList[seriesNum].url);
new SeriesContainer(playList);
}
}
const artPlus = (option) => (art) => {
Object.assign(art.icons, {
forward: '',
rewind: '',
});
const preventEvent = ev => {
if (!ev.target.closest('.art-control')) return;
ev.stopPropagation();
ev.preventDefault();
};
art.controls.add({
name: "forward",
html: art.icons.forward,
position: "left",
tooltip: "三键快进",
mounted(el) {
art.proxy(art.template.$controls,['contextmenu','mousedown','dblclick'],preventEvent);
art.controls.playAndPause.after(el);
art.proxy(el, 'mousedown', ev => {
if (art.duration && ev.button>0) art.currentTime += ev.button == 1 ? 1 : 20;
});
},
click(controls, ev) {
if (art.duration) art.currentTime += 5;
}
});
art.controls.add({
name: "rewind",
html: art.icons.rewind,
position: "left",
tooltip: "三键快退",
mounted(el) {
art.controls.playAndPause.before(el);
art.proxy(el, 'mousedown', ev => {
if (art.duration && ev.button>0) art.currentTime -= ev.button == 1 ? 1 : 20;
});
},
click(controls, ev) {
if (art.duration) art.currentTime -= 5;
}
});
art.controls.add({
name: "resolution",
html: "分辨率",
position: "right",
click(controls, ev) {
art.info.show = !art.info.show;
}
});
// art.on('dblclick', preventEvent);
art.on("video:loadedmetadata", () => {
art.controls.resolution.innerText = art.video.videoHeight + "P";
});
return {name: 'artPlus'};
};
//初始化播放器
function initArt(url) {
let playRate;
art = new Artplayer({
container: ".artplayer-app",
url, pip: true,
fullscreen: true,
fullscreenWeb: true,
screenshot: true,
hotkey: true,
airplay: true,
playbackRate: true,
plugins: [artPlus()],
customType: {
m3u8(v, url) {
if (isSafari && url.endsWith('.m3u8')) {
v.src = url;
return;
}
if (!this.shaka) {
this.shaka = new shaka.Player(v);
this.shaka.configure({
streaming: {
bufferingGoal: skBuffSize +9,
// rebufferingGoal: 15,
bufferBehind: skBuffSize,
}
});
}
this.shaka.load(url);
log(this, 'load:\n'+ url);
}
}
});
art.once('destroy', () => art.shaka?.destroy());
art.on("video:loadedmetadata", async () => {
await sleep(2300);
art.playbackRate = +localStorage.mvPlayRate || 1;
});
}
GM_addStyle(
`.liu-playContainer{
width:100%;
height:100%;
background-color:#222;
position:fixed;
top:0;
z-index:11;
}
.liu-closePlayer{
float:right;
margin-inline:10px;
color:white;
}
.liu-btn {
width: 6.5em;
height: 2em;
margin: 0.5em;
background: #41ac52;
color: white;
border: none;
border-radius: 0.625em;
font-size: 20px;
font-weight: bold;
cursor: pointer;
position: relative;
z-index: 1;
overflow: hidden;
}
.liu-btn:hover {
color: #41ac52;
}
.liu-btn:after {
content: '';
background: white;
position: absolute;
z-index: -1;
left: -20%;
right: -20%;
top: 0;
bottom: 0;
transform: skewX(-45deg) scale(0, 1);
transition: all 0.5s;
}
.liu-btn:hover:after {
transform: skewX(-45deg) scale(1, 1);
-webkit-transition: all 0.5s;
transition: all 0.5s;
}
xy-button{
height:1.5em;
cursor:pointer;
}
.series-select-space xy-button.play{
color:purple;
}
.series-select-space xy-button{
color:#aaa;
}
.playSpace{
display: grid;
height:96vh;
grid-template-rows: 1fr;
grid-template-columns: calc(100vw - 28em) 28em;
grid-gap:0;
}
.series-select-space{
overflow-y: auto;
display: flex;
flex-flow: row wrap;
align-items: flex-start;
align-content: flex-start;
/* grid-gap: 0;
grid-auto-rows: 1.8em;
grid-template-columns: auto auto auto auto auto; */
}
@media screen and (max-width: 1025px) {
.playSpace{
grid-template-rows: 1fr 1fr;
grid-template-columns:1fr;
}
}`
);
playBtn();
GM_registerMenuCommand('设定视频缓存区大小', () => {
const n = +prompt('请输入视频缓存区大小,区间:10 - 800整数秒',''+skBuffSize);
n > 9 && n < 801 && GM_setValue('buffSize', n|0);
});
})();