// ==UserScript== // @name Pixiv收藏夹自动标签 // @name:en Label Pixiv Bookmarks // @namespace http://tampermonkey.net/ // @version 4.3 // @description 自动为Pixiv收藏夹内图片打上已有的标签,并可以搜索收藏夹 // @description:en Automatically add existing labels for images in the bookmarks, and users are able to search the bookmarks // @author philimao // @match https://www.pixiv.net/*users/* // @icon https://www.google.com/s2/favicons?domain=pixiv.net // @resource bootstrapCSS https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css // @resource bootstrapJS https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js // @grant unsafeWindow // @grant GM_getResourceURL // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @license MIT // @downloadURL none // ==/UserScript== let uid, token, lang, userTags, synonymDict, pageInfo, currentWorks, tag; Array.prototype.toUpperCase = function () { return this.map((i) => i.toUpperCase()); }; function isEqualObject(obj1, obj2) { return ( typeof obj1 === "object" && typeof obj2 === "object" && Object.keys(obj1).every((key, i) => key === Object.keys(obj2)[i]) && Object.values(obj1).every((value, i) => value === Object.values(obj2)[i]) ); } function delay(ms) { return new Promise((res) => setTimeout(res, ms)); } function sortByParody(array) { const sortFunc = (a, b) => { let reg = /^[a-zA-Z0-9]/; if (reg.test(a) && !reg.test(b)) return -1; else if (!reg.test(a) && reg.test(b)) return 1; else return a.localeCompare(b, "zh"); }; const withParody = array.filter((key) => key.includes("(")); const withoutParody = array.filter((key) => !key.includes("(")); withoutParody.sort(sortFunc); withParody.sort(sortFunc); withParody.sort((a, b) => sortFunc(a.split("(")[1], b.split("(")[1])); return withoutParody.concat(withParody); } function loadResources() { function cssElement(url) { const link = document.createElement("link"); link.href = url; link.rel = "stylesheet"; link.type = "text/css"; return link; } function jsElement(url) { const script = document.createElement("script"); script.src = url; script.integrity = "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"; script.crossOrigin = "anonymous"; return script; } document.head.appendChild(cssElement(GM_getResourceURL("bootstrapCSS"))); document.head.appendChild(jsElement(GM_getResourceURL("bootstrapJS"))); } const bookmarkBatchSize = 100; async function fetchBookmarks(uid, tagToQuery, offset, publicationType) { const bookmarksRaw = await fetch( `https://www.pixiv.net/ajax/user/${uid}` + `/illusts/bookmarks?tag=${tagToQuery}` + `&offset=${offset}&limit=${bookmarkBatchSize}&rest=${publicationType}` ); await delay(500); const bookmarksRes = await bookmarksRaw.json(); if (!bookmarksRaw.ok || bookmarksRes.error === true) { return alert( `获取用户收藏夹列表失败 Fail to fetch user bookmarks\n` + decodeURI(bookmarksRes.message) ); } else return bookmarksRes.body; } async function updateBookmarkTags(bookmarkIds, tags, removeTags) { if (tags && tags.length) { await fetch("https://www.pixiv.net/ajax/illusts/bookmarks/add_tags", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ tags, bookmarkIds }), method: "POST", }); await delay(500); } if (removeTags && removeTags.length) { await fetch("https://www.pixiv.net/ajax/illusts/bookmarks/remove_tags", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ removeTags, bookmarkIds }), method: "POST", }); await delay(500); } } async function clearBookmarkTags(evt) { evt.preventDefault(); const selected = [ ...document.querySelectorAll("label>div[aria-disabled='true']"), ]; if ( !selected.length || !window.confirm( `确定要删除所选作品的标签吗?(作品的收藏状态不会改变) The tags of work(s) you've selected will be removed (become uncategorized). Is this okay?` ) ) return; window.runFlag = true; const works = selected.map((el) => { const middleChild = Object.values( el.parentNode.parentNode.parentNode.parentNode )[0]["child"]; const work = middleChild["memoizedProps"]["work"]; const bookmarkId = middleChild["memoizedProps"]["bookmarkId"]; work.associatedTags = middleChild["child"]["memoizedProps"]["associatedTags"]; work.bookmarkId = bookmarkId; return work; }); const modal = document.querySelector("#clear_tags_modal"); let instance = bootstrap.Modal.getInstance(modal); if (!instance) instance = new bootstrap.Modal(modal); instance.show(); const prompt = document.querySelector("#clear_tags_prompt"); const progressBar = document.querySelector("#clear_tags_progress_bar"); const total = works.length; for (let index = 1; index <= total; index++) { const work = works[index - 1]; const url = "https://www.pixiv.net/en/artworks/" + work.id; console.log(index, work.title, work.id, url); if (DEBUG) console.log(work); progressBar.innerText = index + "/" + total; const ratio = ((index / total) * 100).toFixed(2); progressBar.style.width = ratio + "%"; prompt.innerText = work.alt + "\n" + work.associatedTags.join(" "); await updateBookmarkTags([work.bookmarkId], undefined, work.associatedTags); if (!window.runFlag) { prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; progressBar.style.width = "100%"; break; } } if (window.runFlag) prompt.innerText = `标签删除完成! Tags Removed!`; setTimeout(() => { instance.hide(); if (window.runFlag) window.location.reload(); }, 1000); } async function removeCurrentTag(evt) { evt.preventDefault(); if ( !tag || tag === "未分類" || !window.confirm(`确定要删除所选的标签 ${tag} 吗?(作品的收藏状态不会改变) The tag ${tag} will be removed and works of ${tag} will keep bookmarked. Is this okay?`) ) return; window.runFlag = true; const modal = document.querySelector("#clear_tags_modal"); let instance = bootstrap.Modal.getInstance(modal); if (!instance) instance = new bootstrap.Modal(modal); instance.show(); const prompt = document.querySelector("#clear_tags_prompt"); const progressBar = document.querySelector("#clear_tags_progress_bar"); let total = 9999, offset = 0, totalBookmarks = [], bookmarks = { works: [] }; do { bookmarks = await fetchBookmarks(uid, tag, offset, "show"); total = bookmarks["total"]; offset += bookmarkBatchSize; offset = Math.min(offset, total); progressBar.innerText = offset + "/" + total; const ratio = ((offset / total) * 90).toFixed(2); progressBar.style.width = ratio + "%"; totalBookmarks = totalBookmarks.concat(bookmarks["works"]); if (!window.runFlag) { prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; progressBar.style.width = "100%"; break; } } while (offset < total); console.log(totalBookmarks); if (window.runFlag) { progressBar.style.width = 90 + "%"; prompt.innerText = `标签${tag}删除中... Removing Tag ${tag} `; const ids = totalBookmarks.map((work) => work["bookmarkData"]["id"]); if (ids.length) await updateBookmarkTags(ids, undefined, [tag]); progressBar.style.width = 100 + "%"; prompt.innerText = `标签${tag}删除完成! Tag ${tag} Removed!`; } setTimeout(() => { instance.hide(); if (window.runFlag) window.location.href = `https://www.pixiv.net/users/${uid}/bookmarks/artworks`; }, 1000); } const DEBUG = false; async function handleLabel(evt) { evt.preventDefault(); const addFirst = document.querySelector("#label_add_first").value; const tagToQuery = document.querySelector("#label_tag_query").value; const publicationType = document.querySelector( "#label_publication_type" ).value; const retainTag = document.querySelector("#label_retain_tag").value; console.log("Label Configuration:"); console.log( `addFirst: ${addFirst === "true"}; tagToQuery: ${tagToQuery}; retainTag: ${ retainTag === "true" }; publicationType: ${publicationType}` ); window.runFlag = true; const promptBottom = document.querySelector("#label_prompt"); promptBottom.innerText = "处理中,请勿关闭窗口\nProcessing. Please do not close the window."; const objDiv = document.querySelector("#label_form"); objDiv.scrollTop = objDiv.scrollHeight; // fetch bookmarks let total, // total bookmarks of specific tag index = 0, // counter of do-while loop offset = 0; // as uncategorized ones will decrease, offset means num of images "successfully" updated // update progress bar const progressBar = document.querySelector("#progress_bar"); progressBar.style.width = "0"; const intervalId = setInterval(() => { if (total) { progressBar.innerText = index + "/" + total; const ratio = ((index / total) * 100).toFixed(2); progressBar.style.width = ratio + "%"; if (!window.runFlag || index === total) { console.log("Progress bar stops updating"); clearInterval(intervalId); } } }, 1000); do { const realOffset = tagToQuery === "未分類" ? offset : index; const bookmarks = await fetchBookmarks( uid, tagToQuery, realOffset, publicationType ); if (DEBUG) console.log("Bookmarks", bookmarks); if (!total) total = bookmarks.total; for (let work of bookmarks["works"]) { const url = "https://www.pixiv.net/en/artworks/" + work.id; if (DEBUG) console.log(index, work.title, work.id, url); index++; // ---- means unavailable, hidden or deleted by author if (work.title === "-----") { offset++; continue; } const workTags = work["tags"]; let intersection = userTags.filter((userTag) => { // if work tags includes this user tag if ( workTags.toUpperCase().includes(userTag.toUpperCase()) || workTags.toUpperCase().includes(userTag.toUpperCase().split("(")[0]) ) return true; // if work tags match an user alias (exact match) return ( synonymDict[userTag] && synonymDict[userTag].find( (alias) => workTags.toUpperCase().includes(alias.toUpperCase()) || workTags.toUpperCase().includes(alias.toUpperCase().split("(")[0]) ) ); }); // if workTags match some alias, add it to the intersection (exact match, with or without parody name) intersection = intersection.concat( workTags .map((workTag) => { for (let aliasName of Object.keys(synonymDict)) { if ( synonymDict[aliasName].toUpperCase().includes(workTag) || synonymDict[aliasName] .toUpperCase() .includes(workTag.split("(")[0]) ) return aliasName; } }) .filter((i) => i) ); // remove duplicate intersection = Array.from(new Set(intersection)); const bookmarkId = work["bookmarkData"]["id"]; const prevTags = bookmarks["bookmarkTags"][bookmarkId] || []; let removeTags = []; if (retainTag === "false") removeTags = prevTags.filter((tag) => !intersection.includes(tag)); const addTags = intersection.filter((tag) => !prevTags.includes(tag)); if (!intersection.length && !prevTags.length) { if (addFirst === "true") { intersection.push(workTags[0]); userTags.push(workTags[0]); } } // for uncategorized if (!intersection.length) { offset++; } if (addTags.length || removeTags.length) { if (!DEBUG) console.log(index, work.title, work.id, url); console.log("\tprevTags:", prevTags); console.log("\tintersection:", intersection); console.log("\taddTags:", addTags, "removeTags:", removeTags); } await updateBookmarkTags([bookmarkId], addTags, removeTags); if (!window.runFlag) { promptBottom.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; index = total; break; } } } while (index < total); if (total === 0) { promptBottom.innerText = `指定分类下暂无符合要求的作品,请关闭窗口 Works needed to be labeled not found. Please close the window. `; } else if (window.runFlag) { promptBottom.innerText = `自动添加标签已完成,请关闭窗口并刷新网页 Auto labeling finished successfully. Please close the window and refresh. `; } window.runFlag = false; } let prevSearch, searchBatch, searchResults, searchOffset, totalBookmarks; async function handleSearch(evt) { evt.preventDefault(); const searchString = document .querySelector("#search_value") .value.replace(/!/g, "!"); const matchPattern = document.querySelector("#search_exact_match").value; const tagToQuery = document.querySelector("#search_select_tag").value; const publicationType = document.querySelector("#search_publication").value; const newSearch = { searchString, matchPattern, tagToQuery, publicationType }; // initialize new search const resultsDiv = document.querySelector("#search_results"); const noResult = document.querySelector("#no_result"); if (noResult) resultsDiv.removeChild(noResult); if (!prevSearch || !isEqualObject(prevSearch, newSearch)) { prevSearch = newSearch; searchResults = []; searchOffset = 0; totalBookmarks = 0; searchBatch = 200; document.querySelector("#search_prompt").innerText = ""; while (resultsDiv.firstChild) { resultsDiv.removeChild(resultsDiv.firstChild); } } else { searchBatch += 200; } if (searchOffset && searchOffset === totalBookmarks) return alert(` 已经完成所选标签下所有收藏的搜索! All Bookmarks Of Selected Tag Have Been Searched! `); document.querySelector("#spinner").style.display = "block"; const collapseIns = bootstrap.Collapse.getInstance( document.querySelector("#advanced_search") ); if (collapseIns) collapseIns.hide(); let includeArray = searchString .split(" ") .filter((el) => el.length && !el.includes("!")); let excludeArray = searchString .split(" ") .filter((el) => el.length && el.includes("!")) .map((el) => el.slice(1)); console.log("Search Configuration:"); console.log( `matchPattern: ${matchPattern}; tagToQuery: ${tagToQuery}; publicationType: ${publicationType}` ); console.log("includeArray:", includeArray, "excludeArray", excludeArray); let index = 0; // index for current search batch do { const bookmarks = await fetchBookmarks( uid, tagToQuery, searchOffset, publicationType ); document.querySelector("#search_prompt").innerText = ` 当前搜索进度 / Searched:${searchOffset} / ${totalBookmarks} `; if (DEBUG) console.log(bookmarks); if (!totalBookmarks) { totalBookmarks = bookmarks.total; } for (let work of bookmarks["works"]) { if (DEBUG) { console.log(searchOffset, work.title, work.id); console.log(work["tags"]); } index++; searchOffset++; if (work.title === "-----") continue; const workTags = work["tags"]; const ifInclude = (keyword) => { // especially, R-18 tag is labelled in work if (["R-18", "R18", "r18"].includes(keyword)) return work["xRestrict"]; // keywords from user input, alias from dict // keyword: 新世纪福音战士 // alias: EVA eva const el = Object.keys(synonymDict) .map((i) => [i.split("(")[0], i]) .find( (el) => el[0].toUpperCase() === keyword.toUpperCase() || (matchPattern === "fuzzy" && el[0].toUpperCase().includes(keyword.toUpperCase())) ); const keywordArray = el ? synonymDict[el[1]].concat(keyword) : [keyword]; if ( keywordArray.some((kw) => workTags.toUpperCase().includes(kw.toUpperCase()) ) || keywordArray.some( ( kw // remove work tag braces ) => workTags .map((tag) => tag.split("(")[0]) .toUpperCase() .includes(kw.toUpperCase()) ) ) return true; if (matchPattern === "exact") return false; return keywordArray.some( (kw) => workTags.some((tag) => tag.toUpperCase().includes(kw.toUpperCase()) ) || keywordArray.some( ( kw // remove work tag braces ) => workTags .toUpperCase() .map((tag) => tag.split("(")[0]) .some((tag) => tag.includes(kw.toUpperCase())) ) ); }; if (includeArray.every(ifInclude) && !excludeArray.some(ifInclude)) { searchResults.push(work); const container = document.createElement("div"); container.className = "col-4 col-lg-3 col-xl-2 p-1"; container.innerHTML = `
`; resultsDiv.appendChild(container); } } } while (searchOffset < totalBookmarks && index < searchBatch); if (totalBookmarks === 0) document.querySelector("#search_prompt").innerText = "无结果 / No Result"; else document.querySelector("#search_prompt").innerText = ` 当前搜索进度 / Searched:${searchOffset} / ${totalBookmarks} `; if (searchOffset < totalBookmarks) document.querySelector("#search_more").style.display = "block"; else document.querySelector("#search_more").style.display = "none"; if (!searchResults.length) { resultsDiv.innerHTML = `