// ==UserScript==
// @name JavBus工具
// @description 暗黑模式、滚动加载、预览视频、离线下载(115)...
// @version 0.2.0
// @icon https://z3.ax1x.com/2021/10/15/53gMFS.png
// @include *://*.javbus.com/*
// @include *://captchaapi.115.com/*
// @require https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js
// @require https://unpkg.com/infinite-scroll@4/dist/infinite-scroll.pkgd.min.js
// @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js
// @resource fail https://z3.ax1x.com/2021/10/15/53gcex.png
// @resource info https://z3.ax1x.com/2021/10/15/53g2TK.png
// @resource success https://z3.ax1x.com/2021/10/15/53gqTf.png
// @run-at document-start
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @grant GM_setClipboard
// @grant GM_getResourceURL
// @grant GM_openInTab
// @grant GM_info
// @grant GM_registerMenuCommand
// @connect *
// @license MIT
// @namespace https://greasyfork.org/users/175514
// ==/UserScript==
/**
* 非完全测试,自用Edge
* TODO:
* ✔️ 暗黑模式
* ✔️ 点击事件(新窗口打开,左键前台,右键后台)
* ✔️ 滚动加载
* ✔️ 获取预览大图(javstore.net)
* ✔️ 获取预览视频(r18.com),如有资源点击封面图或样品图像处进行预览
* ✔️ 获取演员名单(javdb.com)
* ✔️ 离线下载(请求离线成功2秒后查询结果汇报),自动修改文件名称,(如已设置操作目录cid,自动移动视频文件至对应目录并删除原目录)
* ✔️ 一键离线(字幕>尺寸>日期)排队执行离线下载,成功一结束排队
* ✔️ 115账号验证弹窗
* ✔️ 查询115是否已有离线资源
* ✔️ 查询115是否已有离线资源并缓存(默认3天有效),3天后重新查询,支持手动刷新“资源刷新”
* ✔️ 根据缓存数据匹配列表页已有资源状态,添加已有资源列表页
*/
(function () {
"use strict";
const SUFFIX = " !important";
const dm = GM_getValue("dm") ?? matchMedia("(prefers-color-scheme: dark)").matches;
const lm = GM_getValue("lm") ?? false;
const ck = GM_getValue("ck") ?? true;
const rootId = GM_getValue("rid") ?? "";
let mousePoint = 0;
const { host, pathname } = location;
const doc = document;
doc.create = (tag, attr = {}, child = "") => {
if (!tag) return null;
tag = doc.createElement(tag);
Object.keys(attr).forEach(name => {
tag.setAttribute(name, attr[name]);
});
typeof child === "string" && tag.appendChild(doc.createTextNode(child));
typeof child === "object" && tag.appendChild(child);
return tag;
};
const insertBefore = (el, htmlString) => el.insertAdjacentHTML("beforebegin", htmlString);
const insertAfter = (el, htmlString) => el.insertAdjacentHTML("afterend", htmlString);
const appendBefore = (el, htmlString) => el.insertAdjacentHTML("afterbegin", htmlString);
const appendAfter = (el, htmlString) => el.insertAdjacentHTML("beforeend", htmlString);
const lf = localforage;
lf.upItem = async (key, val) => {
let item = (await lf.getItem(key)) ?? {};
await lf.setItem(key, Object.assign(item, val));
};
// 网络请求
const request = (url, method = "GET", data = {} | "", params = {}) => {
if (!url) return;
if (method === "POST") {
params.headers = Object.assign(params.headers ?? {}, {
"Content-Type": "application/x-www-form-urlencoded",
});
}
if (typeof data === "object" && Object.keys(data).length) {
data = Object.keys(data).reduce(
(pre, cur, index, arr) =>
`${pre}=${data[pre]}&${cur}=${data[cur]}${index !== arr.length - 1 ? "&" : ""}`
);
data = encodeURI(data);
if (method === "GET") url = `${url}?${data}`;
}
return new Promise(resolve => {
GM_xmlhttpRequest({
url,
method,
data,
timeout: 20000,
onload: ({ responseText }) => {
if (/<\/?[a-z][\s\S]*>/i.test(responseText)) {
responseText = new DOMParser().parseFromString(responseText, "text/html");
}
if (/^{.*}$/.test(responseText)) {
responseText = JSON.parse(responseText);
}
const errcode = responseText?.errcode;
if (`${errcode}` === "911") verify();
resolve(responseText);
},
...params,
});
});
};
const notifiy = (title = "", text = "", icon = "info", clickUrl = "", params = {}) => {
if (typeof title === "object") params = title;
GM_notification({
title,
text: text || GM_info.script.name,
image: GM_getResourceURL(params.icon || icon),
highlight: true,
timeout: 3000,
onclick: () => {
clickUrl = params.clickUrl || clickUrl;
if (!clickUrl) return;
GM_openInTab(clickUrl, { active: true });
},
...params,
});
};
// 115验证账号
const verify = async () => {
const time = new Date().getTime();
let h = 667;
let w = 375;
let t = (window.screen.availHeight - h) / 2;
let l = (window.screen.availWidth - w) / 2;
window.open(
`https://captchaapi.115.com/?ac=security_code&type=web&cb=Close911_${time}`,
"请验证账号",
`height=${h},width=${w},top=${t},left=${l},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
);
};
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const scriptStart = () => {
doc.addEventListener("keyup", event => {
const e = event || window.event;
if (e && e.keyCode === 191 && !["INPUT", "TEXTAREA"].includes(doc.activeElement.nodeName)) {
doc.querySelector("#search-input").focus();
}
});
const nav = `<a href="https://www.javbus.com/resource">资源</a>`;
let navbar = doc.querySelector("#navbar .nav.navbar-nav");
if (navbar) appendAfter(navbar, `<li class="resource">${nav}</li>`);
navbar = doc.querySelector("#toptb .wp .z ul");
if (navbar) appendAfter(navbar, `<li class="nav-title nav-inactive">${nav}</li>`);
};
class Common {
docStart = () => {};
contentLoaded = () => {};
load = () => {};
}
class Waterfall extends Common {
isHome = /^(\/(page\/\d+)?|\/uncensored(\/page\/\d+)?)+$/i.test(pathname);
docStart = () => {
GM_addStyle(`
.search-header {
padding: 0${SUFFIX};
background: none${SUFFIX};
box-shadow: none${SUFFIX};
}
.photo-frame {
position: relative;
margin: 10px${SUFFIX};
}
.photo-frame img {
height: 100%${SUFFIX};
width: 100%${SUFFIX};
object-fit: cover${SUFFIX};
margin: 0${SUFFIX};
}
.photo-info {
padding: 10px${SUFFIX};
}
.alert-page {
margin: 20px${SUFFIX};
}
`);
if (!lm) return;
const itemSizer = `167px`;
const gutterSizer = `20px`;
GM_addStyle(`
.pagination,
footer {
display: none${SUFFIX};
}
.page-load-status {
display: none;
padding-bottom: ${gutterSizer};
text-align: center;
}
body {
overflow: hidden;
}
.scrollBox {
height: calc(100vh - 50px);
overflow: hidden;
overflow-y: scroll;
}
#waterfall {
opacity: 0;
margin: ${gutterSizer} auto 0 auto${SUFFIX};
}
.item-sizer,
.item a {
width: ${itemSizer}${SUFFIX};
}
.gutter-sizer {
width: ${gutterSizer}${SUFFIX};
}
.item a {
margin: 0 0 ${gutterSizer} 0${SUFFIX};
}
`);
GM_addStyle(`
.hasRes {
background-color: rgba(0,0,0,.2);
position: absolute;
height: 100%;
width: 100%;
top: 0;
display: flex;
align-items: center;
justify-content: center;
}
.playBtn {
opacity: .85;
}
`);
};
contentLoaded = () => {
const nav = doc.querySelector(".search-header .nav");
if (nav) nav.setAttribute("class", "nav nav-pills");
ck && this.handleClick();
if (!lm) return;
this.handleLoadMore();
if (!this.isHome) this.modifyLayout();
};
load = () => {
if (lm && this.isHome) this.modifyLayout();
};
handleClick = () => {
const isItem = e => {
const elem = e.target.offsetParent;
let re = elem.nodeName.toLowerCase() === "div" && /^item/i.test(elem.className);
if (re) re = elem.querySelector("a").href;
return re;
};
doc.body.addEventListener("contextmenu", e => {
isItem(e) && e.preventDefault();
});
doc.body.addEventListener("click", e => {
const href = isItem(e);
if (!href) return;
e.preventDefault();
GM_openInTab(href, { active: true });
});
doc.body.addEventListener("mousedown", e => {
const href = isItem(e);
if (!href || e.button !== 2) return;
e.preventDefault();
mousePoint = e.screenX + e.screenY;
});
doc.body.addEventListener("mouseup", e => {
const href = isItem(e);
const num = e.screenX + e.screenY - mousePoint;
if (!href || e.button !== 2 || num > 5 || num < -5) return;
e.preventDefault();
GM_openInTab(href);
});
};
handleLoadMore = () => {
let oldWaterfall = doc.querySelector("#waterfall");
if (!oldWaterfall) return GM_addStyle(`#waterfall { opacity: 1; }`);
let newWaterfall = doc.querySelector("#waterfall #waterfall");
if (newWaterfall) oldWaterfall.parentNode.replaceChild(newWaterfall, oldWaterfall);
// add element
const waterfall = doc.querySelector("#waterfall");
appendBefore(waterfall, `<div class="item-sizer"></div><div class="gutter-sizer"></div>`);
insertAfter(
waterfall,
`<div class="page-load-status"><span class="loader-ellips infinite-scroll-request">Loading...</span><span class="infinite-scroll-last">End of content</span><span class="infinite-scroll-error">No more pages to load</span></div>`
);
doc.querySelectorAll("div.container-fluid")[1].querySelector(".row").classList.add("scrollBox");
};
modifyLayout = () => {
let waterfall = doc.querySelector("#waterfall");
if (!waterfall) return;
let msnry = new Masonry(waterfall, {
itemSelector: "none",
columnWidth: ".item-sizer",
gutter: ".gutter-sizer",
horizontalOrder: true,
fitWidth: true,
stagger: 30,
visibleStyle: { transform: "translateY(0)", opacity: 1 },
hiddenStyle: { transform: "translateY(120px)", opacity: 0 },
});
imagesLoaded(waterfall, () => {
msnry.options.itemSelector = ".item";
let elems = waterfall.querySelectorAll(".item");
this.modifyItem();
msnry.appended(elems);
GM_addStyle(`#waterfall { opacity: 1; }`);
});
// 搜索页滚动加载需特殊处理
const path = !/^\/(uncensored\/)?(search|searchstar)+\//i.test(pathname)
? "#next"
: function () {
const items = ["search", "searchstar"];
for (const item of items) {
if (pathname.indexOf(`${item}/`) < 0) continue;
let [prefix, suffix] = pathname.split("&");
suffix ??= "";
prefix = prefix.split("/");
let pre = "";
for (let index = 0; index <= prefix.indexOf(item) + 1; index++) {
pre = `${pre}${prefix[index]}/`;
}
return `${pre}${this.loadCount + 2}&${suffix}`;
}
};
let infScroll = new InfiniteScroll(waterfall, {
path,
append: ".item",
outlayer: msnry,
elementScroll: ".scrollBox",
history: false,
historyTitle: false,
hideNav: ".pagination",
status: ".page-load-status",
debug: false,
});
infScroll.on("load", this.modifyItem);
};
modifyItem = (node = doc) => {
const items = node.querySelectorAll(".item");
for (const item of items) {
let [code, date] = item.querySelectorAll("date");
if (code && date) {
code = code.textContent;
date = date.textContent;
const codeKey = `${code.trim().toUpperCase()}/${date.trim().toUpperCase()}`;
lf.keys().then(keys => {
if (!keys.includes(codeKey)) return;
lf.getItem(codeKey).then(val => {
const resource = val?.resource;
if (!resource?.length) return;
const hasRes = doc.create("div", { class: "hasRes" });
const playBtn = doc.create(
"button",
{ class: "btn btn-primary playBtn", title: "点击播放" },
"已有资源"
);
playBtn.addEventListener("click", e => {
e.stopPropagation();
e.preventDefault();
window.open(resource[0].link);
});
hasRes.appendChild(playBtn);
item.querySelector(".photo-frame").appendChild(hasRes);
});
});
}
const info = item.querySelector("a .photo-info span:not(.mleft)");
if (!info) return;
const [titleNode, secondaryNode] = info.childNodes;
const titleTxt = titleNode.nodeValue.trim();
const ellipsis = doc.create("div", { class: "ellipsis", title: titleTxt }, titleTxt);
if (secondaryNode && secondaryNode.nodeName === "BR") {
info.removeChild(secondaryNode);
ellipsis.classList.add("line-4");
}
titleNode.parentNode.replaceChild(ellipsis, titleNode);
}
};
}
class Details extends Common {
codeKey = "";
code = "";
lfItem = {};
config = async codeKey => {
if (!codeKey) return;
this.codeKey = codeKey;
this.code = codeKey.split("/")[0];
this.lfItem = (await lf.getItem(codeKey)) ?? {};
};
docStart = () => {
GM_addStyle(`
.info .glyphicon-info-sign,
h4[style="position:relative"],
h4[style="position:relative"] + .row {
display: none${SUFFIX};
}
.info ul {
margin: 0${SUFFIX};
}
.screencap {
max-height: 600px;
overflow: hidden;
}
#avatar-waterfall,
#sample-waterfall,
#related-waterfall {
margin: -5px${SUFFIX};
}
.screencap {
max-height: 600px;
overflow: hidden;
}
.photo-info {
height: auto${SUFFIX};
}
`);
GM_addStyle(`
#previewVideo { position: relative; }
#previewVideo:after {
background: url(https://javdb.com/packs/media/images/btn-play-b414746c.svg) 50% no-repeat;
background-color: rgba(0,0,0,.2);
background-size: 40px 40px;
bottom: 0;
content: "";
display: block;
left: 0;
position: absolute;
right: 0;
top: 0;
height: 100%;
}
`);
GM_addStyle(`
#mask {
position: fixed;
width: 100%;
height: 100%;
z-index: 9999;
left: 0;
top: 0;
background: rgba(11,11,11,.8);
display: flex;
justify-content: center;
align-items: center;
display: none;
}
`);
GM_addStyle(`
#exp {
display: none;
}
#expBtn {
position: absolute;
top: 0;
height: 40px;
line-height: 40px;
left: 0;
right: 0;
cursor: pointer;
background: rgba(0,0,0,.7);
color: #fff;
margin: 560px 15px 0 15px;
z-index: 99;
}
#expBtn::after {
content: "展开";
}
#exp:checked + .screencap {
max-height: none;
}
#exp:checked + .screencap > #expBtn {
top: auto;
margin: 0 15px;
bottom: 0;
}
#exp:checked + .screencap > #expBtn::after {
content: "收起";
}
@media screen and (max-width: 480px) {
#btnGrp {
margin-bottom: 15px;
}
.screencap {
max-height: 280px;
}
#expBtn {
margin-left: 0;
margin-right: 0;
}
}
#resBox a {
color: #CC0000${SUFFIX};
}
`);
doc.addEventListener("DOMNodeInserted", e => {
let node = e.target;
if (node.nodeName.toLowerCase() !== "tr") return;
let href = node.querySelector("td a")?.href;
if (!href) return;
let td = doc.create("td", { style: "text-align:center;white-space:nowrap" });
let copy = doc.create("a", { href, title: href }, "复制");
let offline = doc.create("a", { href, style: "margin-left:16px" }, "离线下载");
copy.addEventListener("click", this.copyTxt);
offline.addEventListener("click", this.offLine);
td.appendChild(copy);
td.appendChild(offline);
node.appendChild(td);
});
};
contentLoaded = async () => {
ck && this.handleClick();
// copy
const handleCopy = tag => {
const node = doc.querySelector(tag);
if (!node) return;
const href = node.innerText;
const copy = doc.create("a", { title: href, href, style: "margin-left:16px" }, "复制");
copy.addEventListener("click", this.copyTxt);
node.appendChild(copy);
};
handleCopy("h3");
handleCopy("span[style='color:#CC0000;']");
// expBtn
const screencap = doc.querySelector(".col-md-9.screencap");
insertBefore(screencap, `<input type="checkbox" id="exp">`);
appendAfter(screencap, `<label for="exp" id="expBtn"></label>`);
const info = doc.querySelector(".col-md-3.info");
// resource
appendAfter(
info,
`<p id="resBox"><span class="header">已有资源:</span><span class="genre">查询中...</span></p>`
);
// btnGrp
const btnGrp = doc.create("div", {
id: "btnGrp",
class: "btn-group btn-group-justified",
role: "group",
"aria-label": "options",
});
const btns = { smartRes: "一键离线", refreshRes: "资源刷新" };
Object.keys(btns).forEach(id => {
let btn = doc.create("a", { id, class: "btn btn-default" }, btns[id]);
btn.addEventListener("click", () => this.handleOpt(id));
btnGrp.appendChild(btn);
});
info.appendChild(btnGrp);
// table
appendAfter(
doc.querySelector("#magnet-table tbody tr"),
`<td style="text-align:center;white-space:nowrap">操作</td>`
);
// photoinfo
for (const item of doc.querySelectorAll(".photo-info span")) {
item.classList.add("ellipsis");
}
// config
if (!this.codeKey) {
let [code, time] = doc.querySelectorAll("div.col-md-3.info p");
code = code.querySelectorAll("span")[1].childNodes[0].nodeValue.trim().toUpperCase();
time = time.childNodes[1].nodeValue.trim().toUpperCase();
await this.config(`${code}/${time}`);
}
this.handleResource();
this.handleVideo();
this.handlePreview();
this.handleStar();
};
handleClick = () => {
const links = doc.querySelectorAll(".movie-box");
for (const link of links) {
const { href } = link;
link.addEventListener("contextmenu", e => e.preventDefault());
link.addEventListener("click", e => {
e.preventDefault();
GM_openInTab(href, { active: true });
});
link.addEventListener("mousedown", e => {
if (e.button !== 2) return;
e.preventDefault();
mousePoint = e.screenX + e.screenY;
});
link.addEventListener("mouseup", e => {
const num = e.screenX + e.screenY - mousePoint;
if (e.button !== 2 || num > 5 || num < -5) return;
e.preventDefault();
GM_openInTab(href);
});
}
};
load = () => {
const maxHei = 600;
let styleHei = doc.querySelector(".col-md-9.screencap .bigImage img").height;
if (styleHei > maxHei) {
GM_addStyle(`
.screencap { max-height: ${maxHei}px; }
#expBtn { margin-top: ${maxHei - 40}px; }
`);
} else {
GM_addStyle(`
.screencap { max-height: ${styleHei + 40}px; }
#expBtn { margin-top: ${styleHei}px; }
`);
}
};
copyTxt = e => {
e.preventDefault();
e.stopPropagation();
let node = e.target;
let { title } = node;
if (!title) return;
GM_setClipboard(title);
const text = node.innerText;
node.innerText = "成功";
setTimeout(() => {
node.innerText = text;
}, 1000);
};
// 预览大图
handlePreview = async () => {
let image = this.lfItem?.image;
if (!image) {
let res = await request(`https://javstore.net/search/${this.code}.html`);
const link = res?.querySelector("#content_news li a")?.href;
if (link) res = await request(link);
image = res?.querySelector(".news a img[alt*='.th']")?.src?.replace(".th", "");
if (!image) return;
lf.upItem(this.codeKey, { image });
}
const img = doc.create("img", { src: image, title: "点击收起", style: "cursor:pointer" });
img.addEventListener("click", () => {
doc.querySelector("#exp").checked = false;
});
const append = () => doc.querySelector(".col-md-9.screencap").appendChild(img);
if (img.complete) return append();
img.onload = append;
};
// 预览视频
handleVideo = async () => {
let video = this.lfItem?.video;
if (!video) {
const res = await request(`https://www.r18.com/common/search/searchword=${this.code}/`);
video = res?.querySelector("a.js-view-sample")?.getAttribute("data-video-high");
if (!video) return;
lf.upItem(this.codeKey, { video });
}
const title = "预览视频";
// 打开视频弹窗
const playVideo = e => {
e.preventDefault();
e.stopPropagation();
doc.body.setAttribute("style", "overflow: hidden;");
doc.querySelector("#mask").setAttribute("style", "display: flex;");
const video = doc.querySelector("video");
video.play();
video.focus();
doc.onkeydown = event => {
const e = event || window.event;
if (e && e.keyCode === 27) pauseVideo();
};
};
// 关闭视频弹窗
const pauseVideo = () => {
doc.body.setAttribute("style", "overflow: auto;");
doc.querySelector("#mask").setAttribute("style", "display: none;");
doc.querySelector("video").pause();
doc.onkeydown = null;
};
// 视频播放窗口
const videoNode = doc.create("video", { controls: "controls", src: video });
videoNode.currentTime = 5;
videoNode.preload = "auto";
videoNode.muted = true;
const closeBtn = doc.create("button", { title: "Close (Esc)", type: "button", class: "mfp-close" }, "×");
closeBtn.addEventListener("click", pauseVideo);
const mask = doc.create("div", { id: "mask" });
mask.appendChild(closeBtn);
mask.appendChild(videoNode);
doc.body.appendChild(mask);
// 封面图点击播放
const bImg = doc.querySelector(".bigImage img");
bImg.setAttribute("title", title);
bImg.addEventListener("click", playVideo);
// ”样品图像“添加播放项目
const thumb = bImg.src;
const box = doc.create("a", {
class: "sample-box",
id: "previewVideo",
href: thumb,
title,
});
box.addEventListener("click", playVideo);
appendAfter(box, `<div class="photo-frame"><img src="${thumb}" title="${title}"></div>`);
if (!doc.querySelector("#sample-waterfall")) {
insertBefore(doc.querySelector("div.clearfix"), `<div id="sample-waterfall"></div>`);
insertBefore(doc.querySelector("div#sample-waterfall"), `<h4>樣品圖像</h4>`);
}
const waterfall = doc.querySelector("div#sample-waterfall");
const ref = waterfall.querySelector("a");
ref ? waterfall.insertBefore(box, ref) : waterfall.appendChild(box);
};
// 演员列表
handleStar = async () => {
const nodes = doc.querySelector(".col-md-3.info").childNodes;
let starNode = "";
for (const node of nodes) {
if (node.nodeType === 3 && node.nodeValue.trim() === "暫無出演者資訊") {
starNode = node;
break;
}
}
if (!starNode) return;
let star = this.lfItem?.star ?? [];
if (!star.length) {
const site = "https://javdb.com";
let res = await request(`${site}/search?q=${this.code}`);
const href = res.querySelector("#videos .grid-item a").getAttribute("href");
if (href) res = await request(`${site}${href}`);
let panels = res
?.querySelector(".video-meta-panel")
?.querySelectorAll(".column")[1]
?.querySelectorAll(".panel-block");
if (!panels) return;
panels = panels[panels.length - 3]?.querySelector(".value")?.querySelectorAll("a") || [];
for (const panel of panels) {
const starName = panel.innerHTML.trim();
if (starName) star.push(starName);
}
if (!star.length) return;
lf.upItem(this.codeKey, { star });
}
const p = doc.create("p");
const urlStr = "https://www.javbus.com/search/";
appendAfter(
p,
star.reduce((acc, cur) => `${acc}<span class="genre"><a href="${urlStr}${cur}">${cur}</a></span>`, "")
);
starNode.parentNode.replaceChild(p, starNode);
};
// 已有资源(本地)
handleResource = async () => {
const lfItem = await lf.getItem(this.codeKey);
let resource = lfItem?.resource ?? [];
const upDate = lfItem?.upDate;
const bool = !upDate || Math.floor((new Date().getTime() - upDate) / 24 / 3600 / 1000) > 3;
if (bool) resource = await this.fetchResource();
const resBox = doc.querySelector("#resBox");
for (const old of resBox.querySelectorAll(".genre")) resBox.removeChild(old);
if (!resource?.length) return appendAfter(resBox, `<span class="genre">无</span>`);
const child = resource.reduce((acc, { link, getDate, name }) => {
let thunbName = name.replace(/\.\w+$/gi, "");
thunbName = thunbName.length > 20 ? `${thunbName.substr(0, 20)}...` : thunbName;
return `${acc}<span class="genre"><a href="${link}" title="${getDate}/${name}" target="_blank">${thunbName}</a></span>`;
}, "");
appendAfter(resBox, child);
if (!lfItem?.thumb) lf.upItem(this.codeKey, { thumb: doc.querySelector(".screencap .bigImage img").src });
if (!lfItem?.title) lf.upItem(this.codeKey, { title: doc.querySelector("h3").childNodes[0].textContent });
if (!lfItem?.href) lf.upItem(this.codeKey, { href: location.href });
};
// 已有资源(115)
fetchResource = async (lname = "") => {
let code = this.code;
let codes = [
code,
code.replace(/-/g, ""),
code.replace(/-/g, "-0"),
code.replace(/-/g, "0"),
code.replace(/-/g, "00"),
code.replace(/-/g, "_"),
code.replace(/-/g, "_0"),
code.replace(/-0/g, ""),
code.replace(/-0/g, "-"),
code.replace(/-0/g, "00"),
];
if (lname) codes.unshift(lname);
let { data } = await request("https://webapi.115.com/files/search", "GET", {
search_value: encodeURIComponent(codes.join(" ")),
format: "json",
});
let resource = [];
if (data && data?.length) {
const reg = new RegExp(`(${codes.join("|")})`, "gi");
data = data.filter(({ n, play_long }) => n.match(reg) && play_long > 0);
resource = data.map(item => {
return {
fid: item.fid,
cid: item.cid,
link: `https://v.anxia.com/?pickcode=${item.pc}`,
getDate: item.t,
name: item.n,
};
});
}
await lf.upItem(this.codeKey, { upDate: new Date().getTime(), resource });
return resource;
};
// 离线下载
offLine = async e => {
e.preventDefault();
e.stopPropagation();
const node = e.target;
const { href } = node;
if (!href) return;
GM_setClipboard(href);
const text = node.innerText;
node.innerText = "请求中...";
const zh = !!node.parentNode.parentNode.querySelector("td a.btn.btn-mini-new.btn-warning.disabled");
const lname = node.parentNode.parentNode.querySelector("td a").innerText.trim();
const obj = await this.offLineDownload({ link: href, zh, lname });
node.innerText = text;
notifiy(obj);
};
// 排队离线/资源刷新
handleOpt = async action => {
const node = doc.querySelector(`#${action}`);
node.classList.toggle("disabled");
const text = node.innerText;
node.innerText = "请求中...";
if (action === "refreshRes") {
await lf.upItem(this.codeKey, { upDate: 0 });
await this.handleResource();
}
if (action === "smartRes") {
const trs = doc.querySelector("#magnet-table").querySelectorAll("tr");
let magnetArr = [];
for (let index = 1; index < trs.length; index++) {
let item = { zh: false, size: 0, date: 0, lname: "" };
const elem = trs[index];
let [zh, size, date] = elem.querySelectorAll("td");
if (!zh || !size || !date) continue;
const ls = zh.querySelectorAll("a");
item.lname = ls[0].innerText.trim();
for (const a of ls) {
item.zh = a.innerText.trim() === "字幕";
if (item.zh) break;
}
size = size.querySelector("a").innerText.trim().replace(/gb/gi, "");
if (/mb/gi.test(size)) size = (parseInt(size, 10) / 1024).toFixed(2);
item.size = Number(size);
date = date.querySelector("a");
item.date = date.innerText.trim().replace(/-/g, "");
item.link = date.getAttribute("href");
magnetArr.push(item);
}
magnetArr.sort((pre, next) => {
if (pre.zh === next.zh) {
if (pre.size === next.size) return next.date - pre.date;
return next.size - pre.size;
} else {
return pre.zh > next.zh ? -1 : 1;
}
});
for (let index = 0; index < magnetArr.length; index++) {
const obj = await this.offLineDownload(magnetArr[index]);
if (obj?.icon !== "info") {
notifiy(obj);
break;
}
if (index !== magnetArr.length - 1) continue;
notifiy(
"一键离线失败",
"远程未查找到新增资源,接口失效或资源被审核",
"fail",
"http://115.com/?tab=offline&mode=wangpan"
);
}
}
node.innerText = text;
node.classList.toggle("disabled");
};
// 请求离线&结果查询
offLineDownload = async ({ link, zh, lname }) => {
let fname = doc.querySelector("h3").childNodes[0].nodeValue;
if (zh) fname = `【中文字幕】${fname}`;
let notifiyObj = {
title: "操作失败,115未登录",
text: "请登录115账户后再离线下载",
icon: "fail",
clickUrl: "http://115.com/?mode=login",
};
let res = await request("http://115.com/", "GET", {
ct: "offline",
ac: "space",
_: new Date().getTime(),
});
if (!res?.sign) return notifiyObj;
const { sign, time } = res;
res = await request(
"http://115.com/web/lixian/?ct=lixian&ac=add_task_url",
"POST",
`url=${encodeURIComponent(link.substr(0, 60))}&uid=0&sign=${sign}&time=${time}`
);
const { state, errcode, error_msg } = res;
notifiyObj = {
title: "离线失败",
text: error_msg,
icon: "info",
clickUrl: "http://115.com/?tab=offline&mode=wangpan",
};
if (`${errcode}` === "911") {
notifiyObj.title += ",账号异常";
notifiyObj.text = "验证后正常使用";
notifiyObj.icon = "fail";
}
if (!state) return notifiyObj;
// 获取旧的本地缓存数据
let lfItem = (await lf.getItem(this.codeKey)) ?? {};
if (!lfItem?.resource) lfItem.resource = [];
// 远程获取搜索结果
await delay(2000);
let resource = await this.fetchResource(lname);
this.handleResource();
// 当前日期
let date = new Date();
const Y = date.getFullYear();
const M = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
const D = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
date = `${Y}-${M}-${D}`;
const newRes = resource.filter(item => item.getDate === date);
// 搜索结果资源数至少比本地缓存多且获取日期为当前日期
if (resource.length <= lfItem.resource.length || newRes.length < 1) {
notifiyObj.text = "未找到新增资源,接口失效或资源审核";
return notifiyObj;
}
for (let index = 0; index < newRes.length; index++) {
const { name } = newRes[index];
const suffix = name.split(".").pop().toLowerCase();
newRes[index]["rename"] = `${fname}.${suffix}`;
}
this.afterAction(newRes.filter(item => item.name.indexOf(fname) === -1));
return {
title: "离线成功",
text: "点击跳转",
icon: "success",
clickUrl: `https://115.com/?cid=${rootId ?? 0}&offset=0&mode=wangpan`,
};
};
/**
* TODO:
* 115链接任务列表(可用于查询失败链接任务)
*/
handleTaskList = async () => {
const { sign, time } = await request("http://115.com/", "GET", {
ct: "offline",
ac: "space",
_: new Date().getTime(),
});
let { tasks } = await request(
"https://115.com/web/lixian/?ct=lixian&ac=task_lists",
"POST",
`page=1&uid=0&sign=${sign ?? ""}&time=${time ?? new Date().getTime()}`
);
// console.log(tasks);
// del_path
// status 2成功 1下载中 -1下载失败
// url
};
// 离线后重命名,移动,移除原目录等
afterAction = async items => {
// rename
for (const { fid, rename } of items) {
request(
"http://webapi.115.com/files/edit",
"POST",
`fid=${fid}&file_name=${encodeURIComponent(rename)}`
);
}
if (rootId) {
const params = arr => arr.reduce((acc, cur, idx) => `${acc}&fid[${idx}]=${cur}`, "");
const fids = Array.from(new Set(items.map(item => item.fid)));
const cids = Array.from(new Set(items.filter(item => item !== rootId).map(item => item.cid)));
// move
const move_proid = `${new Date()}_${~(100 * Math.random())}_0`;
await request(
"https://webapi.115.com/files/move",
"POST",
`pid=${rootId}&move_proid=${move_proid}${params(fids)}`
);
// del
request("https://webapi.115.com/rb/delete", "POST", `pid=${rootId}&ignore_warn=1${params(cids)}`);
}
lf.upItem(this.codeKey, { upDate: 0 });
await delay(1000);
this.handleResource();
};
}
class JavBusScript {
waterfall = new Waterfall();
genre = {
docStart: () => {
GM_addStyle(`
footer { display: none${SUFFIX}; }
button.btn.btn-danger.btn-block.btn-genre {
position: fixed${SUFFIX};
bottom: 0${SUFFIX};
margin: 0${SUFFIX};
left: 0${SUFFIX};
border: 0${SUFFIX};
border-radius: 0${SUFFIX};
}
`);
},
contentLoaded: () => {
if (!doc.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return;
let box = doc.querySelectorAll(".genre-box");
box[box.length - 1].setAttribute("style", "margin-bottom: 65px;");
},
};
forum = {
docStart: () => {
GM_addStyle(`
.bcpic,
.banner728,
.sd.sd_allbox > div:last-child {
display: none${SUFFIX};
}
.jav-button {
margin-top: -3px${SUFFIX};
}
#toptb {
position: fixed${SUFFIX};
top: 0${SUFFIX};
left: 0${SUFFIX};
right: 0${SUFFIX};
z-index: 999${SUFFIX};
}
#wp {
margin-top: 55px${SUFFIX};
}
`);
},
contentLoaded: () => {},
};
resource = {
docStart: () => {
// layout
GM_addStyle(`
.alert.alert-danger.alert-page.error-page, footer {
display: none !important;
}
.resItem {
margin: 15px 0;
cursor: pointer;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.thumb {
padding-bottom: 67%;
background-repeat: no-repeat;
background-size: cover;
background-color: #fff;
position: relative;
}
.info {
padding: 15px;
}
.title {
margin-bottom: 6px;
}
`);
// light
GM_addStyle(`
.info {
background-color: #fff;
color: #333;
}
`);
// dark
if (!dm) return;
GM_addStyle(`
.info {
background-color: ${SecondaryBackground};
}
.title {
color: ${LabelColor};
}
`);
},
contentLoaded: async () => {
doc.title = "已有资源(0)";
const navbar = doc.querySelector("#navbar .nav.navbar-nav");
navbar.querySelector(".active").classList.remove("active");
navbar.querySelector(".resource").classList.add("active");
const keys = await lf.keys();
let nodes = [];
for (const key of keys) {
const item = await lf.getItem(key);
const resource = item?.resource;
if (!resource?.length) continue;
nodes.push({ ...item, key: key.split("/")[0] });
}
if (!nodes?.length) return;
doc.title = `已有资源(${nodes.length})`;
nodes = nodes.reduce((pre, { thumb, title, resource, href, key, upDate }) => {
let date = new Date(upDate);
const Y = date.getFullYear();
const M = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
const D = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
date = `${Y}-${M}-${D}`;
return `${pre}
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="resItem">
<div
class="thumb"
title="点击播放"
data-link="${resource[0].link}"
style="background-image: url(${thumb})"
>
</div>
<a href="${href ?? "/" + key}">
<div class="info">
<div class="ellipsis title" title="${title}">${title}</div>
<button class="btn btn-xs btn-primary" disabled="disabled">
更新:${date}
</button>
<button class="btn btn-xs btn-danger" disabled="disabled">
资源:${resource.length}
</button>
</div>
</a>
</div>
</div>`;
}, "");
appendAfter(doc.querySelector(".container-fluid .row"), nodes);
for (const item of doc.querySelectorAll(".resItem")) {
const thumb = item.querySelector(".thumb");
thumb.addEventListener("click", () => window.open(thumb.getAttribute("data-link")));
if (!ck) continue;
const link = item.querySelector("a");
const { href } = link;
link.addEventListener("contextmenu", e => {
e.preventDefault();
});
link.addEventListener("click", e => {
e.preventDefault();
GM_openInTab(href, { active: true });
});
link.addEventListener("mousedown", e => {
if (e.button !== 2) return;
e.preventDefault();
mousePoint = e.screenX + e.screenY;
});
link.addEventListener("mouseup", e => {
const num = e.screenX + e.screenY - mousePoint;
if (e.button !== 2 || num > 5 || num < -5) return;
e.preventDefault();
GM_openInTab(href);
});
}
},
};
details = new Details();
}
const Background = "rgb(18,18,18)";
const SecondaryBackground = "rgb(32,32,32)";
const LabelColor = "rgba(255,255,255,.95)";
const SecondaryLabelColor = "rgb(170,170,170)";
const Grey = "rgb(49,49,49)";
const Blue = "rgb(10,132,255)";
const Orange = "rgb(255,159,10)";
const Green = "rgb(48,209,88)";
const Pink = "rgb(255,55,95)";
const Red = "rgb(255,69,58)";
const Yellow = "rgb(255,214,10)";
const darkMode = path => {
if (path === "forum") return;
GM_addStyle(`
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: ${Grey};
}
*:not(span) {
border-color: ${Grey}${SUFFIX};
outline: ${Grey}${SUFFIX};
text-shadow: none${SUFFIX};
}
body, footer {
background: ${Background}${SUFFIX};
color: ${SecondaryLabelColor}${SUFFIX};
}
img {
filter: brightness(90%)${SUFFIX};
}
nav {
background: ${SecondaryBackground}${SUFFIX};
}
input {
background: ${Background}${SUFFIX};
}
*::placeholder {
color: ${SecondaryLabelColor}${SUFFIX};
}
button, input, a, h1, h2, h3, h4, h5, h6 {
color: ${LabelColor}${SUFFIX};
box-shadow: none${SUFFIX};
outline: none${SUFFIX};
}
.btn.disabled, .btn[disabled], fieldset[disabled] .btn {
opacity: .85${SUFFIX};
}
button, .btn-default, .input-group-addon {
background: ${Grey}${SUFFIX};
color: ${LabelColor}${SUFFIX};
}
.btn-primary {
background: ${Blue}${SUFFIX};
border-color: ${Blue}${SUFFIX};
}
.btn-success {
background: ${Green}${SUFFIX};
border-color: ${Green}${SUFFIX};
}
.btn-danger {
background: ${Red}${SUFFIX};
border-color: ${Red}${SUFFIX};
}
.btn-warning {
background: ${Orange}${SUFFIX};
border-color: ${Orange}${SUFFIX};
}
.navbar-nav>.active>a, .navbar-nav>.active>a:focus, .navbar-nav>.active>a:hover, .navbar-nav>.open>a, .dropdown-menu {
background: ${Background}${SUFFIX};
}
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
background: ${Grey}${SUFFIX};
}
.pagination .active a {
border: none${SUFFIX};
color: ${LabelColor}${SUFFIX};
}
.pagination>li>a, .pagination>li>span {
background-color: ${Grey}${SUFFIX};
border: none${SUFFIX};
color: ${LabelColor}${SUFFIX};
}
tr, .modal-content, .alert {
background: ${SecondaryBackground}${SUFFIX};
box-shadow: none${SUFFIX};
}
tr:hover {
background: ${Grey}${SUFFIX};
}
`);
if (path === "waterfall") {
GM_addStyle(`
.item a {
background: ${SecondaryBackground}${SUFFIX};
}
.photo-info {
background: ${SecondaryBackground}${SUFFIX};
color: ${LabelColor}${SUFFIX};
}
date {
color: ${SecondaryLabelColor}${SUFFIX};
}
.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover, .nav-pills>li>a:focus, .nav-pills>li>a:hover {
background-color: ${Grey}${SUFFIX};
}
`);
}
if (path === "genre") {
GM_addStyle(`
.genre-box {
background-color: ${SecondaryBackground}${SUFFIX};
}
`);
}
if (path === "forum") {
GM_addStyle(`
#toptb,
.mn > div,
.biaoqicn_show.slidebar a:not(.on),
.new4_list_top .dspanonhover,
div[id*="con_NewOne_"],
.frame,
.frame-tab,
.main-right-p15,
.bm.bmw.cl,
.bm.bmw.cl .bm_h.cl {
background: ${SecondaryBackground}${SUFFIX};
}
.jav-footer,
.nav-title.nav-active a,
.menu-body-panel.login-detail-wrap,
.menu-body-panel .icon-arrow-t,
.new4_list_top,
.comment-excerpt {
background: ${Background}${SUFFIX};
}
.main-right-zuixin .comment-excerpt:before {
border-bottom-color: ${Background}${SUFFIX};
}
.main-right-tit span {
color: ${LabelColor}${SUFFIX};
}
`);
}
if (path === "details") {
GM_addStyle(`
.movie, .sample-box, .movie-box, .photo-info {
background: ${SecondaryBackground}${SUFFIX};
}
.photo-info {
color: ${LabelColor}${SUFFIX};
}
.avatar-box, .avatar-box span, .info ul li, .info .star-name {
background: ${SecondaryBackground}${SUFFIX};
border-color: ${Grey}${SUFFIX};
color: ${LabelColor}${SUFFIX};
}
`);
}
};
if (/javbus\.com/g.test(host)) {
const menus = [
{ title: "点击事件", name: "ck", val: ck, key: "c" },
{ title: "暗黑模式", name: "dm", val: dm, key: "d" },
{ title: "滚动加载", name: "lm", val: lm, key: "s" },
];
for (const { title, name, val, key } of menus) {
GM_registerMenuCommand(
`${val ? "关闭" : "开启"}${title}`,
() => {
GM_setValue(name, !val);
location.reload();
},
key
);
}
GM_registerMenuCommand(
"离线后操作根目录cid",
() => {
const rid = prompt("用于离线后移动,删除等操作", rootId);
GM_setValue("rid", rid);
location.reload();
},
"a"
);
lf.config({ name: "JBDB", storeName: "AVS" });
GM_addStyle(`
.ad-box {
display: none${SUFFIX};
}
.ellipsis {
overflow : hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.line-4 {
-webkit-line-clamp: 4;
}
`);
const pathReg = {
waterfall:
/^\/((uncensored|uncensored\/)?(page\/\d+)?$)|((uncensored\/)?((search|searchstar|actresses|genre|star|studio|label|series|director|member)+\/)|actresses(\/\d+)?)+/i,
genre: /^\/(uncensored\/)?genre$/i,
forum: /^\/forum\//i,
resource: /^\/resource$/i,
details: /^\/[\w]+(-|_)?[\d]*.*$/i,
};
const path = Object.keys(pathReg).filter(key => pathReg[key].test(pathname))[0];
if (!path) return;
dm && darkMode(path);
let jav = new JavBusScript();
jav = jav[path];
if (!jav) return;
jav.docStart();
doc.addEventListener("DOMContentLoaded", () => {
scriptStart();
jav.contentLoaded();
});
window.addEventListener("load", jav.load);
}
if (/captchaapi\.115\.com/g.test(host)) {
doc.addEventListener("DOMContentLoaded", () => {
window.focus();
const btn = doc.querySelector("#js_ver_code_box button[rel='verify']");
btn.addEventListener("click", () => {
const interval = setInterval(() => {
if (doc.querySelector("div[rel='error_box']").getAttribute("style").indexOf("none") !== -1) {
window.open("", "_self");
window.close();
}
}, 300);
setTimeout(() => clearInterval(interval), 600);
});
});
}
})();