├── .gitignore
├── README.md
├── index.json
├── mh18.js
├── goda.js
├── comic_walker.js
├── manwaba.js
├── lanraragi.js
├── baozi.js
├── zaimanhua.js
├── shonen_jump_plus.js
├── mh1234.js
├── ikmmh.js
├── mxs.js
├── komiic.js
└── komga.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | test/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # venera-configs
2 |
3 | Configuration file repository for venera
4 |
5 | ## Create a new configuration
6 |
7 | 1. Download `_template_.js`, `_venera_.js`, put them in the same directory
8 | 2. Rename `_template_.js` to `your_config_name.js`
9 | 3. Edit `your_config_name.js` to your needs.
10 | - The `_template_.js` file contains comments to help you with that.
11 | - The `_venera_.js` is used for code completion in your IDE.
--------------------------------------------------------------------------------
/index.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "拷贝漫画",
4 | "fileName": "copy_manga.js",
5 | "key": "copy_manga",
6 | "version": "1.4.0"
7 | },
8 | {
9 | "name": "Komiic",
10 | "fileName": "komiic.js",
11 | "key": "Komiic",
12 | "version": "1.0.3"
13 | },
14 | {
15 | "name": "包子漫画",
16 | "fileName": "baozi.js",
17 | "key": "baozi",
18 | "version": "1.1.3"
19 | },
20 | {
21 | "name": "Picacg",
22 | "fileName": "picacg.js",
23 | "key": "picacg",
24 | "version": "1.0.5"
25 | },
26 | {
27 | "name": "nhentai",
28 | "fileName": "nhentai.js",
29 | "key": "nhentai",
30 | "version": "1.0.6"
31 | },
32 | {
33 | "name": "紳士漫畫",
34 | "fileName": "wnacg.js",
35 | "key": "wnacg",
36 | "version": "1.0.4",
37 | "description": "紳士漫畫漫畫源, 不能使用時請嘗試更換URL"
38 | },
39 | {
40 | "name": "ehentai",
41 | "fileName": "ehentai.js",
42 | "key": "ehentai",
43 | "version": "1.1.8"
44 | },
45 | {
46 | "name": "禁漫天堂",
47 | "fileName": "jm.js",
48 | "key": "jm",
49 | "version": "1.3.1",
50 | "description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流"
51 | },
52 | {
53 | "name": "MangaDex",
54 | "fileName": "manga_dex.js",
55 | "key": "manga_dex",
56 | "version": "1.1.0",
57 | "description": "Account feature is not supported yet."
58 | },
59 | {
60 | "name": "爱看漫",
61 | "fileName": "ikmmh.js",
62 | "key": "ikmmh",
63 | "version": "1.0.5"
64 | },
65 | {
66 | "name": "少年ジャンプ+",
67 | "fileName": "shonen_jump_plus.js",
68 | "key": "shonen_jump_plus",
69 | "version": "1.1.1"
70 | },
71 | {
72 | "name": "hitomi.la",
73 | "fileName": "hitomi.js",
74 | "key": "hitomi",
75 | "version": "1.1.2"
76 | },
77 | {
78 | "name": "comick",
79 | "fileName": "comick.js",
80 | "key": "comick",
81 | "version": "1.2.0"
82 | },
83 | {
84 | "name": "优酷漫画",
85 | "fileName": "ykmh.js",
86 | "key": "ykmh",
87 | "version": "1.0.0"
88 | },
89 | {
90 | "name": "再漫画",
91 | "fileName": "zaimanhua.js",
92 | "key": "zaimanhua",
93 | "version": "1.0.2"
94 | },
95 | {
96 | "name": "漫画柜",
97 | "fileName": "manhuagui.js",
98 | "key": "ManHuaGui",
99 | "version": "1.2.1"
100 | },
101 | {
102 | "name": "漫蛙吧",
103 | "fileName": "manwaba.js",
104 | "key": "manwaba",
105 | "version": "1.0.2"
106 | },
107 | {
108 | "name": "Lanraragi",
109 | "fileName": "lanraragi.js",
110 | "key": "lanraragi",
111 | "version": "1.1.0"
112 | },
113 | {
114 | "name": "Komga",
115 | "fileName": "komga.js",
116 | "key": "komga",
117 | "version": "1.0.0"
118 | },
119 | {
120 | "name": "カドコミ",
121 | "fileName": "comic_walker.js",
122 | "key": "comic_walker",
123 | "version": "1.0.0"
124 | },
125 | {
126 | "name": "漫画1234",
127 | "fileName": "mh1234.js",
128 | "key": "mh1234",
129 | "version": "1.0.0"
130 | },
131 | {
132 | "name": "CCC追漫台",
133 | "fileName": "ccc.js",
134 | "key": "ccc",
135 | "version": "1.0.1"
136 | },
137 | {
138 | "name": "GoDa漫画",
139 | "fileName": "goda.js",
140 | "key": "goda",
141 | "version": "1.0.0"
142 | },
143 | {
144 | "name": "18漫画",
145 | "fileName": "mh18.js",
146 | "key": "mh18",
147 | "version": "1.0.0"
148 | },
149 | {
150 | "name": "漫小肆",
151 | "fileName": "mxs.js",
152 | "key": "mxs",
153 | "version": "1.0.0"
154 | },
155 | {
156 | "name": "漫画人",
157 | "fileName": "manhuaren.js",
158 | "key": "manhuaren",
159 | "version": "1.0.0"
160 | }
161 | ]
--------------------------------------------------------------------------------
/mh18.js:
--------------------------------------------------------------------------------
1 | /** @type {import('./_venera_.js')} */
2 | class MH18 extends ComicSource {
3 | // Note: The fields which are marked as [Optional] should be removed if not used
4 |
5 | // name of the source
6 | name = "18漫画"
7 |
8 | // unique id of the source
9 | key = "mh18"
10 |
11 | version = "1.0.0"
12 |
13 | minAppVersion = "1.4.0"
14 |
15 | // update url
16 | url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/mh18.js"
17 |
18 | settings = {
19 | domains: {
20 | title: "域名",
21 | type: "input",
22 | default: "18mh.org"
23 | }
24 | }
25 |
26 | get baseUrl() {
27 | return `https://${this.loadSetting("domains")}`;
28 | }
29 |
30 | get headers() {
31 | return {
32 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
33 | "Referer": this.baseUrl
34 | };
35 | }
36 |
37 | parseComics(doc) {
38 | console.warn(doc)
39 | const result = [];
40 | for (let item of doc.querySelectorAll(".pb-2")) {
41 | result.push(new Comic({
42 | id: item.querySelector("a").attributes["href"],
43 | title: item.querySelector("h3").text,
44 | cover: item.querySelector("img").attributes["src"]
45 | }))
46 | }
47 | return result;
48 | }
49 |
50 | // explore page list
51 | explore = [
52 | {
53 | // title of the page.
54 | // title is used to identify the page, it should be unique
55 | title: this.name,
56 |
57 | /// multiPartPage or multiPageComicList or mixed
58 | type: "multiPartPage",
59 |
60 | load: async () => {
61 | const res = await Network.get(this.baseUrl, this.headers);
62 | const document = new HtmlDocument(res.body);
63 | const result = [{ title: "近期更新", comics: [], viewMore: null }];
64 | for (let item of document.querySelector(".pb-unit-md").querySelectorAll(".slicarda")) {
65 | result[0].comics.push(new Comic({
66 | id: item.attributes["href"],
67 | title: item.querySelector("h3").text,
68 | cover: item.querySelector("img").attributes["src"]
69 | }))
70 | }
71 | const cardlists = document.querySelectorAll(".cardlist");
72 | const hometitles = document.querySelectorAll(".hometitle");
73 | for (let i = 0; i < hometitles.length; i++) {
74 | result.push({
75 | title: hometitles[i].querySelector("h2").text,
76 | comics: this.parseComics(cardlists[i]),
77 | viewMore: {
78 | page: "category",
79 | attributes: {
80 | category: hometitles[i].querySelector("h2").text,
81 | param: hometitles[i].attributes["href"]
82 | },
83 | }
84 | });
85 | }
86 | return result;
87 | }
88 | }
89 | ]
90 |
91 | // categories
92 | category = {
93 | /// title of the category page, used to identify the page, it should be unique
94 | title: this.name,
95 | parts: [
96 | {
97 | name: "类型",
98 | type: "fixed",
99 | categories: [
100 | "全部",
101 | "韓漫",
102 | "真人寫真",
103 | "日漫",
104 | "AI寫真",
105 | "熱門漫畫"
106 | ],
107 | itemType: "category",
108 | categoryParams: [
109 | "/manga",
110 | "/manga-genre/hanman",
111 | "/manga-genre/zhenrenxiezhen",
112 | "/manga-genre/riman",
113 | "/manga-genre/aixiezhen",
114 | "/manga-genre/hots"
115 | ],
116 | },
117 | {
118 | name: "标签",
119 | type: "fixed",
120 | categories: [
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 | "暧昧",
147 | "写真",
148 | "女神",
149 | "大尺度",
150 | "纯情警察"
151 | ],
152 | itemType: "category",
153 | categoryParams: [
154 | "/manga-tag/duoren",
155 | "/manga-tag/yuwang",
156 | "/manga-tag/zhengmei",
157 | "/manga-tag/tongju",
158 | "/manga-tag/nxuesheng",
159 | "/manga-tag/juqing",
160 | "/manga-tag/touqing",
161 | "/manga-tag/xiaoyuan",
162 | "/manga-tag/nixi",
163 | "/manga-tag/bangongshi",
164 | "/manga-tag/youhuo",
165 | "/manga-tag/fanzhuan",
166 | "/manga-tag/shun",
167 | "/manga-tag/renqi",
168 | "/manga-tag/chulian",
169 | "/manga-tag/shaofu",
170 | "/manga-tag/ciji",
171 | "/manga-tag/ndaxuesheng",
172 | "/manga-tag/zhiliao",
173 | "/manga-tag/chaonengli",
174 | "/manga-tag/langmanxiaoyuan",
175 | "/manga-tag/xiju",
176 | "/manga-tag/xuejie",
177 | "/manga-tag/daxuesheng",
178 | "/manga-tag/yongyi",
179 | "/manga-tag/aimei",
180 | "/manga-tag/xiezhen",
181 | "/manga-tag/nshen",
182 | "/manga-tag/dachidu",
183 | "/manga-tag/chunqingjingcha"
184 | ],
185 | }
186 | ],
187 | // enable ranking page
188 | enableRankingPage: false,
189 | }
190 |
191 | /// category comic loading related
192 | categoryComics = {
193 | load: async (category, params, options, page) => {
194 | const res = await Network.get(`${this.baseUrl}${params}/page/${page}`, this.headers);
195 | if (res.status !== 200) {
196 | throw `Invalid status code: ${res.status}`;
197 | }
198 | const document = new HtmlDocument(res.body);
199 | let maxPage = null;
200 | try {
201 | maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
202 | } catch (_) {
203 | maxPage = 1;
204 | }
205 | return {
206 | comics: this.parseComics(document),
207 | maxPage: maxPage
208 | };
209 | }
210 | }
211 |
212 | /// search related
213 | search = {
214 | load: async (keyword, options, page) => {
215 | const res = await Network.get(`${this.baseUrl}/s/${keyword}?page=${page}`);
216 | if (res.status !== 200) {
217 | throw `Invalid status code: ${res.status}`;
218 | }
219 | const document = new HtmlDocument(res.body);
220 | let maxPage = null;
221 | try {
222 | maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
223 | } catch (_) {
224 | maxPage = 1;
225 | }
226 | return {
227 | comics: this.parseComics(document),
228 | maxPage: maxPage
229 | };
230 | },
231 | // enable tags suggestions
232 | enableTagsSuggestions: false,
233 | }
234 |
235 | /// single comic related
236 | comic = {
237 | onThumbnailLoad: (url) => {
238 | return {
239 | headers: this.headers
240 | }
241 | },
242 | loadInfo: async (id) => {
243 | if (!id.startsWith("http")) {
244 | id = this.baseUrl + id;
245 | }
246 | const res = await Network.get(id);
247 | if (res.status !== 200) {
248 | throw `Invalid status code: ${res.status}`;
249 | }
250 | const document = new HtmlDocument(res.body);
251 | const title = document.querySelector(".text-xl").text.trim().split(" ")[0]
252 | const cover = document.querySelector(".object-cover").attributes["src"];
253 | const description = document.querySelector("p.text-medium").text;
254 | const infos = document.querySelectorAll("div.py-1");
255 | const tags = { "作者": [], "类型": [], "标签": [] };
256 | for (let author of infos[0].querySelectorAll("a > span")) {
257 | let author_name = author.text.trim();
258 | if (author_name.endsWith(",")) {
259 | author_name = author_name.slice(0, -1).trim();
260 | }
261 | tags["作者"].push(author_name);
262 | }
263 | for (let category of infos[1].querySelectorAll("a > span")) {
264 | let category_name = category.text.trim();
265 | if (category_name.endsWith(",")) {
266 | category_name = category_name.slice(0, -1).trim();
267 | }
268 | tags["类型"].push(category_name);
269 | }
270 | for (let tag of infos[2].querySelectorAll("a")) {
271 | tags["标签"].push(tag.text.replace("\n", "").replaceAll(" ", "").replace("#", ""));
272 | }
273 | const mangaId = document.querySelector("#mangachapters").attributes["data-mid"];
274 | const chapterRes = await Network.get(`${this.baseUrl}/manga/get?mid=${mangaId}&mode=all&t=${Date.now()}`, this.headers);
275 | const chapterDoc = new HtmlDocument(chapterRes.body);
276 | const chapters = {};
277 | for (let ch of chapterDoc.querySelectorAll(".chapteritem")) {
278 | const info = ch.querySelector("a");
279 | chapters[`${info.attributes["data-ms"]}@${info.attributes["data-cs"]}`] = ch.querySelector(".chaptertitle").text;
280 | }
281 | const recommend = [];
282 | for (let item of document.querySelectorAll("div.cardlist > div.pb-2")) {
283 | recommend.push(new Comic({
284 | id: item.querySelector("a").attributes["href"],
285 | title: item.querySelector("h3").text,
286 | cover: item.querySelector("img").attributes["src"]
287 | }));
288 | }
289 | return new ComicDetails({
290 | title: title,
291 | cover: cover,
292 | description: description,
293 | tags: tags,
294 | chapters: chapters,
295 | recommend: recommend,
296 | });
297 | },
298 |
299 | loadEp: async (comicId, epId) => {
300 | const ids = epId.split("@");
301 | const res = await Network.get(`${this.baseUrl}/chapter/getcontent?m=${ids[0]}&c=${ids[1]}`, this.headers);
302 | if (res.status !== 200) {
303 | throw `Invalid status code: ${res.status}`;
304 | }
305 | const document = new HtmlDocument(res.body);
306 | const images = [];
307 | for (let i of document.querySelector("#chapcontent").querySelectorAll("img")) {
308 | images.push(i.attributes["data-src"] ? i.attributes["data-src"] : i.attributes["src"]);
309 | }
310 | return { images };
311 | },
312 |
313 | // enable tags translate
314 | enableTagsTranslate: false,
315 | }
316 | }
--------------------------------------------------------------------------------
/goda.js:
--------------------------------------------------------------------------------
1 | /** @type {import('./_venera_.js')} */
2 | class Goda extends ComicSource {
3 | // Note: The fields which are marked as [Optional] should be removed if not used
4 |
5 | // name of the source
6 | name = "GoDa漫画"
7 |
8 | // unique id of the source
9 | key = "goda"
10 |
11 | version = "1.0.0"
12 |
13 | minAppVersion = "1.4.0"
14 |
15 | // update url
16 | url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/goda.js"
17 |
18 | settings = {
19 | domains: {
20 | title: "域名",
21 | type: "input",
22 | default: "godamh.com"
23 | },
24 | api: {
25 | title: "API域名",
26 | type: "input",
27 | default: "api-get-v3.mgsearcher.com"
28 | },
29 | image: {
30 | title: "图片域名",
31 | type: "input",
32 | default: "t40-1-4.g-mh.online"
33 | }
34 | }
35 |
36 | get baseUrl() {
37 | return `https://${this.loadSetting("domains")}`;
38 | }
39 |
40 | get apiUrl() {
41 | return `https://${this.loadSetting("api")}/api`;
42 | }
43 |
44 | get imageUrl() {
45 | return `https://${this.loadSetting("image")}`;
46 | }
47 |
48 | get headers() {
49 | return {
50 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
51 | "Referer": this.baseUrl
52 | };
53 | }
54 |
55 | parseComics(doc) {
56 | console.warn(doc)
57 | const result = [];
58 | for (let item of doc.querySelectorAll(".pb-2")) {
59 | result.push(new Comic({
60 | id: item.querySelector("a").attributes["href"],
61 | title: item.querySelector("h3").text,
62 | cover: item.querySelector("img").attributes["src"]
63 | }))
64 | }
65 | return result;
66 | }
67 |
68 | // explore page list
69 | explore = [
70 | {
71 | // title of the page.
72 | // title is used to identify the page, it should be unique
73 | title: this.name,
74 |
75 | /// multiPartPage or multiPageComicList or mixed
76 | type: "multiPartPage",
77 |
78 | load: async () => {
79 | const res = await Network.get(this.baseUrl, this.headers);
80 | const document = new HtmlDocument(res.body);
81 | const result = [{ title: "近期更新", comics: [], viewMore: null }];
82 | for (let item of document.querySelector(".pb-unit-md").querySelectorAll(".slicarda")) {
83 | result[0].comics.push(new Comic({
84 | id: item.attributes["href"],
85 | title: item.querySelector("h3").text,
86 | cover: item.querySelector("img").attributes["src"]
87 | }))
88 | }
89 | const cardlists = document.querySelectorAll(".cardlist");
90 | const hometitles = document.querySelectorAll(".hometitle");
91 | for (let i = 0; i < hometitles.length; i++) {
92 | result.push({
93 | title: hometitles[i].querySelector("h2").text,
94 | comics: this.parseComics(cardlists[i]),
95 | viewMore: {
96 | page: "category",
97 | attributes: {
98 | category: hometitles[i].querySelector("h2").text,
99 | param: hometitles[i].attributes["href"]
100 | },
101 | }
102 | });
103 | }
104 | return result;
105 | }
106 | }
107 | ]
108 |
109 | // categories
110 | category = {
111 | /// title of the category page, used to identify the page, it should be unique
112 | title: this.name,
113 | parts: [
114 | {
115 | name: "类型",
116 | type: "fixed",
117 | categories: [
118 | "全部",
119 | "韩漫",
120 | "热门漫画",
121 | "国漫",
122 | "其他",
123 | "日漫",
124 | "欧美"
125 | ],
126 | itemType: "category",
127 | categoryParams: [
128 | "/manga",
129 | "/manga-genre/kr",
130 | "/manga-genre/hots",
131 | "/manga-genre/cn",
132 | "/manga-genre/qita",
133 | "/manga-genre/jp",
134 | "/manga-genre/ou-mei"
135 | ],
136 | },
137 | {
138 | name: "标签",
139 | type: "fixed",
140 | categories: [
141 | "复仇",
142 | "古风",
143 | "奇幻",
144 | "逆袭",
145 | "异能",
146 | "宅向",
147 | "穿越",
148 | "热血",
149 | "纯爱",
150 | "系统",
151 | "重生",
152 | "冒险",
153 | "灵异",
154 | "大女主",
155 | "剧情",
156 | "恋爱",
157 | "玄幻",
158 | "女神",
159 | "科幻",
160 | "魔幻",
161 | "推理",
162 | "猎奇",
163 | "治愈",
164 | "都市",
165 | "异形",
166 | "青春",
167 | "末日",
168 | "悬疑",
169 | "修仙",
170 | "战斗"
171 | ],
172 | itemType: "category",
173 | categoryParams: [
174 | "/manga-tag/fuchou",
175 | "/manga-tag/gufeng",
176 | "/manga-tag/qihuan",
177 | "/manga-tag/nixi",
178 | "/manga-tag/yineng",
179 | "/manga-tag/zhaixiang",
180 | "/manga-tag/chuanyue",
181 | "/manga-tag/rexue",
182 | "/manga-tag/chunai",
183 | "/manga-tag/xitong",
184 | "/manga-tag/zhongsheng",
185 | "/manga-tag/maoxian",
186 | "/manga-tag/lingyi",
187 | "/manga-tag/danvzhu",
188 | "/manga-tag/juqing",
189 | "/manga-tag/lianai",
190 | "/manga-tag/xuanhuan",
191 | "/manga-tag/nvshen",
192 | "/manga-tag/kehuan",
193 | "/manga-tag/mohuan",
194 | "/manga-tag/tuili",
195 | "/manga-tag/lieqi",
196 | "/manga-tag/zhiyu",
197 | "/manga-tag/doushi",
198 | "/manga-tag/yixing",
199 | "/manga-tag/qingchun",
200 | "/manga-tag/mori",
201 | "/manga-tag/xuanyi",
202 | "/manga-tag/xiuxian",
203 | "/manga-tag/zhandou"
204 | ],
205 | }
206 | ],
207 | // enable ranking page
208 | enableRankingPage: false,
209 | }
210 |
211 | /// category comic loading related
212 | categoryComics = {
213 | load: async (category, params, options, page) => {
214 | const res = await Network.get(`${this.baseUrl}${params}/page/${page}`, this.headers);
215 | if (res.status !== 200) {
216 | throw `Invalid status code: ${res.status}`;
217 | }
218 | const document = new HtmlDocument(res.body);
219 | let maxPage = null;
220 | try {
221 | maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
222 | } catch(_) {
223 | maxPage = 1;
224 | }
225 | return {
226 | comics: this.parseComics(document),
227 | maxPage: maxPage
228 | };
229 | }
230 | }
231 |
232 | /// search related
233 | search = {
234 | load: async (keyword, options, page) => {
235 | const res = await Network.get(`${this.baseUrl}/s/${keyword}?page=${page}`);
236 | if (res.status !== 200) {
237 | throw `Invalid status code: ${res.status}`;
238 | }
239 | const document = new HtmlDocument(res.body);
240 | let maxPage = null;
241 | try {
242 | maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
243 | } catch(_) {
244 | maxPage = 1;
245 | }
246 | return {
247 | comics: this.parseComics(document),
248 | maxPage: maxPage
249 | };
250 | },
251 | // enable tags suggestions
252 | enableTagsSuggestions: false,
253 | }
254 |
255 | /// single comic related
256 | comic = {
257 | onThumbnailLoad: (url) => {
258 | return {
259 | headers: this.headers
260 | }
261 | },
262 | loadInfo: async (id) => {
263 | const res = await Network.get(this.baseUrl + id);
264 | if (res.status !== 200) {
265 | throw `Invalid status code: ${res.status}`;
266 | }
267 | const document = new HtmlDocument(res.body);
268 | const title = document.querySelector(".text-xl").text.trim().split(" ")[0]
269 | const cover = document.querySelector(".object-cover").attributes["src"];
270 | const description = document.querySelector("p.text-medium").text;
271 | const infos = document.querySelectorAll("div.py-1");
272 | const tags = { "作者": [], "类型": [], "标签": [] };
273 | for (let author of infos[0].querySelectorAll("a > span")) {
274 | let author_name = author.text.trim();
275 | if (author_name.endsWith(",")) {
276 | author_name = author_name.slice(0, -1).trim();
277 | }
278 | tags["作者"].push(author_name);
279 | }
280 | for (let category of infos[1].querySelectorAll("a > span")) {
281 | let category_name = category.text.trim();
282 | if (category_name.endsWith(",")) {
283 | category_name = category_name.slice(0, -1).trim();
284 | }
285 | tags["类型"].push(category_name);
286 | }
287 | for (let tag of infos[2].querySelectorAll("a")) {
288 | tags["标签"].push(tag.text.replace("\n", "").replaceAll(" ", "").replace("#", ""));
289 | }
290 | const mangaId = document.querySelector("#mangachapters").attributes["data-mid"];
291 | const jsonRes = await Network.get(`${this.apiUrl}/manga/get?mid=${mangaId}&mode=all&t=${Date.now()}`, this.headers);
292 | const jsonData = JSON.parse(jsonRes.body);
293 | const chapters = {};
294 | for (let ch of jsonData["data"]["chapters"]) {
295 | chapters[`${mangaId}@${ch["id"]}`] = ch["attributes"]["title"];
296 | }
297 | const recommend = [];
298 | for (let item of document.querySelectorAll("div.cardlist > div.pb-2")) {
299 | recommend.push(new Comic({
300 | id: item.querySelector("a").attributes["href"],
301 | title: item.querySelector("h3").text,
302 | cover: item.querySelector("img").attributes["src"]
303 | }));
304 | }
305 | return new ComicDetails({
306 | title: title,
307 | cover: cover,
308 | description: description,
309 | tags: tags,
310 | chapters: chapters,
311 | recommend: recommend,
312 | });
313 | },
314 |
315 | loadEp: async (comicId, epId) => {
316 | const ids = epId.split("@");
317 | const res = await Network.get(`${this.apiUrl}/chapter/getinfo?m=${ids[0]}&c=${ids[1]}`, this.headers);
318 | if (res.status !== 200) {
319 | throw `Invalid status code: ${res.status}`;
320 | }
321 | const jsonData = JSON.parse(res.body);
322 | const images = [];
323 | for (let i of jsonData["data"]["info"]["images"]["images"]) {
324 | images.push(this.imageUrl + i["url"]);
325 | }
326 | return { images };
327 | },
328 |
329 | // enable tags translate
330 | enableTagsTranslate: false,
331 | }
332 | }
--------------------------------------------------------------------------------
/comic_walker.js:
--------------------------------------------------------------------------------
1 | class ComicWalker extends ComicSource {
2 | name = "カドコミ";
3 | key = "comic_walker";
4 | version = "1.0.0";
5 | minAppVersion = "1.6.0";
6 | url =
7 | "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/comic_walker.js";
8 |
9 | api_key = "ytBrdQ2ZYdRQguqEusVLxQVUgakNnVht";
10 |
11 | latestVersion = "1.4.13";
12 |
13 | api_base = "https://mobileapp.comic-walker.com";
14 |
15 | get headers() {
16 | const headers = {
17 | "X-API-Environment-Key": this.api_key,
18 | "User-Agent": `BookWalkerApp/${this.latestVersion} (Android 13)`,
19 | "Host": "mobileapp.comic-walker.com",
20 | "Content-Type": "application/json"
21 | };
22 | const token = this.loadData("token");
23 | if (token) {
24 | headers["Authorization"] = `Bearer ${token}`;
25 | }
26 | return headers;
27 | }
28 |
29 | async refreshToken() {
30 | const res = await this.request(
31 | `${this.api_base}/v1/users`,
32 | this.headers,
33 | "POST",
34 | );
35 |
36 | this.saveData("token", res.resources.access_token);
37 | return res.resources.access_token;
38 | }
39 |
40 | async request(url, headers, method = "GET", data) {
41 | let response;
42 | if (method === "GET") {
43 | response = await Network.get(url, headers);
44 | } else if (method === "POST") {
45 | response = await Network.post(url, headers, data);
46 | } else {
47 | throw new Error(`Unsupported method: ${method}`);
48 | }
49 | if (
50 | response.status === 204
51 | ) {
52 | return response;
53 | }
54 | response = JSON.parse(response.body);
55 | if (
56 | response.code === "invalid_request_parameter" ||
57 | response.code === "free_daily_reward_quota_exceeded" ||
58 | response.code === "unauthorized"
59 | ) {
60 | await this.refreshToken();
61 | if (method === "GET") {
62 | response = await Network.get(url, this.headers);
63 | } else if (method === "POST") {
64 | response = await Network.post(url, this.headers, data);
65 | } else {
66 | throw new Error(`Unsupported method: ${method}`);
67 | }
68 | if (
69 | response.status === 204
70 | ) {
71 | return response;
72 | }
73 | response = JSON.parse(response.body);
74 | }
75 | return response;
76 | }
77 |
78 | async init() {
79 | const itunes_api = "https://itunes.apple.com/lookup?bundleId=jp.co.bookwalker.cwapp.ios&country=jp";
80 |
81 | const resp = await Network.get(itunes_api);
82 |
83 | if (resp.status == 200) {
84 | response = JSON.parse(resp.body);
85 | this.latestVersion = response.version;
86 | }
87 |
88 | await this.refreshToken();
89 | }
90 |
91 | explore = [
92 | {
93 | title: "カドコミ",
94 | type: "singlePageWithMultiPart",
95 | load: async () => {
96 | const res = await this.request(
97 | `${this.api_base}/v2/screens/home`,
98 | this.headers,
99 | );
100 |
101 | const result = {};
102 |
103 | const newArrivals = res.resources.new_arrival_comics.map((item) =>
104 | new Comic({
105 | id: item.id,
106 | title: item.title,
107 | cover: item.thumbnail_1x1 || "",
108 | tags: item.comic_labels?.map((l) => l.name) || [],
109 | }),
110 | );
111 | result["今日の更新"] = newArrivals;
112 |
113 | const attention = res.resources.attention_comics.map((item) =>
114 | new Comic({
115 | id: item.comic_id,
116 | title: item.title,
117 | cover: item.image_url || "",
118 | tags: item.comic_labels?.map((l) => l.name) || [],
119 | }),
120 | );
121 | result["注目作品"] = attention;
122 |
123 | for (const pickup of res.resources.pickup_comics) {
124 | const comics = pickup.comics.map((item) =>
125 | new Comic({
126 | id: item.id,
127 | title: item.title,
128 | cover: item.thumbnail_1x1 || "",
129 | tags: item.comic_labels?.map((l) => l.name) || [],
130 | }),
131 | );
132 | result[pickup.name] = comics;
133 | }
134 |
135 | const newSerialization = res.resources.new_serialization_comics.map((item) =>
136 | new Comic({
137 | id: item.id,
138 | title: item.title,
139 | cover: item.thumbnail_1x1 || "",
140 | tags: item.comic_labels?.map((l) => l.name) || [],
141 | }),
142 | );
143 | result["新連載"] = newSerialization;
144 |
145 |
146 | return result;
147 | },
148 | },
149 | ];
150 |
151 | search = {
152 | load: async (keyword, _, page) => {
153 | const res = await this.request(
154 | `${this.api_base}/v1/search/comics?keyword=${keyword}&limit=20&offset=${
155 | (page - 1) * 20
156 | }`,
157 | this.headers,
158 | );
159 |
160 | const comics = res.resources.map((item) =>
161 | new Comic({
162 | id: item.id,
163 | title: item.title,
164 | cover: item.thumbnail_1x1 || "",
165 | tags: [
166 | ...(item.authors?.map((a) => a.name) || []),
167 | ...(item.comic_labels?.map((l) => l.name) || []),
168 | ],
169 | })
170 | );
171 | const pageInfo = {
172 | hasNextPage: res.resources.length === 20,
173 | endCursor: null,
174 | };
175 |
176 | return {
177 | comics,
178 | maxPage: pageInfo.hasNextPage ? (page || 1) + 1 : (page || 1),
179 | endCursor: pageInfo.endCursor,
180 | };
181 | },
182 | };
183 |
184 | comic = {
185 | loadInfo: async (id) => {
186 | const res = await this.request(
187 | `${this.api_base}/v2/screens/comics/${id}`,
188 | this.headers,
189 | );
190 | const detail = res.resources.detail;
191 |
192 | const totalCount = res.resources.episode_total_count || 0;
193 | let episodes = { resources: [] };
194 | for (let offset = 0; offset < totalCount; offset += 100) {
195 | const chunk = await this.request(
196 | `${this.api_base}/v1/comics/${id}/episodes?offset=${offset}&limit=100&sort=asc`,
197 | this.headers,
198 | );
199 | episodes.resources.push(...(chunk.resources || []));
200 | }
201 |
202 | const tags = new Map();
203 |
204 | if (detail.authors) {
205 | detail.authors.forEach((a) => {
206 | if (!tags.has(a.role)) tags.set(a.role, []);
207 | tags.get(a.role).push(a.name);
208 | });
209 | }
210 |
211 | if (detail.comic_labels) {
212 | detail.comic_labels.forEach((l) => {
213 | if (!tags.has("Labels")) tags.set("Labels", []);
214 | tags.get("Labels").push(l.name);
215 | });
216 | }
217 |
218 | if (detail.tags) {
219 | detail.tags.forEach((t) => {
220 | if (!tags.has(t.type)) tags.set(t.type, []);
221 | tags.get(t.type).push(t.name);
222 | });
223 | }
224 |
225 | const chapters = new Map();
226 | for (const ep of episodes.resources) {
227 | let canRent = false;
228 | const plans = (ep.plans || []).filter((plan) =>
229 | plan.type !== "paid"
230 | );
231 | if (Array.isArray(plans) && plans.length > 0) {
232 | canRent = true;
233 | }
234 | const title = canRent ? ep.title : `❌ ${ep.title}`;
235 | chapters.set(ep.id, title);
236 | }
237 |
238 | return new ComicDetails({
239 | title: detail.title,
240 | subtitle: detail.authors?.map((a) => a.name).join("・") || "",
241 | cover: detail.thumbnail_1x1 || "",
242 | description: detail.story?.replace(/
/gi, "\n") || "",
243 | tags,
244 | chapters,
245 | updateTime: detail.next_update_at,
246 | url: detail.share_url,
247 | maxPage: totalCount,
248 | });
249 | },
250 |
251 | loadEp: async (comicId, epId) => {
252 | let detail = await this.request(
253 | `${this.api_base}/v1/episodes/${epId}`,
254 | this.headers,
255 | );
256 | const plans = (detail.plans || []).filter((plan) =>
257 | // plan.type !== "daily_video_free" &&
258 | plan.type !== "paid"
259 | );
260 | if (
261 | !Array.isArray(plans) ||
262 | plans.length === 0
263 | ) {
264 | throw new Error("No available rental plans after filtering");
265 | }
266 | console.log(plans);
267 | const freePlan = plans.find((plan) => plan.type === "free");
268 | if (!freePlan) {
269 | const plan = plans[randomInt(0, plans.length - 1)];
270 | await this.request(
271 | `${this.api_base}/v1/users/me/rental_episodes`,
272 | this.headers,
273 | "POST",
274 | { episode_id: epId, reading_method: plan.type },
275 | );
276 | }
277 | let res = await this.request(
278 | `${this.api_base}/v1/screens/comics/${comicId}/episodes/${epId}/viewer`,
279 | this.headers,
280 | );
281 | const manuscripts = res.resources.manuscripts || [];
282 | return {
283 | images: manuscripts.map((m) =>
284 | `${m.drm_image_url}&drm_hash=${m.drm_hash}`
285 | ),
286 | };
287 | },
288 |
289 | onImageLoad: (url) => {
290 | let drm_hash = null;
291 | let cleanUrl = url;
292 | const drmHashMatch = url.match(/[?&]drm_hash=([^&]+)/);
293 | if (drmHashMatch) {
294 | drm_hash = decodeURIComponent(drmHashMatch[1]);
295 | cleanUrl = url.replace(/([?&])drm_hash=[^&]+(&)?/, (match, p1, p2) => {
296 | if (p2) return p1;
297 | return "";
298 | }).replace(/[?&]$/, "");
299 | }
300 | cleanUrl = cleanUrl.replace(/([?&])weight=[^&]+(&)?/, (match, p1, p2) => {
301 | if (p2) return p1;
302 | return "";
303 | }).replace(/[?&]$/, "");
304 |
305 | cleanUrl = cleanUrl.replace(/([?&])height=[^&]+(&)?/, (match, p1, p2) => {
306 | if (p2) return p1;
307 | return "";
308 | }).replace(/[?&]$/, "");
309 |
310 | if (drm_hash.length < 2) {
311 | throw new Error(
312 | "drm_hash must be at least 2 characters long",
313 | );
314 | }
315 | var version = drm_hash.slice(0, 2);
316 | if (version !== "01") {
317 | throw new Error("Unsupported version: " + version);
318 | }
319 | var key_part = drm_hash.slice(2);
320 | if (key_part.length < 16) {
321 | throw new Error(
322 | "Key part must be 16 characters long (8 hex numbers)",
323 | );
324 | }
325 | var key = [];
326 | for (var i = 0; i < 8; i++) {
327 | key.push(parseInt(key_part.slice(i * 2, i * 2 + 2), 16));
328 | }
329 |
330 | const keyArray = key;
331 | const onResponseScript = `
332 | function onResponse(buffer) {
333 | var key = [${keyArray.join(',')}];
334 | var view = new Uint8Array(buffer);
335 | for (var i = 0; i < view.length; i++) {
336 | view[i] ^= key[i % key.length];
337 | }
338 | return buffer;
339 | }
340 | onResponse;
341 | `;
342 | return {
343 | url: cleanUrl,
344 | headers: this.headers,
345 | onResponse: async (buffer) => {
346 | return await compute(onResponseScript, buffer);
347 | }
348 | };
349 | },
350 |
351 | onClickTag: (namespace, tag) => {
352 | if (
353 | namespace === "漫画" || namespace === "原作" ||
354 | namespace === "キャラクター原案" || namespace === "著者"
355 | ) {
356 | return {
357 | action: "search",
358 | keyword: tag,
359 | param: null,
360 | };
361 | }
362 | throw "未支持此类Tag检索";
363 | },
364 | };
365 | }
366 |
--------------------------------------------------------------------------------
/manwaba.js:
--------------------------------------------------------------------------------
1 | /** @type {import('./_venera_.js')} */
2 | class ManWaBa extends ComicSource {
3 | // Note: The fields which are marked as [Optional] should be removed if not used
4 |
5 | // name of the source
6 | name = "漫蛙吧";
7 |
8 | // unique id of the source
9 | key = "manwaba";
10 |
11 | version = "1.0.2";
12 |
13 | minAppVersion = "1.4.0";
14 |
15 | // update url
16 | url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manwaba.js";
17 |
18 | //api = "https://www.manwaba.com/api"; //重定向之前的地址无法使用分类
19 | api = "https://www.mhtmh.org/api";
20 |
21 | init() {
22 | /**
23 | * Sends an HTTP request.
24 | * @param {string} url - The URL to send the request to.
25 | * @param {string} method - The HTTP method (e.g., GET, POST, PUT, PATCH, DELETE).
26 | * @param {Object} params - The query parameters to include in the request.
27 | * @param {Object} headers - The headers to include in the request.
28 | * @param {string} payload - The payload to include in the request.
29 | * @returns {Promise