下载PornHub视频和封面
// ==UserScript== // @name pornHelper // @namespace npm/vite-plugin-monkey // @version 1.0.0 // @author Kin // @description 下载PornHub视频和封面 // @license MIT // @icon https://pornhub.com/favicon.ico // @homepage https://github.com/Yorushika-fan/pornHelper // @match *://*.pornhub.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js // @require https://unpkg.com/vue-demi@latest/lib/index.iife.js // @require data:application/javascript,window.Vue%3DVue%3B // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/index.full.min.js // @resource ElementPlus https://cdn.jsdelivr.net/npm/[email protected]/dist/index.full.min.css // @grant GM_addStyle // @grant GM_download // @grant GM_getResourceText // @grant GM_xmlhttpRequest // @grant unsafeWindow // ==/UserScript== (t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const r=document.createElement("style");r.textContent=t,document.head.append(r)})(" *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.fixed{position:fixed}.right-4{right:1rem}.top-16{top:4rem}.z-10{z-index:10}.mb-4{margin-bottom:1rem}.flex{display:flex}.flex-grow{flex-grow:1}.flex-row{flex-direction:row}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.\\!rounded-lg{border-radius:.5rem!important}.rounded{border-radius:.25rem}.\\!bg-\\[\\#ff9900\\]{--tw-bg-opacity: 1 !important;background-color:rgb(255 153 0 / var(--tw-bg-opacity))!important}.\\!bg-gray-200{--tw-bg-opacity: 1 !important;background-color:rgb(229 231 235 / var(--tw-bg-opacity))!important}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.\\!py-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.\\!text-base{font-size:1rem!important;line-height:1.5rem!important}.\\!font-bold{font-weight:700!important}.font-bold{font-weight:700}.\\!text-black{--tw-text-opacity: 1 !important;color:rgb(0 0 0 / var(--tw-text-opacity))!important}.\\!text-gray-700{--tw-text-opacity: 1 !important;color:rgb(55 65 81 / var(--tw-text-opacity))!important}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.\\!transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;transition-duration:.15s!important}.hover\\:\\!bg-\\[\\#ff6600\\]:hover{--tw-bg-opacity: 1 !important;background-color:rgb(255 102 0 / var(--tw-bg-opacity))!important}.hover\\:\\!bg-gray-300:hover{--tw-bg-opacity: 1 !important;background-color:rgb(209 213 219 / var(--tw-bg-opacity))!important}.hover\\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\\:bg-green-600:hover{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity))} "); (function (vue, elementPlus) { 'use strict'; const cssLoader = (e) => { const t = GM_getResourceText(e); return GM_addStyle(t), t; }; cssLoader("ElementPlus"); var _GM_download = /* @__PURE__ */ (() => typeof GM_download != "undefined" ? GM_download : void 0)(); var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); var _a; const isClient = typeof window !== "undefined"; const isString = (val) => typeof val === "string"; const noop = () => { }; isClient && ((_a = window == null ? void 0 : window.navigator) == null ? void 0 : _a.userAgent) && /iP(ad|hone|od)/.test(window.navigator.userAgent); function resolveUnref(r) { return typeof r === "function" ? r() : vue.unref(r); } function createFilterWrapper(filter, fn) { function wrapper(...args) { return new Promise((resolve, reject) => { Promise.resolve(filter(() => fn.apply(this, args), { fn, thisArg: this, args })).then(resolve).catch(reject); }); } return wrapper; } function throttleFilter(ms, trailing = true, leading = true, rejectOnCancel = false) { let lastExec = 0; let timer; let isLeading = true; let lastRejector = noop; let lastValue; const clear = () => { if (timer) { clearTimeout(timer); timer = void 0; lastRejector(); lastRejector = noop; } }; const filter = (_invoke) => { const duration = resolveUnref(ms); const elapsed = Date.now() - lastExec; const invoke = () => { return lastValue = _invoke(); }; clear(); if (duration <= 0) { lastExec = Date.now(); return invoke(); } if (elapsed > duration && (leading || !isLeading)) { lastExec = Date.now(); invoke(); } else if (trailing) { lastValue = new Promise((resolve, reject) => { lastRejector = rejectOnCancel ? reject : resolve; timer = setTimeout(() => { lastExec = Date.now(); isLeading = true; resolve(invoke()); clear(); }, Math.max(0, duration - elapsed)); }); } if (!leading && !timer) timer = setTimeout(() => isLeading = true, duration); isLeading = false; return lastValue; }; return filter; } function identity(arg) { return arg; } function tryOnScopeDispose(fn) { if (vue.getCurrentScope()) { vue.onScopeDispose(fn); return true; } return false; } function useThrottleFn(fn, ms = 200, trailing = false, leading = true, rejectOnCancel = false) { return createFilterWrapper(throttleFilter(ms, trailing, leading, rejectOnCancel), fn); } function tryOnMounted(fn, sync = true) { if (vue.getCurrentInstance()) vue.onMounted(fn); else if (sync) fn(); else vue.nextTick(fn); } function useTimeoutFn(cb, interval, options = {}) { const { immediate = true } = options; const isPending = vue.ref(false); let timer = null; function clear() { if (timer) { clearTimeout(timer); timer = null; } } function stop() { isPending.value = false; clear(); } function start(...args) { clear(); isPending.value = true; timer = setTimeout(() => { isPending.value = false; timer = null; cb(...args); }, resolveUnref(interval)); } if (immediate) { isPending.value = true; if (isClient) start(); } tryOnScopeDispose(stop); return { isPending: vue.readonly(isPending), start, stop }; } function unrefElement(elRef) { var _a2; const plain = resolveUnref(elRef); return (_a2 = plain == null ? void 0 : plain.$el) != null ? _a2 : plain; } const defaultWindow = isClient ? window : void 0; const defaultNavigator = isClient ? window.navigator : void 0; function useEventListener(...args) { let target; let events; let listeners; let options; if (isString(args[0]) || Array.isArray(args[0])) { [events, listeners, options] = args; target = defaultWindow; } else { [target, events, listeners, options] = args; } if (!target) return noop; if (!Array.isArray(events)) events = [events]; if (!Array.isArray(listeners)) listeners = [listeners]; const cleanups = []; const cleanup = () => { cleanups.forEach((fn) => fn()); cleanups.length = 0; }; const register = (el, event, listener, options2) => { el.addEventListener(event, listener, options2); return () => el.removeEventListener(event, listener, options2); }; const stopWatch = vue.watch(() => [unrefElement(target), resolveUnref(options)], ([el, options2]) => { cleanup(); if (!el) return; cleanups.push(...events.flatMap((event) => { return listeners.map((listener) => register(el, event, listener, options2)); })); }, { immediate: true, flush: "post" }); const stop = () => { stopWatch(); cleanup(); }; tryOnScopeDispose(stop); return stop; } function useSupported(callback, sync = false) { const isSupported = vue.ref(); const update = () => isSupported.value = Boolean(callback()); update(); tryOnMounted(update, sync); return isSupported; } function useClipboard(options = {}) { const { navigator = defaultNavigator, read = false, source, copiedDuring = 1500, legacy = false } = options; const events = ["copy", "cut"]; const isClipboardApiSupported = useSupported(() => navigator && "clipboard" in navigator); const isSupported = vue.computed(() => isClipboardApiSupported.value || legacy); const text = vue.ref(""); const copied = vue.ref(false); const timeout = useTimeoutFn(() => copied.value = false, copiedDuring); function updateText() { if (isClipboardApiSupported.value) { navigator.clipboard.readText().then((value) => { text.value = value; }); } else { text.value = legacyRead(); } } if (isSupported.value && read) { for (const event of events) useEventListener(event, updateText); } async function copy(value = resolveUnref(source)) { if (isSupported.value && value != null) { if (isClipboardApiSupported.value) await navigator.clipboard.writeText(value); else legacyCopy(value); text.value = value; copied.value = true; timeout.start(); } } function legacyCopy(value) { const ta = document.createElement("textarea"); ta.value = value != null ? value : ""; ta.style.position = "absolute"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); } function legacyRead() { var _a2, _b, _c; return (_c = (_b = (_a2 = document == null ? void 0 : document.getSelection) == null ? void 0 : _a2.call(document)) == null ? void 0 : _b.toString()) != null ? _c : ""; } return { isSupported, text, copied, copy }; } const _global = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {}; const globalKey = "__vueuse_ssr_handlers__"; _global[globalKey] = _global[globalKey] || {}; var SwipeDirection; (function(SwipeDirection2) { SwipeDirection2["UP"] = "UP"; SwipeDirection2["RIGHT"] = "RIGHT"; SwipeDirection2["DOWN"] = "DOWN"; SwipeDirection2["LEFT"] = "LEFT"; SwipeDirection2["NONE"] = "NONE"; })(SwipeDirection || (SwipeDirection = {})); var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; const _TransitionPresets = { easeInSine: [0.12, 0, 0.39, 0], easeOutSine: [0.61, 1, 0.88, 1], easeInOutSine: [0.37, 0, 0.63, 1], easeInQuad: [0.11, 0, 0.5, 0], easeOutQuad: [0.5, 1, 0.89, 1], easeInOutQuad: [0.45, 0, 0.55, 1], easeInCubic: [0.32, 0, 0.67, 0], easeOutCubic: [0.33, 1, 0.68, 1], easeInOutCubic: [0.65, 0, 0.35, 1], easeInQuart: [0.5, 0, 0.75, 0], easeOutQuart: [0.25, 1, 0.5, 1], easeInOutQuart: [0.76, 0, 0.24, 1], easeInQuint: [0.64, 0, 0.78, 0], easeOutQuint: [0.22, 1, 0.36, 1], easeInOutQuint: [0.83, 0, 0.17, 1], easeInExpo: [0.7, 0, 0.84, 0], easeOutExpo: [0.16, 1, 0.3, 1], easeInOutExpo: [0.87, 0, 0.13, 1], easeInCirc: [0.55, 0, 1, 0.45], easeOutCirc: [0, 0.55, 0.45, 1], easeInOutCirc: [0.85, 0, 0.15, 1], easeInBack: [0.36, 0, 0.66, -0.56], easeOutBack: [0.34, 1.56, 0.64, 1], easeInOutBack: [0.68, -0.6, 0.32, 1.6] }; __spreadValues({ linear: identity }, _TransitionPresets); const _hoisted_1 = { key: 0, class: "fixed top-16 right-4 flex flex-row space-x-2 z-10" }; const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({ __name: "pornhub", setup(__props) { const isDownloadingVideo = vue.ref(false); const isDownloadingCover = vue.ref(false); const downloadProgress = vue.ref(0); const isDownloading = vue.ref(false); vue.onMounted(() => { console.log("脚本已加载"); console.log(` ____ _ _ _ | _ \\ ___ _ __ _ __ | | | |_ _| |__ | |_) / _ \\| '__| '_ \\| |_| | | | | '_ \\ | __/ (_) | | | | | | _ | |_| | |_) | |_| \\___/|_| |_| |_|_| |_|\\__,_|_.__/ `); }); const videoList = vue.ref([]); const { copy } = useClipboard(); const getFlashVars = () => { const flashvarsRegex = /flashvars_\d+/; const flashvarsKey = Object.keys(_unsafeWindow).find((key) => flashvarsRegex.test(key)); if (flashvarsKey) { return _unsafeWindow[flashvarsKey]; } return null; }; const getVideoInfo = () => { videoList.value = []; console.log(videoList.value); const flashvars = getFlashVars(); if (flashvars) { const mediaDefinitions = flashvars.mediaDefinitions; Object.values(mediaDefinitions).forEach((media) => { if (media.format === "mp4") { _GM_xmlhttpRequest({ url: media.videoUrl, method: "GET", responseType: "json", onload: (response) => { response.response.forEach((res) => { videoList.value.push({ videoUrl: res.videoUrl, quality: res.quality }); }); }, ontimeout: () => elementPlus.ElMessage.error("请求视频超时,请稍后再试"), onerror: () => elementPlus.ElMessage.error("请求视频失败,请稍后再试") }); } }); isDownloadingVideo.value = true; } else { elementPlus.ElMessage.error("没有找到flashvars"); } }; const downloadVideo = (video) => { isDownloading.value = true; downloadProgress.value = 0; const xhr = new XMLHttpRequest(); xhr.open("GET", video.videoUrl, true); xhr.responseType = "blob"; xhr.onprogress = (event) => { if (event.lengthComputable) { downloadProgress.value = event.loaded / event.total * 100; } }; xhr.onload = () => { if (xhr.status === 200) { const blob = xhr.response; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `video_${video.quality}.mp4`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); elementPlus.ElMessage.success(`${video.quality} 质量的视频下载成功`); } else { elementPlus.ElMessage.error(`${video.quality} 质量的视频下载失败`); } isDownloading.value = false; }; xhr.onerror = () => { console.error("下载错误"); elementPlus.ElMessage.error(`${video.quality} 质量的视频下载失败`); isDownloading.value = false; }; xhr.send(); }; const downloadCover = useThrottleFn(() => { if (isDownloadingCover.value) return; isDownloadingCover.value = true; const flashvars = getFlashVars(); if (flashvars) { const coverUrl = flashvars.image_url; _GM_download({ url: coverUrl, name: "cover.jpg", onerror: () => elementPlus.ElMessage.error("封面下载失败"), ontimeout: () => elementPlus.ElMessage.error("封面下载超时"), onload: () => elementPlus.ElMessage.success("封面下载成功") }); } setTimeout(() => { isDownloadingCover.value = false; }, 2e3); }, 2e3); const copyVideoLink = (url) => { copy(url); elementPlus.ElMessage.success("下载链接已复制到剪贴板"); }; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [ getFlashVars() && !isDownloadingVideo.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ vue.createElementVNode("button", { class: "bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded shadow", onClick: getVideoInfo }, " 下载视频 "), vue.createElementVNode("button", { class: "bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded shadow", onClick: _cache[0] || (_cache[0] = //@ts-ignore (...args) => vue.unref(downloadCover) && vue.unref(downloadCover)(...args)) }, vue.toDisplayString(isDownloadingCover.value ? "下载中..." : "下载封面"), 1) ])) : vue.createCommentVNode("", true), vue.createVNode(vue.unref(elementPlus.ElDialog), { modelValue: isDownloadingVideo.value, "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => isDownloadingVideo.value = $event), title: "下载视频", width: "400px" }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(elementPlus.ElSkeleton), { loading: videoList.value.length === 0, "animated:true": "" }, { template: vue.withCtx(() => [ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(4, (i) => { return vue.createElementVNode("div", { key: i, class: "mb-4" }, [ vue.createVNode(vue.unref(elementPlus.ElSkeletonItem), { variant: "button", style: { "width": "100%", "height": "48px" } }) ]); }), 64)) ]), default: vue.withCtx(() => [ !isDownloading.value ? (vue.openBlock(true), vue.createElementBlock(vue.Fragment, { key: 0 }, vue.renderList(videoList.value, (video, index) => { return vue.openBlock(), vue.createElementBlock("div", { key: index, class: "mb-4 flex space-x-2" }, [ vue.createVNode(vue.unref(elementPlus.ElButton), { class: vue.normalizeClass([ "flex-grow !py-3 !text-base !font-bold !rounded-lg !transition-colors", "!bg-[#ff9900] hover:!bg-[#ff6600] !text-black" ]), onClick: ($event) => downloadVideo(video) }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(video.quality + "p"), 1) ]), _: 2 }, 1032, ["onClick"]), vue.createVNode(vue.unref(elementPlus.ElButton), { class: vue.normalizeClass([ "!py-3 !text-base !font-bold !rounded-lg !transition-colors", "!bg-gray-200 hover:!bg-gray-300 !text-gray-700" ]), onClick: ($event) => copyVideoLink(video.videoUrl) }, { default: vue.withCtx(() => _cache[2] || (_cache[2] = [ vue.createTextVNode(" 复制链接 ") ])), _: 2 }, 1032, ["onClick"]) ]); }), 128)) : vue.createCommentVNode("", true) ]), _: 1 }, 8, ["loading"]), isDownloading.value ? (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElProgress), { key: 0, percentage: downloadProgress.value, status: "success", "stroke-width": 20 }, null, 8, ["percentage"])) : vue.createCommentVNode("", true) ]), _: 1 }, 8, ["modelValue"]) ], 64); }; } }); const _sfc_main = /* @__PURE__ */ vue.defineComponent({ __name: "App", setup(__props) { return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(_sfc_main$1); }; } }); vue.createApp(_sfc_main).mount( (() => { const app = document.createElement("div"); document.body.append(app); return app; })() ); })(Vue, ElementPlus);