mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-12-16 17:31:16 +00:00
Compare commits
5 Commits
0d3be98981
...
99f36ab6f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99f36ab6f2 | ||
|
|
732111da2b | ||
|
|
ad8f7a40e0 | ||
|
|
933884e6ca | ||
|
|
d12969d4d7 |
758
ccc.js
Normal file
758
ccc.js
Normal file
@@ -0,0 +1,758 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class CCC extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
// name of the source
|
||||
name = "CCC追漫台"
|
||||
|
||||
// unique id of the source
|
||||
key = "ccc"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.6.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ccc.js"
|
||||
|
||||
apiUrl = "https://api.creative-comic.tw"
|
||||
|
||||
processToken(body) {
|
||||
const result = JSON.parse(body);
|
||||
if (result.code != 0) {
|
||||
throw "登錄失敗";
|
||||
}
|
||||
this.saveData("expireTime", Math.floor(Date.now() / 1000) + result.expires_in);
|
||||
this.saveData("refreshToken", result.refresh_token);
|
||||
this.saveData("token", result.access_token);
|
||||
}
|
||||
|
||||
async getApiHeaders(login = false) {
|
||||
let token = this.loadData("token");
|
||||
if (!login && token) {
|
||||
if (Math.floor(Date.now() / 1000) > this.loadData("expireTime")) {
|
||||
const res = await Network.post(`${this.apiUrl}/token`, {
|
||||
device: "web_desktop",
|
||||
uuid: "null"
|
||||
}, {
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": "2",
|
||||
"client_secret": "9eAhsCX3VWtyqTmkUo5EEaoH4MNPxrn6ZRwse7tE",
|
||||
"refresh_token": this.loadData("refreshToken")
|
||||
});
|
||||
this.processToken(res.body);
|
||||
token = this.loadData("token");
|
||||
}
|
||||
return {
|
||||
device: "web_desktop",
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
device: "web_desktop",
|
||||
uuid: "null"
|
||||
}
|
||||
}
|
||||
|
||||
base64ToArrayBuffer(base64) {
|
||||
const base64Data = base64.split(',')[1] || base64;
|
||||
return Convert.decodeBase64(base64Data);
|
||||
}
|
||||
|
||||
async parseComics(url) {
|
||||
const res = await Network.get(url, await this.getApiHeaders());
|
||||
const result = [];
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
for (let c of jsonData["data"]) {
|
||||
const tags = [];
|
||||
for (let a of c["author"]) {
|
||||
tags.push(a["name"]);
|
||||
}
|
||||
if (typeof (c["type"]) == "object") {
|
||||
tags.push(c["type"]["name"]);
|
||||
}
|
||||
result.push({
|
||||
id: (("book_id" in c) ? c["book_id"] : c["id"]).toString(),
|
||||
title: c["name"],
|
||||
subtitle: c["brief"],
|
||||
description: c["description"],
|
||||
cover: c["image1"],
|
||||
tags: tags
|
||||
});
|
||||
}
|
||||
return { comics: result, maxPage: Math.ceil(jsonData["total"] / 20) };
|
||||
}
|
||||
|
||||
// [Optional] account related
|
||||
account = {
|
||||
/**
|
||||
* [Optional] login with account and password, return any value to indicate success
|
||||
* @param account {string}
|
||||
* @param pwd {string}
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
login: async (account, pwd) => {
|
||||
let res = await Network.get(`${this.apiUrl}/recaptcha#${randomInt(0, 999)}`, await this.getApiHeaders(true)); //使用隨機fragment來強制url重新加載
|
||||
const captcha = JSON.parse(res.body);
|
||||
if (captcha.message != "ok") {
|
||||
throw "登錄失敗";
|
||||
}
|
||||
const captcha_code = await UI.showInputDialog("驗證碼", null, this.base64ToArrayBuffer(captcha.result.img));
|
||||
res = await Network.post(`${this.apiUrl}/token`, await this.getApiHeaders(true), {
|
||||
"grant_type": "password",
|
||||
"client_id": "2",
|
||||
"client_secret": "9eAhsCX3VWtyqTmkUo5EEaoH4MNPxrn6ZRwse7tE",
|
||||
"username": account,
|
||||
"password": pwd,
|
||||
"key": captcha.result.key,
|
||||
"captcha": captcha_code
|
||||
})
|
||||
this.processToken(res.body);
|
||||
return "ok";
|
||||
},
|
||||
|
||||
/**
|
||||
* [Optional] login with webview
|
||||
*/
|
||||
loginWithWebview: {
|
||||
url: "https://www.creative-comic.tw/zh/login",
|
||||
/**
|
||||
* check login status.
|
||||
* After successful login, the cookie will be automatically saved, and the localstorage can be retrieved using this.loadData("_localStorage").
|
||||
* @param url {string} - current url
|
||||
* @param title {string} - current title
|
||||
* @returns {boolean} - return true if login success
|
||||
*/
|
||||
checkStatus: (url, title) => {
|
||||
return (title == "CCC追漫台");
|
||||
},
|
||||
/**
|
||||
* [Optional] Callback when login success
|
||||
*/
|
||||
onLoginSuccess: () => {
|
||||
const localStorage = this.loadData("_localStorage");
|
||||
if (localStorage) {
|
||||
const token = localStorage["accessToken"];
|
||||
let base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
base64 = base64 + '='.repeat((4 - base64.length % 4) % 4);
|
||||
const jsonPayload = decodeURIComponent(
|
||||
Convert.decodeUtf8(Convert.decodeBase64(base64))
|
||||
.split('')
|
||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
this.saveData("expireTime", JSON.parse(jsonPayload)["exp"]);
|
||||
this.saveData("refreshToken", localStorage["refreshToken"]);
|
||||
this.saveData("token", token);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* logout function, clear account related data
|
||||
*/
|
||||
logout: () => {
|
||||
this.deleteData("expireTime");
|
||||
this.deleteData("refreshToken");
|
||||
this.deleteData("token");
|
||||
},
|
||||
|
||||
// {string?} - register url
|
||||
registerWebsite: "https://www.creative-comic.tw/zh/signup"
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [
|
||||
{
|
||||
// title of the page.
|
||||
// title is used to identify the page, it should be unique
|
||||
title: "CCC追漫台",
|
||||
|
||||
/// multiPartPage or multiPageComicList or mixed
|
||||
type: "singlePageWithMultiPart",
|
||||
|
||||
/**
|
||||
* load function
|
||||
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
|
||||
* @returns {{}}
|
||||
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: string?}]
|
||||
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
|
||||
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
|
||||
*/
|
||||
load: async () => {
|
||||
const res = await Network.get(`${this.apiUrl}/public/home_v2`, await this.getApiHeaders());
|
||||
const result = {};
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
let curTitle = null;
|
||||
for (let data of jsonData["templates"]) {
|
||||
if (data["type"] == 4) {
|
||||
continue;
|
||||
}
|
||||
const comics = [];
|
||||
for (let c of data["list"]) {
|
||||
comics.push({
|
||||
id: c["value"],
|
||||
title: c["name"],
|
||||
cover: c["image1"],
|
||||
tags: [c["book_type"]["name"]],
|
||||
subtitle: c["brief"]
|
||||
});
|
||||
}
|
||||
if (data["title"]) {
|
||||
curTitle = data["title"];
|
||||
result[curTitle] = comics;
|
||||
} else {
|
||||
result[curTitle] = result[curTitle].concat(comics);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: "CCC追漫台",
|
||||
parts: [
|
||||
{
|
||||
name: "CCC追漫台",
|
||||
type: "fixed",
|
||||
categories: ["排行榜"],
|
||||
itemType: "category",
|
||||
categoryParams: ["top"]
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
categoryComics = {
|
||||
/**
|
||||
* load comics of a category
|
||||
* @param category {string} - category name
|
||||
* @param param {string?} - category param
|
||||
* @param options {string[]} - options from optionList
|
||||
* @param page {number} - page number
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
if (options == null) {
|
||||
options = ["", "read"];
|
||||
}
|
||||
const type = options[0] ? `&type=${options[0]}` : "";
|
||||
const url = `${this.apiUrl}/rank?page=${page}&rows_per_page=20&rank=${options[1]}&class=2${type}`;
|
||||
return await this.parseComics(url);
|
||||
},
|
||||
/**
|
||||
* [Optional] load options dynamically. If `optionList` is provided, this will be ignored.
|
||||
* @param category {string}
|
||||
* @param param {string?}
|
||||
* @return {Promise<{options: string[], label?: string}[]>} - return a list of option group, each group contains a list of options
|
||||
*/
|
||||
optionList: [
|
||||
{
|
||||
label: "分類",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"2-劇情",
|
||||
"6-愛情",
|
||||
"5-青春成長",
|
||||
"3-幽默搞笑",
|
||||
"10-歷史古裝",
|
||||
"7-奇幻架空",
|
||||
"4-溫馨療癒",
|
||||
"9-冒險動作",
|
||||
"8-恐怖驚悚",
|
||||
"12-新感覺推薦",
|
||||
"11-推理懸疑",
|
||||
"13-活動"
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "排行榜",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"read-人氣榜",
|
||||
"buy-銷售榜",
|
||||
"donate-斗内榜",
|
||||
"collect-收藏榜"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
/**
|
||||
* load search result
|
||||
* @param keyword {string}
|
||||
* @param options {(string | null)[]} - options from optionList
|
||||
* @param page {number}
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
options[0] = "&sort_by=" + options[0];
|
||||
if (options[1]) {
|
||||
options[1] = "&type=" + options[1];
|
||||
}
|
||||
if (options[2]) {
|
||||
options[2] = "&serial=" + options[2];
|
||||
}
|
||||
if (options[3]) {
|
||||
options[3] = "&updated_at=" + options[3];
|
||||
}
|
||||
if (options[4]) {
|
||||
options[4] = "&literature_form=" + options[4];
|
||||
}
|
||||
if (options[5]) {
|
||||
options[5] = "&comic_type=" + options[5];
|
||||
}
|
||||
if (options[6]) {
|
||||
options[6] = "&publisher=" + options[6];
|
||||
}
|
||||
const url = `https://api.creative-comic.tw/book?page=${page}&rows_per_page=20&keyword=${keyword}&class=2${options.join("")}`;
|
||||
return await this.parseComics(url);
|
||||
},
|
||||
|
||||
// provide options for search
|
||||
optionList: [
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"updated_at-最新",
|
||||
"read_count-閲覽",
|
||||
"like_count-推薦",
|
||||
"collect_count-收藏"
|
||||
],
|
||||
// option label
|
||||
label: "排序"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"2-劇情",
|
||||
"6-愛情",
|
||||
"5-青春成長",
|
||||
"3-幽默搞笑",
|
||||
"10-歷史古裝",
|
||||
"7-奇幻架空",
|
||||
"4-溫馨療癒",
|
||||
"9-冒險動作",
|
||||
"8-恐怖驚悚",
|
||||
"12-新感覺推薦",
|
||||
"11-推理懸疑",
|
||||
"13-活動"
|
||||
],
|
||||
// option label
|
||||
label: "分類"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"2-已完結",
|
||||
"0-連載中"
|
||||
],
|
||||
// option label
|
||||
label: "連載狀態"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"month-本月",
|
||||
"week-本周"
|
||||
],
|
||||
// option label
|
||||
label: "更新日期"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"1-短篇",
|
||||
"2-中篇",
|
||||
"3-長篇"
|
||||
],
|
||||
// option label
|
||||
label: "作品篇幅"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"3-條漫",
|
||||
"2-格漫",
|
||||
],
|
||||
// option label
|
||||
label: "作品形式"
|
||||
},
|
||||
{
|
||||
type: "dropdown",
|
||||
options: [
|
||||
"-全部",
|
||||
"44-MOJOIN",
|
||||
"37-目宿媒體股份有限公司",
|
||||
"4-大辣出版",
|
||||
"18-MarsCat火星貓科技",
|
||||
"2-CCC創作集",
|
||||
"23-海穹文化",
|
||||
"11-國立歷史博物館",
|
||||
"6-未來數位",
|
||||
"34-虎尾建國眷村再造協會",
|
||||
"24-鏡文學股份有限公司",
|
||||
"43-Taiwan Comic City",
|
||||
"42-聯經出版事業股份有限公司",
|
||||
"48-東立出版社有限公司",
|
||||
"9-留守番工作室",
|
||||
"16-獨步文化",
|
||||
"21-尖端媒體集團",
|
||||
"29-相之丘tōkhiu books",
|
||||
"7-威向文化",
|
||||
"54-白範出版工作室",
|
||||
"22-時報文化出版企業股份有限公司",
|
||||
"20-國立臺灣工藝研究發展中心",
|
||||
"17-獨立出版",
|
||||
"51-大寬文化工作室",
|
||||
"32-金繪國際有限公司",
|
||||
"47-前衛出版社",
|
||||
"36-奇異果文創",
|
||||
"14-綺影映畫",
|
||||
"53-彰化縣政府",
|
||||
"31-艾德萊娛樂",
|
||||
"8-特有生物研究保育中心",
|
||||
"39-聚場文化",
|
||||
"38-XPG",
|
||||
"52-陌上商行有限公司",
|
||||
"49-國際合製|臺漫新視界",
|
||||
"40-KADOKAWA",
|
||||
"10-國立臺灣美術館",
|
||||
"26-金漫獎",
|
||||
"5-台灣東販",
|
||||
"45-國立國父紀念館",
|
||||
"35-國立臺灣歷史博物館",
|
||||
"15-蓋亞文化",
|
||||
"1-長鴻出版社",
|
||||
"19-柒拾陸號原子",
|
||||
"33-台灣角川",
|
||||
"28-一顆星工作室",
|
||||
"46-好人出版",
|
||||
"27-澄波藝術文化股份有限公司",
|
||||
"12-黑白文化",
|
||||
"13-慢工文化 Slowork Publishing",
|
||||
"30-經濟部智慧財產局",
|
||||
"50-Contents Lab. Blue TOKYO",
|
||||
"3-大塊文化",
|
||||
"25-目色出版",
|
||||
"41-文化內容策進院"
|
||||
],
|
||||
label: "出版社"
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
// favorite related
|
||||
favorites = {
|
||||
multiFolder: false,
|
||||
/**
|
||||
* add or delete favorite.
|
||||
* throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite
|
||||
* @param comicId {string}
|
||||
* @param folderId {string}
|
||||
* @param isAdding {boolean} - true for add, false for delete
|
||||
* @param favoriteId {string?} - [Comic.favoriteId]
|
||||
* @returns {Promise<any>} - return any value to indicate success
|
||||
*/
|
||||
addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {
|
||||
if (!this.isLogged) {
|
||||
throw "請先登錄";
|
||||
}
|
||||
const res = await Network.put(`${this.apiUrl}/book/${comicId}/collect`, await this.getApiHeaders(), { "is_collected": isAdding });
|
||||
if (JSON.parse(res.body)["message"] != "ok") {
|
||||
throw `${isAdding ? "添加" : "移除"}收藏失敗`;
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
/**
|
||||
* load comics in a folder
|
||||
* throw `Login expired` to indicate login expired, App will automatically re-login retry.
|
||||
* @param page {number}
|
||||
* @param folder {string?} - folder id, null for non-multi-folder
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
loadComics: async (page, folder) => {
|
||||
return this.parseComics(`${this.apiUrl}/bookcase/collections?page=${page}&rows_per_page=20&sort_by=updated_at&class=2`);
|
||||
},
|
||||
singleFolderForSingleComic: true,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
freeRead: (data) => {
|
||||
let free_read = true;
|
||||
if (!data["is_free"]) {
|
||||
if (data["sales_plan"] != 0) {
|
||||
if ((data["is_coin_buy"] || data["is_point_buy"]) && !data["is_buy"]) {
|
||||
if ((data["is_coin_rent"] || data["is_point_rent"]) && !data["is_rent"]) {
|
||||
free_read = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return free_read;
|
||||
},
|
||||
/**
|
||||
* load comic info
|
||||
* @param id {string}
|
||||
* @returns {Promise<ComicDetails>}
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/${id}/info`, await this.getApiHeaders());
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
const authors = [];
|
||||
for (let a of jsonData["author"]) {
|
||||
authors.push(a["name"]);
|
||||
}
|
||||
const tags = [];
|
||||
for (let t of jsonData["tags"]) {
|
||||
tags.push(t["name"]);
|
||||
}
|
||||
const chapter_res = await Network.get(`${this.apiUrl}/book/${id}/chapter`, await this.getApiHeaders());
|
||||
const chapterData = JSON.parse(chapter_res.body)["data"];
|
||||
const chapters = {};
|
||||
for (let c of chapterData["chapters"]) {
|
||||
chapters[c["id"].toString()] = `${!this.comic.freeRead(c) ? "[付費]" : ""}${c["vol_name"]}-${c["name"]}`;
|
||||
}
|
||||
const recommend_res = await Network.get(`${this.apiUrl}/book/${id}/recommend`, await this.getApiHeaders());
|
||||
const recommendData = JSON.parse(recommend_res.body)["data"];
|
||||
const recommends = [];
|
||||
for (let r of recommendData["hot"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"],
|
||||
id: r["id"].toString(),
|
||||
subtitle: r["brief"]
|
||||
});
|
||||
}
|
||||
for (let r of recommendData["history"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"],
|
||||
id: r["id"].toString()
|
||||
});
|
||||
}
|
||||
for (let r of recommendData["also_buy"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"],
|
||||
id: r["id"].toString()
|
||||
});
|
||||
}
|
||||
return new ComicDetails({
|
||||
title: jsonData["name"],
|
||||
subtitle: jsonData["brief"],
|
||||
cover: jsonData["image1"],
|
||||
description: jsonData["description"],
|
||||
likesCount: jsonData["like_count_only_uuid"],
|
||||
chapters: chapters,
|
||||
tags: {
|
||||
"作者": authors,
|
||||
"分類": [jsonData["type"]["name"]],
|
||||
"標籤": tags,
|
||||
},
|
||||
isFavorite: (jsonData["is_collected"] == 1),
|
||||
updateTime: jsonData["updated_at"],
|
||||
recommend: recommends
|
||||
})
|
||||
},
|
||||
/**
|
||||
* load images of a chapter
|
||||
* @param comicId {string}
|
||||
* @param epId {string?}
|
||||
* @returns {Promise<{images: string[]}>}
|
||||
*/
|
||||
loadEp: async (comicId, epId) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/chapter/${epId}`, await this.getApiHeaders());
|
||||
if (res.status == 403) {
|
||||
UI.showDialog("提示", "該章節需付費后閲讀", [
|
||||
{
|
||||
text: "取消",
|
||||
callback: () => { }
|
||||
},
|
||||
{
|
||||
text: "去購買",
|
||||
callback: () => {
|
||||
UI.launchUrl(`https://www.creative-comic.tw/zh/book/${comicId}/content`);
|
||||
}
|
||||
}
|
||||
]);
|
||||
return { images: [] };
|
||||
}
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
const images = [];
|
||||
for (let img of jsonData["chapter"]["proportion"]) {
|
||||
images.push(img["id"].toString());
|
||||
}
|
||||
return {
|
||||
images: images
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [Optional] provide configs for an image loading
|
||||
* @param url
|
||||
* @param comicId
|
||||
* @param epId
|
||||
* @returns {{} | Promise<{}>}
|
||||
*/
|
||||
onImageLoad: async (url, comicId, epId) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/chapter/image/${url}`, await this.getApiHeaders());
|
||||
const encryptedKey = Convert.decodeBase64(JSON.parse(res.body)["data"]["key"]);
|
||||
let token = this.loadData("token");
|
||||
if (token == null) {
|
||||
token = "freeforccc2020reading";
|
||||
}
|
||||
const hashArray = Convert.sha512(Convert.encodeUtf8(token));
|
||||
const pageKey = hashArray.slice(0, 32);
|
||||
const pageIv = hashArray.slice(15, 31);
|
||||
const decryptedKey = new Uint8Array(Convert.decryptAesCbc(encryptedKey, pageKey, pageIv));
|
||||
const padLen = decryptedKey[decryptedKey.length - 1];
|
||||
const [key, iv] = Convert.decodeUtf8(decryptedKey.slice(0, decryptedKey.length - padLen).buffer).split(":");
|
||||
return {
|
||||
url: `https://storage.googleapis.com/ccc-www/fs/chapter_content/encrypt/${url}/2`,
|
||||
onResponse: function (buffer) {
|
||||
function hexToBytes(hex) {
|
||||
if (hex.length % 2 !== 0) {
|
||||
throw new Error("Invalid hex string");
|
||||
}
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
const decrypted = new Uint8Array(Convert.decryptAesCbc(buffer, hexToBytes(key), hexToBytes(iv)));
|
||||
const padLen_ = decrypted[decrypted.length - 1];
|
||||
const base64 = Convert.decodeUtf8(decrypted.slice(0, decrypted.length - padLen_).buffer);
|
||||
const base64Data = base64.split(',')[1] || base64;
|
||||
return Convert.decodeBase64(base64Data);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [Optional] load comments
|
||||
* @param comicId {string}
|
||||
* @param subId {string?} - ComicDetails.subId
|
||||
* @param page {number}
|
||||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||
* @returns {Promise<{comments: Comment[], maxPage: number?}>}
|
||||
*/
|
||||
loadComments: async (comicId, subId, page, replyTo) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/${comicId}/reply?page=${page}&rows_per_page=20&sort_by=created_at&descending=true#${randomInt(0, 999)}`,
|
||||
//使用隨機fragment來强制url重新加載
|
||||
await this.getApiHeaders());
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
let maxPage = 0;
|
||||
const comments = [];
|
||||
if (replyTo) {
|
||||
for (let c of jsonData["data"]) {
|
||||
if (c["id"].toString() == replyTo) {
|
||||
for (let c_ of c["replies"]) {
|
||||
comments.push({
|
||||
userName: c_["member"]["name"] ? c_["member"]["name"] : c_["member"]["nickname"],
|
||||
avatar: c_["member"]["avatar"],
|
||||
content: c_["content"],
|
||||
time: c_["created_at"],
|
||||
id: c_["id"].toString(),
|
||||
isLiked: (c_["is_like"] == 1),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let c of jsonData["data"]) {
|
||||
comments.push({
|
||||
userName: c["member"]["name"] ? c["member"]["name"] : c["member"]["nickname"],
|
||||
avatar: c["member"]["avatar"],
|
||||
content: c["content"],
|
||||
time: c["created_at"],
|
||||
replyCount: c["reply_count"],
|
||||
id: c["id"].toString(),
|
||||
isLiked: (c["is_like"] == 1),
|
||||
score: c["like_count"]
|
||||
});
|
||||
}
|
||||
maxPage = Math.ceil(jsonData["total"] / 20);
|
||||
}
|
||||
return {
|
||||
comments: comments,
|
||||
maxPage: maxPage
|
||||
};
|
||||
},
|
||||
/**
|
||||
* [Optional] send a comment, return any value to indicate success
|
||||
* @param comicId {string}
|
||||
* @param subId {string?} - ComicDetails.subId
|
||||
* @param content {string}
|
||||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
sendComment: async (comicId, subId, content, replyTo) => {
|
||||
if (!this.isLogged) {
|
||||
throw "請先登錄";
|
||||
}
|
||||
let url = null;
|
||||
if (replyTo) {
|
||||
url = `${this.apiUrl}/book/reply/${replyTo}/reply`;
|
||||
} else {
|
||||
url = `${this.apiUrl}/book/${comicId}/reply`;
|
||||
}
|
||||
const boundary = "----geckoformboundary" + Math.random().toString(16).replace(".", "a") + Math.random().toString(16).replace(".", "a");
|
||||
const body = `--${boundary}\r\n` +
|
||||
`Content-Disposition: form-data; name="content"\r\n\r\n${content}\r\n` +
|
||||
`--${boundary}\r\n` +
|
||||
`Content-Disposition: form-data; name="is_spoiled"\r\n\r\n0\r\n` +
|
||||
`--${boundary}--\r\n`;
|
||||
const headers = await this.getApiHeaders();
|
||||
headers["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
|
||||
const res = await Network.post(url, headers, body);
|
||||
if (JSON.parse(res.body)["message"] != "ok") {
|
||||
throw "評論失敗";
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
likeComment: async (comicId, subId, commentId, isLike) => {
|
||||
if (commentId.endsWith("@")) {
|
||||
throw "不支持點贊";
|
||||
}
|
||||
const res = await Network.put(`${this.apiUrl}/book/reply/${commentId.split("@")[0]}/like`,
|
||||
await this.getApiHeaders(), { "is_like": isLike ? 1 : 0 });
|
||||
if (JSON.parse(res.body)["message"] != "ok") {
|
||||
throw "點贊失敗";
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
/**
|
||||
* [Optional] Handle tag click event
|
||||
* @param namespace {string}
|
||||
* @param tag {string}
|
||||
* @returns {{action: string, keyword: string, param: string?}}
|
||||
*/
|
||||
onClickTag: (namespace, tag) => {
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: tag
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ class Ehentai extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "ehentai"
|
||||
|
||||
version = "1.1.6"
|
||||
version = "1.1.7"
|
||||
|
||||
minAppVersion = "1.5.3"
|
||||
|
||||
@@ -554,6 +554,7 @@ class Ehentai extends ComicSource {
|
||||
favorites = {
|
||||
// whether support multi folders
|
||||
multiFolder: true,
|
||||
singleFolderForSingleComic: true,
|
||||
/**
|
||||
* add or delete favorite.
|
||||
* throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite
|
||||
|
||||
18
index.json
18
index.json
@@ -40,7 +40,7 @@
|
||||
"name": "ehentai",
|
||||
"fileName": "ehentai.js",
|
||||
"key": "ehentai",
|
||||
"version": "1.1.6"
|
||||
"version": "1.1.7"
|
||||
},
|
||||
{
|
||||
"name": "禁漫天堂",
|
||||
@@ -53,7 +53,7 @@
|
||||
"name": "MangaDex",
|
||||
"fileName": "manga_dex.js",
|
||||
"key": "manga_dex",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Account feature is not supported yet."
|
||||
},
|
||||
{
|
||||
@@ -90,7 +90,7 @@
|
||||
"name": "再漫画",
|
||||
"fileName": "zaimanhua.js",
|
||||
"key": "zaimanhua",
|
||||
"version": "1.0.1"
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "漫画柜",
|
||||
@@ -121,5 +121,17 @@
|
||||
"fileName": "comic_walker.js",
|
||||
"key": "comic_walker",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "漫画1234",
|
||||
"fileName": "mh1234.js",
|
||||
"key": "mh1234",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "CCC追漫台",
|
||||
"fileName": "ccc.js",
|
||||
"key": "ccc",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
|
||||
118
manga_dex.js
118
manga_dex.js
@@ -8,7 +8,7 @@ class MangaDex extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "manga_dex"
|
||||
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
@@ -239,16 +239,15 @@ class MangaDex extends ComicSource {
|
||||
// randomNumber: 5,
|
||||
|
||||
// load function for dynamic type
|
||||
loader: () => {
|
||||
loader: () => {
|
||||
let categories = []
|
||||
for (let tag of Object.keys(this.tags)) {
|
||||
categories.push({
|
||||
label: tag,
|
||||
target: {
|
||||
page: "search",
|
||||
attributes: {
|
||||
keyword: `tag:${tag}`,
|
||||
},
|
||||
action: "category",
|
||||
keyword: tag,
|
||||
param: this.tags[tag],
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -260,6 +259,113 @@ class MangaDex extends ComicSource {
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
categoryComics = {
|
||||
load: async (category, param, options = [], page = 1) => {
|
||||
if (!param) {
|
||||
throw new Error("No tag id provided for category comics")
|
||||
}
|
||||
|
||||
const parseOption = (option, fallback) => {
|
||||
if (option === undefined || option === null || option === "") {
|
||||
return fallback
|
||||
}
|
||||
let value = option.split("-")[0]
|
||||
return value || fallback
|
||||
}
|
||||
|
||||
const sortOption = parseOption(options[0], "popular")
|
||||
const ratingOption = parseOption(options[1], "any")
|
||||
const statusOption = parseOption(options[2], "any")
|
||||
|
||||
let params = [
|
||||
"includes[]=cover_art",
|
||||
"includes[]=artist",
|
||||
"includes[]=author",
|
||||
"hasAvailableChapters=true",
|
||||
`limit=${this.comicsPerPage}`,
|
||||
`includedTags[]=${encodeURIComponent(param)}`
|
||||
]
|
||||
|
||||
if (page && page > 1) {
|
||||
params.push(`offset=${(page - 1) * this.comicsPerPage}`)
|
||||
}
|
||||
|
||||
if (sortOption !== "any") {
|
||||
const orderMap = {
|
||||
popular: "followedCount",
|
||||
follows: "followedCount",
|
||||
recent: "createdAt",
|
||||
updated: "latestUploadedChapter",
|
||||
rating: "rating"
|
||||
}
|
||||
const orderKey = orderMap[sortOption]
|
||||
if (orderKey) {
|
||||
params.push(`order[${orderKey}]=desc`)
|
||||
}
|
||||
}
|
||||
|
||||
let ratingList
|
||||
if (ratingOption === "any") {
|
||||
ratingList = ["safe", "suggestive", "erotica"]
|
||||
} else {
|
||||
ratingList = [ratingOption]
|
||||
}
|
||||
for (let rating of ratingList) {
|
||||
params.push(`contentRating[]=${encodeURIComponent(rating)}`)
|
||||
}
|
||||
|
||||
if (statusOption !== "any") {
|
||||
params.push(`status[]=${encodeURIComponent(statusOption)}`)
|
||||
}
|
||||
|
||||
let url = `https://api.mangadex.org/manga?${params.join("&")}`
|
||||
let res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok")
|
||||
}
|
||||
let data = await res.json()
|
||||
let total = data['total'] || 0
|
||||
let comics = []
|
||||
for (let comic of data['data'] || []) {
|
||||
comics.push(this.api.parseComic(comic))
|
||||
}
|
||||
let maxPage = total ? Math.ceil(total / this.comicsPerPage) : (comics.length < this.comicsPerPage ? page : page + 1)
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: maxPage
|
||||
}
|
||||
},
|
||||
optionList: [
|
||||
{
|
||||
options: [
|
||||
"any-Any",
|
||||
"popular-Popular",
|
||||
"recent-Recent",
|
||||
"updated-Updated",
|
||||
"rating-Rating",
|
||||
"follows-Follows"
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"any-Any",
|
||||
"safe-Safe",
|
||||
"suggestive-Suggestive",
|
||||
"erotica-Erotica"
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"any-Any",
|
||||
"ongoing-Ongoing",
|
||||
"completed-Completed",
|
||||
"hiatus-Hiatus",
|
||||
"cancelled-Cancelled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
/**
|
||||
|
||||
321
mh1234.js
Normal file
321
mh1234.js
Normal file
@@ -0,0 +1,321 @@
|
||||
class MH1234 extends ComicSource {
|
||||
// name of the source
|
||||
name = "漫画1234"
|
||||
|
||||
// unique id of the source
|
||||
key = "mh1234"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/mh1234.js"
|
||||
|
||||
settings = {
|
||||
domains: {
|
||||
title: "域名",
|
||||
type: "input",
|
||||
default: "amh1234.com"
|
||||
}
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return `https://b.${this.loadSetting('domains')}`;
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [{
|
||||
title: "漫画1234",
|
||||
type: "singlePageWithMultiPart",
|
||||
load: async () => {
|
||||
const result = {};
|
||||
const res = await Network.get(this.baseUrl);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const doc = new HtmlDocument(res.body);
|
||||
const mangaLists = doc.querySelectorAll("div.imgBox");
|
||||
for (let list of mangaLists) {
|
||||
const tabTitle = list.querySelector(".Title").text;
|
||||
const items = [];
|
||||
for (let item of list.querySelectorAll("li.list-comic")) {
|
||||
const info = item.querySelectorAll("a")[1];
|
||||
items.push(new Comic({
|
||||
id: item.attributes["data-key"],
|
||||
title: item.querySelector("a.txtA").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
result[tabTitle] = items;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}];
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: "漫画1234",
|
||||
parts: [
|
||||
{
|
||||
name: "题材",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"全部", "少年热血", "武侠格斗", "科幻魔幻", "竞技体育", "爆笑喜剧", "侦探推理", "恐怖灵异", "耽美人生",
|
||||
"少女爱情", "恋爱生活", "生活漫画", "战争漫画", "故事漫画", "其他漫画", "爱情", "唯美", "武侠", "玄幻",
|
||||
"后宫", "治愈", "励志", "古风", "校园", "虐心", "魔幻", "冒险", "欢乐向", "节操", "悬疑", "历史", "职场",
|
||||
"神鬼", "明星", "穿越", "百合", "西方魔幻", "纯爱", "音乐舞蹈", "轻小说", "侦探", "伪娘", "仙侠", "四格",
|
||||
"剧情", "萌系", "东方", "性转换", "宅系", "美食", "脑洞", "惊险", "爆笑", "都市", "蔷薇", "恋爱", "格斗",
|
||||
"科幻", "魔法", "奇幻", "热血", "其他", "搞笑", "生活", "恐怖", "架空", "竞技", "战争", "搞笑喜剧", "青春",
|
||||
"浪漫", "爽流", "神话", "轻松", "日常", "家庭", "婚姻", "动作", "战斗", "异能", "内涵", "同人", "惊奇",
|
||||
"正剧", "推理", "宠物", "温馨", "异世界", "颜艺", "惊悚", "舰娘","机战", "彩虹", "耽美", "轻松搞笑",
|
||||
"修真恋爱架空", "复仇", "霸总", "段子", "逆袭", "烧脑", "娱乐圈", "纠结", "感动", "豪门", "体育", "机甲",
|
||||
"末世", "灵异", "僵尸", "宫廷", "权谋", "未来", "科技", "商战", "乡村", "震撼", "游戏", "重口味", "血腥",
|
||||
"逗比", "丧尸", "神魔", "修真", "社会", "召唤兽", "装逼", "新作", "漫改", "真人", "运动", "高智商", "悬疑推理",
|
||||
"机智", "史诗", "萝莉", "宫斗", "御姐", "恶搞", "精品", "日更", "小说改编", "防疫", "吸血", "暗黑", "总裁",
|
||||
"重生", "大女主", "系统", "神仙", "末日", "怪物", "妖怪", "修仙", "宅斗", "神豪", "高甜", "电竞", "豪快",
|
||||
"猎奇", "多世界", "性转", "少女", "改编", "女生", "乙女", "男生", "兄弟情", "智斗", "少男", "连载", "奇幻冒险",
|
||||
"古风穿越", "浪漫爱情", "古装", "幽默搞笑", "偶像", "小僵尸", "BL", "少年", "橘味", "情感", "经典",
|
||||
"腹黑", "都市大女主", "致郁", "美少女", "少儿", "暖萌", "长条", "限制级", "知音漫客", "氪金", "独家",
|
||||
"亲情", "现代", "武侠仙侠", "西幻", "超级英雄", "女神", "幻想", "欧风", "养成", "动作冒险", "GL", "橘调",
|
||||
"悬疑灵异", "古代宫廷", "欧式宫廷", "游戏竞技", "橘系", "奇幻爱情", "架空世界", "ゆり", "福瑞", "秀吉", "现代言情",
|
||||
"古代言情", "豪门总裁", "现言萌宝", "玄幻言情", "虐渣", "团宠", "古言萌宝", "现言甜宠", "古言脑洞", "AA", "金手指",
|
||||
"玄幻脑洞", "都市脑洞", "甜宠", "伦理", "生存", "TL", "悬疑脑洞", "黑暗", "独特", "成长", "幻想言情", "直播",
|
||||
"游戏体育", "现言脑洞", "音乐", "双男主", "迪化", "LGBTQ+", "正能量", "军事", "ABO", "悬疑恐怖",
|
||||
"玄幻科幻", "投稿", "种田", "经营", "反套路", "无节操", "强强", "克苏鲁", "无敌流", "冒险热血", "畅销",
|
||||
"大人系", "宅向", "萌娃", "宠兽", "异形", "撒糖", "诡异", "言情", "西方", "滑稽搞笑", "同居", "人外",
|
||||
"白切黑", "并肩作战", "救赎", "戏精", "美强惨", "非人类", "原创", "黑白漫", "无限流",
|
||||
"升级", "爽", "轻橘", "女帝", "偏执", "自由", "星际", "可盐可甜", "反差萌", "聪颖", "智商在线",
|
||||
"倔强", "狼人", "欢喜冤家", "吸血鬼", "萌宠", "学校", "台湾作品", "彩色", "武术", "短篇", "契约", "魔王",
|
||||
"无敌", "美女", "暧昧", "网游", "宅男", "追逐梦想", "冒险奇幻", "疯批", "中二", "召唤", "法宝", "钓系", "鬼怪",
|
||||
"占有欲", "阳光", "元气", "强制爱", "黑道", "马甲", "阴郁", "忧郁", "哲理", "病娇", "喜剧", "江湖恩怨",
|
||||
"相爱相杀", "萌", "SM", "精选", "生子", "年下", "18+限制", "日久生情", "梦想", "多攻", "竹马", "骨科", "gnbq"
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"", "shaonianrexue", "wuxiagedou", "kehuanmohuan", "jingjitiyu", "baoxiaoxiju", "zhentantuili", "kongbulingyi",
|
||||
"danmeirensheng", "shaonvaiqing", "lianaishenghuo", "shenghuomanhua", "zhanzhengmanhua", "gushimanhua",
|
||||
"qitamanhua", "aiqing", "weimei", "wuxia", "xuanhuan", "hougong", "zhiyu", "lizhi", "gufeng", "xiaoyuan", "nuexin",
|
||||
"mohuan", "maoxian", "huanlexiang", "jiecao", "xuanyi", "lishi", "zhichang", "shengui", "mingxing", "chuanyue",
|
||||
"baihe", "xifangmohuan", "chunai", "yinyuewudao", "qingxiaoshuo", "zhentan", "weiniang", "xianxia", "sige", "juqing",
|
||||
"mengxi", "dongfang", "xingzhuanhuan", "zhaixi", "meishi", "naodong", "jingxian", "baoxiao", "dushi", "qiangwei",
|
||||
"lianai", "gedou", "kehuan", "mofa", "qihuan", "rexue", "qita", "gaoxiao", "shenghuo", "kongbu", "jiakong", "jingji",
|
||||
"zhanzheng", "gaoxiaoxiju", "qingchun", "langman", "shuangliu", "shenhua", "qingsong", "richang", "jiating", "hunyin",
|
||||
"dongzuo", "zhandou", "yineng", "neihan", "tongren", "jingqi", "zhengju", "tuili", "chongwu", "wenxin", "yishijie",
|
||||
"yanyi", "jingsong", "jianniang", "jizhan", "caihong", "danmei", "qingsonggaoxiao", "xiuzhenlianaijiakong", "fuchou",
|
||||
"bazong", "duanzi", "nixi", "shaonao", "yulequan", "jiujie", "gandong", "haomen", "tiyu", "jijia", "moshi", "lingyi",
|
||||
"jiangshi", "gongting", "quanmou", "weilai", "keji", "shangzhan", "xiangcun", "zhenhan", "youxi",
|
||||
"zhongkouwei", "xuexing", "doubi", "sangshi", "shenmo", "xiuzhen", "shehui", "zhaohuanshou", "zhuangbi",
|
||||
"xinzuo", "mangai", "zhenren", "yundong", "gaozhishang", "xuanyituili", "jizhi", "shishi", "luoli","gongdou",
|
||||
"yujie", "egao", "jingpin", "rigeng", "xiaoshuogaibian", "fangyi", "xixie", "anhei", "zongcai", "zhongsheng",
|
||||
"danvzhu", "xitong", "shenxian", "mori", "guaiwu", "yaoguai", "xiuxian", "zhaidou", "shenhao", "gaotian",
|
||||
"dianjing", "haokuai", "lieqi", "duoshijie", "xingzhuan", "shaonv", "gaibian", "nvsheng", "yinv", "nansheng",
|
||||
"xiongdiqing", "zhidou", "shaonan", "lianzai", "qihuanmaoxian", "gufengchuanyue", "langmanaiqing", "guzhuang",
|
||||
"youmogaoxiao", "ouxiang", "xiaojiangshi", "BL", "shaonian", "juwei", "qinggan", "jingdian",
|
||||
"fuhei", "dushidanvzhu", "zhiyu2", "meishaonv", "shaoer", "nuanmeng", "changtiao", "xianzhiji", "zhiyinmanke",
|
||||
"kejin", "dujia", "qinqing", "xiandai", "wuxiaxianxia", "xihuan", "chaojiyingxiong", "nvshen", "huanxiang",
|
||||
"oufeng", "yangcheng", "dongzuomaoxian", "GL", "judiao", "xuanyilingyi", "gudaigongting", "oushigongting",
|
||||
"youxijingji", "juxi", "qihuanaiqing", "jiakongshijie", "unknown", "furui", "xiuji", "xiandaiyanqing", "gudaiyanqing",
|
||||
"haomenzongcai", "xianyanmengbao", "xuanhuanyanqing", "nuezha", "tuanchong", "guyanmengbao", "xianyantianchong",
|
||||
"guyannaodong", "AA", "jinshouzhi", "xuanhuannaodong", "dushinaodong", "tianchong", "lunli", "shengcun", "TL",
|
||||
"xuanyinaodong", "heian", "dute", "chengzhang", "huanxiangyanqing", "zhibo", "youxitiyu", "xianyannaodong",
|
||||
"yinyue", "shuangnanzhu", "dihua", "LGBTQ", "zhengnengliang", "junshi", "ABO", "xuanyikongbu", "xuanhuankehuan", "tougao",
|
||||
"zhongtian", "jingying", "fantaolu", "wujiecao", "qiangqiang", "kesulu", "wudiliu", "maoxianrexue", "changxiao",
|
||||
"darenxi", "zhaixiang", "mengwa", "chongshou", "yixing", "satang", "guiyi", "yanqing", "xifang", "huajigaoxiao", "tongju",
|
||||
"renwai", "baiqiehei", "bingjianzuozhan", "jiushu", "xijing", "meiqiangcan", "feirenlei", "yuanchuang", "heibaiman",
|
||||
"wuxianliu", "shengji", "shuang", "qingju", "nvdi", "pianzhi", "ziyou", "xingji", "keyanketian", "fanchameng", "congying",
|
||||
"zhishangzaixian", "juejiang", "langren", "huanxiyuanjia", "xixiegui", "mengchong", "xuexiao", "taiwanzuopin", "caise",
|
||||
"wushu", "duanpian", "qiyue", "mowang", "wudi", "meinv", "aimei", "wangyou", "zhainan", "zhuizhumengxiang", "maoxianqihuan",
|
||||
"fengpi", "zhonger", "zhaohuan", "fabao", "diaoxi", "guiguai", "zhanyouyu", "yangguang", "yuanqi", "qiangzhiai", "heidao",
|
||||
"majia", "yinyu", "youyu", "zheli", "bingjiao", "xiju", "jianghuenyuan", "xiangaixiangsha", "meng", "SM", "jingxuan", "shengzi",
|
||||
"nianxia", "18xianzhi", "rijiushengqing", "mengxiang", "duogong", "zhuma", "guke", "gnbq"
|
||||
],
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
parseComics(html, onePage = false) {
|
||||
const doc = new HtmlDocument(html);
|
||||
const comics = [];
|
||||
for (let comic of doc.querySelectorAll(".itemBox")) {
|
||||
comics.push(new Comic({
|
||||
id: comic.attributes["data-key"],
|
||||
title: comic.querySelector(".title").text,
|
||||
cover: comic.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
return {comics: comics, maxPage: onePage ? 1 : parseInt(doc.querySelector("#total-page").attributes["value"])};
|
||||
}
|
||||
|
||||
parseList(doc) {
|
||||
const comics = [];
|
||||
for (let comic of doc.querySelectorAll(".list-comic")) {
|
||||
comics.push(new Comic({
|
||||
id: comic.attributes["data-key"],
|
||||
title: comic.querySelector(".txtA").text,
|
||||
cover: comic.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
return comics;
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
categoryComics = {
|
||||
load: async (category, params, options, page) => {
|
||||
if (params.endsWith(".html")) {
|
||||
const res = await Network.get(`${this.baseUrl}${params}`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
return this.parseComics(res.body, true);
|
||||
} else {
|
||||
const res = await Network.get(`${this.baseUrl}/list/?filter=${params}-${options[0]}-${options[1]}-${options[2]}&sort=${options[3]}&page=${page}`);
|
||||
console.warn(`${this.baseUrl}/list/?filter=${params}-${options[0]}-${options[1]}-${options[2]}&sort=${options[3]}&page=${page}`)
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const doc = new HtmlDocument(res.body);
|
||||
return {comics: this.parseList(doc),
|
||||
maxPage: parseInt(doc.querySelector("#total-page").attributes["value"])};
|
||||
}
|
||||
},
|
||||
optionLoader: async (category, params) => {
|
||||
if (!params.endsWith(".html")) {
|
||||
return [
|
||||
{
|
||||
options: [
|
||||
"-全部",
|
||||
"ertong-儿童漫画",
|
||||
"shaonian-少年漫画",
|
||||
"shaonv-少女漫画",
|
||||
"qingnian-青年漫画",
|
||||
"bailingmanhua-白领漫画",
|
||||
"tongrenmanhua-同人漫画"
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"-全部",
|
||||
"wanjie-已完结",
|
||||
"lianzai-连载中",
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"-全部",
|
||||
"rhmh-日韩",
|
||||
"dlmh-大陆",
|
||||
"gtmh-港台",
|
||||
"taiwan-台湾",
|
||||
"ommh-欧美",
|
||||
"hanguo-韩国",
|
||||
"qtmg-其他",
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"update-更新",
|
||||
"post-发布",
|
||||
"click-点击",
|
||||
]
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
const res = await Network.get(`${this.baseUrl}/search/?keywords=${keyword}&sort=${options[0]}&page=${page}`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
return this.parseComics(res.body);
|
||||
},
|
||||
|
||||
// provide options for search
|
||||
optionList: [
|
||||
{
|
||||
options: [
|
||||
"update-更新",
|
||||
"post-发布",
|
||||
"click-点击",
|
||||
],
|
||||
label: "排序"
|
||||
}
|
||||
],
|
||||
|
||||
// enable tags suggestions
|
||||
enableTagsSuggestions: false,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
const res = await Network.get(`${this.baseUrl}/comic/${id}.html`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const doc = new HtmlDocument(res.body);
|
||||
const title = doc.querySelector(".BarTit").text;
|
||||
const cover = doc.querySelector(".pic").querySelector("img").attributes["src"];
|
||||
const description = doc.querySelector("#full-des")?.text;
|
||||
const infos = doc.querySelectorAll(".txtItme");
|
||||
const tags = [];
|
||||
for (let tag of doc.querySelector(".sub_r").querySelectorAll("a")) {
|
||||
const tag_name = tag.text;
|
||||
if (tag_name.length > 0) {
|
||||
tags.push(tag_name);
|
||||
}
|
||||
}
|
||||
const chapters = {};
|
||||
const chapterElements = doc.querySelector(".chapter-warp")?.querySelectorAll("li");
|
||||
if (chapterElements) {
|
||||
for (let ch of chapterElements) {
|
||||
const id = ch.querySelector("a").attributes["href"].replace("/comic/", "").replace(".html", "").split("/").join("_");
|
||||
chapters[id] = ch.querySelector("span").text;
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: {
|
||||
"作者": [infos[0].text.replaceAll("\n", "").replaceAll("\r", "").trim()],
|
||||
"更新": [infos[3].querySelector(".date").text],
|
||||
"标签": tags.slice(0,-1)
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: this.parseList(doc)
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
loadEp: async (comicId, epId) => {
|
||||
const ids = epId.split("_");
|
||||
const res = await Network.get(`${this.baseUrl}/comic/${ids[0]}/${ids[1]}.html`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const html = res.body;
|
||||
const start = html.search(`var chapterImages = `) + 22;
|
||||
const end = html.search(`;var chapterPath = `) - 2;
|
||||
const end2 = html.search(`;var chapterPrice`) - 1;
|
||||
const images = html.substring(start, end).split(`","`);
|
||||
const cpath = html.substring(end + 22, end2);
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
images[i] = "https://gmh1234.wszwhg.net/" + cpath + images[i].replaceAll("\\", "");
|
||||
images[i] = images[i].replaceAll("//", "/");
|
||||
}
|
||||
return { images };
|
||||
},
|
||||
|
||||
// enable tags translate
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
}
|
||||
36
zaimanhua.js
36
zaimanhua.js
@@ -2,7 +2,7 @@ class Zaimanhua extends ComicSource {
|
||||
// 基础信息
|
||||
name = "再漫画";
|
||||
key = "zaimanhua";
|
||||
version = "1.0.1";
|
||||
version = "1.0.2";
|
||||
minAppVersion = "1.0.0";
|
||||
url =
|
||||
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
|
||||
@@ -16,8 +16,31 @@ class Zaimanhua extends ComicSource {
|
||||
}
|
||||
// 构建 URL
|
||||
buildUrl(path) {
|
||||
this.signTask();
|
||||
return `https://v4api.zaimanhua.com/app/v1/${path}`;
|
||||
}
|
||||
// 每日签到
|
||||
async signTask() {
|
||||
if (!this.isLogged) {
|
||||
return;
|
||||
}
|
||||
if (!this.loadSetting("signTask")) {
|
||||
return;
|
||||
}
|
||||
const lastSign = this.loadData("lastSign");
|
||||
const newTime = new Date().toISOString().split("T")[0];
|
||||
if (lastSign == newTime) {
|
||||
return;
|
||||
}
|
||||
const res = await Network.post("https://i.zaimanhua.com/lpi/v1/task/sign_in", this.headers);
|
||||
if (res.status !== 200) {
|
||||
return;
|
||||
}
|
||||
this.saveData("lastSign", newTime);
|
||||
if (JSON.parse(res.body)["errno"] == 0) {
|
||||
UI.showMessage("签到成功");
|
||||
}
|
||||
}
|
||||
|
||||
//账户管理
|
||||
account = {
|
||||
@@ -368,7 +391,8 @@ class Zaimanhua extends ComicSource {
|
||||
},
|
||||
loadEp: async (comicId, epId) => {
|
||||
const res = await Network.get(
|
||||
this.buildUrl(`comic/chapter/${comicId}/${epId}`)
|
||||
this.buildUrl(`comic/chapter/${comicId}/${epId}`),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body).data.data;
|
||||
return { images: data.page_url_hd || data.page_url };
|
||||
@@ -487,4 +511,12 @@ class Zaimanhua extends ComicSource {
|
||||
return "ok";
|
||||
},
|
||||
};
|
||||
|
||||
settings = {
|
||||
signTask: {
|
||||
title: "每日签到",
|
||||
type: "switch",
|
||||
default: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user