// ==UserScript== // @name Pixiv收藏夹自动标签 // @name:en Label Pixiv Bookmarks // @namespace http://tampermonkey.net/ // @version 5.2 // @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.2.0-beta1/dist/css/bootstrap.min.css // @resource bootstrapJS https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js // @grant unsafeWindow // @grant GM_getResourceURL // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @license MIT // @downloadURL none // ==/UserScript== let uid, token, lang, userTags, userTagDict, synonymDict, pageInfo, theme, generator, feature; // noinspection TypeScriptUMDGlobal,JSUnresolvedVariable let unsafeWindow_ = unsafeWindow, GM_getValue_ = GM_getValue, GM_setValue_ = GM_setValue, GM_addStyle_ = GM_addStyle, GM_getResourceURL_ = GM_getResourceURL, GM_registerMenuCommand_ = GM_registerMenuCommand; Array.prototype.toUpperCase = function () { return this.map((i) => i.toUpperCase()); }; function isEqualObject(obj1, obj2) { if (typeof obj1 !== "object") return obj1 === obj2; return ( typeof obj1 === typeof obj2 && Object.keys(obj1).every((key, i) => key === Object.keys(obj2)[i]) && Object.values(obj1).every((value, i) => isEqualObject(value, Object.values(obj2)[i]) ) ); } function delay(ms) { return new Promise((res) => setTimeout(res, ms)); } function getValue(name, defaultValue) { return GM_getValue_(name, defaultValue); } function setValue(name, value) { if (name === "synonymDict" && (!value || !Object.keys(value).length)) return; GM_setValue_(name, value); // backup let valueArray = JSON.parse(window.localStorage.getItem(name)); if (!valueArray) valueArray = []; // save the dict by date if (name === "synonymDict") { const date = new Date().toLocaleDateString(); // not update if of same value if (valueArray.length) { if ( !valueArray.find( (el) => JSON.stringify(el.value) === JSON.stringify(value) ) ) { const lastElem = valueArray[valueArray.length - 1]; if (lastElem.date === date) { // append only for (let key of Object.keys(value)) { if (lastElem.value[key]) { // previous key lastElem.value[key] = Array.from( new Set(lastElem["value"][key].concat(value[key])) ); } else { // new key lastElem.value[key] = value[key]; } } valueArray.pop(); valueArray.push(lastElem); } else { if (valueArray.length > 30) valueArray.shift(); valueArray.push({ date, value }); } window.localStorage.setItem(name, JSON.stringify(valueArray)); } else { // same value, pass } } else { // empty array valueArray.push({ date, value }); window.localStorage.setItem(name, JSON.stringify(valueArray)); } } else { if (valueArray.length > 30) valueArray.shift(); valueArray.push(value); window.localStorage.setItem(name, JSON.stringify(valueArray)); } } function addStyle(style) { GM_addStyle_(style); } // merge all previous dict and return function restoreSynonymDict() { const value = window.localStorage.getItem("synonymDict"); if (!value) return {}; const dictArray = JSON.parse(value); const newDict = {}; for (let elem of dictArray) { const dict = elem.value; // merge all history value for the key Object.keys(dict).forEach((key) => { if (newDict[key]) newDict[key] = Array.from(new Set(newDict[key].concat(dict[key]))); else newDict[key] = dict[key]; }); } const a = document.createElement("a"); a.href = URL.createObjectURL( new Blob([JSON.stringify([newDict].concat(dictArray))], { type: "application/json", }) ); a.setAttribute( "download", `synonym_dict_restored_${new Date().toLocaleDateString()}.json` ); a.click(); } 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.id = "bootstrapCSS"; link.href = url; link.rel = "stylesheet"; link.type = "text/css"; return link; } function jsElement(url) { const script = document.createElement("script"); script.id = "bootstrapJS"; script.src = url; return script; } document.head.appendChild(cssElement(GM_getResourceURL_("bootstrapCSS"))); document.head.appendChild(jsElement(GM_getResourceURL_("bootstrapJS"))); // overwrite bootstrap global box-sizing style const style = document.createElement("style"); style.id = "LB_overwrite"; style.innerHTML = "*,::after,::before { box-sizing: content-box; } .btn,.form-control,.form-select,.row>* { box-sizing: border-box; } body { background: initial; }"; document.head.appendChild(style); } const bookmarkBatchSize = 100; async function fetchBookmarks(uid, tagToQuery, offset, publicationType) { const bookmarksRaw = await fetch( `/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( `获取用户收藏夹列表失败\nFail to fetch user bookmarks\n` + decodeURI(bookmarksRes.message) ); } else return bookmarksRes.body; } async function fetchAllBookmarksByTag( tag, publicationType, progressBar, max = 100 ) { let total = 65535, offset = 0, totalWorks = []; do { if (!window.runFlag) break; const bookmarks = await fetchBookmarks(uid, tag, offset, publicationType); total = bookmarks.total; const works = bookmarks["works"]; works.forEach( (w) => (w.associatedTags = bookmarks["bookmarkTags"][w["bookmarkData"]["id"]] || []) ); totalWorks.push(...works); offset = totalWorks.length; if (progressBar) { progressBar.innerText = offset + "/" + total; const ratio = ((offset / total) * max).toFixed(2); progressBar.style.width = ratio + "%"; } } while (offset < total); return totalWorks; } async function updateBookmarkTags(bookmarkIds, addTags, removeTags) { if (!bookmarkIds?.length) throw new TypeError("BookmarkIds is undefined or empty array"); if (!Array.isArray(addTags) && !Array.isArray(removeTags)) throw new TypeError("Either addTags or removeTags should be valid array"); async function run(ids) { if (addTags && addTags.length) { await fetch("/ajax/illusts/bookmarks/add_tags", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ tags: addTags, bookmarkIds: ids }), method: "POST", }); await delay(500); } if (removeTags && removeTags.length) { await fetch("/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: ids }), method: "POST", }); await delay(500); } } const num = Math.ceil(bookmarkIds.length / bookmarkBatchSize); for (let i of [...Array(num).keys()]) { if (!window.runFlag) break; const ids = bookmarkIds.filter( (_, j) => j >= i * bookmarkBatchSize && j < (i + 1) * bookmarkBatchSize ); await run(ids); } } async function updateBookmarkRestrict( bookmarkIds, bookmarkRestrict, progressBar ) { if (!bookmarkIds?.length) throw new TypeError("BookmarkIds is undefined or empty array"); if (!["public", "private"].includes(bookmarkRestrict)) throw new TypeError("Bookmark restrict should be public or private"); async function run(ids) { await fetch("/ajax/illusts/bookmarks/edit_restrict", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ bookmarkIds: ids, bookmarkRestrict }), method: "POST", }); await delay(500); } const num = Math.ceil(bookmarkIds.length / bookmarkBatchSize); for (let i of [...Array(num).keys()]) { if (!window.runFlag) break; const ids = bookmarkIds.filter( (_, j) => j >= i * bookmarkBatchSize && j < (i + 1) * bookmarkBatchSize ); await run(ids); if (progressBar) { const offset = i * bookmarkBatchSize; progressBar.innerText = offset + "/" + bookmarkIds.length; const ratio = (offset / bookmarkIds.length).toFixed(2); progressBar.style.width = ratio + "%"; } } if (progressBar) { progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; progressBar.style.width = "100%"; } } async function clearBookmarkTags(works) { if (!works?.length) { return alert( `没有获取到收藏夹内容,操作中断,请检查选项下是否有作品\nFetching bookmark information failed. Abort operation. Please check the existence of works with the configuration` ); } if ( !window.confirm( `确定要删除所选作品的标签吗?(作品的收藏状态不会改变)\nThe tags of work(s) you've selected will be removed (become uncategorized). Is this okay?` ) ) return; window.runFlag = true; const modal = document.querySelector("#progress_modal"); // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; let instance = bootstrap_.Modal.getInstance(modal); if (!instance) instance = new bootstrap_.Modal(modal); instance.show(); const prompt = document.querySelector("#progress_modal_prompt"); const progressBar = document.querySelector("#progress_modal_progress_bar"); const tagPool = Array.from( new Set(works.reduce((a, b) => [...a, ...b.associatedTags], [])) ); const workLength = works.length; const tagPoolSize = tagPool.length; if (DEBUG) console.log(works, tagPool); if (workLength > tagPoolSize) { for (let index = 1; index <= tagPoolSize; index++) { if (!window.runFlag) break; const tag = tagPool[index - 1]; const ids = works .filter((w) => w.associatedTags.includes(tag)) .map((w) => w.bookmarkId || w["bookmarkData"]["id"]); if (DEBUG) console.log("Clearing", tag, ids); progressBar.innerText = index + "/" + tagPoolSize; const ratio = ((index / tagPoolSize) * 100).toFixed(2); progressBar.style.width = ratio + "%"; prompt.innerText = `正在清除标签... / Clearing bookmark tags`; await updateBookmarkTags(ids, null, [tag]); } } else { for (let index = 1; index <= workLength; index++) { if (!window.runFlag) break; const work = works[index - 1]; const url = "https://www.pixiv.net/artworks/" + work.id; console.log(index, work.title, work.id, url); if (DEBUG) console.log(work); progressBar.innerText = index + "/" + workLength; const ratio = ((index / workLength) * 100).toFixed(2); progressBar.style.width = ratio + "%"; prompt.innerText = work.alt + "\n" + work.associatedTags.join(" "); await updateBookmarkTags( [work["bookmarkData"]["id"]], undefined, work.associatedTags ); } } if (window.runFlag) prompt.innerText = `标签删除完成!\nFinish Tag Clearing!`; else prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; setTimeout(() => { instance.hide(); if (window.runFlag && !DEBUG) window.location.reload(); }, 1000); } async function handleClearBookmarkTags(evt) { evt.preventDefault(); const selected = [ ...document.querySelectorAll("label>div[aria-disabled='true']"), ]; if (!selected.length) return; const works = selected .map((el) => { const middleChild = Object.values( el.parentNode.parentNode.parentNode.parentNode )[0]["child"]; const work = middleChild["memoizedProps"]["work"]; work.associatedTags = middleChild["child"]["memoizedProps"]["associatedTags"] || []; work.bookmarkId = middleChild["memoizedProps"]["bookmarkId"]; return work; }) .filter((work) => work.associatedTags.length); await clearBookmarkTags(works); } async function deleteTag(tag, publicationType) { if (!tag) return alert( `请选择需要删除的标签\nPlease select the tag you would like to delete` ); if ( tag === "未分類" || !window.confirm( `确定要删除所选的标签 ${tag} 吗?(作品的收藏状态不会改变)\nThe tag ${tag} will be removed and works of ${tag} will keep bookmarked. Is this okay?` ) ) return; window.runFlag = true; const modal = document.querySelector("#progress_modal"); // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; let instance = bootstrap_.Modal.getInstance(modal); if (!instance) instance = new bootstrap_.Modal(modal); await instance.show(); const prompt = document.querySelector("#progress_modal_prompt"); const progressBar = document.querySelector("#progress_modal_progress_bar"); const totalBookmarks = await fetchAllBookmarksByTag( tag, publicationType, progressBar, 90 ); console.log(totalBookmarks); if (window.runFlag) { prompt.innerText = `标签${tag}删除中...\nDeleting Tag ${tag}`; progressBar.style.width = "90%"; } else { prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; progressBar.style.width = "100%"; return; } const ids = totalBookmarks.map((work) => work["bookmarkData"]["id"]); await updateBookmarkTags(ids, undefined, [tag]); progressBar.style.width = "100%"; if (window.runFlag) prompt.innerText = `标签${tag}删除完成!\nTag ${tag} Removed!`; else prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; setTimeout(() => { instance.hide(); if (window.runFlag && !DEBUG) window.location.href = `https://www.pixiv.net/users/${uid}/bookmarks/artworks?rest=${publicationType}`; }, 1000); } async function handleDeleteTag(evt) { evt.preventDefault(); const { tag, restrict } = await updateWorkInfo(); await deleteTag(tag, restrict ? "hide" : "show"); } const DEBUG = true; async function handleLabel(evt) { evt.preventDefault(); const addFirst = document.querySelector("#label_add_first").value; const tagToQuery = document.querySelector("#label_tag_query").value; const publicationType = (await updateWorkInfo())["restrict"] ? "hide" : "show"; const labelR18 = document.querySelector("#label_r18").value; const exclusion = document .querySelector("#label_exclusion") .value.split(/[\s\n]/) .filter((t) => t); console.log("Label Configuration:"); console.log( `addFirst: ${addFirst === "true"}; tagToQuery: ${tagToQuery}; labelR18: ${ labelR18 === "true" }; publicationType: ${publicationType}; exclusion: ${exclusion.join(",")}` ); 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/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()) || // full tag workTags .toUpperCase() .includes(userTag.toUpperCase().split("(")[0]) || // char name workTags.some( (workTag) => (workTag.toUpperCase().split("(")[1] || "").split(")")[0] === userTag ) // parody name ) return true; // if work tags match a user alias (exact match) return ( synonymDict[userTag] && synonymDict[userTag].find( (alias) => workTags.toUpperCase().includes(alias.toUpperCase()) || workTags .toUpperCase() .includes(alias.toUpperCase().split("(")[0]) || workTags.some( (workTag) => (workTag.toUpperCase().split("(")[1] || "").split(")")[0] === alias ) // parody name ) ); }); // 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 ( aliasName.toUpperCase() === workTag.toUpperCase() || synonymDict[aliasName] .toUpperCase() .includes(workTag.toUpperCase()) || synonymDict[aliasName] .toUpperCase() .includes(workTag.split("(")[0].toUpperCase()) ) return aliasName; } }) .filter((i) => i) ); if (work["xRestrict"] && labelR18 === "true") intersection.push("R-18"); // remove duplicate and exclusion intersection = Array.from(new Set(intersection)).filter( (t) => !exclusion.includes(t) ); const bookmarkId = work["bookmarkData"]["id"]; const prevTags = bookmarks["bookmarkTags"][bookmarkId] || []; if (!intersection.length && !prevTags.length) { if (addFirst === "true") { const first = workTags.find((tag) => !exclusion.includes(tag)); if (first) { intersection.push(first); userTags.push(first); } } } const addTags = intersection.filter((tag) => !prevTags.includes(tag)); // for uncategorized if (!intersection.length) { offset++; } if (addTags.length) { if (!DEBUG) console.log(index, work.title, work.id, url); console.log("\tprevTags:", prevTags); console.log("\tintersection:", intersection); console.log("\taddTags:", addTags); } else continue; promptBottom.innerText = `处理中,请勿关闭窗口 / Processing. Please do not close the window.\n${work.alt}`; await updateBookmarkTags([bookmarkId], addTags); 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(); let searchMode = 0; let searchString = document.querySelector("#search_value")?.value; if ( document.querySelector("#basic_search_field").className.includes("d-none") ) { searchMode = 1; searchString = [...document.querySelectorAll(".advanced_search_field")] .map((el) => el.value.split(" ")[0]) .join(" "); } searchString = searchString.replace(/!/g, "!").trim(); const searchStringArray = searchString.split(" "); let searchConfigs = Array(searchStringArray.length).fill(Array(4).fill(true)); if (searchMode) { const advanced = document.querySelector("#advanced_search_fields"); const configContainers = [...advanced.querySelectorAll(".row")]; searchConfigs = configContainers.map((el) => [...el.querySelectorAll("input")].map((i) => i.checked) ); } const matchPattern = document.querySelector("#search_exact_match").value; const tagsLengthMatch = document.querySelector("#search_length_match").value === "true"; const tagToQuery = document.querySelector("#search_select_tag").value; const publicationType = document.querySelector("#search_publication").value; const newSearch = { searchString, searchConfigs, matchPattern, tagsLengthMatch, tagToQuery, publicationType, }; // initialize new search window.runFlag = true; 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); } clearTimeout(timeout); document.querySelector("#search_suggestion").parentElement.style.display = "none"; } else { searchBatch += 200; } if (searchOffset && searchOffset === totalBookmarks) { window.runFlag = false; return alert(` 已经完成所选标签下所有收藏的搜索! All Bookmarks Of Selected Tag Have Been Searched! `); } const spinner = document.querySelector("#spinner"); spinner.style.display = "block"; // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; const collapseIns = bootstrap_.Collapse.getInstance( document.querySelector("#advanced_search") ); if (collapseIns) collapseIns.hide(); let includeArray = searchStringArray.filter( (el) => el.length && !el.includes("!") ); let excludeArray = searchStringArray .filter((el) => el.length && el.includes("!")) .map((el) => el.slice(1)); console.log("Search Configuration:", searchConfigs); console.log( `matchPattern: ${matchPattern}; tagsLengthMatch: ${tagsLengthMatch}; tagToQuery: ${tagToQuery}; publicationType: ${publicationType}` ); console.log("includeArray:", includeArray, "excludeArray", excludeArray); const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; const searchPrompt = document.querySelector("#search_prompt"); let index = 0; // index for current search batch do { const bookmarks = await fetchBookmarks( uid, tagToQuery, searchOffset, publicationType ); searchPrompt.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 bookmarkTags = bookmarks["bookmarkTags"][work["bookmarkData"]["id"]] || []; // empty if uncategorized 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"]; const index = searchStringArray.findIndex((kw) => kw.includes(keyword)); const config = searchConfigs[index]; if (DEBUG) console.log(keyword, config); // convert input keyword to a user tag // keywords from user input, alias from dict // keyword: 新世纪福音战士 // alias: EVA eva const el = Object.keys(synonymDict) .map((key) => [key.split("(")[0], key]) // [char name, full key] .find( (el) => el[0].toUpperCase() === keyword.toUpperCase() || // input match char name el[1].toUpperCase() === keyword.toUpperCase() || // input match full name synonymDict[el[1]] .toUpperCase() .includes(keyword.toUpperCase()) || // input match any alias (matchPattern === "fuzzy" && (el[1].toUpperCase().includes(keyword.toUpperCase()) || synonymDict[el[1]] .toUpperCase() .some((alias) => alias.includes(keyword.toUpperCase())))) ); const keywordArray = [keyword]; if (el) { keywordArray.push(...el); keywordArray.push(...synonymDict[el[1]]); } if ( keywordArray.some( (kw) => (config[0] && work.title.toUpperCase().includes(kw.toUpperCase())) || (config[1] && work["userName"].toUpperCase().includes(kw.toUpperCase())) || (config[2] && workTags.toUpperCase().includes(kw.toUpperCase())) || (config[3] && bookmarkTags.toUpperCase().includes(kw.toUpperCase())) ) ) return true; if (matchPattern === "exact") return false; return keywordArray.some( (kw) => (config[2] && workTags .toUpperCase() .some((tag) => tag.includes(kw.toUpperCase()))) || (config[3] && bookmarkTags .toUpperCase() .some((tag) => tag.includes(kw.toUpperCase()))) ); }; if ( (!tagsLengthMatch || includeArray.length === bookmarkTags.length) && includeArray.every(ifInclude) && !excludeArray.some(ifInclude) ) { searchResults.push(work); displayWork(work, resultsDiv, textColor); } } } 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 = `
暂无结果 / No Result
`; } spinner.style.display = "none"; console.log(searchResults); window.runFlag = false; } function displayWork(work, resultsDiv, textColor) { const tagsString = work.tags .slice(0, 6) .map((i) => "#" + i) .join(" "); const container = document.createElement("div"); const profile = work["profileImageUrl"] || "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; container.className = "col-4 col-lg-3 col-xl-2 p-1"; container.innerHTML = `
square
R-18
${work["pageCount"]}
${tagsString}
${work.title}
profile ${work["userName"]}
`; if (work["xRestrict"]) container.querySelector(".rate-icon").classList.remove("d-none"); if (work["pageCount"] > 1) container.querySelector(".page-icon").classList.remove("d-none"); container.firstElementChild.addEventListener("click", (evt) => galleryMode(evt, work) ); resultsDiv.appendChild(container); } function galleryMode(evt, work) { const modal = evt.path.find((el) => el.id.includes("modal")); const scrollTop = modal.scrollTop; const dialog = evt.path.find((el) => el.className.includes("modal-dialog")); dialog.classList.add("modal-fullscreen"); const title = dialog.querySelector(".modal-header"); const body = dialog.querySelector(".modal-body"); const footer = dialog.querySelector(".modal-footer"); const gallery = modal.querySelector(".gallery"); const works = modal.id === "search_modal" ? searchResults : generatedResults; let index = works.findIndex((w) => w === work); const host = "https://i.pximg.net/img-master"; gallery.innerHTML = `
`; const imageContainer = gallery.querySelector(".images"); const all = gallery.querySelector("#gallery_all"); let pageIndex = 0, pageLoaded = false, masterUrl; function updateWork(work) { masterUrl = work.url.includes("limit_unknown") ? work.url : host + work.url .match(/\/img\/.*/)[0] .replace(/_(custom|square)1200/, "_master1200"); imageContainer.innerHTML = `
master
`; gallery.querySelector("#gallery_link").href = "/artworks/" + work.id; pageIndex = 0; pageLoaded = false; if (work["pageCount"] > 1) all.classList.remove("d-none"); else all.classList.add("d-none"); } updateWork(work); function loadAll() { const work = works[index]; [...Array(work["pageCount"] - 1).keys()].forEach((i) => { const p = i + 1; const url = masterUrl.replace("_p0_", `_p${p}_`); const div = document.createElement("div"); div.className = "text-center"; div.innerHTML = `page${p}`; imageContainer.appendChild(div); }); all.classList.add("d-none"); pageLoaded = true; } all.addEventListener("click", loadAll); gallery.querySelector("#gallery_exit").addEventListener("click", () => { dialog.classList.remove("modal-fullscreen"); gallery.classList.add("d-none"); title.classList.remove("d-none"); body.classList.remove("d-none"); footer.classList.remove("d-none"); modal.removeEventListener("keyup", fnKey); modal.scrollTo({ top: scrollTop, behavior: "smooth" }); }); function preFetch(work) { const url = work.url.includes("limit_unknown") ? work.url : host + work.url .match(/\/img\/.*/)[0] .replace(/_(custom|square)1200/, "_master1200"); const img = new Image(); img.src = url; } function prev() { if (works[index - 1]) { index--; updateWork(works[index]); if (works[index - 1]) preFetch(works[index - 1]); } } function next() { if (works[index + 1]) { index++; updateWork(works[index]); if (works[index + 1]) preFetch(works[index + 1]); } } function up() { if (!pageLoaded) return; const scrollY = gallery.scrollTop; pageIndex = Math.max(0, pageIndex - 1); const elemTop = imageContainer.children[pageIndex].getBoundingClientRect().top; gallery.scrollTo({ top: scrollY + elemTop }); } function down() { if (!pageLoaded) return loadAll(); const scrollY = gallery.scrollTop; pageIndex = Math.min(pageIndex + 1, works[index]["pageCount"] - 1); const elemTop = imageContainer.children[pageIndex].getBoundingClientRect().top; gallery.scrollTo({ top: scrollY + elemTop }); } function fnKey(evt) { if (evt.key === "ArrowLeft") prev(); else if (evt.key === "ArrowRight") next(); else if (evt.key === "ArrowUp") up(); else if (evt.key === "ArrowDown") down(); } gallery.querySelector("#gallery_left").addEventListener("click", prev); gallery.querySelector("#gallery_right").addEventListener("click", next); modal.addEventListener("keyup", fnKey); gallery.classList.remove("d-none"); title.classList.add("d-none"); body.classList.add("d-none"); footer.classList.add("d-none"); } let prevTag, prevRestriction, totalAvailable, generatorBookmarks, generatedResults, generatorDisplayLimit, generatorBatchNum; async function handleGenerate(evt) { evt.preventDefault(); const tag = document.querySelector("#generator_select_tag").value; const batchSize = Math.max( 0, parseInt(document.querySelector("#generator_form_num").value) || 100 ); const publicationType = document.querySelector( "#generator_form_publication" ).value; const restriction = document.querySelector( "#generator_form_restriction" ).value; console.log(tag, batchSize, publicationType, restriction); if ( !tag && !confirm( `加载全部收藏夹需要较长时间,是否确认操作?\nLoad the whole bookmark will take quite long time to process. Is this okay?` ) ) return; const resultsDiv = document.querySelector("#generator_results"); while (resultsDiv.firstChild) { resultsDiv.removeChild(resultsDiv.firstChild); } const display = document.querySelector("#generator_display"); display.classList.remove("d-none"); const prompt = document.querySelector("#generator_save_tag_prompt"); if (prevTag !== tag || !generatorBookmarks?.length) { prevTag = tag; prevRestriction = null; generatorDisplayLimit = 12; generatorBookmarks = []; generatorBatchNum = -1; let offset = 0, total = 0; window.runFlag = true; prompt.classList.remove("d-none"); prompt.innerText = "正在加载收藏夹信息,点击停止可中断运行 / Loading bookmarks, Click stop to abort"; do { if (!window.runFlag) break; const bookmarks = await fetchBookmarks(uid, tag, offset, publicationType); if (!total) { total = bookmarks.total; prompt.innerText = `正在加载收藏夹信息(${total}),点击停止可中断运行 / Loading bookmarks (${total}), Click stop to abort`; } generatorBookmarks.push(...bookmarks["works"]); offset = generatorBookmarks.length; } while (offset < total); prompt.classList.add("d-none"); window.runFlag = false; shuffle(generatorBookmarks); } if (prevRestriction !== restriction) { prevRestriction = restriction; generatorBatchNum = -1; if (restriction !== "all") { generatorBookmarks.forEach((w) => { w.used = !!( (restriction === "sfw" && w["xRestrict"]) || (restriction === "nsfw" && !w["xRestrict"]) ); }); } totalAvailable = generatorBookmarks.filter((w) => !w.used).length; document.querySelector("#generator_spinner").classList.add("d-none"); console.log(generatorBookmarks); } if (!totalAvailable) { display.classList.add("d-none"); prompt.innerText = "图片加载失败 / Image Loading Failed"; return; } document.querySelector("#generator_form_buttons").classList.remove("d-none"); let availableBookmarks = generatorBookmarks.filter((w) => !w.used); if (generatorBookmarks.length && !availableBookmarks.length) { generatorBatchNum = -1; generatorBookmarks.forEach((w) => { if ( restriction === "all" || (restriction === "sfw" && !w["xRestrict"]) || (restriction === "nsfw" && w["xRestrict"]) ) w.used = false; }); availableBookmarks = generatorBookmarks.filter((w) => !w.used); } generatorBatchNum++; const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; generatedResults = availableBookmarks.slice(0, batchSize); generatedResults.forEach((w) => (w.used = true)); generatedResults .filter((_, i) => i < generatorDisplayLimit) .forEach((w) => displayWork(w, resultsDiv, textColor)); if (generatedResults.length > generatorDisplayLimit) { document.querySelector("#generator_more").classList.remove("d-none"); } document.querySelector( "#generator_prompt" ).innerText = `当前批次 / Batch Num: ${generatorBatchNum} | 当前展示 / Display: ${generatedResults.length} / ${totalAvailable}`; } function shuffle(array) { let currentIndex = array.length, randomIndex; // while there remain elements to shuffle. while (currentIndex !== 0) { // pick a remaining element. randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; // swap it with the current element. [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex], ]; } return array; } function createModalElements() { // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; const bgColor = theme ? "bg-white" : "bg-dark"; const textColor = theme ? "text-lp-dark" : "text-lp-light"; addStyle(` .text-lp-dark { color: rgb(31, 31, 31); } .text-lp-light { color: rgb(245, 245, 245); } .label-button.text-lp-dark, .label-button.text-lp-light { color: rgb(133, 133, 133); } .label-button.text-lp-dark:hover { color: rgb(31, 31, 31); } .label-button.text-lp-light:hover { color: rgb(245, 245, 245); } .bg-dark button, .form-control, .form-control:focus, .form-select { color: inherit; background: inherit; } .modal::-webkit-scrollbar, .no-scroll::-webkit-scrollbar { display: none; /* Chrome */ } .modal, .no-scroll { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } .btn-close-empty { background: none; height: initial; width: initial; } .gallery-image { max-height: calc(100vh - 5rem); } .rate-icon { padding: 0px 6px; border-radius: 3px; color: rgb(245, 245, 245); background: rgb(255, 64, 96); font-weight: bold; font-size: 10px; line-height: 16px; user-select: none; height: 16px; } .page-icon { display: flex; -webkit-box-pack: center; justify-content: center; -webkit-box-align: center; align-items: center; flex: 0 0 auto; box-sizing: border-box; height: 20px; min-width: 20px; color: rgb(245, 245, 245); font-weight: bold; padding: 0px 6px; background: rgba(0, 0, 0, 0.32); border-radius: 10px; font-size: 10px; line-height: 10px; } .page-icon:first-child { display: inline-flex; vertical-align: top; -webkit-box-align: center; align-items: center; height: 10px; } #gallery_link:hover { color: var(--bs-btn-hover-color); } `); const backdropConfig = getValue("backdropConfig", "false") === "true"; const svgPin = ` `; const svgUnpin = ` `; const svgClose = ` `; const defaultPinConfig = backdropConfig ? svgPin : svgUnpin; // label const labelModal = document.createElement("div"); labelModal.className = "modal fade"; labelModal.id = "label_modal"; labelModal.tabIndex = -1; labelModal.innerHTML = ` `; // backdrop pin labelModal.setAttribute( "data-bs-backdrop", backdropConfig ? "static" : "true" ); const labelPinButton = labelModal.querySelector("#label_pin"); labelPinButton.addEventListener("click", () => { const ins = bootstrap_.Modal.getOrCreateInstance(labelModal); const backdrop = ins["_config"]["backdrop"] === "static"; if (backdrop) { ins["_config"]["backdrop"] = true; setValue("backdropConfig", "false"); labelPinButton.innerHTML = svgUnpin; } else { ins["_config"]["backdrop"] = "static"; setValue("backdropConfig", "true"); labelPinButton.innerHTML = svgPin; } }); // search const searchModal = document.createElement("div"); searchModal.className = "modal fade"; searchModal.id = "search_modal"; searchModal.tabIndex = -1; searchModal.innerHTML = ` `; // backdrop pin searchModal.setAttribute( "data-bs-backdrop", backdropConfig ? "static" : "true" ); const searchPinButton = searchModal.querySelector("#search_pin"); searchPinButton.addEventListener("click", () => { const ins = bootstrap_.Modal.getOrCreateInstance(searchModal); const backdrop = ins["_config"]["backdrop"] === "static"; if (backdrop) { ins["_config"]["backdrop"] = true; setValue("backdropConfig", "false"); searchPinButton.innerHTML = svgUnpin; } else { ins["_config"]["backdrop"] = "static"; setValue("backdropConfig", "true"); searchPinButton.innerHTML = svgPin; } }); const generatorModal = document.createElement("div"); generatorModal.className = "modal fade"; generatorModal.id = "generator_modal"; generatorModal.tabIndex = -1; generatorModal.innerHTML = ` `; generatorModal.setAttribute( "data-bs-backdrop", backdropConfig ? "static" : "true" ); const generatorPin = generatorModal.querySelector("#generator_pin"); generatorPin.addEventListener("click", () => { const ins = bootstrap_.Modal.getOrCreateInstance(generatorModal); const backdrop = ins["_config"]["backdrop"] === "static"; if (backdrop) { ins["_config"]["backdrop"] = true; setValue("backdropConfig", "false"); generatorPin.innerHTML = svgUnpin; } else { ins["_config"]["backdrop"] = "static"; setValue("backdropConfig", "true"); generatorPin.innerHTML = svgPin; } }); const generatorButtons = generatorModal .querySelector("#generator_form_buttons") .querySelectorAll("button"); generatorButtons[0].addEventListener("click", () => { generatedResults = null; generatorBookmarks.forEach((w) => (w.used = true)); generatorDisplayLimit = 12; document.querySelector("#generator_form_buttons").classList.add("d-none"); document.querySelector("#generator_display").classList.add("d-none"); document.querySelector("#generator_spinner").classList.add("d-none"); document.querySelector("#generator_more").classList.add("d-none"); const resultsDiv = document.querySelector("#generator_results"); while (resultsDiv.firstChild) { resultsDiv.removeChild(resultsDiv.firstChild); } }); generatorButtons[1].addEventListener("click", async () => { window.runFlag = true; const tag = document.querySelector("#generator_select_tag").value; const restriction = document.querySelector( "#generator_form_restriction" ).value; const availableBookmarks = generatorBookmarks.filter( (w) => restriction === "all" || (restriction === "sfw" && !w["xRestrict"]) || (restriction === "nsfw" && w["xRestrict"]) ); const batchSize = Math.max( 0, parseInt(document.querySelector("#generator_form_num").value) || 100 ); const batchNum = Math.ceil(availableBookmarks.length / batchSize); const prompt = document.querySelector("#generator_save_tag_prompt"); prompt.classList.remove("d-none"); for (let index of [...Array(batchNum).keys()]) { if (!window.runFlag) break; const addTag = `S_${index}_${tag}`.slice(0, 30); prompt.innerText = `正在保存至 ${addTag} / Saving to ${addTag}`; const ids = availableBookmarks .slice(index * batchSize, (index + 1) * batchSize) .map((w) => w["bookmarkData"]["id"]); // console.log(addTag, ids); await updateBookmarkTags(ids, [addTag]); } window.runFlag = false; prompt.classList.add("d-none"); }); generatorModal .querySelector("#generator_footer_button") .addEventListener("click", () => generatorButtons[2].click()); generatorModal .querySelector("#generator_footer_stop") .addEventListener("click", () => { window.runFlag = false; document .querySelector("#generator_save_tag_prompt") .classList.add("d-none"); }); generatorModal .querySelector("#generator_more") .addEventListener("click", (evt) => { const resultsDiv = document.querySelector("#generator_results"); const s = resultsDiv.childElementCount; const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; generatorDisplayLimit += 108; if (generatorDisplayLimit >= generatedResults.length) { evt.target.classList.add("d-none"); } generatedResults .filter((_, i) => i >= s && i < generatorDisplayLimit) .forEach((w) => displayWork(w, resultsDiv, textColor)); }); const featureModal = document.createElement("div"); featureModal.className = "modal fade"; featureModal.id = "feature_modal"; featureModal.tabIndex = -1; featureModal.innerHTML = ` `; const featurePrompt = featureModal.querySelector("#feature_prompt"); const featurePublicationType = featureModal.querySelector( "#feature_form_publication" ); const featureTag = featureModal.querySelector("#feature_select_tag"); const featureButtons = featureModal .querySelector("#feature_buttons") .querySelectorAll("button"); featureButtons[0].addEventListener("click", async () => { const publicationType = featurePublicationType.value; const tag = featureTag.value; await deleteTag(tag, publicationType); }); async function featureFetchWorks() { const tag = featureTag.value; const publicationType = featurePublicationType.value; window.runFlag = true; featurePrompt.innerText = "正在获取收藏夹信息 / Fetching bookmark information"; featurePrompt.classList.remove("d-none"); const totalWorks = await fetchAllBookmarksByTag(tag, publicationType); featurePrompt.classList.add("d-none"); console.log(totalWorks); return totalWorks; } featureButtons[1].addEventListener("click", () => featureFetchWorks().then(clearBookmarkTags) ); featureButtons[2].addEventListener("click", () => featureFetchWorks().then(async (works) => { if (!works?.length) { return alert( `没有获取到收藏夹内容,操作中断,请检查选项下是否有作品\nFetching bookmark information failed. Abort operation. Please check the existence of works with the configuration` ); } const tag = featureTag.value; const publicationType = featurePublicationType.value; const restrict = publicationType === "show" ? "private" : "public"; if ( !window.confirm(`标签【${tag || "所有作品"}】下所有【${ publicationType === "show" ? "公开" : "非公开" }】作品(共${works.length}项)将会被移动至【${ publicationType === "show" ? "非公开" : "公开" }】类型,是否确认操作? All works of tag ${tag || "All Works"} and type ${ publicationType === "show" ? "PUBLIC" : "PRIVATE" } (${ works.length } in total) will be set as ${restrict.toUpperCase()}. Is this Okay?`) ) return; const instance = bootstrap_.Modal.getOrCreateInstance(progressModal); instance.show(); await updateBookmarkRestrict( works.map((w) => w["bookmarkData"]["id"]), restrict, progressBar ); setTimeout(() => { instance.hide(); if (window.runFlag && !DEBUG) window.location.reload(); }, 1000); }) ); const progressModal = document.createElement("div"); progressModal.className = "modal fade"; progressModal.id = "progress_modal"; progressModal.setAttribute("data-bs-backdrop", "static"); progressModal.tabIndex = -1; progressModal.innerHTML = ` `; const progressBar = progressModal.querySelector( "#progress_modal_progress_bar" ); const body = document.querySelector("body"); body.appendChild(labelModal); body.appendChild(searchModal); body.appendChild(generatorModal); body.appendChild(featureModal); body.appendChild(progressModal); } async function fetchUserTags() { const tagsRaw = await fetch( `/ajax/user/${uid}/illusts/bookmark/tags?lang=${lang}` ); const tagsObj = await tagsRaw.json(); if (tagsObj.error === true) return alert( `获取tags失败 Fail to fetch user tags` + "\n" + decodeURI(tagsObj.message) ); userTagDict = tagsObj.body; const userTagsSet = new Set(); for (let obj of userTagDict.public) { userTagsSet.add(decodeURI(obj.tag)); } for (let obj of userTagDict["private"]) { userTagsSet.add(decodeURI(obj.tag)); } userTagsSet.delete("未分類"); return sortByParody(Array.from(userTagsSet)); } async function fetchTokenPolyfill() { // get token const userRaw = await fetch( "/bookmark_add.php?type=illust&illust_id=83540927" ); if (!userRaw.ok) { console.log(`获取身份信息失败 Fail to fetch user information`); throw new Error(); } const userRes = await userRaw.text(); const tokenPos = userRes.indexOf("pixiv.context.token"); const tokenEnd = userRes.indexOf(";", tokenPos); return userRes.slice(tokenPos, tokenEnd).split('"')[1]; } async function updateWorkInfo() { const el = await waitForDom("section.sc-jgyytr-0.buukZm"); let workInfo = {}; for (let i = 0; i < 100; i++) { workInfo = Object.values(el)[0]["memoizedProps"]["children"][2]["props"]; if (Object.keys(workInfo).length) break; else await delay(200); } if (DEBUG) console.log(workInfo); return workInfo; } async function initializeVariables() { try { pageInfo = Object.values(document.querySelector(".sc-x1dm5r-0"))[0][ "return" ]["return"]["memoizedProps"]; if (DEBUG) console.log(pageInfo); uid = pageInfo["client"]["userId"]; token = pageInfo["client"]["token"]; lang = pageInfo["client"]["lang"]; } catch (err) { console.log(err); const dataLayer = unsafeWindow_["dataLayer"][0]; uid = dataLayer["user_id"]; lang = dataLayer["lang"]; token = await fetchTokenPolyfill(); } userTags = await fetchUserTags(); // switch between default and dark theme const themeDiv = document.querySelector(".charcoal-token"); theme = themeDiv.getAttribute("data-theme") === "default"; new MutationObserver(() => { theme = themeDiv.getAttribute("data-theme") === "default"; const prevBgColor = theme ? "bg-dark" : "bg-white"; const bgColor = theme ? "bg-white" : "bg-dark"; const prevTextColor = theme ? "text-lp-light" : "text-lp-dark"; const textColor = theme ? "text-lp-dark" : "text-lp-light"; [...document.querySelectorAll(".bg-dark, .bg-white")].forEach((el) => { el.classList.replace(prevBgColor, bgColor); }); [...document.querySelectorAll(".text-lp-dark, .text-lp-light")].forEach( (el) => { el.classList.replace(prevTextColor, textColor); } ); const prevClearTag = theme ? "dydUg" : "jbzOgz"; const clearTag = theme ? "jbzOgz" : "dydUg"; const clearTagsButton = document.querySelector("#clear_tags_button"); if (clearTagsButton) clearTagsButton.children[0].classList.replace(prevClearTag, clearTag); }).observe(themeDiv, { attributes: true }); synonymDict = getValue("synonymDict", {}); if (Object.keys(synonymDict).length) setValue("synonymDict", synonymDict); } const maxRetries = 100; async function waitForDom(selector) { let dom; for (let i = 0; i < maxRetries; i++) { dom = document.querySelector(selector); if (dom) return dom; await delay(500); } throw new ReferenceError( `[Label Bookmarks] Dom element ${selector} not loaded in given time` ); } async function injectElements() { const textColor = theme ? "text-lp-dark" : "text-lp-light"; const pageBody = document.querySelector(".sc-12rgki1-0.jMEnyM"); const root = document.querySelector("nav"); root.classList.add("d-flex"); const buttonContainer = document.createElement("span"); buttonContainer.className = "flex-grow-1 justify-content-end d-flex"; buttonContainer.id = "label_bookmarks_buttons"; const gClass = generator ? "" : "d-none"; const fClass = feature ? "" : "d-none"; buttonContainer.innerHTML = `