您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ユーザーページはわりと雑な再現
// ==UserScript== // @name ニコニコ静画、退会したユーザーの情報を表示 // @namespace http://tampermonkey.net/ // @version 1.14518 // @description ユーザーページはわりと雑な再現 // @author cbxm // @match https://seiga.nicovideo.jp/seiga/* // @match https://seiga.nicovideo.jp/user/illust/* // @connect seiga.nicovideo.jp // @connect sp.seiga.nicovideo.jp // @run-at document-start // @grant GM.setValue // @grant GM.getValue // @grant GM.xmlHttpRequest // ==/UserScript== (async () => { ; ; ; const IsData = (d) => typeof (d.illustCount) == "number" && d.illusts != null && d.user != null; class Util { //xmlString=未探査部分 static XmlToObj(xmlString) { const F = (xmlString, obj) => { //タグを抜き出す let tagMatchs = null; while (true) { tagMatchs = xmlString.match(/<([^>]+)>/); //タグがないということはそれが値になる if (tagMatchs == null) { return xmlString; } if (tagMatchs[1][tagMatchs[1].length - 1] == "/") { xmlString = xmlString.replace(/<[^>]+>([^]*)/, "$1"); } else { break; } } const tag = tagMatchs[1]; //タグの内側とその先を抜き出す const matchChildlen = []; while (true) { const matchs = xmlString.match(new RegExp(`^[^<]*<${tag}>([^]+?)<\/${tag}>([^]*)`)); if (matchs == null) { break; } matchChildlen.push(matchs[1]); xmlString = matchs[2]; } //タグあったのにマッチしなかったおかしい if (matchChildlen.length == 0) { return obj; } //そのタグが一つしかないとき、オブジェクトになる if (matchChildlen.length == 1) { //子を探す obj[tag] = F(matchChildlen[0], {}); } //そのタグが複数あるとき、配列になる if (matchChildlen.length > 1) { obj = []; for (let i = 0; i < matchChildlen.length; i++) { //子を探す obj[i] = F(matchChildlen[i], {}); } } //兄弟を探す F(xmlString, obj); return obj; }; //初期化で<xml>を取り除く xmlString = xmlString.replace(/\s*<[^>]+>([^]+)/, "$1"); return F(xmlString, {}); } static HtmlToDocument(str) { const parser = new DOMParser(); return parser.parseFromString(str, "text/html"); } static HtmlToChildNodes(str) { return this.HtmlToDocument(str).body.childNodes; } static Wait(ms) { return new Promise(r => setTimeout(r, ms)); } } ; class Fetcher { static GMFetchText(url) { return new Promise(r => { GM.xmlHttpRequest({ url: url, method: "GET", onload: (response) => { r(response.responseText); } }); }); } static async FetchIllustDataMax200(userId) { const uel = "https://seiga.nicovideo.jp/api/user/data?id=" + userId; const obj = Util.XmlToObj(await this.GMFetchText(uel)); const list = Array.isArray(obj.response.image_list) ? obj.response.image_list : [obj.response.image_list.image]; //console.log(list); const illusts = []; for (let i = 0; i < list.length; i++) { illusts[i] = { id: list[i].id, title: list[i].title, view_count: list[i].view_count, comment_count: list[i].comment_count, clip_count: list[i].clip_count, isSyunga: list[i].illust_type != "0" }; if (illusts[i].title == null) { illusts[i].title = "(無題)"; } } const illustCount = Number(obj.response.image_count); return { illusts: illusts, count: illustCount }; } static async FetchSpUserIllustDocument(userId, page, sort = "image_created") { const uel = `https:\/\/sp.seiga.nicovideo.jp/user/illust/${userId}?page=${page}&sort=${sort}`; return Util.HtmlToDocument(await this.GMFetchText(uel)); } static GetUserIllustIds(spDocument) { var _a, _b; const list = spDocument.getElementsByClassName("list_item"); const ids = []; for (let i = 0; i < list.length; i++) { const id = (_b = (_a = list.item(i)) === null || _a === void 0 ? void 0 : _a.firstElementChild) === null || _b === void 0 ? void 0 : _b.getAttribute("data-image-id"); if (id != null && id != undefined) { ids.push(id); } } return ids; } static GetUserIllustCount(spDocument) { var _a; const text = (_a = spDocument.getElementsByClassName("num")[0].textContent) !== null && _a !== void 0 ? _a : "0"; const match = text.match(/\d+/); if (match == null) { return 0; } return parseInt(match[0]); } static async FetchIllustData(ids) { if (ids.length == 0) { return []; } const url = `http:\/\/seiga.nicovideo.jp/api/illust/info?id_list=${ids.join()}`; const res = await this.GMFetchText(url); const obj = Util.XmlToObj(res); const list = Array.isArray(obj.response.image_list) ? obj.response.image_list : [obj.response.image_list.image]; const illusts = []; for (let i = 0; i < list.length; i++) { illusts[i] = { id: list[i].id, title: list[i].title, view_count: list[i].view_count, comment_count: list[i].comment_count, clip_count: list[i].clip_count, isSyunga: list[i].adult_level != "0" }; if (illusts[i].title == null) { illusts[i].title = "(無題)"; } } return illusts; } static async FetchUserName(userId) { const url = "http://seiga.nicovideo.jp/api/user/info?id=" + userId; const json = Util.XmlToObj(await this.GMFetchText(url)); return json.response.user.nickname; } static async FetchUserId(illustId) { const url = "https://seiga.nicovideo.jp/api/illust/info?id=im" + illustId; const resultText = await this.GMFetchText(url); const json = Util.XmlToObj(resultText); return json.response.image.user_id; } } ; class Storage { constructor(storageName) { this.storageName = ""; this.storageName = storageName; } async GetStorageData(defaultValue) { const text = await GM.getValue(this.storageName, null); return text != null ? JSON.parse(decodeURIComponent(text)) : defaultValue; } async SetStorageData(data) { await GM.setValue(this.storageName, encodeURIComponent(JSON.stringify(data))); } } ; class Observer { static Wait(predicate, parent = document, option = null) { return new Promise(r => { if (option == null) { option = { childList: true, subtree: true }; } const mutationObserver = new MutationObserver((mrs) => { if (predicate(mrs)) { mutationObserver.disconnect(); r(mrs); return; } }); mutationObserver.observe(parent, option); }); } ; static WaitAddedNode(predicate, parent, option = null) { return new Promise(r => { if (option == null) { option = { childList: true, subtree: true }; } const mutationObserver = new MutationObserver((mrs) => { //console.log(document.head.innerHTML); //console.log(document.body.innerHTML); for (let node of mrs) { node.addedNodes.forEach(added => { //console.log(added); if (predicate(added)) { mutationObserver.disconnect(); r(added); return; } }); } }); mutationObserver.observe(parent, option); }); } ; static async DefinitelyGetElementById(id, parent = document, option = null) { const e = document.getElementById(id); if (e != null) { return e; } return this.WaitAddedNode(e => e.id != null && e.id == id, parent, option); } //getElementsByClassNameをつかうけど単体なので注意 static async DefinitelyGetElementByClassName(className, parent = document, option = null) { const e = document.getElementsByClassName(className)[0]; if (e != null) { return e; } return this.WaitAddedNode(e => e.classList != null && e.classList.contains(className), parent, option); } //getElementsByTagNameをつかうけど単体なので注意 static async DefinitelyGetElementByTagName(tagName, parent = document, option = null) { tagName = tagName.toUpperCase(); const e = document.getElementsByTagName(tagName)[0]; if (e != null) { return e; } return this.WaitAddedNode(e => e.tagName != null && e.tagName == tagName, parent, option); } } ; class State { constructor() { this.storage = new Storage(State.STORAGE_NAME); } async GetDataFromUserId(userId) { try { const promises = [ Fetcher.FetchUserName(userId), Fetcher.FetchIllustDataMax200(userId), Fetcher.FetchSpUserIllustDocument(userId, "1") ]; const ress = await Promise.all(promises); const illustCountNoSyunga = Fetcher.GetUserIllustCount(ress[2]); return { user: { id: userId, name: ress[0] }, illusts: ress[1].illusts, illustCount: ress[1].count, illustCountNoSyunga: illustCountNoSyunga }; } catch (e) { return undefined; } } CacheClear() { return this.storage.SetStorageData([]); } } State.DEFAULT_ICON_URL = "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg"; State.CACHE_MAX = 10; State.STORAGE_NAME = "ListCache"; ; class UserState extends State { constructor() { super(); this.userId = ""; const userIdMatchs = location.href.match(/https:\/\/seiga.nicovideo.jp\/user\/illust\/(\d+)/); if (userIdMatchs != null) { this.userId = userIdMatchs[1]; } else { throw new Error("ユーザーIDが見つかんないや"); } } async IsDisappearedUser() { //console.log(document.head.innerHTML); const title = await Observer.DefinitelyGetElementByTagName("TITLE"); return title.textContent == "ニコニコ静画 (イラスト)"; } HideBody() { Observer.DefinitelyGetElementByTagName("BODY") .then(body => { body.style.visibility = "hidden"; }); } ShowBody() { Observer.DefinitelyGetElementByTagName("BODY") .then(body => { body.style.visibility = "visible"; }); } async Main() { this.HideBody(); this.cacheList = (await this.storage.GetStorageData([])).filter(IsData); let d = this.FindData(this.cacheList); if (d == null) { d = await this.GetData(); if (d == undefined) { const errorElement = await Observer.DefinitelyGetElementByClassName("error__messages"); errorElement.textContent = "取得を試みましたが、古すぎて削除されている可能性があります"; this.ShowBody(); return; } this.cacheList.push(d); while (this.cacheList.length > State.CACHE_MAX) { this.cacheList.shift(); } await this.storage.SetStorageData(this.cacheList); } this.data = d; //console.log(d); await this.Draw(); this.ContinueData(); this.ShowBody(); } async ContinueData() { await Util.Wait(3000); while (true) { //console.log(this.data.illusts.length); //console.log(this.data.illustCount); if (this.data.illusts.length >= this.data.illustCount) { return; } const illustLengthNoSyunga = this.data.illusts.reduce((a, c) => { if (c.isSyunga) return a; else return a + 1; }, 0); //console.log(illustLengthNoSyunga); //console.log(this.data.illustCountNoSyunga); if (illustLengthNoSyunga >= this.data.illustCountNoSyunga) { return; } await Util.Wait(3000); const doc = await Fetcher.FetchSpUserIllustDocument(this.userId, (Math.floor((illustLengthNoSyunga / 30)) + 1).toString()); const ids = Fetcher.GetUserIllustIds(doc); const illusts = await Fetcher.FetchIllustData(ids); const befLen = this.data.illusts.length; for (let d of illusts) { if (!this.data.illusts.some(i => i.id == d.id)) { this.data.illusts.push(d); } } const uls = document.getElementsByClassName("list_item no_trim"); for (let i = befLen; i < uls.length && i < this.data.illusts.length; i++) { const borderText = this.data.illusts[i].isSyunga ? 'style="border: solid 1px black;"' : ""; uls[i].innerHTML = `<a href="/seiga/im${this.data.illusts[i].id}" ${borderText}> <span class="thum"><img src="https:\/\/lohas.nicoseiga.jp/thumb/${this.data.illusts[i].id}q" alt="${this.data.illusts[i].title}"></span> <ul class="illust_info"> <li class="title">${this.data.illusts[i].title}</li> </ul> <ul class="illust_count"> <li class="view"><span class="icon_view"></span>${this.data.illusts[i].view_count}</li> <li class="comment"><span class="icon_comment"></span>${this.data.illusts[i].comment_count}</li> <li class="clip"><span class="icon_clip"></span>${this.data.illusts[i].clip_count}</li> </ul> </a>`; } document.getElementsByClassName("page_count")[0].innerHTML = `<strong>${this.data.illusts.length} / ${this.data.illustCount}</strong>件表示(200件以降の春画は無理)`; await this.storage.SetStorageData(this.cacheList); } } async GetData() { const d = await this.GetDataFromUserId(this.userId); return d; } FindData(cacheList) { return cacheList.find(d => d.user.id == this.userId); } async ChangeTitle(user) { const title = await Observer.DefinitelyGetElementByTagName("TITLE"); title.textContent = `${user.name} さんのイラスト一覧 - ニコニコ静画 (イラスト)`; } async RemoveErrorWrapper() { const errorWrapper = await Observer.DefinitelyGetElementByClassName("error-wrapper"); if (errorWrapper.parentElement != null) { errorWrapper.parentElement.removeChild(errorWrapper); } } async DrawCss() { //css追加 const css_all_l = `<link rel="stylesheet" type="text/css" href="/css/illust/all_l.css?aarx9p">`; const head = await Observer.DefinitelyGetElementByTagName("HEAD"); head.insertAdjacentHTML("beforeend", css_all_l); } //画像下の投稿したその他のイラスト async DrawContent(data) { const wrapperParent = await Observer.DefinitelyGetElementById("wrapper"); let wrapperContentHtml = ` <!-- #list_head_bar --> <section class="userlist_head_bar"> <div class="inner cfix"> <div class="inner_content"> <div class="user_info"> <div class="user_thum"><a href="https://www.nicovideo.jp/user/${data.user.id}"><img alt="${data.user.name}" src="${State.DEFAULT_ICON_URL}"></a></div> <h1><a href="https://www.nicovideo.jp/user/${data.user.id}"><span class="nickname">${data.user.name}</span><span class="suffix">さん</span></a></h1> <ul class="list_header_nav" id="ko_watchlist_header" data-id="${data.user.id}" data-is_active="false" data-count="${data.illusts.length}"> <li class="favorite message_target"> <span></span><span>退会済み</span> </li> </ul> </div> </div> </div> <div class="user_nav" data-target_id="${data.user.id}"> <ul> <li class="illust"><a href="/user/illust/${data.user.id}" class="selected"><span class="span icon_user_illust"></span>投稿イラスト</a></li> </ul> </div> </section> <!-- #content --> <div id="content" class="list" data-view_mode="user_illust"> <!-- #main --> <div id="main"> <!-- .controll --> <div class="controll"> <div class="sort"> 並び:<span>投稿の新しい順</span> </div> <ul class="pager"> <li class="prev disabled"><</li> <li class="current_index">1</li> <li class="next disabled">></li> </ul> <p class="page_count"><strong>${data.illusts.length} / ${data.illustCount}</strong>件表示(200件以降の春画は無理)</p> </div> <!-- //.controll --> <!-- .illust_list --> <div class="illust_list"> <ul class="item_list autopagerize_page_element"> `; //並び替え非対応 //<div class="sort_form"> // <div class="dummy"></div> //</div> // <select class="reload_onchange" basepath="/user/illust/${data.user.id}" queries=""><option value="image_created" selected="selected">投稿の新しい順</option><option value="image_created_a">投稿の古い順</option><option value="comment_count">コメントの多い順</option><option value="comment_count_a">コメントの少ない順</option><option value="image_view">閲覧数の多い順</option><option value="image_view_a">閲覧数の少ない順</option></select> for (let i = 0; i < data.illustCount; i++) { if (i < data.illusts.length) { const borderText = data.illusts[i].isSyunga ? 'style="border: solid 1px black;"' : ""; wrapperContentHtml += `<li class="list_item no_trim"><a href="/seiga/im${data.illusts[i].id}" ${borderText}> <span class="thum"><img src="https:\/\/lohas.nicoseiga.jp/thumb/${data.illusts[i].id}q" alt="${data.illusts[i].title}"></span> <ul class="illust_info"> <li class="title">${data.illusts[i].title}</li> </ul> <ul class="illust_count"> <li class="view"><span class="icon_view"></span>${data.illusts[i].view_count}</li> <li class="comment"><span class="icon_comment"></span>${data.illusts[i].comment_count}</li> <li class="clip"><span class="icon_clip"></span>${data.illusts[i].clip_count}</li> </ul> </a></li> `; } else { wrapperContentHtml += `<li class="list_item no_trim"> </li> `; } } wrapperContentHtml += `</ul> </div> <!-- //.illust_list --> </div> <!-- //#main --> <!-- //#pagetop --> <div id="pagetop" data-target="#content.list" style="display: none; right: 50%;"> <img src="/img/common/new/module/btn_pagetop.png" alt="ページ上部へ"> </div> </div>`; Util.HtmlToChildNodes(wrapperContentHtml).forEach(node => { wrapperParent.appendChild(node); }); } //パンくずリストのエラー表示をユーザー名に変更 async ChangePankuzu(user) { await Observer.Wait(() => document.querySelectorAll(".active span[itemprop=title]").length >= 2, document); document.querySelectorAll(".active span[itemprop=title]").forEach(t => { t.innerHTML = `${user.name}<span class="pankuzu_suffix"> さんのイラスト</span>`; }); } Draw() { const promises = [this.ChangeTitle(this.data.user), this.DrawCss(), this.DrawContent(this.data), this.RemoveErrorWrapper(), this.ChangePankuzu(this.data.user)]; return Promise.allSettled(promises); } } ; class IllustState extends State { constructor() { super(); this.currentIllustId = ""; const match = location.href.match(/https:\/\/seiga.nicovideo.jp\/seiga\/im(\d+)/); if (match != null) { this.currentIllustId = match[1]; } else { throw new Error("イラストIDが見つかんないや"); } } async IsDisappearedUser() { var _a; const test = /.+? \/ .+? さんのイラスト - ニコニコ(春画|静画 \(イラスト\))$/; const title = await Observer.DefinitelyGetElementByTagName("TITLE"); return !test.test((_a = title.textContent) !== null && _a !== void 0 ? _a : ""); } async Main() { const cacheList = (await this.storage.GetStorageData([])).filter(IsData); let d = this.FindData(cacheList); if (d == null) { d = await this.GetData(); if (d == undefined) { return; } cacheList.push(d); while (cacheList.length > State.CACHE_MAX) { cacheList.shift(); } await this.storage.SetStorageData(cacheList); } this.data = d; this.Draw(); } async GetData() { const userId = await Fetcher.FetchUserId(this.currentIllustId); return await this.GetDataFromUserId(userId); } FindData(cacheList) { return cacheList.find(d => d.illusts.some(i => i.id == this.currentIllustId)); } async ChangeTitle(user) { const title = await Observer.DefinitelyGetElementByTagName("TITLE"); if (title.textContent != null) { title.textContent = title.textContent.replace(/- ニコニコ静画 \(イラスト\)$/, `\/ ${user.name} さんのイラスト - ニコニコ静画 \(イラスト\)`); } } async DrawUser(user) { //右上のユーザー表示 const userLinkParent = await Observer.DefinitelyGetElementById("ko_watchlist_header"); let userLinkHtml = `<ul> <li class="user_link"> <a href="/user/illust/${user.id}"> <ul> <li class="thum"><img src="${State.DEFAULT_ICON_URL}" alt=""></li> <li class="user_name"><span class="caption">投稿者</span><strong>${user.name}</strong>さん</li> </ul> </a> </li> <li class="user_favorite message_target" style="pointer-events:none;"> <span> <span>退会済み</span> </span> </li> </ul>`; while (userLinkParent.firstChild) { userLinkParent.removeChild(userLinkParent.firstChild); } Util.HtmlToChildNodes(userLinkHtml).forEach(node => { userLinkParent.appendChild(node); }); } //画像下の投稿したその他のイラスト async DrawRelated(data) { const relatedParent = await Observer.DefinitelyGetElementByClassName("related_info_main"); let relatedSting = `<div class="related_user related_box"> <div class="user" id="ko_watchlist_info" data-id="${data.user.id}" data-status="0" data-count="${data.illusts.length}"> <ul class="cfix"> <li class="thum"><a href="/user/illust/${data.user.id}"><img src="${State.DEFAULT_ICON_URL}" alt=""></a></li> <li class="user_name"> <a href="/user/illust/${data.user.id}"><strong>${data.user.name}</strong>さん</a> </li> <li> <span>退会済み</span> </li> </ul> </div> <div class="other_illust user_illust"> <h2>${data.user.name}さんが投稿した他のイラスト</h2> <div class="illust_list"> <ul class="item_list"> `; //他のイラストを並べる let index = 0, count = 0; while (index < data.illusts.length && count < 5) { if (data.illusts[index].id == this.currentIllustId) { index++; continue; } relatedSting += `<li class="list_item_cutout middle"><a href="/seiga/im${data.illusts[index].id}" title="${data.illusts[index].title}"> <span class="thum"><img src="https:\/\/lohas.nicoseiga.jp//thumb/${data.illusts[index].id}c" alt=""></span> <ul class="illust_info"> <li class="title">${data.illusts[index].title}</li> <li class="user">${data.user.name}</li> </ul> </a></li>`; count++; index++; } relatedSting += ` <li class="list_more_link"><a href="/user/illust/${data.user.id}">もっと見る</a></li> </ul> </div> </div> </div>`; const currentFirst = relatedParent.firstChild; Util.HtmlToChildNodes(relatedSting).forEach(node => { relatedParent.insertBefore(node, currentFirst); }); } async ChangePankuzu(user) { //パンくずリストのエラー表示をユーザー名に変更 await Observer.Wait(() => document.querySelectorAll(".active span[itemprop=title]").length >= 2, document); document.querySelectorAll(".active span[itemprop=title]").forEach(t => { var _a; (_a = t.parentElement) === null || _a === void 0 ? void 0 : _a.insertAdjacentHTML("beforebegin", `<li itemscope="" itemtype="http:\/\/data-vocabulary.org/Breadcrumb"><a href="/user/illust/${user.id}" itemprop="url"><span itemprop="title">${user.name}<span class="pankuzu_suffix"> さんのイラスト</span></span></a></li> `); }); } Draw() { const promises = [this.ChangeTitle(this.data.user), this.DrawUser(this.data.user), this.DrawRelated(this.data), this.ChangePankuzu(this.data.user)]; return Promise.allSettled(promises); } } ; const VERSION_STORAGE_NAME = "Version"; //イラストページかどうか const seigaTest = /https:\/\/seiga.nicovideo.jp\/seiga\//; const isIllustPage = seigaTest.test(location.href); const state = isIllustPage ? new IllustState() : new UserState(); //消えてなかったら終わり!閉廷!解散解散! if (!(await state.IsDisappearedUser())) { return; } //消えてますね・・・悲しいなぁ const versionStorage = new Storage(VERSION_STORAGE_NAME); const storageVer = await versionStorage.GetStorageData(null); const currentVer = "1.14518"; if (storageVer == null || storageVer != currentVer) { console.log("current", currentVer); console.log("strage", storageVer); console.log("更新"); await state.CacheClear(); await versionStorage.SetStorageData(currentVer); } //console.log("悲しいなぁ"); await state.Main(); })();