├── resources ├── home.png ├── info.png ├── left.png ├── history.png ├── loading.png ├── right.png ├── zoom in.png ├── bookmark.png ├── checkmark.png ├── download.png └── zoom out.png ├── requirements.txt ├── visual ├── bookscraper-icon.ico └── bookscraper-splash1.png ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── format_manager.py ├── README.md ├── reader.py ├── scraper.py └── user_interface.py /resources/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/home.png -------------------------------------------------------------------------------- /resources/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/info.png -------------------------------------------------------------------------------- /resources/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/left.png -------------------------------------------------------------------------------- /resources/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/history.png -------------------------------------------------------------------------------- /resources/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/loading.png -------------------------------------------------------------------------------- /resources/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/right.png -------------------------------------------------------------------------------- /resources/zoom in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/zoom in.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | customtkinter==5.2.2 2 | Pillow==10.2.0 3 | Requests==2.31.0 4 | requests_html==0.10.0 5 | -------------------------------------------------------------------------------- /resources/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/bookmark.png -------------------------------------------------------------------------------- /resources/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/checkmark.png -------------------------------------------------------------------------------- /resources/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/download.png -------------------------------------------------------------------------------- /resources/zoom out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/resources/zoom out.png -------------------------------------------------------------------------------- /visual/bookscraper-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/visual/bookscraper-icon.ico -------------------------------------------------------------------------------- /visual/bookscraper-splash1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSherifH/Book-Scraper/HEAD/visual/bookscraper-splash1.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | history.json 3 | ./cache/* 4 | .vscode/* 5 | .venv 6 | .venv/* 7 | cache/bookmark/bookmarks.json 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | - OS: 27 | - Application Version: 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /format_manager.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import zipfile 3 | from PIL import Image 4 | 5 | compressionMethods = {"Stored": zipfile.ZIP_STORED, 6 | "BZIP2": zipfile.ZIP_BZIP2, 7 | "LZMA": zipfile.ZIP_LZMA, 8 | "Deflate": zipfile.ZIP_DEFLATED} 9 | 10 | def createCbz(imageContent, outputCbz): 11 | with zipfile.ZipFile(outputCbz, 'w') as cbz_file: 12 | for pageNum, imageContent in enumerate(imageContent, 1): 13 | cbz_file.writestr(f'{pageNum}.jpg', imageContent) 14 | 15 | def createZip(imageContent, outputZip, zipCompression): 16 | with zipfile.ZipFile(outputZip, 'w', compression=compressionMethods[zipCompression]) as zipFile: 17 | for pageNum, imageContent in enumerate(imageContent, 1): 18 | zipFile.writestr(f'{pageNum}.jpg', imageContent) 19 | 20 | def createJpg(imageContent, chosenDir): 21 | for pageNum, page in enumerate(imageContent, 1): 22 | print(pageNum) 23 | with open(f"{chosenDir}/#{pageNum}.jpg", 'wb') as f: 24 | f.write(page) 25 | 26 | def createPdf(imageContent, outputPdf): 27 | loadedPages = [] 28 | for page in reversed(imageContent): 29 | if page: 30 | loadedPages.append(Image.open(BytesIO(page))) 31 | loadedPages[0].save(outputPdf, "PDF" ,resolution=100.0, save_all=True, append_images=loadedPages[1:]) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/AhmedSherif07) 2 | 3 | # Book Scraper 4 | 5 | Book Scraper is a **free and open source** program made with Python to download comics and manga from a variety of different websites. 6 |

7 | Book-Scraper Splash 8 |

9 | 10 |

11 | Book-Scraper Splash 12 | Book-Scraper Splash 13 | Book-Scraper Splash 14 |

15 | 16 | 17 | 18 | 19 | ## Features 20 | - Different file formats to download Manga and Comics in 21 | - .cbz, .zip, .pdf and .jpg file support 22 | - Different compression algorithms for .zip files 23 | - Stored, Deflate, LZMA and BZIP2 algorithms 24 | - Read Manga and Comics directly without downloading 25 | - Resampling methods such as Nearest-Neighbor, Bicubic and Bilinear 26 | - Different hosts for both Manga and Comics 27 | - Bookmark books and have them displayed in the homepage for easy access 28 | - Download multiple chapters simultaneously 29 | - Lightweight and easy to use 30 | 31 | ## Installation and Usage 32 | * Install the required libraries for the progran: 33 | - Create a python virtual environment: 34 | `python -m venv .venv` 35 | - Activate your environment: 36 | `source .venv/bin/activate` 37 | - Install Dependencies: 38 | `pip install -r requirements.txt` 39 | 40 | * Run the application: 41 | - Linux: `python ./user_interface.py` 42 | - Windows: `python .\user_interface.py` 43 | 44 | 45 | ## Roadmap 46 | * ~~Add .cbz, .pdf and .zip conversion~~ 47 | * ~~Display what's currently being downloaded~~ 48 | * ~~An integrated book reader within the application~~ 49 | * ~~Display book information (e.g, release date, number of chapters)~~ 50 | * Implement 20 websites in total 51 | 52 | 53 | ## Notes 54 | * The developer and contributers have no affiliation with the content providers. 55 | * The project does not host any copyrighted material. 56 | -------------------------------------------------------------------------------- /reader.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from tkinter import BOTTOM, LEFT, RIGHT 3 | import customtkinter 4 | from PIL import Image 5 | from tkinter import filedialog 6 | 7 | 8 | resamplingMethods = {"Nearest": Image.NEAREST, 9 | "Bilinear": Image.BILINEAR, 10 | "Bicubic": Image.BICUBIC, 11 | "Lanczos": Image.LANCZOS, 12 | "Hamming": Image.HAMMING} 13 | 14 | fgColor = "#581845" 15 | headers = {'User-Agent': 'Mozilla/5.0'} 16 | currentPage = 0 17 | readingPage = 1 18 | pageLabelText = "Page {} of {} " 19 | pageSize = (0 , 0) 20 | resamplingMethod = None 21 | 22 | leftButtonImage = customtkinter.CTkImage(light_image=Image.open("./resources/left.png"), 23 | dark_image=Image.open("./resources/left.png")) 24 | rightButtonImage = customtkinter.CTkImage(light_image=Image.open("./resources/left.png"), 25 | dark_image=Image.open("./resources/right.png")) 26 | zoomInButtomImage = customtkinter.CTkImage(light_image=Image.open("./resources/zoom in.png"), 27 | dark_image=Image.open("./resources/zoom in.png")) 28 | zoomOutButtomImage = customtkinter.CTkImage(light_image=Image.open("./resources/zoom out.png"), 29 | dark_image=Image.open("./resources/zoom out.png")) 30 | downloadImage = customtkinter.CTkImage(light_image=Image.open("./resources/download.png"), 31 | dark_image=Image.open("./resources/download.png")) 32 | 33 | 34 | def changeResamplingMethod(choice): 35 | global resamplingMethod 36 | resamplingMethod = resamplingMethods[choice] 37 | print(resamplingMethod) 38 | 39 | def downloadPage(pages): 40 | directory = filedialog.asksaveasfilename(defaultextension=".png", 41 | filetypes=[("PNG", ".png"), ("JPG", ".jpg")]) 42 | imageToDownload = Image.open(BytesIO(pages[currentPage])) 43 | imageToDownload.save(directory) 44 | 45 | 46 | def createReaderWindow(imageContent, bookName): 47 | global readingPage 48 | 49 | readerWindow = customtkinter.CTkToplevel() 50 | readerWindow.geometry("600x600") 51 | readerWindow.wm_iconbitmap("visual/bookscraper-icon.ico") 52 | readerWindow.title(bookName) 53 | 54 | labelFont = customtkinter.CTkFont(family="Arial Rounded MT Bold", size=12) 55 | readingPage = 1 56 | 57 | controlFrame = customtkinter.CTkFrame(readerWindow, width=10) 58 | controlFrame.pack(side=BOTTOM) 59 | 60 | pageNumberDisplay = customtkinter.CTkLabel(controlFrame, width=70, height=40, 61 | text=pageLabelText.format("1", len(imageContent)), 62 | fg_color=fgColor, 63 | font=labelFont) 64 | 65 | resamplingMenu = customtkinter.CTkOptionMenu(controlFrame, width=70, height=40, 66 | values=list(resamplingMethods.keys()), 67 | fg_color=fgColor, 68 | button_color=fgColor, 69 | font=labelFont, 70 | command=changeResamplingMethod) 71 | resamplingMenu.pack(side='left', anchor='sw') 72 | 73 | downloadPageButton = customtkinter.CTkButton(controlFrame, width=70, height=40, 74 | text="", 75 | image=downloadImage, 76 | fg_color=fgColor, 77 | font=labelFont, 78 | command= lambda pages = imageContent: downloadPage(pages)) 79 | downloadPageButton.pack(side='right', anchor='se') 80 | 81 | pageFrame = customtkinter.CTkScrollableFrame(readerWindow) 82 | pageFrame.pack(anchor='center', fill="both", expand=True) 83 | pageLabel = customtkinter.CTkLabel(pageFrame, text="", image=None) 84 | pageLabel.pack(anchor='center', fill="x") 85 | 86 | 87 | leftButton = customtkinter.CTkButton(controlFrame, width=70, height=40, 88 | image=leftButtonImage, 89 | text="", 90 | fg_color=fgColor, 91 | command= lambda pages = imageContent, 92 | pageLabel = pageLabel, 93 | pageNumberDisplay = pageNumberDisplay: getLastPage(pages, pageLabel, pageNumberDisplay)) 94 | leftButton.pack(side=LEFT) 95 | 96 | rightButton = customtkinter.CTkButton(controlFrame, width=70, height=40, 97 | image=rightButtonImage, 98 | text="", 99 | fg_color=fgColor, 100 | command= lambda pages = imageContent, 101 | pageLabel = pageLabel, 102 | pageNumberDisplay = pageNumberDisplay: getNextPage(pages, pageLabel, pageNumberDisplay)) 103 | rightButton.pack(side=RIGHT) 104 | 105 | zoomInButton = customtkinter.CTkButton(controlFrame, width=70, height=40, 106 | image=zoomInButtomImage, 107 | text="", 108 | fg_color=fgColor, 109 | command= lambda pageLabel = pageLabel, pages = imageContent: zoomIn(pageLabel, pages)) 110 | zoomInButton.pack(side=LEFT) 111 | 112 | zoomOutButton = customtkinter.CTkButton(controlFrame, width=70, height=40, 113 | image=zoomOutButtomImage, 114 | text="", 115 | fg_color=fgColor, 116 | command= lambda pageLabel = pageLabel, pages= imageContent: zoomOut(pageLabel, pages)) 117 | zoomOutButton.pack(side=RIGHT) 118 | 119 | pageNumberDisplay.pack(side=RIGHT) 120 | 121 | pageDisplay(imageContent, pageLabel) 122 | 123 | 124 | def getNextPage(pages, pageLabel, pageNumberDisplay): 125 | global currentPage 126 | global readingPage 127 | global resamplingMethod 128 | 129 | if currentPage == len(pages) - 1: 130 | pass 131 | else: 132 | page = Image.open(BytesIO(pages[currentPage+1])) 133 | pageSize = (page.width, page.height) 134 | page.resize((page.width, page.height), resamplingMethod) 135 | pageLabel.configure(image=(customtkinter.CTkImage(light_image=page, 136 | dark_image=page, 137 | size=pageSize))) 138 | 139 | currentPage += 1 140 | readingPage += 1 141 | pageNumberDisplay.configure(text=pageLabelText.format(readingPage, len(pages))) 142 | print(currentPage) 143 | 144 | def getLastPage(pages, pageLabel, pageNumberDisplay): 145 | global currentPage 146 | global readingPage 147 | global resamplingMethod 148 | 149 | if currentPage == 0: 150 | pass 151 | else: 152 | page = Image.open(BytesIO(pages[currentPage-1])) 153 | page.resize((page.width, page.height), resamplingMethod) 154 | pageSize = (page.width, page.height) 155 | pageLabel.configure(image=(customtkinter.CTkImage(light_image=page, 156 | dark_image=page, 157 | size=pageSize))) 158 | 159 | currentPage -= 1 160 | readingPage -= 1 161 | pageNumberDisplay.configure(text=pageLabelText.format(readingPage, len(pages))) 162 | print(currentPage) 163 | 164 | 165 | 166 | def zoomIn(pageLabel, pages): 167 | global pageSize 168 | global resamplingMethod 169 | 170 | page = Image.open(BytesIO(pages[currentPage])) 171 | pageSize = (page.width / 0.7, page.height / 0.7) 172 | page.resize((page.width, page.height), resamplingMethod) 173 | pageLabel.configure(image=(customtkinter.CTkImage(light_image=page, 174 | dark_image=page, 175 | size=pageSize))) 176 | 177 | 178 | def zoomOut(pageLabel, pages): 179 | global pageSize 180 | global resamplingMethod 181 | 182 | page = Image.open(BytesIO(pages[currentPage])) 183 | pageSize = (page.width * 0.7, page.height * 0.7) 184 | page.resize((page.width, page.height), resamplingMethod) 185 | pageLabel.configure(image=(customtkinter.CTkImage(light_image=page, 186 | dark_image=page, 187 | size=pageSize))) 188 | 189 | 190 | def pageDisplay(imageContent, pageLabel): 191 | global currentPage 192 | global pageSize 193 | firstPage = Image.open(BytesIO(imageContent[0])) 194 | firstPage.resize((firstPage.width, firstPage.height), Image.NEAREST) 195 | pageSize = ((firstPage.width), (firstPage.height)) 196 | currentPage = 0 197 | pageLabel.configure(image=(customtkinter.CTkImage(light_image=firstPage, 198 | dark_image=firstPage, 199 | size=pageSize))) 200 | 201 | -------------------------------------------------------------------------------- /scraper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from tkinter import filedialog 3 | from tkinter import messagebox 4 | from format_manager import * 5 | 6 | headers = {'User-Agent': 'Mozilla/5.0'} 7 | issueHref = '' 8 | bookDownloads = [] 9 | compressedChapters = [] 10 | charsToRemove = ['\\', '/', ':', '*', '?', '"', '<', '>', '|'] 11 | 12 | 13 | def scrapeCover(bookLink, session, selectedHost): 14 | coverImage = "" 15 | imageRequest = session.get(bookLink, headers=headers) 16 | # Get coverImage from the website source code, xpath differs across different websites 17 | match selectedHost: 18 | case "comixextra.com": 19 | images = imageRequest.html.xpath('/html/body/main/div/div/div/div[1]/div[1]/div[1]/div[1]/div/div[1]/div/img') 20 | for image in images: 21 | coverImage = image.attrs['src'] 22 | 23 | case "mangakomi.io": 24 | images = imageRequest.html.xpath('/html/body/div[1]/div/div/div/div[1]/div/div/div/div[3]/div[1]/a/img') 25 | img_tag = images[0] 26 | coverImage = img_tag.attrs['data-src'] 27 | 28 | case "mangaread.org": 29 | images = imageRequest.html.xpath('/html/body/div[1]/div/div[2]/div/div[1]/div/div/div/div[3]/div[1]/a/img') 30 | img_tag = images[0] 31 | coverImage = img_tag.attrs['src'] 32 | 33 | case "mangakatana.com": 34 | images = imageRequest.html.xpath('/html/body/div[3]/div/div/div[1]/div/div[1]/div/img') 35 | img_tag = images[0] 36 | coverImage = img_tag.attrs['src'] 37 | 38 | case "mangakakalot.tv": 39 | images = imageRequest.html.find('div.manga-info-pic', first=True) 40 | coverImage = "https://ww8.mangakakalot.tv/" + (images.find('img', first=True).attrs.get('src')) 41 | 42 | case "rawkuma.com": 43 | images = imageRequest.html.xpath('/html/body/div[3]/div/div[1]/article/div[2]/div[1]/div[1]/div[1]/img') 44 | coverImage = images[0].attrs['src'] 45 | print(coverImage) 46 | 47 | case "mangaweebs.org": 48 | images = imageRequest.html.find('.summary_image img', first=True) 49 | print(images) 50 | coverImage = images.attrs.get('src') 51 | print(coverImage) 52 | return coverImage 53 | 54 | def scrapeInformation(bookLink, session, selectedHost): 55 | information = {} 56 | request = session.get(bookLink, headers=headers) 57 | genresList = [] 58 | 59 | match selectedHost: 60 | case "comixextra.com": 61 | information["Title"] = request.html.xpath('/html/body/main/div/div/div/div[1]/div[1]/div[1]/div[1]/div/div[2]/h1/span')[0].text 62 | information["Author/Publisher"] = request.html.xpath('/html/body/main/div/div/div/div[1]/div[1]/div[1]/div[1]/div/div[2]/div/dl/dd[4]')[0].text 63 | genres = request.html.find('.movie-dd')[0] 64 | genreLinks = genres.find('a') 65 | for link in genreLinks: 66 | genre = link.text 67 | genresList.append(genre) 68 | information['Genres'] = ", ".join(genresList) 69 | 70 | case "mangakomi.io": 71 | information['Title'] = request.html.xpath('/html/body/div[1]/div/div/div/div[1]/div/div/div/div[2]/h1')[0].text 72 | authors = request.html.find('.author-content') 73 | if authors: 74 | for author in authors: 75 | information['Author/Publisher'] = author.text 76 | else: 77 | information['Author/Publisher'] = "N/A" 78 | genreLinks = request.html.find('.genres-content') 79 | for link in genreLinks: 80 | genre = (link.text) 81 | genresList.append(genre) 82 | information['Genres'] = ", ".join(genresList) 83 | 84 | case "mangaread.org": 85 | information['Title'] = request.html.xpath('/html/body/div[1]/div/div[2]/div/div[1]/div/div/div/div[2]/h1')[0].text 86 | information['Author/Publisher'] = request.html.xpath('/html/body/div[1]/div/div[2]/div/div[1]/div/div/div/div[3]/div[2]/div/div[1]/div[6]/div[2]/div/a')[0].text 87 | genreLinks = request.html.find('.genres-content') 88 | for genreLink in genreLinks: 89 | genre = (genreLink.text) 90 | genresList.append(genre) 91 | information['Genres'] = ", ".join(genresList) 92 | 93 | case "mangakatana.com": 94 | information['Title'] = request.html.xpath('/html/body/div[3]/div/div/div[1]/div/div[2]/div/h1')[0].text 95 | information['Author/Publisher'] = request.html.xpath('/html/body/div[3]/div/div/div[1]/div/div[2]/div/ul/li[2]/div[2]/a')[0].text 96 | genreLinks = request.html.find('div.genres a.text_0') 97 | if genreLinks: 98 | for genre in genreLinks: 99 | genreName = genre.text 100 | genresList.append(f"{genreName}") 101 | information['Genres'] = ", ".join(genresList) 102 | 103 | case "mangakakalot.tv": 104 | information['Title'] = request.html.xpath('/html/body/div[1]/div[2]/div[1]/div[3]/ul/li[1]/h1')[0].text 105 | information['Author/Publisher'] = request.html.xpath('/html/body/div[1]/div[2]/div[1]/div[3]/ul/li[2]/a')[0].text 106 | genres = request.html.find('li', containing='Genres :')[0].find('a[rel="nofollow"]') 107 | for genre in genres: 108 | genreName = genre.text 109 | genresList.append(f"{genreName}") 110 | information['Genres'] = ", ".join(genresList) 111 | 112 | case "rawkuma.com": 113 | information['Title'] = request.html.find('h1.entry-title[itemprop="name"]', first=True).text 114 | information['Author/Publisher'] = request.html.xpath('/html/body/div[3]/div/div[1]/article/div[2]/div[1]/div[2]/div[4]/div[2]/span')[0].text 115 | genres = request.html.find(".mgen a") 116 | for element in genres: 117 | genre = element.text 118 | genresList.append(genre) 119 | information['Genres'] = ", ".join(genresList) 120 | 121 | case "mangaweebs.org": 122 | information['Title'] = request.html.xpath('/html/body/div[1]/div/div[1]/div/div[1]/div/div/div/div[2]/h1')[0].text 123 | information['Author/Publisher'] = "N/A" 124 | genres = request.html.find('.genres-content a') 125 | for element in genres: 126 | genre = element.text 127 | genresList.append(genre) 128 | information['Genres'] = ", ".join(genresList) 129 | 130 | 131 | return information 132 | 133 | def scrapeTitles(url, selectedHost, requestedBook): 134 | requestedBook = requestedBook.replace(" ", "-") 135 | bookTitles = {} 136 | # Retrieve all book titles that contain the same words as those entered by the user 137 | match selectedHost: 138 | case "comixextra.com": 139 | parsedTitles = url.html.find('div.cartoon-box a') 140 | for title in parsedTitles: 141 | if "https://comixextra.com/comic/" in title.attrs['href'] and title.text: 142 | titleHref = title.attrs['href'] 143 | titleName = title.text 144 | print(titleName) 145 | bookTitles[titleName] = titleHref 146 | 147 | case "mangakomi.io": 148 | links = url.html.find('.post-title a') 149 | for link in links: 150 | titleHref = link.attrs['href'] 151 | titleName = link.text 152 | bookTitles[titleName] = titleHref 153 | 154 | case "mangaread.org": 155 | h3Elements = url.html.find('h3.h4 a') 156 | for h3 in h3Elements: 157 | titleHref = h3.attrs['href'] 158 | titleName = h3.text 159 | bookTitles[titleName] = titleHref 160 | 161 | case "mangakatana.com": 162 | requestedBookList = url.html.xpath('//*[@id="book_list"]//div[@class="item"]') 163 | for item in requestedBookList: 164 | titleClass = item.find('h3.title', first=True) 165 | if titleClass: 166 | titleHref = titleClass.find('a', first=True).attrs['href'] 167 | titleName = titleClass.find('a', first=True).text 168 | bookTitles[titleName] = titleHref 169 | 170 | case "mangakakalot.tv": 171 | requestedBookList = url.html.find('.story_item') 172 | for book in requestedBookList: 173 | aTag = book.find('a', first=True) 174 | imgTag = aTag.find('img', first=True) 175 | if imgTag: 176 | titleName = imgTag.attrs.get('alt', '') 177 | titleHref = "https://ww8.mangakakalot.tv/" + aTag.attrs.get('href', '') 178 | bookTitles[titleName] = titleHref 179 | 180 | case "rawkuma.com": 181 | requestedBookList = url.html.find('.postbody .bsx') 182 | for book in requestedBookList: 183 | anchor = book.find('a', first=True) 184 | titleName = anchor.attrs.get('title') 185 | titleHref = anchor.attrs.get('href') 186 | bookTitles[titleName] = titleHref 187 | 188 | case "mangaweebs.org": 189 | requestedBookList = url.html.find('.row.c-tabs-item__content') 190 | for book in requestedBookList: 191 | bookElement = book.find('a', first=True) 192 | titleName = bookElement.attrs.get('title') 193 | titleHref = bookElement.attrs.get('href') 194 | bookTitles[titleName] = titleHref 195 | 196 | return bookTitles 197 | 198 | def scrapeChapters(url, selectedHost): 199 | bookChapters = {} 200 | # Get all chapters within a book 201 | match selectedHost: 202 | case "comixextra.com": 203 | issues = url.html.find('#list a') 204 | for issue in issues: 205 | chapterHref = issue.attrs['href'] 206 | chapterName = issue.text 207 | bookChapters[chapterName] = chapterHref + "/full" 208 | 209 | case "mangakomi.io": 210 | chapters = url.html.find('li.wp-manga-chapter') 211 | for chapter in chapters: 212 | chapterHref = chapter.find('a', first=True).attrs['href'] 213 | chapterName = chapter.find('a', first=True).text 214 | bookChapters[chapterName] = chapterHref 215 | 216 | case "mangaread.org": 217 | chapters = url.html.find('.wp-manga-chapter') 218 | for chapter in chapters: 219 | chapterName = chapter.find('a', first=True).text 220 | chapterHref = chapter.find('a', first=True).attrs['href'] 221 | bookChapters[chapterName] = chapterHref 222 | 223 | case "mangakatana.com": 224 | chapters = url.html.find('div.chapter') 225 | for chapter in chapters: 226 | chapterName = chapter.find('a', first=True).text 227 | chapterHref = chapter.find('a', first=True).attrs['href'] 228 | bookChapters[chapterName] = chapterHref 229 | 230 | case "mangakakalot.tv": 231 | chapters = url.html.find('div.row') 232 | for chapter in chapters: 233 | aTag = chapter.find('a', first=True) 234 | if aTag: 235 | chapterHref = aTag.attrs.get('href') 236 | chapterName = aTag.attrs.get('title') 237 | bookChapters[chapterName] = "https://ww8.mangakakalot.tv/" + chapterHref 238 | 239 | case "rawkuma.com": 240 | chapters = url.html.find('.eph-num a') 241 | for chapter in chapters: 242 | aTag = chapter.find('a', first=True) 243 | chapterHref = aTag.attrs.get('href') 244 | chapterName = aTag.find('.chapternum', first=True).text 245 | bookChapters[chapterName] = chapterHref 246 | 247 | case "mangaweebs.org": 248 | chapters = url.html.find('.wp-manga-chapter') 249 | for chapter in chapters: 250 | chapterName = chapter.find('a', first=True).text 251 | chapterHref = chapter.find('a', first=True).attrs['href'] 252 | bookChapters[chapterName] = chapterHref 253 | 254 | return bookChapters 255 | 256 | def scrapePages(chapterLink, session, selectedHost, bookName, isMassDownload, directory, format, numberofLoops, loopVerification, zipCompression): 257 | pageNum = 0 258 | imageContents = [] 259 | global compressedChapters 260 | chapterRequest = session.get(chapterLink, headers=headers) 261 | # If isMassDownload is True, we don't ask for the directory because the user has already chosen the directory in user_interface.py 262 | if isMassDownload == False: 263 | chosenDir = filedialog.askdirectory() 264 | else: 265 | chosenDir = directory 266 | try: 267 | if chosenDir != '' and chosenDir is not None: 268 | print(chapterLink) 269 | print(f'SELECTED HOST {selectedHost}') 270 | bookDownloads.append(bookName) 271 | 272 | match selectedHost: 273 | case "comixextra.com": 274 | images = chapterRequest.html.find('div.chapter-container img') 275 | for image in images: 276 | pageNum += 1 277 | src = image.attrs['src'] 278 | print(f"{chosenDir}/#{pageNum}.jpg") 279 | pageResponse = requests.get(src) 280 | imageContents.append(pageResponse.content) 281 | 282 | case "mangakomi.io": 283 | images = chapterRequest.html.xpath('//img') 284 | for image in images: 285 | if 'data-src' in image.attrs: 286 | src = image.attrs.get('data-src') 287 | src = src.strip() 288 | if 'cdn' in src: 289 | pageNum += 1 290 | print(f"#{pageNum}: {src}") 291 | pageResponse = requests.get(src) 292 | imageContents.append(pageResponse.content) 293 | 294 | case "mangaread.org": 295 | images = chapterRequest.html.find('.page-break.no-gaps') 296 | for div in images: 297 | pageNum += 1 298 | image = div.find('img', first=True) 299 | src = image.attrs['src'] 300 | print(f"#{pageNum}: {src}") 301 | pageResponse = requests.get(src) 302 | imageContents.append(pageResponse.content) 303 | 304 | case "mangakatana.com": 305 | thzqScript = chapterRequest.html.find('script', containing='var thzq=')[0].text 306 | thzqStart = thzqScript.find('var thzq=') 307 | thzqEnd = thzqScript.find('];function kxat', thzqStart) + 1 308 | thzqArray = thzqScript[thzqStart:thzqEnd] 309 | pageLinks = thzqArray.replace('var thzq=', '').split(',') 310 | for pageLink in pageLinks: 311 | pageLink = pageLink.replace("[", "",).replace(",", "").replace("'", "").replace("]", "") 312 | print(pageLink) 313 | if pageLink: 314 | pageResponse = requests.get(pageLink) 315 | imageContents.append(pageResponse.content) 316 | 317 | case "mangakakalot.tv": 318 | pages = chapterRequest.html.find('div.vung-doc img') 319 | for div in pages: 320 | pageNum += 1 321 | if div.attrs.get('data-src'): 322 | page = div.attrs.get('data-src') 323 | print(f"#{pageNum}: {page}") 324 | pageResponse = requests.get(page) 325 | imageContents.append(pageResponse.content) 326 | 327 | case "rawkuma.com": 328 | pages = chapterRequest.html.find('#readerarea img') 329 | for page in pages: 330 | pageNum += 1 331 | image = page.attrs['src'] 332 | print(f"#{pageNum}: {image}") 333 | pageResponse = requests.get(image) 334 | imageContents.append(pageResponse.content) 335 | 336 | case "mangaweebs.org": 337 | pages = chapterRequest.html.find('img.wp-manga-chapter-img') 338 | for page in pages: 339 | pageNum += 1 340 | image = page.attrs['src'] 341 | print(f"#{pageNum}: {image}") 342 | pageResponse = requests.get(image) 343 | imageContents.append(pageResponse.content) 344 | 345 | 346 | for char in charsToRemove: 347 | bookName = bookName.replace(char, '') 348 | 349 | match format: 350 | case ".cbz": 351 | for page in imageContents: 352 | compressedChapters.append(page) 353 | 354 | if loopVerification == numberofLoops: 355 | createCbz(compressedChapters, f"{chosenDir}/{bookName}.cbz") 356 | if len(compressedChapters) > 0: 357 | compressedChapters = [] 358 | 359 | case ".zip": 360 | for page in imageContents: 361 | compressedChapters.append(page) 362 | if loopVerification == numberofLoops: 363 | createZip(compressedChapters, f"{chosenDir}/{bookName}.zip", zipCompression) 364 | if len(compressedChapters) > 0: 365 | compressedChapters = [] 366 | 367 | case ".pdf": 368 | for page in imageContents: 369 | compressedChapters.append(page) 370 | if loopVerification == numberofLoops: 371 | createPdf(compressedChapters, f"{chosenDir}/{bookName}.pdf") 372 | if len(compressedChapters) > 0: 373 | compressedChapters = [] 374 | 375 | case ".jpg": 376 | createJpg(imageContents, chosenDir) 377 | 378 | case "Read": 379 | bookDownloads.remove(bookName) 380 | return imageContents 381 | 382 | bookDownloads.remove(bookName) 383 | except: 384 | messagebox.showerror("Error", "There was a problem while downloading. Make sure your directory path is correct.") 385 | 386 | 387 | def getDownloads(): 388 | downloads = ", ".join(list(set(bookDownloads))) 389 | if downloads == "": 390 | messagebox.showinfo(title="Downloads", message=f"Nothing is being downloaded!") 391 | else: 392 | messagebox.showinfo(title="Downloads", message=f"You're currently downloading: {downloads}") -------------------------------------------------------------------------------- /user_interface.py: -------------------------------------------------------------------------------- 1 | from reader import * 2 | import customtkinter 3 | from functools import * 4 | from requests_html import HTMLSession 5 | from scraper import * 6 | import threading 7 | from PIL import Image 8 | from io import BytesIO 9 | from tkinter import messagebox 10 | from tkinter import filedialog 11 | from pathlib import Path 12 | import signal 13 | import os 14 | import json 15 | 16 | # The main window 17 | root = customtkinter.CTk() 18 | root.protocol("WM_DELETE_WINDOW", lambda: os.kill(os.getpid(), signal.SIGTERM)) 19 | root.geometry("800x400") 20 | root.resizable(False, False) 21 | root.iconbitmap("visual/bookscraper-icon.ico") 22 | root.title("Book Scraper") 23 | 24 | fgColor = fgColor 25 | 26 | # The images for the buttons 27 | downloadButtonIcon = customtkinter.CTkImage(light_image=Image.open("./resources/download.png"), 28 | dark_image=Image.open("./resources/download.png")) 29 | historyLabelIcon = customtkinter.CTkImage(light_image=Image.open("./resources/history.png"), 30 | dark_image=Image.open("./resources/history.png")) 31 | bookmarkIcon = customtkinter.CTkImage(light_image=Image.open("./resources/bookmark.png"), 32 | dark_image=Image.open("./resources/bookmark.png")) 33 | loadingIcon = customtkinter.CTkImage(light_image=Image.open("./resources/loading.png"), 34 | dark_image=Image.open("./resources/loading.png")) 35 | infoIcon = customtkinter.CTkImage(light_image=Image.open("./resources/info.png"), 36 | dark_image=Image.open("./resources/info.png")) 37 | homeIcon = customtkinter.CTkImage(light_image=Image.open("./resources/home.png"), 38 | dark_image=Image.open("./resources/home.png")) 39 | checkmarkIcon = customtkinter.CTkImage(light_image=Image.open("./resources/checkmark.png"), 40 | dark_image=Image.open("./resources/checkmark.png")) 41 | 42 | # Global variables 43 | selectedHost = "" 44 | selectedFormat = "" 45 | oddChars = [" ", ":", "/","?", "(", ")"] 46 | hostBase = "" 47 | bookmarkedBooks = [] 48 | hostValues = ["Select a Host", "comixextra.com" , "mangakomi.io", "mangaread.org", "mangakatana.com", "mangakakalot.tv", "rawkuma.com", "mangaweebs.org"] 49 | formatValues = ["Select a Format", "Read", ".jpg", ".cbz", ".zip", ".pdf"] 50 | bookChapterNames = {} 51 | globalBookName = '' 52 | # The user agent is needed for some websites 53 | headers = {'User-Agent': 'Mozilla/5.0'} 54 | session = HTMLSession() 55 | # The path to the history and bookmarks json files 56 | historyJsonPath = Path('./cache/history/history.json') 57 | bookmarksJsonPath = Path('./cache/bookmark/bookmarks.json') 58 | # Check if the cache folders exist, if not, create them 59 | os.makedirs('./cache/history', exist_ok=True) 60 | os.makedirs('./cache/bookmark', exist_ok=True) 61 | 62 | 63 | def selectHost(choice): 64 | global selectedHost 65 | global hostBase 66 | selectedHost = choice 67 | 68 | # Remove "Select a Host" after selecting a website, and assign hostBase to the correct value 69 | if choice != "Select a Host": 70 | if "Select a Host" in hostValues: 71 | hostValues.remove("Select a Host") 72 | 73 | hostSelector.configure(values=hostValues) 74 | match choice: 75 | case "comixextra.com": 76 | hostBase = "https://comixextra.com/search?keyword={}" 77 | case "mangakomi.io": 78 | hostBase = "https://mangakomi.io/?s={}&post_type=wp-manga" 79 | case "mangaread.org": 80 | hostBase = "https://www.mangaread.org/?s={}&post_type=wp-manga" 81 | case "mangakatana.com": 82 | hostBase = "https://mangakatana.com/?search={}&search_by=book_name" 83 | case "mangakakalot.tv": 84 | hostBase = "https://ww8.mangakakalot.tv/search/{}" 85 | case "rawkuma.com": 86 | hostBase = "https://rawkuma.com/?s={}" 87 | case "mangaweebs.org": 88 | hostBase = "https://mangaweebs.org/?s={}&post_type=wp-manga" 89 | 90 | def selectFormat(choice): 91 | global selectedFormat 92 | # Remove "Select a Format" after selecting a format, and assign selectedFormat to the correct value 93 | if choice != "Select a Format": 94 | if "Select a Format" in formatValues: 95 | formatValues.remove("Select a Format") 96 | selectedFormat = choice 97 | formatSelector.configure(values=formatValues) 98 | # Only show the compression method menu if the selected format is .zip 99 | if selectedFormat == ".zip": 100 | compressionMethodMenu.place(x=5, y=362) 101 | else: 102 | compressionMethodMenu.place_forget() 103 | 104 | def getPages(chapterLink, session, selectedHost, bookName, isMassDownload, directory, numberofLoops, cbzVerification): 105 | zipCompressionMethod = compressionMethodMenu.get() 106 | if selectedFormat not in ["Read", ".jpg", ".cbz", ".zip", ".pdf"]: 107 | messagebox.showerror("Error", "Please select the format you'd like to download the pages in.") 108 | 109 | elif selectedFormat in ["Read"]: 110 | # !READING ONLY! isMassDownload is set to True to bypass asking for the directory 111 | isMassDownload = True 112 | # Directory variable is set to "directory" to avoid throwing an error 113 | directory = "directory" 114 | imageContent = scrapePages(chapterLink, session, selectedHost, bookName, isMassDownload, directory, selectedFormat, numberofLoops, cbzVerification, zipCompressionMethod) 115 | createReaderWindow(imageContent, bookName) 116 | 117 | elif selectedFormat in [".jpg", ".cbz", ".zip", ".pdf"]: 118 | scrapePages(chapterLink, session, selectedHost, bookName, isMassDownload, directory, selectedFormat, numberofLoops, cbzVerification, zipCompressionMethod) 119 | 120 | 121 | def getAllChapters(): 122 | global bookChapterNames 123 | global globalBookName 124 | 125 | isMassDownload = True 126 | # Prevent the program from asking for directory if the selected format is "Read" 127 | if selectedFormat not in ["Read"]: 128 | baseDirectory = filedialog.askdirectory() 129 | else: 130 | messagebox.showerror("Error", "This button cannot be used for the selected format.") 131 | directory = '' 132 | folderNum = 0 133 | compressedVerification = 0 134 | 135 | # This option is mainly for .png and .jpg. The number of folders made equals the number of chapters inside a book. 136 | # The folder name is decided by the number of the chapter 137 | if selectedFormat in [".jpg"]: 138 | numberofLoops = 0 139 | compressedVerification = 0 140 | for Chapter in bookChapterNames: 141 | folderNum = folderNum + 1 142 | directory = f"{baseDirectory}/#{folderNum}" 143 | Path(directory).mkdir(parents=True, exist_ok=True) 144 | 145 | for bookChapter in bookChapterNames: 146 | directory = f"{baseDirectory}/#{folderNum}" 147 | folderNum = folderNum - 1 148 | getPages(bookChapterNames[bookChapter], session, selectedHost, globalBookName, isMassDownload, directory, numberofLoops, compressedVerification) 149 | #threading.Thread(target=getPages, args=(bookChapterNames[bookChapter], session, selectedHost, globalBookName, isMassDownload, directory, numberofLoops, compressedVerification)).start() 150 | # A .cbz file is only made after compressedVerification = numberofLoops, where numberofLoops equals the number of chapters found in a book 151 | elif selectedFormat in [".zip", ".cbz", ".pdf"]: 152 | numberofLoops = len(bookChapterNames) 153 | for bookChapter in bookChapterNames: 154 | compressedVerification = compressedVerification + 1 155 | getPages(bookChapterNames[bookChapter], session, selectedHost, globalBookName, isMassDownload, baseDirectory, numberofLoops, compressedVerification) 156 | 157 | if selectedFormat not in ["Read", ".jpg", ".cbz", ".zip", ".pdf"]: 158 | messagebox.showerror("Error", "Please select the format you'd like to download the pages in.") 159 | 160 | 161 | def displayChapters(href, bookName, isHistory): 162 | global bookChapterNames 163 | global globalBookName 164 | coverLink = href 165 | isMassDownload = False 166 | directory = '' 167 | bookIndividualRequest = session.get(href, headers=headers) 168 | numberofLoops = 0 169 | cbzVerification = 0 170 | bookChapterNames = {} 171 | information = {} 172 | globalBookName = bookName 173 | 174 | homeButton.place_forget() 175 | print(f"Displaying Chapters --> {href}, {bookName}") 176 | 177 | # Call scrapeChapters function from scraper.py to get all chapters in a book 178 | # After the function is finished, it will create a button for each chapter 179 | bookChapterNames = scrapeChapters(bookIndividualRequest, selectedHost) 180 | threading.Thread(target=generateChapterButtons, args=(bookChapters, session, selectedHost, bookName, isMassDownload, directory, numberofLoops, cbzVerification)).start() 181 | 182 | information = scrapeInformation(href, session, selectedHost) 183 | information["Number of Chapters"] = len(bookChapterNames) 184 | 185 | 186 | # Get Cover Display, If it throws an error: Ignore cover completely 187 | try: 188 | coverLink = scrapeCover(coverLink, session, selectedHost) 189 | coverResponse = requests.get(coverLink) 190 | cover = Image.open(BytesIO(coverResponse.content)) 191 | coverImage = customtkinter.CTkImage(light_image=cover, dark_image=cover,size=(166, 256)) 192 | coverImageLabel.configure(image=coverImage) 193 | except: 194 | messagebox.showerror("Error", "Couldn't load cover image.") 195 | 196 | # Change the function and text of the bookmark button depending on whether the book is bookmarked or not 197 | if href not in bookmarkedBooks: 198 | bookmarkButton.configure(command=lambda cover=cover, href=href, bookName=bookName: saveBookmark(cover, href, bookName)) 199 | else: 200 | bookmarkButton.configure(command=lambda cover=cover, href=href, bookName=bookName: removeBookmark(href, bookName)) 201 | 202 | # If the book was accessed via history, don't save it to history again; display the return to history button 203 | if not isHistory: 204 | returnToList.place(x=700, y=5) 205 | os.makedirs(f'./cache/history/{selectedHost}', exist_ok=True) 206 | cover.save(f"./cache/history/{selectedHost}/{bookName}.png") 207 | saveBookToHistory(href, bookName) 208 | else: 209 | returnToHistory.place(x=700, y=5) 210 | 211 | # Manage Placement of Widgets 212 | loadingFrame.place_forget() 213 | informationDisplay.place(x=200, y=40) 214 | informationLabel.configure(text=f"{information['Title']} \n Number of Chapters: {information['Number of Chapters']} chapter(s) \n Author/Publisher: {information['Author/Publisher']} \n Genres: {information['Genres']}") 215 | 216 | informationLabel.pack() 217 | downloadallChapters.place(x=5, y=300) 218 | formatSelector.place(x=45, y=330) 219 | bookmarkButton.place(x=5, y=330) 220 | searchButton.place_forget() 221 | bookList.place_forget() 222 | historyList.place_forget() 223 | historyFrame.place_forget() 224 | bookmarkList.place_forget() 225 | bookmarkFrame.place_forget() 226 | coverImageLabel.place(x=10, y=40) 227 | bookChapters.place(x=400 , y=35) 228 | 229 | 230 | def searchProcess(): 231 | historyList.place_forget() 232 | historyFrame.place_forget() 233 | bookmarkList.place_forget() 234 | bookmarkFrame.place_forget() 235 | contextMenu.place_forget() 236 | bookTitles = {} 237 | searchBookURL = "" 238 | 239 | try: 240 | # Set searchBookURL according to the selected host, and get the text entered in the search bar 241 | if selectedHost in ["mangakomi.io", "mangaread.org", "comixextra.com", "mangakatana.com", "mangakakalot.tv", "rawkuma.com", "mangaweebs.org"]: 242 | requestedBook = searchBar.get("0.0", "end").replace(' ', "+").replace('\n', "") 243 | searchBookURL = hostBase.format(requestedBook) 244 | 245 | print(searchBookURL) 246 | searchBookRequest = session.get(searchBookURL, 247 | headers={'User-Agent': 'Mozilla/5.0'}) 248 | 249 | # Get all titles that include the same keywords as the ones entered by the user 250 | bookTitles = scrapeTitles(searchBookRequest, selectedHost, requestedBook) 251 | for title in bookTitles: 252 | bookButton = customtkinter.CTkButton(bookList, width=780, height=30, text=title, 253 | fg_color=fgColor, command=lambda href=bookTitles[title], bookName = title, isHistory=False: 254 | (bookList.place_forget(), 255 | displayChaptersCheck(href, bookName, isHistory))) 256 | bookButton.pack() 257 | 258 | bookList.place(x=0, y=35) 259 | searchButton.place(x=700, y=5) 260 | contextMenu.place(x=20, y=360) 261 | except: 262 | messagebox.showerror("Error", "Please select a host from the dropdown menu.") 263 | 264 | 265 | # Create History widgets 266 | historyList = customtkinter.CTkScrollableFrame(root, width=200, height=340, 267 | fg_color="#242424") 268 | historyFrame = customtkinter.CTkFrame(root) 269 | historyImage = customtkinter.CTkLabel(historyFrame, image=historyLabelIcon, text="", fg_color="#242424") 270 | historyImage.grid(row=0, column=0) 271 | labelFont = customtkinter.CTkFont(family="Arial Rounded MT Bold", size=14) 272 | historyText = customtkinter.CTkLabel(historyFrame, 273 | text="History:", 274 | font=labelFont, 275 | anchor="center", 276 | fg_color="#242424") 277 | historyText.grid(row=0, column=4) 278 | returnToHistory = customtkinter.CTkButton(root, width=70, height=30, 279 | fg_color=fgColor, text="Back", 280 | command=lambda: (returnToHistory.place_forget(), 281 | bookChapters.place_forget(), 282 | bookmarkButton.place_forget(), 283 | coverImageLabel.place_forget(), 284 | informationDisplay.place_forget(), 285 | downloadallChapters.place_forget(), 286 | formatSelector.place_forget(), 287 | compressionMethodMenu.place_forget(), 288 | searchButton.place(x=700, y=5), 289 | historyFrame.place(x=60, y=45), 290 | historyList.place(x=130, y=40), 291 | displayBookmarks(), 292 | searchButton.place(x=700, y=5) 293 | )) 294 | 295 | def displayHistory(): 296 | for widget in historyList.winfo_children(): 297 | # Destroy the children of the bookmarkList widget 298 | # Once they're destroyed, they'll be readded again after loading the data from the JSON file 299 | # This is done to update bookmarkList after a book has been added or removed 300 | widget.destroy() 301 | historyList.place(x=130, y=40) 302 | historyFrame.place(x=60,y=45) 303 | 304 | # Check if the history file exists, if not, create it 305 | if not historyJsonPath.exists(): 306 | historyJsonPath.touch() 307 | base = {"books": []} 308 | with open(historyJsonPath, 'w') as jsonFile: 309 | json.dump(base, jsonFile, indent=4) 310 | 311 | # Load the history JSON file assign the JSON keys to their corresponding variables 312 | # We're reversing the list to display the most recent history first 313 | with open(historyJsonPath, "r") as jsonFile: 314 | data = json.load(jsonFile) 315 | if data['books']: 316 | for book in reversed(data['books']): 317 | bookLink = book.get('bookLink') 318 | bookName = book.get('bookName') 319 | historyHost = book.get('selectedHost') 320 | if bookLink and bookName: 321 | # Don't display the book if its cover image doesn't exist 322 | if os.path.exists(f"./cache/history/{historyHost}/{bookName}.png"): 323 | coverImage = Image.open(f"./cache/history/{historyHost}/{bookName}.png") 324 | bookNameButton = customtkinter.CTkButton(historyList, 325 | image=customtkinter.CTkImage(light_image=coverImage 326 | ,dark_image=coverImage,size=(166, 256)), 327 | text=f"{historyHost}", 328 | compound="top", 329 | font=labelFont, 330 | fg_color=fgColor, 331 | command=lambda href=bookLink, name=bookName, isHistory=True, historyHost=historyHost: 332 | (selectHost(historyHost), historyList.place_forget(), historyFrame.place_forget(), 333 | bookmarkList.place_forget(), bookmarkFrame.place_forget(), 334 | displayChaptersCheck(href, name, isHistory))) 335 | bookNameButton.pack() 336 | 337 | if len(data['books']) == 0: 338 | # Display a button if the history is empty (Keys inside JSON = 0) 339 | emptyJsonButton = customtkinter.CTkButton(historyList, 340 | width=170, 341 | height=30, 342 | text="History is empty!", 343 | fg_color=fgColor) 344 | emptyJsonButton.pack() 345 | 346 | 347 | def saveBookToHistory(bookLink, bookName): 348 | print("Saving") 349 | with open("./cache/history/history.json", "r") as jsonFile: 350 | data = json.load(jsonFile) 351 | 352 | newBook = { "bookLink": bookLink, "bookName": bookName, "selectedHost": selectedHost} 353 | 354 | if len(data["books"]) >= 10: 355 | # Limit the number of history books to 10 356 | bookNameToRemove = data["books"][0].get('bookName') 357 | bookHostToRemove = data["books"][0].get('selectedHost') 358 | os.remove(f"./cache/history/{bookHostToRemove}/{bookNameToRemove}.png") 359 | data["books"].pop(0) 360 | 361 | 362 | for book in data["books"]: 363 | # Check if the book already exists in the history, if so, don't add it 364 | if book == newBook: 365 | return 366 | 367 | data["books"].append(newBook) 368 | with open("./cache/history/history.json", "w") as jsonFile: 369 | json.dump(data, jsonFile, indent=4) 370 | displayHistory() 371 | 372 | # Create Bookmark widgets 373 | bookmarkList = customtkinter.CTkScrollableFrame(root, width=200, height=340, fg_color="#242424") 374 | bookmarkFrame = customtkinter.CTkFrame(root) 375 | bookmarkImage = customtkinter.CTkLabel(bookmarkFrame, image=bookmarkIcon, 376 | text="", fg_color="#242424") 377 | bookmarkImage.grid(row=0, column=0) 378 | bookmarkText = customtkinter.CTkLabel(bookmarkFrame, 379 | text="Bookmarks:", 380 | font=labelFont, 381 | anchor="center", 382 | fg_color="#242424") 383 | bookmarkText.grid(row=0, column=4) 384 | 385 | def displayBookmarks(): 386 | for widget in bookmarkList.winfo_children(): 387 | # Destroy the children of the bookmarkList widget 388 | # Once they're destroyed, they'll be readded again after loading the data from the JSON file 389 | # This is done to update bookmarkList after a book has been added or removed 390 | widget.destroy() 391 | 392 | bookmarkList.place(x=470, y=40) 393 | bookmarkFrame.place(x=370,y=45) 394 | 395 | if not bookmarksJsonPath.exists(): 396 | # Create a new JSON file if it doesn't exist 397 | bookmarksJsonPath.touch() 398 | base = {"books": []} 399 | with open(bookmarksJsonPath, 'w') as jsonFile: 400 | json.dump(base, jsonFile, indent=4) 401 | 402 | with open(bookmarksJsonPath, "r") as jsonFile: 403 | data = json.load(jsonFile) 404 | 405 | if data['books']: 406 | for book in reversed(data['books']): 407 | # Reversed so the most recent book is at the top 408 | bookLink = book.get('bookLink') 409 | bookName = book.get('bookName') 410 | historyHost = book.get('selectedHost') 411 | 412 | bookmarkedBooks.append(bookLink) 413 | 414 | if bookLink and bookName: 415 | # If the cover image does not exist, the book is not going to be added to bookmarkList 416 | if os.path.exists(f"./cache/bookmark/{historyHost}/{bookName}.png"): 417 | coverImage = Image.open(f"./cache/bookmark/{historyHost}/{bookName}.png") 418 | bookNameButton = customtkinter.CTkButton(bookmarkList, 419 | image=customtkinter.CTkImage(light_image=coverImage 420 | ,dark_image=coverImage,size=(166, 256)), 421 | text=f"{historyHost}", 422 | compound="top", 423 | font=labelFont, 424 | fg_color=fgColor, 425 | command=lambda href=bookLink, name=bookName, isHistory=True, historyHost=historyHost: 426 | (selectHost(historyHost), bookmarkFrame.place_forget(), bookmarkList.place_forget(), 427 | historyFrame.place_forget(), historyList.place_forget(), 428 | displayChaptersCheck(href, name, isHistory))) 429 | bookNameButton.pack() 430 | if len(data['books']) == 0: 431 | # If there are no bookmarks, display a button telling the user 432 | emptyJsonButton = customtkinter.CTkButton(bookmarkList, 433 | width=170, 434 | height=30, 435 | text="No bookmarks!", 436 | fg_color=fgColor) 437 | emptyJsonButton.pack() 438 | 439 | 440 | def saveBookmark(cover, link, name): 441 | with open("./cache/bookmark/bookmarks.json", "r") as jsonFile: 442 | data = json.load(jsonFile) 443 | 444 | # Check if the directory exists, if not, create it and save the cover image inside of it 445 | os.makedirs(f'./cache/bookmark/{selectedHost}', exist_ok=True) 446 | cover.save(f"./cache/bookmark/{selectedHost}/{name}.png") 447 | 448 | newBook = { "bookLink": link, "bookName": name, "selectedHost": selectedHost} 449 | if len(data["books"]) >= 10: 450 | # The limit of bookmarkList is 10 books, if the list is full, remove the oldest book 451 | bookNameToRemove = data["books"][0].get('bookName') 452 | bookHostToRemove = data["books"][0].get('selectedHost') 453 | os.remove(f"./cache/bookmark/{bookHostToRemove}/{bookNameToRemove}.png") 454 | data["books"].pop(0) 455 | 456 | for book in data["books"]: 457 | # if the book already exists, don't add it 458 | if book == newBook: 459 | return 460 | 461 | data["books"].append(newBook) 462 | with open("./cache/bookmark/bookmarks.json", "w") as jsonFile: 463 | json.dump(data, jsonFile, indent=4) 464 | 465 | def removeBookmark(link, name): 466 | selectedHost = "" 467 | bookmarkedBooks.remove(link) 468 | 469 | with open(bookmarksJsonPath, "r") as jsonFile: 470 | data = json.load(jsonFile) 471 | 472 | for book in data["books"]: 473 | if book["bookLink"] == link: 474 | # The bookLink contains both the host and the name of the book so we're using it to check for duplicates 475 | selectedHost = book["selectedHost"] 476 | data["books"].remove(book) 477 | break 478 | 479 | os.remove(f"./cache/bookmark/{selectedHost}/{name}.png") 480 | with open(bookmarksJsonPath, "w") as jsonFile: 481 | json.dump(data, jsonFile, indent=4) 482 | displayBookmarks() 483 | 484 | 485 | def searchProcessCheck(): 486 | if selectedHost not in ["comixextra.com" , "mangakomi.io", "mangaread.org", "mangakatana.com", "mangakakalot.tv", "rawkuma.com", "mangaweebs.org"]: 487 | searchButton.place_forget() 488 | messagebox.showerror("Error", "Please select a host from the dropdown menu.") 489 | else: 490 | searchButton.place_forget() 491 | bookList.place_forget() 492 | for widget in bookList.winfo_children(): 493 | if isinstance(widget, customtkinter.CTkButton): 494 | widget.destroy() 495 | threading.Thread(target=searchProcess).start() 496 | 497 | 498 | def generateChapterButtons(bookChapters, session, selectedHost, bookName, isMassDownload, directory, numberofLoops, cbzVerification): 499 | if len(bookChapterNames) > 200: 500 | chapterNames = list(bookChapterNames.keys()) 501 | chapterSelectPlaceholderText = customtkinter.StringVar(value="Select a chapter from this drop-down list.") 502 | chapterSelect = customtkinter.CTkOptionMenu(bookChapters, width=500, height=30, 503 | values=chapterNames, 504 | fg_color=fgColor, button_color=fgColor, 505 | variable=chapterSelectPlaceholderText, 506 | command = lambda chapterName: getChapterFromOptionMenu(chapterName, session, selectedHost, 507 | bookName, isMassDownload, directory, 508 | numberofLoops, cbzVerification, bookChapterNames)) 509 | chapterSelect.pack() 510 | else: 511 | for chapterName in bookChapterNames: 512 | chapterButton = customtkinter.CTkButton(bookChapters, width=300, height=20, text=chapterName, 513 | border_spacing=5, border_color="#000000", 514 | fg_color=fgColor, command = lambda title=bookChapterNames[chapterName], bookName=bookName: 515 | threading.Thread(target=getPages, args=(title, session, selectedHost, 516 | bookName, isMassDownload, directory, numberofLoops, cbzVerification)).start()) 517 | chapterButton.pack() 518 | 519 | def getChapterFromOptionMenu(chapterName, session, selectedHost, bookName, isMassDownload, directory, numberofLoops, cbzVerification, bookChapterNames): 520 | title = bookChapterNames[chapterName] 521 | threading.Thread(target=getPages, args=(title, session, selectedHost, 522 | bookName, isMassDownload, directory, numberofLoops, cbzVerification)).start() 523 | 524 | def displayChaptersCheck(href, bookName, isHistory): 525 | contextMenu.place_forget() 526 | searchButton.place_forget() 527 | returnToHistory.place_forget() 528 | loadingFrame.place(x=350, y=170) 529 | for widget in bookChapters.winfo_children(): 530 | widget.destroy() 531 | threading.Thread(target=displayChapters, args=(href, bookName, isHistory)).start() 532 | 533 | 534 | searchButton = customtkinter.CTkButton(master=root, width=70, height=30, fg_color=fgColor, text="Search", command=searchProcessCheck) 535 | searchButton.place(x=700, y=5) 536 | 537 | 538 | def returnToHome(): 539 | contextMenu.place_forget() 540 | bookList.place_forget() 541 | displayBookmarks() 542 | displayHistory() 543 | 544 | 545 | contextMenu = customtkinter.CTkFrame(master=root, width=150, height=30, fg_color="#242424") 546 | showDownloads = customtkinter.CTkButton(master=contextMenu, width=70, height=30, fg_color=fgColor, text="Downloads", command=getDownloads, image=downloadButtonIcon) 547 | homeButton = customtkinter.CTkButton(master=contextMenu, text="Home", width=30, height=30, fg_color=fgColor, image=homeIcon, command=returnToHome) 548 | showDownloads.pack(side="right", padx=570) 549 | homeButton.pack(side="left") 550 | 551 | 552 | downloadallChapters = customtkinter.CTkButton(master=root, image=downloadButtonIcon, text="Download All Chapters", width=170, fg_color=fgColor, command=lambda: threading.Thread(target=getAllChapters).start()) 553 | 554 | coverImageLabel = customtkinter.CTkLabel(root, text="", image=None) 555 | 556 | 557 | returnToList = customtkinter.CTkButton(master=root, width=70, height=30, 558 | fg_color=fgColor, text="Back", 559 | command=lambda: (bookList.place(x=0, y=35), returnToList.place_forget(), 560 | bookChapters.place_forget(), searchButton.place(x=700, y=5), 561 | coverImageLabel.place_forget(), 562 | informationDisplay.place_forget(), 563 | downloadallChapters.place_forget(), 564 | formatSelector.place_forget(), 565 | bookmarkButton.place_forget(), 566 | compressionMethodMenu.place_forget(), 567 | contextMenu.place(x=20, y=360))) 568 | 569 | hostSelector = customtkinter.CTkOptionMenu(root, width=170 ,values=hostValues, fg_color=fgColor, button_color=fgColor, command=selectHost) 570 | hostSelector.place(x=8, y=5) 571 | 572 | formatSelector = customtkinter.CTkOptionMenu(root, width=100, height=30, values=formatValues, fg_color=fgColor, 573 | button_color=fgColor, command=selectFormat, anchor="center") 574 | compressionMethodMenu = customtkinter.CTkOptionMenu(root, values=["Stored", "BZIP2", "LZMA", "Deflate"], button_color=fgColor, 575 | fg_color=fgColor, anchor="center") 576 | 577 | searchBar = customtkinter.CTkTextbox(master=root, width=500, height=30) 578 | searchBar.place(x=185, y=5) 579 | searchBar.bind('', lambda event: "break") 580 | 581 | 582 | bookList = customtkinter.CTkScrollableFrame(root, width=770, height=300 , fg_color="#242424") 583 | bookChapters = customtkinter.CTkScrollableFrame(root, width=350, height=360, fg_color="#242424") 584 | 585 | bookmarkButton = customtkinter.CTkButton(master=root, text="", width=30, height=30, fg_color=fgColor, image=bookmarkIcon) 586 | 587 | loadingFont = customtkinter.CTkFont(family="Arial Rounded MT Bold", size=25) 588 | loadingFrame = customtkinter.CTkFrame(master=root) 589 | loadingText = customtkinter.CTkLabel(loadingFrame, text="Loading...", font=loadingFont, fg_color="#242424") 590 | loadingImage = customtkinter.CTkLabel(loadingFrame, text="", image=loadingIcon, fg_color="#242424") 591 | loadingImage.grid(row=0, column=0) 592 | loadingText.grid(row=0, column=4) 593 | 594 | informationDisplay = customtkinter.CTkFrame(root, fg_color="#242424") 595 | informationLabel = customtkinter.CTkLabel(informationDisplay, text="Book Name", font= customtkinter.CTkFont(family="Arial Rounded MT Bold", size=12), wraplength=220) 596 | 597 | root.mainloop() 598 | 599 | 600 | --------------------------------------------------------------------------------