Pixiv Ajax Bookmark Mod

Automatically adds artwork tags when you bookmark. ブックマークのタグを自動的につける。

  1. // ==UserScript==
  2. // @name Pixiv Ajax Bookmark Mod
  3. // @namespace com.SaddestPanda.net.moe
  4. // @version 2.9.2
  5. // @description Automatically adds artwork tags when you bookmark. ブックマークのタグを自動的につける。
  6. // @match *://www.pixiv.net/*
  7. // @homepage https://greasyfork.org/en/scripts/22767-pixiv-ajax-bookmark-mod
  8. // @supportURL https://greasyfork.org/en/scripts/22767-pixiv-ajax-bookmark-mod/feedback
  9. // @author qa2 & SaddestPanda
  10. // @require https://cdn.jsdelivr.net/npm/gm-webext-pref@0.4.2/dist/GM_webextPref.user.js
  11. // @grant GM.getValue
  12. // @grant GM.setValue
  13. // @grant GM.deleteValue
  14. // @grant GM_addValueChangeListener
  15. // @grant GM.registerMenuCommand
  16. // @run-at document-start
  17. // @noframes
  18. // ==/UserScript==
  19.  
  20. var tagsArray = [],
  21. currLocation = "",
  22. theToken = "",
  23. restartCheckerInterval = null,
  24. startingSoonInterval = null;
  25.  
  26. if (document.querySelector("#meta-global-data")?.content) {
  27. theToken = JSON.parse(document.querySelector("#meta-global-data").content)?.token || "";
  28. // console.log("🚀 ~ theToken", theToken);
  29. //add it to cookies (15 mins)
  30. let expires = (new Date(Date.now() + 15 * 60 * 1000)).toUTCString();
  31. document.cookie = "TOKENpixivajaxbm=" + theToken + "; expires=" + expires + ";path=/;";
  32. } else {
  33. try {
  34. //get the necessary token
  35. if (!theToken) {
  36. getthetoken();
  37. }
  38. } catch (e) {
  39. console.log("PABM token error: ", e);
  40. }
  41. }
  42.  
  43. /*
  44. ❗ You don't have to touch these settings anymore. Use the new settings ui.
  45. ❗ もうここの設定を手動で変更する必要はあるません。ページ内の新しい設定UIを使用してください。
  46. */
  47.  
  48. const pref = GM_webextPref({
  49. default: {
  50. givelike: true,
  51. r18private: true,
  52. bkm_restrict: false,
  53. add_all_tags: true
  54. },
  55. body: [{
  56. key: "givelike",
  57. type: "checkbox",
  58. label: "Automatically give your bookmarks a like.(ブックマークした作品に自動的に「いいね!」をくれます。)"
  59. },
  60. {
  61. key: "add_all_tags",
  62. type: "checkbox",
  63. label: "Automatically add the work's tags as bookmark tags.(作品に登録されているすべてのタグをブックマークタグとして追加します。)"
  64. },
  65. {
  66. key: "r18private",
  67. type: "checkbox",
  68. label: "R-18 works are automatically added as private bookmarks.(作品はR-18であった場合、自動的に非公開としてブックマークします。)"
  69. },
  70. {
  71. key: "bkm_restrict",
  72. type: "checkbox",
  73. label: "❗ Always add to private bookmarks list.(常に非公開としてブックマークします。)"
  74. },
  75. ]
  76. });
  77.  
  78. //Start running startingSoon if the page is different
  79. function restartChecker() {
  80. //check for url changes
  81. if (document.location != currLocation) {
  82. //Stop restart checker and go for startingSoon instead
  83. clearInterval(restartCheckerInterval);
  84. clearInterval(startingSoonInterval);
  85. startingSoonInterval = setInterval(startingSoon, 150);
  86. }
  87. }
  88.  
  89. function startingSoon() {
  90. try {
  91. //Check if the main bookmark button exists
  92. if ((document.querySelectorAll(".gtm-main-bookmark").length == 1) &&
  93. //Also check if the current url matches /artworks/
  94. (document.location.toString().match(/^https?:\/\/www.pixiv.net\/.*?artworks\/.*/) != null) &&
  95. //Also check if we DO NOT have the hover buttons added into the page yet
  96. (document.querySelector("#pabmButtonsContainer") == null) &&
  97. //Also check if the bookmark button is enabled
  98. (document.querySelector(".gtm-main-bookmark").disabled == false)) {
  99.  
  100. //Continue if everything above succeeds
  101. clearInterval(startingSoonInterval);
  102. try {
  103. //get the necessary token
  104. getthetoken();
  105. setTimeout(startingUp, 150);
  106. } catch (e) {
  107. console.log("PABM token error: ", e);
  108. }
  109. }
  110. } catch (e) {
  111. console.log("PABM startingSoon error: ", e);
  112. }
  113. }
  114.  
  115. //Get tags, add hover buttons
  116. function startingUp() {
  117. //Start restart checker
  118. currLocation = document.location.href;
  119. restartCheckerInterval = setInterval(restartChecker, 150);
  120. //clear tags
  121. tagsArray = [];
  122. //get all tags
  123. document.querySelectorAll("footer li").forEach(thisElement => {
  124. try {
  125. let thisTag = decodeURIComponent(thisElement.querySelector("a").href.match(/tags\/(.*)\/artworks/)[1]);
  126. if (thisTag) {
  127. tagsArray.push(thisTag);
  128. }
  129. } catch (e) { }
  130. });
  131.  
  132. //add css
  133. AddMyStyle("pabmButtonsStyle", `
  134. #pabmButtonsContainer {
  135. position: absolute;
  136. width: 64px;
  137. display: flex;
  138. flex-direction: row;
  139. justify-content: space-around;
  140. background-color: rgba(0, 0, 0, .5);
  141. padding: 3px 8px 2px 8px;
  142. height: 25px;
  143. /*! border: 2px black solid; */
  144. border-radius: 15px;
  145. margin-left: -26px !important;
  146. margin-top: -29px;
  147. z-index: 555;
  148. filter: opacity(100%);
  149. /*! left: -55px; */
  150. display: none
  151. }
  152.  
  153. #pabmButtonsContainer > div {
  154. filter: drop-shadow(0px 0px 2px #fffc) drop-shadow(0px 0px 2px #fffc);
  155. }
  156.  
  157. #pabmButtonsContainer:hover,
  158. .gtm-main-bookmark:hover ~ #pabmButtonsContainer {
  159. display: flex !important
  160. }
  161.  
  162. .pabmButtons:first-of-type {
  163. margin-left: -2px;
  164. }
  165.  
  166. .pabmButtons > svg:hover {
  167. filter: contrast(180%);
  168. stroke: #fff;
  169. stroke-width: .15em;
  170. stroke-opacity: 35%
  171. }
  172.  
  173. .pabmButtonSettings {
  174. margin: 3px 4px 4px 4px;
  175. }
  176.  
  177. .lowOpacity {
  178. opacity: 0.5;
  179. }
  180.  
  181. `);
  182.  
  183. //Set the button action
  184. var bkmNode = document.querySelector(".gtm-main-bookmark");
  185. bkmNode.setAttribute("href", "javascript:void(0)");
  186. bkmNode.addEventListener("click", bkmClickAdd);
  187.  
  188. //Set the hover buttons
  189. var hoverButtons = document.createElement("div");
  190. bkmNode.after(hoverButtons);
  191. hoverButtons.outerHTML = `
  192. <div id="pabmButtonsContainer">
  193. <div class="pabmButtons pabmButtonPublic">
  194. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32">
  195. <path fill-rule="evenodd" clip-rule="evenodd" d="M21 5.5a7 7 0 017 7c0 5.77-3.703 10.652-10.78 14.61a2.5 2.5 0 01-2.44 0C7.703 23.152 4 18.27 4 12.5a7 7 0 017-7c1.83 0 3.621.914 5 2.328C17.379 6.414 19.17 5.5 21 5.5z" fill="#FF4060"></path>
  196. </svg>
  197. </div>
  198. <div class="pabmButtons pabmButtonPrivate">
  199. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32">
  200. <path fill-rule="evenodd" clip-rule="evenodd" d="M21 5.5a7 7 0 017 7c0 5.77-3.703 10.652-10.78 14.61a2.5 2.5 0 01-2.44 0C7.703 23.152 4 18.27 4 12.5a7 7 0 017-7c1.83 0 3.621.914 5 2.328C17.379 6.414 19.17 5.5 21 5.5z" fill="#FF4060"></path>
  201. <path fill-rule="evenodd" clip-rule="evenodd" d="M29.98 20.523A3.998 3.998 0 0132 24v4a4 4 0 01-4 4h-7a4 4 0 01-4-4v-4c0-1.489.814-2.788 2.02-3.477a5.5 5.5 0 0110.96 0z" fill="#fff"></path>
  202. <path fill-rule="evenodd" clip-rule="evenodd" d="M28 22a2 2 0 012 2v4a2 2 0 01-2 2h-7a2 2 0 01-2-2v-4a2 2 0 012-2v-1a3.5 3.5 0 117 0v1zm-5-1a1.5 1.5 0 013 0v1h-3v-1z" fill="#1F1F1F"></path>
  203. </svg>
  204. </div>
  205. <div class="pabmButtons pabmButtonSettings">
  206. <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  207. <path fill="#444" d="M16 9v-2l-1.7-0.6c-0.2-0.6-0.4-1.2-0.7-1.8l0.8-1.6-1.4-1.4-1.6 0.8c-0.5-0.3-1.1-0.6-1.8-0.7l-0.6-1.7h-2l-0.6 1.7c-0.6 0.2-1.2 0.4-1.7 0.7l-1.6-0.8-1.5 1.5 0.8 1.6c-0.3 0.5-0.5 1.1-0.7 1.7l-1.7 0.6v2l1.7 0.6c0.2 0.6 0.4 1.2 0.7 1.8l-0.8 1.6 1.4 1.4 1.6-0.8c0.5 0.3 1.1 0.6 1.8 0.7l0.6 1.7h2l0.6-1.7c0.6-0.2 1.2-0.4 1.8-0.7l1.6 0.8 1.4-1.4-0.8-1.6c0.3-0.5 0.6-1.1 0.7-1.8l1.7-0.6zM8 12c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" />
  208. <path fill="#444" d="M10.6 7.9c0 1.381-1.119 2.5-2.5 2.5s-2.5-1.119-2.5-2.5c0-1.381 1.119-2.5 2.5-2.5s2.5 1.119 2.5 2.5z" />
  209. </svg>
  210. </div>
  211. </div>
  212. `;
  213. document.querySelector(".pabmButtonPublic").addEventListener("click", function () {
  214. bkm(0);
  215. return false;
  216. });
  217. document.querySelector(".pabmButtonPrivate").addEventListener("click", function () {
  218. bkm(1);
  219. return false;
  220. });
  221. document.querySelector(".pabmButtonSettings").addEventListener("click", function () {
  222. pref.openDialog();
  223. return false;
  224. });
  225. }
  226.  
  227. async function getthetoken(ignoreCookie = false) {
  228. var gettingCookie = getCookie("TOKENpixivajaxbm");
  229. if (gettingCookie != "" && !ignoreCookie) {
  230. theToken = gettingCookie;
  231. // console.log("🚀 ~ COOKIE ~ theToken", theToken);
  232. return;
  233. }
  234. if (theToken == "" || ignoreCookie) {
  235. let tryCount = 0;
  236. let retryLimit = 3;
  237.  
  238. function doFetch(url) {
  239. if (!url) {
  240. return;
  241. }
  242.  
  243. //Fetch the url and handle the response
  244. fetch(url, {
  245. method: "GET",
  246. credentials: "same-origin", //same-origin or include
  247. })
  248. .then((response) => response.text())
  249. .then((sss) => {
  250. // console.log("🚀 ~ .then ~ sss:", sss);
  251. //get token
  252. // Convert the HTML string into a document object
  253. let parser = new DOMParser();
  254. let doc = parser.parseFromString(sss, 'text/html');
  255.  
  256. // Get the token (multiple methods)
  257. let data = doc.querySelector("meta#meta-global-data");
  258. if (data) {
  259. try {
  260. theToken = JSON.parse(data?.content)?.token || "";
  261. } catch (error) {
  262. console.log("🚀 ~ PABM gettoken json parse error:", error);
  263. }
  264. }
  265. let match = doc.head.innerHTML.match(/pixiv\.context\.token = "([^"]+)"/);
  266. if (match?.length > 1 && !theToken) {
  267. theToken = match[1] || "";
  268. }
  269. let form = doc.querySelector("form[action^='bookmark_add'] input[name='tt']");
  270. if (form && !theToken) {
  271. theToken = form?.value || "";
  272. }
  273. // console.log("🚀 ~ GET ~ theToken", theToken);
  274.  
  275. if (theToken) {
  276. //add it to cookies (30 mins)
  277. var expires = (new Date(Date.now() + 30 * 60 * 1000)).toUTCString();
  278. document.cookie = "TOKENpixivajaxbm=" + theToken + "; expires=" + expires + ";path=/;";
  279. console.log("🚀 ~ GET ~ getthetoken SUCCESS!");
  280. return;
  281. } else {
  282. console.error("🚀 PABM ~ doFetch ~ error: Token not found!", { data }, { match }, { form }, { doc });
  283. }
  284.  
  285. //Reset the try count
  286. tryCount = 0;
  287. })
  288. .catch((error) => {
  289. console.log("🚀 PABM ~ gettoken doFetch ~ error:", error);
  290. //Increment the try count
  291. tryCount++;
  292.  
  293. //Check if the try count exceeds the retry limit
  294. if (tryCount > retryLimit) {
  295. console.log("🚀 PABM ~ gettoken doFetch ~ error: Fetch retry limit exceeded!");
  296. return;
  297. }
  298.  
  299. //Retry: call the doFetch function again with the current url instead after a delay
  300. setTimeout(function () {
  301. doFetch(document.location.href);
  302. }, 500);
  303. });
  304. }
  305. //Start fetching (use an old version of the bookmark add page, the url and illust id doesn't matter.)
  306. doFetch("https://www.pixiv.net/bookmark_add.php?type=illust&illust_id=60223956");
  307.  
  308.  
  309. }
  310. }
  311.  
  312. function bkmClickAdd(e) {
  313. // console.log("🚀 ~ bkmClickAdd ~ e:", e);
  314. try {
  315. e.stopImmediatePropagation(); //Prevent other event handlers
  316. e.preventDefault();
  317. } catch (e) {
  318. console.trace("PABM", e);
  319. }
  320. bkm(-1);
  321. return false;
  322. }
  323.  
  324. /**
  325. * asPrivate values: value < 0 means auto detect
  326. * value == 1 means always private
  327. * value == 0 means always public
  328. *
  329. */
  330. function bkm(asPrivate = -1) {
  331. let illustid = "";
  332. try {
  333. illustid = document.querySelector("link[rel=canonical]")?.href.split("artworks/")[1] || document.location.href.match(/artworks\/(\d+)/)[1];
  334. } catch (error) {
  335. console.error("🚀 PABM ~ bkm ~ error ERROR when finding illustid:", error, document.location.href);
  336. }
  337.  
  338. if (!illustid) {
  339. console.error("🚀 PABM ~ bkm ~ error ILLUSTid is invalid", document.location.href);
  340. return;
  341. }
  342.  
  343. if (!pref.get("add_all_tags")) {
  344. //don't add the tags
  345. tagsArray = [];
  346. }
  347. //var illusttype = "illust";
  348.  
  349. //Get bkm_restrict as number (As we use it in the post request)
  350. let restrict_value = Number(pref.get("bkm_restrict"));
  351. if (asPrivate >= 0) {
  352. restrict_value = asPrivate;
  353. } else {
  354. //Auto-detect privacy
  355. try {
  356. if (document.querySelector("footer li:first-of-type").innerText == "R-18" && pref.get("r18private")) {
  357. restrict_value = 1;
  358. }
  359. } catch (e) { }
  360. }
  361.  
  362. let like = pref.get("givelike");
  363. if (like) {
  364. //Use parent-sibling relationships to avoid using randomized names
  365. let likeButton = document.querySelector(".gtm-main-bookmark").parentNode.nextElementSibling.firstElementChild;
  366. if (likeButton && likeButton.disabled == false) {
  367. likeButton.classList.add("lowOpacity");
  368. likeButton.click();
  369. }
  370. }
  371.  
  372. let fetchBody = {
  373. illust_id: illustid,
  374. comment: "",
  375. restrict: restrict_value,
  376. tags: tagsArray,
  377. };
  378.  
  379. //Send bkm request
  380. bookmarkRequest(fetchBody);
  381. }
  382.  
  383. async function bookmarkRequest(fetchBody, retries = 0) {
  384. let restrict_value = fetchBody.restrict;
  385. let illustid = fetchBody.illust_id;
  386. let fetchData = JSON.stringify(fetchBody);
  387. console.log("PABM Fetch data (fetchData, token)", { fetchData }, { theToken });
  388.  
  389. //Dim the bookmark button
  390. let bkmButton = document.querySelector(".gtm-main-bookmark");
  391. let bkmButtonSvg = bkmButton.querySelector(".gtm-main-bookmark svg");
  392. bkmButtonSvg.style.opacity = 0.4;
  393.  
  394. //Add to bookmarks
  395. fetch("https://www.pixiv.net/ajax/illusts/bookmarks/add", {
  396. "headers": {
  397. "accept": "application/json",
  398. "content-type": "application/json; charset=utf-8",
  399. "x-csrf-token": theToken
  400. },
  401. "body": fetchData,
  402. "method": "POST",
  403. "credentials": "same-origin" //same-origin or include
  404. })
  405. .then(async (response) => {
  406. // console.log("PABM", response);
  407. if (!response.ok) {
  408. throw Error(response);
  409. }
  410. return response.json();
  411. })
  412. .then(async (response_json) => {
  413. // console.log("PABM", response_json);
  414. //Only continue if the response doesn't give an error
  415. if (!response_json.error) {
  416. if (restrict_value) {
  417. //INSERT THE LOCKED HEART (PRIVATE BOOKMARK) SVG https://yoksel.github.io/url-encoder/
  418. bkmButtonSvg.outerHTML = decodeURIComponent("%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' class='j89e3c-1 kcOjCr' style='color: rgb(255, 64, 96); fill: rgb(255, 64, 96);'%3E%3Cdefs%3E%3Cmask id='uid-mask-2'%3E%3Crect x='0' y='0' width='32' height='32' fill='white'%3E%3C/rect%3E%3Cpath d='M16,11.3317089 C15.0857201,9.28334665 13.0491506,7.5 11,7.5%0AC8.23857625,7.5 6,9.73857647 6,12.5 C6,17.4386065 9.2519779,21.7268174 15.7559337,25.3646328%0AC15.9076021,25.4494645 16.092439,25.4494644 16.2441073,25.3646326 C22.7480325,21.7268037 26,17.4385986 26,12.5%0AC26,9.73857625 23.7614237,7.5 21,7.5 C18.9508494,7.5 16.9142799,9.28334665 16,11.3317089 Z' class='j89e3c-0 kBfARi'%3E%3C/path%3E%3C/mask%3E%3C/defs%3E%3Cg mask='url(%23uid-mask-2)'%3E%3Cpath d='%0AM21,5.5 C24.8659932,5.5 28,8.63400675 28,12.5 C28,18.2694439 24.2975093,23.1517313 17.2206059,27.1100183%0AC16.4622493,27.5342993 15.5379984,27.5343235 14.779626,27.110148 C7.70250208,23.1517462 4,18.2694529 4,12.5%0AC4,8.63400691 7.13400681,5.5 11,5.5 C12.829814,5.5 14.6210123,6.4144028 16,7.8282366%0AC17.3789877,6.4144028 19.170186,5.5 21,5.5 Z'%3E%3C/path%3E%3Cpath d='M16,11.3317089 C15.0857201,9.28334665 13.0491506,7.5 11,7.5%0AC8.23857625,7.5 6,9.73857647 6,12.5 C6,17.4386065 9.2519779,21.7268174 15.7559337,25.3646328%0AC15.9076021,25.4494645 16.092439,25.4494644 16.2441073,25.3646326 C22.7480325,21.7268037 26,17.4385986 26,12.5%0AC26,9.73857625 23.7614237,7.5 21,7.5 C18.9508494,7.5 16.9142799,9.28334665 16,11.3317089 Z' class='j89e3c-0 kBfARi'%3E%3C/path%3E%3C/g%3E%3Cpath d='M29.9796 20.5234C31.1865 21.2121 32 22.511 32 24V28%0AC32 30.2091 30.2091 32 28 32H21C18.7909 32 17 30.2091 17 28V24C17 22.511 17.8135 21.2121 19.0204 20.5234%0AC19.2619 17.709 21.623 15.5 24.5 15.5C27.377 15.5 29.7381 17.709 29.9796 20.5234Z' class='j89e3c-2 jTfVcI' style='fill: rgb(255, 255, 255); fill-rule: evenodd; clip-rule: evenodd;'%3E%3C/path%3E%3Cpath d='M28 22C29.1046 22 30 22.8954 30 24V28C30 29.1046 29.1046 30 28 30H21%0AC19.8954 30 19 29.1046 19 28V24C19 22.8954 19.8954 22 21 22V21C21 19.067 22.567 17.5 24.5 17.5%0AC26.433 17.5 28 19.067 28 21V22ZM23 21C23 20.1716 23.6716 19.5 24.5 19.5C25.3284 19.5 26 20.1716 26 21V22H23%0AV21Z' class='j89e3c-3 fZVtyd' style='fill: rgb(31, 31, 31); fill-rule: evenodd; clip-rule: evenodd;'%3E%3C/path%3E%3C/svg%3E");
  419. }
  420. //Set bookmark button style
  421. bkmButtonSvg.style.opacity = 1.0;
  422. bkmButtonSvg.style.fill = "#ff4060";
  423. bkmButtonSvg.querySelector("path").style.fill = "white";
  424. bkmButton.href = "https://www.pixiv.net/bookmark_add.php?type=illust&illust_id=" + illustid;
  425. bkmButton.removeEventListener("click", bkmClickAdd);
  426. bkmButton.addEventListener("click", function (e) {
  427. e.stopImmediatePropagation(); //Prevent other event handlers
  428. e.preventDefault();
  429. //Open in a new tab
  430. window.open("https://www.pixiv.net/bookmark_add.php?type=illust&illust_id=" + illustid);
  431. return false;
  432. });
  433. document.querySelector("#pabmButtonsContainer").style.visibility = "hidden";
  434. } else {
  435. //Bookmark failure. Retry once after updating the token.
  436. console.error("PABM Bookmark failure 1", response_json, retries);
  437. if (retries == 0) {
  438. //Update token, ignore the cookie
  439. await getthetoken(true);
  440. bookmarkRequest(fetchBody, 1);
  441. } else {
  442. bkmButtonSvg.style.opacity = 1.0;
  443. }
  444. }
  445. }).catch(async function (erroredResponse) {
  446. //Bookmark failure. Retry once after updating the token.
  447. console.error("PABM Bookmark failure 2", erroredResponse, erroredResponse?.statusText, retries);
  448. if (retries == 0) {
  449. //Update token, ignore the cookie
  450. await getthetoken(true);
  451. bookmarkRequest(fetchBody, 1);
  452. } else {
  453. bkmButtonSvg.style.opacity = 1.0;
  454. }
  455. });
  456.  
  457. }
  458.  
  459. function getCookie(cname) {
  460. var name = cname + "=";
  461. var decodedCookie = decodeURIComponent(document.cookie);
  462. var ca = decodedCookie.split(';');
  463. for (var i = 0; i < ca.length; i++) {
  464. var c = ca[i];
  465. while (c.charAt(0) == ' ') {
  466. c = c.substring(1);
  467. }
  468. if (c.indexOf(name) == 0) {
  469. return c.substring(name.length, c.length);
  470. }
  471. }
  472. return "";
  473. }
  474.  
  475. function AddMyStyle(styleID, styleCSS) {
  476. var myStyle = document.createElement('style');
  477. //myStyle.type = 'text/css';
  478. myStyle.id = styleID;
  479. myStyle.textContent = styleCSS;
  480. document.querySelector("head").appendChild(myStyle);
  481. }
  482.  
  483. startingSoonInterval = setInterval(startingSoon, 150);