├── CopymangaDownloader.py ├── CopymangaDownloader_GUI.py ├── README.md ├── requirements.txt ├── sample1.png ├── sample2.png └── sample3.png /CopymangaDownloader.py: -------------------------------------------------------------------------------- 1 | #By Animuses 2 | from encodings import utf_8 3 | import os 4 | import json 5 | import requests 6 | from urllib import parse 7 | import re 8 | from Crypto.Cipher import AES 9 | import binascii 10 | import threading 11 | 12 | 13 | #创建函数 14 | #联网检测函数 15 | def grab(mainurl): 16 | #当前有效网址 17 | print ("确认当前网站地址有效性: " + mainurl) 18 | confirm1=str(requests.get(f'https://{mainurl}')) 19 | confirm2="" 20 | if confirm1==confirm2: 21 | print('当前地址有效!') 22 | else: 23 | mainurl = input('当前网站地址失效,在此输入新的地址') 24 | grab(mainurl) 25 | 26 | #下载函数 27 | def download(num): 28 | print(f'开始下载{num}:{namelist[num]}') 29 | counter = 1 30 | filename = str(counter).zfill(4) 31 | cpurl = f'https://api.{mainurl}/api/v3/comic/{pathword}/chapter/{uuidlist[num]}' 32 | reqget = requests.get(cpurl) 33 | getjson = reqget.json() 34 | jsonfilter=getjson['results']['chapter']['contents'] 35 | if os.path.isdir(f"./{manganame}/{namelist[num]}"): 36 | pass 37 | else: 38 | os.mkdir(f"./{manganame}/{namelist[num]}") 39 | for listname in jsonfilter: 40 | dlurl = listname['url'] 41 | cache = requests.get(dlurl) 42 | with open(f"./{manganame}/{namelist[num]}/{filename}.jpg",'wb') as w: 43 | w.write(cache.content) 44 | w.close 45 | print(f"成功下载图片:{namelist[num]},{filename}.jpg") 46 | counter = counter + 1 47 | filename=str(counter).zfill(4) 48 | 49 | 50 | #联网检测 51 | mainurl = 'copymanga.site' 52 | grab(mainurl) 53 | 54 | #漫画搜索 55 | searchname=parse.quote(input('请输入你想要搜索的漫画名称:')) 56 | searchurl = f'https://{mainurl}/api/kb/web/searchs/comics?offset=0&platform=2&limit=12&q={searchname}' 57 | searchreq = requests.get(searchurl) 58 | searchjson = searchreq.json() 59 | resultlist = searchjson['results']['list'] 60 | scounter = 1 61 | pathwordlist = [0] 62 | manganamelist = [0] 63 | for results in resultlist: 64 | resultname = results['name'] 65 | resultalias = results['alias'] 66 | resultauthorlist = results['author'] 67 | pathwords = results['path_word'] 68 | for resultauthors in resultauthorlist: 69 | resultauthor = resultauthors['name'] 70 | break 71 | print(f'\n{scounter}:\n名称:{resultname}\n又名:{resultalias}\n作者:{resultauthor}') 72 | pathwordlist.insert(scounter,pathwords) 73 | manganamelist.insert(scounter,resultname) 74 | scounter = scounter + 1 75 | 76 | #选择搜索结果 77 | selector=int(input('请输入选择的漫画序号:')) 78 | manganame = manganamelist[selector] 79 | pathword = parse.quote(pathwordlist[selector]) 80 | print(f'已选择:{manganame},漫画路径为{pathword}') 81 | 82 | #拉取加密章节列表 83 | cipherurl = f'https://{mainurl}/comicdetail/{pathword}/chapters' 84 | cipherreq = requests.get(cipherurl) 85 | cipherjson = cipherreq.json() 86 | listcipher = cipherjson['results'] 87 | 88 | #解密 89 | key = 'xxxmanga.woo.key'.encode('utf_8') 90 | iv = listcipher[:16].encode('utf_8') 91 | cipher = binascii.a2b_hex(listcipher[16:]) 92 | mode = AES.MODE_CBC 93 | aes = AES.new(key, mode, iv) 94 | cplistb = aes.decrypt(cipher) 95 | cplist = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff]',"",cplistb.decode('utf-8')) 96 | 97 | 98 | #处理章节列表并显示 99 | listjson = json.loads(cplist) 100 | groups = listjson['groups'] 101 | groupnum = 1 102 | cpnum = 1 103 | namelist = [0] 104 | uuidlist = [0] 105 | for grouplist in groups: 106 | groupname = groups[grouplist]['name'] 107 | print(f'\n{groupnum}:{groupname}') 108 | groupnum = groupnum + 1 109 | 110 | for chapterlist in groups[grouplist]['chapters']: 111 | chaptername = chapterlist.get('name') 112 | chapteruuid = parse.quote(chapterlist['id']) 113 | namelist.insert(cpnum,chaptername) 114 | uuidlist.insert(cpnum,chapteruuid) 115 | print(f'--{cpnum}:{namelist[cpnum]}') 116 | cpnum = cpnum + 1 117 | 118 | #选择章节 119 | dlchoices = int(input('请输入下载范围起始序号:')) 120 | dlchoicee = int(input('请输入下载范围终止序号:')) 121 | 122 | 123 | 124 | #下载 125 | 126 | if os.path.isdir(f"./{manganame}"): 127 | pass 128 | else: 129 | os.mkdir(f"./{manganame}") 130 | i = 0 131 | for i in range(dlchoices,dlchoicee + 1): 132 | dlchoicer = dlchoicee - i 133 | t = threading.Thread(target = download , args = (i,)) 134 | t.start() 135 | else: 136 | t.join() 137 | print('下载结束') 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /CopymangaDownloader_GUI.py: -------------------------------------------------------------------------------- 1 | #By Animuses 2 | from encodings import utf_8 3 | import os 4 | import json 5 | from tkinter import messagebox 6 | from tkinter.ttk import Combobox, Progressbar 7 | import requests 8 | from urllib import parse 9 | import re 10 | from Crypto.Cipher import AES 11 | import binascii 12 | import threading 13 | from tkinter import * 14 | 15 | window = Tk() 16 | dispurl = StringVar() 17 | dispurl.set('copymanga.site') 18 | mainurl = dispurl.get() 19 | validate = '未检测' 20 | 21 | #窗口属性初始化 22 | def init_window(): 23 | window.title("CopymangaDownloader_GUI") 24 | window.geometry('868x681+100+100') 25 | window.rowconfigure(0,weight=1) 26 | window.rowconfigure(1,weight=1) 27 | window.rowconfigure(2,weight=1) 28 | window.rowconfigure(3,weight=1) 29 | window.rowconfigure(4,weight=1) 30 | window.columnconfigure(0,weight=1) 31 | window.columnconfigure(1,weight=1) 32 | window.columnconfigure(2,weight=1) 33 | window.columnconfigure(3,weight=1) 34 | window.columnconfigure(4,weight=1) 35 | window.minsize(400,200) 36 | 37 | #联网检测函数 38 | def grab(mainurl): 39 | #当前有效网址 40 | global validate 41 | try: 42 | requests.get(f'https://{mainurl}') 43 | except requests.exceptions.ConnectionError: 44 | validate = '无效!在此可输入新网址' 45 | else: 46 | validate = '有效!' 47 | 48 | #错误后检测 49 | def check1(): 50 | check_urllabel.grid_remove() 51 | check() 52 | 53 | #联网检测按键处理 54 | def check(): 55 | mainurl = dispurl.get() 56 | grab(mainurl) 57 | global check_urllabel 58 | check_urllabel = Label(check_frame,text=f'当前网址:{mainurl},该网址{validate}') 59 | check_urllabel.grid(row=0,column=0,padx=5,pady=5) 60 | if validate == '无效!在此可输入新网址': 61 | check_frame.pack(expand='true') 62 | elif validate == '有效!': 63 | checknext_button = Button(check_frame, text="下一步", bg="lightblue", width=8,command=search) 64 | checknext_button.grid(row=2, column=0,padx=5,pady=5) 65 | 66 | #跳转搜索 67 | def search(): 68 | check_frame.pack_forget() 69 | search_frame.pack(expand='true') 70 | 71 | #搜索函数 72 | def search_start(): 73 | global pathwordlist,manganamelist,mangaaliaslist,mangaauthorlist 74 | search_frame.pack_forget() 75 | search_result_frame.pack(padx=5,pady=5) 76 | searchname=parse.quote(searchdispname.get()) 77 | searchurl = f'https://{mainurl}/api/kb/web/searchs/comics?offset=0&platform=2&limit=12&q={searchname}' 78 | searchreq = requests.get(searchurl) 79 | searchjson = searchreq.json() 80 | resultlist = searchjson['results']['list'] 81 | scounter = 1 82 | pathwordlist = [0] 83 | manganamelist = [0] 84 | mangaaliaslist = [0] 85 | mangaauthorlist = [0] 86 | for results in resultlist: 87 | resultname = results['name'] 88 | resultalias = results['alias'] 89 | resultauthorlist = results['author'] 90 | pathwords = results['path_word'] 91 | for resultauthors in resultauthorlist: 92 | resultauthor = resultauthors['name'] 93 | break 94 | listdisplay = f'{resultname}' 95 | pathwordlist.insert(scounter,pathwords) 96 | manganamelist.insert(scounter,resultname) 97 | mangaaliaslist.insert(scounter,resultalias) 98 | mangaauthorlist.insert(scounter,resultauthor) 99 | result_search_list.insert(scounter,listdisplay) 100 | scounter = scounter + 1 101 | 102 | def searchdetail(): 103 | global selector 104 | selector = result_search_list.curselection()[0]+1 105 | selectname = manganamelist[selector] 106 | selectalias = mangaaliaslist[selector] 107 | selectauthor = mangaauthorlist[selector] 108 | result_detail_label = Text(search_result_frame,width=30,height=40) 109 | result_detail_label.insert(END,f'漫画名称:\n{selectname}\n\n又名:\n{selectalias}\n\n作者:\n{selectauthor}') 110 | result_detail_label.config(state="disabled") 111 | result_detail_label.grid(row=2, column=2,padx=5,pady=5) 112 | detail_button = Button(search_result_frame, text="确定", bg="lightblue", width=5,command=chapterwindow) 113 | detail_button.grid(row=3, column=2,padx=5,pady=5) 114 | 115 | def chapterwindow(): 116 | search_result_frame.pack_forget() 117 | chapter_frame.pack(padx=5,pady=5) 118 | chapter_fetch() 119 | 120 | def chapter_fetch(): 121 | global manganame,pathword,uuidlist,namelist 122 | manganame = manganamelist[selector] 123 | pathword = parse.quote(pathwordlist[selector]) 124 | chapter_list.insert(END,f'已选择:{manganame}\n') 125 | #拉取加密章节列表 126 | cipherurl = f'https://{mainurl}/comicdetail/{pathword}/chapters' 127 | cipherreq = requests.get(cipherurl) 128 | cipherjson = cipherreq.json() 129 | listcipher = cipherjson['results'] 130 | #解密 131 | key = 'xxxmanga.woo.key'.encode('utf_8') 132 | iv = listcipher[:16].encode('utf_8') 133 | cipher = binascii.a2b_hex(listcipher[16:]) 134 | mode = AES.MODE_CBC 135 | aes = AES.new(key, mode, iv) 136 | cplistb = aes.decrypt(cipher) 137 | cplist = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff]',"",cplistb.decode('utf-8')) 138 | #处理章节列表并显示 139 | listjson = json.loads(cplist) 140 | groups = listjson['groups'] 141 | groupnum = 1 142 | cpnum = 1 143 | namelist = [0] 144 | uuidlist = [0] 145 | selectlist = [] 146 | for grouplist in groups: 147 | groupname = groups[grouplist]['name'] 148 | chapter_list.insert(END,f'\n{groupnum}:{groupname}') 149 | groupnum = groupnum + 1 150 | 151 | for chapterlist in groups[grouplist]['chapters']: 152 | chaptername = chapterlist.get('name') 153 | chapteruuid = parse.quote(chapterlist['id']) 154 | namelist.insert(cpnum,chaptername) 155 | uuidlist.insert(cpnum,chapteruuid) 156 | chapter_list.insert(END,f'\n--{cpnum}:{namelist[cpnum]}') 157 | selectlist.insert(cpnum,f'\n--{cpnum}:{namelist[cpnum]}') 158 | cpnum = cpnum + 1 159 | chapter_list.grid(row=1,column=1) 160 | chapter_list.config(state="disabled") 161 | chapter_selector['value']=selectlist 162 | chapter_selector1['value']=selectlist 163 | 164 | #下载 165 | def downloadwindow(): 166 | dlchoices = int(re.search('--(\d+):',chapter_selector.get()).group(1)) 167 | dlchoicee = int(re.search('--(\d+):',chapter_selector1.get()).group(1)) 168 | if dlchoicee>=dlchoices: 169 | chapter_frame.pack_forget() 170 | download_frame.pack() 171 | if os.path.isdir(f"./{manganame}"): 172 | pass 173 | else: 174 | os.mkdir(f"./{manganame}") 175 | i = 0 176 | for i in range(dlchoices,dlchoicee + 1): 177 | threading.Thread(target = download , args = (i,)).start() 178 | else: 179 | pass 180 | #download_log.config(state="disabled") 181 | else: 182 | messagebox.showerror('错误','下载范围错误,请检查下载起始章节不应大于结束章节') 183 | 184 | def download(num): 185 | download_log.insert(END,f'即将下载:{namelist[num]}\n') 186 | counter = 1 187 | filename = str(counter).zfill(4) 188 | cpurl = f'https://api.{mainurl}/api/v3/comic/{pathword}/chapter/{uuidlist[num]}' 189 | reqget = requests.get(cpurl) 190 | getjson = reqget.json() 191 | jsonfilter=getjson['results']['chapter']['contents'] 192 | lenth = len(jsonfilter) 193 | download_log.insert(END,f'{namelist[num]}:{lenth}个文件\n') 194 | if os.path.isdir(f"./{manganame}/{namelist[num]}"): 195 | pass 196 | else: 197 | os.mkdir(f"./{manganame}/{namelist[num]}") 198 | dlgrid = int(re.search('Thread-(\d+)',str(threading.currentThread())).group(1))-1 199 | pbf = Frame(download_frame) 200 | pbf.grid(row=dlgrid//5+1,column=dlgrid%5+1,padx=8,pady=8) 201 | pbn = Label(pbf,text=f'{namelist[num]}') 202 | pbn.pack(side='top',anchor='center',padx=2,pady=2) 203 | pb = Progressbar(pbf) 204 | pb.pack(side='left',anchor='center',padx=2,pady=2) 205 | pb['maximum'] = lenth 206 | pb['value'] = 0 207 | for listname in jsonfilter: 208 | dlurl = listname['url'] 209 | cache = requests.get(dlurl) 210 | with open(f"./{manganame}/{namelist[num]}/{filename}.jpg",'wb') as w: 211 | w.write(cache.content) 212 | w.close 213 | download_log.insert(END,f"成功下载图片:{namelist[num]},{filename}.jpg,{counter}/{lenth}\n") 214 | download_log.see(END) 215 | pb['value'] =counter 216 | window.update() 217 | counter = counter + 1 218 | filename=str(counter).zfill(4) 219 | pb.pack_forget() 220 | pbn.pack_forget() 221 | pbn = Label(pbf,text=f'{namelist[num]}下载完成') 222 | pbn.pack(side='top',anchor='center',padx=5,pady=5) 223 | 224 | #联网检测窗口 225 | check_frame = Frame(window) 226 | check_frame.pack(expand='true') 227 | 228 | check_urllabel = Label(check_frame,text=f'当前网址:{mainurl},该网址{validate}') 229 | 230 | check_url = Entry(check_frame, width=25,textvariable=dispurl) 231 | check_url.grid(row=1, column=0,padx=5,pady=5) 232 | 233 | check_button = Button(check_frame, text="替换网址", bg="lightblue", width=8,command=check1) 234 | check_button.grid(row=1, column=1,padx=5,pady=5) 235 | 236 | #搜索窗口 237 | searchdispname = StringVar() 238 | 239 | search_frame = Frame(window) 240 | search_frame.pack_forget() 241 | 242 | search_label = Label(search_frame, text="在此输入要找的漫画名") 243 | search_label.grid(row=3, column=0,padx=5,pady=5) 244 | 245 | search_entry = Entry(search_frame, width=30,textvariable=searchdispname) 246 | search_entry.grid(row=4, column=0,padx=5,pady=5) 247 | 248 | search_button = Button(search_frame, text="搜索", bg="lightblue", width=5, command=search_start) 249 | search_button.grid(row=4, column=1,padx=5,pady=5) 250 | 251 | 252 | #搜索结果窗口 253 | search_result_frame = Frame(window) 254 | search_result_frame.pack_forget() 255 | 256 | result_search_label = Label(search_result_frame, text="搜索结果") 257 | result_search_label.grid(row=0, column=0) 258 | 259 | result_search_list = Listbox(search_result_frame,selectmode = 'single',width=40, height=30) 260 | result_search_list.grid(row=1,column=0,rowspan=4,padx=5,pady=5) 261 | 262 | result_button = Button(search_result_frame, text="选择", bg="lightblue", width=5,command=searchdetail) 263 | result_button.grid(row=2, column=1,padx=5,pady=5) 264 | 265 | #章节选择窗口 266 | chapter_frame = Frame(window) 267 | chapter_frame.pack_forget() 268 | 269 | chapter_label = Label(chapter_frame, text="章节列表") 270 | chapter_label.grid(row=0, column=1) 271 | 272 | chapter_bar= Scrollbar(chapter_frame) 273 | chapter_bar.grid(row=1, column=2,sticky=NS) 274 | 275 | chapter_list = Text(chapter_frame,yscrollcommand=chapter_bar.set,width=25) 276 | chapter_bar.config(command=chapter_list.yview) 277 | 278 | selector_label = Label(chapter_frame, text="下载范围") 279 | selector_label.grid(row=2, column=1) 280 | 281 | selector1_label = Label(chapter_frame, text="至") 282 | selector1_label.grid(row=3, column=1) 283 | 284 | chapter_selector = Combobox(chapter_frame) 285 | chapter_selector.grid(row=3,column=0) 286 | 287 | chapter_selector1 = Combobox(chapter_frame) 288 | chapter_selector1.grid(row=3,column=3) 289 | 290 | chapter_button = Button(chapter_frame, text="下载", bg="lightblue", width=5,command=downloadwindow) 291 | chapter_button.grid(row=4, column=1,padx=5,pady=5) 292 | 293 | #下载窗口 294 | download_frame = Frame(window) 295 | download_frame.pack_forget() 296 | 297 | download_label = Label(download_frame,text='下载进度') 298 | download_label.grid(row=0,column=3) 299 | 300 | 301 | download_log = Text(download_frame,height=20) 302 | download_log.grid_forget() 303 | 304 | init_window() 305 | grab(mainurl) 306 | check() 307 | window.mainloop() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CopymangaDownloader 2 | 3 | 4 | ## 声明 5 | 6 | 此工具仅为个人学习交流制作使用,请不要将此工具用于商业用途,也不要恶意长时间大范围使用以免造成服务器负担或者造成服务器限制。出现上述状况均与本人无关。 7 | 8 | ## 简介 9 | 10 | 新加入图形界面,再也不用对着cmd输出了 11 | 12 | 此工具使用 `python`下载Copymanga中的漫画,以jpg的形式保存于电脑中 13 | 14 | 无需打开网页,直接搜索想看的漫画,并支持指定范围下载 15 | 16 | 如果发现无法连接/下载的时候,请多试几次,确保域名输入正确 17 | 18 | ## 如何使用 19 | 20 | ### 注意:由于pycryptodome的原因,目前本工具只测试Windows系统中的使用情况,Linux用户可以在安装pycrypto之后尝试使用。 21 | 22 | 0. 小白用户右转在Releases里下载或者下载仓库后找到exe文件直接打开使用 23 | 24 | 0.1 GUI版本由于各种原因打开速度会比较慢,打开时请有点耐心 25 | 26 | 1. clone本仓库或者直接打包下载 27 | 28 | 2. 确保你安装了python 3.8或以上版本 29 | 30 | 3. cd至本工具的目录并使用pip install -r requirements.txt安装所需依赖 31 | 32 | 4. 使用python打开CopymangaDownloader.py(如命令行执行 python ./CopymangaDownloader.py) 33 | 34 | 4.1 你也可以打开CopymangaDownloader_GUI.py来使用GUI版本通过命令行中的报错来发现问题所在 35 | 36 | 5. 按照提示搜索输入自己想要的结果 37 | 38 | 6.好好享受! 39 | 40 | ## 使用截图 41 | 42 | ![示例1.png](./sample1.png) 43 | ![示例2.png](./sample2.png) 44 | ![示例3.png](./sample3.png) 45 | 46 | 47 | ## 更新 48 | 49 | 2022/8/19: 上传第一版 50 | 51 | 2022/8/22: 修复了部分漫画拉取章节列表失败、替换有效地址不起作用的问题 52 | 53 | 2022/9/27: 加入多线程下载 54 | 55 | 2022/9/29: 加入了图形界面测试版程序 56 | 57 | ## 应该不会再有的未来更新 58 | 59 | * 发现BUG就修一下 60 | 61 | *偶尔会出现json解析错误无法下载的问题,等待调查后修复 62 | 63 | ## 关于api 64 | 65 | 此工具所使用的所有资料获取的API均为官方API。 66 | 使用到的API如下 67 | 68 | ```text 69 | 漫画搜索: 70 | https://api.copymanga.site/api/kb/web/searchs/comics?offset=0&platform=2&limit=12&q={搜索词} 71 | 72 | 漫画章节获取: 73 | https://api.copymanga.site/api/v3/comic/{漫画path_word}/chapters 74 | 75 | 漫画每章图片获取: 76 | https://api.copymanga.site/api/v3/comic/{漫画path_word}/chapter/{章节UUID} 77 | 78 | ``` 79 | 80 | ## 使用过程中出现了问题? 81 | 82 | * 找不到下载的漫画在哪?由于使用了相对目录,查看命令提示行执行时的所选目录可能会有帮助 83 | 84 | * 下载的速度实在是太慢了怎么办?如果你需要下载多个章节不放试试手动多线程(同时开数个本工具下载不同章节),若是下载没速度尝试检查你的网络连接,或者有可能是短时间内下载频繁导致服务器暂时封禁 85 | 86 | * 出现的问题上面没给出解决方案?可以在issue中给我留言,我会抽空查看处理。 87 | * 也欢迎各位pull requests来提升本工具的使用体验, 88 | 89 | ## 特别感谢 90 | 91 | * [ZhuFN's Blog](https://zhufn.fun/archives/cpmgapi/)提供了章节列表解密的方法 92 | * [copymanga-helper](https://github.com/Byaidu/copymanga-helper)参考了一些实现方式 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome==3.15.0 2 | requests==2.25.1 3 | -------------------------------------------------------------------------------- /sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Animuses/Copymanga-Downloader/f19ec6b728cc7f154dfe06f89a07430ad889fef7/sample1.png -------------------------------------------------------------------------------- /sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Animuses/Copymanga-Downloader/f19ec6b728cc7f154dfe06f89a07430ad889fef7/sample2.png -------------------------------------------------------------------------------- /sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Animuses/Copymanga-Downloader/f19ec6b728cc7f154dfe06f89a07430ad889fef7/sample3.png --------------------------------------------------------------------------------