您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在mo的内容页增加图片和视频下载的按钮, 解锁右键功能
// ==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); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址