/* globals Artplayer, XyMessage, Hls, artplayerPluginHlsControl */ // ==UserScript== // @name 我只想好好观影 // @namespace liuser.betterworld.love // @homepage https://bbs.kafan.cn/thread-2253400-1-1.html // @match https://movie.douban.com/subject/* // @match https://m.douban.com/movie/* // @exclude https://movie.douban.com/subject/*/episode/* // @grant GM_addStyle // @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.jsdelivr.net/gh/xinggsf/extFilter@master/lib/hls.min.js?t=12 // @require https://cdn.jsdelivr.net/npm/artplayer/dist/artplayer.js // @version 4.9 // @author liuser, modify by ray // @description 想看就看 // @license MIT // ==/UserScript== /* https://ghproxy.net/https://raw.github.com/xinggsf/extFilter/master/lib/hls.min.js https://artplayer.org/uncompiled/artplayer-plugin-hls-control/index.js ver4.6 修正下载DPL文件的BUG;更新神马源;在hls.js库中加入去广告功能 ver4.5 更换播放库hls.js,以适应:魔都云、闪电云、无尽云、樱花云 ver4.2 更新量子源;新增功能:导出potplayer播放列表 ver4.0 新增魔都云,修正可能出现的重复添加播放按钮 ver3.9 修正播放列表的样式,以匹配长片名 ver3.8 新增木耳、极速、豪华云,对空格分隔的片名进行处理~并校正名字二次搜索资源 ver3.7 更新暴风源、量子、樱花、新浪、索尼、无尽、鱼乐源 ver3.6 新增U酷源,更新非凡源 ver3.4 fix UI bug: 集数过多时撑大播放列表;新增飘花、樱花2个资源搜索 ver3.3 过滤掉量子云的电影解说;新增暴风源、快帆源、索尼源、天空源4个资源搜索;更新淘片源 */ (function () { const buffSize = GM_getValue('buffSize', 80); const _debug = 0; let art; //播放器 let seriesNum = 0; let potList = null; // 暂存剧集地址列表,用于导出potplayer播放列表 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); function html2DOM(html, pNode) { const e = document.createElement('template'); e.innerHTML = html.trim().replace(/[\r\n\t]/g,''); const r = e.content.firstChild; pNode?.append(e.content); return r; } //搜索源 const searchSource = [ //以下域名多数被污染!!必须修改hosts文件: 23.225.147.243 api.ffzyapi.com { name: "非凡云", searchUrl: "http://api.ffzyapi.com/api.php/provide/vod/" }, // ffzy5.tv { name: "量子云", searchUrl: "https://cj.lziapi.com/api.php/provide/vod/" }, { name: "神马云", searchUrl: "https://api.yzzy-api.com/inc/apijson.php" }, { name: "暴风云", searchUrl: "https://app.bfzyapi.com/api.php/provide/vod/"}, { name: "木耳云", searchUrl: "https://www.heimuer.tv/api.php/provide/vod/"}, { name: "魔都云", searchUrl: "https://caiji.moduapi.cc/api.php/provide/vod/"}, // { name: "红牛云", searchUrl: "https://www.hongniuzy2.com/api.php/provide/vod/josn/" }, //https://www.hongniuzy2.com/api.php/provide/vod/from/hnm3u8/ // { name: "豪华云", searchUrl: "https://hhzyapi.com/api.php/provide/vod/"}, // { name: "极速云", searchUrl: "https://8.218.111.47/api.php/provide/vod/"}, { name: "淘片云", searchUrl: "https://taopianapi.com/cjapi/mc/vod/json/m3u8.html" }, { name: "艾昆云", searchUrl: "https://ikunzyapi.com/api.php/provide/vod/from/ikm3u8/at/json/" }, //www.ikunzy.com { 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: "https://caiji.kczyapi.com/api.php/provide/vod/"}, // { name: "新浪云", searchUrl: "https://api.xinlangapi.com/xinlangapi.php/provide/vod/"}, // { 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://jyzyapi.com/provide/vod/from/jinyingm3u8/at/json" }, // { 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:"无尽云", 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) || r.list[0]; 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: 9000, responseType: 'json', onload(r) { resolve(handleResponse(r.response)); }, onerror() {resolve(0)}, ontimeout() {resolve(0)} }); }); //播放按钮 function playBtn() { const e = html2DOM(`<xy-button type="primary">一键播放</xy-button><xy-button type="primary">重设片名和年份</xy-button>`, $(isMobile ? ".sub-original-title" : "h1")); e.nextSibling.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 (this.loading) { this.loading = false; new UI(playList); } //渲染源列表 $(".sourceButtonList").appendChild(sourceButton({ name: item.name, playList })); }; this.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')) { this.loading = !1; tip("未能获取影视URL"); } }; } //影视源选择按钮 参数item 是 {name:"..云",playList:[{name:"第一集",url:""}]} function sourceButton(item) { potList = potList || item.playList; const btn = html2DOM(`<xy-button style="color:#a3a3a3" type="dashed">${item.name}</xy-button>`); btn.onclick = function(){ this.blur(); potList = item.playList; const pInfo = item.playList[seriesNum]; if (!pInfo) return; const time = art.currentTime; time && art.once("video:durationchange", () => { art.video.currentTime = time; }); art.url = pInfo.url; $(".series-select-space").innerHTML = ''; seriesContainer(item.playList); }; return btn; } //剧集选择器 function seriesButton(name, url, index) { const e = html2DOM(`<xy-button type="flat">${name}</xy-button>`); e.onclick = function() { this.blur(); 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'); return e; } //剧集选择的容器 function seriesContainer(playList) { const df = document.createDocumentFragment(); playList.forEach((k,i) => df.append(seriesButton(k.name, k.url, i))); $(".series-select-space").append(df); $(".next-series").hidden = $(".pot-playList").hidden = playList.length < 2; } class UI { constructor(playList) { const e = html2DOM( `<div class="liu-playContainer"> <a class="liu-closePlayer">关闭界面</a> <div class="sourceButtonList"></div> <div class="playSpace" style="width:100%"> <div class="artplayer-app"></div> <div class="series-select-space"></div> </div> <div> <span style="display:inline-block;color:#aaa">不要相信视频中的广告!!解决影视卡顿:快进几秒;或切换影视源,可点击之前选择的影视源</span> <div style="float:right;color:#4aa150;"> <a target="_blank" title="提示不安全,请允许浏览器继续访问" href="https://taopianapi.com/cjapi/mc/vod/json/m3u8.html">解决淘片云不能访问 </a> <a class="next-series">下一集 </a> <a class="pot-playList" title="下载DPL文件">PotPlayer播放列表 </a> <a class="cacheSize" title="设定视频缓存大小">⚙ 缓存 </a> <a target="_blank" title="微信打赏" href="https://cdn.jsdelivr.net/gh/xinggsf/extFilter@master/vx.png">请我喝杯☕</a> </div> </div> </div>`, document.body); e.querySelector(".cacheSize").onclick = function() { const n = +prompt('请输入视频缓存区大小,区间:15 - 800整数秒',''+ buffSize); if (n > 14 && n < 801) GM_setValue('buffSize', n|0); }; e.querySelector(".pot-playList").onclick = async function(ev){ ev.stopPropagation(); const a = potList.map((k,i) => `${i+1}*file*${k.url}\n${i+1}*title*${k.name}\n`); const time = art.currentTime*1000 || 500; a[seriesNum] += `${seriesNum+1}*start*${~~time}\n`;// 插入当前剧集播放进度。 pot列表索引从1开始,故+1 // 插入DPL文件头 a[0] = `DAUMPLAYLIST playname=${potList[seriesNum].url} topindex=0 saveplaypos=1 `.replace(/\t|\r| /g,'') + a[0]; this.download = vName + '.dpl'; this.href = URL.createObjectURL(new Blob(a)); await sleep(900); URL.revokeObjectURL(this.href); }; e.children[0].onclick = function() { art.destroy(); e.remove(); document.body.style.overflow = 'auto'; }; document.body.style.overflow = 'hidden'; e.querySelector(".next-series").onclick = function() { e.querySelector('.play + xy-button')?.click(); }; log(playList[seriesNum].url); initArt(playList[seriesNum].url); seriesContainer(playList); } } const artPlus = (option) => (art) => { Object.assign(art.icons, { forward: '<svg fill="#fff" viewBox="-9 -9 40 40"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z"></path></svg>', rewind: '<svg fill="#fff" viewBox="-9 -9 40 40"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z"></path></svg>', }); 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 loadM3u8(video, url) { if (Hls.isSupported()) { this.hls?.destroy(); this.hls = new Hls({ maxBufferSize: 36 << 20, // 36MB maxBufferLength: buffSize, maxMaxBufferLength: buffSize + 9, backBufferLength: 9 }); this.hls.loadSource(url); this.hls.attachMedia(video); this.on('destroy', () => this.hls.destroy()); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; } else { this.notice.show = '不支持的m3u8格式!'; } } //初始化播放器 function initArt(url) { art = new Artplayer({ container: ".artplayer-app", theme: 'green', url, pip: true, fullscreen: true, fullscreenWeb: true, screenshot: true, hotkey: true, airplay: true, playbackRate: true, plugins: [artPlus()], customType: {m3u8:loadM3u8} }); } 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; &:hover { color: #41ac52; } &: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; } &:hover:after { transform: skewX(-45deg) scale(1, 1); -webkit-transition: all 0.5s; transition: all 0.5s; } } xy-button { height:1.5em; cursor:pointer; } .playSpace { display: grid; height: calc(100vh - 3em); grid-template-rows: 1fr; grid-template-columns: calc(100vw - 25em) 25em; 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; */ & xy-button.play { color:purple; } & xy-button { color:#aaa; } } @media screen and (max-width: 1025px) { .playSpace{ grid-template-rows: 1fr 1fr; grid-template-columns:1fr; } }` ); playBtn(); })();