Jable一键下载收藏 (支持MissAV)

Jable和MissAV一键下载视频,并自动点击收藏(MissAV自动选择最高清晰度,全局收藏列表,自动迁移旧数据,跨站收藏同步)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
    });
  })();
})();