class Comick extends ComicSource { name = "comick" key = "comick" version = "1.0.0" minAppVersion = "1.4.0" // update url url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/comick.js" settings = { domains: { title: "主页源", type: "select", options: [ {value: "comick.io"}, {value: "preview.comick.io"} ], default: "preview.comick.io" }, // language: { // title: "标题语言", // type: "select", // options: [ // { // value: '中文', // text: 'zh', // }, // { // value: '韩文', // text: 'ko', // }, // { // value: '英文', // text: 'en', // }, // ], // default: 'en', // }, } get baseUrl() { let domain = this.loadSetting('domains') || this.settings.domains.default; return `https://${domain}`; } static comic_status = { "1": "连载", "2": "完结", "3": "休刊", "4": "暂停更新", } static category_param_dict = { "romance": "浪漫", "comedy": "喜剧", "drama": "剧情", "fantasy": "奇幻", "slice-of-life": "日常", "action": "动作", "adventure": "冒险", "psychological": "心理", "mystery": "悬疑", "historical": "历史", "tragedy": "悲剧", "sci-fi": "科幻", "horror": "恐怖", "isekai": "异世界", "sports": "运动", "thriller": "惊悚", "mecha": "机甲", "philosophical": "哲学", "wuxia": "武侠", "medical": "医疗", "magical-girls": "魔法少女", "superhero": "超级英雄", "shounen-ai": "少年爱", "mature": "成年", "gender-bender": "性转", "shoujo-ai": "少女爱", "oneshot": "单篇", "web-comic": "网络漫画", "doujinshi": "同人志", "full-color": "全彩", "long-strip": "长条", "adaptation": "改编", "anthology": "选集", "4-koma": "四格", "user-created": "用户创作", "award-winning": "获奖", "official-colored": "官方上色", "fan-colored": "粉丝上色", "school-life": "校园生活", "supernatural": "超自然", "magic": "魔法", "monsters": "怪物", "martial-arts": "武术", "animals": "动物", "demons": "恶魔", "harem": "后宫", "reincarnation": "转生", "office-workers": "上班族", "survival": "生存", "military": "军事", "crossdressing": "女装", "loli": "萝莉", "shota": "正太", "yuri": "百合", "yaoi": "耽美", "video-games": "电子游戏", "monster-girls": "魔物娘", "delinquents": "不良少年", "ghosts": "幽灵", "time-travel": "时间旅行", "cooking": "烹饪", "police": "警察", "aliens": "外星人", "music": "音乐", "mafia": "黑帮", "vampires": "吸血鬼", "samurai": "武士", "post-apocalyptic": "后末日", "gyaru": "辣妹", "villainess": "恶役千金", "reverse-harem": "逆后宫", "ninja": "忍者", "zombies": "僵尸", "traditional-games": "传统游戏", "virtual-reality": "虚拟现实", "adult": "成人", "ecchi": "情色", "sexual-violence": "性暴力", "smut": "肉欲", } transformBookList(bookList, descriptionPrefix = "更新至:") { return bookList.map(book => ({ id: `${book.slug}//${book.title}`, title: book.title, cover: book.md_covers?.[0]?.b2key ? `https://meo.comick.pictures/${book.md_covers[0].b2key}` : 'w7xqzd.jpg', tags: [], description: `${descriptionPrefix}${book.last_chapter || "未知"}` })); } getFormattedManga(manga) { return { id: `${manga.slug}//${manga.title}`, title: manga.title || "无标题", cover: manga.md_covers?.[0]?.b2key ? `https://meo.comick.pictures/${manga.md_covers[0].b2key}` : 'w7xqzd.jpg', tags: [], description: manga.desc || "暂无描述" }; } explore = [{ title: "comick", type: "singlePageWithMultiPart", load: async () => { let url = this.baseUrl === "https://comick.io" ? "https://comick.io/home2" : this.baseUrl; let res = await Network.get(url); if (res.status !== 200) throw "Request Error: " + res.status; let document = new HtmlDocument(res.body); let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text); let mangaData = jsonData.props.pageProps.data; // 使用统一函数转换数据 const result = { "最近热门": this.transformBookList(mangaData.recentRank), "总热门": this.transformBookList(mangaData.rank), "最近上传": this.transformBookList(mangaData.news), "最近更新": this.transformBookList(mangaData.extendedNews), "完结": this.transformBookList(mangaData.completions) }; return result; } }] // categories category = { title: "comick", parts: [{ name: "类型", type: "fixed", categories: Object.values(Comick.category_param_dict), // 使用上方的字典 itemType: "category", categoryParams: Object.keys(Comick.category_param_dict), }], enableRankingPage: false, } categoryComics = { load: async (category, param, options, page) => { // 基础URL let url = "https://api.comick.io/v1.0/search?"; let params = [ `genres=${encodeURIComponent(param)}`, `page=${encodeURIComponent(page)}` ]; if (options[0] && options[0] !== "-全部") { params.push(`country=${encodeURIComponent(options[0].split("-")[0])}`); } if (options[1]) { params.push(`status=${encodeURIComponent(options[1].split("-")[0])}`); } url += params.join('&'); let res = await Network.get(url); if (res.status !== 200) throw "Request Error: " + res.status; let mangaList = JSON.parse(res.body); if (!Array.isArray(mangaList)) throw "Invalid data format"; return { comics: mangaList.map(this.getFormattedManga), maxPage: 50 }; }, optionList: [ {options: ["-全部", "cn-国漫", "jp-日本", "kr-韩国", "others-欧美"]}, {options: ["1-连载", "2-完结", "3-休刊", "4-暂停更新"]} ] } /// search related search = { load: async (keyword, options, page) => { let url = `https://api.comick.io/v1.0/search?q=${keyword}&limit=49&page=${page}`; let res = await Network.get(url); if (res.status !== 200) throw "Request Error: " + res.status; let mangaList = JSON.parse(res.body); if (!Array.isArray(mangaList)) throw "Invalid data format"; return { comics: mangaList.map(this.getFormattedManga), maxPage: 1 }; }, optionList: [] } // favorite related favorites = { // whether support multi folders 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} - return any value to indicate success */ addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => { /* ``` let res = await Network.post('...') if (res.status === 401) { throw `Login expired`; } return 'ok' ``` */ }, /** * load favorite folders. * throw `Login expired` to indicate login expired, App will automatically re-login retry. * if comicId is not null, return favorite folders which contains the comic. * @param comicId {string?} * @returns {Promise<{folders: {[p: string]: string}, favorited: string[]}>} - `folders` is a map of folder id to folder name, `favorited` is a list of folder id which contains the comic */ loadFolders: async (comicId) => { /* ``` let data = JSON.parse((await Network.get('...')).body) let folders = {} data.folders.forEach((f) => { folders[f.id] = f.name }) return { folders: folders, favorited: data.favorited } ``` */ }, /** * add a folder * @param name {string} * @returns {Promise} - return any value to indicate success */ addFolder: async (name) => { /* ``` let res = await Network.post('...') if (res.status === 401) { throw `Login expired`; } return 'ok' ``` */ }, /** * delete a folder * @param folderId {string} * @returns {Promise} - return any value to indicate success */ deleteFolder: async (folderId) => { /* ``` let res = await Network.delete('...') if (res.status === 401) { throw `Login expired`; } 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) => { /* ``` let data = JSON.parse((await Network.get('...')).body) let maxPage = data.maxPage function parseComic(comic) { // ... return new Comic{ id: id, title: title, subTitle: author, cover: cover, tags: tags, description: description } } return { comics: data.list.map(parseComic), maxPage: maxPage } ``` */ }, /** * load comics with next page token * @param next {string | null} - next page token, null for first page * @param folder {string} * @returns {Promise<{comics: Comic[], next: string?}>} */ loadNext: async (next, folder) => { }, /** * If the comic source only allows one comic in one folder, set this to true. */ singleFolderForSingleComic: false, } /// single comic related comic = { // 加载漫画信息 loadInfo: async (id) => { const [cId, cTitle] = id.split("//"); if (!cId) { throw "ID error: "; } let res = await Network.get(`${this.baseUrl}/comic/${cId}`) if (res.status !== 200) { throw "Invalid status code: " + res.status } let document = new HtmlDocument(res.body) let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text); let comicData = jsonData.props.pageProps.comic; let authorData = jsonData.props.pageProps.authors; let title = cTitle? cTitle:comicData?.title|| "未知标题"; let status = comicData?.status || "1"; // 默认连载 let cover = comicData.md_covers?.[0]?.b2key ? `https://meo.comick.pictures/${comicData.md_covers[0].b2key}` : 'w7xqzd.jpg'; let author = authorData[0]?.name || "未知作者"; // 提取标签的slug数组的代码 let extractSlugs = (comicData) => { try { // 获取md_comic_md_genres数组 const genres = comicData.md_comic_md_genres; // 使用map提取每个md_genres中的slug const slugs = genres.map(genre => genre.md_genres.slug); return slugs; } catch (error) { return []; // 返回空数组作为容错处理 } }; let tags = extractSlugs(comicData); // 转换 tags 数组,如果找不到对应值则保留原值 const translatedTags = tags.map(tag => { return Comick.category_param_dict[tag] || tag; // 如果字典里没有,就返回原值 }); let description = comicData.desc || "暂无描述"; if(comicData.chapter_count == 0){ let chapters = new Map() return { title: title, cover: cover, description: description, tags: { "作者": [author], "更新": ["暂无更新"], "标签": translatedTags, "状态": [Comick.comic_status[status]] }, chapters: chapters, } } // let updateTime = comicData.last_chapter ? "第" + comicData.last_chapter + "话" : " "; //这里目前还无法实现更新时间 let buildId = jsonData.buildId; let slug = jsonData.query.slug; let firstChapter = jsonData.props.pageProps.firstChapters[0]; let firstChapters = jsonData.props.pageProps.firstChapters; // 处理无标卷和无标话的情况 if(firstChapter.vol == null && firstChapter.chap == null){ for(let i = 0; i < firstChapters.length; i++) { if(firstChapters[i].vol != null || firstChapters[i].chap != null){ firstChapter = firstChapters[i]; break; } } // 如果处理完成之后依然章节没有卷和话信息,直接返回无标卷 if(firstChapter.vol == null && firstChapter.chap == null){ let chapters = new Map() chapters.set(firstChapter.hid + "//no//-1", "无标卷") return { title: title, cover: cover, description: description, tags: { "作者": [author], "更新": [updateTime], "标签": translatedTags, "状态": [Comick.comic_status[status]] }, chapters: chapters, } } } let chapters_url = `${this.baseUrl}/_next/data/${buildId}/comic/${slug}/${firstChapter.hid}${ firstChapter.chap != null ? `-chapter-${firstChapter.chap}` : `-volume-${firstChapter.vol}` }-${firstChapter.lang}.json`; let list_res = await Network.get(chapters_url) if (list_res.status !== 200) { throw "Invalid status code: " + res.status } let chapters_raw = JSON.parse(list_res.body); let chapters = new Map() // 剩余解析章节信息 //获得更新时间: let updateTime = chapters_raw.pageProps.chapter.updated_at ? chapters_raw.pageProps.chapter.updated_at.split('T')[0] : comicData.last_chapter ? `第${comicData.last_chapter}话`: " "; let chaptersList = chapters_raw.pageProps.chapters || []; let chapters_next = chaptersList.reverse(); chapters_next.forEach((chapter, index) => { if(chapter.chap==null && chapter.vol==null) { let chapNum = "无标卷"; chapters.set(chapter.hid + "//no//-1", chapNum); }else if(chapter.chap!=null && chapter.vol==null){ let chapNum = "第" + chapter.chap + "话" ; chapters.set(chapter.hid + "//chapter//" + chapter.chap + "//" + firstChapter.lang, chapNum); }else if(chapter.chap==null && chapter.vol!==null){ let chapNum = "第" + chapter.vol + "卷" ; chapters.set(chapter.hid + "//volume//" + chapter.vol + "//" + firstChapter.lang, chapNum); }else{ let chapNum = "第" + chapter.chap + "话" ; chapters.set(chapter.hid + "//chapter//" + chapter.chap + "//" + firstChapter.lang, chapNum); } }); return { title: title, cover: cover, description: description, tags: { "作者": [author], "更新": [updateTime], "标签": translatedTags, "状态": [Comick.comic_status[status]] }, chapters: chapters, } }, loadEp: async (comicId, epId) => { const [cId, cTitle] = comicId.split("//"); if (!cId) { throw "ID error: "; } const images = []; const [hid, type, chapter, lang] = epId.split("//"); // 检查分割结果是否有效 if (!hid || !type || !chapter || !lang) { console.error("Invalid epId format. Expected 'hid//chapter'"); return {images}; // 返回空数组 } let url = " "; if(type=="no"){ // 如果是无标卷, 只看第一个 url = `${this.baseUrl}/comic/${cId}/${hid}`; }else{ url = `${this.baseUrl}/comic/${cId}/${hid}-${type}-${chapter}-${lang}.json`; } let maxAttempts = 100; while (maxAttempts > 0) { const res = await Network.get(url); if (res.status !== 200) break; let document = new HtmlDocument(res.body) let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text); //json解析方式 let imagesData = jsonData.props.pageProps.chapter.md_images; // 解析当前页图片 imagesData.forEach(image => { // 处理图片链接 let imageUrl = `https://meo.comick.pictures/${image.b2key}`; images.push(imageUrl); }); // 查找下一页链接 const nextLink = document.querySelector("a#next-chapter"); if (nextLink?.text?.match(/下一页|下一頁/)) { url = nextLink.attributes['href']; } else { break; } maxAttempts--; } return {images}; }, onClickTag: (namespace, tag) => { if (namespace === "标签") { return { action: 'category', keyword: `${tag}`, param: null, } } throw "Click Tag Error" }, /** * [Optional] Handle links */ link: { /** * set accepted domains */ domains: [ 'example.com' ], /** * parse url to comic id * @param url {string} * @returns {string | null} */ linkToId: (url) => { } }, } }