// ==UserScript==
// @name Jable一键下载收藏 (支持MissAV)
// @namespace https://greasyfork.org/zh-CN/scripts/474848-jable%E4%B8%80%E9%94%AE%E4%B8%8B%E8%BD%BD%E6%94%B6%E8%97%8F
// @version 2.2.1
// @description Jable和MissAV一键下载视频,并自动点击收藏(MissAV自动选择最高清晰度,全局收藏列表,自动迁移旧数据,跨站收藏同步)
// @author Pandex
// @match *://jable.tv/*
// @match *://fs1.app/*
// @match *://missav.ws/*
// @match *://missav.live/*
// @match *://missav.ai/*
// @match *://missav123.com/*
// @connect jable.tv
// @connect fs1.app
// @connect missav.ws
// @connect missav.live
// @connect missav.ai
// @connect missav123.com
// @connect surrit.com
// @icon https://assets-cdn.jable.tv/assets/icon/favicon-32x32.png
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_removeValueChangeListener
// @grant GM_addValueChangeListener
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @license MPL
// ==/UserScript==
(function () {
// GM Storage Keys - 统一管理所有存储键名
const GM_KEYS = {
MENU_PROXY_STATUS: "menu_proxy_status",
MENU_NAME_FOLDER_STATUS: "menu_name_folder_status",
MENU_SAVE_FILE_DIRECTORY: "menu_save_file_directory",
GLOBAL_LIKED_CODES: "global_liked_codes",
ARTIST_CN_NAME_MAP: "artist_CN_name_map",
GLOBAL_FAVORITES_INITIALIZED: "global_favorites_initialized_status",
CODE_MAPPING: "cross_site_code_mapping", // 跨站代码映射
SYNC_ENABLED: "cross_site_sync_enabled" // 是否启用跨站同步
};
const defaultSaveFileDirectory = "D:\\videos\\jav";
var liked_codes = [];
var artistCNNameMap = {};
var codeMappingCache = {}; // 代码映射缓存
var syncInProgress = false; // 同步进行中标志,防止循环触发
var destinationMenu, proxyMenu, nameFolderMenu, feedbackMenu, exportMenu, importMenu
registMenus()
var downloadParams = '--maxThreads "48" --minThreads "16" --retryCount "100" --timeOut "100" --enableDelAfterDone';
var proxyParam = ' --noProxy'
function clickProxyMenu() {
GM_setValue(GM_KEYS.MENU_PROXY_STATUS, !getProxyMenuStatus())
registMenus();
}
function getProxyMenuStatus() {
return GM_getValue(GM_KEYS.MENU_PROXY_STATUS) ? true : false
}
function proxyMenuText() {
return getProxyMenuStatus() ? "使用系统代理下载✅" : "使用系统代理下载❌"
}
function clickNameFolderMenu() {
GM_setValue(GM_KEYS.MENU_NAME_FOLDER_STATUS, !getNameFolderMenuStatus())
registMenus()
}
function openFeedBack() {
window.open("https://greasyfork.org/zh-CN/scripts/474848-jable%E4%B8%80%E9%94%AE%E4%B8%8B%E8%BD%BD%E6%94%B6%E8%97%8F/feedback", "_blank");
}
function exportFavoritesList() {
// 获取已收藏的视频codes
let codes = GM_getValue(GM_KEYS.GLOBAL_LIKED_CODES) || [];
if (codes.length === 0) {
alert("没有收藏的视频可以导出!");
return;
}
// 构建JSON格式数据
let exportData = {
version: "1.0",
exportTime: new Date().toISOString(),
count: codes.length,
favorites: codes.map(code => ({
code: code
}))
};
// 创建JSON字符串
let content = JSON.stringify(exportData, null, 2);
// 创建Blob并下载
let blob = new Blob([content], { type: 'application/json;charset=utf-8' });
let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = 'favorites_' + new Date().toISOString().slice(0,10) + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert("已成功导出 " + codes.length + " 个收藏视频到 JSON 文件!");
}
function importFavoritesList() {
// 创建文件选择器
let input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.onchange = function(event) {
let file = event.target.files[0];
if (!file) {
return;
}
let reader = new FileReader();
reader.onload = function(e) {
try {
// 解析JSON
let importData = JSON.parse(e.target.result);
// 验证数据格式
if (!importData.favorites || !Array.isArray(importData.favorites)) {
alert("导入文件格式错误:缺少 favorites 字段或格式不正确!");
return;
}
// 提取导入的codes
let importedCodes = importData.favorites.map(item => item.code).filter(code => code);
if (importedCodes.length === 0) {
alert("导入文件中没有有效的收藏数据!");
return;
}
// 获取当前已有的收藏
let currentCodes = GM_getValue(GM_KEYS.GLOBAL_LIKED_CODES) || [];
let originalCount = currentCodes.length;
// 合并并去重
let mergedCodes = [...currentCodes];
let addedCount = 0;
importedCodes.forEach(code => {
if (mergedCodes.indexOf(code) === -1) {
mergedCodes.push(code);
addedCount++;
}
});
// 保存合并后的数据
GM_setValue(GM_KEYS.GLOBAL_LIKED_CODES, mergedCodes);
liked_codes = mergedCodes;
// 更新页面显示
updateBoxCardCSS(true);
// 显示导入结果
alert(
"导入完成!\n" +
"原有收藏: " + originalCount + " 个\n" +
"导入文件: " + importedCodes.length + " 个\n" +
"新增收藏: " + addedCount + " 个\n" +
"合并后总计: " + mergedCodes.length + " 个"
);
} catch (error) {
console.error('导入错误:', error);
alert("导入失败:文件格式错误或内容无效!\n错误详情: " + error.message);
}
};
reader.onerror = function() {
alert("文件读取失败!");
};
reader.readAsText(file);
};
// 触发文件选择
input.click();
}
function getNameFolderMenuStatus() {
return GM_getValue(GM_KEYS.MENU_NAME_FOLDER_STATUS) ? true : false
}
function nameFolderMenuText() {
return getNameFolderMenuStatus() ? "下载到艺术家名文件夹✅" : "下载到艺术家名文件夹❌"
}
GM_addValueChangeListener(GM_KEYS.MENU_SAVE_FILE_DIRECTORY, (name, old_value, new_value, remote) => {
if (remote) { registMenus() }
});
GM_addValueChangeListener(GM_KEYS.MENU_PROXY_STATUS, (name, old_value, new_value, remote) => {
if (remote) { registMenus() }
});
GM_addValueChangeListener(GM_KEYS.MENU_NAME_FOLDER_STATUS, (name, old_value, new_value, remote) => {
if (remote) { registMenus() }
});
function clickDestinationMenu() {
let destination = prompt("请输入下载地址", getSaveFileDirectory());
if (destination) {
GM_setValue(GM_KEYS.MENU_SAVE_FILE_DIRECTORY, destination);
registMenus();
}
}
function getSaveFileDirectory() {
return GM_getValue(GM_KEYS.MENU_SAVE_FILE_DIRECTORY) || defaultSaveFileDirectory;
}
function registMenus() {
if (destinationMenu) {
GM_unregisterMenuCommand(destinationMenu);
}
destinationMenu = GM_registerMenuCommand(destinationMenuText(), clickDestinationMenu);
if (proxyMenu) {
GM_unregisterMenuCommand(proxyMenu);
}
proxyMenu = GM_registerMenuCommand(proxyMenuText(), clickProxyMenu);
if(nameFolderMenu) {
GM_unregisterMenuCommand(nameFolderMenu);
}
nameFolderMenu = GM_registerMenuCommand(nameFolderMenuText(), clickNameFolderMenu);
if (feedbackMenu) {
GM_unregisterMenuCommand(feedbackMenu);
}
feedbackMenu = GM_registerMenuCommand("给个好评", openFeedBack);
if (exportMenu) {
GM_unregisterMenuCommand(exportMenu);
}
exportMenu = GM_registerMenuCommand("导出已收藏列表", exportFavoritesList);
if (importMenu) {
GM_unregisterMenuCommand(importMenu);
}
importMenu = GM_registerMenuCommand("导入收藏列表", importFavoritesList);
}
function destinationMenuText() {
return `下载地址:"${getRealSaveFileDirectory(['{艺术家名字}'])}"`
}
function getRealSaveFileDirectory(modelNames) {
let dir = getSaveFileDirectory()
if (!dir.match(/[\s\S]*\\$/)) {
dir = dir + '\\'
}
if (getNameFolderMenuStatus()) {
if (modelNames && modelNames.length > 0) {
if (modelNames.length == 1) {
let artistName = modelNames[0]
let artistCNName = getArtistCNName(artistName)
if (artistCNName) {
artistName = artistCNName
}
dir = dir + artistName + '\\'
} else {
dir = dir + '群星' + '\\'
}
} else {
dir = dir + '未知艺术家' + '\\'
}
}
// console.log('getRealSaveFileDirectory', modelNames, dir)
return dir
}
var _a, _b, _c, _d;
("use strict");
// 站点检测
function getCurrentSite() {
if (location.host.match(/jable\.tv|fs1\.app/)) {
return 'jable';
} else if (location.host.match(/missav\.(ws|live|ai)|missav123\.com/)) {
return 'missav';
}
return null;
}
var currentSite = getCurrentSite();
var linkPrefix = currentSite === 'jable' ? `https://${location.host}/videos/` : `https://${location.host}/`;
var r = (_a = Reflect.get(document, "__monkeyWindow")) != null ? _a : window;
r.GM;
r.unsafeWindow = (_b = r.unsafeWindow) != null ? _b : window;
r.unsafeWindow;
r.GM_info;
r.GM_cookie;
var addStyle = (...e) => r.GM_addStyle(...e),
xmlhttpRequest = (...e) => r.GM_xmlhttpRequest(...e);
const jableStyle = `
#site-content > div.container {
max-width: 2000px !important;
}
.video-img-box .title {
white-space: normal;
}
.video-img-box.liked .title a::before {
content: '❤️ ';
}
.absolute-bottom-left.download {
left: 60px;
}
.absolute-bottom-left.download .action {
background: rgba(255,255,255,.18);
opacity: 0;
}
.absolute-bottom-left.download .action.loading {
cursor: wait;
}
.video-img-box:hover .absolute-bottom-left.download .action {
opacity: 1;
}
.video-img-box.hasurl .absolute-bottom-left.download .action {
background: rgba(98,91,255,.4);
}
.video-img-box.hasurl .absolute-bottom-left.download .action:hover {
background: rgba(98,91,255,.8);
}
.video-img-box .detail .sub-title.added-avatar .models {
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
margin-left: 10px;
}
.video-img-box .detail .sub-title.added-avatar .models .model {
width: 1.5rem;
height: 1.5rem;
}
.video-img-box .detail .sub-title.added-avatar .models .placeholder {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
background: #687ae8;
color: #fff;
font-size: .8125rem;
width: 100%;
height: 100%;
-webkit-box-shadow: 2px 2px 16px 0 rgba(17,18,20,.8);
box-shadow: 2px 2px 16px 0 rgba(17,18,20,.8);
}
.video-img-box.hot-1 .title a::after {
content: ' 🔥';
}
.video-img-box.hot-2 .title a::after {
content: ' 🔥🔥';
}
.video-img-box.hot-3 .title a::after {
content: ' 🔥🔥🔥';
}
.video-img-box.hot-1 .title {
color: #f9c8f1;
}
.video-img-box.hot-2 .title {
color: hotpink;
}
.video-img-box.hot-3 .title {
color: #ff367f;
}
.video-img-box.liked .hover-state {
opacity: 1;
}
.btn-action.fav svg {
color: gray !important;
}
.btn-action.fav.active svg {
color: white !important;
}
`;
const paths = {
jable: {
video_like_btn: "#site-content > div > div > div:nth-child(1) > section.video-info.pb-3 > div.text-center > div > button.btn.btn-action.fav.mr-2",
video_title_path: "#site-content > div > div > div:nth-child(1) > section.video-info.pb-3 > div.info-header > div.header-left > h4",
video_avatar_path: "#site-content > div > div > div:nth-child(1) > section.video-info.pb-3 > div.info-header > div.header-left > h6 > div.models",
model_title_name: "#site-content > section > div > div > div > h2"
},
missav: {
video_title_path: "body > div:nth-child(3) > div.mx-auto.px-4.content-without-search.pb-12 > div > div.flex-1.order-first > div.mt-4 > h1",
video_like_btn: "body > div:nth-child(3) > div.mx-auto.px-4.content-without-search.pb-12 > div > div.flex-1.order-first > div.mt-4 > div > button.inline-flex.items-center.whitespace-nowrap.text-sm.leading-4.font-medium",
video_avatar_path: "div.text-secondary",
model_title_name: null // MissAV暂不支持演员页面标题
}
};
function getPath(key) {
return currentSite ? paths[currentSite][key] : null;
}
function isVideoURL(url) {
if (currentSite === 'jable') {
return !!url.match(/https:\/\/(jable\.tv|fs1\.app)\/videos\/*\/*/);
} else if (currentSite === 'missav') {
// MissAV通过页面元素判断:能获取到标题和M3U8就是视频页
const titleEl = document.querySelector(getPath('video_title_path'));
// console.log('titleEl', titleEl, getPath('video_title_path'));
return !!titleEl;
}
return false;
}
function isModelURL(url) {
if (currentSite === 'jable') {
return (
!!url.match(/https:\/\/(jable\.tv|fs1\.app)\/models\/*\/*/) ||
!!url.match(/https:\/\/(jable\.tv|fs1\.app)\/s1\/models\/*\/*/)
);
} else if (currentSite === 'missav') {
return !!url.match(/https:\/\/(missav\.(ws|live|ai)|missav123\.com)\/.*\/actresses\/.*/);
}
return false;
}
function isHotURL(url) {
return !!url.match(/https:\/\/(jable\.tv|fs1\.app)\/hot\/*\/*/);
}
/**
* 检查MissAV视频是否为无码视频
*/
function isUncensoredVideo(url) {
if (currentSite !== 'missav') {
return false;
}
// 检查URL是否包含 uncensored 相关标识
return !!url.match(/-(uncensored-leak|uncensored)$/i);
}
/**
* 从URL提取视频代码(保留原始格式)
*/
function getCodeFromUrl(url) {
if (currentSite === 'jable') {
let code = url.replace(linkPrefix, "").replace(/\/[\s\S]*$/, "");
return code;
} else if (currentSite === 'missav') {
// MissAV URL格式: https://missav.ws/cn/mimk-217-uncensored-leak 或 https://missav.ws/dm14/cn/waaa-323-uncensored-leak
// 提取最后一段并去除版本后缀(-uncensored-leak, -leak, -uncensored等)
const match = url.match(/\/([^\/]+)$/);
if (match && match[1]) {
let code = match[1];
// 去除常见的版本后缀
code = code.replace(/-(uncensored-leak|leak|uncensored|chinese-subtitle|ch-sub)$/i, '');
return code;
}
return "";
}
return "";
}
/**
* 标准化视频代码用于跨站匹配
* 将不同站点的代码统一为小写格式,方便匹配同一视频
*/
function normalizeCode(code) {
if (!code) return '';
// 转小写,去除常见后缀
let normalized = code.toLowerCase()
.replace(/-(uncensored-leak|leak|uncensored|chinese-subtitle|ch-sub)$/i, '')
.trim();
return normalized;
}
/**
* 保存代码映射关系
* @param {string} jableCode - Jable站点的代码
* @param {string} missavCode - MissAV站点的代码
*/
function saveCodeMapping(jableCode, missavCode) {
if (!jableCode && !missavCode) return;
let normalized = normalizeCode(jableCode || missavCode);
let mapping = GM_getValue(GM_KEYS.CODE_MAPPING) || {};
if (!mapping[normalized]) {
mapping[normalized] = {};
}
if (jableCode) mapping[normalized].jable = jableCode;
if (missavCode) mapping[normalized].missav = missavCode;
mapping[normalized].normalized = normalized;
GM_setValue(GM_KEYS.CODE_MAPPING, mapping);
codeMappingCache = mapping;
// console.log('[代码映射] 保存映射:', normalized, mapping[normalized]);
}
/**
* 获取代码映射
*/
function getCodeMapping(code) {
let normalized = normalizeCode(code);
if (Object.keys(codeMappingCache).length === 0) {
codeMappingCache = GM_getValue(GM_KEYS.CODE_MAPPING) || {};
}
return codeMappingCache[normalized];
}
var isVideoPage = isVideoURL(location.href);
var isModelPage = isModelURL(location.href);
var isHotPage = isHotURL(location.href);
var modelPageName = null
if (isModelPage) {
const res = artistPageParseFromDoc(document)
modelPageName = res.modelPageName
}
function artistPageParser(responseText) {
const doc = new DOMParser().parseFromString(responseText, "text/html");
let result = artistPageParseFromDoc(doc)
return result
}
function artistPageParseFromDoc(doc) {
let result = {
modelPageName: null,
modelPageChineseName: null
}
// MissAV 暂不支持演员页面解析
if (currentSite === 'missav') {
return result;
}
let name = doc.querySelector(getPath('model_title_name'))
if (name && name.innerText) {
result.modelPageName = name.innerText
}
let kwdMeta = doc.querySelector('head meta[name="keywords"]')
if (kwdMeta) {
let content = kwdMeta.getAttribute('content')
if (content) {
let titleSplitDict = {}
let kwdDict = {}
let keywords = content.split(',').map(a => {return a.trim()})
let titles = doc.querySelectorAll(".video-img-box .detail .title a");
titles.forEach(title => {
keywords.forEach(kwd => {
if (title.innerText && title.innerText.indexOf(kwd) > 0) {
if (kwdDict.hasOwnProperty(kwd)) {
kwdDict[kwd] = kwdDict[kwd] + 1
} else {
kwdDict[kwd] = 1
}
}
})
if (title.innerText) {
let splt = title.innerText.split(' ')
if (splt && splt.length > 1) {
let lastWord = splt[splt.length - 1]
if (titleSplitDict.hasOwnProperty(lastWord)) {
titleSplitDict[lastWord] = titleSplitDict[lastWord] + 1
} else {
titleSplitDict[lastWord] = 1
}
}
}
})
function getMaxTimesKVFromDict(dict) {
let maxKey = null
let maxTimes = null
for (const key in dict) {
if (Object.hasOwnProperty.call(dict, key)) {
const times = dict[key];
if (!maxTimes || times > maxTimes) {
maxTimes = times
maxKey = key
}
}
}
return {maxKey, maxTimes}
}
function getStringSameNum(str1, str2){
let a = str1.split('');
let b = str2.split('');
let len = 0;
let maxlength = a.length > b.length ? a : b;
let minlength = a.length < b.length ? a : b;
for(let i =0; i < minlength.length; ){
let isdelete = false;
for(let j = 0; j < maxlength.length; ){
if(minlength[i] == maxlength[j]){
len++;
maxlength.splice(j, 1)
isdelete = true;
break;
}else{
j++;
}
}
if(isdelete){
minlength.splice(i,1)
}else{
i++;
}
}
return len;
}
let timesRes = getMaxTimesKVFromDict(kwdDict)
if (!timesRes.maxKey) {
let spltRes = getMaxTimesKVFromDict(titleSplitDict)
if (spltRes.maxTimes && spltRes.maxTimes >= 3) {
// 起码出现3次重复才能判断为姓名
timesRes = spltRes
} else if (spltRes.maxKey && getStringSameNum(spltRes.maxKey, result.modelPageName) >= 2) {
// 中文和日文至少有两个字相同
timesRes = spltRes
}
}
if (timesRes.maxKey) {
result.modelPageChineseName = timesRes.maxKey
saveArtistCNName(result.modelPageName, result.modelPageChineseName)
}
}
}
// console.log('artistPageParseFromDoc', result)
return result
}
function saveArtistCNName(name, cnName) {
if (!artistCNNameMap.hasOwnProperty(name) || !artistCNNameMap[name]) {
artistCNNameMap[name] = cnName
GM_setValue(GM_KEYS.ARTIST_CN_NAME_MAP, artistCNNameMap)
}
}
function getArtistCNName(name) {
// console.log('getArtistCNName', name, artistCNNameMap)
if (artistCNNameMap[name]) {
return artistCNNameMap[name]
} else {
if (Object.keys(artistCNNameMap).length == 0) {
artistCNNameMap = GM_getValue(GM_KEYS.ARTIST_CN_NAME_MAP) || {}
}
return artistCNNameMap[name] || null
}
}
GM_addValueChangeListener(GM_KEYS.ARTIST_CN_NAME_MAP, (name, old_value, new_value, remote) => {
if (remote) {
artistCNNameMap = new_value || {}
// console.log('artist_CN_name_map-Change', new_value)
}
});
async function requestArtistPage(siteUrl) {
let result = {
modelPageName: null,
modelPageChineseName: null
}
const xhrPromise = new Promise((resolve) => {
xmlhttpRequest({
method: "GET",
url: siteUrl,
onload: (response) => {
if (response.status === 404) {
} else {
result = artistPageParser(response.responseText);
}
resolve(result)
},
onerror: (error) => {
// console.log("xhr-error", error);
resolve(result);
},
});
});
return xhrPromise;
}
var logined = false;
var userName = null;
// 根据站点检测登录状态
if (currentSite === 'jable') {
var userNameEl = document.querySelector(".d-lg-block");
if (userNameEl && userNameEl.innerText != "登入") {
logined = true;
userName = userNameEl.innerText;
}
} else if (currentSite === 'missav') {
// MissAV 默认设为已登录状态
// 点击收藏按钮时,如果未登录会自动弹出登录窗口
logined = true;
userName = 'MissAV User';
}
const Base64 = {
encode(str) {
return btoa(
encodeURIComponent(str).replace(
/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode("0x" + p1);
}
)
);
},
decode(str) {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(
atob(str)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
},
};
// console.log('saveFileDirectory:', getRealSaveFileDirectory(), downloadParams, getProxyMenuStatus() ? '' : proxyParam)
function getDownloadSchemeFromHlsUrl(url, title, models) {
var title = title
var models = models.map(model => {
return model.name
})
if (isModelPage) {
if (models && models.length > 1) {
// 如果是在艺术家页面并且该电影包含多个演员,则直接下载到该艺术家的文件夹下
models = [modelPageName]
}
}
// 处理MissAV无码视频:在番号和标题之间插入"[无码破解]"
if (currentSite === 'missav' && isUncensoredVideo(location.href)) {
// 标题格式通常是 "番号 电影名字",需要在中间插入"[无码破解]"
const titleParts = title.match(/^([A-Za-z0-9\-]+)\s+(.*)$/);
if (titleParts && titleParts.length >= 3) {
// titleParts[1] 是番号,titleParts[2] 是电影名字
title = `${titleParts[1]} [无码破解]${titleParts[2]}`;
} else {
// 如果格式不匹配,在标题开头添加标记
title = '[无码破解]' + title;
}
}
if (models && models.length === 1) {
// 检查文件名中是否包含艺术家名称
let artistName = models[0]
let artistCNName = getArtistCNName(artistName)
if (artistCNName) {
artistName = artistCNName
if (title.indexOf(artistName) < 0) {
title = title + ' ' + artistName
}
}
}
let dir = getRealSaveFileDirectory(models)
let proxy = getProxyMenuStatus() ? '' : proxyParam;
let params = `"${url}" --saveName "${title}" --workDir "${dir}" ${downloadParams}${proxy}`
let bs64 = "m3u8dl://" + Base64.encode(params);
// console.log('download-params:', params, url, title, dir, downloadParams, proxy, bs64);
return bs64;
}
// 存储MissAV的清晰度选项和当前选择
let missavQualities = [];
let selectedQualityIndex = 0;
async function detectDownload() {
// 获取页面信息
let parseResult = await videoPageParserFromDoc(document, document.documentElement.outerHTML);
// 提取hlsUrl
if (currentSite === 'jable') {
// Jable从全局变量hlsUrl获取
if (typeof hlsUrl !== 'undefined') {
parseResult.hlsUrl = hlsUrl;
}
} else if (currentSite === 'missav') {
// MissAV从Script标签中提取
const scripts = document.querySelectorAll('script');
let baseUrl = null;
// 找到包含"seek"的script并提取hash
for (let script of scripts) {
if (script.textContent && script.textContent.indexOf('seek') > -1) {
const nodeValue = script.textContent;
const index = nodeValue.indexOf('seek');
if (index !== -1 && index - 32 >= 0) {
const first32Chars = nodeValue.substring(index - 38, index - 2);
baseUrl = `https://surrit.com/${first32Chars}`;
break;
}
}
}
if (baseUrl) {
try {
// 获取主 playlist.m3u8 文件
const playlistUrl = `${baseUrl}/playlist.m3u8`;
const response = await fetch(playlistUrl);
const playlistText = await response.text();
// 解析 m3u8 文件,找出所有清晰度选项
const lines = playlistText.split('\n');
const qualities = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// 查找 #EXT-X-STREAM-INF 行(包含清晰度信息)
if (line.startsWith('#EXT-X-STREAM-INF')) {
// 提取带宽和分辨率信息
const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
const resolutionMatch = line.match(/RESOLUTION=(\d+)x(\d+)/);
// 下一行应该是m3u8文件路径
if (i + 1 < lines.length && lines[i + 1].trim()) {
const m3u8Path = lines[i + 1].trim();
qualities.push({
bandwidth: bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0,
width: resolutionMatch ? parseInt(resolutionMatch[1]) : 0,
height: resolutionMatch ? parseInt(resolutionMatch[2]) : 0,
path: m3u8Path,
label: resolutionMatch ? `${resolutionMatch[2]}P` : 'Unknown'
});
}
}
}
// 如果没有找到清晰度信息,直接使用主 playlist
if (qualities.length === 0) {
parseResult.hlsUrl = playlistUrl;
console.log('MissAV: 使用主 playlist.m3u8:', playlistUrl);
} else {
// 按带宽排序,最高清晰度在最前面
qualities.sort((a, b) => b.bandwidth - a.bandwidth);
missavQualities = qualities;
selectedQualityIndex = 0; // 默认选择最高清晰度
const bestQuality = qualities[0];
// 构建完整URL
if (bestQuality.path.startsWith('http')) {
parseResult.hlsUrl = bestQuality.path;
} else {
parseResult.hlsUrl = `${baseUrl}/${bestQuality.path}`;
}
console.log(`MissAV: 选择最高清晰度 ${bestQuality.width}x${bestQuality.height} (${Math.round(bestQuality.bandwidth/1000000)}Mbps):`, parseResult.hlsUrl);
}
} catch (error) {
console.error('MissAV: 获取M3U8清晰度信息失败:', error);
// 失败时使用默认URL
parseResult.hlsUrl = `${baseUrl}/playlist.m3u8`;
}
}
}
// console.log('detectDownload', parseResult)
var title_el = document.querySelector(getPath('video_title_path'));
if (!title_el) {
return;
}
var download_btn = document.createElement("a");
download_btn.className = "addtion";
download_btn.id = "download_m3u8";
download_btn.href = "javascript:void(0);";
// 根据站点和清晰度设置按钮文字
if (currentSite === 'missav' && missavQualities.length > 0) {
const selectedQuality = missavQualities[selectedQualityIndex];
if (logined) {
download_btn.innerText = `下载并收藏(${selectedQuality.label})`;
} else {
download_btn.innerText = `下载(无法收藏,未登录)(${selectedQuality.label})`;
}
} else {
if (logined) {
download_btn.innerText = "下载并收藏";
} else {
download_btn.innerText = "下载(无法收藏,未登录)";
}
}
download_btn.style.display = "inline-block";
download_btn.style.padding = "10px 20px";
download_btn.style.background = "cornflowerblue";
download_btn.style.color = "white";
download_btn.style.fontSize = "18px";
download_btn.style.margin = "10px 10px 10px 0";
download_btn.style.borderRadius = "5px";
title_el.appendChild(download_btn);
// 创建清晰度选择器(仅MissAV)- 放在下载按钮右边
if (currentSite === 'missav' && missavQualities.length > 0) {
const qualitySelector = document.createElement("select");
qualitySelector.id = "quality_selector";
qualitySelector.style.display = "inline-block";
qualitySelector.style.padding = "10px 15px";
qualitySelector.style.fontSize = "16px";
qualitySelector.style.margin = "10px 0";
qualitySelector.style.borderRadius = "5px";
qualitySelector.style.border = "2px solid #6495ed";
qualitySelector.style.background = "#f0f8ff";
qualitySelector.style.color = "#000";
qualitySelector.style.fontWeight = "bold";
qualitySelector.style.cursor = "pointer";
qualitySelector.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
// 添加清晰度选项
missavQualities.forEach((quality, index) => {
const option = document.createElement("option");
option.value = index;
option.text = `${quality.label} (${Math.round(quality.bandwidth/1000000)}Mbps)`;
option.style.color = "#000";
option.style.background = "#fff";
if (index === selectedQualityIndex) {
option.selected = true;
}
qualitySelector.appendChild(option);
});
// 监听选择变化
qualitySelector.addEventListener('change', function() {
selectedQualityIndex = parseInt(this.value);
const selectedQuality = missavQualities[selectedQualityIndex];
// 更新hlsUrl
const baseUrlMatch = parseResult.hlsUrl.match(/(https:\/\/surrit\.com\/[^\/]+)/);
if (baseUrlMatch) {
const baseUrl = baseUrlMatch[1];
if (selectedQuality.path.startsWith('http')) {
parseResult.hlsUrl = selectedQuality.path;
} else {
parseResult.hlsUrl = `${baseUrl}/${selectedQuality.path}`;
}
}
// 更新下载按钮文字
const download_btn = document.getElementById('download_m3u8');
if (download_btn) {
if (logined) {
download_btn.innerText = `下载并收藏(${selectedQuality.label})`;
} else {
download_btn.innerText = `下载(无法收藏,未登录)(${selectedQuality.label})`;
}
}
console.log(`MissAV: 切换清晰度到 ${selectedQuality.label}:`, parseResult.hlsUrl);
});
title_el.appendChild(qualitySelector);
}
const likeBtn = document.querySelector(getPath('video_like_btn'));
if (likeBtn) {
saveVideoPageStatus();
likeBtn.addEventListener("click", () => {
saveVideoPageStatus(true);
});
}
function checkClickLike() {
const download = () => {
let downloadLink = getDownloadSchemeFromHlsUrl(parseResult.hlsUrl, parseResult.title, parseResult.models);
// console.log('开始下载', downloadLink);
// 使用location.href而不是window.open,避免打开新标签页
window.location.href = downloadLink;
};
if (likeBtn) {
// 通过code判断收藏状态
let code = getCodeFromUrl(location.href);
let isLiked = getLiked(code);
console.log('收藏状态:', isLiked, code, liked_codes);
if (isLiked) {
var r = confirm("你已收藏此影片,可能下载过,是否继续下载?");
if (r == true) {
download();
} else {
// console.log('取消下载');
}
} else {
likeBtn.click();
download();
}
} else {
download();
}
}
download_btn.addEventListener("click", function () {
checkClickLike();
});
}
/**
* 保存视频页面的收藏状态
*/
function saveVideoPageStatus(isClick = false) {
if (!isVideoPage) {
return;
}
const likeBtn = document.querySelector(getPath('video_like_btn'));
if (!likeBtn) {
return;
}
let code = getCodeFromUrl(location.href);
let currentLike = false;
// 根据站点判断收藏状态
if (currentSite === 'missav') {
currentLike = likeBtn.classList.contains("text-primary");
} else {
currentLike = likeBtn.classList.contains("active");
}
if (isClick) {
currentLike = !currentLike;
} else {
if (!currentLike) {
return;
}
}
setLiked(code, currentLike);
}
/**
* 同步收藏按钮状态
* @param {boolean} shouldBeLiked - 目标收藏状态
* @param {boolean} forceClick - 是否强制点击(即使状态已匹配)
*/
function syncFavoriteButton(shouldBeLiked, forceClick = false) {
if (!isVideoPage || syncInProgress) {
return;
}
const likeBtn = document.querySelector(getPath('video_like_btn'));
if (!likeBtn) {
console.log('[收藏同步] 未找到收藏按钮');
return;
}
let currentlyLiked = currentSite === 'missav'
? likeBtn.classList.contains("text-primary")
: likeBtn.classList.contains("active");
// 只在状态不匹配时点击,或强制点击
if (currentlyLiked !== shouldBeLiked || forceClick) {
console.log(`[收藏同步] 同步收藏状态: ${currentlyLiked} -> ${shouldBeLiked} (${currentSite})`);
syncInProgress = true; // 设置同步标志
// 点击按钮
likeBtn.click();
// 延迟重置同步标志
setTimeout(() => {
syncInProgress = false;
}, 1000);
} else {
console.log(`[收藏同步] 状态已同步,无需操作 (${currentSite})`);
}
}
/**
* 页面加载时检查并同步收藏状态
*/
function checkAndSyncOnLoad() {
if (!isVideoPage) {
return;
}
let code = getCodeFromUrl(location.href);
let normalized = normalizeCode(code);
// 检查是否在全局收藏列表中
if (liked_codes.length === 0) {
initialLikedCodes();
}
let shouldBeLiked = liked_codes.indexOf(normalized) >= 0;
console.log(`[收藏同步] 页面加载检查: ${normalized}, 应该收藏: ${shouldBeLiked}`);
if (shouldBeLiked) {
// 延迟确保页面完全加载
setTimeout(() => {
syncFavoriteButton(true);
}, 1500);
}
}
var mouse_timer = null; // 定时器
var manual_loaded_codes = {};
function createNode(htmlStr) {
var div = document.createElement("div");
div.innerHTML = htmlStr;
return div.childNodes[0];
}
function isValidClassName(name) {
return name.match(/-?[_a-zA-Z]+[_a-zA-Z0-9-]*/)
}
// update website CSS
function updateBoxCardCSS(forceLoadLikeStatus = false) {
var imgBoxes = document.querySelectorAll(".video-img-box");
for (let index = 0; index < imgBoxes.length; index++) {
const box = imgBoxes[index];
let title = box.querySelector(".title");
if (!title) {
return;
}
let subTitle = box.querySelector(".sub-title");
if (
subTitle &&
subTitle.innerText &&
subTitle.innerText.split("\n").length >= 2
) {
// 根据观看数和点赞数设置标签
let playText = subTitle.innerText.split("\n")[0];
let likeText = subTitle.innerText.split("\n")[1];
if (playText && likeText) {
let playCount = parseInt(playText.replaceAll(" ", ""));
let likeCount = parseInt(likeText);
if (playCount > 1300000 || likeCount > 13000) {
box.classList.add("hot-3");
} else if (playCount > 1000000 || likeCount > 10000) {
box.classList.add("hot-2");
} else if (playCount > 500000 || likeCount > 5000) {
box.classList.add("hot-1");
}
}
}
let titleLink = title.querySelector("a");
if (titleLink && titleLink.href && isVideoURL(titleLink.href)) {
let code = getCodeFromUrl(titleLink.href);
// 保存代码映射
if (currentSite === 'jable') {
saveCodeMapping(code, null);
} else if (currentSite === 'missav') {
saveCodeMapping(null, code);
}
if (code) {
let className = code
if (!isValidClassName(className)) {
className = 'valid-' + className
}
if (!box.classList.contains(className)) {
box.classList.add(className);
let heartEls = box.querySelectorAll(".action");
heartEls.forEach((heartEl) => {
if (heartEl) {
if (heartEl.classList.contains("fav-restore")) {
heartEl.addEventListener("click", (event) => {
event.preventDefault();
setLiked(code, true);
loadBoxStatus(box, code);
});
} else if (heartEl.classList.contains("fav-remove")) {
heartEl.addEventListener("click", (event) => {
event.preventDefault();
setLiked(code, false);
loadBoxStatus(box, code);
});
} else {
heartEl.classList.add("like");
heartEl.addEventListener("click", (event) => {
event.preventDefault();
let liked = !heartEl.classList.contains("active");
setLiked(code, liked);
loadBoxStatus(box, code);
// console.log('heartEl-click', code, liked);
setTimeout(() => {
requestLike(heartEl, liked)
}, 100);
});
}
}
});
let coverAEl = box.querySelector(".img-box a");
if (coverAEl) {
let downloadbtn = createNode('<div class="absolute-bottom-left download"><span class="action download d-sm-flex"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"><path fill="#ffffff" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-8 4v-5h2v3h12v-3h2v5z"></path></svg></span></div>')
coverAEl.appendChild(downloadbtn);
downloadbtn.addEventListener("click", (event) => {
event.preventDefault();
downloadFilm(box, code);
});
}
function stopMouseTimer() {
clearTimeout(mouse_timer);
mouse_timer = null;
}
box.addEventListener(
"mouseenter",
(event) => {
stopMouseTimer();
if (!manual_loaded_codes.hasOwnProperty(code)) {
mouse_timer = setTimeout(() => {
stopMouseTimer();
getFilmResult(code);
}, 500);
}
},
false
);
box.addEventListener(
"mouseleave",
(event) => {
if (mouse_timer) {
stopMouseTimer();
}
},
false
);
loadBoxStatus(box, code);
} else if (forceLoadLikeStatus) {
loadBoxStatus(box, code);
}
}
}
}
}
function downloadFilm(box, code) {
let result = manual_loaded_codes[code];
let liked = getLiked(code);
if (result && result.hlsUrl && result.title) {
let likeBtn = box.querySelector(".action.like");
const download = () => {
let downloadLink = getDownloadSchemeFromHlsUrl(result.hlsUrl, result.title, result.models);
// console.log('开始下载', downloadLink, result);
// 使用location.href而不是window.open,避免打开新标签页
window.location.href = downloadLink;
};
if (likeBtn) {
if (liked) {
var r = confirm("你已收藏此影片,可能下载过,是否继续下载?");
if (r == true) {
download();
}
} else {
likeBtn.click();
download();
}
} else {
download();
}
}
}
async function requestLike(heartEl, liked) {
if (!logined || !heartEl) {
return;
}
const action = liked ? "add_to_favourites" : "delete_from_favourites";
const fav_id = heartEl.getAttribute("data-fav-video-id");
const url = `${location.href}?mode=async&format=json&action=${action}&video_id=${fav_id}&video_ids%5B%5D=${fav_id}&fav_type=0&playlist_id=0`;
if (!fav_id) {
return;
}
// console.log("requestLike-start", url);
const xhrPromise = new Promise((resolve) => {
xmlhttpRequest({
method: "GET",
url: url,
onload: (response) => {
// console.log("requestLike-done", response);
if (response.status === 404) {
resolve({
status: "fail",
});
} else {
resolve({
status: "success",
});
}
},
onerror: (error) => {
console.log("requestLike-error", error);
resolve({
status: "fail",
});
},
});
});
return xhrPromise;
}
async function loadAllMyFavorites() {
if (!logined) {
return;
}
// 监听全局收藏列表变化(跨标签页/跨站点同步)
GM_addValueChangeListener(
GM_KEYS.GLOBAL_LIKED_CODES,
(name, old_value, new_value, remote) => {
if (remote) {
liked_codes = new_value || [];
console.log("[收藏同步] 收藏列表远程更新:", liked_codes.length, '个');
updateBoxCardCSS(true);
// 检查当前视频页面是否需要同步收藏状态
if (isVideoPage && !syncInProgress) {
let currentCode = getCodeFromUrl(location.href);
let normalizedCurrent = normalizeCode(currentCode);
// 检查当前视频的收藏状态是否变化
let wasLiked = old_value ? old_value.indexOf(normalizedCurrent) >= 0 : false;
let isLiked = new_value ? new_value.indexOf(normalizedCurrent) >= 0 : false;
if (wasLiked !== isLiked) {
console.log(`[收藏同步] 检测到当前视频收藏状态变化: ${wasLiked} -> ${isLiked}`);
setTimeout(() => {
syncFavoriteButton(isLiked);
}, 500);
}
}
}
}
);
const usrkey = GM_KEYS.GLOBAL_FAVORITES_INITIALIZED; // 使用全局key
if (GM_getValue(usrkey)) {
return;
}
var isSuccess = true;
var codes = [];
var result = await requestFavoritesPage(1);
if (result.status == "success") {
codes = codes.concat(result.liked_codes);
while (result.next) {
result = await requestFavoritesPage(result.next);
if (result.status == "success") {
codes = codes.concat(result.liked_codes);
} else {
isSuccess = false;
}
}
} else {
isSuccess = false;
}
if (isSuccess) {
GM_setValue(usrkey, true);
liked_codes = codes;
// console.log("set_liked_codes-1", "global_liked_codes", liked_codes);
GM_setValue(GM_KEYS.GLOBAL_LIKED_CODES, liked_codes); // 使用全局key
updateBoxCardCSS(true);
}
}
function favouritesPageParser(responseText) {
let res = {
status: "fail",
current: 0,
next: 0,
total: 0,
liked_codes: [],
};
const doc = new DOMParser().parseFromString(responseText, "text/html");
const page_item = doc.querySelectorAll(".page-item");
if (page_item && page_item.length > 0) {
let currentCount = 0;
let totalCount = 0;
let nextCount = 0;
const current = doc.querySelector(".page-item .page-link.active");
if (current && current.innerText) {
currentCount = parseInt(current.innerText);
res.current = currentCount;
}
const total = doc.querySelector(".page-item:last-child .page-link");
if (total && total.innerText) {
if (total.classList.contains("active")) {
res.total = total.innerText;
} else {
let parameters = total.attributes["data-parameters"].value;
parameters = parameters.split(";");
for (let index = 0; index < parameters.length; index++) {
const element = parameters[index];
if (element.indexOf("from_my_fav_videos:") == 0) {
res.total = element.split(":")[1];
break;
}
}
}
if (res.total) {
totalCount = parseInt(res.total);
res.total = totalCount;
}
}
if (currentCount && totalCount && currentCount < totalCount) {
nextCount = currentCount + 1;
res.next = nextCount;
}
}
let links = doc.querySelectorAll(".video-img-box .detail .title a");
if (links && links.length > 0) {
let liked_codes = [];
for (let index = 0; index < links.length; index++) {
const element = links[index];
if (element.href.indexOf(linkPrefix) == 0) {
let code = getCodeFromUrl(element.href);
let normalized = normalizeCode(code); // 标准化代码
liked_codes.push(normalized);
// 保存Jable代码映射
saveCodeMapping(code, null);
}
}
res.liked_codes = liked_codes;
if (liked_codes.length > 0) {
res.status = "success";
}
}
return res;
}
async function requestFavoritesPage(page) {
// console.log("requestFavoritesPage-start", page);
let url = `https://jable.tv/my/favourites/videos/?mode=async&function=get_block&block_id=list_videos_my_favourite_videos&fav_type=0&playlist_id=0&sort_by=&from_my_fav_videos=${page}&_=${new Date().getTime()}`;
const xhrPromise = new Promise((resolve) => {
xmlhttpRequest({
method: "GET",
url: url,
onload: (response) => {
if (response.status === 404) {
resolve({
status: "fail",
});
} else {
const res = favouritesPageParser(response.responseText);
// console.log("requestFavoritesPage-done", page, res);
resolve(res);
}
},
onerror: (error) => {
// console.log("requestFavoritesPage-error", error);
resolve({
status: "fail",
});
},
});
});
return xhrPromise;
}
/**
* 检查视频是否已收藏(使用标准化代码)
*/
function getLiked(code) {
if (liked_codes.length === 0) {
initialLikedCodes();
}
let normalized = normalizeCode(code);
return liked_codes.indexOf(normalized) >= 0;
}
/**
* 初始化收藏列表
*/
function initialLikedCodes() {
let res = GM_getValue(GM_KEYS.GLOBAL_LIKED_CODES); // 使用全局key
liked_codes = res || [];
console.log("[收藏同步] 初始化收藏列表:", liked_codes.length, '个');
// 自动迁移旧数据(只在第一次运行时)
migrateOldUserData();
// 标准化现有数据(只在第一次运行时)
normalizeExistingCodes();
}
/**
* 标准化现有的收藏代码
* 将旧的大写代码转换为小写,确保跨站匹配
*/
function normalizeExistingCodes() {
const normalizationKey = "codes_normalization_completed";
if (GM_getValue(normalizationKey)) {
return; // 已经标准化过
}
try {
if (liked_codes.length === 0) {
GM_setValue(normalizationKey, true);
return;
}
let originalCount = liked_codes.length;
let normalizedCodes = [];
let changed = false;
console.log('[代码标准化] 开始标准化现有收藏代码...');
liked_codes.forEach(code => {
let normalized = normalizeCode(code);
if (normalizedCodes.indexOf(normalized) === -1) {
normalizedCodes.push(normalized);
}
if (code !== normalized) {
changed = true;
}
});
if (changed) {
liked_codes = normalizedCodes;
GM_setValue(GM_KEYS.GLOBAL_LIKED_CODES, liked_codes);
console.log(`[代码标准化] 完成! 原始: ${originalCount}, 标准化后: ${normalizedCodes.length}`);
} else {
console.log('[代码标准化] 所有代码已是标准格式');
}
GM_setValue(normalizationKey, true);
} catch (error) {
console.error('[代码标准化] 错误:', error);
}
}
/**
* 迁移旧的用户名key数据到新的全局key
* 遍历所有GM存储的key,找到所有_liked_codes结尾的旧数据并合并
*/
function migrateOldUserData() {
// 检查是否已经迁移过
const migrationKey = "data_migration_completed";
if (GM_getValue(migrationKey)) {
return; // 已经迁移过,不再重复迁移
}
try {
// 获取所有GM存储的key
let allKeys = GM_listValues();
let migratedCodes = [];
let originalCount = liked_codes.length;
let migratedUsers = [];
let oldKeysToDelete = []; // 记录需要删除的旧key
console.log('[数据迁移] 开始检查旧数据...');
console.log('[数据迁移] 找到的所有存储key:', allKeys);
// 查找所有旧的用户收藏列表key
allKeys.forEach(key => {
// 匹配格式: {userName}_liked_codes
if (key.endsWith('_liked_codes') && key !== GM_KEYS.GLOBAL_LIKED_CODES) {
let oldCodes = GM_getValue(key);
if (Array.isArray(oldCodes) && oldCodes.length > 0) {
let userName = key.replace('_liked_codes', '');
console.log(`[数据迁移] 发现用户 "${userName}" 的旧收藏数据:`, oldCodes.length, '个');
// 合并到迁移列表
oldCodes.forEach(code => {
if (migratedCodes.indexOf(code) === -1) {
migratedCodes.push(code);
}
});
migratedUsers.push(userName);
oldKeysToDelete.push(key); // 添加到待删除列表
}
}
// 同时查找并标记旧的 favorites_initialized_status key
if (key.endsWith('_favorites_initialized_status')) {
oldKeysToDelete.push(key);
}
});
if (migratedCodes.length > 0) {
// 合并到当前的liked_codes
migratedCodes.forEach(code => {
if (liked_codes.indexOf(code) === -1) {
liked_codes.push(code);
}
});
// 保存合并后的数据
GM_setValue(GM_KEYS.GLOBAL_LIKED_CODES, liked_codes);
console.log(`[数据迁移] 迁移完成!`);
console.log(`[数据迁移] 原有收藏: ${originalCount} 个`);
console.log(`[数据迁移] 迁移用户: ${migratedUsers.join(', ')}`);
console.log(`[数据迁移] 发现旧数据: ${migratedCodes.length} 个`);
console.log(`[数据迁移] 新增收藏: ${liked_codes.length - originalCount} 个`);
console.log(`[数据迁移] 合并后总计: ${liked_codes.length} 个`);
// 删除所有旧的key
if (oldKeysToDelete.length > 0) {
console.log(`[数据迁移] 开始清理旧数据...`);
let deletedCount = 0;
oldKeysToDelete.forEach(key => {
try {
GM_deleteValue(key);
deletedCount++;
console.log(`[数据迁移] 已删除旧key: ${key}`);
} catch (error) {
console.error(`[数据迁移] 删除key失败: ${key}`, error);
}
});
console.log(`[数据迁移] 清理完成,共删除 ${deletedCount} 个旧key`);
}
// 显示迁移提示(仅在有数据迁移时)
if (liked_codes.length > originalCount) {
// setTimeout(() => {
// alert(
// `检测到旧收藏数据并已自动迁移!\n\n` +
// `原有收藏: ${originalCount} 个\n` +
// `发现旧数据: ${migratedCodes.length} 个(来自 ${migratedUsers.length} 个用户)\n` +
// `新增收藏: ${liked_codes.length - originalCount} 个\n` +
// `合并后总计: ${liked_codes.length} 个\n\n` +
// `迁移的用户: ${migratedUsers.join(', ')}\n` +
// `已清理旧数据: ${oldKeysToDelete.length} 个key\n\n` +
// `现在Jable和MissAV将共享同一个收藏列表!`
// );
// }, 1000);
}
} else {
console.log('[数据迁移] 未发现需要迁移的旧数据');
// 即使没有数据需要迁移,也检查是否有需要清理的旧key
if (oldKeysToDelete.length > 0) {
console.log(`[数据迁移] 发现 ${oldKeysToDelete.length} 个空的旧key,开始清理...`);
oldKeysToDelete.forEach(key => {
try {
GM_deleteValue(key);
console.log(`[数据迁移] 已删除空key: ${key}`);
} catch (error) {
console.error(`[数据迁移] 删除key失败: ${key}`, error);
}
});
}
}
// 标记迁移已完成
GM_setValue(migrationKey, true);
console.log('[数据迁移] 迁移流程结束');
} catch (error) {
console.error('[数据迁移] 迁移过程出错:', error);
}
}
/**
* 设置视频收藏状态(使用标准化代码)
*/
function setLiked(code, liked) {
let normalized = normalizeCode(code);
// 保存代码映射
if (currentSite === 'jable') {
saveCodeMapping(code, null);
} else if (currentSite === 'missav') {
saveCodeMapping(null, code);
}
if (liked) {
if (liked_codes.indexOf(normalized) < 0) {
liked_codes.push(normalized);
console.log('[收藏同步] 添加收藏:', normalized, '来源:', currentSite);
GM_setValue(GM_KEYS.GLOBAL_LIKED_CODES, liked_codes);
}
} else {
let index = liked_codes.indexOf(normalized);
if (index >= 0) {
liked_codes.splice(index, 1);
console.log('[收藏同步] 取消收藏:', normalized, '来源:', currentSite);
GM_setValue(GM_KEYS.GLOBAL_LIKED_CODES, liked_codes);
}
}
}
function loadBoxStatus(boxEl, code) {
let liked = getLiked(code);
if (boxEl) {
let heartEl = boxEl.querySelector(".action.like");
if (liked) {
boxEl.classList.add("liked");
if (heartEl) {
heartEl.classList.add("active");
}
} else {
if (boxEl.classList.contains("liked")) {
boxEl.classList.remove("liked");
if (heartEl && heartEl.classList.contains("active")) {
heartEl.classList.remove("active");
}
}
}
let requestData = manual_loaded_codes[code];
let downloadEl = boxEl.querySelector(".action.download");
if (requestData && requestData.hlsUrl) {
boxEl.classList.add("hasurl");
if (downloadEl) {
downloadEl.setAttribute("hlsUrl", requestData.hlsUrl);
}
} else {
boxEl.classList.remove("hasurl");
if (downloadEl) {
downloadEl.removeAttribute("hlsUrl");
}
}
if (requestData && requestData.avatarDom) {
let subTitle = boxEl.querySelector(".detail .sub-title");
if (subTitle && !subTitle.classList.contains("added-avatar")) {
subTitle.classList.add("added-avatar");
subTitle.appendChild(requestData.avatarDom);
}
}
}
}
async function getFilmResult(code) {
if (!logined) {
return;
}
let className = code
if (!isValidClassName(className)) {
className = 'valid-' + className
}
let boxEl = document.querySelector(`.video-img-box.${className}`);
let downloadEl = boxEl.querySelector(".action.download");
if (downloadEl) {
downloadEl.classList.add('loading')
}
// console.log("getFilmResult", code);
let item = {
status: "loading",
targetLink: `${linkPrefix}${code}/`,
hlsUrl: null,
models: [],
title: "",
code: code,
avatarDom: null,
request_at: 0,
liked: false,
};
const resItem = await requestVideoPage(item);
if (resItem.status != "success") {
return;
}
let liked = getLiked(code);
if (liked && !resItem.liked) {
// console.log('请求结果与记录不符,正在重新点赞!!!', code, resItem)
resItem.liked = true;
let heartEl = boxEl.querySelector(".action.like");
if (heartEl) {
requestLike(heartEl, true);
}
}
manual_loaded_codes[code] = resItem;
setLiked(code, resItem.liked);
// console.log("getFilmResult-finish", resItem);
if (downloadEl) {
downloadEl.classList.remove('loading')
}
loadBoxStatus(boxEl, code);
}
async function videoPageParser(responseText) {
const doc = new DOMParser().parseFromString(responseText, "text/html");
let result = await videoPageParserFromDoc(doc, responseText)
// Jable的M3U8提取
if (currentSite === 'jable') {
var regex = /var\s+hlsUrl\s*=\s*['"]([^'"]+)['"]/;
var match = responseText.match(regex);
if (match && match.length > 1) {
result.hlsUrl = match[1];
// console.log("提取到的 hlsUrl 值为:", result.hlsUrl);
}
}
// MissAV的M3U8提取
else if (currentSite === 'missav') {
const scriptMatch = responseText.match(/<script[^>]*>([\s\S]*?seek[\s\S]*?)<\/script>/);
if (scriptMatch && scriptMatch[1]) {
const nodeValue = scriptMatch[1];
const index = nodeValue.indexOf('seek');
if (index !== -1 && index - 32 >= 0) {
const first32Chars = nodeValue.substring(index - 38, index - 2);
result.hlsUrl = `https://surrit.com/${first32Chars}/playlist.m3u8`;
// console.log("提取到的 MissAV hlsUrl:", result.hlsUrl);
}
}
}
return result
}
async function videoPageParserFromDoc(doc, responseText = null) {
let res = {
isSuccess: false,
liked: false,
models: [],
hlsUrl: null,
title: "",
avatarDom: null,
};
const likeBtn = doc.querySelector(getPath('video_like_btn'));
if (likeBtn) {
res.isSuccess = true;
// 根据站点判断收藏状态
if (currentSite === 'missav') {
res.liked = likeBtn.classList.contains("text-primary");
} else {
res.liked = likeBtn.classList.contains("active");
}
}
var title_el = doc.querySelector(getPath('video_title_path'));
if (title_el && title_el.innerText) {
res.title = title_el.innerText;
}
var avatar_el = doc.querySelector(getPath('video_avatar_path'));
if (avatar_el) {
res.avatarDom = avatar_el;
let models = []
// Jable 的演员信息提取
if (currentSite === 'jable') {
let aModel = avatar_el.querySelectorAll('a.model')
aModel.forEach(a => {
let rc = a.querySelector('.rounded-circle')
let title = rc.getAttribute('title') || rc.getAttribute('data-original-title')
// console.log('title',title)
if (a.href && title) {
models.push({
name: title,
url: a.href
})
}
})
}
// MissAV 的演员信息提取
else if (currentSite === 'missav') {
// 查找 <div class="text-secondary">中第一个子元素为<span>女优:</span>的元素
const allSecondary = doc.querySelectorAll('div.text-secondary');
allSecondary.forEach(div => {
const firstChild = div.querySelector('span');
if (firstChild && firstChild.textContent.trim() === '女优:') {
// 提取所有 <a> 标签
const actressLinks = div.querySelectorAll('a.text-nord13.font-medium');
actressLinks.forEach(a => {
if (a.href && a.textContent) {
models.push({
name: a.textContent.trim(),
url: a.href
});
}
});
}
});
}
res.models = models
if (res.models.length == 1) {
let cnName = getArtistCNName(res.models[0].name)
if (!cnName) {
await requestArtistPage(res.models[0].url)
}
}
}
return res;
}
async function requestVideoPage(siteItem) {
const siteUrl = siteItem.targetLink;
const xhrPromise = new Promise((resolve) => {
xmlhttpRequest({
method: "GET",
url: siteUrl,
onload: async (response) => {
siteItem.request_at = new Date().getTime();
if (response.status === 404) {
siteItem.status = "fail";
resolve(siteItem);
} else {
const { isSuccess, liked, hlsUrl, title, avatarDom, models } = await videoPageParser(response.responseText);
siteItem.status = isSuccess ? "success" : "fail";
siteItem.liked = liked;
siteItem.hlsUrl = hlsUrl
siteItem.models = models
siteItem.title = title
siteItem.avatarDom = avatarDom
setTimeout(() => {
resolve(siteItem);
}, 200);
}
},
onerror: (error) => {
// console.log("xhr-error", error);
siteItem.status = "fail";
resolve(siteItem);
},
});
});
return xhrPromise;
}
function observePageMutations() {
var targetNode = document.body;
var observerOptions = {
childList: true, // Observe direct children being added or removed
subtree: true, // Observe all descendants of the target node
};
var observer = new MutationObserver(function (mutationsList, observer) {
updateBoxCardCSS();
});
observer.observe(targetNode, observerOptions);
}
(function main() {
if (currentSite === 'jable') {
addStyle(jableStyle);
}
window.addEventListener("load", () => {
initialLikedCodes();
console.log("Jable一键下载收藏.js", isVideoPage);
console.log("[收藏同步] 跨站收藏同步功能已启用");
// MissAV页面DOM可能动态加载,延迟再次检查
if (currentSite === 'missav' && !isVideoPage) {
setTimeout(() => {
isVideoPage = isVideoURL(location.href);
console.log("MissAV延迟检测视频页:", isVideoPage);
if (isVideoPage) {
detectDownload();
checkAndSyncOnLoad(); // 检查并同步收藏状态
}
}, 500);
} else if (isVideoPage) {
detectDownload();
checkAndSyncOnLoad(); // 检查并同步收藏状态
}
updateBoxCardCSS();
observePageMutations();
loadAllMyFavorites();
});
})();
})();