[ { "bookSourceComment": "Linpx 书源(更新时间:2024-06-17)\n\n可用功能:✅搜索✅发现✅添加网址✅订阅源\n搜索小说:✅小说名称❌作者名称✅小说标签\n发现小说:❌推荐作者✅最近更新\n添加网址:✅Linpx链接✅Pixiv小说链接❌Pixiv系列小说链接\n订阅用法:点击订阅源打开小说,点击【添加到书架】按钮添加小说到书架\n\n书源发布:兽人阅读频道 https://t.me/FurryReading\n项目地址:https://github.com/windyhusky/PixivSource\n规则订阅:\nhttps://cdn.jsdelivr.net/gh/windyhusky/PixivSource@main/linpx.json\nhttps://raw.githubusercontent.com/windyhusky/PixivSource/main/linpx.json", "bookSourceGroup": "🔞 Pixiv", "bookSourceName": "Linpx", "bookSourceType": 0, "bookSourceUrl": "https://furrynovel.ink", "bookUrlPattern": "(https?://)?(api.|www.)?(furrynovel.(ink|xyz)|pixiv.net)/.*", "customOrder": 2, "enabled": true, "enabledCookieJar": false, "enabledExplore": true, "exploreUrl": "[\n {\n \"title\": \"推荐作者\",\n \"url\": \"https://api.furrynovel.ink/fav/user/cache\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\":0.3\n }\n },\n {\n \"title\": \"最新小说\",\n \"url\": \"https://api.furrynovel.ink/pixiv/novels/recent/cache?page={{page}}\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\":0.3\n }\n }\n]", "header": "{\"referer\":\"https://furrynovel.ink/\"}", "lastUpdateTime": 1718249291450, "loginCheckJs": "var util = {}\n\nfunction objStringify(obj) {\n return JSON.stringify(obj, (n, v) => {\n if (typeof v == \"function\")\n return v.toString();\n return v;\n });\n}\n\nfunction publicFunc() {\n let u = {}\n\n u.cacheGetAndSet = function (key, supplyFunc) {\n let v = cache.get(key)\n if (v === undefined || v === null) {\n v = JSON.stringify(supplyFunc())\n // 缓存10分钟\n cache.put(key, v, 600)\n }\n return JSON.parse(v)\n }\n u.getAjaxJson = function (url) {\n return util.cacheGetAndSet(url, () => {\n return JSON.parse(java.ajax(url))\n })\n }\n u.getWebviewJson = function (url) {\n return util.cacheGetAndSet(url, () => {\n let html = java.webView(null, url, null)\n return JSON.parse((html.match(new RegExp(\">\\\\[\\\\{.*?}]<\"))[0].replace(\">\", \"\").replace(\"<\", \"\")))\n })\n }\n\n\n u.debugFunc = (func) => {\n if (String(source.getVariable()) === \"debug\") {\n func()\n }\n }\n\n u.urlNovelUrl = function (id){\n return `https://api.furrynovel.ink/pixiv/novel/${id}/cache`\n }\n u.urlSeriesUrl = function (id){\n return `https://api.furrynovel.ink/pixiv/series/${id}/cache`\n }\n u.urlUserUrl = function (id) {\n return `https://api.furrynovel.ink/pixiv/user/${id}/cache`\n }\n u.urlSearchNovel = function (novelname) {\n return `https://api.furrynovel.ink/pixiv/search/novel/${novelname}/cache`\n }\n u.urlSearchUsers = function (username) {\n return `https://api.furrynovel.ink/pixiv/search/user/${username}/cache`\n }\n u.urlNovelsDetailed = function (nidList) {\n return `https://api.furrynovel.ink/pixiv/novels/cache?${nidList.map(v => \"ids[]=\" + v).join(\"&\")}`\n }\n u.urlUserDetailed = function (uidList) {\n return `https://api.furrynovel.ink/pixiv/users/cache?${uidList.map(v => \"ids[]=\" + v).join(\"&\")}`\n }\n u.urlCoverUrl = function (pxImgUrl) {\n return `https://pximg.furrynovel.ink/?url=${pxImgUrl}&w=800`\n }\n u.urlIllustOriginalUrl = function (illustId) {\n // 使用 pixiv.cat 获取插图\n // return `https://pixiv.cat/${illustId}.png` // 已墙不可用\n return `https://pixiv.re/${illustId}.png`\n // return `https://pixiv.nl/${illustId}.png`\n }\n\n util = u\n java.put(\"util\", objStringify(u))\n}\n\npublicFunc()\n\n// 获取请求的user id方便其他ajax请求构造\nlet uid = java.getResponse().headers().get(\"x-userid\")\nif (uid != null) {\n cache.put(\"pixiv:uid\", uid)\n}\njava.getStrResponse(null, null)", "loginUrl": "", "respondTime": 180000, "ruleBookInfo": { "author": "author", "coverUrl": "cover_url", "init": "@js:\nvar util = objParse(String(java.get(\"util\")))\n\nfunction objParse(obj) {\n return JSON.parse(obj, (n, v) => {\n if (typeof v == \"string\" && v.match(\"()\")) {\n return eval(`(${v})`)\n }\n return v;\n })\n}\n\n(function (res) {\n result = {} //兼容搜索 for 循环\n result.novels = []\n // java.log(JSON.stringify(res))\n let isHtml = res.startsWith(\"\")\n if (isHtml) {\n let matchResult = baseUrl.match(new RegExp(\"pn|pixiv/novel|pixiv.net/novel\"))\n if (matchResult == null) {\n return []\n }\n let id = baseUrl.match(new RegExp(\"\\\\d+\"))[0]\n java.log(`匹配小说ID:${id}`)\n // res = util.getAjaxJson(util.urlNovelUrl(id))\n result.novels.push(util.getAjaxJson(util.urlNovelUrl(id)))\n } else {\n // 兼容 api 链接\n let isApiJson = baseUrl.match(new RegExp(\"pn|pixiv/novel|pixiv.net/novel\"))\n if (isApiJson && typeof(JSON.parse(res)) === \"object\") {\n result.novels.push(JSON.parse(res))\n } else {\n result = JSON.parse(res)\n if (result.total === 0) {\n return []\n }\n }\n }\n\n\n // api更新后,需要使用 for 循环\n for (let i in result.novels) {\n res = result.novels[i]\n let prop = {}\n //为了兼顾导入书架直接走详情页逻辑\n //这里不能直接用book.xxx 来复用搜索页处理结果\n prop.author = res.userName\n prop.tags = res.tags\n prop.count = res.length\n prop.desc = res.desc\n prop.cover_url = util.urlCoverUrl(res.coverUrl)\n\n if (res.series === undefined || res.series === null) {\n prop.name = res.title\n prop.tags.unshift('单本')\n // prop.catalog = `https://api.furrynovel.ink/pixiv/novel/${res.id}/cache`\n prop.catalog = util.urlNovelUrl(res.id)\n\n } else {\n prop.name = res.series.title\n prop.tags.unshift('长篇')\n // prop.catalog = `https://api.furrynovel.ink/pixiv/series/${res.series.id}/cache`\n prop.catalog = util.urlSeriesUrl(res.series.id)\n }\n prop.tags = prop.tags.join(\",\")\n return prop\n }\n})(result)\n", "intro": "desc", "kind": "tags", "lastChapter": "latest_chapter", "name": "name", "tocUrl": "catalog", "wordCount": "count" }, "ruleContent": { "content": "@js:\nvar util = objParse(String(java.get(\"util\")))\n\nfunction objParse(obj) {\n return JSON.parse(obj, (n, v) => {\n if (typeof v == \"string\" && v.match(\"()\")) {\n return eval(`(${v})`)\n }\n return v;\n })\n}\n\n(function (res) {\n res = JSON.parse(res)\n let content = res.content\n if (res.series !== null && res.desc !== undefined && res.desc !== \"\") {\n content = res.desc + \"\\n\" + \"——————————\\n\".repeat(2) + content\n }\n\n // 获取 [uploadedimage:] 的图片链接\n //将存在的pixiv图片链接替换为可访问的直连\n if (res.images !== undefined && res.images !== null) {\n Object.keys(res.images).forEach((key) => {\n content = content.replace(`[uploadedimage:${key}]`, ``)\n })\n }\n\n // // 获取 [pixivimage:] 的图片链接\n let rex = /\\[pixivimage:(\\d+)]/gm\n let matched = content.match(RegExp(rex))\n if (matched) {\n for (let i in matched) {\n let illustId = matched[i].match(RegExp(\"\\\\d+\"))\n content = content.replace(`${matched[i]}`, ``)\n }\n }\n\n\n\n // 替换 Pixiv 分页标记符号 [newpage]\n matched = content.match(RegExp(/[  ]*\\[newpage][  ]*/gm))\n if (matched) {\n for (let i in matched){\n java.log(matched[i])\n content = content.replace(`${matched[i]}`, `${\"

\".repeat(3)}`)\n }\n }\n\n // 替换 Pixiv 章节标记符号 [chapter:]\n matched = content.match(RegExp(/\\[chapter:(.*?)]/gm))\n if (matched) {\n for (let i in matched) {\n let matched2 = matched[i].match(/\\[chapter:(.*?)]/m)\n let chapter = matched2[1].trim()\n // content = content.replace(`${matched[i]}`, `${\"

\".repeat(3)}${chapter}

`)\n content = content.replace(`${matched[i]}`, `${chapter}

`)\n }\n }\n\n // 替换 Pixiv 跳转页面标记符号 [[jump:]]\n matched = content.match(/\\[jump:(\\d+)]/gm)\n if (matched) {\n for (let i in matched) {\n let page = matched[i].match(/\\d+/)\n content = content.replace(`${matched[i]}`, `\\n\\n跳转至第${page}节`)\n }\n }\n\n // 替换 Pixiv 链接标记符号 [[jumpuri: > ]]\n matched = content.match(/\\[\\[jumpuri:(.*?)>(.*?)]]/gm)\n if (matched) {\n for (let i in matched) {\n let matched2 = matched[i].match(/\\[\\[jumpuri:(.*?)>(.*?)]]/m)\n let matchedText = matched2[0]\n let urlName = matched2[1].trim()\n let urlLink = matched2[2].trim()\n // 阅读不支持超链接\n //content = content.replace(`${matchedText}`, `${urlName}`)\n if (urlLink === urlName) {\n content = content.replace(`${matchedText}`, `${urlName}`)\n } else {\n content = content.replace(`${matchedText}`, `${urlName}: ${urlLink}`)\n }\n }\n }\n\n // 替换 Pixiv 注音标记符号 [[rb: > ]]\n matched = content.match(/\\[\\[rb:(.*?)>(.*?)]]/gm)\n if (matched) {\n for (let i in matched) {\n let matched2 = matched[i].match(/\\[\\[rb:(.*?)>(.*?)]]/m)\n let matchedText = matched2[0]\n let kanji = matched2[1].trim()\n let kana = matched2[2].trim()\n // kana为中文,则替换回《书名号》\n var reg = new RegExp(\"[\\\\u4E00-\\\\u9FFF]+\",\"g\");\n if (reg.test(kana)) {\n content = content.replace(`${matchedText}`, `${kanji}《${kana}》`)\n } else{\n // 阅读不支持 注音\n // content = content.replace(`${matchedText}`, `${kanji}${kana}`)\n content = content.replace(`${matchedText}`, `${kanji}(${kana})`)\n }\n }\n }\n\n return content\n})(result)", "imageStyle": "DEFAULT", "replaceRegex": "", "sourceRegex": "" }, "ruleExplore": { "author": "userName", "bookList": "@js:\nvar util = objParse(String(java.get(\"util\")))\n\nfunction objParse(obj) {\n return JSON.parse(obj, (n, v) => {\n if (typeof v == \"string\" && v.match(\"()\")) {\n return eval(`(${v})`)\n }\n return v;\n })\n}\n\n// 存储seriesID\nvar seriesSet = new Set();\n// 将多个长篇小说解析为一本书\nfunction combineNovels(novels) {\n return novels.filter(novel => {\n //单本直接解析为一本书\n //需要判断是否为null\n if (novel.seriesId === undefined || novel.seriesId === null) {\n return true\n }\n\n //集合中没有该系列解析为一本书\n if (!seriesSet.has(novel.seriesId)) {\n seriesSet.add(novel.seriesId)\n return true\n }\n\n return false\n })\n}\n\n// 将小说的封面规则与详情地址替换\nfunction handlerNovels(novels) {\n novels.forEach(novel => {\n // novel.detailedUrl = `https://api.furrynovel.ink/pixiv/novel/${novel.id}/cache`\n novel.detailedUrl = util.urlNovelUrl(novel.id)\n if (novel.seriesId !== undefined && novel.seriesId !== null) {\n novel.title = novel.seriesTitle\n novel.length = null\n\n java.log(`正在获取系列小说:${novel.seriesId}`)\n let series = util.getAjaxJson(util.urlSeriesUrl(novel.seriesId))\n // 后端目前没有系列的coverUrl字段\n // novel.coverUrl = `https://api.furrynovel.ink/proxy/pximg?url=${series.imageUrl}`\n // novel.coverUrl = `https://api.furrynovel.ink/proxy/pximg?url=${series.novels[0].coverUrl}`\n novel.coverUrl = util.urlCoverUrl(series.novels[0].coverUrl)\n\n if (series.caption === \"\") {\n let firstNovels = util.getAjaxJson(util.urlNovelsDetailed([series.novels[0].id]))\n if (firstNovels.length > 0) {\n novel.desc = firstNovels[0].desc\n } else {\n novel.desc = \"该小说可能部分章节因为权限或者被删除无法查看\"\n }\n } else {\n novel.desc = series.caption\n }\n\n //如果没有标签 取第一章的tag\n if (series.tags.length === 0) {\n // 系列至少会有一章\n novel.tags = series.novels[0].tags\n } else {\n novel.tags = series.tags\n }\n\n if (novel.tags === undefined) {\n novel.tags = []\n }\n novel.tags.unshift(\"长篇\")\n\n\n } else {\n novel.tags.unshift(\"单本\")\n // novel.coverUrl = `https://api.furrynovel.ink/proxy/pximg?url=${novel.coverUrl}`\n novel.coverUrl = util.urlCoverUrl(novel.coverUrl)\n }\n\n novel.tags = novel.tags.join(\",\")\n })\n return novels\n}\n\n/**\n * @params arr 传入的源数组\n * @params length 需要获取的元素的个数\n */\nfunction randomChoseArrayItem(arr, length) {\n let copyArr = JSON.parse(JSON.stringify(arr))\n let newArr = [];\n for (let i = 0; i < length; i++) {\n let index = Math.floor(Math.random() * copyArr.length);\n let item = copyArr[index];\n newArr.push(item)\n copyArr.splice(index, 1)\n }\n return newArr.reverse()\n}\n\n\nfunction handlerRecommendUsers() {\n const MAX_FETCH_USER_NUMBER = 2;\n\n return () => {\n let novelList = []\n let userIds = JSON.parse(result).map(i => i.id)\n // java.log(`用户id个数:${userIds.length}`)\n if (userIds.length > MAX_FETCH_USER_NUMBER) {\n userIds = randomChoseArrayItem(userIds, MAX_FETCH_USER_NUMBER);\n }\n\n // java.log(`查询的用户Ids:${userIds}`)\n\n let usersInfo = util.getWebviewJson(util.urlUserDetailed(userIds))\n // java.log(`返回的${JSON.stringify(usersInfo)}`)\n let queryNovelIds = []\n // java.log(`${JSON.stringify(usersInfo)}`)\n usersInfo.filter(user => user.novels && user.novels.length > 0)\n .map(user => user.novels)\n // 将list展平[1,2,3]变为1,2,3 添加到novelList中\n .forEach(novels => {\n novels.forEach(novel => {\n queryNovelIds.push(novel)\n })\n })\n // 暂时限制最大获取数量\n if (queryNovelIds.length > 10) {\n queryNovelIds = randomChoseArrayItem(queryNovelIds, 10)\n }\n novelList = util.getWebviewJson(util.urlNovelsDetailed(queryNovelIds))\n return handlerNovels(combineNovels(novelList))\n }\n}\n\nfunction handlerFollowLatest() {\n return () => {\n let resp = JSON.parse(result)\n return handlerNovels(combineNovels(resp))\n }\n}\n\nfunction handlerFactory() {\n if (baseUrl.indexOf(\"/fav/user\") !== -1) {\n return handlerRecommendUsers()\n }\n\n if (baseUrl.indexOf(\"/pixiv/novels/recent\") !== -1) {\n return handlerFollowLatest()\n }\n\n}\n\n(() => {\n return handlerFactory()()\n})()\n", "bookUrl": "detailedUrl", "coverUrl": "coverUrl", "intro": "desc", "kind": "tags", "lastChapter": "", "name": "title", "wordCount": "length" }, "ruleReview": {}, "ruleSearch": { "author": "userName", "bookList": "@js:\nvar util = objParse(String(java.get(\"util\")))\n\nfunction objParse(obj) {\n return JSON.parse(obj, (n, v) => {\n if (typeof v == \"string\" && v.match(\"()\")) {\n return eval(`(${v})`)\n }\n return v;\n })\n}\n\nfunction cacheGetAndSet(key, supplyFunc) {\n let v = cache.get(key)\n if (v === undefined || v === null) {\n v = JSON.stringify(supplyFunc())\n // 缓存10分钟\n cache.put(key, v, 600)\n }\n return JSON.parse(v)\n}\n\nfunction getAjaxJson(url) {\n return cacheGetAndSet(url, () => {\n return JSON.parse(java.ajax(url))\n })\n}\n\nfunction getWebviewJson(url) {\n return cacheGetAndSet(url, () => {\n let html = java.webView(null, url, null)\n // todo:搜索作者有问题\n return JSON.parse((html.match(new RegExp(\">\\\\[\\{.*?}]<\"))[0].replace(\">\", \"\").replace(\"<\", \"\")))\n })\n}\n\n\nfunction getUser(username, exactMatch) {\n // 修复传入object的bug\n username = String(username)\n let resp = getAjaxJson(util.urlSearchUsers(username))\n if (resp.users.length === 0) {\n return []\n }\n if (!exactMatch) {\n return resp.users\n }\n // 只返回用户名完全一样的用户\n return resp.users.filter(user => {\n return user.name === username\n })\n}\n\n// 包含所有小说数据\nfunction getUserDetailedList(uidList) {\n // java.log(`UIDLIST:${JSON.stringify(uidList)}`)\n return getWebviewJson(util.urlUserDetailed(uidList))\n}\n\nfunction getNovels(nidList) {\n let page = Number(java.get(\"page\"))\n // 分页\n let list = nidList.slice((page - 1) * 20, page * 20)\n if (list.length === 0) {\n return []\n }\n\n // java.log(`NIDURL:${util.urlNovelsDetailed(list)}`)\n return getWebviewJson(util.urlNovelsDetailed(list))\n}\n\n// 存储seriesID\nvar first = true;\nvar seriesSet = {\n keywords: \"Linpx:Search\",\n has: (value) => {\n let page = Number(java.get(\"page\"))\n if (page === 1 && first) {\n first = false\n cache.deleteMemory(this.keywords)\n return false\n }\n\n let v = cache.getFromMemory(this.keywords)\n if (v === undefined || v === null) {\n return false\n }\n let set = new Set(JSON.parse(v))\n return set.has(value)\n },\n\n add: (value) => {\n let v = cache.getFromMemory(this.keywords)\n if (v === undefined || v === null) {\n cache.putMemory(this.keywords, JSON.stringify([value]))\n\n } else {\n let arr = JSON.parse(v)\n if (typeof arr === \"string\") {\n arr = Array(arr)\n }\n arr.push(value)\n cache.putMemory(this.keywords, JSON.stringify(arr))\n }\n },\n};\n\n// 将多个长篇小说解析为一本书\nfunction combineNovels(novels) {\n return novels.filter(novel => {\n //单本直接解析为一本书\n //需要判断是否为null\n if (novel.seriesId === undefined || novel.seriesId === null) {\n return true\n }\n\n //集合中没有该系列解析为一本书\n if (!seriesSet.has(novel.seriesId)) {\n seriesSet.add(novel.seriesId)\n return true\n }\n\n return false\n })\n}\n\n// 将小说的封面规则与详情地址替换\nfunction formatNovels(novels) {\n novels.forEach(novel => {\n // novel.detailedUrl = `https://api.furrynovel.ink/pixiv/novel/${novel.id}`\n novel.detailedUrl = util.urlNovelUrl(novel.id)\n if (novel.seriesId !== undefined && novel.seriesId !== null) {\n novel.title = novel.seriesTitle\n novel.length = null\n\n let series = getAjaxJson(util.urlSeriesUrl(novel.seriesId))\n // 后端目前没有系列的coverUrl字段\n // novel.coverUrl = `https://api.furrynovel.ink/proxy/pximg?url=${series.imageUrl}`\n // novel.coverUrl = `https://api.furrynovel.ink/proxy/pximg?url=${series.novels[0].coverUrl}`\n novel.coverUrl = util.urlCoverUrl(series.novels[0].coverUrl)\n if (series.caption === \"\") {\n let firstNovels = getAjaxJson(util.urlNovelsDetailed([series.novels[0].id]))\n if (firstNovels.length > 0) {\n novel.desc = firstNovels[0].desc\n } else {\n novel.desc = \"该小说可能部分章节因为权限或者被删除无法查看\"\n }\n } else {\n novel.desc = series.caption\n }\n\n //如果没有标签 取第一章的tag\n if (series.tags.length === 0) {\n // 系列至少会有一章\n novel.tags = series.novels[0].tags\n } else {\n novel.tags = series.tags\n }\n\n if (novel.tags === undefined) {\n novel.tags = []\n }\n novel.tags.unshift(\"长篇\")\n\n\n } else {\n if (novel.tags === undefined) {\n novel.tags = []\n }\n novel.tags.unshift(\"单本\")\n // novel.coverUrl = `https://api.furrynovel.ink/proxy/pximg?url=${novel.coverUrl}`\n novel.coverUrl = util.urlCoverUrl(novel.coverUrl)\n }\n\n novel.tags = novel.tags.join(\",\")\n })\n return novels\n}\n\nfunction findUserNovels(username) {\n let novelList = []\n // 查询用户\n let userArr = getUser(username, true)\n // 获取用户所有小说\n let uidList = userArr.filter(user => {\n return user.novels.length > 0\n }).map(user => user.id)\n\n if (uidList.length > 0) {\n let list = getUserDetailedList(uidList)\n let nidList = []\n // 从两层数组中提取novelsId\n list.forEach(user => {\n user.novels\n // 按id降序排序-相当于按时间降序排序\n .reverse()\n .forEach(nid => nidList.push(nid))\n })\n getNovels(nidList).forEach(novel => {\n novelList.push(novel)\n })\n }\n return novelList\n}\n\n(function (res) {\n res = JSON.parse(res)\n let novels = []\n findUserNovels(java.get(\"key\")).forEach(v => {\n novels.push(v)\n })\n novels = novels.concat(res.novels)\n // 返回空列表中止流程\n if (novels.length === 0) {\n return []\n }\n return formatNovels(combineNovels(novels))\n}(result))", "bookUrl": "detailedUrl", "coverUrl": "coverUrl", "intro": "desc", "kind": "tags", "name": "title", "wordCount": "length" }, "ruleToc": { "chapterList": "@js:\nvar util = objParse(String(java.get(\"util\")))\n\nfunction objParse(obj) {\n return JSON.parse(obj, (n, v) => {\n if (typeof v == \"string\" && v.match(\"()\")) {\n return eval(`(${v})`)\n }\n return v;\n })\n}\n\n(function (res) {\n res = JSON.parse(res)\n if (res.novels !== undefined) {\n res.novels.forEach(v => {\n // v['url'] = `https://api.furrynovel.ink/pixiv/novel/${v.id}${cache}`\n v['url'] = util.urlNovelUrl(v.id)\n })\n return res.novels\n }\n return [{\n id: res.id,\n title: res.title\n }]\n})(result)\n", "chapterName": "title", "chapterUrl": "url" }, "searchUrl": "@js:\njava.put(\"page\",page);java.put(\"key\",key);\n`https://api.furrynovel.ink/pixiv/search/novel/${encodeURI(key)}/cache?page=${page}`;", "variableComment": "", "weight": 0 } ]