Civitai Direct Link Helper

Adds a convenient copy button next to download buttons on Civitai to easily get direct download links for models

  1. // ==UserScript==
  2. // @name Civitai Direct Link Helper
  3. // @name:zh-CN Civitai 下载助手
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.21
  6. // @description Adds a convenient copy button next to download buttons on Civitai to easily get direct download links for models
  7. // @description:zh-CN 在 Civitai 的下载按钮旁添加复制按钮,轻松复制直链地址 还有去广告
  8. // @author hua
  9. // @match https://civitai.com/*
  10. // @match https://civitai.green/*
  11. // @grant unsafeWindow
  12. // @grant GM_xmlhttpRequest
  13. // @run-at document-start
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17.  
  18.  
  19.  
  20. (function () {
  21. 'use strict';
  22. unsafeWindow.setInterval = function (fn, time) {
  23. };
  24. const origin_setTimeout = unsafeWindow.setTimeout;
  25. unsafeWindow.setTimeout = function (fn, time) {
  26. const tags = ['schedule', 'coreAdServerStart', 'exited', 'maybeFetchNotificationAndTrackCurrentUrl', 'googletagservices', '/api/internal/activity', 'iframe_api'];
  27. if (tags.some(tag => fn.toString().includes(tag))) {
  28. return;
  29. }
  30. function fn_() {
  31. fn();
  32. }
  33. origin_setTimeout(fn_, time);
  34. };
  35.  
  36. hookCreateElement();
  37. changeInfo();
  38. modifywebpack();
  39. function modifywebpack() {
  40. let webpackChunk_N_E;
  41. const hookPush = () => {
  42. const originPush = webpackChunk_N_E.push;
  43. webpackChunk_N_E.push = function (chunk) {
  44. const funs = chunk?.[1];
  45. if (funs?.['68714'] && !funs['68714'].inject) {
  46. let funStr = funs['68714'].toString();
  47. // funStr = funStr.replace('function(e,i,t){', 'function(e,i,t){debugger;');
  48. // funStr = funStr.replace('function(e,t,n){"use strict";', 'function(e,t,n){"use strict";debugger;');
  49. let match_tag = funStr.match(/return (.{1,5})\.length\?\(0,/);
  50. if (match_tag) {
  51. const tag = match_tag[1];
  52. funStr = funStr.replace(`return ${tag}.length?(0,`, `${tag}=${tag}.filter(item => item.type !== "ad");return ${tag}.length?(0,`);
  53. }
  54. funs['68714'] = new Function('return ' + funStr)();
  55. funs['68714'].inject = true;
  56. }
  57. if (funs?.['56053'] && !funs['56053'].inject) {
  58. let funStr = funs['56053'].toString();
  59. // funStr = funStr.replace('function(e,t,i){"use strict";', 'function(e,t,i){"use strict";debugger;');
  60. let match_tag = funStr.match(/children\:(.{1,5})\.map\(\(/);
  61. if (match_tag) {
  62. const tag = match_tag[1];
  63. const re_match = funStr.match(/return\(0,(.{1,5}).jsx\)\("div"/);
  64. if (re_match) {
  65. const re_str = re_match[0];
  66. funStr = funStr.replace(re_str, `${tag}.forEach((item,i)=>{ ${tag}[i] = item.filter(ite => ite.data.type !== "ad")});${re_str}`);
  67. }
  68. }
  69. funs['56053'] = new Function('return ' + funStr)();
  70. funs['56053'].inject = true;
  71. }
  72. originPush.call(this, chunk);
  73. };
  74. };
  75. Object.defineProperty(unsafeWindow, 'webpackChunk_N_E', {
  76. get: function () {
  77. return webpackChunk_N_E;
  78. },
  79. set: function (value) {
  80. webpackChunk_N_E = value;
  81. hookPush();
  82. }
  83. });
  84. }
  85.  
  86. function changeInfo() {
  87. const originFetch = unsafeWindow.fetch;
  88. unsafeWindow.fetch = function (url, options) {
  89. async function fetch_request(response) {
  90. if (url.includes('/announcement.getAnnouncements')) {
  91. try {
  92. const data = await response.json();
  93. if (data.result?.data?.json) data.result.data.json = [];
  94. console.log('modify announcement.getAnnouncements');
  95. response = new Response(JSON.stringify(data), response);
  96. } catch (e) {
  97. console.log('fetch_request error', e);
  98. }
  99. }
  100. if (url.includes('auth/session')) {
  101. try {
  102. const data = await response.json();
  103. if (data.user) {
  104. const user = data.user;
  105. user.allowAds = false;
  106. console.log('modify auth/session');
  107. }
  108. response = new Response(JSON.stringify(data), response);
  109. } catch (e) {
  110. console.log('fetch_request error', e);
  111. }
  112. }
  113. return response;
  114. }
  115. return originFetch(url, options).then(fetch_request);
  116. };
  117.  
  118. let monitorcount = 1;
  119. const observer = new MutationObserver((mutations) => {
  120. for (const mutation of mutations) {
  121. for (const node of mutation.addedNodes) {
  122. if (node.tagName === 'SCRIPT' && node.id === '__NEXT_DATA__') {
  123. monitorcount--;
  124. console.log('modify __NEXT_DATA__');
  125. if (monitorcount <= 0) {
  126. observer.disconnect();
  127. }
  128. modify(node);
  129. }
  130. }
  131. }
  132. });
  133.  
  134. observer.observe(document.documentElement, {
  135. childList: true,
  136. subtree: true
  137. });
  138. function modify(node) {
  139. const initalData = JSON.parse(node.textContent);
  140. const trpcData = initalData.props?.pageProps?.trpcState?.json;
  141. if (trpcData) {
  142. const queries = trpcData.queries || [];
  143. if (queries.length > 0) {
  144. const query = queries[0];
  145. const data = query.state?.data || [];
  146. const remveIndex = [];
  147. data.forEach((item, index) => {
  148. const ignoreFlags = ['Announcement', 'Event', 'CosmeticShop'];
  149. if (ignoreFlags.includes(item.type)) {
  150. remveIndex.push(index);
  151. }
  152. });
  153. remveIndex.reverse();
  154. remveIndex.forEach(index => {
  155. data.splice(index, 1);
  156. });
  157. }
  158. }
  159.  
  160. const flags = initalData.props?.pageProps?.flags;
  161. if (flags) {
  162. flags.adsEnabled = false;
  163. }
  164. const session = initalData.props?.pageProps?.session;
  165. if (session?.user) {
  166. const user = session.user;
  167. user.allowAds = false;
  168. }
  169. node.textContent = JSON.stringify(initalData);
  170. }
  171.  
  172. }
  173.  
  174. function paraseDownloadUrl(button) {
  175. let originalColor = window.getComputedStyle(button).color;
  176. const restoreTimeout = 10000;
  177. let interval = null;
  178. const restore = () => {
  179. button.style.color = originalColor;
  180. };
  181.  
  182. const onError = () => {
  183. clearInterval(interval);
  184. interval = null;
  185. button.style.color = '#FF0000';
  186. setTimeout(() => {
  187. restore();
  188. }, restoreTimeout);
  189. };
  190.  
  191. const onSuccess = () => {
  192. clearInterval(interval);
  193. interval = null;
  194. navigator.clipboard.writeText(button.downloadUrl).then(() => {
  195. }).catch((e) => {
  196. alert('copy error:' + e.message);
  197. });
  198. button.style.color = '#00FF00';
  199. setTimeout(() => {
  200. restore();
  201. }, restoreTimeout);
  202. };
  203. if (button.downloadUrl) {
  204. onSuccess();
  205. return;
  206. }
  207. const uri = button.getAttribute('href');
  208. interval = setInterval(() => {
  209. button.style.color = `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`;
  210. }, 100);
  211.  
  212. GM_xmlhttpRequest({
  213. method: "GET",
  214. url: `https://civitai.com${uri}`,
  215. timeout: 10000,
  216. anonymous: false,
  217. redirect: 'manual',
  218. maxRedirects: 0,
  219. onload: function (response) {
  220. const downloadUrl = response.responseHeaders.match(/location:(.*?)(?:\r?\n)/i)?.[1];
  221. button.downloadUrl = downloadUrl;
  222. downloadUrl ? onSuccess() : onError();
  223. },
  224. onerror: function (error) {
  225. console.log('onerror', error);
  226. onError();
  227. },
  228. ontimeout: function () {
  229. console.log('ontimeout');
  230. onError();
  231. }
  232. });
  233. }
  234.  
  235. function hookDownloadButton(node) {
  236. let isClick = false;
  237. let timers = [];
  238. node.addEventListener('click', function (e) {
  239. if (isClick) {
  240. isClick = false;
  241. return;
  242. }
  243. e.preventDefault();
  244. const timer = setTimeout(() => {
  245. isClick = true;
  246. timers.forEach(timer => clearTimeout(timer));
  247. timers.length = 0;
  248. node.click();
  249. }, 300);
  250. timers.push(timer);
  251. });
  252. node.addEventListener('dblclick', function (e) {
  253. e.preventDefault();
  254. timers.forEach(timer => clearTimeout(timer));
  255. timers.length = 0;
  256. paraseDownloadUrl(node);
  257. });
  258. }
  259.  
  260. function hookCreateElement() {
  261. const origin_createElement = unsafeWindow.document.createElement;
  262. unsafeWindow.document.createElement = function () {
  263. const node = origin_createElement.apply(this, arguments);
  264. if (arguments[0].toUpperCase() === 'A') {
  265. const originSetAttribute = node.setAttribute;
  266. node.setAttribute = function (name, value) {
  267. if (name === 'href' ) {
  268. if (value?.startsWith('/api/download/models/')){
  269. console.log('hookButton');
  270. hookDownloadButton(node);
  271. }
  272. if (value?.includes('/pricing?utm_campaign=holiday_promo')) {
  273. node.style.display = 'none';
  274. console.log('hookPricing');
  275. }
  276. }
  277. return originSetAttribute.call(this, name, value);
  278. };
  279. }
  280. if (arguments[0].toUpperCase() === 'IFRAME') {
  281. return null;
  282. }
  283. return node;
  284. };
  285. unsafeWindow.document.createElement.toString = origin_createElement.toString.bind(origin_createElement);
  286. }
  287.  
  288. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址