From 1eed103f2761c31cd5864c9ab6c568ee7a164310 Mon Sep 17 00:00:00 2001 From: nyne <67669799+wgh136@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:48:18 +0800 Subject: [PATCH] Create komiic.js --- komiic.js | 441 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 komiic.js diff --git a/komiic.js b/komiic.js new file mode 100644 index 0000000..ace285b --- /dev/null +++ b/komiic.js @@ -0,0 +1,441 @@ +class Komiic extends ComicSource { + + // 此漫画源的名称 + name = "Komiic" + + // 唯一标识符 + key = "Komiic" + + version = "1.0.0" + + minAppVersion = "1.0.0" + + // 更新链接 + url = "https://raw.githubusercontent.com/venera-app/venera_configs/master/komiic.js" + + get headers() { + let token = this.loadData('token') + let headers = { + 'Referer': 'https://komiic.com/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Content-Type': 'application/json' + } + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + return headers + } + + async queryJson(query) { + let operationName = query["operationName"] + + let res = await Network.post( + 'https://komiic.com/api/query', + this.headers, + query + ) + + if (res.status !== 200) { + throw `Invalid Status Code ${res.status}` + } + + let json = JSON.parse(res.body) + + if (json.errors != undefined) { + if(json.errors[0].message.toString().indexOf('token is expired') >= 0){ + throw 'Login expired' + } + throw json.errors[0].message + } + + return json + } + + async queryComics(query) { + let operationName = query["operationName"] + let json = await this.queryJson(query) + + function parseComic(comic) { + let author = '' + if (comic.authors.length > 0) { + author = comic.authors[0].name + } + let tags = [] + comic.categories.forEach((c) => { + tags.push(c.name) + }) + + function getTimeDifference(date) { + const now = new Date(); + const timeDifference = now - date; + + const millisecondsPerHour = 1000 * 60 * 60; + const millisecondsPerDay = millisecondsPerHour * 24; + + if (timeDifference < millisecondsPerHour) { + return '剛剛更新'; + } else if (timeDifference < millisecondsPerDay) { + const hours = Math.floor(timeDifference / millisecondsPerHour); + return `${hours}小時前更新`; + } else { + const days = Math.floor(timeDifference / millisecondsPerDay); + return `${days}天前更新`; + } + } + + let updateTime = new Date(comic.dateUpdated) + let description = getTimeDifference(updateTime) + + return { + id: comic.id, + title: comic.title, + subTitle: author, + cover: comic.imageUrl, + tags: tags, + description: description + } + } + + return { + comics: json.data[operationName].map(parseComic), + // 没找到最大页数的接口 + maxPage: null + } + } + + /// 账号 + /// 设置为null禁用账号功能 + account = { + /// 登录 + /// 返回任意值表示登录成功 + login: async (account, pwd) => { + let res = await Network.post( + 'https://komiic.com/api/login', + this.headers, + { + email: account, + password: pwd + } + ) + + if (res.status === 200) { + this.saveData('token', JSON.parse(res.body).token) + return 'ok' + } + + throw 'Failed to login' + }, + + // 退出登录时将会调用此函数 + logout: () => { + this.deleteData('token') + }, + + registerWebsite: "https://komiic.com/register" + } + + /// 探索页面 + /// 一个漫画源可以有多个探索页面 + explore = [ + { + /// 标题 + /// 标题同时用作标识符, 不能重复 + title: "Komiic", + + /// singlePageWithMultiPart 或者 multiPageComicList + type: "multiPageComicList", + + load: async (page) => { + return await this.queryComics({ "operationName": "recentUpdate", "variables": { "pagination": { "limit": 20, "offset": (page - 1) * 20, "orderBy": "DATE_UPDATED", "status": "", "asc": true } }, "query": "query recentUpdate($pagination: Pagination!) {\n recentUpdate(pagination: $pagination) {\n id\n title\n status\n year\n imageUrl\n authors {\n id\n name\n __typename\n }\n categories {\n id\n name\n __typename\n }\n dateUpdated\n monthViews\n views\n favoriteCount\n lastBookUpdate\n lastChapterUpdate\n __typename\n }\n}" }) + } + } + ] + + category = { + title: "Komiic", + enableRankingPage: true, + parts: [ + { + name: "主题", + + type: "fixed", + + categories: ['全部', '愛情', '神鬼', '校園', '搞笑', '生活', '懸疑', '冒險', '職場', '魔幻', '後宮', '魔法', '格鬥', '宅男', '勵志', '耽美', '科幻', '百合', '治癒', '萌系', '熱血', '競技', '推理', '雜誌', '偵探', '偽娘', '美食', '恐怖', '四格', '社會', '歷史', '戰爭', '舞蹈', '武俠', '機戰', '音樂', '體育', '黑道'], + + itemType: "category", + + // 若提供, 数量需要和`categories`一致, `categoryComics.load`方法将会收到此参数 + categoryParams: ['0', '1', '3', '4', '5', '6', '7', '8', '10', '11', '2', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '9', '28', '31', '32', '33', '34', '35', '36', '37', '40', '42'] + } + ] + } + + /// 分类漫画页面, 即点击分类标签后进入的页面 + categoryComics = { + load: async (category, param, options, page) => { + return await this.queryComics({ "operationName": "comicByCategory", "variables": { "categoryId": param, "pagination": { "limit": 30, "offset": (page - 1) * 30, "orderBy": options[0], "asc": false, "status": options[1] } }, "query": "query comicByCategory($categoryId: ID!, $pagination: Pagination!) {\n comicByCategory(categoryId: $categoryId, pagination: $pagination) {\n id\n title\n status\n year\n imageUrl\n authors {\n id\n name\n __typename\n }\n categories {\n id\n name\n __typename\n }\n dateUpdated\n monthViews\n views\n favoriteCount\n lastBookUpdate\n lastChapterUpdate\n __typename\n }\n}" }) + }, + // 提供选项 + optionList: [ + { + options: [ + "DATE_UPDATED-更新", + "VIEWS-觀看數", + "FAVORITE_COUNT-喜愛數", + ], + notShowWhen: null, + showWhen: null + }, + { + options: [ + "-全部", + "ONGOING-連載中", + "END-完結", + ], + notShowWhen: null, + showWhen: null + }, + ], + ranking: { + options: [ + "MONTH_VIEWS-月", + "VIEWS-綜合" + ], + load: async (option, page) => { + return this.queryComics({ "operationName": "hotComics", "variables": { "pagination": { "limit": 20, "offset": (page - 1) * 20, "orderBy": option, "status": "", "asc": true } }, "query": "query hotComics($pagination: Pagination!) {\n hotComics(pagination: $pagination) {\n id\n title\n status\n year\n imageUrl\n authors {\n id\n name\n __typename\n }\n categories {\n id\n name\n __typename\n }\n dateUpdated\n monthViews\n views\n favoriteCount\n lastBookUpdate\n lastChapterUpdate\n __typename\n }\n}" }) + } + } + } + + /// 搜索 + search = { + load: async (keyword, options, page) => { + let json = await this.queryJson({ "operationName": "searchComicAndAuthorQuery", "variables": { "keyword": keyword }, "query": "query searchComicAndAuthorQuery($keyword: String!) {\n searchComicsAndAuthors(keyword: $keyword) {\n comics {\n id\n title\n status\n year\n imageUrl\n authors {\n id\n name\n __typename\n }\n categories {\n id\n name\n __typename\n }\n dateUpdated\n monthViews\n views\n favoriteCount\n lastBookUpdate\n lastChapterUpdate\n __typename\n }\n authors {\n id\n name\n chName\n enName\n wikiLink\n comicCount\n views\n __typename\n }\n __typename\n }\n}" }) + + function parseComic(comic) { + let author = '' + if (comic.authors.length > 0) { + author = comic.authors[0].name + } + let tags = [] + comic.categories.forEach((c) => { + tags.push(c.name) + }) + + function getTimeDifference(date) { + const now = new Date(); + const timeDifference = now - date; + + const millisecondsPerHour = 1000 * 60 * 60; + const millisecondsPerDay = millisecondsPerHour * 24; + + if (timeDifference < millisecondsPerHour) { + return '剛剛更新'; + } else if (timeDifference < millisecondsPerDay) { + const hours = Math.floor(timeDifference / millisecondsPerHour); + return `${hours}小時前更新`; + } else { + const days = Math.floor(timeDifference / millisecondsPerDay); + return `${days}天前更新`; + } + } + + let updateTime = new Date(comic.dateUpdated) + let description = getTimeDifference(updateTime) + + return { + id: comic.id, + title: comic.title, + subTitle: author, + cover: comic.imageUrl, + tags: tags, + description: description + } + } + + return { + comics: json.data.searchComicsAndAuthors.comics.map(parseComic), + // 没找到最大页数的接口 + maxPage: 1 + } + }, + + optionList: [] + } + + /// 收藏 + favorites = { + /// 是否为多收藏夹 + multiFolder: true, + /// 添加或者删除收藏 + addOrDelFavorite: async (comicId, folderId, isAdding) => { + let query = {} + if (isAdding) { + query = { "operationName": "addComicToFolder", "variables": { "comicId": comicId, "folderId": folderId }, "query": "mutation addComicToFolder($comicId: ID!, $folderId: ID!) {\n addComicToFolder(comicId: $comicId, folderId: $folderId)\n}" } + } else { + query = { "operationName": "removeComicToFolder", "variables": { "comicId": comicId, "folderId": folderId }, "query": "mutation removeComicToFolder($comicId: ID!, $folderId: ID!) {\n removeComicToFolder(comicId: $comicId, folderId: $folderId)\n}" } + } + await this.queryJson(query) + return "ok" + }, + // 加载收藏夹, 仅当multiFolder为true时有效 + // 当comicId不为null时, 需要同时返回包含该漫画的收藏夹 + loadFolders: async (comicId) => { + let json = await this.queryJson({ "operationName": "myFolder", "variables": {}, "query": "query myFolder {\n folders {\n id\n key\n name\n views\n comicCount\n dateCreated\n dateUpdated\n __typename\n }\n}" }) + let folders = {} + json.data.folders.forEach((f) => { + folders[f.id] = f.name + }) + let favorited = null + if (comicId) { + let json2 = await this.queryJson({ "operationName": "comicInAccountFolders", "variables": { "comicId": comicId }, "query": "query comicInAccountFolders($comicId: ID!) {\n comicInAccountFolders(comicId: $comicId)\n}" }) + favorited = json2.data.comicInAccountFolders + } + return { + folders: folders, + favorited: favorited + } + }, + /// 创建收藏夹 + addFolder: async (name) => { + let json = await this.queryJson({ "operationName": "createFolder", "variables": { "name": name }, "query": "mutation createFolder($name: String!) {\n createFolder(name: $name) {\n id\n key\n name\n account {\n id\n nickname\n __typename\n }\n comicCount\n views\n dateCreated\n dateUpdated\n __typename\n }\n}" }) + return "ok" + }, + deleteFolder: async (id) => { + let json = await this.queryJson({ "operationName": "removeFolder", "variables": { "folderId": id }, "query": "mutation removeFolder($folderId: ID!) {\n removeFolder(folderId: $folderId)\n}" }) + return "ok" + }, + /// 加载漫画 + loadComics: async (page, folder) => { + let json = await this.queryJson({ "operationName": "folderComicIds", "variables": { "folderId": folder, "pagination": { "limit": 30, "offset": (page - 1) * 30, "orderBy": "DATE_UPDATED", "status": "", "asc": true } }, "query": "query folderComicIds($folderId: ID!, $pagination: Pagination!) {\n folderComicIds(folderId: $folderId, pagination: $pagination) {\n folderId\n key\n comicIds\n __typename\n }\n}" }) + let ids = json.data.folderComicIds.comicIds + if (ids.length == 0) { + return { + comics: [], + maxPage: 1 + } + } + return this.queryComics({ "operationName": "comicByIds", "variables": { "comicIds": ids }, "query": "query comicByIds($comicIds: [ID]!) {\n comicByIds(comicIds: $comicIds) {\n id\n title\n status\n year\n imageUrl\n authors {\n id\n name\n __typename\n }\n categories {\n id\n name\n __typename\n }\n dateUpdated\n monthViews\n views\n favoriteCount\n lastBookUpdate\n lastChapterUpdate\n __typename\n }\n}" }) + } + } + + /// 单个漫画相关 + comic = { + // 加载漫画信息 + loadInfo: async (id) => { + let json1 = await this.queryJson({ "operationName": "recommendComicById", "variables": { "comicId": id }, "query": "query recommendComicById($comicId: ID!) {\n recommendComicById(comicId: $comicId)\n}" }) + let recommend = json1.data.recommendComicById + recommend.push(id) + + let getFavoriteStatus = async () => { + let token = this.loadData('token') + if (!token) { + return false + } + let json = await this.queryJson({ "operationName": "comicInAccountFolders", "variables": { "comicId": id }, "query": "query comicInAccountFolders($comicId: ID!) {\n comicInAccountFolders(comicId: $comicId)\n}" }) + let folders = json.data.comicInAccountFolders + return folders.length !== 0 + } + + let getChapter = async () => { + let json = await this.queryJson({ "operationName": "chapterByComicId", "variables": { "comicId": id }, "query": "query chapterByComicId($comicId: ID!) {\n chaptersByComicId(comicId: $comicId) {\n id\n serial\n type\n dateCreated\n dateUpdated\n size\n __typename\n }\n}" }) + let all = json.data.chaptersByComicId + let books = [], chapters = [] + all.forEach((c) => { + if(c.type == 'book') { + books.push(c) + } else { + chapters.push(c) + } + }) + let res = new Map() + books.forEach((c) => { + let name = '卷' + c.serial + res.set(c.id, name) + }) + chapters.forEach((c) => { + let name = c.serial + res.set(c.id, name) + }) + return res + } + + let results = await Promise.all([ + this.queryComics({ "operationName": "comicByIds", "variables": { "comicIds": recommend }, "query": "query comicByIds($comicIds: [ID]!) {\n comicByIds(comicIds: $comicIds) {\n id\n title\n status\n year\n imageUrl\n authors {\n id\n name\n __typename\n }\n categories {\n id\n name\n __typename\n }\n dateUpdated\n monthViews\n views\n favoriteCount\n lastBookUpdate\n lastChapterUpdate\n __typename\n }\n}" }), + getChapter.call() + ]) + + let info = results[0].comics.pop() + + return { + // string 标题 + title: info.title, + // string 封面url + cover: info.cover, + // map 标签 + tags: { + "作者": [info.subTitle], + "更新": [info.description], + "标签": info.tags + }, + // map?, key为章节id, value为章节名称 + chapters: results[1], + recommend: results[0].comics + } + }, + // 获取章节图片 + loadEp: async (comicId, epId) => { + let json = await this.queryJson({ "operationName": "imagesByChapterId", "variables": { "chapterId": epId }, "query": "query imagesByChapterId($chapterId: ID!) {\n imagesByChapterId(chapterId: $chapterId) {\n id\n kid\n height\n width\n __typename\n }\n}" }) + return { + images: json.data.imagesByChapterId.map((i) => { + return `https://komiic.com/api/image/${i.kid}` + }) + } + }, + // 可选, 调整图片加载的行为 + onImageLoad: (url, comicId, epId) => { + return { + headers: { + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'referer': `https://komiic.com/comic/${comicId}/chapter/${epId}/images/all` + } + } + }, + // 加载评论 + loadComments: async (comicId, subId, page, replyTo) => { + let operationName = replyTo ? "messageChan" : "getMessagesByComicId" + let promise = replyTo + ? this.queryJson({ "operationName": "messageChan", "variables": { "messageId": replyTo }, "query": "query messageChan($messageId: ID!) {\n messageChan(messageId: $messageId) {\n id\n comicId\n account {\n id\n nickname\n profileText\n profileTextColor\n profileBackgroundColor\n profileImageUrl\n __typename\n }\n message\n replyTo {\n id\n __typename\n }\n upCount\n downCount\n dateUpdated\n dateCreated\n __typename\n }\n}" }) + : this.queryJson({ "operationName": "getMessagesByComicId", "variables": { "comicId": comicId, "pagination": { "limit": 100, "offset": (page - 1) * 100, "orderBy": "DATE_UPDATED", "asc": true } }, "query": "query getMessagesByComicId($comicId: ID!, $pagination: Pagination!) {\n getMessagesByComicId(comicId: $comicId, pagination: $pagination) {\n id\n comicId\n account {\n id\n nickname\n profileText\n profileTextColor\n profileBackgroundColor\n profileImageUrl\n __typename\n }\n message\n replyTo {\n id\n message\n account {\n id\n nickname\n profileText\n profileTextColor\n profileBackgroundColor\n profileImageUrl\n __typename\n }\n __typename\n }\n upCount\n downCount\n dateUpdated\n dateCreated\n __typename\n }\n}" }) + let json = await promise + return { + comments: json.data[operationName].map(e => { + return { + // string + userName: e.account.nickname, + // string + avatar: e.account.profileImageUrl, + // string + content: e.message, + // string? + time: e.dateUpdated, + // number? + // TODO: 没有数量信息, 但是设为null会禁用回复功能 + replyCount: 0, + // string + id: e.id, + } + }), + maxPage: null, + } + }, + // 发送评论, 返回任意值表示成功 + sendComment: async (comicId, subId, content, replyTo) => { + if (!replyTo) { + replyTo = "0" + } + let json = await this.queryJson({ "operationName": "addMessageToComic", "variables": { "comicId": comicId, "message": content, "replyToId": replyTo }, "query": "mutation addMessageToComic($comicId: ID!, $replyToId: ID!, $message: String!) {\n addMessageToComic(message: $message, comicId: $comicId, replyToId: $replyToId) {\n id\n message\n comicId\n account {\n id\n nickname\n __typename\n }\n replyTo {\n id\n message\n account {\n id\n nickname\n profileText\n profileTextColor\n profileBackgroundColor\n profileImageUrl\n __typename\n }\n __typename\n }\n dateCreated\n dateUpdated\n __typename\n }\n}" }) + return "ok" + } + } +}