add missing file

This commit is contained in:
lzyme.dev@gmail.com
2026-08-29 16:54:48 +08:00
parent 9235e1add9
commit 3c582535c0
49 changed files with 24566 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class ActressInfoPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "apiUrl", "https://ja.wikipedia.org/wiki/");
}
getName() {
return "ActressInfoPlugin";
}
async handle() {
"yes" === await storageManager.getSetting("enableLoadActressInfo", "yes") && this.loadActressInfo();
}
loadActressInfo() {
this.handleDetailPage().then();
this.handleStarPage().then();
}
async initCss() {
return "\n <style>\n .info-tag {\n background-color: #ecf5ff;\n display: inline-block;\n height: 32px;\n padding: 0 10px;\n line-height: 30px;\n font-size: 12px;\n color: #409eff;\n border: 1px solid #d9ecff;\n border-radius: 4px;\n box-sizing: border-box;\n white-space: nowrap;\n }\n </style>\n ";
}
async handleDetailPage() {
if ($(".actress-info").length > 0) return;
let nameList = $(".female").prev().map(((i, el) => $(el).text().trim())).get();
if (!nameList.length) return;
const cacheKey = "jhs_actress_info", cacheData = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {};
let result = null, infoHtml = "";
for (let i = 0; i < nameList.length; i++) {
let name2 = nameList[i];
result = cacheData[name2];
if (!result) try {
result = await this.searchInfo(name2);
result && (cacheData[name2] = result);
} catch (e) {
console.error("该名称查询失败,尝试其它名称");
}
let contentHtml = "";
contentHtml = result ? `\n <div class="panel-block actress-info">\n <strong>${name2}:</strong>\n <a href="${result.url}" style="margin-left: 5px" target="_blank">\n <span class="info-tag">${result.birthday} ${result.age}</span>\n <span class="info-tag">${result.height} ${result.weight}</span>\n <span class="info-tag">${result.threeSizeText} ${result.braSize}</span>\n </a>\n </div>\n ` : `<div class="panel-block actress-info"><a href="${this.apiUrl + name2}" target="_blank"><strong>${name2}:</strong></a></div> `;
infoHtml += contentHtml;
}
$('strong:contains("演員")').parent().after(infoHtml);
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
}
async handleStarPage() {
if ($(".actress-info").length > 0) return;
let nameList = [], $actor = $(".actor-section-name");
$actor.length && $actor.text().trim().split(",").forEach((name2 => {
nameList.push(name2.trim());
}));
let $sectionMeta = $(".section-meta:not(:contains('影片'))");
$sectionMeta.length && $sectionMeta.text().trim().split(",").forEach((name2 => {
nameList.push(name2.trim());
}));
if (!nameList.length) return;
const cacheKey = "jhs_actress_info", cacheData = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {};
let result = null;
for (let i = 0; i < nameList.length; i++) {
let name2 = nameList[i];
result = cacheData[name2];
if (result) break;
try {
result = await this.searchInfo(name2);
} catch (e) {
console.error("该名称查询失败,尝试其它名称");
}
if (result) break;
}
result && nameList.forEach((name2 => {
cacheData[name2] = result;
}));
let contentHtml = '<div class="actress-info" style="font-size: 17px; font-weight: normal; margin-top: 5px;">无此相关演员信息</div>';
result && (contentHtml = `\n <a class="actress-info" href="${result.url}" target="_blank">\n <div style="font-size: 17px; font-weight: normal; margin-top: 5px;">\n <div style="display: flex; margin-bottom: 10px;">\n <span style="width: 300px;">出生日期: ${result.birthday}</span>\n <span style="width: 200px;">年龄: ${result.age}</span>\n <span style="width: 200px;">身高: ${result.height}</span>\n </div>\n <div style="display: flex; margin-bottom: 10px;">\n <span style="width: 300px;">体重: ${result.weight}</span>\n <span style="width: 200px;">三围: ${result.threeSizeText}</span>\n <span style="width: 200px;">罩杯: ${result.braSize}</span>\n </div>\n </div>\n </a>\n `);
$actor.parent().append(contentHtml);
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
}
async searchInfo(name2) {
"三上悠亞" === name2 && (name2 = "三上悠亜");
let url = this.apiUrl + name2;
const html = await gmHttp.get(url), parser = new DOMParser, $dom = $(parser.parseFromString(html, "text/html"));
let birthday = $dom.find('a[title="誕生日"]').parent().parent().find("td").text().trim(), age = $dom.find("th:contains('現年齢')").parent().find("td").text().trim() ? parseInt($dom.find("th:contains('現年齢')").parent().find("td").text().trim()) + "岁" : "", height = $dom.find('tr:has(a[title="身長"]) td').text().trim().split(" ")[0] + "cm", weight = $dom.find('tr:has(a[title="体重"]) td').text().trim().split("/")[1].trim();
"― kg" === weight && (weight = "");
return {
birthday: birthday,
age: age,
height: height,
weight: weight,
threeSizeText: $dom.find('a[title="スリーサイズ"]').closest("tr").find("td").text().replace("cm", "").trim(),
braSize: $dom.find('th:contains("ブラサイズ")').next("td").contents().first().text().trim(),
url: url
};
}
}
export { ActressInfoPlugin };
+27
View File
@@ -0,0 +1,27 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class AliyunPanPlugin extends BasePlugin {
getName() {
return "AliyunPanPlugin";
}
handle() {
$("body").append('<a class="a-success" id="refresh-token-btn" style="position:fixed; right: 0; top:50%;z-index:99999">获取refresh_token</a>');
$("#refresh-token-btn").on("click", (event => {
let tokenStr = localStorage.getItem("token");
if (!tokenStr) {
alert("请先登录!");
return;
}
let refresh_token = JSON.parse(tokenStr).refresh_token;
navigator.clipboard.writeText(refresh_token).then((() => {
alert("已复制到剪切板 如失败, 请手动复制: " + refresh_token);
})).catch((err => {
console.error("Failed to copy refresh token: ", err);
}));
}));
}
}
export { AliyunPanPlugin };
+446
View File
@@ -0,0 +1,446 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, currentHref, NO, YES } from '../core/constants.js';
import { GM_xmlhttpRequest } from 'vite-plugin-monkey/dist/client';
class AutoPagePlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "preloadDistance", 500);
__publicField(this, "currentPage", this.getInitialPageNumber());
__publicField(this, "pageItems", []);
}
getName() {
return "AutoPagePlugin";
}
async initCss() {
return "\n <style>\n .jhs-scroll {\n text-align: center;\n padding-top: 20px;\n font-size: 14px;\n }\n .jhs-scroll.waterfall-loading { color: #000; }\n .jhs-scroll.waterfall-error { color: #f44336; cursor: pointer; }\n .jhs-scroll.waterfall-no-more { color: #4CAF50; }\n </style>\n ";
}
async handle() {
this.waterfall().then();
}
getInitialPageNumber() {
if (isJavBus) {
const match = currentHref.match(/\/(page|star\/[^/]+)\/(\d+)/);
return match ? parseInt(match[2], 10) : 1;
}
if (isJavDb) {
const match = currentHref.match(/[?&]page=(\d+)/);
return match ? parseInt(match[1], 10) : 1;
}
return 1;
}
async waterfall() {
if (await this.shouldDisablePaging()) return;
const selector = this.getSelector();
this.container = document.querySelector(selector.boxSelector);
if (!this.container) {
console.error("没有找到容器节点,停止瀑布流!");
return;
}
this.loader = document.createElement("div");
this.loader.className = "jhs-scroll";
this.container.parentNode.insertBefore(this.loader, this.container.nextSibling);
this.pageItems.push({
page: this.currentPage,
top: 0,
url: window.location.href
});
this.loader.addEventListener("click", (() => {
this.loader.classList.contains("waterfall-error") && this.loadNextPage().then();
}));
window.addEventListener("scroll", (() => {
this.checkLoad();
this.checkScrollPosition();
}));
const nextLink = document.querySelector(selector.nextPageSelector);
this.nextUrl = null == nextLink ? void 0 : nextLink.href;
this.hasMore = !!this.nextUrl;
setTimeout((() => {
this.checkLoad();
}), 1e3);
this.hasMore || this.setState("waterfall-no-more", "已经到底了");
}
async loadNextPage() {
var _a2;
if (await storageManager.getSetting("autoPage", YES) === NO) {
this.setState("waterfall-loading", "");
return;
}
if (this.isLoading || !this.nextUrl) return;
this.isLoading = !0;
this.setState("waterfall-loading", "加载中...");
const selector = this.getSelector();
try {
const pageNum = utils.getUrlParam(this.nextUrl, "page");
let maxPage = 60;
currentHref.includes("c11") && (maxPage = 30);
if (isJavDb && pageNum > maxPage || currentHref.includes("month")) {
const beyond60Plugin = this.getBean("Beyond60Plugin");
if (beyond60Plugin) {
const {html: html2, nextUrl: nextUrl, hasMore: hasMore} = await beyond60Plugin.handleBeyond60(this.nextUrl);
if (html2) {
const pageTop2 = this.container.scrollHeight;
this.pageItems.push({
page: this.currentPage + 1,
top: pageTop2,
url: this.nextUrl
});
$(".movie-list").append(html2);
}
this.hasMore = hasMore;
this.nextUrl = nextUrl;
const $ul = beyond60Plugin.createPagination(pageNum, hasMore);
$(".pagination").html($ul);
this.setState("waterfall-loading", "");
this.hasMore || this.setState("waterfall-no-more", "已经到底了");
return;
}
}
const html = await gmHttp.get(this.nextUrl);
clog.log("请求下一页内容:", this.nextUrl);
const $dom = utils.htmlTo$dom(html);
isJavBus && $dom.find(".avatar-box").length > 0 && $dom.find(".avatar-box").parent().remove();
let itemList = $dom.find(this.getSelector().requestDomItemSelector);
const currentBoxCarInfoList = this.getBoxCarInfoList(), nextPageBoxCarInfoList = this.getBoxCarInfoList(itemList);
if (this.checkDuplicateCarNumbers(currentBoxCarInfoList, nextPageBoxCarInfoList)) {
this.nextUrl = null;
this.hasMore = !1;
this.setState("waterfall-error", "翻页内容出现重复数据, 可能首页已更新了新视频 或 页码受JavDB限制, 停止瀑布流");
return;
}
const pageTop = this.container.scrollHeight;
this.pageItems.push({
page: this.currentPage + 1,
top: pageTop,
url: this.nextUrl
});
const listPagePlugin = this.getBean("ListPagePlugin");
let coverImgNodeList = $dom.find(this.getSelector().coverImgSelector);
listPagePlugin.replaceHdImg(coverImgNodeList);
$(this.getSelector().boxSelector).append(itemList);
this.nextUrl = null == (_a2 = $dom.find(selector.nextPageSelector)) ? void 0 : _a2.attr("href");
this.hasMore = !!this.nextUrl;
let pagination = $dom.find(".pagination");
$(".pagination").replaceWith(pagination);
this.setState("waterfall-loading", "");
this.hasMore || this.setState("waterfall-no-more", "已经到底了");
} catch (e) {
clog.error("加载失败:", e);
this.setState("waterfall-error", "加载失败,点击重试");
} finally {
this.isLoading = !1;
}
}
checkScrollPosition() {
const scrollPosition = window.scrollY;
for (let i = this.pageItems.length - 1; i >= 0; i--) {
const page = this.pageItems[i];
if (scrollPosition >= page.top) {
if (this.currentPage !== page.page) {
this.currentPage = page.page;
this.updatePageUrl(page.url);
}
break;
}
}
}
checkLoad() {
if (!this.loader) return;
this.loader.getBoundingClientRect().top < window.innerHeight + this.preloadDistance && this.loadNextPage().then();
}
async shouldDisablePaging() {
if (!window.isListPage) return !0;
await storageManager.getSetting("autoPage", YES);
return [ "search?q", "handlePlayback=1", "handleTop=1", "/want_watch_videos", "/watched_videos", "/advanced_search?type=100" ].some((path => currentHref.includes(path)));
}
updatePageUrl_old(href) {
window.history.pushState({}, "", href);
if (isJavBus) {
const match = href.match(/\/(page|star\/.*?)\/(\d+)/), pageNumber = match ? parseInt(match[2], 10) : null;
document.title = document.title.replace(/第\d+頁/, "第" + pageNumber + "頁");
}
}
updatePageUrl(url) {
window.history.replaceState({}, "", url);
isJavBus && (document.title = document.title.replace(/第\d+頁/, `第${this.currentPage}頁`));
}
setState(state, text) {
this.loader.className = `jhs-scroll ${state}`;
this.loader.textContent = text;
}
}
class AliyunApi {
constructor(refresh_token) {
this.baseApiUrl = "https://api.aliyundrive.com";
this.refresh_token = refresh_token;
this.authorization = null;
this.default_drive_id = null;
this.backupFolderId = null;
}
async getDefaultDriveId() {
if (this.default_drive_id) return this.default_drive_id;
this.userInfo = await this.getUserInfo();
this.default_drive_id = this.userInfo.default_drive_id;
return this.default_drive_id;
}
async getHeaders() {
if (this.authorization) return {
authorization: this.authorization
};
this.authorization = await this.getAuthorization();
return {
authorization: this.authorization
};
}
async getAuthorization() {
let url = this.baseApiUrl + "/v2/account/token", data = {
refresh_token: this.refresh_token,
grant_type: "refresh_token"
};
try {
return "Bearer " + (await gmHttp.post(url, data)).access_token;
} catch (e) {
throw e.message.includes("is not valid") ? new Error("refresh_token无效, 请重新填写并保存") : e;
}
}
async getUserInfo() {
const headers = await this.getHeaders();
let url = this.baseApiUrl + "/v2/user/get";
return await gmHttp.post(url, {}, headers);
}
async deleteFile(file_id, drive_id = null) {
if (!file_id) throw new Error("未传入file_id");
drive_id || (drive_id = await this.getDefaultDriveId());
let data = {
file_id: file_id,
drive_id: drive_id
}, url = this.baseApiUrl + "/v2/recyclebin/trash";
const headers = await this.getHeaders();
await gmHttp.post(url, data, headers);
return {};
}
async createFolder(name2, drive_id = null, parent_folder_id = "root") {
drive_id || (drive_id = await this.getDefaultDriveId());
let url = this.baseApiUrl + "/adrive/v2/file/createWithFolders", data = {
name: name2,
type: "folder",
parent_file_id: parent_folder_id,
check_name_mode: "auto_rename",
content_hash_name: "sha1",
drive_id: drive_id
};
const headers = await this.getHeaders();
return await gmHttp.post(url, data, headers);
}
async getFileList(parent_folder_id = "root", drive_id = null) {
drive_id || (drive_id = await this.getDefaultDriveId());
let url = this.baseApiUrl + "/adrive/v3/file/list";
const data = {
drive_id: drive_id,
parent_file_id: parent_folder_id,
limit: 200,
all: !1,
url_expire_sec: 14400,
image_thumbnail_process: "image/resize,w_256/format,avif",
image_url_process: "image/resize,w_1920/format,avif",
video_thumbnail_process: "video/snapshot,t_120000,f_jpg,m_lfit,w_256,ar_auto,m_fast",
fields: "*",
order_by: "updated_at",
order_direction: "DESC"
}, headers = await this.getHeaders();
return (await gmHttp.post(url, data, headers)).items;
}
async uploadFile(folder_id, fileName, uploadContent, drive_id = null) {
show.info("请求存储空间中...");
let createFileUrl = this.baseApiUrl + "/adrive/v2/file/createWithFolders";
drive_id || (drive_id = await this.getDefaultDriveId());
let data = {
drive_id: drive_id,
part_info_list: [ {
part_number: 1
} ],
parent_file_id: folder_id,
name: fileName,
type: "file",
check_name_mode: "auto_rename"
};
const headers = await this.getHeaders(), createFileResult = await gmHttp.post(createFileUrl, data, headers), upload_id = createFileResult.upload_id, upload_file_id = createFileResult.file_id, upload_url = createFileResult.part_info_list[0].upload_url;
show.info("开始上传文件...");
await this._doUpload(upload_url, uploadContent);
await gmHttp.post("https://api.aliyundrive.com/v2/file/complete", data = {
drive_id: drive_id,
file_id: upload_file_id,
upload_id: upload_id
}, headers);
}
_doUpload(upload_url, uploadContent) {
return new Promise(((resolve, reject) => {
$.ajax({
type: "PUT",
url: upload_url,
data: uploadContent,
contentType: " ",
processData: !1,
success: (res, status, xhr) => {
200 === xhr.status ? resolve({}) : reject(xhr);
},
error: xhr => {
clog.error("上传失败", xhr.responseText);
reject(xhr);
}
});
}));
}
async getDownloadUrl(file_id, drive_id = null) {
drive_id || (drive_id = await this.getDefaultDriveId());
let url = this.baseApiUrl + "/v2/file/get_download_url";
const headers = await this.getHeaders();
let data = {
file_id: file_id,
drive_id: drive_id
};
return (await gmHttp.post(url, data, headers)).url;
}
async _createBackupFolder(folderName) {
const fileList = await this.getFileList();
let folderObj = null;
for (let i = 0; i < fileList.length; i++) {
let file = fileList[i];
if (file.name === folderName) {
folderObj = file;
break;
}
}
if (!folderObj) {
show.info("不存在备份目录, 进行创建");
folderObj = await this.createFolder(folderName);
}
this.backupFolderId = folderObj.file_id;
}
async backup(folderName, fileName, uploadContent) {
if (this.backupFolderId) await this.uploadFile(this.backupFolderId, fileName, uploadContent); else {
await this._createBackupFolder(folderName);
await this.uploadFile(this.backupFolderId, fileName, uploadContent);
}
}
async getBackupList(folderName) {
let dataList;
if (this.backupFolderId) dataList = await this.getFileList(this.backupFolderId); else {
await this._createBackupFolder(folderName);
dataList = await this.getFileList(this.backupFolderId);
}
const fileList = [];
dataList.forEach((data => {
fileList.push({
name: data.name,
fileId: data.file_id,
createTime: data.created_at,
size: data.size
});
}));
return fileList;
}
}
class WebDavApi {
constructor(davUrl, username, password) {
this.davUrl = davUrl.endsWith("/") ? davUrl : davUrl + "/";
this.username = username;
this.password = password;
this.folderName = null;
}
_getAuthHeaders() {
return {
Authorization: `Basic ${btoa(`${this.username}:${this.password}`)}`,
Depth: "1"
};
}
_sendRequest(method, path, headers = {}, data) {
return new Promise(((resolve, reject) => {
const url = this.davUrl + path, allHeaders = {
...this._getAuthHeaders(),
...headers
};
GM_xmlhttpRequest({
method: method,
url: url,
headers: allHeaders,
data: data,
onload: response => {
if (response.status >= 200 && response.status < 300) resolve(response); else {
console.error(response);
reject(new Error(`请求失败 ${response.status}: ${response.statusText}`));
}
},
onerror: response => {
console.error("请求WebDav发生错误:", response);
reject(new Error("请求WebDav失败, 请检查服务是否启动, 凭证是否正确"));
}
});
}));
}
async backup(folderName, fileName, uploadContent) {
await this._sendRequest("MKCOL", folderName);
const path = folderName + "/" + fileName;
await this._sendRequest("PUT", path, {
"Content-Type": "text/plain"
}, uploadContent);
}
async getFileList(folderName) {
var _a2, _b, _c;
const xmlResponse = (await this._sendRequest("PROPFIND", folderName, {
"Content-Type": "application/xml"
}, '<?xml version="1.0"?>\n <d:propfind xmlns:d="DAV:">\n <d:prop>\n <d:displayname />\n <d:getcontentlength />\n <d:creationdate />\n <d:getlastmodified />\n <d:iscollection />\n </d:prop>\n </d:propfind>\n ')).responseText, items = (new DOMParser).parseFromString(xmlResponse, "text/xml").getElementsByTagNameNS("DAV:", "response"), fileList = [];
for (let i = 0; i < items.length; i++) {
if (0 === i) continue;
let item = items[i];
console.log(item);
const name2 = item.getElementsByTagNameNS("DAV:", "displayname")[0].textContent, size = (null == (_a2 = item.getElementsByTagNameNS("DAV:", "getcontentlength")[0]) ? void 0 : _a2.textContent) || "0", createTime = (null == (_b = item.getElementsByTagNameNS("DAV:", "creationdate")[0]) ? void 0 : _b.textContent) || (null == (_c = item.getElementsByTagNameNS("DAV:", "getlastmodified")[0]) ? void 0 : _c.textContent) || "";
"0" !== size && fileList.push({
fileId: name2,
name: name2,
size: Number(size),
createTime: createTime
});
}
fileList.reverse();
return fileList;
}
async deleteFile(fileId) {
let path = this.folderName + "/" + encodeURI(fileId);
await this._sendRequest("DELETE", path, {
"Cache-Control": "no-cache"
});
}
async getBackupList(folderName) {
this.folderName = folderName;
await this._sendRequest("MKCOL", folderName);
return this.getFileList(folderName);
}
async getFileContent(filePath) {
let path = this.folderName + "/" + filePath;
return (await this._sendRequest("GET", path, {
Accept: "application/octet-stream"
})).responseText;
}
}
const SALT = "x7k9p3";
function simpleEncrypt(str) {
return (SALT + str + SALT).split("").map((char => {
const code = char.codePointAt(0);
return String.fromCodePoint(code + 5);
})).join("");
}
function simpleDecrypt(encryptedStr) {
return encryptedStr.split("").map((char => {
const code = char.codePointAt(0);
return String.fromCodePoint(code - 5);
})).join("").slice(SALT.length, -SALT.length);
}
export { AutoPagePlugin, AliyunApi, WebDavApi, SALT, simpleEncrypt };
+585
View File
@@ -0,0 +1,585 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, currentHref, Status_FILTER } from '../core/constants.js';
class BlacklistPlugin extends BasePlugin {
getName() {
return "BlacklistPlugin";
}
async addBlacklist(event) {
let tempEvent = {
clientX: event.clientX,
clientY: event.clientY + 80
};
const hasAddBlacklist = $("#addBlacklistBtn span").text().includes("已加入");
let dataInfo, title;
if (currentHref.includes("/tags")) {
const urlObj = new URL(currentHref);
urlObj.searchParams.delete("page");
const checkTag = $("#jhs-check-tag").text().trim();
dataInfo = {
starId: "no-" + checkTag,
name: "虚拟演员-" + checkTag,
allName: [ "虚拟演员" ],
role: "虚拟演员",
movieType: "虚拟演员",
blacklistUrl: urlObj.toString()
};
title = `是否将分类 <span style="color: #f40">${checkTag}</span> 加入到黑名单中?`;
hasAddBlacklist && (title = `分类 <span style="color: #f40">${checkTag}</span> 已在黑名单中, 是否从当前页开始追加屏蔽?`);
} else {
dataInfo = this.getActressPageInfo();
title = `是否将该演员 <span style="color: #f40">${dataInfo.name}</span> 加入到黑名单中?`;
hasAddBlacklist && (title = `演员 <span style="color: #f40">${dataInfo.name}</span> 已在黑名单中, 是否从当前页开始追加屏蔽?`);
}
const {starId: starId, name: name2, allName: allName, role: role, movieType: movieType, blacklistUrl: blacklistUrl} = dataInfo;
currentHref.includes("page") && !currentHref.includes("page=1") && (title += "<br/> 注意: 当前页面非第一页, 屏蔽数据将从此页面开始");
if (isJavBus) {
const afterStarParts = currentHref.split("/star/")[1].split("/");
if (afterStarParts.length > 1) {
parseInt(afterStarParts[1]) > 1 && (title += "<br/> 注意: 当前页面非第一页, 屏蔽数据将从此页面开始");
}
}
utils.q(tempEvent, title, (async () => {
const taskPlugin = this.getBean("TaskPlugin");
navigator.locks.request(taskPlugin.singleTaskKey, {
ifAvailable: !0
}, (async lock => {
if (lock) {
this.loadObj = loading();
try {
await storageManager.addBlacklistItem({
starId: starId,
name: name2,
allName: allName,
role: role,
movieType: movieType,
url: blacklistUrl
});
await this.filterActorVideo(name2, starId);
const toast = show.ok(`屏蔽结束,是否跳转到最后一页: ${this.lastPageLink || "无"}`, {
duration: -1,
close: !0,
onClick: () => {
toast.closeShow();
this.lastPageLink && (window.location.href = this.lastPageLink);
}
});
} catch (e) {
clog.error(e);
const toast = show.error("发生错误, 是否填转到解析失败的那一页? (点击并跳转)", {
duration: -1,
close: !0,
onClick: () => {
toast.closeShow();
window.location.href = this.nextPageLink;
}
});
} finally {
this.loadObj.close();
}
} else show.error("当前有定时任务在后台执行中, 无法发起此操作");
})).catch((error => {
console.error("锁任务出现错误:", error);
clog.error("锁任务出现错误:", error);
}));
}));
}
async resetBtnTip() {
const taskPlugin = this.getBean("TaskPlugin"), lastCheckBlacklistTimeStr = localStorage.getItem(taskPlugin.lastCheckBlacklistTimeKey) || "无", checkBlacklist_intervalTime = await storageManager.getSetting("checkBlacklist_intervalTime", 12);
this.checkBlacklist_ruleTime = await storageManager.getSetting("checkBlacklist_ruleTime", 8760);
$("#checkBlacklistBtn").attr("data-tip", `上次检测时间: ${lastCheckBlacklistTimeStr}; 检测间隔时间: ${checkBlacklist_intervalTime}小时`);
}
async openBlacklistDialog() {
const taskPlugin = this.getBean("TaskPlugin"), settingObj = await storageManager.getSetting();
let html = `\n <div style="padding: 10px 20px; height: 100%;overflow:hidden;"> \n <div style="display: flex;justify-content: space-between;">\n <div style="display: flex; gap:5px">\n <a id="checkBlacklistBtn" class="a-danger" data-tip="上次检测时间: ${localStorage.getItem(taskPlugin.lastCheckBlacklistTimeKey) || "无"}; 检测间隔时间: ${settingObj.checkBlacklist_intervalTime}小时">${this.blacklistSvg} &nbsp;手动检测黑名单</a>\n <a class="a-warning" id="clearKeywordBlacklist" data-tip="检测黑名单番号数据列表, 是否包含标题关键词">${this.removeSvg} &nbsp;&nbsp; 清理数据</a>\n <a class="a-info" id="toSetting">${this.settingSvg} &nbsp;&nbsp; 配置</a>\n </div>\n <div style="display: flex; gap:5px">\n <select id="dataType" style="text-align: center;min-width: 150px;">\n <option value="" selected>所有</option>\n <option value="actor">男演员</option>\n <option value="actress">女演员</option>\n <option value="虚拟演员">虚拟演员</option>\n </select>\n <select id="statusType" style="text-align: center;min-width: 150px;">\n <option value="" selected>--检测状态--</option>\n <option value="normal">正常检测</option>\n <option value="stop">停止检测</option>\n </select>\n <select id="urlType" data-tip="在演员页屏蔽时,是否选择了分类" style="text-align: center;min-width: 150px; ${isJavDb ? "" : "display: none;"}">\n <option value="" selected>--屏蔽类型--</option>\n <option value="hasT">按所选分类屏蔽</option>\n <option value="noT">所有分类</option>\n </select>\n <input id="searchValue" type="text" placeholder="搜索演员" style="padding: 4px 5px;">\n <a id="cleanQueryBtn" class="a-info" style="margin-left: 0">重置</a>\n </div>\n\n </div>\n <div id="table-container" style="margin-top:20px !important; height: calc(100% - 50px);"></div>\n </div>\n `;
layer.open({
type: 1,
title: "演员黑名单",
content: html,
scrollbar: !1,
area: utils.getResponsiveArea([ "80%", "90%" ]),
anim: -1,
success: async layero => {
await this.loadTableData();
$(".layui-layer-content").on("click", "#cleanQueryBtn", (async event => {
$("#searchValue").val("");
$("#dataType").val("");
$("#statusType").val("");
await this.reloadTable();
})).on("focusout keydown", "#searchValue", (async event => {
if ("focusout" === event.type || "Enter" === event.key) {
"Enter" === event.key && event.preventDefault();
if ("keydown" === event.type && "Enter" !== event.key) return;
$("#dataType").val("");
await this.reloadTable();
}
})).on("change", "#dataType", (async () => {
$("#searchValue").val("");
await this.reloadTable();
})).on("change", "#statusType", (async () => {
await this.reloadTable();
})).on("change", "#urlType", (async () => {
await this.reloadTable();
})).on("click", "#toSetting", (() => {
this.getBean("SettingPlugin").openSettingDialog("task-panel", (() => {
$("#setting-blacklist").css({
border: "1px solid #f40"
});
}));
})).on("click", "#clearKeywordBlacklist", (async () => {
await this.clearKeywordBlacklist();
})).on("click", ".open-url", (event => {
event.preventDefault();
const $el = $(event.currentTarget), url = $el.attr("data-url"), name2 = $el.attr("data-name");
utils.openPage(url, name2, !0, event);
})).on("click", "#checkBlacklistBtn", (event => {
utils.q({
clientX: event.clientX,
clientY: event.clientY + 20
}, "是否手动检测黑名单?", (() => {
navigator.locks.request(taskPlugin.singleTaskKey, {
ifAvailable: !0
}, (async lock => {
if (lock) {
await taskPlugin.loadConfig();
await taskPlugin.checkBlacklist(!0);
} else show.error("当前有定时任务在后台执行中, 无法发起手动任务");
})).catch((error => {
console.error("锁任务出现错误:", error);
clog.error("锁任务出现错误:", error);
}));
}));
}));
},
end: () => {
if (this.tableObj) {
this.tableObj.destroy();
this.tableObj = null;
}
window.refresh();
}
});
}
async reloadTable() {
if (!this.tableObj) return;
const tableData = await this.getTableData();
this.tableObj.setData(tableData);
}
async getTableData() {
const taskPlugin = this.getBean("TaskPlugin"), blacklist = await storageManager.getBlacklist(), blacklistCarList = await storageManager.getBlacklistCarList(), searchValue = $("#searchValue").val(), statusType = $("#statusType").val(), $dataType = $("#dataType"), dataType = $dataType.val(), urlType = $("#urlType").val(), blacklistCount = blacklist.length;
let actorCount = 0, actressCount = 0, noStarCount = 0;
const filteredBlacklist = blacklist.map((item => {
"actor" === item.role ? actorCount++ : "actress" === item.role ? actressCount++ : "虚拟演员" === item.role && noStarCount++;
let isUnCheck = !1;
item.lastPublishTime && (isUnCheck = !taskPlugin.isUnnecessaryCheck(item.lastPublishTime, this.checkBlacklist_ruleTime));
return {
...item,
isUnCheck: isUnCheck
};
})).filter((item => !(searchValue && !item.name.includes(searchValue)) && (("normal" !== statusType || !item.isUnCheck) && (!("stop" === statusType && !item.isUnCheck) && (dataType ? item.role === dataType : !("hasT" === urlType && !item.url.includes("t=") && !item.url.includes("/tags")) && ("noT" !== urlType || !item.url.includes("t=") && !item.url.includes("/tags")))))));
$dataType.html(`\n <option value="">所有 (${blacklistCount})</option>\n <option value="actor">男演员 (${actorCount})</option>\n <option value="actress">女演员 (${actressCount})</option>\n <option value="虚拟演员">虚拟演员 (${noStarCount})</option>\n `);
$dataType.val(dataType);
const carListMap = new Map;
for (const car of blacklistCarList) {
const starId = car.starId;
carListMap.has(starId) || carListMap.set(starId, []);
carListMap.get(starId).push(car);
}
const finalResult = filteredBlacklist.map((item => {
const starId = item.starId, carList = carListMap.get(starId) || [];
return {
...item,
carList: carList,
count: carList.length
};
}));
this.currentCarCount = finalResult.reduce(((accumulator, currentObject) => accumulator + (currentObject.count || 0)), 0);
return finalResult;
}
async loadTableData() {
this.checkBlacklist_ruleTime = await storageManager.getSetting("checkBlacklist_ruleTime") || 8760;
const tableData = await this.getTableData();
this.tableObj = new Tabulator("#table-container", {
layout: "fitColumns",
placeholder: "暂无数据",
virtualDom: !0,
data: tableData,
pagination: !0,
paginationMode: "local",
paginationSize: 20,
paginationSizeSelector: [ 20, 50, 100, 1e3, 99999 ],
paginationCounter: (pageSize, currentRow, currentPage, totalRows, totalPages) => `演员: ${totalRows} &nbsp;&nbsp;&nbsp;番号总数: ${this.currentCarCount} <span id="checkBlacklistMsg" style="margin-left: 10px"></span>`,
responsiveLayout: "collapse",
responsiveLayoutCollapse: !0,
columnDefaults: {
headerHozAlign: "center",
hozAlign: "center"
},
index: "starId",
columns: [ {
title: "演员",
field: "name",
sorter: "string",
minWidth: 100,
responsive: 0,
headerSort: !1,
formatter: (cell, formatterParams, onRendered) => {
const item = cell.getData();
return `<a class="open-url" data-url="${item.url}" href="${item.url}" data-name="${item.name}" target="_blank">${item.name}</a>`;
}
}, {
title: "性别角色",
field: "role",
sorter: "string",
width: 120,
responsive: 5,
formatter: (cell, formatterParams, onRendered) => {
const role = cell.getData().role;
let content = role;
"actor" === role ? content = "男演员" : "actress" === role && (content = "女演员");
return content;
}
}, {
title: "影视类别",
field: "movieType",
sorter: "string",
width: 120,
responsive: 5,
formatter: (cell, formatterParams, onRendered) => {
const movieType = cell.getData().movieType;
let content = movieType;
"censored" === movieType ? content = "有码" : "uncensored" === movieType && (content = "无码");
return content;
}
}, {
title: "屏蔽类型",
field: "url",
sorter: "string",
minWidth: 120,
responsive: 4,
visible: isJavDb,
formatter: (cell, formatterParams, onRendered) => {
const item = cell.getData();
if ("虚拟演员" === item.role) {
const nameSplitList = item.name.split("-");
return `<span style="color:#cc4444">${nameSplitList.length > 0 ? nameSplitList[1] : item.name}</span>`;
}
const tParam = utils.getUrlParam(item.url, "t");
if (!tParam) return "<span>所有分类</span>";
{
const tParamList = tParam.toString().split(",");
let selectTypeStr = "";
tParamList.forEach((i => {
let value = categoryMap[i];
selectTypeStr += value ? value + " " : "未知类别:" + i + " ";
}));
selectTypeStr = selectTypeStr.trim();
if (selectTypeStr) return `<span style="color:#cc4444">${selectTypeStr}</span>`;
}
}
}, {
title: "番号数量",
field: "count",
sorter: "number",
width: 170,
responsive: 1
}, {
title: "创建时间",
field: "createTime",
sorter: "string",
width: 170,
responsive: 5
}, {
title: "最后发行时间",
field: "lastPublishTime",
sorter: "string",
width: 170,
responsive: 1
}, {
title: "状态",
field: "isUnCheck",
sorter: "string",
width: 120,
responsive: 1,
formatter: (cell, formatterParams, onRendered) => {
let dataTip = "", content = "正常检测";
if (cell.getData().isUnCheck) {
dataTip = `停更${this.checkBlacklist_ruleTime / 24 / 365}年以上, 下轮任务不再进行检测`;
content = "停止检测";
}
return `<span data-tip="${dataTip}" style="${dataTip ? "color: #cc4444;" : ""}">${content}</span>`;
}
}, {
title: "操作",
sorter: "string",
cssClass: "action-cell-dropdown",
minWidth: 150,
responsive: 0,
headerSort: !1,
formatter: (cell, formatterParams, onRendered) => {
const item = cell.getData();
onRendered((() => {
var _a2, _b;
null == (_a2 = cell.getElement().querySelector(".delete-btn")) || _a2.addEventListener("click", (e => {
const name2 = item.name, starId = item.starId;
name2 ? starId ? utils.q(e, `是否移除对 ${name2} 的屏蔽?`, (async () => {
await storageManager.removeBlacklistCarList(starId);
await storageManager.deleteBlacklistItem(starId);
show.info("操作成功");
this.reloadTable().then();
})) : show.error("获取starId失败") : show.error("获取名称失败");
}));
null == (_b = cell.getElement().querySelector(".keyword-btn")) || _b.addEventListener("click", (e => {
const prefixMap = item.carList.reduce(((dataMap, carItem) => {
const prefix = carItem.carNum.split("-")[0] + "-";
dataMap[prefix] = (dataMap[prefix] || 0) + 1;
return dataMap;
}), {}), sortedPrefixList = Object.entries(prefixMap).map((([prefix, count]) => ({
prefix: prefix,
count: count
}))).sort(((a, b) => b.count - a.count));
console.log(sortedPrefixList);
}));
}));
return '\n \x3c!-- <a class="a-normal keyword-btn"> <span>提取屏蔽词</span> </a>--\x3e\n <a class="a-danger delete-btn"> <span>✂️ 删除</span> </a>\n ';
}
} ],
initialSort: [ {
column: "createTime",
dir: "desc"
} ],
locale: "zh-cn",
langs: {
"zh-cn": {
pagination: {
first: "首页",
first_title: "首页",
last: "尾页",
last_title: "尾页",
prev: "上一页",
prev_title: "上一页",
next: "下一页",
next_title: "下一页",
all: "所有",
page_size: "每页行数"
}
}
}
});
}
getCurrentStarUrl() {
let urlWithoutSortType = window.location.href.replace(/([&?])sort_type=[^&]+(&|$)/, "$1");
urlWithoutSortType = urlWithoutSortType.replace(/[&?]$/, "");
urlWithoutSortType = urlWithoutSortType.replace(/\?&/, "?");
let urlWithoutPageNumber = urlWithoutSortType;
urlWithoutPageNumber = urlWithoutPageNumber.replace(/([&?])page=\d+(&|$)/, "$1");
urlWithoutPageNumber = urlWithoutPageNumber.replace(/[&?]$/, "");
urlWithoutPageNumber = urlWithoutPageNumber.replace(/\?&/, "?");
urlWithoutPageNumber = urlWithoutPageNumber.replace(/\/(\d+)(?:\/(\d+))?(\?|$)/, ((match, id, page, suffix) => void 0 !== page ? `/${id}${suffix}` : match));
return urlWithoutPageNumber;
}
parseUrlId(url) {
if (!url) throw new Error("url未传入");
return new URL(url).pathname.split("/").filter((segment => "" !== segment.trim())).pop();
}
async filterAllVideo(actorName, $dom) {
let movieList, nextPageLink;
if ($dom) {
isJavBus && $dom.find(".avatar-box").length > 0 && $dom.find(".avatar-box").parent().remove();
movieList = $dom.find(this.getSelector().requestDomItemSelector);
nextPageLink = $dom.find(this.getSelector().nextPageSelector).attr("href");
} else {
movieList = $(this.getSelector().itemSelector);
nextPageLink = $(this.getSelector().nextPageSelector).attr("href");
}
if (nextPageLink && 0 === movieList.length) {
show.error("解析列表失败");
throw new Error("解析列表失败");
}
for (const element of movieList) {
const $item = $(element), {carNum: carNum2, url: url, publishTime: publishTime} = this.getBoxCarInfo($item);
if (url && carNum2) try {
if (await storageManager.getCar(carNum2)) continue;
await storageManager.saveCar({
carNum: carNum2,
url: url,
names: actorName,
actionType: Status_FILTER,
publishTime: publishTime
});
clog.log("屏蔽演员番号", actorName, carNum2);
} catch (error) {
console.error(`保存失败 [${carNum2}]:`, error);
}
}
if (nextPageLink) {
show.info("请不要关闭窗口, 正在解析下一页:" + nextPageLink);
await new Promise((resolve => setTimeout(resolve, 500)));
const html = await gmHttp.get(nextPageLink), parser = new DOMParser, next$dom = $(parser.parseFromString(html, "text/html"));
await this.filterAllVideo(actorName, next$dom);
} else {
show.ok("执行结束!");
window.refresh();
}
}
async filterActorVideo(actorName, starId, $dom) {
let {nextPageLink: nextPageLink} = await this.parseAndSaveFilterInfo($dom, actorName, starId);
this.nextPageLink = nextPageLink;
if (nextPageLink) {
this.lastPageLink = nextPageLink;
show.info("请不要关闭窗口, 正在解析下一页:" + nextPageLink);
let next$dom;
const pageNum = utils.getUrlParam(nextPageLink, "page") || 0, beyond60Plugin = this.getBean("Beyond60Plugin");
if (isJavDb && beyond60Plugin && pageNum > 60) {
let {html: html, nextUrl: nextUrl, hasMore: hasMore} = await beyond60Plugin.handleBeyond60(nextPageLink), mergeHtml = `\n <div class ='movie-list'>${html}</div>\n ${nextUrl ? `<a class="pagination-next" href="${nextUrl}"></a>` : ""}\n `;
next$dom = utils.htmlTo$dom(mergeHtml);
} else {
clog.log("正在请求下一页内容:", nextPageLink);
const html = await gmHttp.get(nextPageLink);
next$dom = utils.htmlTo$dom(html);
}
await this.filterActorVideo(actorName, starId, next$dom);
} else {
show.ok("执行结束!");
window.refresh();
}
}
async parseAndSaveFilterInfo($dom, actorName, starId) {
let movieList, nextPageLink;
if ($dom) {
let tempIsJavBus = !1, selectorType = "javdb";
if ($dom.text().includes("javbus")) {
tempIsJavBus = !0;
selectorType = "javbus";
}
tempIsJavBus && $dom.find(".avatar-box").length > 0 && $dom.find(".avatar-box").parent().remove();
movieList = $dom.find(this.getSelector(selectorType).requestDomItemSelector);
nextPageLink = $dom.find(this.getSelector(selectorType).nextPageSelector).attr("href");
} else {
movieList = $(this.getSelector().itemSelector);
nextPageLink = $(this.getSelector().nextPageSelector).attr("href");
}
if (nextPageLink && 0 === movieList.length) return {
nextPageLink: null,
lastPublishTime: null
};
const filterKeywordList = await storageManager.getTitleFilterKeyword();
let carDataList = [], lastPublishTime = null;
for (const element of movieList) {
const $item = $(element), {carNum: carNum2, url: url, publishTime: publishTime, title: title} = this.getBoxCarInfo($item);
lastPublishTime || (lastPublishTime = publishTime);
filterKeywordList.find((kw => title.includes(kw) || carNum2.startsWith(kw))) || url && carNum2 && carDataList.push({
carNum: carNum2,
url: url,
names: actorName,
actionType: Status_FILTER,
starId: starId,
publishTime: publishTime
});
}
await storageManager.batchSaveBlacklistCarList(carDataList);
return {
nextPageLink: nextPageLink,
lastPublishTime: lastPublishTime
};
}
async clearKeywordBlacklist() {
let filterKeywordList = await storageManager.getTitleFilterKeyword();
const processedKeywordsSet = new Set;
filterKeywordList.forEach((item => {
/^[a-z]{2,}-/i.test(item) && processedKeywordsSet.add(item);
}));
if (0 === processedKeywordsSet.size) {
show.info("没有需要清理的关键词数据");
return;
}
const carList = await storageManager.getCarList(), blacklistCarList = await storageManager.getBlacklistCarList(), carNumSet = new Set(carList.map((car => car.carNum))), keywords = Array.from(processedKeywordsSet), waitRemoveCarNumList = [], removableCarData = [];
for (const car of blacklistCarList) {
const carNum2 = car.carNum;
let matched = !1;
for (const keyword of keywords) if (carNum2.startsWith(keyword)) {
removableCarData.push({
carNum: carNum2,
names: car.names,
matchedKeyword: keyword
});
waitRemoveCarNumList.push(carNum2);
matched = !0;
break;
}
if (!matched && carNumSet.has(carNum2)) {
removableCarData.push({
carNum: carNum2,
names: car.names,
matchedKeyword: "已在鉴定记录中"
});
waitRemoveCarNumList.push(carNum2);
}
}
const carNumCount = waitRemoveCarNumList.length;
0 !== carNumCount ? layer.open({
type: 1,
title: `确认清理黑名单数据 (共 ${carNumCount} 个番号)`,
area: [ "80%", "70%" ],
anim: -1,
content: `\n <div style="height: 100%;overflow:hidden;">\n <div style="padding: 10px; overflow-y: auto;">\n <p>以下匹配到 <strong style="color: red;">${carNumCount}</strong> 个黑名单番号存在于屏蔽关键词中, 是否移除?</p>\n </div>\n <div id="wait-remove-table" style="height: calc(100% - 70px);"></div>\n </div>\n `,
btn: [ "确定清理", "取消" ],
success: function(layero, index) {
new Tabulator("#wait-remove-table", {
data: removableCarData,
virtualDom: !0,
layout: "fitColumns",
pagination: !0,
paginationMode: "local",
paginationSize: 50,
paginationSizeSelector: [ 50, 100, 1e3, 99999 ],
columns: [ {
title: "演员",
field: "names"
}, {
title: "番号",
field: "carNum"
}, {
title: "匹配关键词",
field: "matchedKeyword"
} ],
locale: "zh-cn",
initialSort: [ {
column: "names",
dir: "asc"
} ],
langs: {
"zh-cn": {
pagination: {
first: "首页",
first_title: "首页",
last: "尾页",
last_title: "尾页",
prev: "上一页",
prev_title: "上一页",
next: "下一页",
next_title: "下一页",
all: "所有",
page_size: "每页行数"
}
}
}
});
},
yes: async index => {
const removedCount = await storageManager.batchRemoveBlacklistCars(waitRemoveCarNumList);
if (0 === removedCount) show.error("移除失败"); else {
show.ok(`🎉 清理完成!已移除${removedCount}个相关黑名单番号数据`);
await this.reloadTable();
layer.close(index);
}
},
btn2: function(index) {
layer.close(index);
}
}) : show.info("没有需要清理的黑名单番号数据");
}
}
export { BlacklistPlugin };
+66
View File
@@ -0,0 +1,66 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class BusDetailPagePlugin extends BasePlugin {
getName() {
return "BusDetailPagePlugin";
}
async initCss() {
if (!window.isDetailPage) return "";
$("h4:contains('推薦')").hide();
}
async handle() {
if (window.location.href.includes("/star/")) {
const $avatarBox = $(".avatar-box");
if ($avatarBox.length > 0) {
let parent2 = $avatarBox.parent();
parent2.css("position", "initial");
parent2.insertBefore(parent2.parent());
}
}
$(".genre a").each((function() {
const href2 = $(this).attr("href");
href2 && (href2.startsWith("http://") || href2.startsWith("https://") || href2.startsWith("/")) && $(this).attr("target", "_blank");
}));
this.addCopyCarNumBtn();
}
addCopyCarNumBtn() {
let headerSpan = null;
const headerSpans = document.querySelectorAll("span.header");
for (const span of headerSpans) if ("識別碼:" === span.textContent.trim()) {
headerSpan = span;
break;
}
if (headerSpan) {
const targetSpan = headerSpan.nextElementSibling;
if (targetSpan && "SPAN" === targetSpan.tagName) {
const identifierText = targetSpan.textContent.trim(), copyButton = document.createElement("button");
copyButton.textContent = "复制";
copyButton.style.marginLeft = "10px";
copyButton.style.padding = "0 10px";
copyButton.style.cursor = "pointer";
copyButton.style.border = "1px solid #ccc";
copyButton.style.borderRadius = "5px";
copyButton.style.backgroundColor = "#f0f0f0";
copyButton.style.fontSize = "12px";
copyButton.addEventListener("click", (function(event) {
event.preventDefault();
const copyAction = text => {
this.textContent = "已复制";
setTimeout((() => {
this.textContent = "复制";
}), 1500);
};
navigator.clipboard && navigator.clipboard.writeText && navigator.clipboard.writeText(identifierText).then((() => copyAction())).catch((err => {
console.error("无法通过标准API复制:", err);
alert("复制失败,请手动复制: " + identifierText);
}));
}));
targetSpan.parentNode.insertBefore(copyButton, targetSpan.nextSibling);
}
}
}
}
export { BusDetailPagePlugin };
+56
View File
@@ -0,0 +1,56 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, NO, YES } from '../core/constants.js';
class BusImgPlugin extends BasePlugin {
getName() {
return "BusImgPlugin";
}
handle() {}
async getVisibleImageItems(itemSelector, imgSelector) {
let itemList = [];
const allItems = document.querySelectorAll(itemSelector);
for (const item of allItems) {
if (!utils.isHidden(item)) {
const imgElement = item.querySelector(imgSelector);
if (!(imgElement instanceof HTMLImageElement)) continue;
imgElement.style.removeProperty("height");
let imageHeight = imgElement.offsetHeight;
imageHeight > 0 && itemList.push({
element: item,
imgElement: imgElement,
height: imageHeight
});
}
}
return itemList;
}
async logImageHeightsByRow() {
if (await storageManager.getSetting("enableVerticalModel", NO) === YES) return;
const itemSelector = this.getSelector().itemSelector, containerColumns = await storageManager.getSetting("containerColumns", 5), itemList = await this.getVisibleImageItems(itemSelector, "img");
if (0 === itemList.length) return;
const groupedItems = [];
for (let i = 0; i < itemList.length; i++) {
const rowIndex = Math.floor(i / containerColumns);
groupedItems[rowIndex] || (groupedItems[rowIndex] = []);
groupedItems[rowIndex].push(itemList[i]);
}
groupedItems.forEach(((row, rowIndex) => {
const originalHeights = row.map((item => item.height));
if (originalHeights.length < 2) return;
const minHeight = Math.min(...originalHeights), maxHeight = Math.max(...originalHeights);
let targetHeight = 0;
if (maxHeight - minHeight > 50) {
targetHeight = minHeight;
row.forEach((item => {
if (item.height !== targetHeight) {
const heightValue = `${targetHeight}px`;
item.imgElement.style.setProperty("height", heightValue, "important");
}
}));
}
}));
}
}
export { BusImgPlugin };
+17
View File
@@ -0,0 +1,17 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class BusNavBarPlugin extends BasePlugin {
getName() {
return "BusNavBarPlugin";
}
handle() {
$("#navbar > div > div > span").append('\n <button class="btn btn-default" style="color: #0d9488" id="search-img-btn">识图</button>\n ');
$("#search-img-btn").on("click", (() => {
this.getBean("ImageRecognitionPlugin").open();
}));
}
}
export { BusNavBarPlugin };
+110
View File
@@ -0,0 +1,110 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, qualityOptions } from '../core/constants.js';
class BusPreviewVideoPlugin extends BasePlugin {
getName() {
return "BusPreviewVideoPlugin";
}
async initCss() {
return "\n /* 弹窗/Modal 样式 */\n .bus-preview-modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.95); \n /* 关键修改:更新 z-index */\n z-index: 12345699; \n display: flex;\n justify-content: center;\n align-items: center;\n opacity: 0; \n visibility: hidden; \n transition: opacity 0.2s ease;\n }\n .bus-preview-modal.is-open {\n opacity: 1;\n visibility: visible;\n }\n /* 垂直排列视频和按钮,并居中 */\n .bus-preview-modal-content {\n position: relative;\n max-width: 95%; \n max-height: 95%;\n display: flex; \n flex-direction: column; \n align-items: center; \n gap: 15px; \n }\n \n /* 移除 .bus-preview-close-btn 的样式 */\n\n /* 视频播放器容器 */\n .video-player-wrapper {\n /* 关键修改:更新 width 和 max-height */\n width: 80vw; \n max-height: 85vh; \n aspect-ratio: 16 / 9; \n position: relative; \n background-color: black; \n max-width: 100%; \n }\n /* 视频元素 */\n .video-player-wrapper #preview-video {\n position: absolute; \n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: block;\n }\n\n /* 画质控制盒 (底部按钮) */\n .video-control-box {\n display: flex;\n flex-direction: row; \n justify-content: center; \n flex-wrap: wrap; \n gap: 10px;\n padding: 10px 0; \n }\n\n /* 按钮样式 (保留) */\n .video-control-btn {\n min-width:80px;\n padding: 6px 12px;\n background: rgba(255,255,255,0.2);\n color: white;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 4px;\n cursor: pointer;\n text-align: center;\n font-size: 14px;\n transition: background-color 0.2s, border-color 0.2s;\n }\n .video-control-btn:hover {\n background: rgba(255,255,255,0.4);\n }\n .video-control-btn.active {\n background-color: #1890ff; \n color: white;\n font-weight: bold;\n border: 1px solid #096dd9;\n }\n ";
}
initModal() {
if (0 === $("#bus-preview-modal").length) {
$("body").append('\n <div id="bus-preview-modal" class="bus-preview-modal">\n <div class="bus-preview-modal-content">\n </div>\n </div>\n ');
const $modal = $("#bus-preview-modal");
$modal.on("click", (e => {
"bus-preview-modal" === e.target.id && this.closeVideoModal();
}));
$(document).on("keydown", (e => {
"Escape" === e.key && $modal.hasClass("is-open") && this.closeVideoModal();
}));
}
}
closeVideoModal() {
const $previewVideo = $("#preview-video");
$previewVideo.length > 0 && $previewVideo[0].pause();
$("#bus-preview-modal").removeClass("is-open");
}
async handle() {
if (!window.isDetailPage) return;
this.initModal();
const firstImageSrc = $("#sample-waterfall .sample-box .photo-frame img:first").attr("src"), videoPreview = $(`\n <a class="preview-video-container sample-box" style="cursor: pointer">\n <div class="photo-frame" style="position:relative;">\n <img src="${firstImageSrc}" class="video-cover" alt="">\n <div class="play-icon" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); \n color:white; font-size:40px; text-shadow:0 0 10px rgba(0,0,0,0.5);">\n ▶\n </div>\n </div>\n </a>`);
$("#sample-waterfall").prepend(videoPreview);
"yes" === await storageManager.getSetting("enableLoadPreviewVideo", "yes") && getDmmVideo(this.getPageInfo().carNum, !1).then();
let isHandlingVideo = !1, $preview = $(".preview-video-container");
$preview.on("click", (async event => {
event.preventDefault();
event.stopPropagation();
if (isHandlingVideo) show.info("正在加载中, 勿重复点击"); else {
isHandlingVideo = !0;
try {
await this.handleVideo();
} finally {
isHandlingVideo = !1;
}
}
}));
window.location.href.includes("autoPlay=1") && $preview.trigger("click");
}
async handleVideo() {
const $modal = $("#bus-preview-modal"), $modalContent = $modal.find(".bus-preview-modal-content");
let $previewVideo = $("#preview-video");
if ($previewVideo.length > 0) {
$modal.addClass("is-open");
$previewVideo[0].play().catch((e => console.warn("尝试播放失败 (可能被浏览器阻止):", e)));
return;
}
let carNum2 = this.getPageInfo().carNum;
const dmmVideoMap = await getDmmVideo(carNum2);
if (dmmVideoMap && 0 !== Object.keys(dmmVideoMap).length) {
await this.createVideoPlayerAndControls(dmmVideoMap, $modalContent);
$previewVideo = $("#preview-video");
if ($previewVideo.length > 0) {
$modal.addClass("is-open");
$previewVideo[0].play().catch((e => console.warn("尝试播放失败 (可能被浏览器阻止):", e)));
} else show.error("视频播放器创建失败。");
} else show.error("未找到可用的视频源。");
}
async createVideoPlayerAndControls(dmmVideoMap, $container) {
let defaultVideoQuality = await storageManager.getSetting("videoQuality");
defaultVideoQuality = selectDefaultQuality(Object.keys(dmmVideoMap), defaultVideoQuality);
let defaultVideoUrl = dmmVideoMap[defaultVideoQuality];
$container.html(`\n <div class="video-player-wrapper">\n <video id="preview-video" controls playsinline>\n <source src="${defaultVideoUrl}" />\n </video>\n </div>\n <div class="video-control-box">\n </div>\n `);
const $videoEl = $("#preview-video"), $previewSource = $videoEl.find("source"), $qualityControlsBox = $container.find(".video-control-box");
if (!$videoEl.length || !$previewSource.length) return;
const videoEl = $videoEl[0], jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
videoEl.muted = !jhs_videoMuted || "yes" === jhs_videoMuted;
videoEl.addEventListener("volumechange", (function() {
localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no");
}));
let buttonsHtml = "";
qualityOptions.forEach((option => {
let dmmVideoUrl = dmmVideoMap[option.quality];
if (dmmVideoUrl) {
const isActive = defaultVideoQuality === option.quality;
buttonsHtml += `\n <button class="video-control-btn${isActive ? " active" : ""}" \n data-quality="${option.quality}"\n data-video-src="${dmmVideoUrl}">\n ${option.text}\n </button>\n `;
}
}));
$qualityControlsBox.html(buttonsHtml);
const $buttons = $qualityControlsBox.find(".video-control-btn");
$qualityControlsBox.off("click").on("click", ".video-control-btn", (async e => {
try {
const $button = $(e.currentTarget);
if ($button.hasClass("active")) return;
let videoSrc = $button.attr("data-video-src");
$previewSource.attr("src", videoSrc);
const currentTime = videoEl.currentTime;
videoEl.load();
videoEl.currentTime = currentTime;
await videoEl.play();
$buttons.removeClass("active");
$button.addClass("active");
} catch (error) {
console.error("切换画质失败:", error);
}
}));
}
}
export { BusPreviewVideoPlugin };
File diff suppressed because one or more lines are too long
+336
View File
@@ -0,0 +1,336 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from '../core/constants.js';
import { HotkeyManager } from './preview-video.js';
class DetailPageButtonPlugin extends BasePlugin {
getName() {
return "DetailPageButtonPlugin";
}
constructor() {
super();
this.answerCount = 1;
}
async handle() {
let settingObj = await storageManager.getSetting();
this.filterHotKey = settingObj.filterHotKey;
this.favoriteHotKey = settingObj.favoriteHotKey;
this.hasDownHotKey = settingObj.hasDownHotKey;
this.hasWatchHotKey = settingObj.hasWatchHotKey;
this.speedVideoHotKey = settingObj.speedVideoHotKey;
this.bindHotkey().then();
this.hideVideoControls();
window.isDetailPage && this.createMenuBtn();
}
async createMenuBtn() {
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum, buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n </div>\n </div>\n ';
isJavDb && $(".tabs").after(buttonsHtml);
isJavBus && $("#mag-submit-show").before(buttonsHtml);
$("#favoriteBtn").on("click", (() => this.favoriteOne()));
$("#filterBtn").on("click", (event => this.filterOne(event)));
$("#hasDownBtn").on("click", (async () => this.hasDownOne()));
$("#hasWatchBtn").on("click", (async () => this.hasWatchOne()));
$("#magnetSearchBtn").on("click", (() => {
let magnetHub = this.getBean("MagnetHubPlugin").createMagnetHub(pageInfo.carNum);
layer.open({
type: 1,
title: "磁力搜索 " + pageInfo.carNum,
content: '<div id="magnetHubBox"></div>',
area: utils.getResponsiveArea([ "60%", "80%" ]),
scrollbar: !1,
success: () => {
$("#magnetHubBox").append(magnetHub);
}
});
}));
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"), enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", YES);
$("#magnets-span").text(enableMagnetsFilter === YES ? "关闭磁力过滤" : "开启磁力过滤");
enableMagnetsFilter === YES && highlightMagnetPlugin.doFilterMagnet();
$("#enable-magnets-filter").on("click", (event => {
let $span = $("#magnets-span");
if ("关闭磁力过滤" === $span.text()) {
highlightMagnetPlugin.showAll();
$span.text("开启磁力过滤");
storageManager.saveSettingItem("enableMagnetsFilter", NO);
} else {
highlightMagnetPlugin.doFilterMagnet();
$span.text("关闭磁力过滤");
storageManager.saveSettingItem("enableMagnetsFilter", YES);
}
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
this.showStatus(carNum2).then();
}
async showStatus(carNum2) {
const $filterBtn = $("#filterBtn span"), $favoriteBtn = $("#favoriteBtn span"), $hasDownBtn = $("#hasDownBtn span"), $hasWatchBtn = $("#hasWatchBtn span"), hotKeyDisplay = hotKey => hotKey ? `(${hotKey})` : "";
$filterBtn.text(`🚫 屏蔽 ${hotKeyDisplay(this.filterHotKey)}`);
$favoriteBtn.text(`⭐ 收藏 ${hotKeyDisplay(this.favoriteHotKey)}`);
$hasDownBtn.text(`📥️ 已下载 ${hotKeyDisplay(this.hasDownHotKey)}`);
$hasWatchBtn.text(`🔍 已观看 ${hotKeyDisplay(this.hasWatchHotKey)}`);
const car = await storageManager.getCar(carNum2);
if (car) switch (car.status) {
case Status_FILTER:
$filterBtn.text(`🚫 已屏蔽 ${hotKeyDisplay(this.filterHotKey)}`);
break;
case Status_FAVORITE:
$favoriteBtn.text(`⭐ 已收藏 ${hotKeyDisplay(this.favoriteHotKey)}`);
break;
case Status_HAS_DOWN:
$hasDownBtn.text(`📥️ 已标记下载 ${hotKeyDisplay(this.hasDownHotKey)}`);
break;
case Status_HAS_WATCH:
$hasWatchBtn.text(`🔍 已标记观看 ${hotKeyDisplay(this.hasWatchHotKey)}`);
}
}
async favoriteOne() {
let pageInfo = this.getPageInfo();
await storageManager.saveCar({
carNum: pageInfo.carNum,
url: pageInfo.url,
names: pageInfo.actress,
actionType: Status_FAVORITE,
publishTime: pageInfo.publishTime
});
this.showStatus(pageInfo.carNum).then();
window.refresh();
utils.closePage();
}
async hasDownOne() {
let pageInfo = this.getPageInfo();
await storageManager.saveCar({
carNum: pageInfo.carNum,
url: pageInfo.url,
names: pageInfo.actress,
actionType: Status_HAS_DOWN,
publishTime: pageInfo.publishTime
});
this.showStatus(pageInfo.carNum).then();
window.refresh();
utils.closePage();
}
async hasWatchOne() {
let pageInfo = this.getPageInfo();
await storageManager.saveCar({
carNum: pageInfo.carNum,
url: pageInfo.url,
names: pageInfo.actress,
actionType: Status_HAS_WATCH,
publishTime: pageInfo.publishTime
});
this.showStatus(pageInfo.carNum).then();
window.refresh();
utils.closePage();
}
searchXunLeiSubtitle(carNum2) {
let loadObj = loading();
gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${carNum2}`).then((res => {
let dataList = res.data;
dataList && 0 !== dataList.length ? layer.open({
type: 1,
title: "迅雷字幕",
content: '\n <div style="height: 100%;overflow:hidden;"> \n <div id="xunlei-table-container" style="height: 100%;padding-bottom: 20px"></div>\n </div>\n ',
scrollbar: !1,
area: utils.getResponsiveArea([ "60%", "70%" ]),
anim: -1,
success: (layero, index) => {
new Tabulator("#xunlei-table-container", {
layout: "fitColumns",
placeholder: "暂无数据",
virtualDom: !0,
data: dataList,
responsiveLayout: "collapse",
responsiveLayoutCollapse: !0,
columnDefaults: {
headerHozAlign: "center",
hozAlign: "center"
},
columns: [ {
title: "文件名",
field: "name",
headerSort: !1,
responsive: 0
}, {
title: "类型",
field: "ext",
headerSort: !1,
responsive: 0
}, {
title: "操作",
responsive: 0,
headerSort: !1,
formatter: (cell, formatterParams, onRendered) => {
const item = cell.getData();
onRendered((() => {
const previewButton = cell.getElement().querySelector(".a-primary"), downButton = cell.getElement().querySelector(".a-success");
previewButton && previewButton.addEventListener("click", (async e => {
let url = item.url, name2 = carNum2 + "." + item.ext;
this.previewSubtitle(url, name2);
}));
downButton && downButton.addEventListener("click", (async e => {
let url = item.url, name2 = carNum2 + "." + item.ext, content = await gmHttp.get(url);
utils.download(content, name2);
}));
}));
return '\n <a class="a-primary">预览</a>\n <a class="a-success">下载</a>\n ';
}
} ],
locale: "zh-cn",
langs: {
"zh-cn": {
pagination: {
first: "首页",
first_title: "首页",
last: "尾页",
last_title: "尾页",
prev: "上一页",
prev_title: "上一页",
next: "下一页",
next_title: "下一页",
all: "所有",
page_size: "每页行数"
}
}
}
});
utils.setupEscClose(index);
}
}) : show.error("迅雷中找不到相关字幕!");
})).catch((e => {
console.error(e);
show.error(e);
})).finally((() => {
loadObj.close();
}));
}
async filterOne(event, noAlert) {
event && event.preventDefault();
let pageInfo = this.getPageInfo();
if (noAlert) {
await storageManager.saveCar({
carNum: pageInfo.carNum,
url: pageInfo.url,
names: pageInfo.actress,
actionType: Status_FILTER,
publishTime: pageInfo.publishTime
});
this.showStatus(pageInfo.carNum).then();
window.refresh();
utils.closePage();
layer.closeAll();
this.answerCount = 1;
} else utils.q(event, `是否屏蔽${pageInfo.carNum}?`, (async () => {
await storageManager.saveCar({
carNum: pageInfo.carNum,
url: pageInfo.url,
names: pageInfo.actress,
actionType: Status_FILTER,
publishTime: pageInfo.publishTime
});
this.showStatus(pageInfo.carNum).then();
window.refresh();
utils.closePage();
}), (() => {
this.answerCount = 1;
}));
}
speedVideo() {
if ($("#preview-video").is(":visible")) {
const videoEl = document.getElementById("preview-video");
if (videoEl) {
videoEl.muted = !1;
videoEl.controls = !1;
if (videoEl.currentTime + 5 < videoEl.duration) videoEl.currentTime += 5; else {
show.info("预览视频结束, 已回到开头");
videoEl.currentTime = 1;
}
}
return;
}
const iframe = $('iframe[id^="layui-layer-iframe"]');
if (iframe.length > 0) {
iframe[0].contentWindow.postMessage("speedVideo", "*");
return;
}
let $videoPlayBtn = $(".preview-video-container");
if ($videoPlayBtn.length > 0) {
$videoPlayBtn[0].click();
const videoEl = document.getElementById("preview-video");
if (videoEl) {
videoEl.currentTime += 5;
videoEl.muted = !1;
}
} else $("#javTrailersBtn").click();
}
hideVideoControls() {
$(document).on("mouseenter", "#preview-video", (function() {
$(this).prop("controls", !0);
}));
}
async bindHotkey() {
const handlers = {};
this.filterHotKey && (handlers[this.filterHotKey] = () => {
this.answerCount >= 2 ? this.filterOne(null, !0) : this.filterOne(null);
this.answerCount++;
});
this.favoriteHotKey && (handlers[this.favoriteHotKey] = () => this.favoriteOne(null));
this.hasDownHotKey && (handlers[this.hasDownHotKey] = () => this.hasDownOne());
this.hasWatchHotKey && (handlers[this.hasWatchHotKey] = () => this.hasWatchOne());
this.speedVideoHotKey && (handlers[this.speedVideoHotKey] = () => this.speedVideo());
const registerHotkey = (key, handler) => {
HotkeyManager.registerHotkey(key, (event => {
const activeElement = document.activeElement;
"INPUT" === activeElement.tagName || "TEXTAREA" === activeElement.tagName || activeElement.isContentEditable || (window.isDetailPage ? handler() : (message => {
const childIframe = $(".layui-layer-content iframe");
if (0 === childIframe.length) return !1;
childIframe[0].contentWindow.postMessage(message, "*");
})(key));
}));
};
window.isDetailPage && window.addEventListener("message", (event => {
handlers[event.data] && handlers[event.data]();
}));
Object.entries(handlers).forEach((([key, handler]) => {
registerHotkey(key, handler);
}));
}
async previewSubtitle(url, name2) {
if (!url) {
console.error("未提供文件URL");
return;
}
const fileExt = url.split(".").pop().toLowerCase();
if ("ass" === fileExt || "srt" === fileExt) try {
let resText = await gmHttp.get(url), title = "字幕预览";
"ass" === fileExt ? title = "ASS字幕预览 - " + name2 : "srt" === fileExt && (title = "SRT字幕预览 - " + name2);
const lines = resText.split("\n");
let numberedContent = "";
const maxLineNumberLength = String(lines.length).length;
lines.forEach(((line, index) => {
const paddedLineNumber = String(index + 1).padStart(maxLineNumberLength, " ");
numberedContent += `<span style="color:#AAA;">${paddedLineNumber}. </span>${line}\n`;
}));
const finalContent = numberedContent;
layer.open({
type: 1,
title: title,
area: [ "80%", "80%" ],
scrollbar: !1,
content: `<div style="padding:15px 5px;background:#1E1E1E;color:#FFF;font-family:Consolas,Monaco,monospace;white-space:pre-wrap;overflow:auto;height:100%;">${finalContent}</div>`,
btn: [ "下载", "关闭" ],
btn1: function(index, layero, that) {
utils.download(resText, name2);
return !1;
}
});
} catch (error) {
show.error(`预览失败: ${error.message}`);
console.error("预览字幕文件出错:", error);
} else show.error("仅支持预览ASS和SRT字幕文件");
}
}
export { DetailPageButtonPlugin };
+39
View File
@@ -0,0 +1,39 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
const _unsafeWindow = (() => typeof unsafeWindow !== 'undefined' ? unsafeWindow : window)();
class DetailPagePlugin extends BasePlugin {
getName() {
return "DetailPagePlugin";
}
constructor() {
super();
}
handle() {
if (window.isDetailPage) {
$(".video-meta-panel a").each((function() {
const href = $(this).attr("href");
href && (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("/")) && $(this).attr("target", "_blank");
}));
this.handleFancyBox();
}
}
handleFancyBox() {
document.addEventListener("click", (function(e) {
if (e.target.closest(".fancybox-button--thumbs")) {
const isVisible = !$(".fancybox-thumbs").is(":hidden");
localStorage.setItem("jhs_fancyboxThumbs", isVisible.toString());
_unsafeWindow.$.fancybox.defaults.thumbs.autoStart = isVisible;
}
}));
if (void 0 !== _unsafeWindow.$.fancybox) {
const savedState = localStorage.getItem("jhs_fancyboxThumbs");
_unsafeWindow.$.fancybox.defaults.thumbs.autoStart = "true" === savedState;
}
}
}
export { DetailPagePlugin };
+104
View File
@@ -0,0 +1,104 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, YES } from '../core/constants.js';
class FavoriteActressesPlugin extends BasePlugin {
getName() {
return "FavoriteActressesPlugin";
}
async handle() {
this.bindEvent();
await this.highlightActress();
this.replaceActressAvatar();
}
async highlightActress() {
if (!window.isDetailPage) return;
if (await storageManager.getSetting("enableFavoriteActresses", YES) !== YES) return;
const favoriteActressesInfoList = await storageManager.getFavoriteActressList();
if (!favoriteActressesInfoList || 0 === favoriteActressesInfoList.length) return;
const favoriteStarIdSet = new Set;
favoriteActressesInfoList.forEach((actress => {
actress.starId && favoriteStarIdSet.add(String(actress.starId).trim());
}));
0 !== favoriteStarIdSet.size && $(".female").prev().each(((index, element) => {
const $element = $(element), href = $element.attr("href");
let elementStarId = null;
if (href) {
const parts = (href.endsWith("/") ? href.slice(0, -1) : href).split("/"), lastPart = parts[parts.length - 1];
lastPart && (elementStarId = lastPart.trim());
}
let isFavorite = !1;
elementStarId && (isFavorite = favoriteStarIdSet.has(elementStarId));
if (isFavorite) {
$element.addClass("highlighted");
$element.attr("title", "高亮已收藏演员, 可在设置-基础配置中关闭");
}
}));
}
async removeActorFromStorage(actorId) {
await storageManager.removeFavoriteActress(actorId) && clog.log("移除演员成功");
}
bindEvent() {
const listIdRegex = /\/actors\/(\w+)\/(collect|uncollect)/;
$(document).on("confirm:complete", 'a[href*="/actors/"][href*="/uncollect"]', (async event => {
const [userConfirmed] = event.detail;
if (!userConfirmed) return;
const match = $(event.currentTarget).attr("href").match(listIdRegex), actorId = match ? match[1] : null;
actorId && await this.removeActorFromStorage(actorId);
}));
$("#button-collect-actor").click((async event => {
const match = $("#button-collect-actor").attr("href").match(listIdRegex), actorId = match ? match[1] : null;
let nameList = [], $actor = $(".actor-section-name");
$actor.length && $actor.text().trim().split(",").forEach((name2 => {
nameList.push(name2.trim());
}));
let $sectionMeta = $(".section-meta:not(:contains('影片'))");
$sectionMeta.length && $sectionMeta.text().trim().split(",").forEach((name2 => {
nameList.push(name2.trim());
}));
if (!nameList) {
clog.error("获取演员名称失败");
return;
}
const actorName = nameList[0];
if (!actorId) {
clog.error("无法获取演员ID进行收藏操作。");
return;
}
const avatar = ($(".avatar").first().css("background-image") || "").replace(/^url\(["']?|["']?\)$/g, ""), actressInfo = {
starId: actorId,
name: actorName,
allName: nameList,
avatar: avatar
};
1 === await storageManager.addFavoriteActressList([ actressInfo ]) ? clog.log(`收藏演员成功: ${actorName} (ID: ${actorId})`) : clog.log(`收藏演员失败: ${actorName} (ID: ${actorId})`);
}));
$("#button-uncollect-actor").click((async event => {
const match = $("#button-uncollect-actor").attr("href").match(listIdRegex), actorId = match ? match[1] : null;
actorId ? await this.removeActorFromStorage(actorId) : clog.error("无法获取演员ID进行取消收藏操作。");
}));
}
async replaceActressAvatar() {
const actressId = this.getActressId();
if (!actressId) return;
const actress = (await storageManager.getFavoriteActressList()).find((item => item.starId === actressId));
if (actress && actress.avatar) {
const newAvatarUrl = `url('${actress.avatar}')`;
let $avatarElement = $(".avatar").first();
if (0 === $avatarElement.length) {
const newAvatarHtml = '<div class="column actor-avatar"> <div class="image"> <span class="avatar"></span> </div> </div>';
$(".section-columns").prepend(newAvatarHtml);
$avatarElement = $(".avatar").first();
}
if (0 === $avatarElement.length) return;
if ($avatarElement.css("background-image").trim().toLowerCase() !== newAvatarUrl.trim().toLowerCase()) {
$avatarElement.css("background-image", newAvatarUrl);
$avatarElement.css("background-size", "cover");
$avatarElement.css("background-position", "top center");
$avatarElement.css("background-repeat", "no-repeat");
}
}
}
}
export { FavoriteActressesPlugin };
+354
View File
@@ -0,0 +1,354 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, currentHref, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, YES } from '../core/constants.js';
class Fc2By123AvPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "$contentBox", $(".section .container"));
__publicField(this, "urlParams", new URLSearchParams(window.location.search));
__publicField(this, "sortVal", this.urlParams.get("sort") || "release_date");
__publicField(this, "currentPage", this.urlParams.get("page") ? parseInt(this.urlParams.get("page")) : 1);
__publicField(this, "maxPage", null);
__publicField(this, "keyword", this.urlParams.get("keyword") || null);
}
getName() {
return "Fc2By123AvPlugin";
}
async getBaseUrl() {
const otherSitePlugin = this.getBean("OtherSitePlugin");
return await otherSitePlugin.getAv123Url() + "/ja";
}
handle() {
$("#navbar-menu-hero > div > div:nth-child(1) > div > a:nth-child(4)").after('<a class="navbar-item" href="/advanced_search?type=100&released_start=2099-09">123Av-Fc2</a>');
$('.tabs li:contains("FC2")').after('<li><a href="/advanced_search?type=100&released_start=2099-09"><span>123Av-Fc2</span></a></li>');
if (currentHref.includes("/advanced_search?type=100")) {
this.hookPage();
this.handleQuery().then();
}
}
hookPage() {
let $h2 = $("h2.section-title");
$h2.contents().first().replaceWith("123Av");
$h2.css("marginBottom", "0");
$h2.append('\n <div style="margin-left: 100px; width: 400px;">\n <input id="search-123av-keyword" type="text" placeholder="搜索123Av Fc2ppv内容" style="padding: 4px 5px;margin-right: 0">\n <a id="search-123av-btn" class="a-primary" style="margin-left: 0">搜索</a>\n <a id="clear-123av-btn" class="a-info" style="margin-left: 0">重置</a>\n </div>\n ');
$("#search-123av-keyword").val(this.keyword);
$("#search-123av-btn").on("click", (async () => {
let keyword = $("#search-123av-keyword").val().trim();
if (keyword) {
this.keyword = keyword;
utils.setHrefParam("keyword", keyword);
await this.handleQuery();
}
}));
$("#clear-123av-btn").on("click", (async () => {
$("#search-123av-keyword").val("");
this.keyword = "";
utils.setHrefParam("keyword", "");
$(".page-box").show();
$(".tool-box").show();
await this.handleQuery();
}));
$(".empty-message").remove();
$("#foldCategoryBtn").remove();
$(".section .container .box").remove();
$("#sort-toggle-btn").remove();
this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>');
this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>');
this.$contentBox.append('<div class="page-box"></div>');
$(".tool-box").append('\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="release_date">发布日期</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="recent_update">最近更新</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="trending">热门</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_today">今天最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_week">本周最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_month">本月最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed">最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_favourited">最受欢迎</a>\n </div>\n </div>\n ');
$(`#conditionBox a[data-sort="${this.sortVal}"]`).addClass("is-info");
utils.setHrefParam("sort", this.sortVal);
utils.setHrefParam("page", this.currentPage);
$("#conditionBox").on("click", "a.button", (e => {
let $target = $(e.target);
this.sortVal = $target.data("sort");
utils.setHrefParam("sort", this.sortVal);
$target.siblings().removeClass("is-info");
$target.addClass("is-info");
this.handleQuery();
}));
$(".page-box").append('\n <nav class="pagination">\n <a class="pagination-previous">上一页</a>\n <ul class="pagination-list"></ul>\n <a class="pagination-next">下一页</a>\n </nav>\n ');
$(document).on("click", ".pagination-link", (e => {
e.preventDefault();
this.currentPage = parseInt($(e.target).data("page"));
utils.setHrefParam("page", this.currentPage);
this.renderPagination();
this.handleQuery();
}));
$(".pagination-previous").on("click", (e => {
e.preventDefault();
if (this.currentPage > 1) {
this.currentPage--;
utils.setHrefParam("page", this.currentPage);
this.renderPagination();
this.handleQuery();
}
}));
$(".pagination-next").on("click", (e => {
e.preventDefault();
if (this.currentPage < this.maxPage) {
this.currentPage++;
utils.setHrefParam("page", this.currentPage);
this.renderPagination();
this.handleQuery();
}
}));
}
renderPagination() {
const $paginationList = $(".pagination-list");
$paginationList.empty();
let startPage = Math.max(1, this.currentPage - 2), endPage = Math.min(this.maxPage, this.currentPage + 2);
this.currentPage <= 3 ? endPage = Math.min(6, this.maxPage) : this.currentPage >= this.maxPage - 2 && (startPage = Math.max(this.maxPage - 5, 1));
if (startPage > 1) {
$paginationList.append('<li><a class="pagination-link" data-page="1">1</a></li>');
startPage > 2 && $paginationList.append('<li><span class="pagination-ellipsis">…</span></li>');
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === this.currentPage ? " is-current" : "";
$paginationList.append(`<li><a class="pagination-link${activeClass}" data-page="${i}">${i}</a></li>`);
}
if (endPage < this.maxPage) {
endPage < this.maxPage - 1 && $paginationList.append('<li><span class="pagination-ellipsis">…</span></li>');
$paginationList.append(`<li><a class="pagination-link" data-page="${this.maxPage}">${this.maxPage}</a></li>`);
}
}
async handleQuery() {
let loadObj = loading();
try {
let pagesToFetch = [];
pagesToFetch = 1 === this.currentPage ? [ 1, 2 ] : [ 2 * this.currentPage - 1, 2 * this.currentPage ];
if (this.keyword) {
pagesToFetch = [ 1 ];
$(".page-box").hide();
$(".tool-box").hide();
}
const baseUrl = await this.getBaseUrl(), fetchPromises = pagesToFetch.map((page => {
let url = `${baseUrl}/tags/fc2?sort=${this.sortVal}&page=${page}`;
this.keyword && (url = `${baseUrl}/search?keyword=${this.keyword}`);
return gmHttp.get(url);
})), htmlResults = await Promise.all(fetchPromises);
let dataList = [];
for (const html of htmlResults) {
let $dom = $(html);
$dom.find(".box-item").each(((index, element) => {
const $item = $(element), imgSrc = $item.find("img").attr("data-src");
let carNum2 = $item.find("img").attr("title");
const detailLink = $item.find(".detail a"), link = detailLink.attr("href"), href = baseUrl + (link.startsWith("/") ? link : "/" + link), title = detailLink.text().trim().replace(carNum2 + " - ", "");
carNum2 = carNum2.replace("FC2-PPV", "FC2");
dataList.push({
imgSrc: imgSrc,
carNum: carNum2,
href: href,
title: title
});
}));
if (!this.maxPage) {
let rawMaxPage, lastPageItem = $dom.find(".page-item:not(.disabled)").last();
if (lastPageItem.find("a.page-link").length) {
let href = lastPageItem.find("a.page-link").attr("href");
rawMaxPage = parseInt(href.split("page=")[1]);
} else rawMaxPage = parseInt(lastPageItem.find("span.page-link").text());
this.maxPage = Math.ceil(rawMaxPage / 2);
this.renderPagination();
}
}
if (0 === dataList.length) {
console.log(dataList);
show.error("无结果");
let errorUrl = `${baseUrl}/dm4/tags/fc2?sort=${this.sortVal}`;
this.keyword && (errorUrl = `${baseUrl}/search?keyword=${this.keyword}`);
console.error("获取数据失败!", errorUrl);
}
let movieHtml = this.markDataListHtml(dataList);
$(".movie-list").html(movieHtml);
await utils.smoothScrollToTop();
} catch (e) {
console.error(e);
} finally {
loadObj.close();
}
}
async open123AvFc2Dialog(carNum2, href) {
let otherHtml = "";
await storageManager.getSetting("enableLoadOtherSite", YES) === YES && (otherHtml = '<div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方站点: </strong></div>');
let pageHtml = `\n <div class="movie-detail-container">\n \x3c!-- <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n ${otherHtml}\n \n <div style="margin: 10px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!-- </div>--\x3e\n </div>\n `;
layer.open({
type: 1,
title: carNum2,
content: pageHtml,
area: utils.getDefaultArea(),
skin: "movie-detail-layer",
scrollbar: !1,
success: (layero, index) => {
utils.setupEscClose(index);
this.loadData(carNum2, href);
let keyword = carNum2.replace("FC2-", "");
$("#magnets-content").append(this.getBean("MagnetHubPlugin").createMagnetHub(keyword));
$("#favoriteBtn").on("click", (async event => {
const actress = $("#data-actress").text(), publishTime = $("#data-publishTime").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_FAVORITE,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
}));
$("#filterBtn").on("click", (event => {
utils.q(event, `是否屏蔽${carNum2}?`, (async () => {
const actress = $("#data-actress").text(), publishTime = $("#data-publishTime").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_FILTER,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
window.location.href.includes("collection_codes?movieId") && utils.closePage();
}));
}));
$("#hasDownBtn").on("click", (async event => {
const actress = $("#data-actress").text(), publishTime = $("#data-publishTime").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_HAS_DOWN,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
}));
$("#hasWatchBtn").on("click", (async event => {
const actress = $("#data-actress").text(), publishTime = $("#data-publishTime").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_HAS_WATCH,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
let tempCarNum = carNum2.replace("FC2-", "");
this.getBean("OtherSitePlugin").loadOtherSite(tempCarNum, carNum2).then();
}
});
}
async loadData(carNum2, href) {
let loadObj = loading();
try {
const {id: id, publishDate: publishDate, title: title, moviePoster: moviePoster} = await this.get123AvVideoInfo(href);
$(".movie-info-container").html(`\n <h3 class="movie-title" style="margin-bottom: 10px"><strong class="current-title">${title || "无标题"}</strong></h3>\n <div class="movie-meta" style="margin-bottom: 10px">\n <span><strong>番号: </strong>${carNum2 || "未知"}</span>\n <span><strong>年份: </strong>${publishDate || "未知"}</span>\n <span>\n <strong>站点: </strong>\n <a href="https://fc2ppvdb.com/articles/${carNum2.replace("FC2-", "")}" target="_blank">fc2ppvdb</a>\n <a style="margin-left: 5px;" href="https://adult.contents.fc2.com/article/${carNum2.replace("FC2-", "")}/" target="_blank">fc2电子市场</a>\n </span>\n </div>\n <div class="movie-actors" style="margin-bottom: 10px">\n <div class="actor-list"><strong>主演: </strong></div>\n </div>\n <div class="movie-seller" style="margin-bottom: 10px">\n <span><strong>販売者: </strong></span>\n </div>\n <div class="movie-gallery" style="margin-bottom: 10px">\n <strong>剧照: </strong>\n <div class="image-list"></div>\n </div>\n \n <div id="data-publishTime" style="display: none">${publishDate || ""}</div>\n\n `);
this.getImgList(carNum2).then();
this.getActressInfo(carNum2).then();
this.getBean("TranslatePlugin").translate(carNum2, !1).then();
} catch (e) {
console.error(e);
} finally {
loadObj.close();
}
}
handleLongImg(carNum2) {
utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => {
$(".movie-gallery .image-list").prepend(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 150px;max-width:150px; text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
const imgUrl = await this.getBean("ScreenShotPlugin").getScreenshot(carNum2);
if (imgUrl) {
$(".screen-container").html(`<img src="${imgUrl}" alt="" loading="lazy" style="width: 100%;">`);
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
showImageViewer(event.currentTarget);
}));
}
}));
}
async get123AvVideoInfo(href) {
const html = await gmHttp.get(href), match = html.match(/v-scope="Movie\({id:\s*(\d+),/), id = match ? match[1] : null, $dom = utils.htmlTo$dom(html);
return {
id: id,
publishDate: $dom.find('span:contains("リリース日:")').next("span").text(),
title: $dom.find("h1").text().trim(),
moviePoster: $dom.find("#player").attr("data-poster")
};
}
async getActressInfo(fc2Num) {
let url = `https://fc2ppvdb.com/articles/${fc2Num.replace("FC2-", "")}`;
const html = await gmHttp.get(url), $dom = $(html), actressNodeList = $dom.find("div").filter((function() {
return 0 === $(this).text().trim().indexOf("女優:");
}));
if (0 === actressNodeList.length || actressNodeList.length > 1) {
show.error("解析女优信息失败");
return;
}
const $actress = $(actressNodeList[0]).find("a");
let actorsHtml = "<strong>主演: </strong>";
if ($actress.length > 0) {
let actress = "";
$actress.each(((index, ele) => {
let $actor = $(ele), name2 = $actor.text(), actressHref = $actor.attr("href");
actorsHtml += `<span class="actor-tag"><a href="https://fc2ppvdb.com${actressHref}" target="_blank">${name2}</a></span>`;
actress += name2 + " ";
}));
$("#data-actress").text(actress);
} else actorsHtml += "<span>暂无演员信息</span>";
$(".actor-list").html(actorsHtml);
const sellerNodeList = $dom.find("div").filter((function() {
return 0 === $(this).text().trim().indexOf("販売者:");
}));
if (sellerNodeList.length > 0) {
const $sellerA = $(sellerNodeList[0]).find("a");
if ($sellerA.length > 0) {
const $seller = $($sellerA[0]);
let name2 = $seller.text(), sellerHref = $seller.attr("href");
$(".movie-seller").html(`<span><strong>販売者: </strong><a href="https://fc2ppvdb.com${sellerHref}" target="_blank">${name2}</a></span>`);
}
}
}
async getImgList(fc2Num) {
let tempCarNum = fc2Num.replace("FC2-", ""), url = `https://adult.contents.fc2.com/article/${fc2Num.replace("FC2-", "")}/`;
const html = await gmHttp.get(url, null, {
referer: url
});
let imgList = $(html).find(".items_article_SampleImagesArea img").map((function() {
return $(this).attr("src");
})).get(), imagesHtml = "";
Array.isArray(imgList) && imgList.length > 0 ? imagesHtml = imgList.map(((img, index) => `\n <a href="${img}" data-fancybox="movie-gallery" data-caption="剧照 ${index + 1}">\n <img src="${img}" class="movie-image-thumb" alt=""/>\n </a>\n `)).join("") : $(".movie-gallery").html("<h4>剧照: 暂无剧照</h4>");
$(".image-list").html(imagesHtml);
this.handleLongImg(tempCarNum);
}
async getMovie(id, moviePoster) {
let url = `${await this.getBaseUrl()}/ajax/v/${id}/videos`, loadObj = loading();
try {
let movieList = (await gmHttp.get(url)).result.watch;
if (movieList.length > 0) {
movieList.forEach((movieItem => {
movieItem.url = movieItem.url + "?poster=" + moviePoster;
}));
return movieList;
}
return null;
} catch (e) {
console.error(e);
} finally {
loadObj.close();
}
}
markDataListHtml(movies) {
let moviesHtml = "";
movies.forEach((movie => {
moviesHtml += `\n <div class="item">\n <a href="${movie.href}" class="box" title="${movie.title}">\n <div class="cover ">\n <img loading="lazy" src="${movie.imgSrc.replace("/s360", "")}" alt="">\n </div>\n <div class="video-title"><strong>${movie.carNum}</strong> ${movie.title}</div>\n <div class="score">\n </div>\n <div class="meta">\n </div>\n <div class="tags has-addons">\n </div>\n </a>\n </div>\n `;
}));
return moviesHtml;
}
}
export { Fc2By123AvPlugin };
+190
View File
@@ -0,0 +1,190 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, currentHref, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH } from '../core/constants.js';
import { javDbApi } from '../api/javdb.js';
class Fc2Plugin extends BasePlugin {
getName() {
return "Fc2Plugin";
}
async initCss() {
return "\n <style>\n /* 弹层样式 */\n .movie-detail-layer .layui-layer-title {\n font-size: 18px;\n color: #333;\n background: #f8f8f8;\n }\n \n \n /* 容器样式 */\n .movie-detail-container {\n margin: 40px;\n height: 100%;\n background: #fff;\n }\n \n .movie-poster-container {\n flex: 0 0 60%;\n padding: 15px;\n }\n \n .right-box {\n flex: 1;\n padding: 20px;\n overflow-y: auto;\n }\n \n /* 预告片iframe */\n .movie-trailer {\n width: 100%;\n height: 100%;\n min-height: 400px;\n background: #000;\n border-radius: 4px;\n }\n \n /* 电影信息样式 */\n .movie-title {\n font-size: 24px;\n margin-bottom: 15px;\n color: #333;\n }\n \n .movie-meta {\n margin-bottom: 20px;\n color: #666;\n }\n \n .movie-meta span {\n margin-right: 15px;\n }\n \n /* 演员列表 */\n .actor-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n }\n \n .actor-tag {\n padding: 4px 12px;\n background: #f0f0f0;\n border-radius: 15px;\n font-size: 12px;\n color: #555;\n }\n \n /* 图片列表 */\n .image-list {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-top: 10px;\n }\n \n .movie-image-thumb {\n width: 120px;\n height: 80px;\n object-fit: cover;\n border-radius: 4px;\n cursor: pointer;\n transition: transform 0.3s;\n }\n \n .movie-image-thumb:hover {\n transform: scale(1.05);\n }\n \n /* 加载中和错误状态 */\n .search-loading, .movie-error {\n padding: 40px;\n text-align: center;\n color: #999;\n }\n \n .movie-error {\n color: #f56c6c;\n }\n \n .fancybox-container{\n z-index:99999999\n }\n \n \n /* 错误提示样式 */\n .movie-not-found, .movie-error {\n text-align: center;\n padding: 30px;\n color: #666;\n }\n \n .movie-not-found h3, .movie-error h3 {\n color: #f56c6c;\n margin: 15px 0;\n }\n \n .icon-warning, .icon-error {\n font-size: 50px;\n color: #e6a23c;\n }\n \n .icon-error {\n color: #f56c6c;\n }\n \n .fc2-movie-panel-info .panel-block {\n padding: 0 !important;\n }\n </style>\n ";
}
handle() {
let fc2Url = "/advanced_search?type=3&score_min=0&d=1";
$('.navbar-item:contains("FC2")').attr("href", fc2Url);
$('.tabs a:contains("FC2")').attr("href", fc2Url);
if (currentHref.includes("advanced_search?type=3")) {
$("h2.section-title").contents().first().replaceWith("Fc2PPV");
$(".section .container > .box").remove();
}
if (currentHref.includes("collection_codes?movieId")) {
$("section").html("");
const urlParams = new URLSearchParams(window.location.search);
let movieId = urlParams.get("movieId"), carNum2 = urlParams.get("carNum"), url = urlParams.get("url");
movieId && carNum2 && url && this.openFc2Dialog(movieId, carNum2, url);
}
}
openFc2Dialog(movieId, carNum2, href) {
let tempCarNum = carNum2.replace("FC2-", "");
if (href.includes("123av")) {
this.getBean("Fc2By123AvPlugin").open123AvFc2Dialog(carNum2, href);
return;
}
layer.open({
type: 1,
title: carNum2,
content: '\n <div class="movie-detail-container">\n \x3c!--<div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>--\x3e\n \x3c!-- <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n <div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方资源: </strong></div>\n \n <div style="margin: 30px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn fr-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!--</div>--\x3e\n </div>\n ',
area: utils.getResponsiveArea([ "70%", "90%" ]),
skin: "movie-detail-layer",
scrollbar: !1,
success: (layero, index) => {
this.loadData(movieId, carNum2);
$("#favoriteBtn").on("click", (async event => {
const actress = $("#data-actress").text(), publishTime = $("#data-releaseDate").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_FAVORITE,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
}));
$("#filterBtn").on("click", (event => {
utils.q(event, `是否屏蔽${carNum2}?`, (async () => {
const actress = $("#data-actress").text(), publishTime = $("#data-releaseDate").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_FILTER,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
window.location.href.includes("collection_codes?movieId") && utils.closePage();
}));
}));
$("#hasDownBtn").on("click", (async event => {
const actress = $("#data-actress").text(), publishTime = $("#data-releaseDate").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_HAS_DOWN,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
}));
$("#hasWatchBtn").on("click", (async event => {
const actress = $("#data-actress").text(), publishTime = $("#data-releaseDate").text();
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: actress,
actionType: Status_HAS_WATCH,
publishTime: publishTime
});
window.refresh();
layer.closeAll();
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#magnetSearchBtn").on("click", (() => {
let magnetHub = this.getBean("MagnetHubPlugin").createMagnetHub(carNum2);
layer.open({
type: 1,
title: "磁力搜索",
content: '<div id="magnetHubBox"></div>',
area: utils.getResponsiveArea([ "60%", "80%" ]),
scrollbar: !1,
success: () => {
$("#magnetHubBox").append(magnetHub);
}
});
}));
this.getBean("OtherSitePlugin").loadOtherSite(tempCarNum, carNum2).then();
utils.setupEscClose(index);
},
end() {
window.location.href.includes("collection_codes?movieId") && utils.closePage();
}
});
}
loadData(movieId, carNum2) {
let tempCarNum = carNum2.replace("FC2-", "");
this.handleMovieDetail(movieId);
this.handleLongImg(tempCarNum);
this.handleMagnets(movieId);
this.getBean("ReviewPlugin").showReview(movieId, $("#reviews-content")).then();
this.getBean("RelatedPlugin").showRelated($("#related-content"), movieId).then();
}
handleMovieDetail(movieId) {
javDbApi.getMovieDetail(movieId).then((res => {
const actors = res.actors || [], imgList = res.imgList || [];
let actorsHtml = "";
if (actors.length > 0) {
let actress = "";
for (let i = 0; i < actors.length; i++) {
let actor = actors[i];
actorsHtml += `<span class="actor-tag"><a href="/actors/${actor.id}" target="_blank">${actor.name}</a></span>`;
0 === actor.gender && (actress += actor.name + " ");
}
$("#data-actress").text(actress);
} else actorsHtml = '<span class="no-data">暂无演员信息</span>';
let imagesHtml = "";
imagesHtml = Array.isArray(imgList) && imgList.length > 0 ? imgList.map(((img, index) => `\n <a href="${img}" data-fancybox="movie-gallery" data-caption="剧照 ${index + 1}">\n <img src="${img}" class="movie-image-thumb" alt=""/>\n </a>\n `)).join("") : '<div class="no-data">暂无剧照</div>';
$(".movie-info-container").html(`\n <h3 class="movie-title"><strong class="current-title">${res.title || "无标题"}</strong></h3>\n <div class="movie-meta">\n <span><strong>番号: </strong>${res.carNum || "未知"}</span>\n <span><strong>年份: </strong>${res.releaseDate || "未知"}</span>\n <span><strong>评分: </strong>${res.score || "无"}</span>\n <span><strong>时长: </strong>${res.duration + " m" || "无"}</span>\n </div>\n <div class="movie-meta">\n <span>\n <strong>站点: </strong>\n <a href="https://fc2ppvdb.com/articles/${res.carNum.replace("FC2-", "")}" target="_blank">fc2ppvdb</a>\n <a style="margin-left: 5px;" href="https://adult.contents.fc2.com/article/${res.carNum.replace("FC2-", "")}/" target="_blank">fc2电子市场</a>\n </span>\n </div>\n <div class="movie-actors">\n <div class="actor-list"><strong>主演: </strong>${actorsHtml}</div>\n </div>\n <div class="movie-gallery" style="margin-top:10px">\n <strong>剧照: </strong>\n <div class="image-list">${imagesHtml}</div>\n </div>\n <div id="data-releaseDate" style="display: none">${res.releaseDate || ""}</div>\n `);
this.getBean("TranslatePlugin").translate(res.carNum, !1).then();
})).catch((err => {
console.error(err);
$(".movie-info-container").html(`\n <div class="movie-error">加载失败: ${err.message}</div>\n `);
}));
}
handleLongImg(carNum2) {
utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => {
$(".movie-gallery .image-list").prepend(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 150px;max-width:150px; text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
const screenShotPlugin = this.getBean("ScreenShotPlugin"), imgUrl = await screenShotPlugin.getScreenshot(carNum2);
imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl);
}));
}
handleMagnets(movieId) {
javDbApi.getMagnets(movieId).then((magnetList => {
let magnetsHtml = "";
if (magnetList.length > 0) for (let i = 0; i < magnetList.length; i++) {
let magnet = magnetList[i], oddClass = "";
i % 2 == 0 && (oddClass = "odd");
magnetsHtml += `\n <div class="item columns is-desktop ${oddClass}">\n <div class="magnet-name column is-four-fifths">\n <a href="magnet:?xt=urn:btih:${magnet.hash}" title="右鍵點擊並選擇「複製鏈接地址」">\n <span class="name">${magnet.name}</span>\n <br>\n <span class="meta">\n ${(magnet.size / 1024).toFixed(2)}GB, ${magnet.files_count}個文件 \n </span>\n <br>\n <div class="tags">\n ${magnet.hd ? '<span class="tag is-primary is-small is-light">高清</span>' : ""}\n ${magnet.cnsub ? '<span class="tag is-warning is-small is-light">字幕</span>' : ""}\n </div>\n </a>\n </div>\n <div class="buttons column">\n <button class="button is-info is-small copy-to-clipboard" data-clipboard-text="magnet:?xt=urn:btih:${magnet.hash}" type="button">&nbsp;複製&nbsp;</button>\n </div>\n <div class="date column"><span class="time">${magnet.created_at}</span></div>\n </div>\n `;
} else magnetsHtml = '<span class="no-data">暂无磁力信息</span>';
$("#magnets-content").html(magnetsHtml);
$(".buttons button[data-clipboard-text*='magnet:']").each(((i, el) => {
$(el).parent().append($("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
try {
await this.getBean("WangPan115TaskPlugin").handleAddTask($(el).attr("data-clipboard-text"));
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
})));
}));
})).catch((err => {
console.error(err);
$("#magnets-content").html(`\n <div class="movie-error">加载失败: ${err.message}</div>\n `);
}));
}
async openFc2Page(movieId, carNum2, url) {
const otherSitePlugin = this.getBean("OtherSitePlugin");
let javDbUrl = await otherSitePlugin.getJavDbUrl();
window.open(`${javDbUrl}/users/collection_codes?movieId=${movieId}&carNum=${carNum2}&url=${url}`);
}
}
export { Fc2Plugin };
+375
View File
@@ -0,0 +1,375 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, YES } from '../core/constants.js';
class FilterTitleKeywordPlugin extends BasePlugin {
getName() {
return "FilterTitleKeywordPlugin";
}
async handle() {
if (!window.isDetailPage && !window.isFc2Page) return;
if (await storageManager.getSetting("enableTitleSelectFilter", YES) !== YES) return;
let targetSelector;
isJavDb ? targetSelector = ".title strong, .current-title" : isJavBus && (targetSelector = "h3");
utils.rightClick(document.body, targetSelector, (event => {
const selectedText = window.getSelection().toString();
if (selectedText) {
event.preventDefault();
let tempEvent = {
clientX: event.clientX,
clientY: event.clientY + 80
};
utils.q(tempEvent, `是否屏蔽标题关键词 ${selectedText}?`, (async () => {
await storageManager.saveTitleFilterKeyword(selectedText);
window.refresh();
utils.closePage();
}));
}
}));
}
}
const categoryMap = {
p: "可播放",
s: "單體作品",
d: "含磁鏈",
c: "含字幕",
1: "高中女生",
2: "其他戀物癖",
3: "眼鏡",
4: "制服外套",
5: "美少女",
6: "白人",
7: "處女",
8: "精選綜合",
9: "國外進口",
10: "4小時以上作品",
11: "戲劇",
12: "成人電影",
13: "性愛",
14: "乳交",
15: "熟女",
16: "婆婆",
17: "巨乳",
18: "中出",
19: "蘿莉塔",
20: "已婚婦女",
21: "凌辱",
22: "無毛",
23: "淫亂真實",
24: "多P",
25: "69",
26: "介紹影片",
27: "藝人",
28: "單體作品",
29: "捆綁",
30: "女大學生",
31: "緊縛",
32: "偶像",
33: "學校作品",
34: "妄想",
36: "淫語",
37: "女上位",
38: "自慰",
39: "跳蛋",
41: "輪姦",
42: "放尿",
43: "角色扮演",
44: "姐姐",
45: "第一人稱攝影",
46: "顏射",
47: "各種職業",
48: "蕩婦",
49: "內衣",
50: "玩具",
51: "出軌",
52: "強姦",
53: "監禁",
54: "亂倫",
55: "母親",
56: "溫泉",
57: "美臀",
58: "制服",
60: "SM",
61: "足交",
62: "高挑",
63: "M男",
64: "女同性戀",
65: "苗條",
66: "戶外",
67: "辣妹",
68: "潮吹",
69: "按摩",
70: "妓女",
71: "乳液",
72: "口交",
73: "舔陰",
74: "肛門・肛交",
75: "跳舞",
76: "按摩棒",
77: "手指插入",
78: "水手服",
79: "羞恥",
80: "首次亮相",
81: "吞精",
82: "明星臉",
83: "薄馬賽克",
84: "泳裝",
85: "數位馬賽克",
86: "企畫",
87: "投稿",
88: "業餘",
89: "裸體圍裙",
90: "女優按摩棒",
91: "美乳",
92: "孕婦",
93: "拘束",
94: "調教",
95: "和服,喪服",
96: "女忍者",
97: "紀錄片",
98: "巨大陰莖",
99: "身體意識",
100: "母乳",
101: "新娘,年輕妻子",
102: "手淫",
103: "戀物癖",
104: "胖女人",
105: "大小姐",
106: "連褲襪",
107: "戀腿癖",
108: "秘書",
109: "戀乳癖",
110: "濫交",
111: "惡作劇",
112: "獵豔",
113: "女傭",
114: "OL",
115: "護士",
116: "貓耳女",
117: "去背影片",
118: "角色扮演者",
120: "插入異物",
121: "獨立製作",
123: "深喉",
124: "接吻",
125: "短裙",
126: "學校泳裝",
127: "運動短褲",
128: "運動",
129: "迷你裙",
130: "倒追",
131: "大屁股",
132: "女同接吻",
133: "賽車女郎",
134: "瘦小身型",
135: "美容院",
136: "家教",
137: "黑人演員",
138: "灌腸",
139: "妹妹",
140: "寡婦",
141: "拳交",
142: "女醫生",
143: "浴衣",
144: "老闆娘,女主人",
145: "偷窺",
146: "平胸",
147: "變性者",
148: "主觀視角",
149: "奴隸",
150: "戰鬥行動",
151: "女戰士",
152: "特效",
153: "女主播",
154: "校服",
155: "藥物",
156: "猥褻穿著",
157: "白天出軌",
158: "女教師",
159: "露出",
160: "流汗",
161: "16小時以上作品",
162: "性騷擾",
163: "其他學生",
164: "故事集",
165: "洗澡",
166: "肌肉",
168: "局部特寫",
169: "模特兒",
170: "立即口交",
171: "汽車性愛",
172: "格鬥家",
173: "情侶",
175: "經典",
176: "催眠",
177: "緊身衣",
178: "戀愛",
179: "鴨嘴",
180: "糞便",
181: "娃娃",
182: "蘿莉角色扮演",
184: "爛醉如泥的",
185: "魔鬼系",
187: "處男",
188: "展場女孩",
189: "重印版",
190: "禮儀小姐",
191: "歷史劇",
192: "女裝人妖",
194: "寫真偶像",
195: "飲尿",
196: "殘忍畫面",
197: "騎乗位",
198: "絲襪、過膝襪",
199: "泡泡襪",
200: "性感的",
201: "女檢察官",
202: "排便",
203: "空中小姐",
204: "講師",
206: "曬黑",
207: "旗袍",
209: "食糞",
211: "雙性人",
212: "VR",
213: "感謝祭",
216: "兔女郎",
217: "超乳",
218: "全裸",
221: "正太控",
223: "女祭司",
224: "服務生",
225: "脫衣",
227: "3D",
230: "動畫人物",
232: "子宮頸",
233: "伴侶",
234: "觸手",
236: "給女性觀眾",
237: "車掌小姐",
238: "原作改編",
242: "訪問",
243: "正常",
244: "奇異的",
245: "女兒",
246: "年輕女孩",
248: "迷你裙警察",
249: "蠻橫嬌羞",
250: "教學",
253: "恐怖",
254: "西洋片",
258: "性轉換·女體化",
264: "公主",
265: "童年朋友",
266: "科幻",
267: "行動",
270: "修女",
271: "綜合短篇",
273: "滑稽模仿",
274: "飛特族",
277: "男同性戀",
280: "男性",
282: "亞洲女演員",
284: "冒險",
285: "模擬",
291: "韓國",
292: "愛好,文化",
293: "偷窺",
296: "懸疑",
297: "COSPLAY服飾",
300: "形象俱樂部",
305: "友誼",
311: "R-15",
312: "美少女電影",
314: "感官作品",
315: "導尿",
316: "亞洲",
318: "暗黑系",
323: "天賦",
325: "觸摸打字",
330: "素人作品",
335: "HDTV",
338: "素人",
339: "美腳",
340: "剃毛",
341: "痴漢",
342: "二穴同入",
343: "被外國人幹",
344: "刺青紋身",
345: "無碼流出",
346: "蒙面・面罩",
347: "4K",
348: "無碼破解",
349: "黑白配",
350: "唾液敷面",
351: "綜藝",
352: "高跟鞋",
353: "乳釘、穿孔、乳環",
354: "兩女一男",
355: "兩男兩女",
356: "兩男一女",
357: "口球",
358: "靴子",
360: "絕頂高潮",
361: "運動",
362: "純欲",
363: "御宅族",
364: "輔助自慰",
365: "老太婆",
366: "老年男性",
367: "心理驚悚",
368: "打屁股",
369: "夫妻交換",
370: "養尊處優",
371: "拉拉隊",
372: "假陽具",
373: "約會",
374: "不穿內褲",
375: "不穿胸罩",
376: "後入",
377: "媽媽的朋友",
378: "瑜伽·健身",
379: "白眼失神",
380: "鼻鉤",
381: "共演",
382: "蠟燭",
383: "養女",
384: "女王",
385: "搔癢",
386: "經歷告白",
387: "濕身",
388: "多毛",
389: "站立後入",
2001: "2001",
2002: "2002",
2003: "2003",
2004: "2004",
2005: "2005",
2006: "2006",
2007: "2007",
2008: "2008",
2009: "2009",
2010: "2010",
2011: "2011",
2012: "2012",
2013: "2013",
2014: "2014",
2015: "2015",
2016: "2016",
2017: "2017",
2018: "2018",
2019: "2019",
2020: "2020",
2021: "2021",
2022: "2022",
2023: "2023",
2024: "2024",
2025: "2025",
"lt-45": "45分鍾以內",
"45-90": "45-90分鍾",
"90-120": "90-120分鍾",
"gt-120": "120分鍾以上"
};
export { FilterTitleKeywordPlugin };
+85
View File
@@ -0,0 +1,85 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, currentHref, NO, YES } from '../core/constants.js';
class FoldCategoryPlugin extends BasePlugin {
getName() {
return "FoldCategoryPlugin";
}
async initCss() {
const settingObj = await storageManager.getSetting();
return `\n <style>\n #tags a.tag, .tags a.tag {\n position:relative;\n }\n .highlight-btn {\n position: absolute;\n top: -10px;\n right: -10px;\n background-color: #4CAF50;\n color: white;\n border: none;\n border-radius: 50%;\n width: 24px;\n height: 24px;\n font-size: 14px;\n line-height: 24px;\n text-align: center;\n cursor: pointer;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n display: none;\n z-index: 999;\n }\n /* 当父元素被高亮时,按钮变为其他颜色 */\n .highlighted .highlight-btn {\n background-color: #FF5722;\n }\n /* 高亮状态下的标签样式 */\n .highlighted {\n /* 浅黄色 */\n border: ${settingObj.highlightedTagNumber || 1}px solid ${settingObj.highlightedTagColor || "#ce2222"};\n }\n </style>\n `;
}
async handle() {
if (window.isListPage && !currentHref.includes("advanced_search")) {
this.highlightTag();
utils.loopDetector((() => $("#waitCheckBtn").length), (() => {
this.createFoldBtn();
}), 1, 1e4, !0);
$("#tags .tag-category .tag-expand").each(((index, el) => {
$(el).parent().hasClass("collapse") && el.click();
}));
}
}
highlightTag() {
(async () => {
const tags = await storageManager.getHighlightedTags();
tags && tags.forEach((text => {
$(`#tags a.tag:contains(${text})`).addClass("highlighted");
$(`.tags a.tag:contains(${text})`).addClass("highlighted");
}));
})().then();
$("#tags a.tag, .tags a.tag").hover((function() {
const $tag = $(this), button = $('<button class="highlight-btn" title="高亮显示">★</button>');
$tag.append(button);
button.fadeIn(0);
}), (function() {
$(this).find(".highlight-btn").fadeOut(0, (function() {
$(this).remove();
}));
}));
$(document).on("click", ".highlight-btn", (async function(e) {
e.stopPropagation();
e.preventDefault();
const $tag = $(this).closest("a.tag"), clonedTag = $tag.clone();
clonedTag.find(".highlight-btn").remove();
const tagText = clonedTag.text().trim().replace(/\s*\(\d+\)$/, "");
let highlightedTags = await storageManager.getHighlightedTags();
if (highlightedTags.includes(tagText)) {
highlightedTags = highlightedTags.filter((item => item !== tagText));
$tag.removeClass("highlighted");
} else {
highlightedTags.push(tagText);
$tag.addClass("highlighted");
}
await storageManager.setHighlightedTags(highlightedTags);
}));
}
async createFoldBtn() {
const foldCategoryHotKey = await storageManager.getSetting("foldCategoryHotKey");
let $subTags = $("#tags"), checkTagStr = $("#tags dl div.tag.is-info").map((function() {
return $(this).text().replaceAll("\n", "").replaceAll(" ", "");
})).get().join(" ");
if (!checkTagStr) return;
$(".tabs").append(`\n <div style="display: flex;align-items: center;flex-grow:1;justify-content: flex-end;">\n <div>已选分类: <span id="jhs-check-tag">${checkTagStr}</span></div>\n <a class="menu-btn main-tab-btn" id="foldCategoryBtn" style="background-color:#d23e60 !important;">\n <span></span>\n ${foldCategoryHotKey ? ` (${foldCategoryHotKey})` : ""}\n <i style="margin-left: 10px"></i>\n </a>\n\n </div>\n `);
let $section = $("h2.section-title");
if ($section.length > 0) {
$section.append('\n <div id="foldCategoryBtn">\n <a class="menu-btn" style="background-color:#d23e60 !important;margin-left: 20px;border-bottom:none !important;border-radius:3px;">\n <span></span>\n <i style="margin-left: 10px"></i>\n </a>\n </div>\n ');
$subTags = $("section > div > div.box");
}
if (!$subTags) return;
let $foldCategoryBtn = $("#foldCategoryBtn"), isFolded = localStorage.getItem("jhs_foldCategory") === YES, [newText, newIcon] = isFolded ? [ "展开", "icon-angle-double-down" ] : [ "折叠", "icon-angle-double-up" ];
$foldCategoryBtn.find("span").text(newText).end().find("i").attr("class", newIcon);
window.location.href.includes("noFold=1") || $subTags[isFolded ? "hide" : "show"]();
$foldCategoryBtn.on("click", (async event => {
event.preventDefault();
isFolded = !isFolded;
localStorage.setItem("jhs_foldCategory", isFolded ? YES : NO);
const [newText2, newIcon2] = isFolded ? [ "展开", "icon-angle-double-down" ] : [ "折叠", "icon-angle-double-up" ];
$foldCategoryBtn.find("span").text(newText2).end().find("i").attr("class", newIcon2);
$subTags[isFolded ? "hide" : "show"]();
}));
}
}
export { FoldCategoryPlugin };
+56
View File
@@ -0,0 +1,56 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb } from '../core/constants.js';
class HighlightMagnetPlugin extends BasePlugin {
getName() {
return "HighlightMagnetPlugin";
}
doFilterMagnet() {
this.handleDb();
this.handleBus();
}
handleDb() {
if (!isJavDb) return;
let magnetNameList = $("#magnets-content .name");
if (0 === magnetNameList.length) return;
const HIGH_QUALITY_KEYWORDS = [ "4k", "-c", "-u", "-uc" ];
let hasHighQuality = !1;
magnetNameList.each(((_, el) => {
const $el = $(el), text = $el.text().toLowerCase(), isHighQuality = HIGH_QUALITY_KEYWORDS.some((keyword => text.includes(keyword)));
$el.parent().parent().parent().addClass("magnet-row");
text.includes("4k") && $el.css("color", "#f40");
if (isHighQuality) {
hasHighQuality = !0;
$el.parent().parent().parent().addClass("high-quality");
}
}));
hasHighQuality ? $("#magnets-content .magnet-row").not(".high-quality").hide() : $("#enable-magnets-filter").addClass("do-hide");
}
handleBus() {
isJavBus && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => {
const rows = $("#magnet-table tr"), QUALITY_KEYWORDS = [ "4k", "-c", "-u", "-uc" ];
let hasHighQuality = !1;
rows.each(((_, row) => {
const $row = $(row), $firstTd = $row.find("td:first-child"), $firstLink = $firstTd.find("a:first-child"), $secondLink = $firstTd.find("a:nth-child(2)"), linkText = $firstLink.text().toLowerCase();
linkText.includes("4k") && $firstLink.css("color", "#f40");
if (QUALITY_KEYWORDS.some((keyword => linkText.includes(keyword))) || $secondLink.length && $secondLink.text().includes("字幕")) {
hasHighQuality = !0;
$row.addClass("high-quality");
}
}));
hasHighQuality ? rows.each(((_, row) => {
const $row = $(row);
$row.hasClass("high-quality") || $row.hide();
})) : $("#enable-magnets-filter").addClass("do-hide");
}));
}
showAll() {
if (isJavDb) {
$("#magnets-content .item").toArray().forEach((el => $(el).show()));
}
isJavBus && $("#magnet-table tr").toArray().forEach((el => $(el).show()));
}
}
export { HighlightMagnetPlugin };
+479
View File
@@ -0,0 +1,479 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH } from '../core/constants.js';
class HistoryPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "tableObj", null);
}
getName() {
return "HistoryPlugin";
}
async initCss() {
return "\n <style>\n /* 下拉菜单容器(相对定位) */\n .sub-btns {\n position: relative;\n display: inline-block;\n }\n \n /* 下拉菜单内容(默认隐藏) */\n .sub-btns-menu {\n display: none;\n position: absolute;\n right: 80px;\n top:-10px;\n background: white;\n padding:10px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n z-index: 100;\n border-radius: 4px;\n overflow: hidden;\n }\n \n \n /* 点击后显示菜单(JS 控制) */\n .sub-btns-menu.show {\n display: flex !important;\n flex-direction: column;\n }\n \n .table-link-param {\n cursor: pointer;\n }\n </style\n ";
}
handleResize() {
if ($(".navbar-search").is(":hidden")) {
$(".historyBtnBox").show();
$(".miniHistoryBtnBox").hide();
} else {
$(".historyBtnBox").hide();
$(".miniHistoryBtnBox").show();
}
}
handle() {
if (isJavDb) {
$(".navbar-end").prepend('<div class="navbar-item has-sub-btns is-hoverable historyBtnBox">\n <a id="historyBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-right:15px !important;">\n 鉴定记录\n </a>\n </div>');
$(".navbar-search").css("margin-left", "0").before('\n <div class="navbar-item miniHistoryBtnBox">\n <a id="miniHistoryBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-left:0 !important;padding-right:0 !important;">\n 鉴定记录\n </a>\n </div>\n ');
this.handleResize();
$(window).resize((() => {
this.handleResize();
}));
$("#historyBtn,#miniHistoryBtn").on("click", (event => this.openHistory()));
}
isJavBus && utils.loopDetector((() => $("#setting-btn").length), (() => {
$("#top-right-box").append('\n <a id="historyBtn" class="menu-btn main-tab-btn" style="background-color:#b68625 !important;">\n 鉴定记录\n </a>\n ');
$("#historyBtn,#miniHistoryBtn").on("click", (event => this.openHistory()));
}), 1, 1e4, !1);
this.bindClick();
}
openHistory() {
layer.open({
type: 1,
title: "鉴定记录",
content: '\n <div style="padding: 10px 20px; height: 100%;overflow:hidden;"> \n <div id="filterBox" style="display: flex;gap: 5px;">\n <select id="dataType" style="text-align: center;min-width: 150px;">\n <option value="all" selected>所有</option>\n <option value="filter">🚫 已屏蔽</option>\n <option value="favorite">⭐ 已收藏</option>\n <option value="hasDown">📥️ 已下载</option>\n <option value="hasWatch">🔍 已观看</option>\n </select>\n <input id="searchCarNum" type="text" placeholder="搜索番号|演员" style="padding: 4px 5px;">\n <a id="clearSearchbtn" class="a-info" style="margin-left: 0">重置</a>\n </div>\n <div id="allSelectBox" style="margin-top: 8px;display: none">\n <a class="menu-btn multiple-history-deleteBtn" style="background-color:#8c8080; color:white; margin-bottom: 5px;"> <span>✂️ 移除</span> </a>\n <a class="menu-btn multiple-history-hasWatchBtn" style="background-color:#d7a80c;margin-bottom: 5px">🔍 已观看</a>\n <a class="menu-btn multiple-history-hasDownBtn" style="background-color:#7bc73b;margin-bottom: 5px">📥️ 已下载</a>\n <a class="menu-btn multiple-history-favoriteBtn" style="background-color:#25b1dc;margin-bottom: 5px">⭐ 收藏</a>\n <a class="menu-btn multiple-history-filterBtn" style="background-color:#de3333;margin-bottom: 5px">🚫 屏蔽</a>\n </div>\n <div id="table-container" style="margin-top:20px !important; height: calc(100% - 50px); overflow-x:hidden;"></div>\n </div>\n ',
scrollbar: !1,
shadeClose: !0,
area: utils.getResponsiveArea([ "70%", "90%" ]),
anim: -1,
success: async layero => {
await this.loadTableData();
$(".layui-layer-content").on("click", "#clearSearchbtn", (async event => {
$("#searchCarNum").val("");
$("#dataType").val("all");
await this.reloadTable();
$("#allSelectBox").hide();
})).on("focusout keydown", "#searchCarNum", (async event => {
if ("focusout" === event.type || "Enter" === event.key) {
"Enter" === event.key && event.preventDefault();
if ("keydown" === event.type && "Enter" !== event.key) return;
await this.reloadTable();
}
})).on("click", ".table-link-param", (async event => {
let $targetEl = $(event.currentTarget);
$("#searchCarNum").val($targetEl.text());
await this.reloadTable();
})).on("change", "#dataType", (async () => {
await this.reloadTable();
}));
},
end: () => {
if (this.tableObj) {
this.tableObj.destroy();
this.tableObj = null;
}
window.refresh();
}
});
}
async reloadTable() {
this.tableObj.deselectRow();
this.tableObj.setPage(1);
}
bindClick() {
document.addEventListener("click", (function(e) {
if (e.target.closest(".sub-btns-toggle")) {
const menu = e.target.closest(".sub-btns").querySelector(".sub-btns-menu");
document.querySelectorAll(".sub-btns-menu.show").forEach((openMenu => {
openMenu !== menu && openMenu.classList.remove("show");
}));
menu.classList.toggle("show");
} else document.querySelectorAll(".sub-btns-menu.show").forEach((menu => {
menu.classList.remove("show");
}));
}));
$(document).on("click", ".history-deleteBtn, .history-filterBtn, .history-favoriteBtn, .history-hasDownBtn, .history-hasWatchBtn, .history-detailBtn", (event => {
event.preventDefault();
event.stopPropagation();
const $btn = $(event.currentTarget), $menuBox = $btn.closest(".action-btns"), carNum2 = $menuBox.attr("data-car-num"), aHref = $menuBox.attr("data-href"), handleAction = async status => {
await storageManager.saveCar({
carNum: carNum2,
url: aHref,
names: null,
actionType: status
});
window.refresh();
await this.reloadTable();
};
$btn.hasClass("history-filterBtn") ? utils.q(event, `是否屏蔽${carNum2}?`, (() => handleAction(Status_FILTER))) : $btn.hasClass("history-favoriteBtn") ? handleAction(Status_FAVORITE).then() : $btn.hasClass("history-hasDownBtn") ? handleAction(Status_HAS_DOWN).then() : $btn.hasClass("history-hasWatchBtn") ? handleAction(Status_HAS_WATCH).then() : $btn.hasClass("history-deleteBtn") ? this.handleDelete(event, carNum2) : $btn.hasClass("history-detailBtn") && this.handleClickDetail(event, {
carNum: carNum2,
url: aHref
}).then();
}));
$(document).on("click", ".multiple-history-deleteBtn, .multiple-history-filterBtn, .multiple-history-favoriteBtn, .multiple-history-hasDownBtn, .multiple-history-hasWatchBtn", (event => {
event.preventDefault();
event.stopPropagation();
const $btn = $(event.currentTarget);
let selectedRows = this.tableObj.getSelectedData(), handleText = "", handleStatus = "";
if ($btn.hasClass("multiple-history-filterBtn")) {
handleText = "屏蔽";
handleStatus = Status_FILTER;
} else if ($btn.hasClass("multiple-history-favoriteBtn")) {
handleText = "收藏";
handleStatus = Status_FAVORITE;
} else if ($btn.hasClass("multiple-history-hasDownBtn")) {
handleText = "已下载";
handleStatus = Status_HAS_DOWN;
} else if ($btn.hasClass("multiple-history-hasWatchBtn")) {
handleText = "已观看";
handleStatus = Status_HAS_WATCH;
} else if ($btn.hasClass("multiple-history-deleteBtn")) {
handleText = "移除";
handleStatus = "delete";
}
utils.q(event, `当前已勾选${selectedRows.length}条数据, 是否全标记为 ${handleText}?`, (async () => {
let loadObj = loading();
try {
if ("delete" === handleStatus) {
const deleteCarNumList = selectedRows.map((row => row.carNum)), removedCount = await storageManager.batchRemoveCars(deleteCarNumList);
removedCount > 0 ? show.ok(`已成功删除 ${removedCount} 个番号`) : !1 === removedCount && show.error("提供的番号中没有一个存在于列表中。");
} else {
const carRecords = JSON.parse(JSON.stringify(selectedRows));
carRecords.forEach((record => {
record.actionType = handleStatus;
}));
await storageManager.saveCarList(carRecords);
show.ok("操作成功");
}
this.tableObj.deselectRow();
this.reloadTable().then();
} catch (e) {
console.error(e);
} finally {
loadObj.close();
}
}));
}));
}
async getDataList(page, size, sort) {
let dataList = await storageManager.getCarList();
this.allCount = dataList.length;
this.filterCount = 0;
this.favoriteCount = 0;
this.hasDownCount = 0;
this.hasWatchCount = 0;
dataList.forEach((item => {
switch (item.status) {
case Status_FILTER:
this.filterCount++;
break;
case Status_FAVORITE:
this.favoriteCount++;
break;
case Status_HAS_DOWN:
this.hasDownCount++;
break;
case Status_HAS_WATCH:
this.hasWatchCount++;
}
}));
$('#dataType option[value="all"]').text(`所有 (${this.allCount})`);
$('#dataType option[value="filter"]').text(`🚫 已屏蔽 (${this.filterCount})`);
$('#dataType option[value="favorite"]').text(`⭐ 已收藏 (${this.favoriteCount})`);
$('#dataType option[value="hasDown"]').text(`📥️ 已下载 (${this.hasDownCount})`);
$('#dataType option[value="hasWatch"]').text(`🔍 已观看 (${this.hasWatchCount})`);
const dataType = $("#dataType").val();
let filterDataList = "all" === dataType ? dataList : dataList.filter((item => item.status === dataType));
const searchCarNum = $("#searchCarNum").val().trim();
if (searchCarNum) {
let tempCarNum = searchCarNum.toLowerCase().replace("-c", "").replace("-uc", "").replace("-4k", "");
filterDataList = filterDataList.filter((item => {
const result1 = item.carNum.toLowerCase().includes(tempCarNum);
const result2 = (item.names ? item.names : "").toLowerCase().includes(tempCarNum);
return result1 || result2;
}));
}
if (sort && sort.length > 0) {
const sorter = sort[0], field = sorter.field, dir = sorter.dir;
filterDataList.sort(((a, b) => {
const valA = a[field], valB = b[field], isValANullish = null == valA || "" === valA, isValBNullish = null == valB || "" === valB;
return isValANullish && !isValBNullish ? 1 : !isValANullish && isValBNullish ? -1 : isValANullish && isValBNullish ? 0 : valA < valB ? "asc" === dir ? -1 : 1 : valA > valB ? "asc" === dir ? 1 : -1 : 0;
}));
}
const totalCount = filterDataList.length, maxPage = Math.ceil(totalCount / size), startIndex = (page - 1) * size, endIndex = startIndex + size;
filterDataList = filterDataList.slice(startIndex, endIndex);
return {
maxPage: maxPage,
dataList: filterDataList,
totalCount: totalCount
};
}
async loadTableData() {
this.tableObj = new Tabulator("#table-container", {
layout: "fitColumns",
placeholder: "暂无数据",
virtualDom: !0,
pagination: !0,
paginationMode: "remote",
sortMode: "remote",
ajaxURL: "queryRealm",
dataLoader: !1,
ajaxRequestFunc: async (url, ajaxConfig, params) => {
const page = params.page, size = params.size, sort = params.sort;
return await this.getDataList(page, size, sort);
},
dataReceiveParams: {
last_page: "maxPage",
last_row: "totalCount",
data: "dataList"
},
paginationSize: 50,
paginationSizeSelector: [ 50, 100, 1e3, 99999 ],
paginationCounter: (pageSize, currentRow, currentPage, totalRows, totalPages) => `共 ${totalRows} 条记录`,
responsiveLayout: "collapse",
responsiveLayoutCollapse: !0,
columnDefaults: {
headerHozAlign: "center",
hozAlign: "center"
},
selectableRowsPersistence: !1,
index: "carNum",
columns: [ {
formatter: "rowSelection",
titleFormatter: "rowSelection",
hozAlign: "center",
headerSort: !1,
responsive: 0,
width: 40,
titleFormatterParams: {
rowRange: "active"
},
cellClick: (e, cell) => {
cell.getRow().toggleSelect();
}
}, {
title: "番号",
field: "carNum",
width: 120,
sorter: "string",
responsive: 0,
formatter: (cell, formatterParams, onRendered) => {
const carNumString = cell.getData().carNum, hyphenIndex = carNumString.indexOf("-");
if (-1 === hyphenIndex) return carNumString;
return `<a class="table-link-param">${carNumString.substring(0, hyphenIndex + 1)}</a>${carNumString.substring(hyphenIndex + 1)}`;
}
}, {
title: "演员",
field: "names",
minWidth: 200,
sorter: "string",
responsive: 5,
headerSort: !0,
formatter: (cell, formatterParams, onRendered) => (cell.getData().names || "").split(" ").filter((name2 => "" !== name2.trim())).map((name2 => `<a class="table-link-param">${name2}</a>`)).join(" ")
}, {
title: "创建时间",
field: "createDate",
width: 170,
sorter: "string",
responsive: 4
}, {
title: "修改时间",
field: "updateDate",
width: 170,
sorter: "string",
responsive: 4
}, {
title: "发行时间",
field: "publishTime",
width: 170,
sorter: "string",
responsive: 4
}, {
title: "来源",
field: "url",
width: 80,
sorter: "string",
responsive: 5,
hozAlign: "left",
formatter: (cell, formatterParams, onRendered) => {
let url = cell.getData().url;
return url ? url.includes("javdb") ? '<span style="color:#d34f9e">Javdb</span>' : url.includes("javbus") ? '<span style="color:#eaa813">JavBus</span>' : url.includes("123av") ? '<span style="color:#eaa813">123Av</span>' : `<span style="color:#050505">${url}</span>` : "";
}
}, {
title: "状态",
field: "status",
width: 100,
sorter: "string",
responsive: 1,
headerSort: !1,
formatter: (cell, formatterParams, onRendered) => {
const statusValue = cell.getData().status;
let color = "", text = "";
switch (statusValue) {
case "filter":
color = "#de3333";
text = "🚫 屏蔽";
break;
case "favorite":
color = "#25b1dc";
text = "⭐ 收藏";
break;
case "hasDown":
color = "#7bc73b";
text = "📥️ 已下载";
break;
case "hasWatch":
color = "#d7a80c";
text = "🔍 已观看";
break;
default:
text = statusValue;
}
return `<span style="color:${color}">${text}</span>`;
}
}, {
title: "备注",
field: "remark",
width: 100,
sorter: "string",
responsive: 6
}, {
title: "操作",
sorter: "string",
minWidth: 150,
cssClass: "action-cell-dropdown",
responsive: 0,
headerSort: !1,
formatter: (cell, formatterParams, onRendered) => {
const item = cell.getData();
onRendered((() => {
var _a2;
null == (_a2 = cell.getElement().querySelector(".history-editBtn")) || _a2.addEventListener("click", (e => {
this.editRecord(item);
}));
}));
return `\n <div class="action-btns" style="display: flex; gap: 5px;justify-content:center" data-car-num="${item.carNum}" data-href="${item.url ? item.url : ""}">\n <div class="sub-btns">\n <a class="menu-btn sub-btns-toggle" style="background-color:#c59d36; color:white; margin-bottom: 5px;">\n <span>✏️ 变更</span>\n </a>\n <div class="sub-btns-menu">\n <a class="menu-btn history-editBtn" style="background-color:#007bff; color:white; margin-bottom: 5px;"> <span>✏️ 编辑</span> </a>\n <a class="menu-btn history-deleteBtn" style="background-color:#8c8080; color:white; margin-bottom: 5px;"> <span>✂️ 移除</span> </a>\n <a class="menu-btn history-hasWatchBtn" style="background-color:#d7a80c;margin-bottom: 5px">🔍 已观看</a>\n <a class="menu-btn history-hasDownBtn" style="background-color:#7bc73b;margin-bottom: 5px">📥️ 已下载</a>\n <a class="menu-btn history-favoriteBtn" style="background-color:#25b1dc;margin-bottom: 5px">⭐ 收藏</a>\n <a class="menu-btn history-filterBtn" style="background-color:#de3333;margin-bottom: 5px">🚫 屏蔽</a>\n </div>\n </div>\n \n <a class="menu-btn history-detailBtn" style="background-color:#3397de; color:white; margin-bottom: 5px;"> <span>📄 详情页</span> </a>\n \n </div>\n `;
}
} ],
initialSort: [ {
column: "updateDate",
dir: "desc"
} ],
locale: "zh-cn",
langs: {
"zh-cn": {
pagination: {
first: "首页",
first_title: "首页",
last: "尾页",
last_title: "尾页",
prev: "上一页",
prev_title: "上一页",
next: "下一页",
next_title: "下一页",
all: "所有",
page_size: "每页行数"
}
}
}
});
this.tableObj.on("rowSelectionChanged", ((data, rows, selected, deselected) => {
const $allSelectBox = $("#allSelectBox"), $filterBox = $("#filterBox");
if (data && data.length > 0) {
$filterBox.hide();
$allSelectBox.show();
} else {
$filterBox.show();
$allSelectBox.hide();
}
}));
this.tableObj.on("rowDblClick", (function(e, row) {
row.toggleSelect();
}));
this.tableObj.on("tableBuilt", (async () => {}));
}
handleDelete(event, carNum2) {
utils.q(event, `是否移除${carNum2}?`, (async () => {
await storageManager.removeCar(carNum2);
this.reloadTable().then();
}));
}
async handleClickDetail(event, data) {
if (isJavDb) if (data.carNum.includes("FC2-")) {
const movieId = this.parseMovieId(data.url);
this.getBean("Fc2Plugin").openFc2Dialog(movieId, data.carNum, data.url);
} else {
if (!data.url) {
window.open("/search?q=" + data.carNum, "_blank");
return;
}
utils.openPage(data.url, data.carNum, !1, event);
}
if (isJavBus) {
let url = data.url;
if (url.includes("javdb")) if (data.carNum.includes("FC2-")) {
const movieId = this.parseMovieId(url);
await this.getBean("Fc2Plugin").openFc2Page(movieId, data.carNum, url);
} else window.open(url, "_blank"); else utils.openPage(data.url, data.carNum, !1, event);
}
}
async editRecord(item) {
const initialCarNum = item.carNum, initialNames = item.names || "", initialUrl = item.url || "", initialStatus = item.status, initialRemark = item.remark || "", textareaStyle = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; min-height: 60px; overflow-y: hidden;", inputStyle = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;", editFormHtml = `\n <div style="padding: 20px;">\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">番号:</label>\n <input type="text" id="edit-carNum" value="${initialCarNum}" style="${inputStyle} background-color: #f0f0f0;" readonly>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">演员 (用空格隔开):</label>\n <textarea id="edit-names" style="${textareaStyle}">${initialNames}</textarea>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">状态:</label>\n <select id="edit-status" style="width: 100%; padding: 10px; border: 1px solid #ddd;">\n <option value="" ${"" === initialStatus ? "selected" : ""}>-- 请选择 --</option>\n ${[ {
value: Status_FILTER,
text: "🚫 屏蔽"
}, {
value: Status_FAVORITE,
text: "⭐ 收藏"
}, {
value: Status_HAS_DOWN,
text: "📥️ 已下载"
}, {
value: Status_HAS_WATCH,
text: "🔍 已观看"
} ].map((option => `\n <option value="${option.value}" ${initialStatus === option.value ? "selected" : ""}>${option.text}</option>\n `)).join("")}\n </select>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">链接:</label>\n <input type="text" id="edit-url" value="${initialUrl}" style="${inputStyle}">\n </div>\n \n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">备注:</label>\n <textarea id="edit-remark" style="${textareaStyle}">${initialRemark}</textarea>\n </div>\n </div>\n `;
layer.open({
type: 1,
title: `编辑记录: ${initialCarNum}`,
area: [ "500px", "650px" ],
content: editFormHtml,
btn: [ "保存", "取消" ],
success: (layero, index) => {
const autoResizeTextarea = $textarea => {
$textarea.css("height", "auto");
$textarea.css("height", $textarea[0].scrollHeight + 15 + "px");
}, $namesTextarea = $("#edit-names");
$namesTextarea.on("input", (function() {
autoResizeTextarea($(this));
}));
autoResizeTextarea($namesTextarea);
const $remarkTextarea = $("#edit-remark");
$remarkTextarea.on("input", (function() {
autoResizeTextarea($(this));
}));
autoResizeTextarea($remarkTextarea);
},
yes: async index => {
const newNames = $("#edit-names").val().trim(), newStatus = $("#edit-status").val(), newUrl = $("#edit-url").val().trim(), newRemark = $("#edit-remark").val().trim(), updatedRecord = {
...item,
names: newNames,
actionType: newStatus,
url: newUrl,
remark: newRemark
};
await storageManager.updateCarInfo(updatedRecord);
this.tableObj.setData();
layer.close(index);
}
});
}
}
export { HistoryPlugin };
+93
View File
@@ -0,0 +1,93 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
import { javDbApi } from '../api/javdb.js';
class HitShowPlugin extends BasePlugin {
constructor() {
super();
__publicField(this, "$contentBox", $(".section .container"));
}
getName() {
return "HitShowPlugin";
}
handle() {
$('a[href*="rankings/playback"]').on("click", (event => {
event.preventDefault();
event.stopPropagation();
window.location.href = "/advanced_search?handlePlayback=1&period=daily";
}));
this.handlePlayback().then();
}
hookPage() {
let $h2 = $("h2.section-title");
$h2.contents().first().replaceWith("热播");
$h2.css("marginBottom", "0");
$(".empty-message").remove();
$(".section .container .box").remove();
$("#sort-toggle-btn").remove();
this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>');
this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>');
}
async handlePlayback() {
if (!window.location.href.includes("handlePlayback=1")) return;
let period = new URLSearchParams(window.location.search).get("period");
this.toolBar(period);
this.hookPage();
let $movieBox = $(".movie-list");
$movieBox.html("");
let loadObj = loading();
try {
const movies = await javDbApi.playback(period);
let moviesHtml = javDbApi.markDataListHtml(movies);
$movieBox.html(moviesHtml);
} catch (e) {
clog.error("发生错误:", e);
} finally {
loadObj.close();
}
}
toolBar(period) {
let conditionHtml = `\n <div class="button-group" style="margin-top:18px">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small ${"daily" === period ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=daily">日榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"weekly" === period ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=weekly">周榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"monthly" === period ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=monthly">月榜</a>\n </div>\n </div>\n `;
this.$contentBox.append(conditionHtml);
}
getStarRating(score) {
let stars = "";
const fullStars = Math.floor(score);
for (let i = 0; i < fullStars; i++) stars += '<i class="icon-star"></i>';
for (let i = 0; i < 5 - fullStars; i++) stars += '<i class="icon-star gray"></i>';
return stars;
}
loadScore(movies) {
if (0 === movies.length) return;
(async () => {
for (const movie of movies) try {
const movieId = movie.id;
if (!$(`#score_${movieId}`).length) return;
if ($(`#${movieId}`).is(":hidden")) continue;
const cacheData = localStorage.getItem("jhs_score_info") ? JSON.parse(localStorage.getItem("jhs_score_info")) : {}, cached = cacheData[movieId];
if (cached) {
this.appendScoreHtml(movieId, cached);
continue;
}
for (;!document.hasFocus(); ) await new Promise((r => setTimeout(r, 500)));
const res = await javDbApi.getMovieDetail(movieId);
let score = res.score, watchedCount = res.watchedCount, html = `\n <span class="value">\n <span class="score-stars">${this.getStarRating(score)}</span> \n &nbsp; ${score}分,由${watchedCount}人評價\n </span>\n `;
this.appendScoreHtml(movieId, html);
cacheData[movieId] = html;
localStorage.setItem("jhs_score_info", JSON.stringify(cacheData));
await new Promise((r => setTimeout(r, 500)));
} catch (err) {
clog.error(`🚨 解析评分数据失败 | 编号: ${movie.number}\n`, `错误详情: ${err.message}\n`, err.stack ? `调用栈:\n${err.stack}` : "");
}
})();
}
appendScoreHtml(movieId, scoreHtml) {
let $scoreBox = $(`#score_${movieId}`);
$scoreBox.length && "" === $scoreBox.html().trim() && $scoreBox.slideUp(0, (function() {
$(this).html(scoreHtml).slideDown(500);
}));
}
}
export { HitShowPlugin };
+192
View File
@@ -0,0 +1,192 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class ImageRecognitionPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "siteList", [ {
name: "Google旧版",
url: "https://www.google.com/searchbyimage?image_url={占位符}&client=firefox-b-d",
ico: "https://www.google.com/favicon.ico"
}, {
name: "Google",
url: "https://lens.google.com/uploadbyurl?url={占位符}",
ico: "https://www.google.com/favicon.ico"
}, {
name: "Yandex",
url: "https://yandex.ru/images/search?rpt=imageview&url={占位符}",
ico: "https://yandex.ru/favicon.ico"
} ]);
__publicField(this, "isUploading", !1);
}
getName() {
return "ImageRecognitionPlugin";
}
async initCss() {
return "\n <style>\n #upload-area {\n border: 2px dashed #85af68;\n border-radius: 8px;\n padding: 40px;\n text-align: center;\n margin-bottom: 20px;\n transition: all 0.3s;\n background-color: #f9f9f9;\n }\n #upload-area:hover {\n border-color: #76b947;\n background-color: #f0f0f0;\n }\n /* 拖拽进入 */\n #upload-area.highlight {\n border-color: #2196F3;\n background-color: #e3f2fd;\n }\n \n \n #select-image-btn {\n background-color: #4CAF50;\n color: white;\n border: none;\n padding: 10px 20px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 16px;\n transition: background-color 0.3s;\n }\n #select-image-btn:hover {\n background-color: #45a049;\n }\n \n \n #handle-btn, #cancel-btn {\n padding: 8px 16px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 14px;\n border: none;\n transition: opacity 0.3s;\n }\n #handle-btn {\n background-color: #2196F3;\n color: white;\n }\n #handle-btn:hover {\n opacity: 0.9;\n }\n #cancel-btn {\n background-color: #f44336;\n color: white;\n }\n #cancel-btn:hover {\n opacity: 0.9;\n }\n \n .search-img-site-btns-container {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-top: 15px;\n }\n .search-img-site-btn {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n background-color: #f5f5f5;\n border-radius: 4px;\n text-decoration: none;\n color: #333;\n transition: all 0.2s;\n font-size: 14px;\n border: 1px solid #ddd;\n }\n .search-img-site-btn:hover {\n background-color: #e0e0e0;\n transform: translateY(-2px);\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n .search-img-site-btn img {\n width: 16px;\n height: 16px;\n margin-right: 6px;\n }\n .search-img-site-btn span {\n white-space: nowrap;\n }\n </style>\n ";
}
open(onOpenFun) {
layer.open({
type: 1,
title: "以图识图",
content: '\n <div style="padding: 20px">\n <div id="upload-area">\n <div style="color: #555;margin-bottom: 15px;">\n <p>拖拽图片到此处 或 点击按钮选择图片</p>\n <p>也可以直接 Ctrl+V 粘贴图片或 图片URL</p>\n </div>\n <button id="select-image-btn">选择图片</button>\n <input type="file" style="display: none" id="image-file" accept="image/*">\n </div>\n \n <div id="url-input-container" style="margin-top: 15px;display: none;">\n <input type="text" id="image-url" placeholder="粘贴图片URL地址..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">\n </div>\n \n <div id="preview-area" style="margin-bottom: 20px; text-align: center; display: none;">\n <img id="preview-image" alt="" src="" style="max-width: 100%; max-height: 300px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">\n <div style="margin-top: 15px; display: flex; justify-content: center; gap: 10px;" id="action-btns">\n <button id="handle-btn">搜索图片</button>\n <button id="cancel-btn">取消</button>\n </div>\n \n <div id="search-results" style="display: none;">\n <p style="margin: 20px auto">请选择识图网站:<a id="openAll" style="cursor: pointer">全部打开</a></p>\n <div class="search-img-site-btns-container" id="search-img-site-btns-container"></div>\n </div>\n </div>\n \n </div>\n ',
area: utils.isMobile() ? utils.getResponsiveArea() : [ "40%", "80%" ],
success: async layero => {
this.initEventListeners();
onOpenFun && onOpenFun();
},
end: () => {
$(document).off("paste.searchImg");
}
});
}
initEventListeners() {
const $uploadArea = $("#upload-area"), $fileInput = $("#image-file"), $selectBtn = $("#select-image-btn"), $previewArea = $("#preview-area"), $previewImage = $("#preview-image"), $actionBtns = $("#action-btns"), $searchImage = $("#handle-btn"), $cancelBtn = $("#cancel-btn"), $urlInputContainer = $("#url-input-container"), $imageUrlInput = $("#image-url"), $searchResults = $("#search-results"), $siteBtnsContainer = $("#search-img-site-btns-container");
$uploadArea.on("dragover", (e => {
e.preventDefault();
$uploadArea.addClass("highlight");
})).on("dragleave", (() => {
$uploadArea.removeClass("highlight");
})).on("drop", (e => {
e.preventDefault();
$uploadArea.removeClass("highlight");
if (e.originalEvent.dataTransfer.files && e.originalEvent.dataTransfer.files[0]) {
this.handleImageFile(e.originalEvent.dataTransfer.files[0]);
this.resetSearchUI();
}
}));
$selectBtn.on("click", (() => {
$fileInput.trigger("click");
}));
$fileInput.on("change", (e => {
if (e.target.files && e.target.files[0]) {
this.handleImageFile(e.target.files[0]);
this.resetSearchUI();
}
}));
$(document).on("paste.searchImg", (async e => {
const items = e.originalEvent.clipboardData.items;
for (let i = 0; i < items.length; i++) if (-1 !== items[i].type.indexOf("image")) {
const blob = items[i].getAsFile();
this.handleImageFile(blob);
this.resetSearchUI();
return;
}
const text = e.originalEvent.clipboardData.getData("text");
if (text && utils.isUrl(text)) {
$urlInputContainer.show();
$imageUrlInput.val(text);
$previewImage.attr("src", text);
$previewArea.show();
this.resetSearchUI();
}
}));
$searchImage.on("click", (async () => {
const imageSrc = $previewImage.attr("src");
if (imageSrc) {
if (!this.isUploading) {
this.isUploading = !0;
try {
const imgUrl = await this.searchByImage(imageSrc);
$actionBtns.hide();
$searchResults.show();
$siteBtnsContainer.empty();
const key = "jhs_selectedSites", selectedSites = JSON.parse(localStorage.getItem(key) || "{}");
this.siteList.forEach((site => {
const siteUrl = site.url.replace("{占位符}", encodeURIComponent(imgUrl)), isChecked = !1 !== selectedSites[site.name];
$siteBtnsContainer.append(`\n <a href="${siteUrl}" class="search-img-site-btn" target="_blank" title="${site.name}">\n <input type="checkbox" \n class="site-checkbox" \n data-site-name="${site.name}" \n style="margin-right: 5px"\n ${isChecked ? "checked" : ""}>\n <img src="${site.ico}" alt="${site.name}">\n <span>${site.name}</span>\n </a>\n `);
}));
$siteBtnsContainer.on("change", ".site-checkbox", (function() {
const siteName = $(this).data("site-name");
selectedSites[siteName] = $(this).is(":checked");
localStorage.setItem(key, JSON.stringify(selectedSites));
}));
$siteBtnsContainer.show();
} finally {
this.isUploading = !1;
}
}
} else show.info("请粘贴或上传图片");
}));
$cancelBtn.on("click", (() => {
$previewArea.hide();
$urlInputContainer.hide();
$fileInput.val("");
$imageUrlInput.val("");
}));
$imageUrlInput.on("change", (() => {
if (utils.isUrl($imageUrlInput.val())) {
$previewImage.attr("src", $imageUrlInput.val());
$previewArea.show();
}
}));
$("#openAll").on("click", (() => {
$(".search-img-site-btn").each((function() {
$(this).find(".site-checkbox").is(":checked") && window.open($(this).attr("href"));
}));
}));
}
resetSearchUI() {
$("#action-btns").show();
$("#search-results").hide();
$("#search-img-site-btns-container").hide().empty();
}
handleImageFile(file) {
const previewImage = document.getElementById("preview-image"), previewArea = document.getElementById("preview-area"), urlInputContainer = document.getElementById("url-input-container");
if (!file.type.match("image.*")) {
show.info("请选择图片文件");
return;
}
const reader = new FileReader;
reader.onload = e => {
previewImage.src = e.target.result;
previewArea.style.display = "block";
urlInputContainer.style.display = "none";
$("#handle-btn")[0].click();
};
reader.readAsDataURL(file);
}
async searchByImage(imageSrc) {
let loadObj = loading();
try {
let imageUrl = imageSrc;
if (imageSrc.startsWith("data:")) {
show.info("开始上传图片...");
const imgurUrl = await async function(base64Data) {
var _a2;
const matches = base64Data.match(/^data:(.+);base64,(.+)$/);
if (!matches || matches.length < 3) throw new Error("无效的Base64图片数据");
const mimeType = matches[1], imageData = matches[2], byteCharacters = atob(imageData), byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) byteNumbers[i] = byteCharacters.charCodeAt(i);
const byteArray = new Uint8Array(byteNumbers), blob = new Blob([ byteArray ], {
type: mimeType
}), formData = new FormData;
formData.append("image", blob);
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID d70305e7c3ac5c6"
},
body: formData
}), data = await response.json();
if (data.success && data.data && data.data.link) return data.data.link;
throw new Error((null == (_a2 = data.data) ? void 0 : _a2.error) || "上传到Imgur失败");
}(imageSrc);
if (!imgurUrl) {
show.error("上传到失败");
return;
}
imageUrl = imgurUrl;
}
return imageUrl;
} catch (error) {
show.error(`搜索失败: ${error.message}`);
console.error("搜索失败:", error);
} finally {
loadObj.close();
}
}
}
export { ImageRecognitionPlugin };
+88
View File
@@ -0,0 +1,88 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
import { HotkeyManager } from './preview-video.js';
class JavTrailersPlugin extends BasePlugin {
getName() {
return "JavTrailersPlugin";
}
constructor() {
super();
this.hasBand = !1;
}
handle() {
let href = window.location.href;
if (!href.includes("handle=1")) return;
if ($("h1:contains('Page not found')").length) {
console.log("番号无法匹配, 跳搜索");
let keyword = href.split("?")[0].split("video/")[1].toLowerCase().replace("00", "-");
window.location.href = "/search/" + encodeURIComponent(keyword) + window.location.search;
return;
}
let findList = $(".videos-list .video-link").toArray();
if (findList.length) {
const keyword = href.split("?")[0].split("search/")[1].toLowerCase(), matchedLink = findList.find((el => $(el).find(".vid-title").text().toLowerCase().includes(keyword)));
if (matchedLink) {
window.location.href = $(matchedLink).attr("href") + window.location.search;
return;
}
}
this.handlePlayJavTrailers();
$("#videoPlayerContainer").on("click", (() => {
this.handlePlayJavTrailers();
}));
window.addEventListener("message", (event => {
let videoEl = document.getElementById("vjs_video_3_html5_api");
videoEl && (videoEl.currentTime += 5);
}));
const urlParams = new URLSearchParams(window.location.search), filterHotKey = urlParams.get("filterHotKey"), favoriteHotKey = urlParams.get("favoriteHotKey"), speedVideoHotKey = urlParams.get("speedVideoHotKey");
filterHotKey && HotkeyManager.registerHotkey(filterHotKey, (() => window.parent.postMessage(filterHotKey, "*")));
favoriteHotKey && HotkeyManager.registerHotkey(favoriteHotKey, (() => window.parent.postMessage(favoriteHotKey, "*")));
speedVideoHotKey && HotkeyManager.registerHotkey(speedVideoHotKey, (() => {
const videoEl = document.getElementById("vjs_video_3_html5_api");
videoEl && (videoEl.currentTime += 5);
}));
}
handlePlayJavTrailers() {
if (!this.hasBand) {
utils.loopDetector((() => 0 !== $("#vjs_video_3_html5_api").length), (() => {
setTimeout((() => {
this.hasBand = !0;
let videoEl = document.getElementById("vjs_video_3_html5_api");
console.log(videoEl);
videoEl.play();
videoEl.currentTime = 5;
videoEl.addEventListener("timeupdate", (function() {
videoEl.currentTime >= 14 && videoEl.currentTime < 16 && (videoEl.currentTime += 2);
}));
$("#vjs_video_3_html5_api").css({
position: "fixed",
width: "100vw",
height: "100vh",
objectFit: "cover",
zIndex: "999999999"
});
$(".vjs-control-bar").css({
position: "fixed",
bottom: "20px",
zIndex: "999999999"
});
}), 100);
}));
utils.loopDetector((() => $("#vjs_video_3 canvas").length > 0), (() => {
0 !== $("#vjs_video_3 canvas").length && $("#vjs_video_3 canvas").css({
position: "fixed",
width: "100vw",
height: "100vh",
objectFit: "cover",
top: "0",
right: "0",
zIndex: "999999998"
});
}));
}
}
}
export { JavTrailersPlugin };
+283
View File
@@ -0,0 +1,283 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, isSearchPage, currentHref, Status_FAVORITE, NO, YES } from '../core/constants.js';
class ListPageButtonPlugin extends BasePlugin {
getName() {
return "ListPageButtonPlugin";
}
async handle() {
if (!window.isListPage) return;
await this.createMenuBtn();
this.bindEvent();
await storageManager.getSetting("autoPage") === YES ? $("#sort-toggle-btn").hide() : this.sortItems().then();
}
async createMenuBtn() {
const showWaitCheckBtn = await storageManager.getSetting("showWaitCheckBtn", YES), showWaitDownBtn = await storageManager.getSetting("showWaitDownBtn", YES);
if (isJavDb) {
const isStarPage = currentHref.includes("/actors/");
let $el = $(".main-tabs, .tabs"), addBlacklistBtnText = "加入黑名单", addBlacklistBtnColor = "#d22020", otherCss = "", blacklistItem = null;
if (isStarPage) {
$el = $(".toolbar, .section-addition").filter(":last");
const blacklist = await storageManager.getBlacklist(), actressPageInfo = this.getActressPageInfo();
if (blacklist.find((item => item.starId === actressPageInfo.starId))) {
addBlacklistBtnText = "已加入黑名单";
addBlacklistBtnColor = "#885d5d";
}
} else currentHref.includes("/tags") && utils.loopDetector((() => $("#jhs-check-tag").text().trim()), (async () => {
const $addBlacklistBtn = $("#addBlacklistBtn");
$addBlacklistBtn.attr("data-tip", "将当前分类标签加入到黑名单, 后续有作品更新也会纳入屏蔽中");
const checkTag = $("#jhs-check-tag").text().trim();
if (!checkTag) return;
const tagStarId = "no-" + checkTag, blacklist = await storageManager.getBlacklist();
blacklistItem = blacklist.find((item => item.starId === tagStarId));
if (blacklistItem) {
$addBlacklistBtn.css("backgroundColor", "#885d5d");
$("#addBlacklistBtn span").text("已加入黑名单");
}
}));
const isFc2Page2 = currentHref.includes("advanced_search");
isFc2Page2 ? $el = $("h2.section-title") : otherCss = "flex-grow:1;";
const jhs_sortMethod = localStorage.getItem("jhs_sortMethod"), sortText = "当前排序方式: " + ("rateCount" === jhs_sortMethod ? "评价人数" : "date" === jhs_sortMethod ? "时间" : "默认");
$el.append(`\n <div style="display: flex;align-items: center; ${otherCss} ">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" data-tip="打开未鉴定的列表项, 并自动播放视频" style="background-color:#56c938 !important; ${showWaitCheckBtn === NO ? "display: none" : ""}"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important; ${showWaitDownBtn === NO ? "display: none" : ""}"><span>打开已收藏</span></a>\n ${isStarPage ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;margin-right: 30px!important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n ` : ""}\n ${currentHref.includes("/tags") ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n ` : ""}\n </div>\n <div style="display: flex;align-items: center;">\n <a id="newVideoBtn" class="menu-btn main-tab-btn" style="background-color:#2c6cc0 !important;"><span>新作品检测 (<span id="newVideoCount">0</span>)</span></a>\n <a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>\n ${isSearchPage || isFc2Page2 ? "" : `<a id="sort-toggle-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;"> ${sortText} </a>`}\n </div>\n `);
}
if (isJavBus) {
const isStarPage = currentHref.includes("/star/");
let addBlacklistBtnText = "加入黑名单", addBlacklistBtnColor = "#d22020";
if (isStarPage) {
const blacklist = await storageManager.getBlacklist(), actressPageInfo = this.getActressPageInfo();
if (blacklist.find((item => item.starId === actressPageInfo.starId))) {
addBlacklistBtnText = "已加入黑名单";
addBlacklistBtnColor = "#885d5d";
}
}
$(".masonry").parent().prepend(`\n <div style="margin: 10px; display: flex;">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" style="background-color:#56c938 !important; ${showWaitCheckBtn === NO ? "display: none" : ""}"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important; ${showWaitDownBtn === NO ? "display: none" : ""}"><span>打开已收藏</span></a>\n \n ${isStarPage ? ` \n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n ` : '<a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>'}\n </div>\n `);
}
const newVideoPlugin = window.pluginManager.getBean("NewVideoPlugin");
newVideoPlugin && newVideoPlugin.showNewVideoCount().then();
}
bindEvent() {
$("#waitCheckBtn").on("click", (event => {
this.openWaitCheck(event).then();
}));
$("#waitDownBtn").on("click", (event => {
this.openFavorite(event).then();
}));
$("#newVideoBtn").on("click", (event => {
this.getBean("NewVideoPlugin").openDialog();
}));
$("#blacklistBtn").on("click", (event => {
this.getBean("BlacklistPlugin").openBlacklistDialog();
}));
$("#sort-toggle-btn").on("click", (event => {
const currentMethod = localStorage.getItem("jhs_sortMethod") || "default";
// 1. 定义排序状态的循环顺序
const sortCycle = ["default", "rateCount", "totalScore", "date"];
let nextIndex = sortCycle.indexOf(currentMethod) + 1;
if (nextIndex >= sortCycle.length || nextIndex < 0) nextIndex = 0;
const newMethod = sortCycle[nextIndex];
// 2. 映射对应的文本
const methodText = {
default: "默认",
rateCount: "评价人数",
totalScore: "总分",
date: "时间"
}[newMethod];
$(event.target).text(`当前排序方式: ${methodText}`);
localStorage.setItem("jhs_sortMethod", newMethod);
this.sortItems().then();
}));
const blacklistPlugin = this.getBean("BlacklistPlugin");
$("#addBlacklistBtn").on("click", (async event => {
await blacklistPlugin.addBlacklist(event);
}));
$("#filterAllVideo").on("click", (async event => {
let tempEvent = {
clientX: event.clientX,
clientY: event.clientY + 80
}, $actor = isJavDb ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10");
if (0 === $actor.length) {
show.error("获取演员名称失败");
return;
}
let actorName = $actor.text().trim().split(",")[0];
utils.q(tempEvent, "一键屏蔽已选分类的视频列表至鉴定记录中?", (async () => {
this.loadObj = loading();
try {
await blacklistPlugin.filterAllVideo(actorName);
window.refresh();
} catch (e) {
console.error(e);
} finally {
this.loadObj.close();
}
}));
}));
}
async sortItems() {
if (currentHref.includes("handle") || currentHref.includes("advanced_search")) return;
const autoPage = await storageManager.getSetting("autoPage");
if (isSearchPage || autoPage === YES) return;
const method = localStorage.getItem("jhs_sortMethod");
if (!method) return;
$(".movie-list .item").each((function(index) {
$(this).attr("data-original-index") || $(this).attr("data-original-index", index);
}));
const $container = $(".movie-list"), $items = $(".item", $container);
if ("default" === method) {
$items.sort((function(a, b) {
return $(a).data("original-index") - $(b).data("original-index");
})).appendTo($container);
} else {
const items = $items.get();
items.sort((function(a, b) {
// 按评价人数排序
if ("rateCount" === method) {
const getScore = el => {
const match = $(el).find(".score .value").text().match(/由(\d+)人/);
return match ? parseFloat(match[1]) : 0;
};
return getScore(b) - getScore(a);
}
// 新增:按总分排序 (平均分 * 评价人数)
else if ("totalScore" === method) {
const getTotalScore = el => {
const text = $(el).find(".score .value").text();
// 分别提取分数和人数,增加容错率
const scoreMatch = text.match(/([\d.]+)分/);
const countMatch = text.match(/由(\d+)人/);
const score = scoreMatch ? parseFloat(scoreMatch[1]) : 0;
const count = countMatch ? parseFloat(countMatch[1]) : 0;
return score * count;
};
return getTotalScore(b) - getTotalScore(a);
}
// 按时间排序
else {
const getDate = el => {
const dateStr = $(el).find(".meta").text().trim();
return new Date(dateStr);
};
return getDate(b) - getDate(a);
}
}));
$container.empty().append(items);
}
}
async openWaitCheck(event) {
const maxCount = await storageManager.getSetting("waitCheckCount", 5);
let count = 0;
$(`${this.getSelector().itemSelector}:visible`).each(((i, el) => {
if (count >= maxCount) return !1;
const $box2 = $(el);
if ($box2.find(".status-tag").length > 0) return;
const {carNum: carNum2, aHref: aHref} = this.getBoxCarInfo($box2);
if (carNum2.includes("FC2-")) {
const movieId = this.parseMovieId(aHref);
this.getBean("Fc2Plugin").openFc2Page(movieId, carNum2, aHref);
} else {
let url = aHref + (aHref.includes("?") ? "&autoPlay=1" : "?autoPlay=1");
window.open(url);
}
count++;
}));
0 === count && show.info("没有需鉴定的视频");
}
async openFavorite() {
let favoriteList, openCount = await storageManager.getSetting("waitCheckCount", 5), randomOpenWaitDown = await storageManager.getSetting("randomOpenWaitDown", NO), dataList = await storageManager.getCarList();
favoriteList = randomOpenWaitDown === YES ? dataList.filter((item => item.status === Status_FAVORITE)).sort((() => Math.random() - .5)) : dataList.filter((item => item.status === Status_FAVORITE)).sort(((a, b) => b.createDate - a.createDate));
for (let i = 0; i < openCount; i++) {
if (i >= favoriteList.length) return;
let data = favoriteList[i], carNum2 = data.carNum, url = data.url;
if (carNum2.includes("FC2-")) {
const movieId = this.parseMovieId(url);
await this.getBean("Fc2Plugin").openFc2Page(movieId, carNum2, url);
} else window.open(url);
clog.debug("打开已收藏", carNum2, url);
}
}
}
export const translateText = async (text, sourceLang = "ja", targetLang = "zh-CN") => {
if (!text) throw new Error("翻译文本不能为空");
const url = "https://translate-pa.googleapis.com/v1/translate?" + new URLSearchParams({
"params.client": "gtx",
dataTypes: "TRANSLATION",
key: "AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA",
"query.sourceLanguage": sourceLang,
"query.targetLanguage": targetLang,
"query.text": text
}), res = await fetch(url);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return (await res.json()).translation;
}, TAG_STYLES = {
IS_FILTERED: {
text: "🚫 已屏蔽",
color: "#de3333",
reasonType: "单番号屏蔽",
isCounted: !0,
countKey: "currentPageFilterCount"
},
IS_FAVORITE: {
text: "⭐ 已收藏",
color: "#25b1dc",
reasonType: "",
isCounted: !0,
countKey: "currentPageFavoriteCount"
},
IS_HAS_DOWN: {
text: "📥️ 已下载",
color: "#7bc73b",
reasonType: "",
isCounted: !0,
countKey: "currentPageHasDownCount"
},
IS_HAS_WATCH: {
text: "🔍 已观看",
color: "#d7a80c",
reasonType: "",
isCounted: !0,
countKey: "currentPageHasWatchCount"
},
IS_KEYWORD_FILTER: {
text: "❌ 关键词屏蔽",
color: "#de3333",
reasonType: "",
isCounted: !0,
countKey: "currentPageKeywordFilterCount"
},
IS_ACTOR_FILTER: {
text: "♂️ 男演员屏蔽",
color: "#b22222",
reasonType: "",
isCounted: !0,
countKey: "currentPageActorFilterCount"
},
IS_ACTRESS_FILTER: {
text: "♀️ 女演员屏蔽",
color: "#cd5c5c",
reasonType: "",
isCounted: !0,
countKey: "currentPageActorFilterCount"
},
IS_WAIT_CHECK: {
text: "",
color: "",
reasonType: "",
isCounted: !0,
countKey: "currentPageWaitCheckCount"
}
};
export { ListPageButtonPlugin };
+475
View File
@@ -0,0 +1,475 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, isSearchPage, currentHref, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from '../core/constants.js';
import { HotkeyManager } from './preview-video.js';
import { translateText, TAG_STYLES } from './list-page-button.js';
class ListPagePlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "currentPageFilterCount", 0);
__publicField(this, "currentPageFavoriteCount", 0);
__publicField(this, "currentPageHasDownCount", 0);
__publicField(this, "currentPageHasWatchCount", 0);
__publicField(this, "currentPageKeywordFilterCount", 0);
__publicField(this, "currentPageActorFilterCount", 0);
__publicField(this, "currentPageWaitCheckCount", 0);
__publicField(this, "currentPageTotalCount", 0);
__publicField(this, "cache", localStorage.getItem("jhs_translate") ? JSON.parse(localStorage.getItem("jhs_translate")) : {});
__publicField(this, "writeQueue", Promise.resolve());
}
getName() {
return "ListPagePlugin";
}
async handle() {
this.cleanRepeatId();
this.replaceHdImg();
this.addJumpPageControl();
this.fixBusTitleBox();
await this.doFilter();
this.bindClick().then();
this.bindListPageHotKey().then();
this.rememberTagExpand();
$(this.getSelector().itemSelector + " a").attr("target", "_blank");
this.checkDom();
}
rememberTagExpand() {
if (!window.location.href.includes("actors")) return;
const $expandButton = $(".tag-expand");
if (0 === $expandButton.length) return;
const isExpanded = "true" === localStorage.getItem("jhs_tag_expand"), $actorTagsContent = $(".actor-tags .content");
isExpanded && $actorTagsContent.hasClass("collapse") && $expandButton[0].click();
$expandButton.on("click", (function() {
const newState = !$actorTagsContent.hasClass("collapse");
localStorage.setItem("jhs_tag_expand", newState.toString());
}));
}
checkDom() {
if (!window.isListPage) return;
const selector = this.getSelector(), targetNode = document.querySelector(selector.boxSelector);
if (!targetNode) {
console.error("没有找到容器节点!");
return;
}
const observer = new MutationObserver((async mutations => {
observer.disconnect();
try {
this.replaceHdImg();
this.addJumpPageControl();
this.fixBusTitleBox();
await this.doFilter();
await this.getBean("ListPageButtonPlugin").sortItems();
this.getBean("CoverButtonPlugin").addSvgBtn().then();
$(this.getSelector().itemSelector + " a").attr("target", "_blank");
this.getBean("AutoPagePlugin").checkLoad();
} finally {
observer.observe(targetNode, config);
}
})), config = {
childList: !0,
subtree: !1
};
observer.observe(targetNode, config);
}
fixBusTitleBox() {
if (!isJavBus) return;
$(this.getSelector().itemSelector).toArray().forEach((ele => {
var _a2;
let $box2 = $(ele);
if ($box2.find(".avatar-box").length > 0) return;
const title = (null == (_a2 = $box2.find("img").attr("title")) ? void 0 : _a2.trim()) || "";
$box2.find(".photo-info span:first").contents().first().wrap(`<span class="video-title" title="${title}">${title}</span>`);
$box2.find("br").remove();
}));
}
cleanRepeatId() {
if (!isJavBus) return;
$("#waterfall_h").removeAttr("id").attr("id", "no-page");
const $waterfalls = $('[id="waterfall"]');
0 !== $waterfalls.length && $waterfalls.each((function() {
const $current = $(this);
if (!$current.hasClass("masonry")) {
$current.children().insertAfter($current);
$current.remove();
}
}));
}
async doFilter() {
if (!window.isListPage) return;
let movieList = $(this.getSelector().itemSelector).toArray();
if (movieList.length) {
await this.filterMovieList(movieList);
await this.getBean("WangPan115MatchPlugin").matchMovieList(movieList);
isJavBus && await this.getBean("BusImgPlugin").logImageHeightsByRow();
}
}
async filterMovieList(movieList) {
utils.time("累计耗费时间");
utils.time("读取数据耗时");
const [carList, filterKeywordList, blacklist, blacklistCarList, settingObj] = await Promise.all([ storageManager.getCarList(), storageManager.getTitleFilterKeyword(), storageManager.getBlacklist(), storageManager.getBlacklistCarList(), storageManager.getSetting() ]), t1 = utils.time("读取数据耗时"), carNumSets = carList.reduce(((dataMap, item) => {
const statusKey = item.status;
dataMap.hasOwnProperty(statusKey) && dataMap[statusKey].add(item.carNum);
return dataMap;
}), {
[Status_FILTER]: new Set,
[Status_FAVORITE]: new Set,
[Status_HAS_DOWN]: new Set,
[Status_HAS_WATCH]: new Set
});
utils.time("组装数据耗时");
const starIdToRoleMap = new Map(blacklist.map((item => [ item.starId, item.role ]))), {actorCarNumToNameMap: actorCarNumToNameMap, actressCarNumToNameMap: actressCarNumToNameMap} = blacklistCarList.reduce(((dataMap, carItem) => {
const role = starIdToRoleMap.get(carItem.starId);
if (!role) {
clog.error("黑名单数据源丢失演员信息", carItem);
return dataMap;
}
const targetMap = "actor" === role ? dataMap.actorCarNumToNameMap : dataMap.actressCarNumToNameMap;
targetMap.has(carItem.carNum) || targetMap.set(carItem.carNum, carItem.names);
return dataMap;
}), {
actorCarNumToNameMap: new Map,
actressCarNumToNameMap: new Map
}), t2 = utils.time("组装数据耗时"), settings = {
showFilterItem: (null == settingObj ? void 0 : settingObj.showFilterItem) ?? NO,
showFilterActorItem: (null == settingObj ? void 0 : settingObj.showFilterActorItem) ?? NO,
showFilterKeywordItem: (null == settingObj ? void 0 : settingObj.showFilterKeywordItem) ?? NO,
showFavoriteItem: (null == settingObj ? void 0 : settingObj.showFavoriteItem) ?? YES,
showHasDownItem: (null == settingObj ? void 0 : settingObj.showHasDownItem) ?? YES,
showHasWatchItem: (null == settingObj ? void 0 : settingObj.showHasWatchItem) ?? YES,
showAllItem: (null == settingObj ? void 0 : settingObj.showAllItem) ?? NO,
tagPosition: (null == settingObj ? void 0 : settingObj.tagPosition) || "rightTop",
movieShowType: (null == settingObj ? void 0 : settingObj.movieShowType) || "hide"
};
this.currentPageFilterCount = 0;
this.currentPageFavoriteCount = 0;
this.currentPageHasDownCount = 0;
this.currentPageHasWatchCount = 0;
this.currentPageKeywordFilterCount = 0;
this.currentPageActorFilterCount = 0;
this.currentPageWaitCheckCount = 0;
this.currentPageTotalCount = 0;
utils.time("处理页面耗时");
await Promise.all(movieList.map((async ele => {
let $box2 = $(ele);
if (isJavBus && $box2.find(".avatar-box").length > 0) return;
const {carNum: carNum2, title: title} = this.getBoxCarInfo($box2), {filter: filter, favorite: favorite, hasDown: hasDown, hasWatch: hasWatch} = carNumSets, isFavorite = favorite.has(carNum2), isHasDown = hasDown.has(carNum2), isHasWatch = hasWatch.has(carNum2), isFiltered = filter.has(carNum2), isFilterActorMale = actorCarNumToNameMap.has(carNum2), isFilterActorFemale = actressCarNumToNameMap.has(carNum2), isFilterActor = isFilterActorMale || isFilterActorFemale, foundKeyword = filterKeywordList.find((kw => title.includes(kw) || carNum2.startsWith(kw))), isFilterKeyword = !!foundKeyword;
if (!isSearchPage) {
let shouldHide = settings.showFavoriteItem === NO && isFavorite || settings.showHasDownItem === NO && isHasDown || settings.showHasWatchItem === NO && isHasWatch || settings.showFilterItem === NO && isFiltered && !(isFavorite || isHasDown || isHasWatch) || settings.showFilterActorItem === NO && isFilterActor || settings.showFilterKeywordItem === NO && isFilterKeyword;
if ($box2.attr("data-movieShowType") !== settings.movieShowType) {
$box2.css("border", "");
$box2.children().css("visibility", "");
$box2.removeAttr("data-hide");
$box2.show();
}
const isCurrentlyHidden = $box2.attr("data-hide") === YES;
settings.showAllItem === YES && (shouldHide = !1);
if (shouldHide !== isCurrentlyHidden) {
shouldHide ? $box2.attr("data-hide", YES) : $box2.removeAttr("data-hide");
if ("hide" === settings.movieShowType) shouldHide ? $box2.hide() : $box2.show(); else {
if ("visibility" !== settings.movieShowType) throw new Error("movieShowType值有误:" + settings.movieShowType);
{
const $content = $box2.children(), borderStyle = shouldHide ? "1px solid rgb(192 176 176)" : "none", visibilityValue = shouldHide ? "hidden" : "visible";
$box2.css("border", borderStyle);
$content.css("visibility", visibilityValue);
}
}
$box2.attr("data-movieShowType") !== settings.movieShowType && $box2.attr("data-movieShowType", settings.movieShowType);
}
}
let tag = TAG_STYLES.IS_WAIT_CHECK, filterReason = null;
if (isFiltered) tag = TAG_STYLES.IS_FILTERED; else if (isFavorite) tag = TAG_STYLES.IS_FAVORITE; else if (isHasDown) tag = TAG_STYLES.IS_HAS_DOWN; else if (isHasWatch) tag = TAG_STYLES.IS_HAS_WATCH; else if (isFilterKeyword) {
tag = TAG_STYLES.IS_KEYWORD_FILTER;
filterReason = foundKeyword || "未知";
} else if (isFilterActorMale) {
tag = TAG_STYLES.IS_ACTOR_FILTER;
filterReason = actorCarNumToNameMap.get(carNum2) || "";
} else if (isFilterActorFemale) {
tag = TAG_STYLES.IS_ACTRESS_FILTER;
filterReason = actressCarNumToNameMap.get(carNum2) || "";
}
filterReason || (filterReason = tag.reasonType);
tag.isCounted && this[tag.countKey]++;
this.currentPageTotalCount++;
$box2.find(".status-tag").remove();
const tagPositionCss = "rightTop" === settings.tagPosition ? "right: 0; top:5px;" : "left: 0; top:5px;";
if (tag.text) {
const tagHtml = isJavDb ? `<span class="tag is-success status-tag" data-tip="${filterReason}" title=""\n style="margin-right: 5px; border-radius:10px; position:absolute; \n z-index:10; background-color: ${tag.color} !important; ${tagPositionCss}">\n ${tag.text}\n </span>` : `<a class="a-primary status-tag" data-tip="${filterReason}" title=""\n style="margin-right: 5px; padding: 0 5px; color: #fff !important; border-radius:10px; position:absolute; \n z-index:10; background-color: ${tag.color} !important; ${tagPositionCss}">\n <span class="tag" style="color:#fff !important;">${tag.text}</span>\n </a>`;
isJavDb && $box2.find(".tags").append(tagHtml);
if (isJavBus) {
const $itemTag = $box2.find(".item-tag");
$itemTag.length ? $itemTag.append(tagHtml) : $box2.find(".photo-info > span > div").append(tagHtml);
}
}
await this.translate($box2);
})));
const t3 = utils.time("处理页面耗时"), t4 = utils.time("累计耗费时间");
$("#waitDownBtn span").text(`打开已收藏 (${carNumSets.favorite.size})`);
clog.log(`\n <table class="countTable" style='border-collapse: collapse; width: 100%'>\n <tr>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${t1}</td>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${t2}</td>\n </tr>\n \n <tr>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${t3}</td>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${t4}</td>\n </tr>\n <tr>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>项目</td>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>数量</td>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>项目</td>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>数量</td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>屏蔽单番号</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageFilterCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'>收藏</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageFavoriteCount}</strong></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>屏蔽演员</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageActorFilterCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'>已下载</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageHasDownCount}</strong></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>屏蔽关键词</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageKeywordFilterCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'>已观看</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageHasWatchCount}</strong></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>待鉴定</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageWaitCheckCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'></td>\n <td style='padding: 3px; border: 1px solid #ccc;'></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>总数</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageTotalCount}</strong></td>\n </tr>\n </table>\n `);
}
async bindClick() {
let selector = this.getSelector();
$(selector.boxSelector).on("click", ".item img", (async event => {
event.preventDefault();
event.stopPropagation();
if ($(event.target).closest("div.meta-buttons").length) return;
const $box2 = $(event.target).closest(".item"), {carNum: carNum2, aHref: aHref} = this.getBoxCarInfo($box2);
let dialogOpenDetail = await storageManager.getSetting("dialogOpenDetail", YES);
if (carNum2.includes("FC2-")) {
let movieId = this.parseMovieId(aHref);
this.getBean("Fc2Plugin").openFc2Dialog(movieId, carNum2, aHref);
} else if (dialogOpenDetail === YES) {
utils.openPage(aHref, carNum2, !0, event);
this.$currentImage = null;
} else window.open(aHref);
}));
$(selector.boxSelector).on("click", ".item video", (async event => {
const video = event.currentTarget;
video.paused ? video.play().catch((e => console.error("播放失败:", e))) : video.pause();
event.preventDefault();
event.stopPropagation();
}));
$(selector.boxSelector).on("click", ".item .video-title", (async event => {
if ($(event.target).closest('[class^="jhs-match-"]').length) return;
const $box2 = $(event.currentTarget).closest(".item"), {carNum: carNum2, aHref: aHref} = this.getBoxCarInfo($box2);
if (carNum2.includes("FC2-")) {
event.preventDefault();
let movieId = this.parseMovieId(aHref);
this.getBean("Fc2Plugin").openFc2Dialog(movieId, carNum2, aHref);
}
}));
$(selector.boxSelector).on("contextmenu", ".item img, .item video", (async event => {
event.preventDefault();
const $box2 = $(event.target).closest(".item"), {carNum: carNum2, url: url, publishTime: publishTime} = this.getBoxCarInfo($box2);
let $actor = isJavDb ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10"), actorName = "";
$actor.length && (actorName = $actor.text().trim().split(",")[0].replace("(無碼)", ""));
utils.q(event, `是否屏蔽番号 ${carNum2}?`, (async () => {
setTimeout((async () => {
actorName || (actorName = await this.parseActressName(url));
await storageManager.saveCar({
carNum: carNum2,
url: url,
names: actorName,
actionType: Status_FILTER,
publishTime: publishTime
});
window.refresh();
show.ok("操作成功");
}));
}));
}));
}
async parseActressName(url) {
let actorName = null;
if (await storageManager.getSetting("enableSaveActressCarInfo", NO) === YES) {
clog.debug("鉴定补录演员信息-已启用, 开始解析详情页");
clog.debug("开始解析演员详情页", url);
const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html);
isJavDb ? actorName = $dom.find(".female").prev().map(((i, el) => $(el).text())).get().join(" ") : isJavBus && (actorName = $dom.find('span[onmouseover*="star_"] a').map(((i, el) => $(el).text())).get().join(" "));
clog.debug("解析到名称:", actorName);
}
return actorName;
}
async bindListPageHotKey() {
this.$currentImage = null;
$(document).on("mouseenter", this.getSelector().coverImgSelector, (e => {
this.$currentImage = $(e.currentTarget);
})).on("mouseleave", this.getSelector().coverImgSelector, (() => {
this.$currentImage = null;
}));
let settingObj = await storageManager.getSetting();
this.filterHotKey = settingObj.filterHotKey;
this.favoriteHotKey = settingObj.favoriteHotKey;
this.hasDownHotKey = settingObj.hasDownHotKey;
this.hasWatchHotKey = settingObj.hasWatchHotKey;
this.enableImageHotKey = settingObj.enableImageHotKey || NO;
this.clogHotKey = settingObj.clogHotKey;
this.foldCategoryHotKey = settingObj.foldCategoryHotKey;
this.showFilterItemHotKey = settingObj.showFilterItemHotKey;
this.showFilterActorItemHotKey = settingObj.showFilterActorItemHotKey;
this.showFilterKeywordItemHotKey = settingObj.showFilterKeywordItemHotKey;
this.showFavoriteItemHotKey = settingObj.showFavoriteItemHotKey;
this.showHasDownItemHotKey = settingObj.showHasDownItemHotKey;
this.showHasWatchItemHotKey = settingObj.showHasWatchItemHotKey;
this.showAllItemHotKey = settingObj.showAllItemHotKey;
const HOTKEY_REGISTRY = {
showFilterItemHotKey: "showFilterItem",
showFilterActorItemHotKey: "showFilterActorItem",
showFilterKeywordItemHotKey: "showFilterKeywordItem",
showFavoriteItemHotKey: "showFavoriteItem",
showHasDownItemHotKey: "showHasDownItem",
showHasWatchItemHotKey: "showHasWatchItem",
showAllItemHotKey: "showAllItem"
}, registerToggleHotkey = (hotkeyName, settingKey) => {
const hotkeyString = this[hotkeyName];
hotkeyString && HotkeyManager.registerHotkey(hotkeyString, (async event => {
const newVal = await storageManager.getSetting(settingKey) === YES ? NO : YES;
await storageManager.saveSettingItem(settingKey, newVal);
window.refresh();
}));
};
for (const hotkeyName in HOTKEY_REGISTRY) if (HOTKEY_REGISTRY.hasOwnProperty(hotkeyName)) {
registerToggleHotkey(hotkeyName, HOTKEY_REGISTRY[hotkeyName]);
}
if (this.enableImageHotKey === NO) return;
const handleAction = async (boxInfo, status) => {
setTimeout((async () => {
let actorName = await this.parseActressName(boxInfo.url);
await storageManager.saveCar({
carNum: boxInfo.carNum,
url: boxInfo.url,
names: actorName,
actionType: status,
publishTime: boxInfo.publishTime
});
window.refresh();
show.ok("操作成功");
}));
}, handlers = {};
this.filterHotKey && (handlers[this.filterHotKey] = boxInfo => {
handleAction(boxInfo, Status_FILTER);
});
this.favoriteHotKey && (handlers[this.favoriteHotKey] = boxInfo => {
handleAction(boxInfo, Status_FAVORITE);
});
this.hasDownHotKey && (handlers[this.hasDownHotKey] = boxInfo => {
handleAction(boxInfo, Status_HAS_DOWN);
});
this.hasWatchHotKey && (handlers[this.hasWatchHotKey] = boxInfo => {
handleAction(boxInfo, Status_HAS_WATCH);
});
this.clogHotKey && HotkeyManager.registerHotkey(this.clogHotKey, (event => {
clog.toggleExpandCollapsed();
}));
this.foldCategoryHotKey && HotkeyManager.registerHotkey(this.foldCategoryHotKey, (event => {
const $btn = $("#foldCategoryBtn");
$btn.length && $btn[0].click();
}));
const registerImageHotkey = (key, handler) => {
HotkeyManager.registerHotkey(key, (event => {
const activeElement = document.activeElement;
if (!("INPUT" === activeElement.tagName || "TEXTAREA" === activeElement.tagName || activeElement.isContentEditable) && this.$currentImage) {
const $box2 = this.$currentImage.closest(".item"), boxInfo = this.getBoxCarInfo($box2);
handler(boxInfo);
}
}));
};
Object.entries(handlers).forEach((([key, handler]) => {
registerImageHotkey(key, handler);
}));
}
replaceHdImg(coverImgNodeList) {
coverImgNodeList && "string" == typeof coverImgNodeList.jquery && (coverImgNodeList = coverImgNodeList.toArray());
coverImgNodeList || (coverImgNodeList = document.querySelectorAll(this.getSelector().coverImgSelector));
isJavDb && coverImgNodeList.forEach((img => {
img.src = img.src.replace("thumbs", "covers");
img.title = "";
}));
if (isJavBus) {
const THUMB_PATH_REGEX = /\/(imgs|pics)\/(thumb|thumbs)\//, IMG_EXT_REGEX = /(\.jpg|\.jpeg|\.png)$/i, replaceWithHd = img => {
if (img.src && THUMB_PATH_REGEX.test(img.src) && "true" !== img.dataset.hdReplaced) {
img.src = img.src.replace(THUMB_PATH_REGEX, "/$1/cover/").replace(IMG_EXT_REGEX, "_b$1");
img.dataset.hdReplaced = "true";
img.dataset.title = img.title;
img.title = "";
}
}, DMM_THUMB_REGEX = /ps(\.jpg|\.jpeg|\.png)$/i, replaceWithHdForDMM = img => {
if (img.src && DMM_THUMB_REGEX.test(img.src) && "true" !== img.dataset.hdReplaced) {
img.src = img.src.replace(DMM_THUMB_REGEX, "pl$1");
img.dataset.hdReplaced = "true";
img.dataset.title = img.title;
img.title = "";
}
};
coverImgNodeList.forEach((img => {
replaceWithHd(img);
replaceWithHdForDMM(img);
}));
}
storageManager.getSetting("hoverBigImg", NO).then((hoverBigImg => {
hoverBigImg === YES && (window.imageHoverPreviewObj ? window.imageHoverPreviewObj.bindEvents() : window.imageHoverPreviewObj = new ImageHoverPreview({
selector: this.getSelector().coverImgSelector
}));
}));
}
async translate($box2) {
if (await storageManager.getSetting("translateTitle", YES) !== YES) return;
let content, carNum2, $title = $box2.find(".video-title");
if (isJavDb) {
content = $title.contents().filter(((i, node) => 3 === node.nodeType && "" !== node.textContent.trim())).text().trim();
carNum2 = $box2.find(".video-title strong").text().trim();
} else {
content = $box2.find("img").attr("data-title").trim();
carNum2 = $box2.find("a").attr("href").split("/").filter(Boolean).pop().trim();
}
if (this.cache[carNum2]) {
let _this = this;
$title.contents().each((function() {
3 === this.nodeType && "" !== this.textContent.trim() && (this.textContent = " " + _this.cache[carNum2] + " ");
}));
$title.attr("title", _this.cache[carNum2]);
} else translateText(content).then((result => {
if (isJavDb) {
$title.contents().each((function() {
3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(carNum2) || (this.textContent = " " + result + " ");
}));
$title.attr("title", result);
} else $title.text(result);
this.writeQueue = this.writeQueue.then((() => {
this.cache[carNum2] = result;
localStorage.setItem("jhs_translate", JSON.stringify(this.cache));
}));
})).catch((error => {
console.error("翻译失败:", error);
}));
}
async revertTranslation() {
$(this.getSelector().itemSelector).toArray().forEach((ele => {
let $box2 = $(ele);
const originalContent = $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
let carNum2;
isJavDb && (carNum2 = $box2.find(".video-title strong").text().trim());
const $title = $box2.find(".video-title");
$title.contents().each((function() {
3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(carNum2) || (this.textContent = " " + originalContent + " ");
}));
$title.removeAttr("title");
}));
}
addJumpPageControl() {
if ($("#gemini-jump-page-control").length > 0) return;
if (0 === $(".pagination-link.is-current").length) return;
const currentPageNum = utils.getUrlParam(currentHref, "page") || 1, $input = $("<input>", {
type: "number",
id: "jumpPageInput",
placeholder: "页码",
min: "1",
style: "width: 60px; margin-left: 10px; padding: 10px; border: 1px solid #ccc; font-size: 14px;",
value: currentPageNum + 1
}), $button = $("<button>", {
text: "跳转",
style: "margin-left: 5px; padding: 9px 8px; cursor: pointer; border: 1px solid #ccc; background-color: #f0f0f0; font-size: 14px;"
}), $jumpLi = $("<li>", {
id: "gemini-jump-page-control"
}).append($input).append($button);
$(".pagination-list").append($jumpLi);
const jumpToPage = () => {
const pageNumber = parseInt($input.val(), 10);
if (isNaN(pageNumber) || pageNumber < 1) {
$input.focus();
return;
}
const newUrl = new URL(window.location.href);
newUrl.searchParams.set("page", pageNumber.toString());
window.location.href = newUrl.toString();
};
$button.on("click", jumpToPage);
$input.on("keypress", (function(e) {
if (13 === e.which) {
jumpToPage();
e.preventDefault();
}
}));
}
}
export { ListPagePlugin };
+212
View File
@@ -0,0 +1,212 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
import { GM_xmlhttpRequest } from 'vite-plugin-monkey/dist/client';
class MagnetHubPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "currentEngine", null);
__publicField(this, "searchEngines", [ {
name: "U9A9",
id: "u9a9",
url: "https://u9a9.com/?type=2&search={keyword}",
targetPage: "https://u9a9.com/?type=2&search={keyword}",
parseHtml: this.parseU3C3
}, {
name: "U3C3",
id: "u3c3",
url: "https://u3c3.com/?search2=a8lr16lo&search={keyword}",
targetPage: "https://u3c3.com/?search2=a8lr16lo&search={keyword}",
parseHtml: this.parseU3C3
}, {
name: "Sukebei",
id: "Sukebei",
url: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}",
targetPage: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}",
parseHtml: this.parseSukebei
} ]);
}
getName() {
return "MagnetHubPlugin";
}
async initCss() {
return "\n <style>\n .magnet-container {\n margin: 20px auto;\n width: 100%;\n font-family: Arial, sans-serif;\n }\n .magnet-tabs {\n display: flex;\n border-bottom: 1px solid #ddd;\n margin-bottom: 15px;\n justify-content: space-between;\n }\n .magnet-tab {\n padding: 5px 12px;\n cursor: pointer;\n border: 1px solid transparent;\n border-bottom: none;\n margin-right: 5px;\n background: #f5f5f5;\n border-radius: 5px 5px 0 0;\n }\n .magnet-tab.active {\n background: #fff;\n border-color: #ddd;\n border-bottom: 1px solid #fff;\n margin-bottom: -1px;\n font-weight: bold;\n }\n .magnet-tab:hover:not(.active) {\n background: #e9e9e9;\n }\n \n .magnet-results {\n min-height: 200px;\n }\n .magnet-result {\n padding: 15px;\n border-bottom: 1px solid #eee;\n position: relative; \n }\n .magnet-result:hover {\n background-color: #f9f9f9;\n }\n .magnet-title {\n font-weight: bold;\n margin-bottom: 5px;\n white-space: nowrap;\n overflow: hidden; \n text-overflow: ellipsis;\n padding-right: 80px; \n }\n .magnet-info {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: #666;\n margin-bottom: 5px;\n }\n .magnet-loading {\n text-align: center;\n padding: 20px;\n }\n .magnet-error {\n color: #f44336;\n padding: 10px;\n }\n \n .magnet-copy {\n position: absolute;\n right: 15px;\n top: 12px;\n }\n .magnet-hub-btn {\n background-color: #f0f0f0;\n color: #555;\n border: 1px solid #ddd;\n padding: 3px 8px;\n border-radius: 3px;\n cursor: pointer;\n font-size: 12px;\n transition: all 0.2s;\n margin-left: 10px;\n }\n .magnet-hub-btn:hover {\n background-color: #e0e0e0;\n border-color: #ccc;\n }\n .magnet-hub-btn.copied {\n background-color: #4CAF50;\n color: white;\n border-color: #4CAF50;\n }\n </style>\n ";
}
createMagnetHub(keyword) {
keyword = keyword.replace("FC2-", "");
const $container = $('<div class="magnet-container"></div>'), $tabs = $('<div class="magnet-tabs"></div>'), key = "jhs_magnetHub_selectedEngine", savedEngineId = localStorage.getItem(key);
let defaultEngineIndex = 0;
const $tabBox = $('<div style="display: flex;"></div>');
this.searchEngines.forEach(((engine, index) => {
const $tab = $(`<div class="magnet-tab" data-engine="${engine.id}">${engine.name}</div>`);
if (savedEngineId && engine.id === savedEngineId) {
$tab.addClass("active");
this.currentEngine = engine;
defaultEngineIndex = index;
} else if (0 === index && !savedEngineId) {
$tab.addClass("active");
this.currentEngine = engine;
}
$tabBox.append($tab);
}));
$tabs.append($tabBox);
$tabs.append(`<a style="margin-right: 20px;margin-top:3px" id="targetBox" href="${this.currentEngine.targetPage.replace("{keyword}", encodeURIComponent(keyword))}" target="_blank">原网页</a>`);
$container.append($tabs);
const $resultsContainer = $('<div class="magnet-results"></div>');
$container.append($resultsContainer);
$container.on("click", ".magnet-tab", (e => {
const engineId = $(e.target).data("engine");
this.currentEngine = this.searchEngines.find((engine => engine.id === engineId));
$("#targetBox").attr("href", this.currentEngine.targetPage.replace("{keyword}", encodeURIComponent(keyword)));
localStorage.setItem(key, engineId);
$container.find(".magnet-tab").removeClass("active");
$(e.target).addClass("active");
this.searchEngine($resultsContainer, this.currentEngine, keyword);
}));
this.searchEngine($resultsContainer, this.currentEngine || this.searchEngines[defaultEngineIndex], keyword);
return $container;
}
searchEngine($container, engine, keyword) {
$container.html(`<div class="magnet-loading">正在从 ${engine.name} 搜索 "${keyword}"...</div>`);
const cacheKey = `${engine.name}_${keyword}`;
sessionStorage.getItem(cacheKey);
const url = engine.url.replace("{keyword}", encodeURIComponent(keyword));
engine.parseHtml && GM_xmlhttpRequest({
method: "GET",
url: url,
onload: response => {
try {
const results = engine.parseHtml.call(this, response.responseText, keyword);
results.length > 0 && sessionStorage.setItem(cacheKey, JSON.stringify(results));
this.displayResults($container, results, engine.name);
} catch (e) {
$container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`);
}
},
onerror: error => {
$container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`);
}
});
engine.parseJson && engine.parseJson.call(this, $container, engine, keyword, cacheKey);
}
displayResults($container, results, engineName) {
$container.empty();
if (0 !== results.length) {
results.forEach((result => {
const $result = $(`\n <div class="magnet-result">\n <div class="magnet-title"><a href="${result.magnet}">${result.title}</a></div>\n <div class="magnet-info">\n <span>大小: ${result.size || "未知"}</span>\n <span>日期: ${result.date || "未知"}</span>\n </div>\n <div class="magnet-copy">\n <button class="magnet-hub-btn copy-btn" data-magnet="${result.magnet}">复制链接</button>\n <button class="magnet-hub-btn down-115" data-magnet="${result.magnet}">115离线下载</button>\n </div>\n </div>\n `);
$container.append($result);
}));
$container.on("click", ".copy-btn", (function() {
const $btn = $(this), magnet = $btn.data("magnet");
navigator.clipboard ? navigator.clipboard.writeText(magnet).then((() => {
showCopiedFeedback($btn);
})).catch((err => {
fallbackCopy(magnet, $btn);
})) : fallbackCopy(magnet, $btn);
}));
$container.on("click", ".down-115", (async event => {
const magnet = $(event.currentTarget).data("magnet");
let loadObj = loading();
try {
await this.getBean("WangPan115TaskPlugin").handleAddTask(magnet);
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
}));
} else $container.append('<div class="magnet-error">没有找到相关结果</div>');
function showCopiedFeedback($btn) {
const originalText = $btn.text();
$btn.addClass("copied").text("已复制");
setTimeout((() => {
$btn.removeClass("copied").text(originalText);
}), 2e3);
}
function fallbackCopy(text, $btn) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
showCopiedFeedback($btn);
} catch (err) {
console.error("复制失败:", err);
alert("复制失败,请手动复制链接");
}
document.body.removeChild(textarea);
}
}
parseBTSOW($container, engine, keyword, cacheKey) {
const _this = this;
GM_xmlhttpRequest({
method: "POST",
url: engine.url,
headers: {
"Content-Type": "application/json"
},
data: `[{"search":"${keyword}"},50,1]`,
onload: response => {
try {
const dataList = JSON.parse(response.responseText).data, results = [];
for (let i = 0; i < dataList.length; i++) {
let item = dataList[i];
results.push({
title: item.name,
magnet: "magnet:?xt=urn:btih:" + item.hash,
size: (item.size / 1073741824).toFixed(2) + " GB",
date: utils.formatDate(new Date(1e3 * item.lastUpdateTime))
});
}
results.length > 0 && sessionStorage.setItem(cacheKey, JSON.stringify(results));
_this.displayResults($container, results, engine.name);
} catch (e) {
$container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`);
}
},
onerror: error => {
$container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`);
}
});
}
parseU3C3(html, keyword) {
const $dom = utils.htmlTo$dom(html), results = [];
$dom.find(".torrent-list tbody tr").each(((i, el) => {
const $el = $(el);
if ($el.text().includes("置顶")) return;
const title = $el.find("td:nth-child(2) a").attr("title") || $el.find("td:nth-child(2) a").text().trim();
if (!title.toLowerCase().includes(keyword.toLowerCase())) return;
const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"), size = $el.find("td:nth-child(4)").text().trim(), date = $el.find("td:nth-child(5)").text().trim();
magnet && results.push({
title: title,
magnet: magnet,
size: size,
date: date
});
}));
return results;
}
parseSukebei(html, keyword) {
const $dom = utils.htmlTo$dom(html), results = [];
$dom.find(".torrent-list tbody tr").each(((i, el) => {
const $el = $(el);
if ($el.text().includes("置顶")) return;
const title = $el.find("td:nth-child(2) a").attr("title") || $el.find("td:nth-child(2) a").text().trim();
if (!title.toLowerCase().includes(keyword.toLowerCase())) return;
const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"), size = $el.find("td:nth-child(4)").text().trim(), date = $el.find("td:nth-child(5)").text().trim();
magnet && results.push({
title: title,
magnet: magnet,
size: size,
date: date
});
}));
return results;
}
}
export { MagnetHubPlugin };
+92
View File
@@ -0,0 +1,92 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class NavBarPlugin extends BasePlugin {
getName() {
return "NavBarPlugin";
}
async initCss() {
return "\n .highlight-red {\n /* 核心要求:高亮红色文本 */\n color: red !important; \n \n /* 建议:增加字体加粗,效果更明显 */\n font-weight: bold;\n \n /* 建议:增加背景色,效果更突出 */\n /* background-color: yellow; */ \n}\n ";
}
handle() {
this.margeNav();
this.hookSearch();
this.hookOldSearch();
this.toggleOtherNavItem();
$(window).resize(this.toggleOtherNavItem);
if (window.location.href.includes("/search")) {
const urlParams = new URLSearchParams(window.location.search);
let q = urlParams.get("q"), f = urlParams.get("f");
$("#search-keyword").val(q);
f && $("#search-type").val(f);
q && this.highlightKeyword(q);
}
}
highlightKeyword(keyword) {
const trimedKeyword = keyword.trim();
if (!trimedKeyword) return;
const lowerCaseKeyword = trimedKeyword.toLowerCase();
$(".video-title strong, .actor-box strong").each((function() {
const $strongElement = $(this);
$strongElement.text().toLowerCase().includes(lowerCaseKeyword) && $strongElement.addClass("highlight-red");
}));
}
hookSearch() {
$("#navbar-menu-hero").after('\n <div class="navbar-menu" id="search-box">\n <div class="navbar-start" style="display: flex; align-items: center; gap: 5px;">\n <select id="search-type" style="padding: 8px 12px; border: 1px solid #555; border-radius: 4px; background-color: #333; color: #eee; font-size: 14px; outline: none;">\n <option value="all">影片</option>\n <option value="actor">演員</option>\n <option value="series">系列</option>\n <option value="maker">片商</option>\n <option value="director">導演</option>\n <option value="code">番號</option>\n <option value="list">清單</option>\n </select>\n <input id="search-keyword" type="text" placeholder="輸入影片番號,演員名等關鍵字進行檢索" style="padding: 8px 12px; border: 1px solid #555; border-radius: 4px; flex-grow: 1; font-size: 14px; background-color: #333; color: #eee; outline: none;">\n <a href="/advanced_search?noFold=1" title="進階檢索" style="padding: 6px 12px; background-color: #444; border-radius: 4px; text-decoration: none; color: #ddd; font-size: 14px; border: 1px solid #555;"><span>...</span></a>\n <a id="search-img-btn" style="padding: 6px 16px; background-color: #444; color: #fff; border-radius: 4px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid #555;">识图</a>\n <a id="search-btn" style="padding: 6px 16px; background-color: #444; color: #fff; border-radius: 4px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid #555;">檢索</a>\n </div>\n </div>\n ');
$("#search-keyword").on("paste", (event => {
const items = event.originalEvent.clipboardData.items;
for (let i = 0; i < items.length; i++) if (-1 !== items[i].type.indexOf("image")) {
const blob = items[i].getAsFile();
$("#search-keyword").blur();
const imageRecognitionPlugin = this.getBean("ImageRecognitionPlugin");
imageRecognitionPlugin.open((() => {
imageRecognitionPlugin.handleImageFile(blob);
imageRecognitionPlugin.resetSearchUI();
}));
return;
}
setTimeout((() => {
$("#search-btn").click();
}), 0);
})).on("keypress", (event => {
"Enter" === event.key && setTimeout((() => {
$("#search-btn").click();
}), 0);
}));
$("#search-btn").on("click", (event => {
let keyword = $("#search-keyword").val(), searchCurrentType = $("#search-type option:selected").val();
"" !== keyword && (window.location.href.includes("/search") ? window.location.href = "/search?q=" + keyword + "&f=" + searchCurrentType : window.open("/search?q=" + keyword + "&f=" + searchCurrentType));
}));
$("#search-img-btn").on("click", (() => {
this.getBean("ImageRecognitionPlugin").open();
}));
}
hookOldSearch() {
const searchImage = document.querySelector(".search-image");
if (!searchImage) return;
const clonedImage = searchImage.cloneNode(!0);
searchImage.parentNode.replaceChild(clonedImage, searchImage);
$("#button-search-image").attr("data-tooltip", "以图识图");
$(".search-image").on("click", (event => {
this.getBean("ImageRecognitionPlugin").open();
}));
}
margeNav() {
$('a[href*="/feedbacks/new"]').remove();
$('a[href*="theporndude.com"]').remove();
$('a.navbar-link[href="/makers"]').parent().after('\n <div class="navbar-item has-dropdown is-hoverable">\n <a class="navbar-link">其它</a>\n <div class="navbar-dropdown is-boxed">\n <a class="navbar-item" href="/feedbacks/new" target="_blank" >反饋</a>\n <a class="navbar-item" rel="nofollow noopener" target="_blank" href="https://theporndude.com/zh">ThePornDude</a>\n </div>\n </div>\n ');
}
toggleOtherNavItem() {
let $searchBox = $("#search-box"), $oldSearchBox = $("#search-bar-container");
if ($(window).width() < 1600 && $(window).width() > 1023) {
$searchBox.hide();
$oldSearchBox.show();
}
if ($(window).width() > 1600) {
$searchBox.show();
$oldSearchBox.hide();
}
}
}
export { NavBarPlugin };
File diff suppressed because one or more lines are too long
+275
View File
@@ -0,0 +1,275 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb } from '../core/constants.js';
import { AsyncQueue } from '../core/utils.js';
class OtherSitePlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "okBackgroundColor", "#7bc73b");
__publicField(this, "errorBackgroundColor", "#de3333");
__publicField(this, "warnBackgroundColor", "#d7a80c");
__publicField(this, "domainErrorBackgroundColor", "#d7780c");
__publicField(this, "asyncQueue", new AsyncQueue);
__publicField(this, "siteConfigs", [ {
id: "javTrailersBtn",
getBaseUrl: async () => await this.getJavTrailersUrl(),
itemSelector: ".videos-list .video-link",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.find("p.card-text").text()
}, {
id: "123AvBtn",
getBaseUrl: async () => await this.getAv123Url() + "/ja",
itemSelector: ".box-item",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search?keyword=${carNum2}`,
getDetailPageHref: $box2 => $box2.find(".detail a").attr("href"),
findCarNumOrTitle: $box2 => $box2.find("img").attr("title")
}, {
id: "jableBtn",
getBaseUrl: async () => await this.getjableUrl(),
itemSelector: "#list_videos_videos_list_search_result .detail .title a",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}/`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "avgleBtn",
getBaseUrl: async () => await this.getAvgleUrl(),
itemSelector: ".text-secondary",
searchPath: (baseUrl, carNum2) => `${baseUrl}/vod/search.html?wd=${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "missAvBtn",
getBaseUrl: async () => await this.getMissAvUrl(),
itemSelector: ".text-secondary",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "supJavBtn",
getBaseUrl: async () => await this.getSupJavUrl(),
itemSelector: ".posts post",
searchPath: (baseUrl, carNum2) => `${baseUrl}/?s=${carNum2}`,
getDetailPageHref: ($box2, baseUrl, carNum2) => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.attr("title")
}, {
id: "javDbBtn",
getBaseUrl: async () => await this.getJavDbUrl(),
itemSelector: ".movie-list .item",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search?q=${carNum2}`,
getDetailPageHref: $box2 => $box2.find("a").attr("href"),
findCarNumOrTitle: $box2 => $box2.find(".video-title").text(),
condition: sourceCarNum => isJavBus
}, {
id: "javBusBtn",
getBaseUrl: async () => await this.getJavBusUrl(),
itemSelector: ".container h3",
searchPath: (baseUrl, carNum2) => `${baseUrl}/${carNum2}`,
getDetailPageHref: ($box2, baseUrl, carNum2) => `${baseUrl}/${carNum2}`,
findCarNumOrTitle: $box2 => $box2.text(),
condition: sourceCarNum => isJavDb && sourceCarNum && !sourceCarNum.includes("FC2")
}, {
id: "fanzaBtn",
noHandle: !0,
initUrl: carNum2 => `https://www.dmm.co.jp/search/=/searchstr=${carNum2}`,
condition: sourceCarNum => sourceCarNum && !sourceCarNum.includes("FC2")
} ]);
__publicField(this, "settingCache", null);
__publicField(this, "lastFetchTime", 0);
__publicField(this, "CACHE_DURATION", 1e4);
}
getName() {
return "OtherSitePlugin";
}
async initCss() {
return "\n <style>\n .site-btn {\n position: relative !important;\n min-width: 80px;\n display: inline-block;\n padding: 5px 10px;\n color: white !important;\n background-color:#938585;\n text-decoration: none;\n border-radius: 4px;\n text-align: center;\n margin-bottom: 5px;\n }\n .site-btn:hover {\n color: white;\n transform: translateY(-2px);\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n .site-tag {\n position: absolute; \n top: -15px; \n right: 0; \n background-color: #ffc107; \n color: #333; \n font-size: 12px; \n padding: 2px 6px; \n border-radius: 4px;\n }\n </style>\n ";
}
async handle() {
window.isDetailPage && this.loadOtherSite().then();
}
async loadOtherSite(carNum2, sourceCarNum) {
if ("yes" !== await storageManager.getSetting("enableLoadOtherSite", "yes")) return;
carNum2 || (carNum2 = this.getPageInfo().carNum);
const enabledSites = this.getEnabledSites(), html = `\n <div id="otherSiteBox" class="panel-block" style="${isJavDb ? "margin-top:8px;font-size:13px" : "margin-top:10px;font-size:13px"}; user-select: none; ">\n <div style="display: flex;gap: 5px;flex-wrap: wrap">\n ${this.siteConfigs.map((config => {
config.sourceCarNum = sourceCarNum;
if (config.condition && !1 === config.condition(config.sourceCarNum)) return "";
return `<a target="_blank" class="site-btn" style="${enabledSites.includes(config.id) ? "" : "display:none"}" id="${config.id}"><span>${config.id.replace("Btn", "")}</span></a>`;
})).join("")}\n <a id="settingSiteBtn" class="site-btn"><span>设置</span></a>\n </div>\n </div>\n \n <div id="settingsArea" class="panel-block" style="display: none; margin-top:10px; margin-bottom: 10px; user-select: none; ">\n <div id="siteCheckboxes" style="display: flex;gap: 5px;flex-wrap: wrap">\n </div>\n </div>\n `;
$(".movie-panel-info").append(html);
$(".container .info").append(html);
$("#javTrailersBtn").on("click", (async event => {
event.preventDefault();
let settingObj = await storageManager.getSetting();
const filterHotKey = settingObj.filterHotKey, favoriteHotKey = settingObj.favoriteHotKey, speedVideoHotKey = settingObj.speedVideoHotKey;
let href = $("#javTrailersBtn").attr("href"), url = href + `?handle=1&filterHotKey=${filterHotKey}&favoriteHotKey=${favoriteHotKey}&speedVideoHotKey=${speedVideoHotKey}`;
event && (event.ctrlKey || event.metaKey) && (url = href);
utils.openPage(url, carNum2, !1, event);
}));
await Promise.all(this.siteConfigs.map((async config => {
config.condition && !1 === config.condition(config.sourceCarNum) || await this.handleSite(carNum2, config);
})));
this.renderSettingsArea();
this.setupEventListeners();
}
async handleSite(carNum2, config) {
const $btn = $(`#${config.id}`);
if (config.initUrl) {
$btn.attr("href", config.initUrl(carNum2));
$btn.css("backgroundColor", this.warnBackgroundColor);
}
if (config.noHandle && !0 === config.noHandle) {
const dmmCacheKey = "jhs_other_site_dmm", dmmCachedResult = (localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {})[carNum2];
if (dmmCachedResult) if ("single" === dmmCachedResult.type) {
$btn.attr("href", dmmCachedResult.url);
$btn.css("backgroundColor", this.okBackgroundColor);
} else if ("multiple" === dmmCachedResult.type) {
$btn.attr("href", dmmCachedResult.url);
$btn.append('<span class="site-tag" style="top:-15px">多结果</span>');
$btn.css("backgroundColor", this.okBackgroundColor);
}
} else try {
if ($btn.attr("href")) return;
if (utils.isHidden($btn)) return;
const cacheKey = "jhs_other_site", cacheData = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {}, siteKey = carNum2 + "_" + config.id.replace("Btn", ""), cachedResult = cacheData[siteKey];
if (cachedResult) {
if ("single" === cachedResult.type) {
$btn.attr("href", cachedResult.url);
$btn.css("backgroundColor", this.okBackgroundColor);
} else if ("multiple" === cachedResult.type) {
$btn.attr("href", cachedResult.url);
$btn.append('<span class="site-tag" style="top:-15px">多结果</span>');
$btn.css("backgroundColor", this.okBackgroundColor);
}
return;
}
const baseUrl = await config.getBaseUrl(), searchUrl = config.searchPath(baseUrl, carNum2);
$btn.attr("href", searchUrl);
const html = await gmHttp.get(searchUrl, null, config.headers, !0), $dom = utils.htmlTo$dom(html), resultUrlList = [];
$dom.find(config.itemSelector).each(((index, element) => {
const $box2 = $(element);
if (!config.findCarNumOrTitle($box2).toLowerCase().includes(carNum2.toLowerCase())) return;
let href = config.getDetailPageHref($box2, baseUrl, carNum2);
if (!href) throw new Error("解析href失败");
href.includes("http") || (href = baseUrl + (href.startsWith("/") ? href : "/" + href));
resultUrlList.push(href);
}));
let tagHtml = "", insertCacheData = null;
if (1 === resultUrlList.length) {
let resultUrl = resultUrlList[0];
$btn.attr("href", resultUrl);
$btn.css("backgroundColor", this.okBackgroundColor);
insertCacheData = {
type: "single",
url: resultUrl
};
} else if (resultUrlList.length > 1) {
$btn.attr("href", searchUrl);
tagHtml += '<span class="site-tag" style="top:-15px">多结果</span>';
$btn.css("backgroundColor", this.okBackgroundColor);
insertCacheData = {
type: "multiple",
url: searchUrl
};
} else {
$btn.attr("href", searchUrl);
$btn.attr("title", "未查询到, 点击前往搜索页");
$btn.css("backgroundColor", this.errorBackgroundColor);
}
insertCacheData && this.asyncQueue.addTask((() => {
const newCacheData = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {};
newCacheData[siteKey] = insertCacheData;
localStorage.setItem(cacheKey, JSON.stringify(newCacheData));
}));
tagHtml && $btn.append(tagHtml);
} catch (e) {
const errorString = String(e), siteName = config.id.replace("Btn", "");
if (errorString.includes("Just a moment")) {
$btn.attr("title", "请求失败:Cloudflare 安全检查。");
$btn.css("backgroundColor", this.warnBackgroundColor);
clog.warn(`检测第三方资源失败, ${siteName} 需Cloudflare安全检查`);
} else if (errorString.includes("重定向")) {
$btn.attr("title", "域名失效");
$btn.css("backgroundColor", this.domainErrorBackgroundColor);
clog.warn(`检测第三方资源失败, ${siteName} 域名被重定向`);
} else if (errorString.includes("404 Page Not Found")) {
$btn.attr("title", "未查询到, 点击前往搜索页");
$btn.css("backgroundColor", this.errorBackgroundColor);
} else {
console.error(e);
$btn.attr("title", "请求失败。");
$btn.css("backgroundColor", this.errorBackgroundColor);
clog.warn(`检测第三方资源失败, ${siteName}`);
}
}
}
async getSettingCache() {
const now = Date.now();
if (!this.settingCache || now - this.lastFetchTime > this.CACHE_DURATION) {
this.settingCache = await storageManager.getSetting();
this.lastFetchTime = now;
}
return this.settingCache;
}
async getMissAvUrl() {
return (await this.getSettingCache()).missAvUrl || "https://missav.live";
}
async getjableUrl() {
return (await this.getSettingCache()).jableUrl || "https://jable.tv";
}
async getAvgleUrl() {
return (await this.getSettingCache()).avgleUrl || "https://jav.rs";
}
async getJavTrailersUrl() {
return (await this.getSettingCache()).javTrailersUrl || "https://javtrailers.com";
}
async getAv123Url() {
return (await this.getSettingCache()).av123Url || "https://123av.com";
}
async getJavDbUrl() {
return (await this.getSettingCache()).javDbUrl || "https://javdb.com";
}
async getJavBusUrl() {
return (await this.getSettingCache()).javBusUrl || "https://www.javbus.com";
}
async getSupJavUrl() {
return (await this.getSettingCache()).supJavUrl || "https://supjav.com";
}
getEnabledSites() {
const enabledSites = localStorage.getItem("jhs_enabled_sites");
return enabledSites ? JSON.parse(enabledSites) : this.siteConfigs.map((c => c.id));
}
saveEnabledSites(sites) {
localStorage.setItem("jhs_enabled_sites", JSON.stringify(sites));
}
renderSettingsArea() {
const enabledSites = this.getEnabledSites(), checkboxesDiv = document.getElementById("siteCheckboxes");
checkboxesDiv && (checkboxesDiv.innerHTML = this.siteConfigs.map((config => {
const isEnabled = enabledSites.includes(config.id);
return `\n <div style="margin-right: 15px; display: flex; align-items: ${isJavDb ? "center" : "flex-start"};">\n <input type="checkbox" id="checkbox-${config.id}" data-site-id="${config.id}" ${isEnabled ? "checked" : ""} style="margin-right: 8px; cursor: pointer;">\n <label for="checkbox-${config.id}" style="color: #333; font-weight: 500; cursor: pointer;">${config.id.replace("Btn", "")}</label>\n </div>\n `;
})).join(""));
}
setupEventListeners() {
const settingsArea = document.getElementById("settingsArea");
document.addEventListener("click", (event => {
if ("settingSiteBtn" === event.target.id || event.target.closest("#settingSiteBtn")) {
const isHidden = "none" === settingsArea.style.display || "" === settingsArea.style.display;
settingsArea.style.display = isHidden ? "block" : "none";
}
}));
settingsArea.addEventListener("change", (event => {
if ("checkbox" === event.target.type) {
const siteId = event.target.getAttribute("data-site-id");
if (event.target.checked) {
$(`#${siteId}`).show();
const carNum2 = this.getPageInfo().carNum, config = this.siteConfigs.find((item => item.id === siteId));
this.handleSite(carNum2, config).then();
} else $(`#${siteId}`).hide();
const enabledSites = Array.from(settingsArea.querySelectorAll('input[type="checkbox"]:checked')).map((checkbox => checkbox.getAttribute("data-site-id")));
this.saveEnabledSites(enabledSites);
}
}));
}
}
export { OtherSitePlugin };
+221
View File
@@ -0,0 +1,221 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, currentHref, NO, YES, qualityOptions } from '../core/constants.js';
class PreviewVideoPlugin extends BasePlugin {
getName() {
return "PreviewVideoPlugin";
}
async initCss() {
return "\n .video-control-btn {\n min-width:120px;\n padding: 7px 12px;\n font-size: 12px;\n background: rgba(0,0,0,0.7);\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n .video-control-btn.active {\n background-color: #1890ff;\n color: white;\n font-weight: bold;\n border: 2px solid #096dd9;\n }\n ";
}
async handle() {
if (!window.isDetailPage) return;
let settingObj = await storageManager.getSetting();
this.filterHotKey = settingObj.filterHotKey;
this.favoriteHotKey = settingObj.favoriteHotKey;
this.speedVideoHotKey = settingObj.speedVideoHotKey;
let $preview = $(".preview-video-container");
$preview.on("click", (event => {
utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => {
this.handleVideo().then();
}));
}));
await storageManager.getSetting("enableLoadPreviewVideo", YES) !== YES || currentHref.includes("autoPlay=1") || this.initDmm().then();
let href = window.location.href;
(href.includes("gallery-1") || href.includes("gallery-2")) && utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => {
$(".fancybox-content #preview-video").length > 0 && this.handleVideo().then();
}));
href.includes("autoPlay=1") && $preview.length > 0 && $preview[0].click();
}
async initDmm() {
try {
const dmmVideoMap = await getDmmVideo(this.getPageInfo().carNum, !1);
if (!dmmVideoMap) return;
let settingDefaultVideoQuality = await storageManager.getSetting("videoQuality");
clog.debug("解析其它画质预览视频", "设置-期望画质", settingDefaultVideoQuality);
const defaultVideoUrl = dmmVideoMap[selectDefaultQuality(Object.keys(dmmVideoMap), settingDefaultVideoQuality)];
clog.log("切换其它画质预览视频: ", defaultVideoUrl);
const $previewVideoEL = $("#preview-video"), videoEl = $previewVideoEL.length ? $previewVideoEL[0] : null, isVideoElementHidden = !videoEl || utils.isHidden($previewVideoEL);
if ($previewVideoEL.length) {
if (videoEl) {
const currentTime = videoEl.currentTime;
$previewVideoEL.attr("src", defaultVideoUrl);
if (!isVideoElementHidden) {
clog.debug("播放器已手动打开, 变更进度条");
videoEl.currentTime = currentTime;
videoEl.play();
}
}
} else {
clog.debug("JavDB没有视频播放元素, 开始创建...");
const videoCoverSrc = $(".column-video-cover img").attr("src");
$(".preview-images").prepend(`\n <a class="preview-video-container" data-fancybox="gallery" href="#preview-video">\n <span>預告片</span>\n <img src="${videoCoverSrc}" class="video-cover" style="width: 150px; height: auto;" alt="">\n </a>\n `);
$(".preview-video-container").on("click", (event => {
utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (async () => {
await this.handleVideo();
}));
}));
}
} catch (error) {
clog.error("预加载dmm失败:", error);
}
}
async handleVideo() {
if (await storageManager.getSetting("enableLoadPreviewVideo", YES) === NO) return;
const $videoEl = $("#preview-video");
if (!$videoEl.length) return;
const $videoContainer = $videoEl.parent();
$videoContainer.css("position", "relative");
const videoEl = $videoEl[0], jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
jhs_videoMuted && (videoEl.muted = "yes" === jhs_videoMuted);
videoEl.addEventListener("volumechange", (function() {
localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no");
}));
videoEl.play();
let carNum2 = this.getPageInfo().carNum;
const dmmVideoMap = await getDmmVideo(carNum2);
let $bottomToolbar = $("<div></div>").attr("id", "video-bottom-toolbar").css({
display: "flex",
gap: "5px",
"align-items": "center",
"flex-wrap": "wrap"
}), $qualityButtonGroup = $("<div></div>").css({
display: "flex",
gap: "5px",
"align-items": "center"
}), defaultVideoQuality = null;
if (dmmVideoMap) {
let storedQuality = await storageManager.getSetting("videoQuality");
defaultVideoQuality = selectDefaultQuality(Object.keys(dmmVideoMap), storedQuality);
let defaultVideoUrl = dmmVideoMap[defaultVideoQuality];
if ($videoEl.attr("src") !== defaultVideoUrl) {
$videoEl.attr("src", defaultVideoUrl);
videoEl.load();
videoEl.play();
}
qualityOptions.forEach((option => {
let dmmVideoUrl = dmmVideoMap[option.quality];
if (dmmVideoUrl) {
const isActive = defaultVideoQuality === option.quality;
let qualityButton = $(`\n <button class="video-control-btn${isActive ? " active" : ""}" \n id="${option.id}" \n data-quality="${option.quality}"\n data-video-src="${dmmVideoUrl}"\n style="min-width: 40px; border: 1px solid #ccc; background-color: ${isActive ? "#007bff" : "#fff"}; color: ${isActive ? "white" : "black"};">\n ${option.text}\n </button>\n `);
$qualityButtonGroup.append(qualityButton);
}
}));
}
$bottomToolbar.append($qualityButtonGroup);
let $functionButtonGroup = $("<div></div>").css({
display: "flex",
gap: "5px",
"align-items": "center",
"margin-left": "auto"
}), filterButton = $(`<button class="menu-btn" id="video-filterBtn" style="min-width: 120px; background-color:#de3333;">屏蔽 ${this.filterHotKey ? "(" + this.filterHotKey + ")" : ""}</button>`);
$functionButtonGroup.append(filterButton);
let favoriteButton = $(`<button class="menu-btn" id="video-favoriteBtn" style="min-width: 120px; background-color:#25b1dc;">收藏 ${this.favoriteHotKey ? "(" + this.favoriteHotKey + ")" : ""}</button>`);
$functionButtonGroup.append(favoriteButton);
let speedButton = $(`<button class="menu-btn" id="speed-btn" style="min-width: 120px; background-color:#76b45d;">快进 ${this.speedVideoHotKey ? "(" + this.speedVideoHotKey + ")" : ""}</button>`);
$functionButtonGroup.append(speedButton);
$bottomToolbar.append($functionButtonGroup);
$videoContainer.append($bottomToolbar);
$bottomToolbar.on("click", ".video-control-btn", (async e => {
const $button = $(e.currentTarget), videoSrc = $button.data("video-src");
if (!$button.hasClass("active")) try {
const currentTime = videoEl.currentTime;
$videoEl.attr("src", videoSrc);
videoEl.load();
videoEl.currentTime = currentTime;
await videoEl.play();
$bottomToolbar.find(".video-control-btn").removeClass("active").css({
"background-color": "#fff",
color: "black"
});
$button.addClass("active").css({
"background-color": "#007bff",
color: "white"
});
} catch (error) {
console.error("切换画质失败:", error);
}
}));
$("#speed-btn").on("click", (() => {
this.getBean("DetailPageButtonPlugin").speedVideo();
}));
utils.rightClick(document.body, "#speed-btn", (event => {
this.getBean("DetailPageButtonPlugin").filterOne(event);
}));
$("#video-filterBtn").on("click", (event => {
this.getBean("DetailPageButtonPlugin").filterOne(event);
}));
$("#video-favoriteBtn").on("click", (event => {
this.getBean("DetailPageButtonPlugin").favoriteOne(event);
}));
}
}
const _HotkeyManager = class _HotkeyManager {
constructor() {
if (new.target === _HotkeyManager) throw new Error("HotkeyManager cannot be instantiated.");
}
static registerHotkey(hotkeyString, callback, keyupCallback = null) {
if (Array.isArray(hotkeyString)) {
let id_list = [];
hotkeyString.forEach((hotkey => {
if (!this.isHotkeyFormat(hotkey)) throw new Error("快捷键格式错误");
let id = this.recordHotkey(hotkey, callback, keyupCallback);
id_list.push(id);
}));
return id_list;
}
if (!this.isHotkeyFormat(hotkeyString)) throw new Error("快捷键格式错误");
return this.recordHotkey(hotkeyString, callback, keyupCallback);
}
static recordHotkey(hotkeyString, callback, keyupCallback) {
let id = Math.random().toString(36).substr(2);
this.registerHotKeyMap.set(id, {
hotkeyString: hotkeyString,
callback: callback,
keyupCallback: keyupCallback
});
return id;
}
static unregisterHotkey(id) {
this.registerHotKeyMap.has(id) && this.registerHotKeyMap.delete(id);
}
static isHotkeyFormat(hotkeyString) {
return hotkeyString.toLowerCase().split("+").map((k => k.trim())).every((k => [ "ctrl", "shift", "alt" ].includes(k) || 1 === k.length));
}
static judgeHotkey(hotkeyString, event) {
const keyList = hotkeyString.toLowerCase().split("+").map((k => k.trim())), ctrl = keyList.includes("ctrl"), shift = keyList.includes("shift"), alt = keyList.includes("alt"), key = keyList.find((k => "ctrl" !== k && "shift" !== k && "alt" !== k));
return (this.isMac ? event.metaKey : event.ctrlKey) === ctrl && event.shiftKey === shift && event.altKey === alt && event.key.toLowerCase() === key;
}
};
__publicField(_HotkeyManager, "isMac", 0 === navigator.platform.indexOf("Mac"));
__publicField(_HotkeyManager, "registerHotKeyMap", new Map);
__publicField(_HotkeyManager, "handleKeydown", (event => {
for (const [id, data] of _HotkeyManager.registerHotKeyMap) {
let hotkeyString = data.hotkeyString, callback = data.callback;
_HotkeyManager.judgeHotkey(hotkeyString, event) && callback(event);
}
}));
__publicField(_HotkeyManager, "handleKeyup", (event => {
for (const [id, data] of _HotkeyManager.registerHotKeyMap) {
let hotkeyString = data.hotkeyString, keyupCallback = data.keyupCallback;
keyupCallback && (_HotkeyManager.judgeHotkey(hotkeyString, event) && keyupCallback(event));
}
}));
let HotkeyManager = _HotkeyManager;
document.addEventListener("keydown", (event => {
HotkeyManager.handleKeydown(event);
}));
document.addEventListener("keyup", (event => {
HotkeyManager.handleKeyup(event);
}));
export { PreviewVideoPlugin, HotkeyManager };
+96
View File
@@ -0,0 +1,96 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, NO, YES } from '../core/constants.js';
import { javDbApi } from '../api/javdb.js';
class RelatedPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "floorIndex", 1);
__publicField(this, "isInit", !1);
}
getName() {
return "RelatedPlugin";
}
async showRelated($eleBox, movieId) {
const enableLoadRelated = await storageManager.getSetting("enableLoadRelated", NO), $magnets = $eleBox;
if (movieId) {
$magnets.append(`\n <div style="display: flex; align-items: center; margin: 16px 0; color: #666; font-size: 14px;">\n <span style="flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent);"></span>\n <span style="padding: 0 10px;">相关清单</span>\n <a id="relatedFold" style="margin-left: 8px; color: #1890ff; text-decoration: none; display: flex; align-items: center;">\n <span class="toggle-text">${enableLoadRelated === YES ? "折叠" : "展开"}</span>\n <span class="toggle-icon" style="margin-left: 4px;">${enableLoadRelated === YES ? "▲" : "▼"}</span>\n </a>\n <span style="flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent);"></span>\n </div>\n `);
$("#relatedFold").on("click", (event => {
event.preventDefault();
event.stopPropagation();
const $text = $("#relatedFold .toggle-text"), $icon = $("#relatedFold .toggle-icon"), isFolded = "展开" === $text.text();
$text.text(isFolded ? "折叠" : "展开");
$icon.text(isFolded ? "▲" : "▼");
if (isFolded) {
$("#relatedContainer").show();
$("#relatedFooter").show();
if (!this.isInit) {
this.fetchAndDisplayRelateds(movieId);
this.isInit = !0;
}
storageManager.saveSettingItem("enableLoadRelated", YES);
} else {
$("#relatedContainer").hide();
$("#relatedFooter").hide();
storageManager.saveSettingItem("enableLoadRelated", NO);
}
}));
$magnets.append('<div id="relatedContainer"></div>');
$magnets.append('<div id="relatedFooter"></div>');
enableLoadRelated === YES && await this.fetchAndDisplayRelateds(movieId);
} else show.error("未传入movieId");
}
async fetchAndDisplayRelateds(movieId) {
const $relatedContainer = $("#relatedContainer"), $relatedFooter = $("#relatedFooter");
$relatedContainer.append('<div id="relatedLoading" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取清单中...</div>');
let dataList = null;
try {
dataList = await javDbApi.related(movieId, 1, 20);
} catch (e) {
console.error("获取清单失败:", e);
} finally {
$("#relatedLoading").remove();
}
if (dataList) if (0 !== dataList.length) {
this.displayRelateds(dataList, $relatedContainer);
if (20 === dataList.length) {
$relatedFooter.html('\n <button id="loadMoreRelateds" style="width:100%; background-color: #e1f5fe; border:none; padding:10px; margin-top:10px; cursor:pointer; color:#0277bd; font-weight:bold; border-radius:4px;">\n 加载更多清单\n </button>\n <div id="relatedEnd" style="display:none; text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部清单</div>\n ');
let currentPage = 1, $loadMoreRelateds = $("#loadMoreRelateds");
$loadMoreRelateds.on("click", (async () => {
$loadMoreRelateds.text("加载中...").prop("disabled", !0);
currentPage++;
let moreData;
try {
moreData = await javDbApi.related(movieId, currentPage, 20);
} catch (e) {
console.error("加载更多清单失败:", e);
} finally {
$loadMoreRelateds.text("加载失败, 请点击重试").prop("disabled", !1);
}
if (moreData) {
this.displayRelateds(moreData, $relatedContainer);
if (moreData.length < 20) {
$loadMoreRelateds.remove();
$("#relatedEnd").show();
} else $loadMoreRelateds.text("加载更多清单").prop("disabled", !1);
}
}));
} else $relatedFooter.html('<div style="text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部清单</div>');
} else $relatedContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无清单</div>'); else {
$relatedContainer.append('\n <div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">\n 获取清单失败\n <a id="retryFetchRelateds" href="javascript:;" style="margin-left: 10px; color: #1890ff; text-decoration: none;">重试</a>\n </div>\n ');
$("#retryFetchRelateds").on("click", (async () => {
$("#retryFetchRelateds").parent().remove();
await this.fetchAndDisplayRelateds(movieId);
}));
}
}
displayRelateds(dataList, $container) {
dataList.length && dataList.forEach((item => {
let commentHtml = `\n <div class="item columns is-desktop" style="display:block;margin-top:6px;background-color:#ffffff;padding:10px;margin-left: -10px;word-break: break-word;position:relative;">\n <span style="position:absolute;top:5px;right:10px;color:#999;font-size:12px;">#${this.floorIndex++}</span>\n <span style="position:absolute;bottom:5px;right:10px;color:#999;font-size:12px;">创建时间: ${item.createTime}</span>\n <p><a href="/lists/${item.relatedId}" target="_blank" style="color:#2e8abb">${item.name}</a></p>\n <p style="margin-top: 5px;">视频个数: ${item.movieCount}</p>\n <p style="margin-top: 5px;">收藏次数: ${item.collectionCount} 被查看次数: ${item.viewCount}</p>\n </div>\n `;
$container.append(commentHtml);
}));
}
}
export { RelatedPlugin };
+153
View File
@@ -0,0 +1,153 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, NO, YES } from '../core/constants.js';
import { javDbApi } from '../api/javdb.js';
class ReviewPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "floorIndex", 1);
__publicField(this, "isInit", !1);
}
getName() {
return "ReviewPlugin";
}
async handle() {
$(document).on("click", ".down-115", (async event => {
const magnet = $(event.currentTarget).data("magnet");
let loadObj = loading();
try {
await this.getBean("WangPan115TaskPlugin").handleAddTask(magnet);
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
}));
if (window.isDetailPage) {
if (isJavDb) {
const movieId = this.parseMovieId(window.location.href);
await this.showReview(movieId);
await this.getBean("RelatedPlugin").showRelated($("#magnets-content"), movieId);
}
if (isJavBus) {
let carNum2 = this.getPageInfo().carNum;
const movies = await javDbApi.searchMovie(carNum2);
let movieId = null;
for (let i = 0; i < movies.length; i++) {
let item = movies[i];
if (item.number.toLowerCase() === carNum2.toLowerCase()) {
movieId = item.id;
break;
}
}
if (!movieId) return;
this.showReview(movieId, $("#sample-waterfall")).then();
}
}
}
async showReview(movieId, $eleBox) {
const enableLoadReview = await storageManager.getSetting("enableLoadReview", YES), $magnets = $eleBox || $("#magnets-content");
$magnets.append(`\n <div style="display: flex; align-items: center; margin: 16px 0; color: #666; font-size: 14px;">\n <span style="flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent);"></span>\n <span style="padding: 0 10px;" data-tip="想要发表评论? 滑上去, 点击上面的按钮-看过">❓ 评论区</span>\n <a id="reviewsFold" style="margin-left: 8px; color: #1890ff; text-decoration: none; display: flex; align-items: center;">\n <span class="toggle-text">${enableLoadReview === YES ? "折叠" : "展开"}</span>\n <span class="toggle-icon" style="margin-left: 4px;">${enableLoadReview === YES ? "▲" : "▼"}</span>\n </a>\n <span style="flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent);"></span>\n </div>\n `);
$("#reviewsFold").on("click", (event => {
event.preventDefault();
event.stopPropagation();
const $text = $("#reviewsFold .toggle-text"), $icon = $("#reviewsFold .toggle-icon"), isFolded = "展开" === $text.text();
$text.text(isFolded ? "折叠" : "展开");
$icon.text(isFolded ? "▲" : "▼");
if (isFolded) {
$("#reviewsContainer").show();
$("#reviewsFooter").show();
if (!this.isInit) {
this.fetchAndDisplayReviews(movieId);
this.isInit = !0;
}
storageManager.saveSettingItem("enableLoadReview", YES);
} else {
$("#reviewsContainer").hide();
$("#reviewsFooter").hide();
storageManager.saveSettingItem("enableLoadReview", NO);
}
}));
$magnets.append('<div id="reviewsContainer"></div>');
$magnets.append('<div id="reviewsFooter"></div>');
enableLoadReview === YES && await this.fetchAndDisplayReviews(movieId);
}
async fetchAndDisplayReviews(movieId) {
const $reviewsContainer = $("#reviewsContainer"), $reviewsFooter = $("#reviewsFooter");
$reviewsContainer.append('<div id="reviewsLoading" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取评论中...</div>');
const reviewCount = await storageManager.getSetting("reviewCount", 20);
let dataList = null;
try {
dataList = await javDbApi.getReviews(movieId, 1, reviewCount);
} catch (e) {
e.toString().includes("簽名已過期") && show.error("生成签名失败, 请检查系统时间及时区是否正确!");
clog.error("获取评论失败:", e);
console.error("获取评论失败:", e);
} finally {
$("#reviewsLoading").remove();
}
if (!dataList) {
$reviewsContainer.append('\n <div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">\n 获取评论失败\n <a id="retryFetchReviews" href="javascript:;" style="margin-left: 10px; color: #1890ff; text-decoration: none;">重试</a>\n </div>\n ');
$("#retryFetchReviews").on("click", (async () => {
$("#retryFetchReviews").parent().remove();
await this.fetchAndDisplayReviews(movieId);
}));
return;
}
if (0 === dataList.length) {
$reviewsContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无评论</div>');
return;
}
const reviewKeywordList = await storageManager.getReviewFilterKeywordList();
this.displayReviews(dataList, $reviewsContainer, reviewKeywordList);
if (dataList.length === reviewCount) {
$reviewsFooter.html('\n <button id="loadMoreReviews" style="width:100%; background-color: #e1f5fe; border:none; padding:10px; margin-top:10px; cursor:pointer; color:#0277bd; font-weight:bold; border-radius:4px;">\n 加载更多评论\n </button>\n <div id="reviewsEnd" style="display:none; text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部评论</div>\n ');
let currentPage = 1, $loadMoreReviews = $("#loadMoreReviews");
$loadMoreReviews.on("click", (async () => {
$loadMoreReviews.text("加载中...").prop("disabled", !0);
currentPage++;
let moreData;
try {
moreData = await javDbApi.getReviews(movieId, currentPage, reviewCount);
} catch (e) {
console.error("加载更多评论失败:", e);
} finally {
$loadMoreReviews.text("加载失败, 请点击重试").prop("disabled", !1);
}
if (moreData) {
this.displayReviews(moreData, $reviewsContainer, reviewKeywordList);
if (moreData.length < reviewCount) {
$loadMoreReviews.remove();
$("#reviewsEnd").show();
} else $loadMoreReviews.text("加载更多评论").prop("disabled", !1);
}
}));
} else $reviewsFooter.html('<div style="text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部评论</div>');
}
displayReviews(dataList, $container, reviewKeywordList) {
if (dataList.length) {
dataList.forEach((item => {
if (reviewKeywordList.some((keyword => item.content.includes(keyword)))) return;
const starsHtml = Array(item.score).fill('<i class="icon-star"></i>').join(""), content = item.content.replace(/ed2k:\/\/\|file\|[^|]+\|\d+\|[a-fA-F0-9]{32}\|\/|magnet:\?[^\s"'<>`\u4e00-\u9fa5,。?!()【】]+|https?:\/\/[^\s"'<>`\u4e00-\u9fa5,。?!()【】]+/g, (match => match.startsWith("ed2k://") ? `\n <span style="word-break: break-all;background: #e0f2fe;color: #0369a1;">${match}</span>\n <button class="button is-info down-115" data-magnet="${match}" style="font-size: 11px">115离线下载</button>\n ` : match.startsWith("magnet:") ? `\n <a href="${match}" class="a-primary" style="padding:0; word-break: break-all; white-space: pre-wrap;" target="_blank" rel="noopener noreferrer">${match}</a>\n <button class="button is-info down-115" data-magnet="${match}" style="font-size: 11px">115离线下载</button>\n ` : match.startsWith("http://") || match.startsWith("https://") ? `\n <a href="${match}" class="a-primary" style="padding:0; word-break: break-all; white-space: pre-wrap;" target="_blank" rel="noopener noreferrer">${match}</a>\n ` : match)), commentHtml = `\n <div class="item columns is-desktop" style="display:block;margin-top:6px;background-color:#ffffff;padding:10px;margin-left: -10px;word-break: break-word;position:relative;">\n <span style="position:absolute;top:5px;right:10px;color:#999;font-size:12px;">#${this.floorIndex++}楼</span>\n ${item.username} &nbsp;&nbsp; <span class="score-stars">${starsHtml}</span> \n <span class="time">${utils.formatDate(item.created_at)}</span> \n &nbsp;&nbsp; 点赞:${item.likes_count}\n <p class="review-content" style="margin-top: 5px;"> ${content} </p>\n </div>\n `;
$container.append(commentHtml);
}));
this.rightClickFilter();
}
}
async rightClickFilter() {
await storageManager.getSetting("enableTitleSelectFilter", YES) === YES && utils.rightClick(document.body, ".review-content", (async event => {
const selectedText = window.getSelection().toString();
if (selectedText) {
event.preventDefault();
await utils.q(event, `是否将 '${selectedText}' 加入评论区关键词?`, (async () => {
await storageManager.saveReviewFilterKeyword(selectedText);
show.ok("操作成功, 刷新页面后生效");
}));
}
}));
}
}
export { ReviewPlugin };
+153
View File
@@ -0,0 +1,153 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb } from '../core/constants.js';
import { searchFiles } from './wangpan-115-task.js';
class ScreenShotPlugin extends BasePlugin {
getName() {
return "ScreenShotPlugin";
}
async handle() {
this.loadScreenShot().then();
}
async loadScreenShot() {
if (!window.isDetailPage) return;
if ("yes" !== await storageManager.getSetting("enableLoadScreenShot", "yes")) return;
let carNum2 = this.getPageInfo().carNum;
isJavDb && $(".preview-images .tile-item").first().before(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 215px;text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
isJavBus && $("#sample-waterfall .sample-box:first").after(' <a class="sample-box screen-container" style="overflow:hidden; height: 110px; text-align:center;"><div style="margin-top: 30px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
try {
const imgUrl = await this.getScreenshot(carNum2);
this.addImg("缩略图", imgUrl);
clog.log("加载缩略图:", imgUrl);
} catch (e) {
this.showErrorFallback(carNum2, e);
}
}
async getScreenshot(carNum2) {
const cacheData = localStorage.getItem("jhs_screenShot") ? JSON.parse(localStorage.getItem("jhs_screenShot")) : {};
if (cacheData[carNum2]) {
clog.debug("缓存中存在缩略图:", carNum2, cacheData[carNum2]);
return cacheData[carNum2];
}
let imgUrl;
try {
imgUrl = await Promise.any([ this.getJavStoreScreenShot(carNum2) ]);
} catch (e) {
clog.error("获取缩略图资源失败:", imgUrl, e);
throw e;
}
if (!imgUrl) {
this.showErrorFallback(carNum2, null);
return null;
}
const httpsIndex = imgUrl.indexOf("https://");
-1 !== httpsIndex && (imgUrl = imgUrl.substring(httpsIndex));
cacheData[carNum2] = imgUrl;
clog.log("缩略图获取成功:", imgUrl);
localStorage.setItem("jhs_screenShot", JSON.stringify(cacheData));
return imgUrl;
}
async getJavStoreScreenShot(carNum2) {
let url = `https://javstore.net/search/${carNum2}.html`;
clog.log("正在解析缩略图:", url);
let html = await gmHttp.get(url);
const $dom = utils.htmlTo$dom(html);
let detailPageUrl = null;
$dom.find("#content_news h3 span a").each((function() {
if ($(this).attr("title").toLowerCase().includes(carNum2.toLowerCase())) {
detailPageUrl = $(this).attr("href");
return !1;
}
}));
if (!detailPageUrl) {
clog.error("JavStore, 查询番号失败:", url);
return null;
}
let detailPageHtml = await gmHttp.get(detailPageUrl);
const $detailPageDom = utils.htmlTo$dom(detailPageHtml);
let imgUrl = $detailPageDom.find("a:contains('CLICK HERE')").attr("href") || $detailPageDom.find("img[src*='_s.jpg']").attr("src");
if (!imgUrl) {
clog.error("JavStore, 解析预览图失败:", url);
return null;
}
return imgUrl.replace(".th", "");
}
async getJavBestScreenShot(carNum2) {
let url = `https://javbest.net/?s=${carNum2}`;
clog.log("正在解析缩略图:", url);
let html = await gmHttp.get(url);
const $dom = utils.htmlTo$dom(html), href = $dom.find(".app_loop_thumb a").first().attr("href");
if (!href) {
clog.error("解析JavBest搜索页失败:", url);
throw new Error("解析JavBest搜索页失败");
}
const title = $dom.find(".app_loop_thumb a").first().attr("title");
if (!title.toLowerCase().includes(carNum2.toLowerCase())) {
clog.error("解析JavBest搜索页失败:", title);
throw new Error("解析JavBest搜索页失败");
}
const detailPageHtml = await gmHttp.get(href);
let imgUrl = $(detailPageHtml).find('#content a img[src*="_t.jpg"]').attr("src");
if (!imgUrl) {
clog.error("解析JavBest缩略图失败:", url);
throw new Error("解析JavBest缩略图失败");
}
imgUrl = imgUrl.replace("_t", "").replace("http:", "https:");
return imgUrl;
}
async getJavFreeScreenShot(carNum2) {
let url = `https://javfree.me/search/${carNum2}/`, html = await gmHttp.get(url);
const $itemList = utils.htmlTo$dom(html).find("article h2.entry-title a");
if (!$itemList || 0 === $itemList.length) {
clog.error("解析JavFree搜索页失败:", url);
throw new Error("解析JavFree搜索页失败");
}
let detailPageUrl = $($itemList[0]).attr("href"), detailPageHtml = await gmHttp.get(detailPageUrl);
const $imgList = utils.htmlTo$dom(detailPageHtml).find("#main > article > .entry-content > p img");
if (!$imgList || 0 === $imgList.length) {
clog.error("解析JavFree详情页失败:", detailPageUrl);
throw new Error("解析JavFree详情页失败");
}
const srcList = $imgList.filter((function() {
const src = $(this).attr("src");
return src && src.toLowerCase().endsWith(".jpeg");
})).map((function() {
return $(this).attr("src");
})).get();
console.log(srcList);
return srcList.at(-1);
}
addImg(title, imgUrl) {
if (imgUrl) {
isJavDb && $(".screen-container").html(`<img src="${imgUrl}" alt="${title}" loading="lazy" style="width: 100%;">`);
isJavBus && $(".screen-container").html(`<div class="photo-frame"><img src="${imgUrl}" style="height: inherit;width: 100%;" title="${title}" alt="${title}"></div>`);
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
showImageViewer(event.currentTarget);
}));
}
}
showErrorFallback(carNum2, error) {
var _a2;
console.error("获取缩略图失败:", null == (_a2 = null == error ? void 0 : error.message) ? void 0 : _a2.substring(0, 100));
let differentCss = isJavBus ? "margin-top: 30px" : "margin-top: 50px";
$(".screen-container").html(`<div style="${differentCss}; cursor:auto;color:#000;">获取缩略图失败</div><br/><a href='#' class='retry-link'>点击重试</a> 或 <a class="check-link" href='https://javstore.net/search/${carNum2}.html' target='_blank'>前往确认</a>`).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", (async e => {
e.stopPropagation();
e.preventDefault();
$(".screen-container").html(`<div style="${differentCss};cursor:auto;color:#000;">正在重新加载...</div>`);
try {
const imgUrl = await this.getScreenshot(carNum2);
this.addImg("缩略图", imgUrl);
} catch (err) {
this.showErrorFallback(carNum2, err);
}
})).on("click", ".check-link", (async e => {
e.stopPropagation();
e.preventDefault();
window.open(`https://javstore.net/search/${carNum2}.html`, "_blank");
}));
}
}
export { ScreenShotPlugin };
File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
class SubTitleCatPlugin extends BasePlugin {
getName() {
return "SubTitleCatPlugin";
}
handle() {
$(".t-banner-inner").hide();
$("#navbar").hide();
let keyword = new URLSearchParams(window.location.search).get("search").toLowerCase(), findList = $(".sub-table tr td a").toArray(), visibleCount = 0;
findList.forEach((el => {
let item = $(el);
item.text().toLowerCase().includes(keyword) ? visibleCount++ : item.parent().parent().hide();
}));
0 === visibleCount && show.error("该番号无字幕!");
const $secTitle = $(".sec-title"), newHTML = $secTitle.html().replace(/^\d+/, visibleCount);
$secTitle.html(newHTML);
}
}
export { SubTitleCatPlugin };
+434
View File
@@ -0,0 +1,434 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, YES } from '../core/constants.js';
import { AsyncQueue } from '../core/utils.js';
class TaskPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "singleTaskKey", "checkNewActressActorFilterCar");
__publicField(this, "taskConfig", null);
__publicField(this, "storageQueue", new AsyncQueue);
__publicField(this, "lastCheckFavoriteActressTimeKey", "jhs_time_checkFavoriteActress");
__publicField(this, "lastCheckBlacklistTimeKey", "jhs_time_checkBlacklist");
__publicField(this, "lastCheckNewVideoTimeKey", "jhs_time_checkNewVideo");
}
getName() {
return "TaskPlugin";
}
async limitConcurrency(taskParamList, checkConcurrencyCount, checkRequestSleep, taskFn) {
this.showIsRun();
const activePromises = [], totalItems = taskParamList.length;
let processedCount = 0;
for (const taskParam of taskParamList) {
const p = taskFn(taskParam).finally((() => {
activePromises.splice(activePromises.indexOf(p), 1);
}));
activePromises.push(p);
processedCount++;
if (activePromises.length >= checkConcurrencyCount) {
const remainingToStart = totalItems - processedCount;
clog.debug(`剩余任务数: <span style="color: #f40">${remainingToStart}</span>`);
await Promise.race(activePromises);
await utils.sleep(checkRequestSleep);
}
}
await Promise.all(activePromises);
}
isUnnecessaryCheck(lastCheckTimeStr, checkIntervalTime) {
if (!checkIntervalTime) throw new Error("未传入checkIntervalTime");
checkIntervalTime = parseInt(checkIntervalTime);
return utils.getHourDifference(new Date(lastCheckTimeStr), new Date) < checkIntervalTime;
}
handle() {
this.doTask().then();
}
showIsRun() {
show.info("正在执行检测任务中, 请勿关闭当前窗口", {
duration: 3e3
});
}
async doTask() {
if (window.isListPage) {
await this.loadConfig();
this.javDbUrl = await this.getBean("OtherSitePlugin").getJavDbUrl();
return navigator.locks.request(this.singleTaskKey, {
ifAvailable: !0
}, (async lock => {
if (lock) {
if (window.isListPage) {
this.taskConfig.enableCheckBlacklist === YES ? await this.checkBlacklist() : clog.warn("自动检测屏蔽黑名单-禁用");
if (!isJavBus) {
if (this.taskConfig.enableCheckFavoriteActress === YES) {
const lastCheckFavoriteActressTimeStr = localStorage.getItem(this.lastCheckFavoriteActressTimeKey), checkFavoriteActress_IntervalTime = this.taskConfig.checkFavoriteActress_IntervalTime, isUnCheck = lastCheckFavoriteActressTimeStr && this.isUnnecessaryCheck(lastCheckFavoriteActressTimeStr, checkFavoriteActress_IntervalTime), isLogin = $('a[href*="/users/profile"]').length > 0;
isUnCheck && clog.debug(`检测同步演员, 上次检测时间: ${lastCheckFavoriteActressTimeStr} 检测间隔时间: ${checkFavoriteActress_IntervalTime}小时 未到时间`);
!isUnCheck && isLogin && await this.checkFavoriteActress();
} else clog.warn("自动同步已收藏的演员-禁用");
this.taskConfig.enableCheckNewVideo === YES ? await this.checkNewVideo() : clog.warn("自动检测已收藏演员的最新作品-禁用");
}
}
} else clog.debug("争夺任务锁失败, 跳过执行");
})).catch((error => {
console.error("锁任务出现错误:", error);
clog.error("锁任务出现错误:", error);
})).finally((() => {
setTimeout((() => {
this.doTask();
}), 3e5);
}));
}
}
async loadConfig() {
const settingObj = await storageManager.getSetting();
this.taskConfig = {
checkConcurrencyCount: settingObj.checkConcurrencyCount ? Number(settingObj.checkConcurrencyCount) : 2,
checkRequestSleep: settingObj.checkRequestSleep ? Number(settingObj.checkRequestSleep) : 100,
enableCheckBlacklist: settingObj.enableCheckBlacklist || YES,
checkBlacklist_intervalTime: settingObj.checkBlacklist_intervalTime ? Number(settingObj.checkBlacklist_intervalTime) : 12,
checkBlacklist_ruleTime: settingObj.checkBlacklist_ruleTime ? Number(settingObj.checkBlacklist_ruleTime) : 8760,
enableCheckFavoriteActress: settingObj.enableCheckFavoriteActress || YES,
checkFavoriteActress_IntervalTime: settingObj.checkFavoriteActress_IntervalTime ? Number(settingObj.checkFavoriteActress_IntervalTime) : 24,
enableCheckNewVideo: settingObj.enableCheckNewVideo || YES,
checkNewVideo_intervalTime: settingObj.checkNewVideo_intervalTime ? Number(settingObj.checkNewVideo_intervalTime) : 12,
checkNewVideo_ruleTime: settingObj.checkNewVideo_ruleTime ? Number(settingObj.checkNewVideo_ruleTime) : 8760
};
}
async checkBlacklist(isManual) {
let blacklist = await storageManager.getBlacklist();
if (0 === blacklist.length) return;
blacklist = blacklist.sort(((a, b) => a.createTime < b.createTime ? 1 : a.createTime > b.createTime ? -1 : 0));
const checkConcurrencyCount = this.taskConfig.checkConcurrencyCount, checkRequestSleep = this.taskConfig.checkRequestSleep, checkBlacklist_intervalTime = this.taskConfig.checkBlacklist_intervalTime, checkBlacklist_ruleTime = this.taskConfig.checkBlacklist_ruleTime, lastCheckBlacklistTimeStr = localStorage.getItem(this.lastCheckBlacklistTimeKey);
if (!isManual && lastCheckBlacklistTimeStr && this.isUnnecessaryCheck(lastCheckBlacklistTimeStr, checkBlacklist_intervalTime)) {
clog.debug(`检测黑名单, 上次检测时间: ${lastCheckBlacklistTimeStr} 检测间隔时间: ${checkBlacklist_intervalTime}小时 未到时间`);
return;
}
const taskParamList = [], msgList = [];
for (const blacklistItem of blacklist) {
let name2 = blacklistItem.name, checkTime = blacklistItem.checkTime, lastPublishTime = blacklistItem.lastPublishTime, url = blacklistItem.url;
if (new URL(window.location.href).hostname === new URL(url).hostname) {
if (isManual || !checkTime || !this.isUnnecessaryCheck(checkTime, checkBlacklist_intervalTime)) if (!lastPublishTime || 0 === checkBlacklist_ruleTime || this.isUnnecessaryCheck(lastPublishTime, checkBlacklist_ruleTime)) taskParamList.push(blacklistItem); else {
let msg = `检测黑名单: ${name2} ${lastPublishTime} 停更超过${checkBlacklist_ruleTime / 24 / 365}年,跳过检测`;
msgList.push(msg);
$("#checkBlacklistMsg").text(msg);
}
} else clog.log("黑名单地址非同域名,跳过", url);
}
if (0 === taskParamList.length) return;
msgList.forEach((msg => {
clog.log(msg);
}));
clog.log(`<span style='color: #f40'>检测屏蔽黑名单, 总任务数: ${taskParamList.length}, 并发限制:${checkConcurrencyCount}, 请求间隔时间:${checkRequestSleep}ms</span>`);
const blacklistPlugin = this.getBean("BlacklistPlugin");
await this.limitConcurrency(taskParamList, checkConcurrencyCount, checkRequestSleep, (async task => {
let {starId: starId, name: name2, url: url} = task;
try {
clog.log("正在检屏黑名单演员:", name2, url);
$("#checkBlacklistMsg").text(`正在检屏黑名单演员: ${name2} ${url}`);
const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html);
this.storageQueue.addTask((async () => {
let {lastPublishTime: lastPublishTime} = await blacklistPlugin.parseAndSaveFilterInfo($dom, name2, starId);
await storageManager.updateBlacklistItem({
starId: starId,
name: name2,
checkTime: utils.getNowStr(),
lastPublishTime: lastPublishTime
});
}));
} catch (e) {
$("#checkBlacklistMsg").text(`检测屏蔽演员信息, 发生错误: ${url}`);
clog.error("检测屏蔽演员信息, 发生错误:", url, e);
show.error("检测屏蔽演员信息, 发生错误:" + e, "bottom", "right");
}
}));
await this.storageQueue.waitAllFinished();
const updateLastCheckBlacklistTimeStr = utils.getNowStr();
localStorage.setItem(this.lastCheckBlacklistTimeKey, updateLastCheckBlacklistTimeStr);
clog.log('<span style="color: #f40">-------- END 检测屏蔽黑名单 END --------</span>');
$("#checkBlacklistMsg").text("检测屏蔽黑名单, 结束");
this.getBean("BlacklistPlugin").resetBtnTip().then();
}
async checkFavoriteActress() {
const checkUrl = `${this.javDbUrl}/users/collection_actors`, actorInfoList = [];
await this.scrapeActorInfo(checkUrl, actorInfoList);
clog.log("所有演员信息已收集, 总计数量:", actorInfoList.length);
$("#checkNewVideoMsg").text("同步完成");
if (actorInfoList.length > 0) {
await storageManager.addFavoriteActressList(actorInfoList);
localStorage.setItem(this.lastCheckFavoriteActressTimeKey, utils.getNowStr());
this.getBean("NewVideoPlugin").resetBtnTip().then();
}
}
async scrapeActorInfo(currentUrl, actorInfoList) {
clog.log(`正在抓取页面: ${currentUrl}`);
$("#checkNewVideoMsg").text(`正在解析已收藏的演员: ${currentUrl}`);
try {
const html = await gmHttp.get(currentUrl), $dom = utils.htmlTo$dom(html);
$dom.find("#actors .actor-box a").each(((i, element) => {
const $a = $(element), title = $a.attr("title"), href = $a.attr("href");
if (title && href) {
const allName = title.split(",").map((name3 => name3.trim())).filter((name3 => name3.length > 0)), name2 = allName[0] || "", segments = new URL(href, this.javDbUrl).pathname.split("/").filter((s => s.length > 0));
let starId = "";
segments.length > 0 && (starId = segments[segments.length - 1]);
let actressType = "censored";
const avatar = $a.find("img").attr("src"), $infoSpan = $a.find(".info");
$infoSpan.length && $infoSpan.text().trim().includes("無碼") && (actressType = "uncensored");
actorInfoList.push({
starId: starId,
name: name2,
allName: allName,
avatar: avatar,
actressType: actressType,
lastCheckTime: null,
lastUpdateTime: null
});
}
}));
const nextRelativeUrl = $dom.find(".pagination-next").attr("href");
if (nextRelativeUrl) {
const nextAbsoluteUrl = new URL(nextRelativeUrl, this.javDbUrl).href;
await this.scrapeActorInfo(nextAbsoluteUrl, actorInfoList);
}
} catch (error) {
clog.error(`抓取 ${currentUrl} 时发生错误:`, error);
}
}
async checkNewVideo(isManual) {
const dataActressInfoList = await storageManager.getFavoriteActressList(), actressInfoList = utils.genericSort(dataActressInfoList, [ {
key: item => {
var _a2;
return (null == (_a2 = item.newVideoList) ? void 0 : _a2.length) ?? 0;
},
order: "desc"
}, {
key: "lastPublishTime",
order: "desc"
} ]), checkConcurrencyCount = this.taskConfig.checkConcurrencyCount, checkRequestSleep = this.taskConfig.checkRequestSleep, checkNewVideo_intervalTime = this.taskConfig.checkNewVideo_intervalTime, checkNewVideo_ruleTime = this.taskConfig.checkNewVideo_ruleTime, lastCheckNewVideoTimeStr = localStorage.getItem(this.lastCheckNewVideoTimeKey);
if (!isManual && lastCheckNewVideoTimeStr && this.isUnnecessaryCheck(lastCheckNewVideoTimeStr, checkNewVideo_intervalTime)) {
clog.debug(`检测新作品, 上次检测时间: ${lastCheckNewVideoTimeStr} 检测间隔时间: ${checkNewVideo_intervalTime}小时 未到时间`);
return;
}
const taskParamList = [], msgList = [];
for (const actress of actressInfoList) {
const {lastCheckTime: lastCheckTime, lastPublishTime: lastPublishTime, name: name2} = actress;
!isManual && lastCheckTime && this.isUnnecessaryCheck(lastCheckTime, checkNewVideo_intervalTime) || (!lastPublishTime || 0 === checkNewVideo_ruleTime || this.isUnnecessaryCheck(lastPublishTime, checkNewVideo_ruleTime) ? taskParamList.push(actress) : msgList.push(`检测新作品: ${name2} ${lastPublishTime} 停更超过${checkNewVideo_ruleTime / 24 / 365}年,跳过检测`));
}
if (0 === taskParamList.length) return;
msgList.forEach((msg => {
clog.log(msg);
}));
clog.log(`<span style='color: #f40'>检测最新作品, 总任务数: ${taskParamList.length}, 并发限制:${checkConcurrencyCount}, 请求间隔时间:${checkRequestSleep}ms</span>`);
const filterKeywordList = await storageManager.getTitleFilterKeyword(), filterActorActressCarList = await storageManager.getBlacklistCarList(), filterActorActressCarNumList = new Set(filterActorActressCarList.map((car => car.carNum)));
await this.limitConcurrency(taskParamList, checkConcurrencyCount, checkRequestSleep, (async task => {
const {lastCheckTime: lastCheckTime, name: name2, starId: starId} = task;
let url = `${this.javDbUrl}/actors/${starId}?t=d`;
try {
clog.log("正在检测最新作品, 演员:", name2, url);
$("#checkNewVideoMsg").text(`正在检测最新作品, 演员: ${name2}`);
const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html);
this.storageQueue.addTask((async () => {
await this.parsePage($dom, starId, name2, filterKeywordList, filterActorActressCarNumList);
}));
} catch (e) {
clog.error("检测屏蔽演员信息, 发生错误:", url, e);
console.error("检测屏蔽演员信息, 发生错误:", url, e);
show.error("检测屏蔽演员信息, 发生错误:" + e, "bottom", "right");
}
}));
await this.storageQueue.waitAllFinished();
localStorage.setItem(this.lastCheckNewVideoTimeKey, utils.getNowStr());
clog.log('<span style="color: #f40">检测最新作品---结束</span>');
$("#checkNewVideoMsg").text("检测完毕");
const newVideoPlugin = this.getBean("NewVideoPlugin");
newVideoPlugin.loadData();
newVideoPlugin.resetBtnTip().then();
}
async parsePage($dom, starId, name2, filterKeywordList, filterActorActressCarNumList) {
let movieList, nextPageLink, tempIsJavBus = !1, selectorType = "javdb";
if ($dom.text().includes("javbus")) {
tempIsJavBus = !0;
selectorType = "javbus";
}
tempIsJavBus && $dom.find(".avatar-box").length > 0 && $dom.find(".avatar-box").parent().remove();
movieList = $dom.find(this.getSelector(selectorType).requestDomItemSelector);
nextPageLink = $dom.find(this.getSelector(selectorType).nextPageSelector).attr("href");
if (nextPageLink && 0 === movieList.length) {
clog.error("新作品检测-解析列表失败");
show.error("新作品检测-解析列表失败");
throw new Error("新作品检测-解析列表失败");
}
let carNumList = [], lastPublishTime = null;
for (const element of movieList) {
const $item = $(element), {carNum: carNum2, url: url, title: title, publishTime: publishTime} = this.getBoxCarInfo($item);
if (!carNum2) continue;
if (!filterKeywordList.find((kw => title.includes(kw) || carNum2.startsWith(kw))) && !filterActorActressCarNumList.has(carNum2)) {
lastPublishTime || (lastPublishTime = publishTime);
carNumList.push(carNum2);
}
}
const carList = await storageManager.getCarList(), storageCarNumList = new Set(carList.map((car => car.carNum))), nonExistingCarNumList = carNumList.filter((carNum2 => !storageCarNumList.has(carNum2)));
nonExistingCarNumList.length > 0 && clog.log(`<span style='color: #f40'>检测出新作品, ${name2}, 共${nonExistingCarNumList.length}部</span>`);
await storageManager.updateFavoriteActress({
starId: starId,
lastCheckTime: utils.getNowStr(),
newVideoList: nonExistingCarNumList,
lastPublishTime: lastPublishTime
});
}
async checkOneNewVideo(actress) {
const filterKeywordList = await storageManager.getTitleFilterKeyword(), filterActorActressCarList = await storageManager.getBlacklistCarList(), filterActorActressCarNumList = new Set(filterActorActressCarList.map((car => car.carNum))), {lastCheckTime: lastCheckTime, name: name2, starId: starId} = actress;
let url = `${this.javDbUrl}/actors/${starId}?t=d`;
const $checkNewVideoMsg = $("#checkNewVideoMsg");
try {
clog.log("正在检测最新作品, 演员:", name2, url);
$checkNewVideoMsg.text(`正在检测最新作品, 演员: ${name2}`);
const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html);
await this.parsePage($dom, starId, name2, filterKeywordList, filterActorActressCarNumList);
clog.log('<span style="color: #f40">检测最新作品---结束</span>');
$checkNewVideoMsg.text("检测完毕");
this.getBean("NewVideoPlugin").loadData();
} catch (e) {
clog.error("检测屏蔽演员信息, 发生错误:", url, e);
show.error("检测屏蔽演员信息, 发生错误:" + e, "bottom", "right");
$checkNewVideoMsg.text(`检测屏蔽演员信息, 发生错误: ${url}`);
}
}
}
const CDN_SOURCES = [ {
name: "jsDelivr (全球CDN)",
json: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Filetree.json",
base: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Content/"
}, {
name: "GitHub Raw (备用)",
json: "https://raw.githubusercontent.com/gfriends/gfriends/master/Filetree.json",
base: "https://raw.githubusercontent.com/gfriends/gfriends/master/Content/"
} ];
let currentCdnIndex = parseInt(localStorage.getItem("jhs_img_cdn_index") || "0", 10);
(currentCdnIndex >= CDN_SOURCES.length || currentCdnIndex < 0) && (currentCdnIndex = 0);
window.G_FRIENDS_JSON_URL = CDN_SOURCES[currentCdnIndex].json;
window.window.CDN_BASE_URL = CDN_SOURCES[currentCdnIndex].base;
const STORE_NAME = "filetreeStore", dbHelper = {
db: null,
async open() {
return this.db ? this.db : new Promise(((resolve, reject) => {
const request = indexedDB.open("GfriendsAvatarDB", 1);
request.onupgradeneeded = event => {
this.db = event.target.result;
this.db.objectStoreNames.contains(STORE_NAME) || this.db.createObjectStore(STORE_NAME);
};
request.onsuccess = event => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = event => {
console.error("IndexedDB open error:", event.target.errorCode);
reject(new Error("Failed to open IndexedDB"));
};
}));
},
async get(key) {
await this.open();
return new Promise((resolve => {
const request = this.db.transaction([ STORE_NAME ], "readonly").objectStore(STORE_NAME).get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => resolve(null);
}));
},
async set(key, value) {
await this.open();
return new Promise(((resolve, reject) => {
const request = this.db.transaction([ STORE_NAME ], "readwrite").objectStore(STORE_NAME).put(value, key);
request.onsuccess = () => resolve();
request.onerror = event => {
console.error("IndexedDB set error:", event.target.errorCode);
reject(new Error("Failed to write to IndexedDB"));
};
}));
}
};
window.window.G_FRIENDS_DATA_CACHE = null;
window.window.G_FRIENDS_AVATAR_MAP = null;
function buildAvatarMap(rawData) {
if (!rawData || !rawData.Content) return null;
const map = {}, contentData = rawData.Content;
for (const companyName in contentData) {
const encodedCompany = encodeURIComponent(companyName);
for (const fileName in contentData[companyName]) {
let cleanNamePart = fileName.replace(/\.jpg$/i, "").split("-")[0];
cleanNamePart.startsWith("AI-Fix-") && (cleanNamePart = cleanNamePart.substring(7));
const actorNameKey = cleanNamePart.toLowerCase().trim();
if (actorNameKey.length > 0) {
const actualResourcePath = contentData[companyName][fileName], queryIndex = actualResourcePath.indexOf("?");
let encodedFileNamePart, queryString = "";
if (queryIndex > -1) {
encodedFileNamePart = encodeURIComponent(actualResourcePath.substring(0, queryIndex));
queryString = actualResourcePath.substring(queryIndex);
} else encodedFileNamePart = encodeURIComponent(actualResourcePath);
const fullUrl = `${window.CDN_BASE_URL}${encodedCompany}/${encodedFileNamePart}${queryString}`;
map[actorNameKey] || (map[actorNameKey] = []);
map[actorNameKey].includes(fullUrl) || map[actorNameKey].push(fullUrl);
}
}
}
return map;
}
async function searchActorAvatars(searchNames) {
let loadObj = loading();
try {
await async function() {
if (window.G_FRIENDS_DATA_CACHE && window.G_FRIENDS_AVATAR_MAP) return window.G_FRIENDS_DATA_CACHE;
let cachedData = null;
try {
cachedData = await dbHelper.get("filetree_data");
} catch (e) {
console.error("读取 IndexedDB 失败:", e);
}
if (cachedData && cachedData.Content) {
window.G_FRIENDS_DATA_CACHE = cachedData;
window.G_FRIENDS_AVATAR_MAP = buildAvatarMap(cachedData);
if (window.G_FRIENDS_AVATAR_MAP) return window.G_FRIENDS_DATA_CACHE;
}
show.info("正在载入头像数据源...");
const response = await fetch(window.G_FRIENDS_JSON_URL);
if (!response.ok) throw new Error(`请求头像源失败: ${response.status}`);
const data = await response.json();
if (data && data.Content) {
window.G_FRIENDS_DATA_CACHE = data;
window.G_FRIENDS_AVATAR_MAP = buildAvatarMap(data);
try {
await dbHelper.set("filetree_data", data);
clog.debug("载入头像数据源并写入缓存成功!");
} catch (e) {
clog.error(e);
show.error("头像数据源写入缓存失败,可能磁盘已满或其他权限问题。");
}
return window.G_FRIENDS_DATA_CACHE;
}
console.log(data);
throw new Error("解析头像数据源失败");
}();
} catch (e) {
show.error(e);
return [];
} finally {
loadObj.close();
}
if (!window.G_FRIENDS_AVATAR_MAP) return [];
const foundLinks = new Set, searchKeys = searchNames.map((name2 => name2.toLowerCase().trim())).filter((n => n.length > 0));
if (0 === searchKeys.length) return [];
for (const searchKey of searchKeys) {
const links = window.G_FRIENDS_AVATAR_MAP[searchKey];
links && links.forEach((link => foundLinks.add(link)));
}
return Array.from(foundLinks);
}
export { TaskPlugin, CDN_SOURCES, STORE_NAME, dbHelper, currentCdnIndex };
+186
View File
@@ -0,0 +1,186 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField } from '../core/constants.js';
import { javDbApi } from '../api/javdb.js';
import { GM_openInTab } from 'vite-plugin-monkey/dist/client';
class TOP250Plugin extends BasePlugin {
constructor() {
super();
__publicField(this, "has_cnsub", "");
__publicField(this, "$contentBox", $(".section .container"));
__publicField(this, "movies", []);
}
getName() {
return "TOP250Plugin";
}
handle() {
$('.main-tabs ul li:contains("猜你喜歡")').html('<a href="/rankings/top"><span>Top250</span></a>');
$('a[href*="rankings/top"]').on("click", (event => {
event.preventDefault();
event.stopPropagation();
const $target = $(event.target), href = ($target.is("a") ? $target : $target.closest("a")).attr("href");
let queryString = href.includes("?") ? href.split("?")[1] : href;
const urlParams = new URLSearchParams(queryString);
this.checkLogin(event, urlParams);
}));
this.handleTop().then();
}
hookPage() {
$("h2.section-title").contents().first().replaceWith("Top250");
$(".empty-message").remove();
$(".section .container .box").remove();
$("#sort-toggle-btn").remove();
this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>');
this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>');
this.renderPagination();
}
renderPagination() {
const urlParams = new URLSearchParams(window.location.search);
let currentPage = parseInt(urlParams.get("page")) || 1;
this.$contentBox.append((page => {
const isNextDisabled = page >= 5;
let paginationListHTML = "";
for (let i = 1; i <= 5; i++) {
paginationListHTML += `<li><a class="pagination-link ${page === i ? "is-current" : ""}" data-page="${i}">${i}</a></li>`;
}
return `\n <nav class="pagination">\n <a class="pagination-previous ${page <= 1 ? "do-hide" : ""}" data-page="${page - 1}">上一頁</a>\n <a class="pagination-next ${isNextDisabled ? "do-hide" : ""}" data-page="${page + 1}">下一頁</a>\n \n <ul class="pagination-list">\n ${paginationListHTML}\n </ul>\n </nav>\n `;
})(currentPage));
this.$contentBox.on("click", ".pagination-link, .pagination-previous, .pagination-next", (event => {
event.preventDefault();
const newPage = parseInt($(event.currentTarget).data("page"));
!isNaN(newPage) && newPage > 0 && (newPage => {
urlParams.set("page", newPage);
window.history.pushState({}, "", "?" + urlParams.toString());
window.location.reload();
})(newPage);
}));
}
async handleTop() {
if (!window.location.href.includes("handleTop=1")) return;
const urlParams = new URLSearchParams(window.location.search);
let type = urlParams.get("handleType") || "all", type_value = urlParams.get("type_value") || "";
this.has_cnsub = urlParams.get("has_cnsub") || "";
let page = urlParams.get("page") || 1;
this.toolBar(type, type_value, page);
this.hookPage();
let $movieBox = $(".movie-list");
$movieBox.html("");
let loadObj = loading();
try {
const res = await javDbApi.top250(type, type_value, page, 50);
let successFlag = res.success, message = res.message, action = res.action;
if (1 === successFlag) {
let dataList = res.data.movies;
if (0 === dataList.length) {
show.error("无数据");
loadObj.close();
return;
}
this.movies = dataList;
const filter_movies = dataList.filter((item => "1" === this.has_cnsub ? item.has_cnsub : "0" !== this.has_cnsub || !item.has_cnsub));
let moviesHtml = javDbApi.markDataListHtml(filter_movies);
$movieBox.html(moviesHtml);
} else {
clog.error(res);
$movieBox.html(`<h3>${message}</h3>`);
show.error(message);
if ("JWTVerificationError" === action) {
await localStorage.removeItem("jhs_appAuthorization");
await this.checkLogin(null, new URLSearchParams(window.location.search));
}
}
} catch (e) {
clog.error("发生错误:", e);
} finally {
loadObj.close();
}
}
toolBar(type, type_value, currentPage) {
"5" === currentPage.toString() && $(".pagination-next").remove();
$(".pagination-ellipsis").closest("li").remove();
$(".pagination-list li a").each((function() {
parseInt($(this).text()) > 5 && $(this).closest("li").remove();
}));
let yearHtml = "";
for (let year = (new Date).getFullYear(); year >= 2008; year--) yearHtml += `\n <a style="padding:18px 18px !important;" \n class="button is-small ${type_value === year.toString() ? "is-info" : ""}" \n href="/advanced_search?handleTop=1&handleType=year&type_value=${year}&has_cnsub=${this.has_cnsub}">\n ${year}\n </a>\n `;
let conditionHtml = `\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox" style="margin-bottom: 0!important;">\n <a style="padding:18px 18px !important;" class="button is-small ${"all" === type ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=all&type_value=&has_cnsub=${this.has_cnsub}">全部</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=0&has_cnsub=${this.has_cnsub}">有码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"1" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=1&has_cnsub=${this.has_cnsub}">无码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"2" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=2&has_cnsub=${this.has_cnsub}">欧美</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"3" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=3&has_cnsub=${this.has_cnsub}">Fc2</a>\n \n <a style="padding:18px 18px !important;margin-left: 50px" class="button is-small ${"1" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="1">含中字磁鏈</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="0">无字幕</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-cnsub-value="">重置</a>\n </div>\n \n <div class="buttons has-addons" id="conditionBox">\n ${yearHtml}\n </div>\n </div>\n `;
this.$contentBox.append(conditionHtml);
$("a[data-cnsub-value]").on("click", (event => {
const cnsubValue = $(event.currentTarget).data("cnsub-value");
this.has_cnsub = cnsubValue.toString();
$("a[data-cnsub-value]").removeClass("is-info");
$(event.currentTarget).addClass("is-info");
$(".toolbar a.button").not("[data-cnsub-value]").each(((index, element) => {
const $link = $(element), url = new URL($link.attr("href"), window.location.origin);
url.searchParams.set("has_cnsub", cnsubValue);
$link.attr("href", url.toString());
}));
const filter_movies = this.movies.filter((item => "1" === this.has_cnsub ? item.has_cnsub : "0" !== this.has_cnsub || !item.has_cnsub));
let moviesHtml = javDbApi.markDataListHtml(filter_movies);
$(".movie-list").html(moviesHtml);
}));
}
async checkLogin(event, urlParams) {
if (!localStorage.getItem("jhs_appAuthorization")) {
show.error("该类别依赖移动端接口,请先完成登录");
this.openLoginDialog();
return;
}
let type = "all", type_value = "", t = urlParams.get("t") || "";
if (/^y\d+$/.test(t)) {
type = "year";
type_value = t.substring(1);
} else if ("" !== t) {
type = "video_type";
type_value = t;
}
let url = `/advanced_search?handleTop=1&handleType=${type}&type_value=${type_value}`;
event && (event.ctrlKey || event.metaKey) ? GM_openInTab(window.location.origin + url, {
insert: 0
}) : window.location.href = url;
}
openLoginDialog() {
layer.open({
type: 1,
title: "JavDB",
closeBtn: 1,
area: [ "360px", "auto" ],
shadeClose: !1,
content: '\n <div style="padding: 30px; font-family: \'Helvetica Neue\', Arial, sans-serif;">\n <div style="margin-bottom: 25px;">\n <input type="text" id="username" name="username" \n style="width: 100%; padding: 12px 15px; border: 1px solid #e0e0e0; border-radius: 4px; \n box-sizing: border-box; transition: all 0.3s; font-size: 14px;\n background: #f9f9f9; color: #333;"\n placeholder="用户名 | 邮箱"\n onfocus="this.style.borderColor=\'#4a8bfc\'; this.style.background=\'#fff\'"\n onblur="this.style.borderColor=\'#e0e0e0\'; this.style.background=\'#f9f9f9\'">\n </div>\n \n <div style="margin-bottom: 15px;">\n <input type="password" id="password" name="password" \n style="width: 100%; padding: 12px 15px; border: 1px solid #e0e0e0; border-radius: 4px; \n box-sizing: border-box; transition: all 0.3s; font-size: 14px;\n background: #f9f9f9; color: #333;"\n placeholder="密码"\n onfocus="this.style.borderColor=\'#4a8bfc\'; this.style.background=\'#fff\'"\n onblur="this.style.borderColor=\'#e0e0e0\'; this.style.background=\'#f9f9f9\'">\n </div>\n \n <button id="loginBtn" \n style="width: 100%; padding: 12px; background: #4a8bfc; color: white; \n border: none; border-radius: 4px; font-size: 15px; cursor: pointer;\n transition: background 0.3s;"\n onmouseover="this.style.background=\'#3a7be0\'"\n onmouseout="this.style.background=\'#4a8bfc\'">\n 登录\n </button>\n </div>\n ',
success: (layero, index) => {
$("#loginBtn").click((function() {
const username = $("#username").val(), password = $("#password").val();
if (!username || !password) {
show.error("请输入用户名和密码");
return;
}
let loadObj = loading();
javDbApi.login(username, password).then((async res => {
let success = res.success;
if (0 === success) show.error(res.message); else {
if (1 !== success) {
clog.error("登录失败", res);
throw new Error(res.message);
}
{
let token = res.data.token;
await localStorage.setItem("jhs_appAuthorization", token);
show.ok("登录成功");
layer.close(index);
window.location.href = "/advanced_search?handleTop=1&period=daily";
}
}
})).catch((err => {
clog.error("登录异常:", err);
show.error(err.message);
})).finally((() => {
loadObj.close();
}));
}));
}
});
}
}
export { TOP250Plugin };
+41
View File
@@ -0,0 +1,41 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, YES } from '../core/constants.js';
import { translateText } from './list-page-button.js';
class TranslatePlugin extends BasePlugin {
getName() {
return "TranslatePlugin";
}
async initCss() {
return "\n <style> \n .translated-title {\n margin-top: 8px; \n padding: 12px; \n border-radius: 5px; \n border-left: 4px solid rgb(76, 175, 80);\n background: linear-gradient(135deg, rgb(255, 255, 255) 0%, rgb(245, 245, 245) 100%); \n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);\n font-size: 20px;\n }\n </style>\n ";
}
handle() {
window.isDetailPage && this.translate();
}
async translate(carNum2, showCarNum = !0) {
if (await storageManager.getSetting("translateTitle", YES) !== YES) return;
isJavBus && (showCarNum = !1);
let $titleElement = $(".origin-title");
$titleElement.length || ($titleElement = $(".current-title"));
$titleElement.length || ($titleElement = $("h3"));
if (!$titleElement.length) return;
const originalText = $titleElement.text().trim();
if (!originalText) {
show.error("获取标题失败, 无法进行翻译");
return;
}
$titleElement.after('<div class="translated-title">翻译中...</div>');
const $loadingElement = $titleElement.next(".translated-title");
carNum2 || (carNum2 = this.getPageInfo().carNum);
const cache = localStorage.getItem("jhs_translate") ? JSON.parse(localStorage.getItem("jhs_translate")) : {};
cache[carNum2] ? $loadingElement.html(showCarNum ? carNum2 + "&nbsp;&nbsp;&nbsp;" + cache[carNum2] : cache[carNum2]) : translateText(originalText, "ja", "zh-CN").then((translatedText => {
$loadingElement.html(showCarNum ? carNum2 + "&nbsp;&nbsp;&nbsp;" + translatedText : translatedText);
})).catch((error => {
console.error("翻译失败:", error);
$loadingElement.replaceWith(`<div class="translated-title" style="color: red;">翻译失败: ${error.message}</div>`);
}));
}
}
export { TranslatePlugin };
+151
View File
@@ -0,0 +1,151 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus } from '../core/constants.js';
const getDownPathList = async () => {
const res = await gmHttp.get("https://webapi.115.com/offine/downpath");
return "object" == typeof res ? res.data : null;
};
const searchFiles = async (search_value, offset = 0, limit = 30) => {
const url = `https://webapi.115.com/files/search?search_value=${encodeURIComponent(search_value)}&offset=${offset}&limit=${limit}`;
return await gmHttp.get(url);
};
class WangPan115TaskPlugin extends BasePlugin {
getName() {
return "WangPan115TaskPlugin";
}
async handle() {
$(".buttons button[data-clipboard-text*='magnet:']").each(((i, el) => {
$(el).parent().append($("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
try {
await this.handleAddTask($(el).attr("data-clipboard-text"));
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
})));
}));
isJavBus && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => {
this.bus115Down();
}));
}
async bus115Down() {
$("#magnet-table tr").each(((i, row) => {
const magnetLink = $(row).find("td:nth-child(1) a").attr("href");
if (magnetLink && magnetLink.includes("magnet:")) {
const actionCell = $("<td>").addClass("action-cell");
$("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
try {
await this.handleAddTask(magnetLink);
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
})).appendTo(actionCell);
$(row).append(actionCell);
}
}));
$("#magnet-table tbody").length > 0 && $("#magnet-table tbody tr").append($("<td>").text("操作"));
}
async getSavePathId(nyName) {
let savePath115 = await storageManager.getSetting("savePath115", "云下载");
nyName && (savePath115 = savePath115.replaceAll("{ny}", nyName));
savePath115 = savePath115.replaceAll("{date}", utils.formatDate(new Date));
return savePath115;
}
async handleAddTask(magnetLink, savePath) {
const singInfo = await (async () => {
const res = await gmHttp.get("https://115.com/?ct=offline&ac=space&_=" + (new Date).getTime());
return "object" == typeof res ? res : null;
})();
if (!singInfo) {
show.error("未登录115网盘", {
close: !0,
duration: -1,
callback: async () => {
window.open("https://115.com");
}
});
return;
}
const sign = singInfo.sign, time = singInfo.time, userId = this.getUserId(), result = await (async (magnet, wp_path_id = "", uid, sign, time) => {
const data = {
url: encodeURIComponent(magnet),
wp_path_id: wp_path_id,
uid: uid,
sign: sign,
time: time
};
return await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=add_task_url", data);
})(magnetLink, savePath || "", userId, sign, time);
console.log("离线下载返回值:", result);
let infoHash = result.info_hash, fileId = await this.getFileId(userId, sign, time, infoHash), openUrl = "https://115.com/?tab=offline&mode=wangpan";
fileId && (openUrl = `https://115.com/?cid=${fileId}&offset=0&mode=wangpan`);
let title = "添加成功, 是否前往查看?";
!1 === result.state && (title = result.error_msg + " 是否前往查看?");
utils.q(null, title, (async () => {
let fileId2 = await this.getFileId(userId, sign, time, infoHash);
fileId2 && (openUrl = `https://115.com/?cid=${fileId2}&offset=0&mode=wangpan`);
window.open(openUrl);
}));
}
async getUserId() {
let downPathList = await getDownPathList();
if (downPathList && downPathList.length > 0) return downPathList[0].id;
{
show.info("没有默认离线目录, 正在创建中...");
const dirId = (await (async (dirName, pid = 0) => {
const data = {
pid: pid,
cname: dirName
};
return await gmHttp.postFileFormData("https://webapi.115.com/files/add", data);
})("云下载")).file_id;
await (async dirId => {
const data = {
file_id: dirId
};
return await gmHttp.postFileFormData("https://webapi.115.com/offine/downpath", data);
})(dirId);
show.info("创建完成, 开始执行离线下载");
downPathList = await getDownPathList();
if (downPathList && downPathList.length > 0) return downPathList[0].id;
throw new Error("获取115用户Id失败");
}
}
async getFileId(userId, sign, time, infoHash) {
const taskList = await (async (uid, sign, time) => {
const data = {
page: 1,
uid: uid,
sign: sign,
time: time
};
return (await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=task_lists", data)).tasks;
})(userId, sign, time);
console.log("云离线列表:", taskList);
let fileId = null;
for (let i = 0; i < taskList.length; i++) {
let task = taskList[i];
if (task.info_hash === infoHash) {
fileId = task.file_id;
break;
}
}
return fileId;
}
}
export { WangPan115TaskPlugin, searchFiles, getDownPathList };
+418
View File
@@ -0,0 +1,418 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, currentHref, NO } from '../core/constants.js';
import { searchFiles } from './wangpan-115-task.js';
class WangPan115Plugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "JHS_115_COOKIE", "jhs_115_cookie");
__publicField(this, "JHS_115_MAX_AGE", "jhs_115_max_age");
}
getName() {
return "WangPan115Plugin";
}
async initCss() {
return "\n <style>\n .login-box .ltab-office {\n border: 1px solid #DEE4EE;\n }\n \n .change-bg::before {\n background-color:#F9FAFB !important;\n }\n \n .site-login-wrap {\n height: auto;\n }\n \n #jhs-cookie-panel {\n width: 200px;\n position: fixed;\n bottom: 20px;\n right: 20px;\n z-index: 10000;\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n cursor: pointer;\n background-color: #FFFFFF;\n color: #333333;\n padding: 0;\n border-radius: 6px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n transition: all 0.3s ease;\n border: 1px solid #E0E0E0;\n }\n \n #jhs-cookie-panel.expanded {\n padding: 0;\n border-radius: 8px;\n background-color: #FFFFFF;\n color: #333333;\n box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);\n }\n \n #jhs-cookie-header {\n padding: 10px 15px;\n background-color: #0078D4;\n color: white;\n border-radius: 6px 6px 0 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n font-weight: 600;\n }\n \n #jhs-cookie-panel:not(.expanded) #jhs-cookie-header {\n border-radius: 6px;\n padding: 8px 15px;\n }\n \n #jhs-cookie-content {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.3s ease-out;\n padding: 0 15px;\n }\n \n #jhs-cookie-panel.expanded #jhs-cookie-content {\n max-height: 250px;\n padding: 15px;\n }\n \n #jhs-cookie-value {\n max-height: 100px;\n overflow-y: auto;\n white-space: pre-wrap;\n word-break: break-all;\n margin-bottom: 15px;\n padding: 10px;\n border: 1px solid #CCCCCC;\n background-color: #F8F8F8;\n font-size: 12px;\n border-radius: 4px;\n color: #555;\n }\n \n #jhs-copy-btn {\n background-color: #10B981;\n color: white;\n border: none;\n padding: 8px 15px;\n text-align: center;\n text-decoration: none;\n display: inline-block;\n font-size: 14px;\n margin: 0;\n cursor: pointer;\n border-radius: 4px;\n width: 100%;\n font-weight: 600;\n transition: background-color 0.2s ease;\n }\n \n #jhs-copy-btn:hover {\n background-color: #059669;\n }\n </style>\n ";
}
async handle() {
if (!currentHref.includes("&ac=userfile") && currentHref.includes("115")) {
utils.loopDetector((() => $("#js-login-box").length > 0), (() => {
if (0 !== $("#js-login-box").length) {
this.reLogin();
this.hookPage();
this.bindClick();
}
}), 20, 4e3, !0);
this.createCookiePanel();
}
}
reLogin() {
utils.loopDetector((() => $(".login-finished").length > 0), (() => {
if ($(".login-finished").length > 0 || 0 === $("#js-login-box").length) return;
const jhs_115_cookie = localStorage.getItem(this.JHS_115_COOKIE), jhs_115_max_age = localStorage.getItem(this.JHS_115_MAX_AGE);
document.cookie.includes("SEID") || null === jhs_115_cookie || utils.q(null, "检测到上次登录已有缓存cookie, 是否使用并登录?", (() => {
utils.addCookie(jhs_115_cookie, {
maxAge: parseInt(jhs_115_max_age),
domain: ".115.com"
});
window.location.href = "https://115.com/?cid=0&offset=0&mode=wangpan";
}));
}), 20, 1500, !0);
}
hookPage() {
const $cookieTab = $('<a id="jhs-cookie"><s>🔰 JHS-扫码</s></a>');
$(".ltab-office").after($cookieTab);
const $cookieScene = $(`\n <div id="jhs_cookie_box" style="display: none; padding: 0 20px; max-width: 300px; margin: auto;">\n <div style="margin-bottom: 15px; text-align: center;">\n <span style="font-size: 18px; font-weight: bold; color: #333; display: block; margin-bottom: 10px;"> 使用115App扫码登录 </span>\n <div style="text-align: left;">\n <select id="login-115-type" style="width: 100%; padding: 10px; border-radius: 4px; border: 1px solid #ddd; font-size: 14px; box-sizing: border-box; background-color: white;">\n <option value="" style="color: #999;">请选择登录方式</option>\n <option value="wechatmini">微信小程序</option>\n <option value="alipaymini">支付宝小程序</option>\n </select>\n </div>\n </div>\n \n <div style="text-align: left;">\n <select id="cookie-expiry-select" style="width: 100%; padding: 10px; border-radius: 4px; border: 1px solid #ddd; font-size: 14px; box-sizing: border-box; background-color: white;">\n ${[ {
label: "有效期: 会话 (关闭浏览器)",
value: 0
}, {
label: "有效期: 1 天",
value: 86400
}, {
label: "有效期: 7 天",
value: 604800
}, {
label: "有效期: 30 天",
value: 2592e3,
default: !0
}, {
label: "有效期: 60 天",
value: 5184e3
}, {
label: "有效期: 180 天",
value: 15552e3
} ].map((c => `<option value="${c.value}" ${c.default ? "selected" : ""} > ${c.label} </option>`)).join("")}\n </select>\n </div>\n \n <div id="qrcode-box" style="display: none; justify-content:center; min-height: 100px; border: 1px dashed #aaa; padding: 15px; text-align: center; margin-top: 15px; border-radius: 4px; background-color: #fff; line-height: 70px; color: #666;">\n 二维码占位区域\n </div>\n \n \n <div style="margin-bottom: 15px; text-align: center; margin-top:50px">\n <span style="font-size: 18px; font-weight: bold; color: #333; display: block; margin-bottom: 10px;">已有Cookie? 在此输入并回车</span>\n <div style="text-align: left;">\n <input type="text" id="cookie-str-input" style="width: 100%; padding: 10px; border-radius: 4px; border: 1px solid #ddd; font-size: 14px; box-sizing: border-box; background-color: white;">\n </div>\n <div style="text-align: center;margin-top:5px">\n <a class="a-primary" id="submit-cookie-btn">提交</a>\n </div>\n </div>\n </div>\n `);
$("#js-login_box").find(".login-footer").before($cookieScene);
}
bindClick() {
$("#jhs-cookie").on("click", (() => {
const finishedEl = document.querySelector('[lg_rel="finished"]');
if (finishedEl) finishedEl.style.display = "none"; else {
document.querySelector('[lg_rel="qrcode"]').style.display = "none";
document.querySelector(".login-footer").style.display = "none";
document.querySelector(".list-other-login").style.display = "none";
}
document.querySelectorAll("#js-login_way > *").forEach((tab => {
tab.classList.remove("current");
}));
document.querySelector("#jhs_cookie_box").style.display = "block";
$("#jhs-cookie").css("background", "#fff");
$(".ltab-cloud").addClass("change-bg");
}));
$(".ltab-cloud").on("click", (() => {
document.querySelector("#jhs_cookie_box").style.display = "none";
const finishedEl = document.querySelector('[lg_rel="finished"]');
if (finishedEl) finishedEl.style.display = "flex"; else {
document.querySelector('[lg_rel="qrcode"]').style.display = "block";
document.querySelector(".login-footer").style.display = "block";
document.querySelector(".list-other-login").style.display = "block";
}
$("#jhs-cookie").css("background", "#F9FAFB");
$(".ltab-cloud").removeClass("change-bg");
}));
let loginTimeout = null;
$("#login-115-type").on("change", (async event => {
let login115Type = $("#login-115-type").val();
if (!login115Type) return;
const loginInfo = (await (async loginType => {
let url = `https://qrcodeapi.115.com/api/1.0/${loginType}/1.0/token/`;
return await gmHttp.get(url);
})(login115Type)).data, qrcode = loginInfo.qrcode, sign = loginInfo.sign, time = loginInfo.time, uid = loginInfo.uid;
console.log("生成二维码:", loginInfo);
const $qrcodeBox = $("#qrcode-box");
$qrcodeBox.css("display", "flex");
$qrcodeBox.html("");
new QRCode($qrcodeBox[0], {
text: qrcode,
width: 150,
height: 150,
correctLevel: QRCode.CorrectLevel.H
});
loginTimeout && clearTimeout(loginTimeout);
const checkLoginRecursive = async () => {
try {
const loginResult = await (async (uid, time, sign) => {
let url = `https://qrcodeapi.115.com/get/status/?uid=${uid}&time=${time}&sign=${sign}`;
return await gmHttp.get(url);
})(uid, time, sign);
console.log("已扫码, 正在获取结果:", loginResult);
let data = loginResult.data, msg = data.msg, status = data.status;
if (msg) {
console.log(msg);
show.info(msg);
}
if (2 === status) {
show.ok("扫码登录成功");
const checkResult = await (async (loginType, uid) => {
const data = {
app: loginType,
account: uid
}, url = `https://passportapi.115.com/app/1.0/${loginType}/1.0/login/qrcode/`;
return await gmHttp.postFileFormData(url, data);
})(login115Type, uid);
console.log("扫码登录成功:", checkResult);
if (checkResult.data && checkResult.data.cookie) {
const cookie = checkResult.data.cookie, cookieStr = `UID=${cookie.UID}; CID=${cookie.CID}; SEID=${cookie.SEID}; KID=${cookie.KID}`;
console.log("解析出cookie:", cookieStr);
localStorage.setItem(this.JHS_115_COOKIE, cookieStr);
localStorage.setItem(this.JHS_115_MAX_AGE, $("#cookie-expiry-select").val());
window.location.href = "https://115.com/?cid=0&offset=0&mode=wangpan";
}
return;
}
loginTimeout = setTimeout(checkLoginRecursive, 500);
} catch (error) {
console.error("登录检查失败:", error);
}
};
await checkLoginRecursive();
}));
const handleCookie = () => {
const cookieStr = cookieInput.value, expirySelect = document.getElementById("cookie-expiry-select");
let maxAge = parseInt(expirySelect.value);
utils.addCookie(cookieStr, {
maxAge: maxAge,
domain: ".115.com"
});
window.location.href = "https://115.com/?cid=0&offset=0&mode=wangpan";
}, cookieInput = document.getElementById("cookie-str-input");
cookieInput.addEventListener("keydown", (function(event) {
if ("Enter" === event.key) {
event.preventDefault();
handleCookie();
}
}));
$("#submit-cookie-btn").on("click", (() => {
handleCookie();
}));
}
showMessage(message) {
const toast = document.createElement("div");
toast.textContent = message;
toast.style.cssText = "\n position: fixed;\n top: 20px;\n right: 20px;\n background-color: #333;\n color: white;\n padding: 10px 20px;\n border-radius: 5px;\n z-index: 20000;\n opacity: 0;\n transition: opacity 0.5s ease-in-out;\n ";
document.body.appendChild(toast);
setTimeout((() => {
toast.style.opacity = "1";
}), 10);
setTimeout((() => {
toast.style.opacity = "0";
setTimeout((() => toast.remove()), 500);
}), 3e3);
}
createCookiePanel() {
const cookieValue = localStorage.getItem(this.JHS_115_COOKIE);
if (!cookieValue) return;
const panel = document.createElement("div");
panel.id = "jhs-cookie-panel";
panel.innerHTML = `\n <div id="jhs-cookie-header">\n <span>JHS-115-Cookie</span>\n <span id="jhs-toggle-icon">▼</span>\n </div>\n <div id="jhs-cookie-content">\n <div id="jhs-cookie-value">${cookieValue}</div>\n <button id="jhs-copy-btn">复制 Cookie</button>\n </div>\n `;
document.body.appendChild(panel);
const header = document.getElementById("jhs-cookie-header");
document.getElementById("jhs-cookie-content");
const toggleIcon = document.getElementById("jhs-toggle-icon"), copyButton = document.getElementById("jhs-copy-btn");
header.addEventListener("click", (() => {
const isExpanded = panel.classList.toggle("expanded");
toggleIcon.textContent = isExpanded ? "▲" : "▼";
}));
copyButton.addEventListener("click", (async e => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(cookieValue);
this.showMessage("Cookie 已成功复制到剪贴板!");
} catch (err) {
console.error("Failed to copy text using clipboard API: ", err);
const tempTextArea = document.createElement("textarea");
tempTextArea.value = cookieValue;
document.body.appendChild(tempTextArea);
tempTextArea.select();
document.execCommand("copy");
document.body.removeChild(tempTextArea);
this.showMessage("Cookie 已复制! (回退方案)");
}
}));
panel.classList.remove("expanded");
}
}
const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "loginStatus", _WangPan115MatchPlugin.LoginStatus.UNCHECKED);
}
getName() {
return "WangPan115MatchPlugin";
}
async initCss() {
return "\n <style>\n [class^='jhs-match-'] {\n padding: 1px 2px;\n margin-left: 0;\n margin-right: 5px;\n }\n \n .jhs-match-detail {\n display: inline-block;\n width: 50%;\n z-index: 1000;\n background: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n padding: 10px;\n overflow-y: auto;\n }\n .jhs-match-detail.isListPage{\n position: absolute;\n box-shadow: 0 2px 10px rgba(0,0,0,0.2);\n }\n .jhs-match-detail table {\n width: 100%;\n border-collapse: collapse;\n }\n .jhs-match-detail th, .jhs-match-detail td {\n padding: 4px 8px;\n border: 1px solid #eee;\n text-align: left;\n }\n .jhs-match-detail th {\n background-color: #f5f5f5;\n }\n .jhs-match-detail tr:hover {\n background-color: #f9f9f9;\n }\n </style>\n ";
}
async handle() {
$(document).on("click", ".jhs-match-no-login-btn", (async event => {
event.preventDefault();
event.stopPropagation();
await this.handleLoginRedirect();
}));
$(document).on("click", ".jhs-match-btn", (event => {
event.preventDefault();
event.stopImmediatePropagation();
this.showMatchDetail(event.currentTarget);
}));
$(document).on("click", ".jhs-match-error-btn", (async event => {
event.preventDefault();
event.stopPropagation();
await this.retryMatch(event.currentTarget);
}));
await this.matchDetailPage();
$(document).on("click", ".jhs-match-detail-error-btn", (async event => {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).replaceWith("<a class='jhs-match-btn' title=\"匹配中...\">匹配中...</a>");
try {
const carNum2 = this.getPageInfo().carNum, matchList = await this.searchFiles(carNum2);
$(".jhs-115-match-detail").remove();
await this.matchDetailPage(matchList);
} catch (error) {
console.error(`重新匹配失败 [${carNum}]:`, error);
this.showMatchError($box, carNum, error);
}
}));
}
async matchDetailPage(matchList) {
if (!window.isDetailPage) return;
if (await storageManager.getSetting("enable115Match", NO) === NO) return;
const $detail = $('\n <div class="jhs-match-detail jhs-115-match-detail" id="115-match-table">\n <table>\n <thead>\n <tr style="text-align: center">\n <th colspan="4">115匹配</th>\n </tr>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n </tbody>\n </table>\n </div>\n '), $tbody = $detail.find("tbody");
try {
const carNum2 = this.getPageInfo().carNum;
matchList || (matchList = await this.searchFiles(carNum2));
await this.checkLoginStatus();
if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-no-login-btn a-info'\n data-keyword="${carNum2}"\n title="未登录115网盘">未登录</a>\n </td></tr>`); else if (matchList.length > 0) {
const rowsHtml = matchList.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0"\n target="_blank"\n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("");
$tbody.append(rowsHtml);
} else $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-detail-error-btn a-info'\n data-keyword="${carNum2}"\n title="未匹配,点击重试">未匹配</a>\n </td></tr>`);
} catch (error) {
$tbody.append(`<tr><td colspan="4">\n <a class="a-danger jhs-match-detail-error-btn" title="${error.message || "加载失败"}">加载失败,请重试</a>\n </td></tr>`);
console.error("加载文件列表时发生错误:", error);
}
if (isJavDb) if ($("#all-match-box").length) $("#all-match-box").append($detail); else {
$("#tabs-container").before("<div style='display: flex' id='all-match-box'></div>");
$("#all-match-box").append($detail);
} else $("#mag-submit-show").before($detail);
}
async matchMovieList(movieListElement) {
if (await storageManager.getSetting("enable115Match", NO) !== NO) {
await this.checkLoginStatus();
await this.processMovieElements(movieListElement);
} else $(".video-title [class^='jhs-match-']").remove();
}
showMatchDetail(buttonElement) {
const $el = $(buttonElement), matchData = $el.attr("data-match");
$(".jhs-match-detail").remove();
const matches = this.parseMatchData(matchData);
if (0 === matches.length) return;
if (1 === matches.length) {
const pickcode = matches[0].videoId;
window.open(`https://115vod.com/?pickcode=${pickcode}&share_id=0`, "_blank");
return;
}
const $detail = this.createMatchDetailElement(matches);
this.positionDetailElement($detail, $el);
this.addOutsideClickHandler($detail);
$detail.on("click", (e => {
e.stopPropagation();
}));
}
parseMatchData(matchData) {
try {
return JSON.parse(matchData) || [];
} catch (e) {
console.error("解析匹配数据失败:", e);
return [];
}
}
createMatchDetailElement(matches) {
const $detail = $(`\n <div class="jhs-match-detail isListPage">\n <table>\n <thead>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n ${matches.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0" \n target="_blank" \n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("")}\n </tbody>\n </table>\n </div>\n `);
$("body").append($detail);
return $detail;
}
positionDetailElement($detail, $trigger) {
const offset = $trigger.offset();
$detail.css({
top: offset.top - $detail.outerHeight() + 20,
left: offset.left
});
}
addOutsideClickHandler($detail) {
setTimeout((() => {
$(document).on("click.jhs-match-detail", (e => {
if (!$detail.is(e.target) && 0 === $detail.has(e.target).length) {
$detail.remove();
$(document).off("click.jhs-match-detail");
}
}));
}), 100);
}
async retryMatch(buttonElement) {
const $el = $(buttonElement), $box2 = $el.closest(".movie-box, .item"), carNum2 = $el.attr("data-keyword");
$el.replaceWith("<a class='jhs-match-btn' title=\"匹配中...\">匹配中...</a>");
try {
const matchList = await this.searchFiles(carNum2);
this.updateMatchStatus($box2, carNum2, matchList);
} catch (error) {
console.error(`重新匹配失败 [${carNum2}]:`, error);
this.showMatchError($box2, carNum2, error);
}
}
updateMatchStatus($box2, carNum2, matchList) {
matchList.length > 0 ? $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="点击查看匹配详情">匹配${matchList.length}个</a>`) : $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn a-info' data-keyword="${carNum2}" \n title="点击重新尝试匹配">未匹配</a>`);
}
async handleLoginRedirect() {
window.open("https://115.com");
}
async searchFiles(carNum2) {
var _a2;
let searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
return (null == (_a2 = (await searchFiles(searchKeyword)).data) ? void 0 : _a2.map((data => ({
folderId: data.fid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => {
var _a3;
return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext);
}))
}))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || [];
}
showMatchError($box2, carNum2, error) {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn' data-keyword="${carNum2}" \n title="匹配失败,点击重试">匹配失败</a>`);
show.error(`${carNum2} 匹配失败: ${error.message || "网络错误"}`);
}
async checkLoginStatus() {
var _a2;
if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.UNCHECKED) try {
const testResult = await searchFiles("test");
this.loginStatus = (null == (_a2 = testResult.error) ? void 0 : _a2.includes("登录")) ? _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT : _WangPan115MatchPlugin.LoginStatus.LOGGED_IN;
} catch {
this.loginStatus = _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT;
}
}
async processMovieElements(movieListElement) {
const promises = Array.from(movieListElement).filter((ele => !utils.isHidden(ele))).filter((ele => !(isJavBus && $(ele).find(".avatar-box").length > 0))).map((ele => this.processSingleMovieElement(ele)));
await Promise.all(promises);
}
async processSingleMovieElement(element) {
const $box2 = $(element), {carNum: carNum2} = this.getBoxCarInfo($box2);
if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus !== _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) try {
const matchList = await this.searchFiles(carNum2);
this.addTag($box2, carNum2, matchList);
} catch (error) {
console.error(`搜索失败 [${carNum2}]:`, error);
this.addTag($box2, carNum2, []);
} else this.addTag($box2, carNum2, []);
}
addTag($box2, carNum2, matchList) {
if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $box2.find(".video-title").prepend(`<a class='jhs-match-no-login-btn a-info' \n data-keyword="${carNum2}" \n title="未登录115网盘">未登录</a>`); else if (matchList.length > 0) {
const title = 1 === matchList.length ? "点击直接播放" : `点击查看${matchList.length}个匹配结果`;
$box2.find(".video-title").prepend(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="${title}">匹配${matchList.length}个</a>`);
} else $box2.find(".video-title").prepend(`<a class='jhs-match-error-btn a-info' \n data-keyword="${carNum2}" \n title="未匹配,点击重试">未匹配</a>`);
}
formatSize(bytes) {
if (!bytes) return "-";
const units = [ "B", "KB", "MB", "GB", "TB" ];
let size = parseFloat(bytes), unit = 0;
for (;size >= 1024 && unit < units.length - 1; ) {
size /= 1024;
unit++;
}
return `${size.toFixed(2)} ${units[unit]}`;
}
};
__publicField(_WangPan115MatchPlugin, "LoginStatus", {
UNCHECKED: -1,
LOGGED_OUT: 0,
LOGGED_IN: 1
});
let WangPan115MatchPlugin = _WangPan115MatchPlugin;
export { WangPan115Plugin, WangPan115MatchPlugin };
+90
View File
@@ -0,0 +1,90 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, Status_FAVORITE, Status_HAS_DOWN } from '../core/constants.js';
class WantAndWatchedVideosPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "type", null);
}
getName() {
return "WantAndWatchedVideosPlugin";
}
async handle() {
if (window.location.href.includes("/want_watch_videos")) {
$("h3").append('<a class="a-primary" id="wantWatchBtn" style="padding:10px;">导入至 JHS</a>');
$("#wantWatchBtn").on("click", (event => {
this.type = Status_FAVORITE;
this.importWantWatchVideos(event, "是否将 想看的影片 导入到 JHS-收藏?");
}));
}
if (window.location.href.includes("/watched_videos")) {
$("h3").append('<a class="a-success" id="wantWatchBtn" style="padding:10px;">导入至 JHS</a>');
$("#wantWatchBtn").on("click", (event => {
this.type = Status_HAS_DOWN;
this.importWantWatchVideos(event, "是否将 看过的影片 导入到 JHS-已下载?");
}));
}
}
importWantWatchVideos(event, title) {
utils.q(null, `${title} <br/> <span style='color: #f40'>执行此功能前请记得备份数据</span>`, (async () => {
let loadObj = loading();
try {
await this.parseMovieList();
} catch (e) {
console.error(e);
} finally {
loadObj.close();
}
}));
}
async parseMovieList($dom) {
let movieList, nextPageLink;
if ($dom) {
movieList = $dom.find(this.getSelector().itemSelector);
nextPageLink = $dom.find(".pagination-next").attr("href");
} else {
movieList = $(this.getSelector().itemSelector);
nextPageLink = $(".pagination-next").attr("href");
}
for (const element of movieList) {
const item = $(element), href = item.find("a").attr("href"), carNum2 = item.find(".video-title strong").text().trim(), publishTime = item.find(".meta").text().trim();
if (href && carNum2) try {
if (await storageManager.getCar(carNum2)) {
show.info(`${carNum2} 已存在, 跳过`);
continue;
}
await storageManager.saveCar({
carNum: carNum2,
url: href,
names: null,
actionType: this.type,
publishTime: publishTime
});
} catch (error) {
console.error(`保存失败 [${carNum2}]:`, error);
}
}
if (nextPageLink) {
show.info("发现下一页,正在解析:", nextPageLink);
await new Promise((resolve => setTimeout(resolve, 1e3)));
$.ajax({
url: nextPageLink,
method: "GET",
success: html => {
const parser = new DOMParser, next$dom = $(parser.parseFromString(html, "text/html"));
this.parseMovieList(next$dom);
},
error: function(err) {
console.error(err);
show.error("加载下一页失败:" + err.message);
}
});
} else {
show.ok("导入结束!");
window.refresh();
}
}
}
export { WantAndWatchedVideosPlugin };