Jable一键下载收藏

Jable一键下载视频,并自动点击收藏

As of 2024-02-15. See the latest version.

// ==UserScript==
// @name         Jable一键下载收藏
// @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      1.3.4
// @description  Jable一键下载视频,并自动点击收藏
// @author       Pandex
// @match        *://jable.tv/*
// @match        *://fs1.app/*
// @connect      jable.tv
// @connect      fs1.app
// @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_removeValueChangeListener
// @grant        GM_addValueChangeListener
// @license      MPL
// ==/UserScript==

(function () {
  const saveFileDirectory = "D:\\videos\\jav";
  const downloadParams =
    ' --maxThreads "48" --minThreads "16" --retryCount "100" --timeOut "100" --enableDelAfterDone';
  const autoDetectLiked = true;

  var _a, _b, _c, _d;
  ("use strict");

  var linkPrefix = `https://${location.host}/videos/`;

  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),
    b = (...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: '❤️ ';
    }

    .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 = {
    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",
  };

  function isVideoURL(url) {
    return !!url.match(/https:\/\/(jable\.tv|fs1\.app)\/videos\/*\/*/);
  }

  function isModelURL(url) {
    return (
      !!url.match(/https:\/\/(jable\.tv|fs1\.app)\/models\/*\/*/) ||
      !!url.match(/https:\/\/(jable\.tv|fs1\.app)\/s1\/models\/*\/*/)
    );
  }

  function isHotURL(url) {
    return !!url.match(/https:\/\/(jable\.tv|fs1\.app)\/hot\/*\/*/);
  }

  function getCodeFromUrl(url) {
    let code = url.replace(linkPrefix, "").replace(/\/[\s\S]*$/, "");
    return code;
  }

  var liked_codes = [];

  var isVideoPage = isVideoURL(location.href);
  var isModelPage = isModelURL(location.href);
  var isHotPage = isHotURL(location.href);

  var logined = false;
  var userName = null;
  var userNameEl = document.querySelector(".d-lg-block");
  if (userNameEl && userNameEl.innerText != "登入") {
    logined = true;
    userName = userNameEl.innerText;
  }

  function detectDownload() {
    let 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("")
        );
      },
    };

    var title_path =
      "#site-content > div > div > div:nth-child(1) > section.video-info.pb-3 > div.info-header > div.header-left > h4";
    var title_el = document.querySelector(title_path);
    var title = title_el.innerText;
    var url = hlsUrl;

    var download_btn = document.createElement("a");
    download_btn.className = "addtion";
    download_btn.id = "download_m3u8";
    var params =
      '"' +
      url +
      '"' +
      '  --saveName "' +
      title +
      '" --workDir "' +
      saveFileDirectory +
      '"' +
      downloadParams;
    params = Base64.encode(params);
    var downloadLink = "m3u8dl://" + params;
    download_btn.href = "javascript:void(0);";
    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 = "0 10px";
    download_btn.style.borderRadius = "5px";
    title_el.appendChild(download_btn);

    const likeBtn = document.querySelector(paths.video_like_btn);
    saveVideoPageStatus();
    likeBtn.addEventListener("click", () => {
      saveVideoPageStatus(true);
    });

    function checkClickLike() {
      const download = () => {
        // console.log('开始下载', downloadLink);
        window.open(downloadLink, "_blank");
      };
      if (likeBtn) {
        if (likeBtn.classList.contains("active")) {
          var r = confirm("你已收藏此影片,可能下载过,是否继续下载?");
          if (r == true) {
            download();
          } else {
            // console.log('取消下载');
          }
        } else {
          likeBtn.click();
          download();
        }
      } else {
        download();
      }
    }
    document
      .getElementById("download_m3u8")
      .addEventListener("click", function () {
        checkClickLike();
      });
  }

  function saveVideoPageStatus(isClick = false) {
    if (!isVideoPage) {
      return;
    }
    const likeBtn = document.querySelector(paths.video_like_btn);
    if (!likeBtn) {
      return;
    }
    let code = getCodeFromUrl(location.href);
    let currentLike = likeBtn.classList.contains("active");
    if (isClick) {
      currentLike = !currentLike;
    }
    setLiked(code, currentLike);
  }
  var mouse_code = null;
  var mouse_timer = null; // 定时器
  var manual_loaded_codes = [];

  // 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 (code) {
          if (!box.classList.contains(code)) {
            box.classList.add(code);
            let heartEl = box.querySelector(".action");
            if (heartEl) {
              heartEl.addEventListener("click", () => {
                let liked = !heartEl.classList.contains("active");
                setLiked(code, liked);
                loadBoxStatus(box, liked);
              });
            }

            function stopMouseTimer() {
              clearTimeout(mouse_timer);
              mouse_timer = null;
            }
            box.addEventListener(
              "mouseenter",
              (event) => {
                mouse_code = code;
                stopMouseTimer();
                if (manual_loaded_codes.indexOf(code) < 0) {
                  mouse_timer = setTimeout(() => {
                    stopMouseTimer();
                    getFilmResult(code);
                    manual_loaded_codes.push(code);
                  }, 500);
                }
              },
              false
            );
            box.addEventListener(
              "mouseleave",
              (event) => {
                mouse_code = null;
                if (mouse_timer) {
                  stopMouseTimer();
                }
              },
              false
            );

            loadBoxStatus(box, getLiked(code));
          } else if (forceLoadLikeStatus) {
            loadBoxStatus(box, getLiked(code));
          }
        }
      }
    }
  }

  async function loadAllMyFavorites() {
    if (!logined) {
      return;
    }

    GM_addValueChangeListener(
      userName + "_liked_codes",
      (name, old_value, new_value, remote) => {
        console.log(
          "GM_addValueChangeListener",
          name,
          old_value,
          new_value,
          remote
        );
        if (remote) {
          liked_codes = new_value;
          console.log("initial-liked_codes-refresh", liked_codes);
          updateBoxCardCSS(true);
        }
      }
    );

    const usrkey = userName + "_favorites_initialized_status";
    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", userName + "_liked_codes", liked_codes);
      GM_setValue(userName + "_liked_codes", liked_codes);
      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) {
          liked_codes.push(getCodeFromUrl(element.href));
        }
      }
      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) => {
      b({
        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) {
      return liked_codes.indexOf(code) >= 0;
    }
    initialLikedCodes()
    return liked_codes.indexOf(code) >= 0;
  }

  function initialLikedCodes() {
    let res = GM_getValue(userName + "_liked_codes");
    liked_codes = res || [];
    console.log("initial-liked_codes", liked_codes);
  }

  function setLiked(code, liked) {
    if (liked) {
      if (liked_codes.indexOf(code) < 0) {
        liked_codes.push(code);
        console.log(
          "set_liked_codes-2",
          userName + "_liked_codes",
          liked_codes
        );
        GM_setValue(userName + "_liked_codes", liked_codes);
      }
    } else {
      let index = liked_codes.indexOf(code);
      if (index >= 0) {
        liked_codes.splice(index, 1);
        console.log(
          "set_liked_codes-3",
          userName + "_liked_codes",
          liked_codes
        );
        GM_setValue(userName + "_liked_codes", liked_codes);
      }
    }
  }

  function loadBoxStatus(boxEl, liked) {
    if (boxEl) {
      let heartEl = boxEl.querySelector(".action");
      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");
          }
        }
      }
    }
  }

  async function getFilmResult(code) {
    if (!logined) {
      return;
    }

    console.log("getFilmResult", code);
    let item = {
      status: "loading",
      targetLink: `${linkPrefix}${code}/`,
      code: code,
      liked: false,
    };
    const resItem = await requestVideoPage(item);
    setLiked(code, resItem.liked);
    console.log("getFilmResult-finish", resItem);

    let boxEl = document.querySelector(`.video-img-box.${code}`);
    loadBoxStatus(boxEl, resItem.liked);
  }

  function videoPageParser(responseText) {
    let res = {
      isSuccess: false,
      liked: false,
    };
    const doc = new DOMParser().parseFromString(responseText, "text/html");
    const likeBtn = doc.querySelector(paths.video_like_btn);
    if (likeBtn) {
      res.isSuccess = true;
      if (likeBtn.classList.contains("active")) {
        res.liked = true;
      }
    }
    return res;
  }

  async function requestVideoPage(siteItem) {
    const siteUrl = siteItem.targetLink;
    const xhrPromise = new Promise((resolve) => {
      b({
        method: "GET",
        url: siteUrl,
        onload: (response) => {
          if (response.status === 404) {
            siteItem.status = "fail";
            resolve(siteItem);
          } else {
            const { isSuccess, liked } = videoPageParser(response.responseText);
            siteItem.status = isSuccess ? "success" : "fail";
            siteItem.liked = liked;
            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() {
    addStyle(jableStyle);
    window.addEventListener("load", () => {
      initialLikedCodes();
      if (isVideoPage) {
        detectDownload();
      }
      updateBoxCardCSS();
      observePageMutations();
      loadAllMyFavorites();
    });
  })();
})();