mo (LDH) 下载器

在mo的内容页增加图片和视频下载的按钮, 解锁右键功能

ही स्क्रिप्ट इंस्टॉल करा?
लेखकाने सुचवलेली स्क्रिप्ट

तुम्हाला कदाचित ही स्क्रिप्टदेखील आवडेल: m3u8视频侦测下载器【自动嗅探】.

ही स्क्रिप्ट इंस्टॉल करा
// ==UserScript==
// @name                mo (LDH) 下载器
// @namespace           https://1mether.me/
// @version             0.85
// @description         在mo的内容页增加图片和视频下载的按钮, 解锁右键功能
// @author              乙醚(@locoda)
// @match               http*://m.tribe-m.jp/*
// @match               http*://m.ex-m.jp/*
// @match               http*://m.ldh-m.jp/*
// @match               http*://m.ldhgirls-m.jp/*
// @match               http*://*.exfamily.jp/*
// @icon                https://www.google.com/s2/favicons?sz=64&domain=ldh.co.jp
// @source              https://github.com/locoda/mo-downloader
// @license             MIT
// ==/UserScript==

(function () {
  "use strict";
  // ================
  // =    Consts    =
  // ================

  const isMobile = () =>
    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
      navigator.userAgent
    );

  const keywords = ["uplcmn", "upload"];

  // ==============
  // =    Main    =
  // ==============

  // Mo 下载图片
  if (window.location.host.includes("-m.jp")) {
    // 删除图片保护
    removeProtectImg();
    // 在详情页注入按钮
    if (window.location.pathname.includes("detail")) {
      injectDownloadAllButtons();
    }
    // 在视频页面注入按钮
    if (window.location.pathname.includes("movie")) {
      if (document.querySelector("div.limelight-player")) {
        injectPerVideoDownloadButton(document);
      }
    }
    // 在时间轴界面初始化并设置Listener
    if (window.location.pathname.includes("timeline")) {
      // 等待加载scroll
      (function init() {
        var counter = document.querySelector("ldh-infinite-scroll");
        if (counter) {
          // 设置按钮和监听
          removeProtectImg();
          customizedTimelinePage();
        } else {
          setTimeout(init, 300);
        }
      })();
    }
    // 在A写页面注入按钮
    if (
      window.location.pathname.includes("artistphoto") ||
      window.location.pathname.includes("artist_photo")
    ) {
      injectArtistPhotoDownloadButton();
    }
    // 在Offshot页面注入按钮
    if (window.location.pathname.includes("ldh_off_shot")) {
      injectOffshotDownloadButton();
    }
  }

  // FC 下载图片
  if (window.location.host.includes("exfamily.jp")) {
    removeNotCopy();
    injectFCMagazineButton();
    injectFCGalleryButton();
  }

  // ===============
  // =    Utils    =
  // ===============

  function removeProtectImg() {
    // 移除右键限制
    document.oncontextmenu = function () {
      return true;
    };
    // 移除protectimg限制
    document
      .querySelectorAll(".protectimg")
      .forEach((node) => node.classList.remove("protectimg"));
    moDownloaderLog("移除右键限制");
  }

  function removeNotCopy() {
    // 移除限制
    NotCopy.set = function () {};
    // 加入下载权限
    document
      .querySelectorAll("img")
      .forEach((node) => node.classList.add("is-dl-permission"));
  }

  // ================================
  // =    Button Injection Utils    =
  // ================================

  function injectDownloadAllButtons() {
    var article = document.querySelector("article");
    if (article.classList.contains("article--news")) {
      // 新闻页面特殊处理
      article = article.querySelector(".article__body");
    }
    attachButtonToArticle(article);
  }

  function findEligibleImgsFromArticle(article) {
    return Array.from(article.querySelectorAll("img"))
      .map((img) => img.src)
      .filter((img) => keywords.some((k) => img.includes(k)));
  }

  function attachButtonToArticle(article) {
    var imgs = findEligibleImgsFromArticle(article);
    // 注入按钮 div
    var buttonsDiv = getButtonDiv();
    article.insertBefore(buttonsDiv, article.firstChild);
    // 图片链接生成按钮
    injectImageDownloadButton(buttonsDiv, imgs, getPrefixFromArticle(article));
    // 视频下载按钮
    if (
      article.querySelector("div.limelight-player") ||
      article.querySelector("a.popup_link")
    ) {
      // List View 视频
      injectPerVideoDownloadButton(article);
      // Timeline 视频
      injectPerVideoDownloadButtonForTimeline(article);
    }
    moDownloaderLog("注入按钮");
  }

  function injectPerVideoDownloadButton(div) {
    div.querySelectorAll("div.limelight-player").forEach((videoDiv) => {
      var mediaId = videoDiv.id.substring(videoDiv.id.lastIndexOf("_") + 1);
      moDownloaderDebug("正在下载视频: " + mediaId);
      injectOneButton(videoDiv.parentElement, "下载视频", function (event) {
        downloadVideo(event.target, mediaId, getPrefixFromArticle(div));
      });
    });
  }

  function injectArtistPhotoDownloadButton() {
    var inner = document.querySelector("#cms-inner");
    var title, imgs;
    if (inner) {
      // New Page
      title = inner.querySelector(".cms-section__inner__one-col");
      imgs = Array.from(inner.querySelectorAll("ldh-cms-img img")).map(
        (img) => img.src
      );
    } else {
      // Legacy Page
      inner = document.querySelector(".inner");
      title = inner.querySelector("section");
      imgs = Array.from(inner.querySelectorAll("img")).map((img) => img.src);
    }
    var buttonsDiv = getButtonDiv();
    title.append(buttonsDiv);
    injectImageDownloadButton(buttonsDiv, imgs, getPrefixFromDocument());
  }

  function injectOffshotDownloadButton() {
    var inner = document.querySelector(".inner");
    var divs = Array.from(inner.querySelectorAll("div[id^='js-gallery']"));
    divs.forEach((div) => {
      var buttonsDiv = getButtonDiv();
      div.parentElement.appendChild(buttonsDiv);
      var imgs = Array.from(div.querySelectorAll("a")).map(
        (a) => new URL(a.href, window.location.origin).href
      );
      injectImageDownloadButton(buttonsDiv, imgs, getPrefixFromDocument());
    });
  }

  function injectPerVideoDownloadButtonForTimeline(div) {
    div.querySelectorAll("a.popup_link").forEach((videoDiv) => {
      var mediaId = videoDiv
        .getAttribute("onclick")
        .split("movie/")[1]
        .split("/")[0];
      moDownloaderDebug("正在下载视频: " + mediaId);
      injectOneButton(videoDiv.parentElement, "下载视频", function (event) {
        downloadVideo(event.target, mediaId, getPrefixFromArticle(div));
      });
    });
  }

  function injectFCMagazineButton() {
    var article = document.querySelector("article");
    if (!article) return;
    var imgs = Array.from(article.querySelectorAll("img")).map(
      (img) =>
        new URL(img.getAttribute("data-src") || img.src, window.location.origin)
          .href
    );
    imgs = [...new Set(imgs)];
    var buttonsDiv = getButtonDiv();
    article.insertBefore(buttonsDiv, article.firstChild);
    injectImageDownloadButton(buttonsDiv, imgs, getPrefixFromArticle(article));
    Array.from(article.querySelectorAll(".js-img")).forEach((jsImg) => {
      var buttonsDiv = getButtonDiv();
      jsImg.parentElement.insertBefore(buttonsDiv, jsImg);
      var imgs = Array.from(jsImg.querySelectorAll("img")).map(
        (img) =>
          new URL(
            img.getAttribute("data-src") || img.src,
            window.location.origin
          ).href
      );
      injectImageDownloadButton(
        buttonsDiv,
        imgs,
        getPrefixFromArticle(article),
        false
      );
    });
    Array.from(
      article.querySelectorAll(".js-gallery .c-gallery__main")
    ).forEach((jsGallery) => {
      var buttonsDiv = getButtonDiv();
      jsGallery.parentElement.insertBefore(buttonsDiv, jsGallery);
      var imgs = Array.from(jsGallery.querySelectorAll("img")).map(
        (img) =>
          new URL(
            img.getAttribute("data-src") || img.src,
            window.location.origin
          ).href
      );
      imgs = [...new Set(imgs)];
      injectImageDownloadButton(
        buttonsDiv,
        imgs,
        getPrefixFromArticle(article)
      );
    });
  }

  function injectFCGalleryButton() {
    var gallery = document.querySelector(".p-gallery_detail");
    if (!gallery) return;
    var imgs = Array.from(gallery.querySelectorAll(".thumbnail img"))
      .filter((img) => !img.src.includes("photoCover.png"))
      .map((img) => new URL(img.src, window.location.origin).href);
    imgs = [...new Set(imgs)];
    var buttonsDiv = getButtonDiv();
    gallery.insertBefore(buttonsDiv, gallery.firstChild);
    injectImageDownloadButton(buttonsDiv, imgs, getPrefixFromArticle(gallery));
  }

  function injectImageDownloadButton(element, imgs, prefix, infix = true) {
    if (isMobile()) {
      // 手机用图片链接生成按钮
      injectOneButton(
        element,
        "生成图片链接(" + imgs.length + ")",
        function () {
          generateOnClickHandler(element, imgs);
        }
      );
    } else {
      // 图片下载按钮
      injectOneButton(
        element,
        (infix ? "下载所有图片 (" : "下载图片 (") + imgs.length + ")",
        function () {
          downloadImages(imgs, prefix);
        }
      );
    }
  }

  function injectOneButton(element, textOnButton, clickListener) {
    var btn = document.createElement("BUTTON");
    var btnText = document.createTextNode(textOnButton);
    btn.appendChild(btnText);
    btn.addEventListener("click", clickListener);
    btn.style =
      "background-color: transparent; border: solid #808080 2px; border-radius: 20px; color: #545454; margin: 0.2em;";
    element.appendChild(btn);
  }

  function getButtonDiv() {
    var buttonsDiv = document.createElement("div");
    buttonsDiv.className = "ldh-mo-dl";
    buttonsDiv.style = "margin-top: 0.4em; margin-bottom: 0.4em";
    return buttonsDiv;
  }

  function generateOnClickHandler(buttonsDiv, imgs) {
    var textarea = buttonsDiv.querySelector("textarea.ldh-mo-dl");
    if (!textarea) {
      textarea = document.createElement("textarea");
      textarea.className = "ldh-mo-dl";
      textarea.style = "height: 100px; width: 80%;";
      var br = document.createElement("br");
      buttonsDiv.insertBefore(br, buttonsDiv.firstChild);
      buttonsDiv.insertBefore(textarea, buttonsDiv.firstChild);
    }
    textarea.value = imgs.join("\n");
    textarea.select();
  }

  function customizedTimelinePage() {
    // 初始化
    document
      .querySelectorAll("ldh-infinite-scroll article")
      .forEach((article) => attachButtonToArticle(article));
    document
      .querySelectorAll("ldh-infinite-scroll ldh-imagediary-timeline-list-item")
      .forEach((article) => attachButtonToArticle(article));
    const infiniteScrollContainer = document.querySelector(
      "ldh-infinite-scroll"
    );
    const config = { childList: true };
    const observer = new MutationObserver(function (mutations, observer) {
      var nodes = mutations.find((r) =>
        Array.from(r.addedNodes).filter(
          (n) =>
            n.className == "article" ||
            n.className == "ldh-imagediary-timeline-list-item"
        )
      ).addedNodes;
      nodes.forEach((node) =>
        attachButtonToArticle(
          node.querySelector("article") ||
            node.querySelector("ldh-imagediary-timeline-list-item")
        )
      );
      removeProtectImg();
      moDownloaderDebug("解除刷新后时间线右键限制");
    });
    observer.observe(infiniteScrollContainer, config);
    moDownloaderLog("Timeline页面注入按钮");
  }

  // ========================
  // =    Download Utils    =
  // ========================

  function downloadImages(imgs, prefix = "") {
    moDownloaderDebug("正在下载图片: " + imgs);
    // Thanks to https://github.com/y252328/Instagram_Download_Button
    if (imgs.length <= 10) {
      // 同时最多下载十张图
      imgs.forEach((img, index) =>
        downloadOneImage(img, appendIndexToPrefix(prefix, index))
      );
    } else {
      // 设置延时下载更多图片 https://stackoverflow.com/questions/56244902/56245610#56245610
      imgs.forEach((img, index) => {
        setTimeout(function () {
          downloadOneImage(img, appendIndexToPrefix(prefix, index));
        }, index * 200);
      });
    }
  }

  function appendIndexToPrefix(prefix, index) {
    return (
      prefix +
      (index + 1).toLocaleString("en-US", {
        minimumIntegerDigits: 3,
        useGrouping: false,
      }) +
      "_"
    );
  }

  function downloadOneImage(img, prefix = "") {
    fetch(img, {
      headers: new Headers({
        Origin: window.location.origin,
      }),
      mode: "cors",
      cache: "no-cache",
    })
      .then((response) => response.blob())
      .then((blob) =>
        dowloadBlob(
          window.URL.createObjectURL(blob),
          prefix + img.substring(img.lastIndexOf("/") + 1)
        )
      )
      .catch((e) => moDownloaderError(e));
  }

  function downloadVideo(button, video, prefix = "") {
    const videoRequestURL =
      "https://production-ps.lvp.llnw.net/r/PlaylistService/media/<mediaId>/getMobilePlaylistByMediaId";
    fetch(videoRequestURL.replace("<mediaId>", video), {
      headers: new Headers({
        Origin: window.location.origin,
        Referer: window.location.origin,
      }),
      mode: "cors",
      cache: "no-cache",
    })
      .then((response) => response.json())
      .then((response) => {
        // 主m3u8
        let m3u8Url = response.mediaList[0].mobileUrls
          .find((v) => v.targetMediaPlatform == "HttpLiveStreaming")
          .mobileUrl.replace("http://", "https://");
        fetch(m3u8Url)
          .then((response) => response.text())
          .then((response) => {
            // 获取m3u8中最高清的
            const bandwithRe = /BANDWIDTH=(\d+)/i;
            var eligibleStreamsRaw = response
              .split("#EXT-X-STREAM-INF:")
              .slice(1);
            eligibleStreamsRaw.sort((a, b) => {
              return (
                parseInt(b.match(bandwithRe)[1]) -
                parseInt(a.match(bandwithRe)[1])
              );
            });
            var candidate = eligibleStreamsRaw[0].split("\n")[1];
            return new URL(candidate, new URL(m3u8Url).origin).href;
          })
          .then((m3u8Url) =>
            openM3U8ToolBox(
              button,
              m3u8Url,
              prefix + getFilenameFromVideoUrl(m3u8Url)
            )
          );
      });
  }

  function dowloadBlob(blob, filename) {
    var a = document.createElement("a");
    a.download = filename;
    a.href = blob;
    document.body.appendChild(a);
    a.click();
    a.remove();
  }

  // ====================
  // =    M3U8 Utils    =
  // ====================

  function openM3U8ToolBox(button, m3u8Url, filename) {
    // 打开 https://tools.thatwind.com/tool/m3u8downloader
    if (isMobile()) {
      let id = btoa(encodeURIComponent(filename));
      var a = button.parentElement.querySelector("a.mo-downloader");
      if (!a) {
        a = document.createElement("a");
        var aText = document.createTextNode("点击打开下载页面");
        a.className = "mo-downloader";
        a.append(aText);
        a.target = "_blank";
        a.href = constructM3U8ToolBoxURL(m3u8Url, filename);
        button.parentElement.appendChild(document.createElement("br"));
        button.parentElement.appendChild(a);
      }
    } else {
      a = document.createElement("a");
      a.target = "_blank";
      a.href = constructM3U8ToolBoxURL(m3u8Url, filename);
      document.body.appendChild(a);
      a.click();
      a.remove();
    }
  }

  function constructM3U8ToolBoxURL(m3u8Url, filename) {
    var url =
      "https://tools.thatwind.com/tool/m3u8downloader#" +
      "m3u8=" +
      encodeURIComponent(m3u8Url) +
      "&referer=" +
      encodeURIComponent(window.location.href) +
      "&filename=" +
      filename;
    moDownloaderDebug(url);
    return url;
  }

  // ======================
  // =    Naming Utils    =
  // ======================

  function getPrefixFromArticle(article) {
    var candidate =
      article.querySelector(".article__head") ||
      article.querySelector(".article__header") ||
      article.querySelector(".p-detail_article__header-text") ||
      article.querySelector(".p-gallery_detail__header");
    if (candidate) {
      return sanitizeFileName(candidate.textContent) + "_";
    }
    return getPrefixFromDocument();
  }

  function getPrefixFromDocument() {
    // 如果存在,使用movie标题
    var candidate = document.querySelector(".movie-title-block");
    if (candidate) {
      return sanitizeFileName(candidate.textContent) + "_";
    }
    return sanitizeFileName(document.title.split("|")[0] + "_");
  }

  function getFilenameFromVideoUrl(url) {
    let tempName = url.replace("/root-message-cxf-apache", "");
    tempName = tempName
      .substring(tempName.lastIndexOf("/") + 1)
      .replace(".m3u8", ".ts");
    return tempName;
  }

  function sanitizeFileName(input, replacement = "_") {
    // Thanks to https://github.com/parshap/node-sanitize-filename/blob/master/index.js
    const illegalRe = /[\/\?<>\\:\*\|"]/g;
    const controlRe = /[\x00-\x1f\x80-\x9f]/g;
    const reservedRe = /^\.+$/;
    const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
    const windowsTrailingRe = /[\. ]+$/;
    return input
      .split(/\s/g)
      .filter((s) => s)
      .join("_")
      .replace(illegalRe, replacement)
      .replace(controlRe, replacement)
      .replace(reservedRe, replacement)
      .replace(windowsReservedRe, replacement)
      .replace(windowsTrailingRe, replacement);
  }

  // =======================
  // =    Logging Utils    =
  // =======================

  function moDownloaderLog(msg) {
    console.log("[mo-downloder] " + msg);
  }

  function moDownloaderDebug(msg) {
    console.debug("[mo-downloder] " + msg);
  }

  function moDownloaderError(msg) {
    console.error("[mo-downloder] " + msg);
  }
})();