-
-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathlinpx.json
More file actions
146 lines (146 loc) · 102 KB
/
linpx.json
File metadata and controls
146 lines (146 loc) · 102 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
[
{
"bookSourceComment": "🦊 Linpx 书源(单篇)(更新📆:2026/04/04)\n\n书源版本:265\n使用说明:📌阅读 Plus 3.26.0129 版本可用\n可用功能:✅搜索✅发现✅添加网址✅订阅源\n搜索小说:✅单篇❌系列✅作者✅标签\n发现小说:✅推荐✅最新✅更新书源\n添加网址:✅小说✅作者\n订阅用法:点击订阅源打开小说,点击【添加到书架】按钮添加小说到书架\n温馨提示:⚠️Linpx 搜索暂时不支持系列小说名称,添加系列小说后,无法再次通过搜索获取并更新目录\n\n书源发布:Pixiv 书源频道 https://t.me/PixivSource\n兽人阅读频道 https://t.me/FurryReading\n项目地址:https://github.com/DowneyRem/PixivSource\n使用教程:https://pixivsource.pages.dev/Linpx\n\n规则订阅:Import 订阅源\nhttps://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/import.json\nhttps://raw.githubusercontent.com/DowneyRem/PixivSource/main/import.json\n\n⚙️ 书源设置:\n设置:打开小说 - 菜单 - 登录 - 点击下方按钮",
"bookSourceGroup": "🔞 Pixiv,🐲 Furry",
"bookSourceName": "🦊 Linpx",
"bookSourceType": 0,
"bookSourceUrl": "https://furrynovel.ink",
"bookUrlPattern": "(https?://)?(api\\.|www\\.)?(furrynovel\\.(ink|xyz))/(pn|pixiv/(novel|user))/\\d+(/cache)?",
"concurrentRate": "30/5000",
"customButton": true,
"customOrder": 3,
"enabled": true,
"enabledCookieJar": true,
"enabledExplore": true,
"eventListener": true,
"exploreUrl": "@js:\nli = [\n {\"💯 推荐\": \"https://api.furrynovel.ink/fav/user/cache\"},\n {\"🆕 最新\": \"https://api.furrynovel.ink/pixiv/novels/recent/cache?page={{page}}\"},\n {\"🔄 随便\": \"https://furrynovel.ink\"},\n {\"🆙 更新\": \"https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/linpx.json\"},\n {\"📙 书源相关 📙\": \"\"},\n {\"🏠 主页\": \"https://pixivsource.pages.dev\"},\n {\"🔰 指南\": \"https://pixivsource.pages.dev/Linpx\"},\n {\"🐞 反馈\": \"https://github.com/DowneyRem/PixivSource/issues\"},\n {\"💰 打赏\": \"https://pixivsource.pages.dev/Sponsor\"},\n]\n\n// 格式化发现地址\nli.forEach(item => {\n item.title = Object.keys(item)[0]\n item.url = Object.values(item)[0]\n delete item[Object.keys(item)[0]]\n item.style = {}\n item.style.layout_flexGrow = 1\n item.style.layout_flexShrink = 1\n item.style.layout_alignSelf = \"auto\"\n item.style.layout_wrapBefore = \"false\"\n if (item.url === \"\") {\n item.style.layout_flexBasisPercent = 1\n } else {\n item.style.layout_flexBasisPercent = -1\n }\n})\nJSON.stringify(li)",
"header": "",
"jsLib": "var cacheSaveSeconds = 30*24*60*60 // 长期缓存 30 天\nvar cacheTempSeconds = 10*60*1000 // 冷却时间 10 分钟\n\nfunction cacheGetAndSet(key, supplyFunc, requestUpdate) {\n const {java, cache} = this\n let timestamp = 0\n let v = this.getFromCacheObject(key)\n if (Array.isArray(v)) {\n try {\n timestamp = v[0].timestamp\n } catch (e) {\n timestamp = 0\n }\n } else if (v) {\n timestamp = v.timestamp\n }\n\n const isExpired = v && (new Date().getTime() >= timestamp + cacheTempSeconds)\n const isError = v && (v.error === true) && isExpired\n requestUpdate = requestUpdate && isExpired\n\n if (!v || requestUpdate || isError) {\n v = supplyFunc()\n let now = new Date().getTime()\n // getAjaxJson getWebviewJson 时间戳写入对象本身\n if (!Array.isArray(v)) {\n v = Object.assign({timestamp: now}, v)\n }\n // else {\n // // getAjaxAllJson 时间戳写入第一个元素(读取时 v[0].timestamp)// 不重复写入\n // if (v.length > 0) v[0] = Object.assign({timestamp: now}, v[0])\n // }\n this.putInCacheObject(key, v, cacheSaveSeconds)\n }\n return v\n}\n\nfunction putInCache(name, object, saveSeconds) {\n const {java, cache} = this\n if (saveSeconds === undefined) saveSeconds = 0\n if (object) {\n cache.put(name, object, saveSeconds)\n }\n}\nfunction getFromCache(name) {\n const {java, cache} = this\n let object = cache.get(name)\n if (object === undefined) return null // 兼容源阅\n return object\n}\n\nfunction putInCacheObject(objectName, object, saveSeconds) {\n const {java, cache} = this\n if (object === undefined) object = null\n if (saveSeconds === undefined) saveSeconds = 0\n cache.put(objectName, JSON.stringify(object), saveSeconds)\n}\nfunction getFromCacheObject(objectName) {\n const {java, cache} = this\n let object = cache.get(objectName)\n if (object === undefined) return null // 兼容源阅\n return JSON.parse(object)\n}\n\nfunction isHtmlString(str) {\n return str.startsWith(\"<!DOCTYPE html>\")\n}\nfunction isJsonString(str) {\n try {\n let result = JSON.parse(str)\n return typeof result === \"object\" && result !== null\n } catch(e) {\n return false\n }\n}\n\nfunction getAjaxJson(url, requestUpdate) {\n const {java, cache} = this\n return this.cacheGetAndSet(url, () => {\n return JSON.parse(java.ajax(url))\n }, requestUpdate)\n}\nfunction getAjaxAllJson(urls, requestUpdate) {\n const {java, cache} = this\n let batchKey = JSON.stringify(urls)\n return this.cacheGetAndSet(batchKey, () => {\n let results = []\n let now = new Date().getTime()\n let responses = java.ajaxAll(urls)\n for (let i in urls) {\n let data = JSON.parse(responses[i].body())\n data = Object.assign({timestamp: now}, data)\n results.push(data)\n this.putInCacheObject(urls[i], data, cacheSaveSeconds)\n }\n return results\n }, requestUpdate)\n}\nfunction getAjaxParseJson(url, parseFunc, requestUpdate) {\n const {java, cache} = this\n return this.cacheGetAndSet(url, () => {\n let resp = parseFunc(java.ajax(url))\n if (resp instanceof Object) return resp\n else return JSON.parse(resp)\n }, requestUpdate)\n}\n// function getWebviewJson(url, parseFunc, requestUpdate) {\n// const {java, cache} = this\n// return this.cacheGetAndSet(url, () => {\n// let html = java.webView(null, url, null)\n// return JSON.parse(parseFunc(html))\n// }, requestUpdate)\n// }\nfunction getWebviewJson(url) {\n const {java, cache} = this\n return this.cacheGetAndSet(url, () => {\n let html = java.webView(null, url, null)\n return JSON.parse((html.match(new RegExp(\">\\\\[{.*?}]<\"))[0].replace(\">\", \"\").replace(\"<\", \"\")))\n })\n}\n\nfunction getWebViewUA() {\n const {java, cache} = this\n let userAgent = this.getFromCache(\"userAgent\")\n if (userAgent) return String(userAgent)\n\n userAgent = String(java.getWebViewUA())\n if (userAgent.includes(\"Windows NT 10.0; Win64; x64\")) {\n userAgent = \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36\"\n }\n userAgent += \" Reader\"\n // java.log(`userAgent=${userAgent}`)\n this.putInCache(\"userAgent\", userAgent, cacheSaveSeconds/7)\n return String(userAgent)\n}\nfunction startBrowser(url, title) {\n const {java} = this\n if (!title) title = url\n let msg = \"\"\n let headers = {}\n headers[\"User-Agent\"] = this.getWebViewUA()\n headers[\"Referer\"] = \"https://furrynovel.ink/\"\n\n if (url.includes(\"github.com\") || url.includes(\"github.io\")) {\n msg += \"\\n\\n即将打开 Github\\n请确认已开启代理/梯子/VPN等\"\n }\n this.sleepToast(msg, 0.01)\n java.startBrowser(`${url}, ${JSON.stringify({headers: headers})}`, title)\n}\n\nfunction urlNovelUrl(novelId) {\n return `https://furrynovel.ink/pixiv/novel/${novelId}/cache`\n}\nfunction urlNovelDetailed(novelId) {\n return `https://api.furrynovel.ink/pixiv/novel/${novelId}/cache`\n}\nfunction urlNovelsDetailed(nidList) {\n return `https://api.furrynovel.ink/pixiv/novels/cache?${nidList.map(v => \"ids[]=\" + v).join(\"&\")}`\n}\nfunction urlNovelComments(novelId) {\n return `https://api.furrynovel.ink/pixiv/novel/${novelId}/comments`\n}\n\nfunction urlSourceUrl(novelId) {\n return `https://www.pixiv.net/novel/show.php?id=${novelId}`\n}\n\nfunction urlSeriesUrl(seriesId) {\n return `https://www.pixiv.net/novel/series/${seriesId}`\n}\nfunction urlSeriesDetailed(seriesId) {\n return `https://api.furrynovel.ink/pixiv/series/${seriesId}/cache`\n}\n\nfunction urlUserUrl(userId) {\n return `https://furrynovel.ink/pixiv/user/${userId}/cache`\n}\nfunction urlUserDetailed(userId) {\n return `https://api.furrynovel.ink/pixiv/user/${userId}/cache`\n}\nfunction urlUsersDetailed(uidList) {\n return `https://api.furrynovel.ink/pixiv/users/cache?${uidList.map(v => \"ids[]=\" + v).join(\"&\")}`\n}\n\nfunction urlSearchNovel(novelName, page) {\n return `https://api.furrynovel.ink/pixiv/search/novel/${novelName}/cache?page=${page}`\n}\nfunction urlSearchUsers(userName) {\n return `https://api.furrynovel.ink/pixiv/search/user/${userName}/cache`\n}\n\nfunction urlCoverUrl(pxImgUrl) {\n let url = `https://pximg.furrynovel.ink/?url=${pxImgUrl}&w=800`\n let headers = {\"Referer\": \"https://furrynovel.ink/\"}\n return `${url}, ${JSON.stringify({headers: headers})}`\n}\n\nfunction urlIllustUrl(illustId) {\n return `https://www.pixiv.net/artworks/${illustId}`\n}\nfunction urlIllustDetailed(illustId) {\n return `https://www.pixiv.net/ajax/illust/${illustId}?lang=zh`\n}\n// 直连功能参考自 洛娅橙的阅读仓库\n// https://github.com/Luoyacheng/yuedu\n// 其直连功能参考自 PixEz Flutter\n// https://github.com/Notsfsssf/pixez-flutter\nfunction urlIP(url) {\n const {java, cache} = this\n url = url.replace(\"http://\", \"https://\").replace(\"www.pixiv.net\", \"210.140.139.155\")\n let headers = {\n \"User-Agent\": this.getWebViewUA(),\n \"X-Requested-With\": \"XMLHttpRequest\",\n \"Host\": \"www.pixiv.net\",\n \"Referer\": \"https://www.pixiv.net/\"\n }\n return `${url}, ${JSON.stringify({headers: headers})}`\n}\nfunction urlPixivCoverUrl(url) {\n const {java, cache} = this\n if (url && !url.trim()) return \"\"\n let headers = {\"Referer\": \"https://www.pixiv.net/\"}\n\n if (url.trim()) {\n if (url.includes(\"i.pximg.net\")) {\n url = url.replace(\"https://i.pximg.net\", \"https://210.140.139.133\")\n headers.Host = \"i.pximg.net\"\n } else {\n url = url.replace(\"https://s.pximg.net\", \"https://210.140.139.133\")\n headers.Host = \"s.pximg.net\"\n }\n }\n return `${url}, ${JSON.stringify({headers: headers})}`\n}\nfunction urlIllustOriginal(illustId, order) {\n const {java, cache} = this\n if (!order || order <= 1) order = 1\n let illustOriginal = \"\"\n\n let resp = this.getAjaxJson(this.urlIP(urlIllustDetailed(illustId)))\n try {\n illustOriginal = resp.body.urls.original\n } catch (e) {\n try {\n let illustThumb = resp.body.userIllusts[illustId].url\n let date = illustThumb.match(\"\\\\d{4}\\\\/\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{2}\")[0]\n illustOriginal = `https://i.pximg.net/img-master/img/${date}/${illustId}_p0_master1200.jpg`\n } catch (e) {}\n }\n\n if (illustOriginal.split(\",\")[0] === \"\") return \"\"\n return this.urlPixivCoverUrl(illustOriginal.replace(`_p0`, `_p${order - 1}`))\n}\n\nfunction addZero(num) {\n return String(num).padStart(2, '0')\n}\nfunction dateFormat(str) {\n let time = new Date(str);\n let Y = time.getFullYear() + \"年\";\n let M = this.addZero(time.getMonth() + 1) + \"月\";\n let D = this.addZero(time.getDate()) + \"日\";\n return Y + M + D;\n}\nfunction timeFormat(str) {\n let time = new Date(str);\n let YY = time.getFullYear()\n let MM = this.addZero(time.getMonth() + 1)\n let DD = this.addZero(time.getDate())\n let hh = this.addZero(time.getHours())\n let mm = this.addZero(time.getMinutes())\n let ss = this.addZero(time.getSeconds())\n return `${YY}-${MM}-${DD} ${hh}:${mm}:${ss}`\n}\nfunction timeTextFormat(text) {\n return `${text.slice(0, 10)} ${text.slice(11, 19)}`\n}\n\nfunction sleep(seconds) {\n return Packages.java.lang.Thread.sleep(1000*seconds)\n}\nfunction sleepToast(text, seconds) {\n let {java} = this\n java.log(text)\n java.longToast(text)\n if (seconds === undefined) {seconds = 0.01}\n this.sleep(seconds)\n}\n\nfunction setDefaultSettings() {\n const {java, cache} = this\n let settings = {}\n settings.SEARCH_AUTHOR = false // 搜索:默认搜索作者名称\n settings.CONVERT_CHINESE = true // 搜索:搜索时进行繁简转换\n\n settings.MORE_INFORMATION = false // 详情:书籍简介显示更多信息\n settings.SHOW_ORIGINAL_LINK = true // 目录:显示原始链接,但会增加大量请求\n\n settings.REPLACE_TITLE_MARKS = true // 正文:注音内容为汉字时,替换为书名号\n settings.SHOW_CAPTIONS = true // 正文:章首显示描述\n\n settings.DEBUG = false // 全局:调试模式\n\n this.putInCacheObject(\"linpxSettings\", settings)\n return settings\n}\n\nfunction updateSource() {\n const {java, source} = this\n java.longToast(\"🆙 更新书源\\n\\nJsdelivr CDN 更新有延迟\\nGithub 更新需代理\")\n let onlineSource, comment, sourceName, sourceNameCapitalize, index = 0\n if (source.bookSourceUrl.includes(\"pixiv\")) sourceName = \"pixiv\"\n else if (source.bookSourceUrl.includes(\"furrynovel\")) sourceName = \"linpx\"\n sourceNameCapitalize = sourceName[0].toUpperCase() + sourceName.substring(1)\n\n if (source.bookSourceName.includes(\"备用\")) index = 1\n else if (source.bookSourceName.includes(\"漫画\")) index = 2\n if (source.bookSourceUrl.includes(\"furrynovel.com\")) {\n sourceNameCapitalize = \"FurryNovel\"\n index = 1\n }\n\n try {\n let updateUrl = `https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/${sourceName}.json`\n onlineSource = JSON.parse(java.get(updateUrl,{'User-Agent': 'Mozilla/5.0 (Linux; Android 14)','X-Requested-With': 'XMLHttpRequest'}).body())[index]\n } catch (e) {\n try {\n let updateUrl = `https://raw.githubusercontent.com/DowneyRem/PixivSource/main/${sourceName}.json`\n onlineSource = JSON.parse(java.get(updateUrl,{'User-Agent': 'Mozilla/5.0 (Linux; Android 14)','X-Requested-With': 'XMLHttpRequest'}).body())[index]\n } catch (e) {\n onlineSource = {lastUpdateTime: new Date().getTime(), bookSourceComment: source.bookSourceComment}\n }\n }\n comment = onlineSource.bookSourceComment.split(\"\\n\")\n // onlineSource = source\n // comment = source.bookSourceComment.split(\"\\n\")\n\n let htm = `\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>更新 ${source.bookSourceName} 书源</title>\n <style> \n table { text-align: center; margin: 0 auto; } .ann { display: flex; justify-content: center; align-items: center; height: 5vh; } \n button { background-color: rgb(76, 175, 80); color: white; border: none; border-radius: 4px; height: 6vh; width: 30vw; overflow: hidden; } \n button span { cursor: pointer; display: inline-block; position: relative; transition: 0.4s; } \n button span:after { content: '>'; position: absolute; opacity: 0; top: 0; right: 30px; transition: 0.2s; } \n button:active span { padding-right: 20px; } \n button:active span:after { opacity: 1; right: -40px; }\n </style>\n</head>\n\n<body>\n <table border=\"1\" cellspacing=\"0\">\n <th colspan=\"2\"> ${source.bookSourceName} 书源 \n <a href=\"https://pixivsource.pages.dev/${sourceNameCapitalize}\">🔰 使用指南</a>\n || <a href=\"https://pixivsource.pages.dev/Sponsor\">❤️ 赞助开发</a>\n </th>\n <tr>\n <td>☁️ 远程版本:${onlineSource.bookSourceComment.split(\"\\n\")[2].replace(\"书源版本:\", \"\")}</td>\n <td>📆 更新:${timeFormat(onlineSource.lastUpdateTime)}</td>\n </tr>\n <tr>\n <td>📥 本地版本:${source.bookSourceComment.split(\"\\n\")[2].replace(\"书源版本:\", \"\")}</td>\n <td>📆 更新:${timeFormat(source.lastUpdateTime)}</td>\n </tr> \n <tr><td colspan=\"2\" style=\"text-align: left;\">${comment.slice(3, 10).join(\"<br>\")}</td></tr>\n <tr><td colspan=\"2\" style=\"text-align: left;\">${comment.slice(comment.length-2, comment.length).join(\"<br>\")}</td></tr>\n </table>\n \n <table border=\"0\" cellspacing=\"20\">\n <th colspan=\"2\"> 更新 ${source.bookSourceName} 书源 </th>\n <tr><td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/${sourceName}.json\">\n <button><span>更新书源<br>(Jsdelivr CDN)</span></button>\n </a></div></td>\n \n <td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/btsrk.json\">\n <button><span>更新订阅<br>(Jsdelivr CDN)</span></button>\n </a></div></td>\n </tr>\n \n <tr><td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://raw.githubusercontent.com/DowneyRem/PixivSource/main/${sourceName}.json\">\n <button><span>书源链接<br>(GitHub)</span></button>\n </a></div></td>\n \n <td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://raw.githubusercontent.com/DowneyRem/PixivSource/main/btsrk.json\">\n <button><span>订阅链接<br>(GitHub)</span></button>\n </a></div></td>\n </tr>\n \n <tr><td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://codeberg.org/DowneyRem/PixivSource/raw/branch/main/${sourceName}.json\">\n <button><span>备用书源链接<br>(Codeberg)</span></button>\n </a></div></td>\n \n <td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://codeberg.org/DowneyRem/PixivSource/raw/branch/main/btsrk.json\">\n <button><span>备用订阅链接<br>(Codeberg)</span></button>\n </a></div></td>\n </tr>\n </table>\n</body>\n</html>`\n java.startBrowser(`data:text/html;charset=utf-8;base64, ${java.base64Encode(htm)}`, '更新书源')\n return []\n}",
"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\n// 检测 源阅\n// 可用 java.ajax() 不可用 java.webview() java.ajaxAll()\n// 可用 java.getCookie() cache.put() cache.get() 默认值为 undefined\n// 可用 java.startBrowser() 不可用 java.startBrowserAwaitAwait\n// 可用 source.bookSourceName source.getVariable() source.setVariable()等\n// java.getUserAgent() java.getWebViewUA() 目前返回内容相同\nfunction isSourceRead() {\n return java.getUserAgent() === java.getWebViewUA()\n}\n// 正式版 不支持在 JSlib 的函数直接设置默认参数\n// 正式版 不支持 a?.b 的写法\n// 检测 阅读 正式版 与 Beta 版本\nfunction isLegadoOfficial() {\n let isLegadoOfficialStatus\n try {\n eval('({})?.value')\n isLegadoOfficialStatus = false\n } catch (e) {\n isLegadoOfficialStatus = true\n }\n return isLegadoOfficialStatus\n}\n// 检测 阅读 Beta 版本 与 LYC 版本\n// LYC 版本新增函数\n// java.ajaxTestAll()\n// java.openVideoPlayer(url: String, title: String, float: Boolean)\n// cookie.setWebCookie(url,cookie)\n// source.refreshExplore()\n// source.refreshJSLib()\nfunction isLegadoLYC() {\n return typeof java.ajaxTestAll === \"function\"\n}\n\nfunction publicFunc() {\n let u = {}, settings \n // 输出书源信息\n java.log(`${source.bookSourceComment.split(\"\\n\")[0]}`)\n java.log(`📌 ${source.bookSourceComment.split(\"\\n\")[2]}`)\n java.log(`📆 更新时间:${java.timeFormat(source.lastUpdateTime)}`)\n\n if (isSourceRead()) {\n java.log(\"📱 软件平台:🍎 源阅 SourceRead\")\n } else if (isLegadoOfficial()) {\n java.log(\"📱 软件平台:🤖 阅读 正式版\")\n sleepToast(\"\\n⚠️当前软件为:阅读【正式版】\\n【正式版】已年久失修,不推荐继续使用\\n\\n为了更好的使用体验,请用:\\n【阅读 Plus】或【阅读 Beta 新包名】\\n\\n即将为您打开【阅读 Plus】下载界面\")\n sleep(3); startBrowser(\"https://loyc.xyz/c/legado.html#download\", \"下载阅读 Plus\")\n\n } else {\n if (isLegadoLYC()) {\n java.log(\"📱 软件平台:🤖 阅读 Beta【新包名】/ 阅读 Plus\")\n } else {\n java.log(\"📱 软件平台:🤖 阅读 Beta【原包名】\")\n sleepToast(\"\\n⚠️当前软件为:阅读 Beta【原包名】\\n\\n为了更好的使用体验,请用:\\n【阅读 Plus】或【阅读 Beta 新包名】\\n\\n即将为您打开【阅读 Plus】下载界面\")\n sleep(3); startBrowser(\"https://loyc.xyz/c/legado.html#download\", \"下载阅读 Plus\")\n }\n }\n\n // 设置初始化\n // cache.delete(\"linpxSettings\")\n settings = getFromCacheObject(\"linpxSettings\")\n if (settings) {\n java.log(\"⚙️ 使用自定义设置\")\n } else {\n java.log(\"⚙️ 使用默认设置\")\n settings = setDefaultSettings()\n }\n u.settings = settings\n putInCacheObject(\"linpxSettings\", settings) // 设置写入缓存\n\n u.environment = {}\n u.environment.IS_SOURCEREAD = isSourceRead()\n u.environment.IS_LEGADO = !isSourceRead()\n u.environment.IS_LYC_BRUNCH = isLegadoLYC()\n putInCacheObject(\"pixivEnvironment\", u.environment) // 设置写入缓存\n\n u.debugFunc = (func) => {\n if (util.settings.DEBUG) {\n func()\n }\n }\n\n // 将多个长篇小说解析为一本书\n u.combineNovels = function(novels) {\n return novels.filter(novel => {\n // 单本直接解析为一本书,需要判断是否为 null\n if (novel.seriesId === undefined || novel.seriesId === null) {\n return true\n }\n //集合中没有该系列解析为一本书\n if (!seriesSet.has(novel.seriesId)) {\n seriesSet.add(novel.seriesId)\n return true\n }\n return false\n })\n }\n\n // 处理 novels 列表\n u.handNovels = function (novels) {\n novels.forEach(novel => {\n if (!novel.id) novel.id = novel._id\n // novel.title = novel.title\n // novel.userName = novel.userName\n // novel.tags = novel.tags\n if (novel.tags === undefined) {\n novel.tags = []\n }\n // 兼容详情页\n if (novel.content) {\n if (novel.series) {\n novel.seriesId = novel.series.id\n novel.seriesTitle = novel.series.title\n }\n novel.description = novel.desc\n novel.textCount = novel.length = novel.content.length\n }\n\n if (!novel.seriesId) {\n novel.tags.unshift(\"单本\")\n novel.textCount = novel.length\n novel.latestChapter = novel.title\n novel.description = novel.desc\n // novel.coverUrl = novel.coverUrl\n novel.detailedUrl = urlNovelDetailed(novel.id)\n }\n\n // 优化 未缓存系列目录的情况\n let series = getAjaxJson(urlSeriesDetailed(novel.seriesId)) // 兼容详情\n if (novel.seriesId) {\n novel.latestChapter = novel.title\n novel.title = novel.seriesTitle\n novel.tags.unshift(\"长篇\")\n // novel.createDate = novel.createDate\n novel.description = novel.desc\n novel.detailedUrl = urlNovelDetailed(novel.id)\n }\n\n if (novel.seriesId && !series.error) {\n java.log(`正在获取系列小说:${novel.seriesId}`)\n novel.id = series.novels[0].id\n // novel.title = series.title\n if (series.tags) {\n novel.tags = novel.tags.concat(series.tags)\n }\n novel.latestChapter = series.novels.reverse()[0].title\n novel.description = series.caption\n // 后端目前没有系列的 coverUrl 字段\n // novel.coverUrl = series.coverUrl\n novel.coverUrl = series.novels[0].coverUrl\n\n let firstNovel = getAjaxJson(urlNovelDetailed(novel.id))\n if (firstNovel.error !== true) {\n novel.tags = novel.tags.concat(firstNovel.tags)\n novel.createDate = firstNovel.createDate\n if (novel.description === \"\") {\n novel.description = firstNovel.desc\n }\n }\n }\n })\n return novels\n }\n\n // 小说信息格式化\n u.formatNovels = function (novels) {\n novels.forEach(novel => {\n novel.title = novel.title.trim()\n if (!novel.userName.startsWith(\"@\")) novel.userName = `@${novel.userName}`\n novel.coverUrl = urlCoverUrl(novel.coverUrl)\n novel.createDate = dateFormat(novel.createDate)\n\n novel.tags2 = []\n for (let i in novel.tags) {\n let tag = novel.tags[i]\n if (tag.includes(\"/\")) {\n let tags = tag.split(\"/\")\n novel.tags2 = novel.tags2.concat(tags)\n } else {\n novel.tags2.push(tag)\n }\n }\n novel.tags = Array.from(new Set(novel.tags2))\n novel.tags = novel.tags.join(\",\")\n\n if (util.settings.MORE_INFORMATION) {\n novel.description = `\\n书名:${novel.title}\\n作者:${novel.userName}\\n标签:${novel.tags}\\n上传:${novel.createDate}\\n简介:${novel.description}`\n } else {\n novel.description = `\\n${novel.description}\\n上传时间:${novel.createDate}`\n }\n })\n return novels\n }\n\n // 从网址获取id,返回单篇小说 res\n u.getNovelRes = function (novel) {\n let novelId = 0, res = []\n let isJson = isJsonString(result)\n let isHtml = isHtmlString(result)\n if (!isJson && isHtml) {\n let id = baseUrl.match(new RegExp(\"\\\\d+\"))[0]\n let pattern = \"(https?://)?(api\\\\.|www\\\\.)?(furrynovel\\\\.(ink|xyz))/pixiv/user/\\\\d+(/cache)?\"\n let isAuthor = baseUrl.match(new RegExp(pattern))\n if (isAuthor) {\n java.log(`作者ID:${id}`)\n novelId = getAjaxJson(urlUserDetailed(id)).novels.reverse()[0]\n java.log(`最新一篇小说ID:${novelId}`)\n }\n\n pattern = \"(https?://)?(api\\\\.|www\\\\.)?(furrynovel\\\\.(ink|xyz))/(pn|pixiv/novel)/\\\\d+(/cache)?\"\n let isNovel = baseUrl.match(new RegExp(pattern))\n if (isNovel) {\n novelId = id\n java.log(`匹配小说ID:${id}`)\n }\n res = getAjaxJson(urlNovelDetailed(novelId))\n }\n\n if (isJson) {\n res = JSON.parse(result)\n }\n if (res.error) {\n java.log(`无法从 Linpx 获取当前小说`)\n java.log(JSON.stringify(res))\n }\n return res\n }\n\n util = u\n java.put(\"util\", objStringify(u))\n}\n\npublicFunc()\nif (result.code() === 200) getWebViewUA()\njava.getStrResponse(null, null)",
"loginUi": "@js:\nlet source = [\n {\"🆙 更新书源\": \"updateSource()\" },\n {\"🔰 使用指南\": \"startGithubReadme()\" },\n {\"🐞 反馈问题\": \"startGithubIssue()\" },\n]\n\nlet novel = [\n {\"章节名称\": \"text\" },\n {\"⤴️ 分享章节\": \"shareFactory('novel')\" },\n {\"⤴️ 分享作者\": \"shareFactory('author')\" },\n {\"🅿️ 分享小说\": \"shareFactory('pixiv')\" },\n]\n\nlet settings = [\n {\"书源设置\": \"text\" },\n {\"⚙️ 当前设置\": \"showSettings()\" },\n {\"🔧 默认设置\": \"setDefaultSettingsLoginUrl()\" },\n {\"👤 🚫 搜索作者\": \"editSettings('SEARCH_AUTHOR')\" },\n\n {\"🀄 🚫 繁简通搜\": \"editSettings('CONVERT_CHINESE')\" },\n {\"🚫 📖 更多简介\": \"editSettings('MORE_INFORMATION')\" },\n {\"🔗 🚫 原始链接\": \"editSettings('SHOW_ORIGINAL_LINK')\" },\n\n {\"📚 🚫 恢复《》\": \"editSettings('REPLACE_TITLE_MARKS')\" },\n {\"🖼️ 🚫 显示描述\": \"editSettings('SHOW_CAPTIONS')\" },\n {\"🐞 🚫 调试模式\": \"editSettings('DEBUG')\" },\n]\n\n// 添加按钮\nlet li = source\ntry {\n if (book) li = li.concat(novel)\n} catch (e) {}\nli = li.concat(settings)\n\n// 处理按钮\nli.forEach(item => {\n item.name = Object.keys(item)[0]\n let list = item.name.split(\" \")\n if (list.length === 1 ) {\n item.type = \"text\"\n } else if (list.length === 2) {\n item.type = \"button\"\n item.action = Object.values(item)[0]\n } else {\n item.name = list[list.length - 1]\n item.type = \"toggle\"\n item.default = `${list[0]} `\n list.length = list.length - 1\n item.chars = list.map(char => `${char} `)\n item.action = Object.values(item)[0]\n }\n delete item[Object.keys(item)[0]]\n // 添加格式\n if (item.type === \"button\" || item.type === \"toggle\") {\n item.style = {}\n item.style.layout_flexGrow = 1\n item.style.layout_flexBasisPercent = -1\n }\n})\n\nJSON.stringify(li)",
"loginUrl": "function login() {}\n\nfunction getNovel() {\n let novel = {}\n novel.id = chapter.url.match(/\\d+/)[0]\n novel.title = chapter.title\n novel.userName = book.author.replace(\"@\", \"\")\n if (book.tocUrl.includes(\"series\")) {\n novel.seriesId = book.tocUrl.match(/\\d+/)[0]\n novel.seriesTitle = book.name\n } else {\n novel.seriesId = 0\n novel.seriesTitle = \"\"\n }\n\n let resp = getAjaxJson(urlNovelDetailed(novel.id))\n novel.userId = resp.userId\n if (!novel.seriesId && resp.series) {\n novel.seriesId = resp.series.id\n novel.seriesTitle = resp.series.title\n }\n return novel\n}\n\nfunction shareFactory(type) {\n let novel = getNovel()\n if (!novel) return sleepToast(\"🔰 功能提示\\n\\n⚠️ 请在小说阅读页面,使用本功能\")\n if (type.includes(\"author\")) {\n sleepToast(\"\\n\\n已复制当前作者链接\", 1)\n java.copyText(urlUserUrl(novel.userId))\n // startBrowser(urlUserUrl(novel.userId), novel.userName)\n }\n else if (type.includes(\"novel\")) {\n sleepToast(\"\\n\\n已复制当前小说链接\", 1)\n java.copyText(urlNovelUrl(novel.id))\n // startBrowser(urlNovelUrl(novel.id), novel.title)\n }\n else if (type.includes(\"pixiv\") && !novel.seriesId) {\n sleepToast(\"\\n\\n已复制当前小说系列 Pixiv 链接\", 1)\n java.copyText(urlSourceUrl(novel.id))\n // startBrowser(urlSourceUrl(novel.id), novel.title)\n }\n else if (type.includes(\"pixiv\") && novel.seriesId) {\n sleepToast(\"\\n\\n已复制当前小说系列 Pixiv 链接\", 1)\n java.copyText(urlSeriesUrl(novel.seriesId))\n // startBrowser(urlSeriesUrl(novel.seriesId), novel.seriesTitle)\n }\n}\n\nfunction startGithubReadme() {\n startBrowser(\"https://pixivsource.pages.dev/Linpx\", \"使用指南\")\n}\nfunction startGithubIssue() {\n startBrowser(\"https://github.com/DowneyRem/PixivSource/issues\", \"反馈问题\")\n}\n\nfunction checkStatus(status) {\n if (eval(String(status)) === true) return \"❤️\"\n else return \"🖤\"\n}\n\nlet settingsName = {\n \"SEARCH_AUTHOR\": \"🔍 搜索作者\",\n \"CONVERT_CHINESE\": \"🀄️ 繁简通搜\",\n \"MORE_INFORMATION\": \"📖 更多简介\",\n \"SHOW_ORIGINAL_LINK\": \"🔗 原始链接\",\n \"REPLACE_TITLE_MARKS\": \"📚 恢复《》\",\n \"SHOW_CAPTIONS\": \"🖼️ 显示描述\",\n \"DEBUG\": \"🐞 调试模式\",\n}\n\nfunction statusMsg(status) {\n if (status === true) return \"✅ 已开启\"\n else if (status === false) return \"🚫 已关闭\"\n else return \"🈚️ 未设置\"\n}\n\n// 检测快速模式修改的4个设置\nfunction getSettingStatus(mode) {\n if (mode === undefined) mode = \"\"\n let keys = [], msgList = []\n let settings = getFromCacheObject(\"linpxSettings\")\n keys = Object.keys(settingsName)\n for (let i in keys) {\n msgList.push(`${statusMsg(settings[keys[i]])} ${settingsName[keys[i]]}`)\n }\n return msgList.join(\"\\n\").trim()\n}\n\nfunction showSettings() {\n sleepToast(`\\n⚙️ 当前设置\\n\\n${getSettingStatus()}`)\n}\n\nfunction setDefaultSettingsLoginUrl() {\n setDefaultSettings()\n sleepToast(`\\n✅ 已恢复 🔧 默认设置\\n\\n${getSettingStatus()}`)\n}\n\nfunction editSettings(settingName) {\n let msg, status\n let settings = getFromCacheObject(\"linpxSettings\")\n if (!settings) settings = setDefaultSettings()\n if (!!settings[settingName]) {\n status = settings[settingName] = !settings[settingName]\n } else {\n status = settings[settingName] = true\n }\n putInCacheObject(\"linpxSettings\", settings)\n msg = `\\n\\n${statusMsg(status)} ${settingsName[settingName]}`\n sleepToast(msg)\n}",
"respondTime": 180000,
"ruleBookInfo": {
"author": "userName",
"canReName": "true",
"coverUrl": "coverUrl",
"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\nfunction novelHandler(novel) {\n novel = util.formatNovels(util.handNovels([novel]))[0]\n // 优化 未缓存系列目录的情况:目录链接设置为单篇链接\n let result = getAjaxJson(urlSeriesDetailed(novel.seriesId))\n if (!novel.seriesId || novel.seriesId && result.error) {\n book.bookUrl = novel.detailedUrl = urlNovelUrl(novel.id)\n book.tocUrl = novel.catalogUrl = urlNovelDetailed(novel.id)\n } else {\n book.bookUrl = novel.detailedUrl = urlNovelUrl(novel.id)\n book.tocUrl = novel.catalogUrl = urlSeriesDetailed(novel.seriesId)\n }\n return novel\n}\n\n(() => {\n return novelHandler(util.getNovelRes(result))\n})();",
"intro": "description",
"kind": "tags",
"lastChapter": "latestChapter",
"name": "title",
"tocUrl": "catalogUrl",
"wordCount": "textCount"
},
"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\nfunction replaceUploadedImage(res, content) {\n // 获取 [uploadedimage:] 的图片链接\n if (res.images !== undefined && res.images !== null) {\n Object.keys(res.images).forEach((key) => {\n // content = content.replace(`[uploadedimage:${key}]`, `<img src=\"${urlCoverUrl(res.images[key].origin)}\">`)\n content = content.replace(`[uploadedimage:${key}]`, `<img src=\"${urlPixivCoverUrl(res.images[key].origin)}\">`)\n })\n }\n return content\n}\nfunction replacePixivImage(content) {\n // 获取 [pixivimage:] 的图片链接 [pixivimage:1234] [pixivimage:1234-1]\n let matched = content.match(RegExp(/\\[pixivimage:(\\d+)-?(\\d+)]/gm))\n if (matched) {\n matched.forEach(pixivimage => {\n let matched2, illustId, order = 0\n if (pixivimage.includes(\"-\")) {\n matched2 = pixivimage.match(RegExp(\"(\\\\d+)-(\\\\d+)\"))\n illustId = matched2[1]; order = matched2[2]\n } else {\n matched2 = pixivimage.match(RegExp(\"\\\\d+\"))\n illustId = matched2[0];\n }\n if (urlIllustOriginal(illustId, order)) {\n content = content.replace(`${pixivimage}`, `<img src=\"${urlIllustOriginal(illustId, order)}\">`)\n } else {\n content = content.replace(`${pixivimage}`, ``)\n }\n })\n }\n return content\n}\nfunction replaceNewPage(content) {\n // 替换 Pixiv 分页标记符号 [newpage]\n if (!util.environment.IS_LYC_BRUNCH) {\n let matched = content.match(RegExp(/[ ]*\\[newpage][ ]*/gm))\n if (matched) {\n for (let i in matched) {\n content = content.replace(`${matched[i]}`, `${\"<p><p/>\".repeat(3)}`)\n }\n }\n }\n return content\n}\nfunction replaceChapter(content) {\n // 替换 Pixiv 章节标记符号 [chapter:]\n let 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 // 替换 Pixiv 分页标记符号 [newpage]\n if (util.environment.IS_LYC_BRUNCH) {\n content = content.replace(`${matched[i]}`, `<usehtml><h3>${chapter}</h3></usehtml>`)\n } else {\n content = content.replace(`${matched[i]}`, `${chapter}<p><p/>`)\n }\n }\n }\n return content\n}\nfunction replaceJumpPage(content) {\n // 替换 Pixiv 跳转页面标记符号 [[jump:]]\n let matched = content.match(RegExp(/\\[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 return content\n}\nfunction replaceJumpUrl(content) {\n // 替换 Pixiv 链接标记符号 [[jumpuri: > ]]\n let matched = content.match(RegExp(/\\[\\[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 if (util.environment.IS_LYC_BRUNCH) {\n content = content.replace(`${matchedText}`, `<usehtml><p> <a href=${urlLink}>${urlName}</a></p></usehtml>`)\n } else {\n if (urlLink === urlName) {\n content = content.replace(`${matchedText}`, `${urlName}`)\n } else {\n content = content.replace(`${matchedText}`, `${urlName}: ${urlLink}`)\n }\n }\n }\n }\n return content\n}\nfunction replaceRb(content) {\n // 替换 Pixiv 注音标记符号 [[rb: > ]]\n let matched = content.match(RegExp(/\\[\\[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\n if (!util.settings.REPLACE_TITLE_MARKS) {\n // 默认替换成(括号)\n content = content.replace(`${matchedText}`, `${kanji}(${kana})`)\n } else {\n let reg = RegExp(\"[\\\\u4E00-\\\\u9FFF]+\", \"g\");\n if (reg.test(kana)) {\n // kana为中文,则替换回《书名号》\n content = content.replace(`${matchedText}`, `${kanji}《${kana}》`)\n } else {\n // 阅读不支持 <ruby> <rt> 注音\n // content = content.replace(`${matchedText}`, `<ruby>${kanji}<rt>${kana}</rt></ruby>`)\n content = content.replace(`${matchedText}`, `${kanji}(${kana})`)\n }\n }\n }\n }\n return content\n}\n\nfunction getContent(res) {\n let content = res.content\n if (util.settings.SHOW_CAPTIONS === true && res.desc !== \"\") {\n content = res.desc + \"\\n\" + \"——————————\\n\".repeat(2) + content\n }\n\n // 替换 Pixiv 标记符\n content = replaceUploadedImage(res, content)\n content = replacePixivImage(content)\n content = replaceNewPage(content)\n content = replaceChapter(content)\n content = replaceJumpPage(content)\n content = replaceJumpUrl(content)\n content = replaceRb(content)\n return content\n}\n\n(() => {\n return getContent(util.getNovelRes(result))\n})()",
"imageStyle": "FULL",
"callBackJs": "function shareBook() {\n let text = `我正在看:【${book.author.replace(\"@\", \"\")}】创作的《${book.name}》`\n if (!!book.durChapterTitle && String(book.name) !== String(book.durChapterTitle)) {\n text += `的 【${book.durChapterTitle}】`\n }\n text += `\\n\\n小说链接:\\n${book.bookUrl}\\n\\n分享自【开源阅读】Linpx书源。使用添加网址,快速添加本文`\n java.copyText(text)\n return true\n}\n\nfunction clearCache() {\n let novel = getNovel()\n cache.delete(urlNovelDetailed(novel.id))\n if (novel.seriesId) cache.delete(urlSeriesDetailed(novel.seriesId))\n return true\n}\n\nfunction copyBookUrl() {\n java.copyText(book.bookUrl)\n return true\n}\ncopyTocUrl = copyBookUrl\n\n// 保存阅读,更新登录界面的章节名称\nfunction saveRead() {\n source.putLoginInfo(JSON.stringify({\"章节名称\": book.durChapterTitle}))\n}\n\nfunction startShelfRefresh() {\n source.putConcurrent(\"18/30000\")\n}\n\nfunction endShelfRefresh() {\n source.putConcurrent(\"25/5000\")\n}\n\nfunction customButton(){\n java.open(\"login\")\n}\n\nfunction callBackFactory(event) {\n switch (event) {\n // case \"clickBookName\":\n // return clickBookName()\n // case \"longClickBookName\":\n // return longClickBookName()\n // case \"clickAuthor\":\n // return clickAuthor()\n // case \"longClickAuthor\":\n // return longClickAuthor()\n case \"clickCustomButton\":\n return customButton()\n // case \"longClickCustomButton\":\n // return longcustomButton()\n\n case \"clickShareBook\":\n return shareBook()\n case \"clickClearCache\":\n return clearCache()\n case \"clickCopyBookUrl\":\n return copyBookUrl()\n case \"clickCopyTocUrl\":\n return copyTocUrl()\n\n // 下面的事件无法被回调结果消费\n // case \"addBookShelf\":\n // return addBookShelf()\n // case \"delBookShelf\":\n // return delBookShelf()\n case \"saveRead\":\n return saveRead()\n // case \"startRead\":\n // return startRead()\n // case \"endRead\":\n // return endRead()\n case \"startShelfRefresh\":\n return startShelfRefresh()\n case \"endShelfRefresh\":\n return endShelfRefresh()\n }\n}\n\n(() => {\n return callBackFactory(event)\n})()"
},
"ruleExplore": {
"author": "userName",
"bookList": "@js:\nvar util = objParse(String(java.get(\"util\")))\nvar seriesSet = new Set(); // 存储seriesID\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/**\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 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 // java.log(`查询的用户Ids:${userIds}`)\n let usersInfo = getWebviewJson(urlUsersDetailed(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 = getWebviewJson(urlNovelsDetailed(queryNovelIds))\n return util.formatNovels(util.handNovels(util.combineNovels(novelList)))\n }\n}\n\nfunction handlerFollowLatest() {\n return () => {\n let resp = JSON.parse(result)\n return util.formatNovels(util.handNovels(util.combineNovels(resp)))\n }\n}\n\nfunction handlerRegexNovels() {\n return () => {\n let result = java.webView(null, \"https://furrynovel.ink\", null)\n let name = result.match(RegExp('<div class=\" font-bold text-xl line-clamp-1\">(.*?)</div>'))[1]\n let resp = getAjaxJson(urlSearchNovel(name))\n if (resp.total !== undefined) {\n return util.formatNovels(util.handNovels(util.combineNovels(resp.novels)))\n }\n return []\n }\n}\n\nfunction handlerFactory() {\n if (baseUrl.includes(\"https://cdn.jsdelivr.net\")) {\n return () => {updateSource(); return []}\n }\n if (baseUrl.includes(\"/fav/user\")) {\n return handlerRecommendUsers()\n }\n if (baseUrl.includes(\"/pixiv/novels/recent\")) {\n return handlerFollowLatest()\n }\n if (baseUrl.includes(\"https://furrynovel.ink\")) {\n return handlerRegexNovels()\n }\n else {\n return () => {startBrowser(baseUrl, \"\"); return []}\n }\n}\n\n(() => {\n return handlerFactory()()\n})()",
"bookUrl": "detailedUrl",
"coverUrl": "coverUrl",
"intro": "description",
"kind": "tags",
"lastChapter": "latestChapter",
"name": "title",
"wordCount": "textCount"
},
"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\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\nfunction getUser(username, exactMatch) {\n let resp = getAjaxJson(urlSearchUsers(String(username)))\n java.log(urlSearchUsers(String(username)))\n // java.log(JSON.stringify(resp))\n if (resp.error || resp.total === 0) {\n return []\n }\n\n // 去除无小说作者\n resp.users = resp.users.filter(user => user.novels.length >= 1)\n // java.log(JSON.stringify(resp))\n if (resp.total > resp.users.length) {\n java.log(`已经去除 ${resp.total - resp.users.length} 位无小说的作者`)\n }\n if (resp.users.length === 0) {\n return []\n }\n\n if (!exactMatch) {\n return resp.users\n }\n // 只返回用户名完全一样的用户\n return resp.users.filter(user => {\n return user.name === username\n })\n}\n\nfunction getUserNovels(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 // java.log(`NIDURL:${urlNovelsDetailed(list)}`)\n return getWebviewJson(urlNovelsDetailed(list))\n}\n\nfunction findUserNovels() {\n let novelList = []\n let username = String(java.get(\"key\"))\n let userArr = getUser(username, false)\n // 获取用户所有小说\n let uidList = userArr.filter(user => {\n return user.novels.length > 0\n }).map(user => user.id)\n // java.log(JSON.stringify(uidList))\n\n if (uidList.length > 0) {\n let list = getWebviewJson(urlUsersDetailed(uidList)) // 包含所有小说数据\n // java.log(JSON.stringify(list))\n let nidList = []\n // 从两层数组中提取novelsId\n list.forEach(user => {\n // 按id降序排序-相当于按时间降序排序\n user.novels.reverse().forEach(nid => nidList.push(nid))\n // 优化 未缓存系列目录的情况:series 数据写入缓存\n user.series.forEach(series => {\n series.novels = []\n putInCacheObject(`LSeries${series.id}`, series)\n })\n })\n // java.log(JSON.stringify(nidList))\n getUserNovels(nidList).forEach(novel => {\n novelList.push(novel)\n })\n }\n\n // 优化 未缓存系列目录的情况:series 数据写入缓存\n novelList.forEach(novel =>{\n if (novel.seriesId) {\n let series = getFromCacheObject(`LSeries${novel.seriesId}`)\n series.novels.push(novel)\n putInCacheObject(`LSeries${novel.seriesId}`, series)\n }\n })\n return novelList.reverse() // 新小说前置\n}\n\nfunction getNovels() {\n if (result.startsWith(\"<!DOCTYPE html>\") || JSON.parse(result).error) {\n return []\n } else {\n return JSON.parse(result).novels\n }\n}\n\nfunction search(name, page=1) {\n let resp = getAjaxJson(urlSearchNovel(name, page))\n java.log(urlSearchNovel(name, page))\n if (resp.error === undefined && resp.total > 0) {\n return resp.novels\n } else {\n return []\n }\n}\n\nfunction getConvertNovels() {\n let novels = []\n let novelName = String(java.get(\"key\"))\n let name1 = String(java.s2t(novelName))\n let name2 = String(java.t2s(novelName))\n if (name1 !== novelName) novels = novels.concat(search(name1))\n if (name2 !== novelName) novels = novels.concat(search(name2))\n return novels\n}\n\n(() => {\n let novels = []\n let key = String(java.get(\"key\"))\n if (key.startsWith(\"@\")) {\n java.put(\"key\", key.slice(1))\n novels = novels.concat(findUserNovels())\n } else if (key.startsWith(\"#\")) {\n java.put(\"key\", key.slice(1))\n novels = novels.concat(getNovels())\n // if (util.settings.CONVERT_CHINESE) novels = novels.concat(getConvertNovels())\n } else {\n novels = novels.concat(getNovels())\n if (util.settings.SEARCH_AUTHOR) novels = novels.concat(findUserNovels())\n if (util.settings.CONVERT_CHINESE) novels = novels.concat(getConvertNovels())\n }\n // java.log(JSON.stringify(novels))\n // 返回空列表中止流程\n if (novels.length === 0) {\n return []\n }\n return util.formatNovels(util.handNovels(util.combineNovels(novels)))\n})()",
"bookUrl": "detailedUrl",
"checkKeyWord": "#测试页面",
"coverUrl": "coverUrl",
"intro": "description",
"kind": "tags",
"lastChapter": "latestChapter",
"name": "title",
"wordCount": "textCount"
},
"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\nfunction urlNovel(novelId) {\n if (util.settings.SHOW_ORIGINAL_LINK) {\n return urlNovelUrl(novelId)\n } else {\n return urlNovelDetailed(novelId)\n }\n}\n\nfunction oneShotHandler(resp) {\n resp.textCount = resp.content.length\n resp.updateDate = timeTextFormat(resp.createDate)\n return [{\n title: resp.title.trim(),\n chapterUrl: urlNovel(resp.id),\n chapterInfo:`${resp.updateDate} ${resp.textCount}字`\n }]\n}\n\nfunction seriesHandler(resp) {\n resp.novels.forEach(novel => {\n novel.title = novel.title.trim()\n novel.chapterUrl = urlNovel(novel.id)\n // novel.updateDate = String(novel.coverUrl.match(RegExp(\"\\\\d{4}/\\\\d{2}/\\\\d{2}\"))) //fake\n novel.detail = getAjaxJson(urlNovelDetailed(novel.id))\n novel.textCount = novel.detail.content.length\n novel.updateDate = timeTextFormat(novel.detail.createDate)\n novel.chapterInfo = `${novel.updateDate} ${novel.textCount}字`\n delete novel.detail\n })\n return resp.novels\n}\n\n// 优化 未缓存系列目录的情况:从章节数据中,获取系列目录\nfunction seriesContentHandler(resp) {\n let novels = [], prevNovels = [], nextNovels = []\n while (resp.series.prev !== null && resp.series.prev !== undefined) {\n prevNovels.push(resp.series.prev)\n resp = getAjaxJson(urlNovelDetailed(resp.series.prev.id))\n }\n nextNovels.push({id: resp.id, order: resp.series.order, title: resp.title})\n while (resp.series.next !== null && resp.series.prev !== undefined) {\n nextNovels.push(resp.series.next)\n resp = getAjaxJson(urlNovelDetailed(resp.series.next.id))\n }\n novels = novels.concat(prevNovels.reverse())\n novels = novels.concat(nextNovels)\n novels.forEach(novel => {\n novel.title = novel.title.trim()\n novel.chapterUrl = urlNovel(novel.id)\n novel.detail = getAjaxJson(urlNovelDetailed(novel.id))\n novel.textCount = novel.detail.content.length\n novel.updateDate = timeTextFormat(novel.detail.createDate)\n novel.chapterInfo = `${novel.updateDate} ${novel.textCount}字`\n delete novel.detail\n })\n // java.log(JSON.stringify(novels))\n return novels\n}\n\n(() => {\n let resp = util.getNovelRes(result)\n if (resp.novels) {\n return seriesHandler(resp)\n } else if (resp.series) {\n // 优化 未缓存系列目录的情况\n return seriesContentHandler(resp)\n } else {\n return oneShotHandler(resp)\n }\n})()",
"chapterName": "title",
"chapterUrl": "chapterUrl",
"updateTime": "chapterInfo"
},
"searchUrl": "@js:\njava.put(\"key\", key)\njava.put(\"page\", page)\nif (key.startsWith(\"@\") || key.startsWith(\"@\")) {\n key = key.slice(1)\n java.log(`👤 搜索作者:${key}`)\n} else if (key.startsWith(\"#\") || key.startsWith(\"#\")) {\n key = key.slice(1)\n java.log(`#️⃣ 搜索标签:${key}`)\n} else {\n java.log(`🔍 搜索内容:${key}`)\n}\nurlSearchNovel(key, page)",
"variableComment": "⚙️ 书源设置:\n设置:打开小说 - 菜单 - 登录 - 点击下方按钮\n\n",
"weight": 0,
"lastUpdateTime": 1775232000251
},
{
"bookSourceComment": "🐯 兽人控小说站书源(更新📆:2026/04/04)\n\n书源版本:265\n使用说明:📌阅读 Plus 3.26.0129 版本可用\n可用功能:✅搜索✅发现✅添加网址✅订阅源\n搜索小说:✅单篇✅系列✅作者✅标签\n发现小说:✅热门小说✅最新小说✅随便来点\n添加网址:✅兽人控小说站链接\n订阅用法:点击订阅源打开小说/系列小说,点击【加入书架】按钮,添加小说到书架\n\n书源发布:Pixiv 书源频道 https://t.me/PixivSource\n兽人阅读频道 https://t.me/FurryReading\n项目地址:https://github.com/DowneyRem/PixivSource\n使用教程:https://pixivsource.pages.dev/FurryNovel\n\n规则订阅:Import 订阅源\nhttps://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/import.json\nhttps://raw.githubusercontent.com/DowneyRem/PixivSource/main/import.json\n\n⚙️ 书源设置:\n书源管理 - 编辑书源 - 基本 - 变量说明 - 修改并保存\n\n🔎 筛选发现:\n发现 - 长按\"兽人控小说站\" - 编辑 - 右上角菜单 - 设置源变量\n设置源变量:输入想要搜索/筛选的标签,以空格间隔(或一行一个),保存\n发现 - 长按\"兽人控小说站\" - 刷新 - 查看他人收藏",
"bookSourceGroup": "🔞 Pixiv,🐲 Furry",
"bookSourceName": "🐯 兽人小说站",
"bookSourceType": 0,
"bookSourceUrl": "https://furrynovel.com",
"bookUrlPattern": "(https?://)?(www\\.|api\\.)?furrynovel\\.com(/api)?/(zh|en|ja)/novel/\\d+(/chapter/d+)?",
"concurrentRate": "30/5000",
"customButton": true,
"customOrder": 4,
"enabled": true,
"enabledCookieJar": true,
"enabledExplore": true,
"eventListener": true,
"exploreUrl": "@js:\nlet keyword = String(source.getVariable()).replace(\"#\", \"\")\nlet key = keyword.split(/[ ,,、\\n]/)\nif (key.includes(\"\")) {\n key.splice(key.indexOf(\"\"), 1)\n}\nif (key.length === 0) {\n sleepToast(\"可设置源变量,筛选发现 🔍 \")\n sleepToast('发现页 - 长按\"兽人控小说站\" - 编辑 - 右上角菜单 - 设置源变量')\n} else {\n sleepToast(`正在搜索:${key.join(\"、\")}`)\n}\n\nlet li = [\n {\"🔥 热门\": `https://api.furrynovel.com/api/novel?page={{page}}&order_by=popular&${key.map(v => \"tags[]=\" + v).join(\"&\")}`},\n {\"🆕 最新\": `https://api.furrynovel.com/api/novel?page={{page}}&order_by=latest&${key.map(v => \"tags[]=\" + v).join(\"&\")}`},\n {\"🔄 随便\": `https://api.furrynovel.com/api/novel?page={{page}}&order_by=random&${key.map(v => \"tags[]=\" + v).join(\"&\")}`},\n {\"🆙 更新\": \"https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/linpx.json\"},\n {\"📙 书源相关 📙\": \"\"},\n {\"🏠 主页\": \"https://pixivsource.pages.dev\"},\n {\"🔰 指南\": \"https://pixivsource.pages.dev/FurryNovel\"},\n {\"🐞 反馈\": \"https://github.com/DowneyRem/PixivSource/issues\"},\n {\"💰 打赏\": \"https://pixivsource.pages.dev/Sponsor\"},\n]\n\n// 格式化发现地址\nli.forEach(item => {\n item.title = Object.keys(item)[0]\n item.url = Object.values(item)[0]\n delete item[Object.keys(item)[0]]\n item.style = {}\n item.style.layout_flexGrow = 1\n item.style.layout_flexShrink = 1\n item.style.layout_alignSelf = \"auto\"\n item.style.layout_wrapBefore = \"false\"\n if (item.url === \"\") {\n item.style.layout_flexBasisPercent = 1\n } else {\n item.style.layout_flexBasisPercent = -1\n }\n})\nJSON.stringify(li)",
"header": "",
"jsLib": "var cacheSaveSeconds = 30*24*60*60 // 长期缓存 30 天\nvar cacheTempSeconds = 10*60*1000 // 冷却时间 10 分钟\n\nfunction cacheGetAndSet(key, supplyFunc, requestUpdate) {\n const {java, cache} = this\n let timestamp = 0\n let v = this.getFromCacheObject(key)\n if (Array.isArray(v)) {\n try {\n timestamp = v[0].timestamp\n } catch (e) {\n timestamp = 0\n }\n } else if (v) {\n timestamp = v.timestamp\n }\n\n const isExpired = v && (new Date().getTime() >= timestamp + cacheTempSeconds)\n const isError = v && (v.error === true) && isExpired\n requestUpdate = requestUpdate && isExpired\n\n if (!v || requestUpdate || isError) {\n v = supplyFunc()\n let now = new Date().getTime()\n // getAjaxJson getWebviewJson 时间戳写入对象本身\n if (!Array.isArray(v)) {\n v = Object.assign({timestamp: now}, v)\n }\n // else {\n // // getAjaxAllJson 时间戳写入第一个元素(读取时 v[0].timestamp)// 不重复写入\n // if (v.length > 0) v[0] = Object.assign({timestamp: now}, v[0])\n // }\n this.putInCacheObject(key, v, cacheSaveSeconds)\n }\n return v\n}\n\nfunction putInCache(name, object, saveSeconds) {\n const {java, cache} = this\n if (saveSeconds === undefined) saveSeconds = 0\n if (object) {\n cache.put(name, object, saveSeconds)\n }\n}\nfunction getFromCache(name) {\n const {java, cache} = this\n let object = cache.get(name)\n if (object === undefined) return null // 兼容源阅\n return object\n}\n\nfunction putInCacheObject(objectName, object, saveSeconds) {\n const {java, cache} = this\n if (object === undefined) object = null\n if (saveSeconds === undefined) saveSeconds = 0\n cache.put(objectName, JSON.stringify(object), saveSeconds)\n}\nfunction getFromCacheObject(objectName) {\n const {java, cache} = this\n let object = cache.get(objectName)\n if (object === undefined) return null // 兼容源阅\n return JSON.parse(object)\n}\n\nfunction getAjaxJson(url, requestUpdate) {\n const {java, cache} = this\n return this.cacheGetAndSet(url, () => {\n return JSON.parse(java.ajax(url))\n }, requestUpdate)\n}\nfunction getAjaxAllJson(urls, requestUpdate) {\n const {java, cache} = this\n let batchKey = JSON.stringify(urls)\n return this.cacheGetAndSet(batchKey, () => {\n let results = []\n let now = new Date().getTime()\n let responses = java.ajaxAll(urls)\n for (let i in urls) {\n let data = JSON.parse(responses[i].body())\n data = Object.assign({timestamp: now}, data)\n results.push(data)\n this.putInCacheObject(urls[i], data, cacheSaveSeconds)\n }\n return results\n }, requestUpdate)\n}\n\nfunction getWebViewUA() {\n const {java, cache} = this\n let userAgent = this.getFromCache(\"userAgent\")\n if (userAgent) return String(userAgent)\n\n userAgent = String(java.getWebViewUA())\n if (userAgent.includes(\"Windows NT 10.0; Win64; x64\")) {\n userAgent = \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36\"\n }\n userAgent += \" Reader\"\n // java.log(`userAgent=${userAgent}`)\n this.putInCache(\"userAgent\", userAgent, cacheSaveSeconds/7)\n return String(userAgent)\n}\nfunction startBrowser(url, title) {\n const {java} = this\n if (!title) title = url\n let msg = \"\"\n let headers = {}\n headers[\"User-Agent\"] = this.getWebViewUA()\n\n if (url.includes(\"github.com\") || url.includes(\"github.io\")) {\n msg += \"\\n\\n即将打开 Github\\n请确认已开启代理/梯子/VPN等\"\n }\n this.sleepToast(msg, 0.01)\n java.startBrowser(`${url}, ${JSON.stringify({headers: headers})}`, title)\n}\n\nfunction urlNovelUrl(novelId) {\n return `https://furrynovel.com/zh/novel/${novelId}`\n}\nfunction urlNovelDetail(novelId) {\n return `https://api.furrynovel.com/api/zh/novel/${novelId}`\n}\nfunction urlNovelsDetail(novelIds) {\n return `https://api.furrynovel.com/api/zh/novel?${novelIds.map(v => \"ids[]=\" + v).join(\"&\")}`\n}\nfunction urlNovelChapterUrl(novelId, chapterId) {\n return `https://furrynovel.com/zh/novel/${novelId}/chapter/${chapterId}`\n}\nfunction urlNovelChapterInfo(novelId) {\n return `https://api.furrynovel.com/api/zh/novel/${novelId}/chapter`\n}\nfunction urlNovelChapterDetail(novelId, chapterId) {\n return `https://api.furrynovel.com/api/zh/novel/${novelId}/chapter/${chapterId}`\n}\n\nfunction urlSourceUrl(source, oneShot, sourceId) {\n if (source === \"bilibili\") {\n return `https://www.bilibili.com/read/readlist/rl${sourceId}/`\n }\n if (source === \"pixiv\" && oneShot === true) {\n return `https://www.pixiv.net/novel/show.php?id=${sourceId}`\n }\n if (source === \"pixiv\" && oneShot === false) {\n return `https://www.pixiv.net/novel/series/${sourceId}`\n }\n}\n\nfunction urlSearchNovel(name, page) {\n return `https://api.furrynovel.com/api/zh/novel?page=${page}&order_by=popular&keyword=${name}`\n}\nfunction urlSearchUser(name, page) {\n return `https://api.furrynovel.com/api/zh/novel?keyword=${name}&page=${page}`\n}\nfunction urlUserUrl(name) {\n return `https://furrynovel.com/zh/search?keyword=${name}`\n}\n\nfunction urlLinpxNovelDetail(sourceId) {\n return `https://api.furrynovel.ink/pixiv/novel/${sourceId}/cache`\n}\nfunction urlLinpxCoverUrl(pxImgUrl) {\n if (!pxImgUrl.trim()) return \"\"\n let url = `https://pximg.furrynovel.ink/?url=${pxImgUrl}&w=800`\n let headers = {\"Referer\": \"https://furrynovel.ink/\"}\n return `${url}, ${JSON.stringify({headers: headers})}`\n}\n\nfunction urlIllustUrl(illustId) {\n return `https://www.pixiv.net/artworks/${illustId}`\n}\nfunction urlIllustDetailed(illustId) {\n return `https://www.pixiv.net/ajax/illust/${illustId}?lang=zh`\n}\n// 直连功能参考自 洛娅橙的阅读仓库\n// https://github.com/Luoyacheng/yuedu\n// 其直连功能参考自 PixEz Flutter\n// https://github.com/Notsfsssf/pixez-flutter\nfunction urlIP(url) {\n const {java, cache} = this\n url = url.replace(\"http://\", \"https://\").replace(\"www.pixiv.net\", \"210.140.139.155\")\n let headers = {\n \"User-Agent\": this.getWebViewUA(),\n \"X-Requested-With\": \"XMLHttpRequest\",\n \"Host\": \"www.pixiv.net\",\n \"Referer\": \"https://www.pixiv.net/\"\n }\n return `${url}, ${JSON.stringify({headers: headers})}`\n}\nfunction urlPixivCoverUrl(url) {\n const {java, cache} = this\n if (url && !url.trim()) return \"\"\n let headers = {\"Referer\": \"https://www.pixiv.net/\"}\n\n if (url.trim()) {\n if (url.includes(\"i.pximg.net\")) {\n url = url.replace(\"https://i.pximg.net\", \"https://210.140.139.133\")\n headers.Host = \"i.pximg.net\"\n } else {\n url = url.replace(\"https://s.pximg.net\", \"https://210.140.139.133\")\n headers.Host = \"s.pximg.net\"\n }\n }\n return `${url}, ${JSON.stringify({headers: headers})}`\n}\nfunction urlIllustOriginal(illustId, order) {\n const {java, cache} = this\n if (!order || order <= 1) order = 1\n let illustOriginal = \"\"\n\n let resp = this.getAjaxJson(this.urlIP(urlIllustDetailed(illustId)))\n try {\n illustOriginal = resp.body.urls.original\n } catch (e) {\n try {\n let illustThumb = resp.body.userIllusts[illustId].url\n let date = illustThumb.match(\"\\\\d{4}\\\\/\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{2}\")[0]\n illustOriginal = `https://i.pximg.net/img-master/img/${date}/${illustId}_p0_master1200.jpg`\n } catch (e) {}\n }\n\n if (illustOriginal.split(\",\")[0] === \"\") return \"\"\n return this.urlPixivCoverUrl(illustOriginal.replace(`_p0`, `_p${order - 1}`))\n}\n\nfunction addZero(num) {\n return String(num).padStart(2, '0')\n}\nfunction dateFormat(str) {\n let time = new Date(str);\n let Y = time.getFullYear() + \"年\";\n let M = this.addZero(time.getMonth() + 1) + \"月\";\n let D = this.addZero(time.getDate()) + \"日\";\n return Y + M + D;\n}\nfunction timeFormat(str) {\n let time = new Date(str);\n let YY = time.getFullYear()\n let MM = this.addZero(time.getMonth() + 1)\n let DD = this.addZero(time.getDate())\n let hh = this.addZero(time.getHours())\n let mm = this.addZero(time.getMinutes())\n let ss = this.addZero(time.getSeconds())\n return `${YY}-${MM}-${DD} ${hh}:${mm}:${ss}`\n}\nfunction timeTextFormat(text) {\n return `${text.slice(0, 10)} ${text.slice(11, 19)}`\n}\n\nfunction sleep(seconds) {\n return Packages.java.lang.Thread.sleep(1000*seconds)\n}\nfunction sleepToast(text, seconds) {\n let {java} = this\n java.log(text)\n java.longToast(text)\n if (seconds === undefined) {seconds = 0.01}\n this.sleep(seconds)\n}\n\nfunction setDefaultSettings() {\n const {java, cache} = this\n let settings = {}\n settings.CONVERT_CHINESE = true // 搜索:搜索时进行繁简转换\n settings.MORE_INFORMATION = false // 详情:书籍简介显示更多信息\n\n settings.SHOW_ORIGINAL_LINK = true // 目录:显示原始链接,但会增加大量请求\n settings.REPLACE_TITLE_MARKS = true // 正文:注音内容为汉字时,替换为书名号\n\n settings.DEBUG = false // 全局:调试模式\n\n this.putInCacheObject(\"FNSettings\", settings)\n return settings\n}\n\nfunction updateSource() {\n const {java, source} = this\n java.longToast(\"🆙 更新书源\\n\\nJsdelivr CDN 更新有延迟\\nGithub 更新需代理\")\n let onlineSource, comment, sourceName, sourceNameCapitalize, index = 0\n if (source.bookSourceUrl.includes(\"pixiv\")) sourceName = \"pixiv\"\n else if (source.bookSourceUrl.includes(\"furrynovel\")) sourceName = \"linpx\"\n sourceNameCapitalize = sourceName[0].toUpperCase() + sourceName.substring(1)\n\n if (source.bookSourceName.includes(\"备用\")) index = 1\n else if (source.bookSourceName.includes(\"漫画\")) index = 2\n if (source.bookSourceUrl.includes(\"furrynovel.com\")) {\n sourceNameCapitalize = \"FurryNovel\"\n index = 1\n }\n\n try {\n let updateUrl = `https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/${sourceName}.json`\n onlineSource = JSON.parse(java.get(updateUrl,{'User-Agent': 'Mozilla/5.0 (Linux; Android 14)','X-Requested-With': 'XMLHttpRequest'}).body())[index]\n } catch (e) {\n try {\n let updateUrl = `https://raw.githubusercontent.com/DowneyRem/PixivSource/main/${sourceName}.json`\n onlineSource = JSON.parse(java.get(updateUrl,{'User-Agent': 'Mozilla/5.0 (Linux; Android 14)','X-Requested-With': 'XMLHttpRequest'}).body())[index]\n } catch (e) {\n onlineSource = {lastUpdateTime: new Date().getTime(), bookSourceComment: source.bookSourceComment}\n }\n }\n comment = onlineSource.bookSourceComment.split(\"\\n\")\n // onlineSource = source\n // comment = source.bookSourceComment.split(\"\\n\")\n\n let htm = `\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>更新 ${source.bookSourceName} 书源</title>\n <style> \n table { text-align: center; margin: 0 auto; } .ann { display: flex; justify-content: center; align-items: center; height: 5vh; } \n button { background-color: rgb(76, 175, 80); color: white; border: none; border-radius: 4px; height: 6vh; width: 30vw; overflow: hidden; } \n button span { cursor: pointer; display: inline-block; position: relative; transition: 0.4s; } \n button span:after { content: '>'; position: absolute; opacity: 0; top: 0; right: 30px; transition: 0.2s; } \n button:active span { padding-right: 20px; } \n button:active span:after { opacity: 1; right: -40px; }\n </style>\n</head>\n\n<body>\n <table border=\"1\" cellspacing=\"0\">\n <th colspan=\"2\"> ${source.bookSourceName} 书源 \n <a href=\"https://pixivsource.pages.dev/${sourceNameCapitalize}\">🔰 使用指南</a>\n || <a href=\"https://pixivsource.pages.dev/Sponsor\">❤️ 赞助开发</a>\n </th>\n <tr>\n <td>☁️ 远程版本:${onlineSource.bookSourceComment.split(\"\\n\")[2].replace(\"书源版本:\", \"\")}</td>\n <td>📆 更新:${timeFormat(onlineSource.lastUpdateTime)}</td>\n </tr>\n <tr>\n <td>📥 本地版本:${source.bookSourceComment.split(\"\\n\")[2].replace(\"书源版本:\", \"\")}</td>\n <td>📆 更新:${timeFormat(source.lastUpdateTime)}</td>\n </tr> \n <tr><td colspan=\"2\" style=\"text-align: left;\">${comment.slice(3, 9).join(\"<br>\")}</td></tr>\n <tr><td colspan=\"2\" style=\"text-align: left;\">${comment.slice(comment.length-7, comment.length).join(\"<br>\")}</td></tr>\n </table>\n \n <table border=\"0\" cellspacing=\"20\">\n <th colspan=\"2\"> 更新 ${source.bookSourceName} 书源 </th>\n <tr><td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/${sourceName}.json\">\n <button><span>更新书源<br>(Jsdelivr CDN)</span></button>\n </a></div></td>\n \n <td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://cdn.jsdelivr.net/gh/DowneyRem/PixivSource@main/btsrk.json\">\n <button><span>更新订阅<br>(Jsdelivr CDN)</span></button>\n </a></div></td>\n </tr>\n \n <tr><td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://raw.githubusercontent.com/DowneyRem/PixivSource/main/${sourceName}.json\">\n <button><span>书源链接<br>(GitHub)</span></button>\n </a></div></td>\n \n <td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://raw.githubusercontent.com/DowneyRem/PixivSource/main/btsrk.json\">\n <button><span>订阅链接<br>(GitHub)</span></button>\n </a></div></td>\n </tr>\n \n <tr><td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://codeberg.org/DowneyRem/PixivSource/raw/branch/main/${sourceName}.json\">\n <button><span>备用书源链接<br>(Codeberg)</span></button>\n </a></div></td>\n \n <td><div class=\"ann\">\n <a href=\"legado://import/importonline?src=https://codeberg.org/DowneyRem/PixivSource/raw/branch/main/btsrk.json\">\n <button><span>备用订阅链接<br>(Codeberg)</span></button>\n </a></div></td>\n </tr>\n </table>\n</body>\n</html>`\n java.startBrowser(`data:text/html;charset=utf-8;base64, ${java.base64Encode(htm)}`, '更新书源')\n return []\n}",
"lastUpdateTime": 1775232000251,
"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\n// 检测 源阅\n// 可用 java.ajax() 不可用 java.webview() java.ajaxAll()\n// 可用 java.getCookie() cache.put() cache.get() 默认值为 undefined\n// 可用 java.startBrowser() 不可用 java.startBrowserAwaitAwait\n// 可用 source.bookSourceName source.getVariable() source.setVariable()等\n// java.getUserAgent() java.getWebViewUA() 目前返回内容相同\nfunction isSourceRead() {\n return java.getUserAgent() === java.getWebViewUA()\n}\n// 正式版 不支持在 JSlib 的函数直接设置默认参数\n// 正式版 不支持 a?.b 的写法\n// 检测 阅读 正式版 与 Beta 版本\nfunction isLegadoOfficial() {\n let isLegadoOfficialStatus\n try {\n eval('({})?.value')\n isLegadoOfficialStatus = false\n } catch (e) {\n isLegadoOfficialStatus = true\n }\n return isLegadoOfficialStatus\n}\n// 检测 阅读 Beta 版本 与 LYC 版本\n// LYC 版本新增函数\n// java.ajaxTestAll()\n// java.openVideoPlayer(url: String, title: String, float: Boolean)\n// cookie.setWebCookie(url,cookie)\n// source.refreshExplore()\n// source.refreshJSLib()\nfunction isLegadoLYC() {\n return typeof java.ajaxTestAll === \"function\"\n}\n\nfunction publicFunc() {\n let u = {}\n // 输出书源信息\n java.log(`${source.bookSourceComment.split(\"\\n\")[0]}`)\n java.log(`📌 ${source.bookSourceComment.split(\"\\n\")[2]}`)\n java.log(`📆 更新时间:${java.timeFormat(source.lastUpdateTime)}`)\n\n if (isSourceRead()) {\n java.log(\"📱 软件平台:🍎 源阅 SourceRead\")\n } else if (isLegadoOfficial()) {\n java.log(\"📱 软件平台:🤖 阅读 正式版\")\n sleepToast(\"\\n⚠️当前软件为:阅读【正式版】\\n【正式版】已年久失修,不推荐继续使用\\n\\n为了更好的使用体验,请用:\\n【阅读 Plus】或【阅读 Beta 新包名】\\n\\n即将为您打开【阅读 Plus】下载界面\")\n sleep(3); startBrowser(\"https://loyc.xyz/c/legado.html#download\", \"下载阅读 Plus\")\n\n } else {\n if (isLegadoLYC()) {\n java.log(\"📱 软件平台:🤖 阅读 Beta【新包名】/ 阅读 Plus\")\n } else {\n java.log(\"📱 软件平台:🤖 阅读 Beta【原包名】\")\n sleepToast(\"\\n⚠️当前软件为:阅读 Beta【原包名】\\n\\n为了更好的使用体验,请用:\\n【阅读 Plus】或【阅读 Beta 新包名】\\n\\n即将为您打开【阅读 Plus】下载界面\")\n sleep(3); startBrowser(\"https://loyc.xyz/c/legado.html#download\", \"下载阅读 Plus\")\n }\n }\n\n // 设置初始化\n // cache.delete(\"linpxSettings\")\n settings = getFromCacheObject(\"linpxSettings\")\n if (settings) {\n java.log(\"⚙️ 使用自定义设置\")\n } else {\n java.log(\"⚙️ 使用默认设置\")\n settings = setDefaultSettings()\n }\n u.settings = settings\n putInCacheObject(\"FNSettings\", settings) // 设置写入缓存\n\n u.environment = {}\n u.environment.IS_SOURCEREAD = isSourceRead()\n u.environment.IS_LEGADO = !isSourceRead()\n u.environment.IS_LYC_BRUNCH = isLegadoLYC()\n putInCacheObject(\"pixivEnvironment\", u.environment) // 设置写入缓存\n\n if (u.DEBUG === true) {\n java.log(JSON.stringify(settings, null, 4))\n java.log(`DEBUG = ${u.DEBUG}`)\n }\n u.debugFunc = (func) => {\n if (util.settings.DEBUG) {\n func()\n }\n }\n\n u.getNovels = function () {\n if (JSON.parse(result).code === 200 && JSON.parse(result).count > 0) {\n return JSON.parse(result).data\n } else {\n return []\n }\n }\n\n u.handNovels = function (novels) {\n novels.forEach(novel =>{\n // novel.id = novel.id\n novel.title = novel.name\n // novel.tags = novel.tags\n novel.userName = novel.author.name\n // novel.userId = novel.author.id\n novel.textCount = null\n if (novel.latest_chapters === undefined) {\n novel.latestChapter = null\n novel.detailedUrl = urlNovelDetail(novel.id)\n } else {\n novel.latestChapter = novel.latest_chapters[0].name\n novel.detailedUrl = urlNovelUrl(novel.id)\n }\n novel.description = novel.desc\n novel.coverUrl = novel.cover\n novel.sourceUrl = urlSourceUrl(novel.source, novel.ext_data.oneshot, novel.source_id)\n\n novel.createDate = novel.created_at\n novel.updateDate = novel.updated_at\n novel.syncDate = novel.fetched_at\n // novel.status = novel.status\n // if (novel.status !== \"publish\") { // suspend\n // java.log(urlNovelUrl(novel.id))\n // java.log(novel.sourceUrl)\n // }\n })\n return novels\n }\n\n u.formatNovels = function (novels) {\n novels.forEach(novel => {\n novel.title = novel.title.trim()\n if (!novel.userName.startsWith(\"@\")) novel.userName = `@${novel.userName}`\n novel.tags2 = []\n for (let i in novel.tags) {\n let tag = novel.tags[i]\n if (tag.includes(\"/\")) {\n let tags = tag.split(\"/\")\n novel.tags2 = novel.tags2.concat(tags)\n } else {\n novel.tags2.push(tag)\n }\n }\n novel.tags = Array.from(new Set(novel.tags2))\n novel.tags = novel.tags.join(\",\")\n novel.createDate = dateFormat(novel.createDate)\n novel.updateDate = dateFormat(novel.updateDate)\n novel.syncDate = dateFormat(novel.syncDate)\n if (util.settings.MORE_INFORMATION) {\n novel.description = `\\n书名:${novel.title}\\n作者:${novel.userName}\\n标签:${novel.tags}\\n上传:${novel.createDate}\\n更新:${novel.updateDate}\\n同步:${novel.syncDate}\\n简介:${novel.description}`\n } else {\n novel.description = `\\n${novel.description}\\n上传时间:${novel.createDate}\\n更新时间:${novel.updateDate}\\n同步时间:${novel.syncDate}`\n }\n })\n return novels\n }\n\n u.getNovelRes = function (result, type) {\n let res = {data: []}, chapterId = 0\n let isHtml = result.startsWith(\"<!DOCTYPE html>\")\n let pattern = \"(https?://)?(www\\\\.)?furrynovel\\\\.com/(zh|en|ja)/novel/\\\\d+(/chapter/d+)?\"\n let fnWebpage = baseUrl.match(new RegExp(pattern))\n\n if (isHtml && fnWebpage) {\n let novelId = baseUrl.match(new RegExp(\"\\\\d+\"))[0]\n if (type === \"detail\") {\n res = getAjaxJson(urlNovelDetail(novelId))\n } else if (type === \"catalog\") {\n res = getAjaxJson(urlNovelChapterInfo(novelId))\n } else if (type === \"content\") {\n try {\n chapterId = baseUrl.match(RegExp(/\\/(\\d+)\\/chapter\\/(\\d+)/))[2]\n } catch (e) {\n chapterId = getAjaxJson(urlNovelChapterInfo(novelId)).data[0].id\n } finally {\n res = getAjaxJson(urlNovelChapterDetail(novelId, chapterId))\n }\n }\n } else {\n res = JSON.parse(result)\n }\n if (res.data.length === 0) {\n java.log(`无法从 FurryNovel 获取当前小说`)\n java.log(JSON.stringify(res))\n }\n return res.data\n }\n\n util = u\n java.put(\"util\", objStringify(u))\n}\n\npublicFunc()\njava.getStrResponse(null, null)",
"loginUi": "@js:\nlet source = [\n {\"🆙 更新书源\": \"updateSource()\" },\n {\"🔰 使用指南\": \"startGithubReadme()\" },\n {\"🐞 反馈问题\": \"startGithubIssue()\" },\n]\n\nlet novel = [\n {\"章节名称\": \"text\" },\n {\"⤴️ 分享章节\": \"shareFactory('novel')\" },\n {\"⤴️ 分享作者\": \"shareFactory('author')\" },\n {\"🅿️ 分享小说\": \"shareFactory('pixiv')\" },\n]\n\nlet settings = [\n {\"书源设置\": \"text\" },\n {\"⚙️ 当前设置\": \"showSettings()\" },\n {\"🔧 默认设置\": \"setDefaultSettingsLoginUrl()\" },\n {\"🀄 🚫 繁简通搜\": \"editSettings('CONVERT_CHINESE')\" },\n\n {\"🚫 📖 更多简介\": \"editSettings('MORE_INFORMATION')\" },\n {\"🔗 🚫 原始链接\": \"editSettings('SHOW_ORIGINAL_LINK')\" },\n {\"📚 🚫 恢复《》\": \"editSettings('REPLACE_TITLE_MARKS')\" },\n]\n\n// 添加按钮\nlet li = source\ntry {\n if (book) li = li.concat(novel)\n} catch (e) {}\nli = li.concat(settings)\n\n// 处理按钮\nli.forEach(item => {\n item.name = Object.keys(item)[0]\n let list = item.name.split(\" \")\n if (list.length === 1 ) {\n item.type = \"text\"\n } else if (list.length === 2) {\n item.type = \"button\"\n item.action = Object.values(item)[0]\n } else {\n item.name = list[list.length - 1]\n item.type = \"toggle\"\n item.default = `${list[0]} `\n list.length = list.length - 1\n item.chars = list.map(char => `${char} `)\n item.action = Object.values(item)[0]\n }\n delete item[Object.keys(item)[0]]\n // 添加格式\n if (item.type === \"button\" || item.type === \"toggle\") {\n item.style = {}\n item.style.layout_flexGrow = 1\n item.style.layout_flexBasisPercent = -1\n }\n})\n\nJSON.stringify(li)",
"loginUrl": "function login() {}\n\nfunction getNovel() {\n let novel = {}\n novel.bookId = book.bookUrl.match(/\\d+/)[0]\n novel.bookName = book.name\n novel.userName = book.author.replace(\"@\", \"\")\n\n let resp = getAjaxJson(urlNovelDetail(novel.bookId)).data\n novel.sourceUrl = urlSourceUrl(resp.source, resp.ext_data.oneshot, resp.source_id)\n sleepToast(JSON.stringify(novel, null, 4))\n return novel\n}\n\nfunction shareFactory(type) {\n let novel = getNovel()\n if (!novel) return sleepToast(\"🔰 功能提示\\n\\n⚠️ 请在小说阅读页面,使用本功能\")\n if (type.includes(\"author\")) {\n sleepToast(\"\\n\\n已复制当前作者链接\", 1)\n java.copyText(urlUserUrl(novel.userName))\n // startBrowser(urlUserUrl(novel.userName), novel.userName)\n }\n else if (type.includes(\"novel\")) {\n sleepToast(\"\\n\\n已复制当前小说详情链接\", 1)\n java.copyText(urlNovelUrl(novel.bookId))\n // startBrowser(urlNovelUrl(novel.bookId), novel.bookName)\n }\n else if (type.includes(\"pixiv\")) {\n sleepToast(\"\\n\\n已复制当前小说 Pixiv 链接\", 1)\n java.copyText(novel.sourceUrl)\n // startBrowser(novel.sourceUrl, novel.bookName)\n }\n}\n\nfunction startGithubReadme() {\n startBrowser(\"https://pixivsource.pages.dev/FurryNovel\", \"使用指南\")\n}\nfunction startGithubIssue() {\n startBrowser(\"https://github.com/DowneyRem/PixivSource/issues\", \"反馈问题\")\n}\n\nfunction checkStatus(status) {\n if (eval(String(status)) === true) return \"❤️\"\n else return \"🖤\"\n}\n\nlet settingsName = {\n \"CONVERT_CHINESE\": \"🀄️ 繁简通搜\",\n \"MORE_INFORMATION\": \"📖 更多简介\",\n \"SHOW_ORIGINAL_LINK\": \"🔗 原始链接\",\n \"REPLACE_TITLE_MARKS\": \"📚 恢复《》\",\n \"DEBUG\": \"🐞 调试模式\",\n}\n\nfunction statusMsg(status) {\n if (status === true) return \"✅ 已开启\"\n else if (status === false) return \"🚫 已关闭\"\n else return \"🈚️ 未设置\"\n}\n\n// 检测快速模式修改的4个设置\nfunction getSettingStatus(mode) {\n if (mode === undefined) mode = \"\"\n let keys = [], msgList = []\n let settings = getFromCacheObject(\"FNSettings\")\n keys = Object.keys(settingsName)\n for (let i in keys) {\n msgList.push(`${statusMsg(settings[keys[i]])} ${settingsName[keys[i]]}`)\n }\n return msgList.join(\"\\n\").trim()\n}\n\nfunction showSettings() {\n sleepToast(`\\n⚙️ 当前设置\\n\\n${getSettingStatus()}`)\n}\n\nfunction setDefaultSettingsLoginUrl() {\n setDefaultSettings()\n sleepToast(`\\n✅ 已恢复 🔧 默认设置\\n\\n${getSettingStatus()}`)\n}\n\nfunction editSettings(settingName) {\n let msg, status\n let settings = getFromCacheObject(\"FNSettings\")\n if (!settings) settings = setDefaultSettings()\n if (!!settings[settingName]) {\n status = settings[settingName] = !settings[settingName]\n } else {\n status = settings[settingName] = true\n }\n putInCacheObject(\"FNSettings\", settings)\n msg = `\\n\\n${statusMsg(status)} ${settingsName[settingName]}`\n sleepToast(msg)\n}",
"respondTime": 180000,
"ruleBookInfo": {
"author": "userName",
"canReName": "true",
"coverUrl": "coverUrl",
"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\nfunction novelHandler(novel) {\n novel = util.formatNovels(util.handNovels([novel]))[0]\n book.bookUrl = novel.detailedUrl = urlNovelUrl(novel.id)\n book.tocUrl = novel.catalogUrl = urlNovelChapterInfo(novel.id)\n return novel\n}\n\n(() => {\n return novelHandler(util.getNovelRes(result, \"detail\"))\n})();",
"intro": "description",
"kind": "tags",
"lastChapter": "latestChapter",
"name": "title",
"tocUrl": "catalogUrl",
"wordCount": "textCount"
},
"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\nfunction replaceUploadedImage(res, content) {\n // 获取 [uploadedimage:] 的图片链接\n let hasEmbeddedImages = content.match(RegExp(/\\[uploadedimage:(\\d+)-?(\\d+)]/gm))\n if (hasEmbeddedImages) {\n resp = getAjaxJson(urlLinpxNovelDetail(res.source_id))\n Object.keys(resp.images).forEach((key) => {\n content = content.replace(`[uploadedimage:${key}]`, `<img src=\"${urlLinpxCoverUrl(resp.images[key].origin)}\">`)\n })\n }\n return content\n}\nfunction replacePixivImage(content) {\n // 获取 [pixivimage:] 的图片链接 [pixivimage:1234] [pixivimage:1234-1]\n let matched = content.match(RegExp(/\\[pixivimage:(\\d+)-?(\\d+)]/gm))\n if (matched) {\n matched.forEach(pixivimage => {\n let matched2, illustId, order = 0\n if (pixivimage.includes(\"-\")) {\n matched2 = pixivimage.match(RegExp(\"(\\\\d+)-(\\\\d+)\"))\n illustId = matched2[1]; order = matched2[2]\n } else {\n matched2 = pixivimage.match(RegExp(\"\\\\d+\"))\n illustId = matched2[0];\n }\n if (urlIllustOriginal(illustId, order)) {\n content = content.replace(`${pixivimage}`, `<img src=\"${urlIllustOriginal(illustId, order)}\">`)\n } else {\n content = content.replace(`${pixivimage}`, ``)\n }\n })\n }\n return content\n}\nfunction replaceNewPage(content) {\n // 替换 Pixiv 分页标记符号 [newpage]\n if (!util.environment.IS_LYC_BRUNCH) {\n let matched = content.match(RegExp(/[ ]*\\[newpage][ ]*/gm))\n if (matched) {\n for (let i in matched) {\n content = content.replace(`${matched[i]}`, `${\"<p><p/>\".repeat(3)}`)\n }\n }\n }\n return content\n}\nfunction replaceChapter(content) {\n // 替换 Pixiv 章节标记符号 [chapter:]\n let 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 // 替换 Pixiv 分页标记符号 [newpage]\n if (util.environment.IS_LYC_BRUNCH) {\n content = content.replace(`${matched[i]}`, `<usehtml><h3>${chapter}</h3></usehtml>`)\n } else {\n content = content.replace(`${matched[i]}`, `${chapter}<p><p/>`)\n }\n }\n }\n return content\n}\nfunction replaceJumpPage(content) {\n // 替换 Pixiv 跳转页面标记符号 [[jump:]]\n let matched = content.match(RegExp(/\\[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 return content\n}\nfunction replaceJumpUrl(content) {\n // 替换 Pixiv 链接标记符号 [[jumpuri: > ]]\n let matched = content.match(RegExp(/\\[\\[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 if (util.environment.IS_LYC_BRUNCH) {\n content = content.replace(`${matchedText}`, `<usehtml><p> <a href=${urlLink}>${urlName}</a></p></usehtml>`)\n } else {\n if (urlLink === urlName) {\n content = content.replace(`${matchedText}`, `${urlName}`)\n } else {\n content = content.replace(`${matchedText}`, `${urlName}: ${urlLink}`)\n }\n }\n }\n }\n return content\n}\nfunction replaceRb(content) {\n // 替换 Pixiv 注音标记符号 [[rb: > ]]\n let matched = content.match(RegExp(/\\[\\[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\n if (!util.settings.REPLACE_TITLE_MARKS) {\n // 默认替换成(括号)\n content = content.replace(`${matchedText}`, `${kanji}(${kana})`)\n } else {\n let reg = RegExp(\"[\\\\u4E00-\\\\u9FFF]+\", \"g\");\n if (reg.test(kana)) {\n // kana为中文,则替换回《书名号》\n content = content.replace(`${matchedText}`, `${kanji}《${kana}》`)\n } else {\n // 阅读不支持 <ruby> <rt> 注音\n // content = content.replace(`${matchedText}`, `<ruby>${kanji}<rt>${kana}</rt></ruby>`)\n content = content.replace(`${matchedText}`, `${kanji}(${kana})`)\n }\n }\n }\n }\n return content\n}\n\nfunction getContent(res) {\n let content = res.content\n // 替换 Pixiv 标记符\n content = replaceUploadedImage(res, content)\n content = replacePixivImage(content)\n content = replaceNewPage(content)\n content = replaceChapter(content)\n content = replaceJumpPage(content)\n content = replaceJumpUrl(content)\n content = replaceRb(content)\n return content\n}\n\n(function () {\n return getContent(util.getNovelRes(result, \"content\"))\n})()",
"imageStyle": "FULL",
"callBackJs": "function shareBook() {\n let text = `我正在看:【${book.author.replace(\"@\", \"\")}】创作的《${book.name}》`\n if (!!book.durChapterTitle && String(book.name) !== String(book.durChapterTitle)) {\n text += `的 【${book.durChapterTitle}】`\n }\n text += `\\n\\n小说链接:\\n${book.bookUrl}\\n\\n分享自【开源阅读】兽人控小说站书源。使用添加网址,快速添加本文`\n java.copyText(text)\n return true\n}\n\nfunction clearCache() {\n let novel = getNovel()\n cache.delete(urlNovelDetailed(novel.id))\n if (novel.seriesId) cache.delete(urlSeriesDetailed(novel.seriesId))\n return true\n}\n\nfunction copyBookUrl() {\n java.copyText(book.bookUrl)\n return true\n}\ncopyTocUrl = copyBookUrl\n\n// 保存阅读,更新登录界面的章节名称\nfunction saveRead() {\n source.putLoginInfo(JSON.stringify({\"章节名称\": book.durChapterTitle}))\n}\n\nfunction startShelfRefresh() {\n source.putConcurrent(\"18/30000\")\n}\n\nfunction endShelfRefresh() {\n source.putConcurrent(\"25/5000\")\n}\n\nfunction customButton(){\n java.open(\"login\")\n}\n\nfunction callBackFactory(event) {\n switch (event) {\n // case \"clickBookName\":\n // return clickBookName()\n // case \"longClickBookName\":\n // return longClickBookName()\n // case \"clickAuthor\":\n // return clickAuthor()\n // case \"longClickAuthor\":\n // return longClickAuthor()\n case \"clickCustomButton\":\n return customButton()\n // case \"longClickCustomButton\":\n // return longcustomButton()\n\n case \"clickShareBook\":\n return shareBook()\n case \"clickClearCache\":\n return clearCache()\n case \"clickCopyBookUrl\":\n return copyBookUrl()\n case \"clickCopyTocUrl\":\n return copyTocUrl()\n\n // 下面的事件无法被回调结果消费\n // case \"addBookShelf\":\n // return addBookShelf()\n // case \"delBookShelf\":\n // return delBookShelf()\n case \"saveRead\":\n return saveRead()\n // case \"startRead\":\n // return startRead()\n // case \"endRead\":\n // return endRead()\n case \"startShelfRefresh\":\n return startShelfRefresh()\n case \"endShelfRefresh\":\n return endShelfRefresh()\n }\n}\n\n(() => {\n return callBackFactory(event)\n})()"
},
"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\nfunction handlerFactory() {\n if (baseUrl.includes(\"https://cdn.jsdelivr.net\")) {\n return updateSource()\n }\n if (baseUrl.includes(\"furrynovel.com\")) {\n return util.formatNovels(util.handNovels(util.getNovels()))\n }\n else {\n return startBrowser(baseUrl, \"\")\n }\n}\n\n(() => {\n return handlerFactory()\n})()",
"bookUrl": "detailedUrl",
"coverUrl": "coverUrl",
"intro": "description",
"kind": "tags",
"lastChapter": "latestChapter",
"name": "title",
"wordCount": "textCount"
},
"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 search(name, page=1) {\n let resp = getAjaxJson(urlSearchNovel(name, page))\n java.log(urlSearchNovel(name, page))\n if (resp.code === 200 && resp.count > 0) {\n return resp.data\n } else {\n return []\n }\n}\n\nfunction getConvertNovels() {\n let novels = []\n let novelName = String(java.get(\"key\"))\n let name1 = String(java.s2t(novelName))\n let name2 = String(java.t2s(novelName))\n if (name1 !== novelName) novels = novels.concat(search(name1))\n if (name2 !== novelName) novels = novels.concat(search(name2))\n return novels\n}\n\n(() => {\n let novels = []\n novels = novels.concat(util.getNovels())\n if (util.settings.CONVERT_CHINESE) novels = novels.concat(getConvertNovels())\n novels = novels.sort((a, b) => a.source_id > b.source_id ? 1 : -1)\n // java.log(JSON.stringify(novels))\n // 返回空列表中止流程\n if (novels.length === 0) {\n return []\n }\n return util.formatNovels(util.handNovels(novels))\n})();",
"bookUrl": "detailedUrl",
"checkKeyWord": "#测试页面",
"coverUrl": "coverUrl",
"intro": "description",
"kind": "tags",
"lastChapter": "latestChapter",
"name": "title",
"wordCount": "textCount"
},
"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\nfunction urlNovelChapter(novelId, chapterId) {\n if (util.settings.SHOW_ORIGINAL_LINK) {\n return urlNovelChapterUrl(novelId, chapterId)\n } else {\n return urlNovelChapterDetail(novelId, chapterId)\n }\n}\n\nfunction novelHandler(novels) {\n novels.forEach(novel => {\n novel.chapterId = novel.id\n novel.novelId = baseUrl.match(RegExp(/\\d+/))[0]\n novel.detailedUrl = urlNovelUrl(novel.novelId)\n novel.chapterName = novel.title = novel.name\n novel.chapterUrl = urlNovelChapter(novel.novelId, novel.chapterId)\n novel.chapterInfo = `${novel.created_at} ${novel.text_count}字`\n })\n return novels\n}\n\n(function () {\n return novelHandler(util.getNovelRes(result, \"catalog\"))\n})()",
"chapterName": "title",
"chapterUrl": "chapterUrl",
"updateTime": "chapterInfo"
},
"searchUrl": "@js:\njava.put(\"key\", key)\njava.put(\"page\", page)\nif (key.startsWith(\"@\") || key.startsWith(\"@\")) {\n key = key.slice(1)\n java.log(`👤 搜索作者:${key}`)\n} else if (key.startsWith(\"#\") || key.startsWith(\"#\")) {\n key = key.slice(1)\n java.log(`#️⃣ 搜索标签:${key}`)\n} else {\n java.log(`🔍 搜索内容:${key}`)\n}\n\njava.put(\"key\", key)\nurlSearchNovel(key, page)",
"variableComment": "🔎 筛选发现:\n发现 - 长按\"兽人小说站\" - 编辑 - 右上角菜单 - 设置源变量\n设置源变量:输入想要搜索/筛选的标签,以空格间隔(或一行一个),保存\n发现 - 长按\"兽人小说站\" - 刷新 - 查看他人收藏\n以下内容为源变量模板:\n中文 原创 纯爱\n\n\n⚙️ 书源设置:\n设置:打开小说 - 菜单 - 登录 - 点击下方按钮\n\n",
"weight": 0
}
]