Image Webinfo

Extract JSON-/string-style info from Pixiv/Twitter/Booru image pages.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
  1. // ==UserScript==
  2. // @name Image Webinfo
  3. // @name:zh-CN 图片网页信息提取
  4. // @version 1.3.2
  5. // @description Extract JSON-/string-style info from Pixiv/Twitter/Booru image pages.
  6. // @description:zh-CN 将 Pixiv/Twitter/Booru 图片页面中的信息以 JSON 或字符串形式导出。
  7. // @author wklchris
  8. // @match https://danbooru.donmai.us/posts/*
  9. // @match https://www.pixiv.net/artworks/*
  10. // @match https://x.com/*/status/*
  11. // @grant none
  12. // @license MIT
  13. // @namespace https://greasyfork.org/users/672201
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. // --- Customization ---
  19. // Specify returned keys when copy string
  20. let pixiv_str_keys = [
  21. // 'artist',
  22. // 'illust_id',
  23. 'artist_id',
  24. 'post_date',
  25. 'post_title'
  26. ];
  27. let booru_str_keys = [
  28. // 'artists',
  29. // 'copyrights',
  30. // 'characters',
  31. // 'general',
  32. 'booru_id',
  33. 'rating_level'
  34. ];
  35. // --- Main ---
  36. let artwork_data = {};
  37. let export_json_btn = document.createElement("button");
  38. export_json_btn.innerHTML = "Export WebInfo as JSON";
  39. let export_str_btn = document.createElement("button");
  40. export_str_btn.innerHTML = "Export WebInfo as String";
  41.  
  42. // General functions
  43. function logMessage(msg) {
  44. console.log("[ImageWebInfo] " + msg);
  45. }
  46. let popMessage = 'Image webinfo exported to clipboard.';
  47. function copyToClipboard(s) {
  48. try {
  49. navigator.clipboard.writeText(s);
  50. } catch (err) {
  51. popMessage = 'Image webinfo export failed.';
  52. logMessage(popMessage);
  53. }
  54. }
  55.  
  56. function attemptedQuerySelector(qs, callback) {
  57. // querySelector may not find the element when the website loads slowly.
  58. // Keep attempting until found or exceed the given maximum attempts.
  59. var attempts = 0, max_attempts = 20;
  60. var attemptCall = function() {
  61. var elem = document.querySelector(qs);
  62. if (elem) {
  63. console.log(`Found querySelector '${qs}' in ${attempts} attempts`);
  64. return callback(elem);
  65. } else {
  66. attempts++;
  67. if (attempts >= max_attempts) {
  68. console.warn(`Could not find: ${qs} within ${max_attempts} attempts`);
  69. } else {
  70. setTimeout(attemptCall, 4000*(attempts + 1)/max_attempts + 1000)
  71. }
  72. }
  73. };
  74. attemptCall();
  75. }
  76. // --- Pixiv posts ---
  77.  
  78. if (location.hostname.includes("pixiv")) {
  79. function getPixivInfo(as_JSON=true) {
  80. let data = {};
  81. // Get user
  82. let user_tag = document.querySelector('aside div[aria-haspopup="true"] > div > a');
  83. data['artist'] = user_tag.childNodes[0].innerText
  84. let split_user_url = user_tag.href.split('/');
  85. data['artist_id'] = split_user_url[split_user_url.length - 1];
  86. // Get post id
  87. let split_url = location.href.split('/');
  88. data['illust_id'] = split_url[split_url.length - 1];
  89. // Get upload date
  90. let timestr = document.getElementsByTagName('time')[0].innerText;
  91. let time_seps = {'year': '年', 'month': '月', 'day': '日'};
  92. let i_start = 0, i_end = -1, tmp_data = {};
  93. for (const [key, sep] of Object.entries(time_seps)) {
  94. i_start = i_end+1;
  95. i_end = timestr.search(sep);
  96. tmp_data[key] = timestr.substring(i_start, i_end);
  97. }
  98. data['post_date'] = `${tmp_data['year']}-${tmp_data['month'].padStart(2, 0)}-${tmp_data['day'].padStart(2, 0)}`;
  99. // Get post title (if having a title)
  100. let title_dom = document.querySelector('figcaption h1');
  101. data['post_title'] = title_dom ? title_dom.innerText : '' ;
  102.  
  103. let s = "";
  104. if (as_JSON == true) {
  105. s = JSON.stringify(data)
  106. } else {
  107. s = pixiv_str_keys.map((x, i) => data[x]).join("\t");
  108. }
  109. copyToClipboard(s);
  110. return data;
  111. }
  112.  
  113. // Add buttons & their click functions
  114. let search_booru_btn = document.createElement("button");
  115. search_booru_btn.innerHTML = "Search artist on Booru";
  116. let search_booru_illust_btn = document.createElement("button");
  117. search_booru_illust_btn.innerHTML = "Search illust on Booru";
  118.  
  119. attemptedQuerySelector('aside section button', (exist_dom) => {
  120. exist_dom.parentNode.insertBefore(export_json_btn, exist_dom.nextSibling);
  121. exist_dom.parentNode.insertBefore(export_str_btn, export_json_btn.nextSibling);
  122. exist_dom.parentNode.insertBefore(search_booru_btn, export_str_btn.nextSibling);
  123. exist_dom.parentNode.insertBefore(search_booru_illust_btn, search_booru_btn.nextSibling);
  124. });
  125.  
  126. export_json_btn.onclick = function() {
  127. artwork_data = getPixivInfo(true);
  128. export_json_btn.style.background = 'lightgreen';
  129. export_str_btn.style.removeProperty("background");
  130. };
  131. export_str_btn.onclick = function() {
  132. artwork_data = getPixivInfo(false);
  133. export_str_btn.style.background = 'lightgreen';
  134. export_json_btn.style.removeProperty("background");
  135. };
  136. search_booru_btn.onclick = function() {
  137. if (!('artist_id' in artwork_data)) {
  138. artwork_data = getPixivInfo(false);
  139. }
  140. let _prefix = "https://danbooru.donmai.us/artists?commit=Search&search%5Border%5D=created_at&search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fusers%2F";
  141. let _url = _prefix + artwork_data["artist_id"];
  142. window.open(_url, "_blank");
  143. };
  144. search_booru_illust_btn.onclick = function() {
  145. if (!('illust_id' in artwork_data)) {
  146. artwork_data = getPixivInfo(false);
  147. }
  148. let illust_search_prefix = "https://danbooru.donmai.us/posts?tags=pixiv%3A";
  149. let illust_search_url = illust_search_prefix + artwork_data["illust_id"];
  150. window.open(illust_search_url, "_blank");
  151. };
  152. }
  153.  
  154. // --- Twitter/X Post ---
  155. if (["twitter.com", "x.com"].includes(location.hostname)) {
  156. let twitter_url_id = '';
  157. function getTwitterInfo(as_JSON=true) {
  158. let data = {};
  159. // Get user
  160. let user_tag = document.querySelector('article > div div[data-testid="User-Name"]');
  161. data['artist'] = user_tag.childNodes[0].innerText;
  162. // -- Twitter user only has username, doesn't have a user ID number.
  163. // The matched id is only used for creating search buttons.
  164. data['artist_id'] = '';
  165. let split_user_url = user_tag.childNodes[1].querySelector('a').href.split('/');
  166. twitter_url_id = split_user_url[split_user_url.length - 1];
  167. // Get post id
  168. data['illust_id'] = "T" + location.href.split('status/')[1].split('/')[0];
  169. // Get upload date
  170. data['post_date'] = document.getElementsByTagName('time')[0].dateTime.split('T')[0];
  171. // Get post title (if having a title)
  172. let tweet_nodes = document.querySelector('article div[data-testid="tweetText"]').querySelectorAll('span, img');
  173. data['post_title'] = Array.from(tweet_nodes).map(
  174. elem => elem.alt || elem.innerText.trim()).filter(d => d.length && d[0] != '#'
  175. ).join('').split('\n')[0];
  176.  
  177. let s = "";
  178. if (as_JSON == true) {
  179. s = JSON.stringify(data)
  180. } else {
  181. s = pixiv_str_keys.map((x, i) => data[x]).join("\t");
  182. }
  183. copyToClipboard(s);
  184. return data;
  185. }
  186.  
  187. // Add buttons & their click functions
  188. let search_booru_btn = document.createElement("button");
  189. search_booru_btn.innerHTML = "Search artist on Booru";
  190. let search_booru_illust_btn = document.createElement("button");
  191. search_booru_illust_btn.innerHTML = "Search illust on Booru";
  192.  
  193. attemptedQuerySelector('div[data-testid="User-Name"]', (exist_dom) => {
  194. exist_dom.parentNode.insertBefore(export_json_btn, exist_dom.nextSibling);
  195. exist_dom.parentNode.insertBefore(export_str_btn, export_json_btn.nextSibling);
  196. exist_dom.parentNode.insertBefore(search_booru_btn, export_str_btn.nextSibling);
  197. exist_dom.parentNode.insertBefore(search_booru_illust_btn, search_booru_btn.nextSibling);
  198. });
  199.  
  200. export_json_btn.onclick = function() {
  201. artwork_data = getTwitterInfo(true);
  202. export_json_btn.style.background = 'lightgreen';
  203. export_str_btn.style.removeProperty("background");
  204. };
  205. export_str_btn.onclick = function() {
  206. artwork_data = getTwitterInfo(false);
  207. export_str_btn.style.background = 'lightgreen';
  208. export_json_btn.style.removeProperty("background");
  209. };
  210. search_booru_btn.onclick = function() {
  211. if (!('artist_id' in artwork_data)) {
  212. artwork_data = getTwitterInfo(false);
  213. }
  214. let _prefix = "https://danbooru.donmai.us/artists?commit=Search&search%5Border%5D=created_at&search%5Burl_matches%5D=https%3A%2F%2Fwww.twitter.com%2F";
  215. let _url = _prefix + twitter_url_id;
  216. window.open(_url, "_blank");
  217. };
  218. search_booru_illust_btn.onclick = function() {
  219. if (!('illust_id' in artwork_data)) {
  220. artwork_data = getTwitterInfo(false);
  221. }
  222. let illust_search_prefix = "https://danbooru.donmai.us/posts?tags=source%3A";
  223. let illust_search_url = illust_search_prefix + encodeURIComponent(location.href.replace('x.com/','twitter.com/'));
  224. window.open(illust_search_url, "_blank");
  225. };
  226. }
  227.  
  228. // --- Booru posts ---
  229. if (location.hostname.includes("booru")) {
  230. // Add renamed link
  231. let renamed_download_link = document.createElement("a");
  232. renamed_download_link.href = document.getElementById('post-option-download').querySelector('a').href;
  233. renamed_download_link.style.display = 'inline-block';
  234. renamed_download_link.style.paddingBottom = '3px';
  235. renamed_download_link.text = "RENAMED FILENAME";
  236. // Add listener so that the renamed text is copied when right-click
  237. renamed_download_link.addEventListener('contextmenu', function(event) {
  238. let _renamed_filename = renamed_download_link.download;
  239. navigator.clipboard.writeText(_renamed_filename)
  240. });
  241.  
  242. function getBooruInfo(as_JSON=true) {
  243. let data = {};
  244. let tag_list = document.getElementById("tag-list").querySelector("div");
  245.  
  246. function query_tags(ul_class, sep=", ") {
  247. // Get tag list by ul_class name. Join by sep if multiple.
  248. let query_str = `ul.${ul_class} a.search-tag`;
  249. let _tags = tag_list.querySelectorAll(query_str);
  250. return Array.from(_tags).map((x, i) => x.innerText).join(sep)
  251. }
  252. data["artists"] = query_tags("artist-tag-list");
  253. data["copyrights"] = query_tags("copyright-tag-list");
  254. data["characters"] = query_tags("character-tag-list");
  255. data["general"] = query_tags("general-tag-list");
  256.  
  257. // Get information (rating-levels, etc.)
  258. function query_by_id(dom_id, remove_str) {
  259. let _text = document.getElementById(dom_id).innerText;
  260. return _text.replace(remove_str, "").trim();
  261. }
  262. data["booru_id"] = query_by_id("post-info-id", "ID:");
  263. data["rating_level"] = query_by_id("post-info-rating", "Rating:");
  264.  
  265. let s = "";
  266. if (as_JSON == true) {
  267. s = JSON.stringify(data)
  268. } else {
  269. s = booru_str_keys.map((x, i) => data[x]).join("\t");
  270. }
  271. copyToClipboard(s);
  272. return data;
  273. }
  274.  
  275. function updateRenamedDownloadLink() {
  276. let renamed_filename = '';
  277. const file_link = document.getElementById('post-info-size').querySelector('a').href;
  278. const post_sources = document.getElementById('post-info-source').querySelectorAll('a');
  279. const post_url = post_sources[0].href;
  280. const post_imgurl = post_sources[post_sources.length - 1].href;
  281.  
  282. function getSitePostId() {
  283. const _site_url = post_sources[0].href;
  284. const _site_id = _site_url.substring(_site_url.lastIndexOf('/') + 1);
  285. return _site_id;
  286. }
  287.  
  288. function getSiteFilenamePrefix(site_url) {
  289. const prefix_dict = {
  290. 'pixiv.net': '',
  291. 'fanbox.cc': 'PF',
  292. 'patreon.com': 'PT',
  293. 'twitter.com': 'T',
  294. 'x.com': 'T'
  295. };
  296. for (const key in prefix_dict) {
  297. if (site_url.includes(key)) {
  298. return prefix_dict[key];
  299. }
  300. }
  301. return '#';
  302. }
  303.  
  304. if (post_url.includes('pixiv.net') ) {
  305. // If pixiv, simply return original filename (from last <a> in sources)
  306. renamed_filename = post_imgurl.substring(post_imgurl.lastIndexOf('/') + 1);
  307. } else {
  308. // Otherwise, find the parent/children relations of the current post
  309. const children_previews = document.querySelector('.posts-container');
  310. let id_child = 0; // If not found parent
  311. if (children_previews) {
  312. const articles = children_previews.querySelectorAll('article');
  313. for (let i = 0; i < articles.length; i++) {
  314. if (articles[i].classList.contains('current-post')) {
  315. id_child = i;
  316. break;
  317. };
  318. }
  319. }
  320. // Concat the filename
  321. const file_prefix = getSiteFilenamePrefix(post_url);
  322. const site_id = getSitePostId();
  323. const file_ext = file_link.substring(file_link.lastIndexOf('.'));
  324. renamed_filename = file_prefix + site_id + "_p" + id_child + file_ext;
  325. }
  326. // Update the renamed link element
  327. renamed_download_link.download = renamed_filename;
  328. const meta_tags = document.querySelectorAll('ul.meta-tag-list a');
  329. // If it is bad id (the post has revised version), mark it out in the text
  330. let msg_bad_id = '';
  331. for (const _meta_tag of meta_tags) {
  332. if (_meta_tag.innerText == 'bad id') {
  333. msg_bad_id = '(bad) ';
  334. break;
  335. }
  336. }
  337. renamed_download_link.text = msg_bad_id + renamed_filename;
  338. }
  339. // Update link for (renamed) image file download
  340. updateRenamedDownloadLink();
  341.  
  342. // Add renamed link and buttons
  343. attemptedQuerySelector('section[id="search-box"]', (exist_dom) => {
  344. exist_dom.parentNode.insertBefore(renamed_download_link, exist_dom.nextSibling);
  345. exist_dom.parentNode.insertBefore(export_json_btn, renamed_download_link.nextSibling);
  346. exist_dom.parentNode.insertBefore(export_str_btn, export_json_btn.nextSibling);
  347. });
  348.  
  349. export_json_btn.onclick = function() {
  350. artwork_data = getBooruInfo(true);
  351. export_json_btn.style.background = 'lightgreen';
  352. export_str_btn.style.removeProperty("background");
  353. };
  354. export_str_btn.onclick = function() {
  355. artwork_data = getBooruInfo(false);
  356. export_str_btn.style.background = 'lightgreen';
  357. export_json_btn.style.removeProperty("background");
  358. };
  359. }
  360.  
  361. })();