├── 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 | [](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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------