├── .github
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
└── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── feature_request.yml
├── .gitignore
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── app.py
├── docs
├── help.md
├── images
│ ├── 2023-04-02
│ │ ├── hero(2023-04-02).png
│ │ ├── new-manga(2023-04-02).png
│ │ └── update-manga(2023-04-02).png
│ ├── 2023-05-01
│ │ ├── hero(2023-05-01).png
│ │ ├── new-manga(2023-05-01).png
│ │ └── update-manga(2023-05-01).png
│ ├── 2023-06-24
│ │ ├── anime-list(2023-06-24).png
│ │ ├── hero(2023-06-24).png
│ │ ├── manga-list(2023-06-24).png
│ │ ├── update-anime(2023-06-24).png
│ │ └── update-manga(2023-06-24).png
│ └── others
│ │ └── star.png
├── index.html
├── readme.txt
└── videos
│ └── host-on-pythonanywhere.mp4
├── pythonanywhere.py
├── requirements.txt
└── src
├── __init__.py
├── anime
├── __init__.py
├── backup.py
├── forms.py
├── routes.py
└── utils.py
├── api
├── anime
│ ├── __init__.py
│ └── routes.py
└── manga
│ ├── __init__.py
│ └── routes.py
├── config.py
├── errors
├── __init__.py
└── handlers.py
├── home
├── __init__.py
├── routes.py
└── utils.py
├── manga
├── __init__.py
├── backup.py
├── forms.py
├── routes.py
├── utils.py
└── web_scraper.py
├── models.py
├── settings
├── __init__.py
├── forms.py
└── routes.py
├── static
├── anime_cover
│ └── default-anime.svg
├── error_assests
│ ├── 404 not found.gif
│ ├── 405 not allowed.jpg
│ └── 500 server down.jpg
├── favicon
│ ├── about.txt
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
└── manga_cover
│ └── default-manga.svg
└── templates
├── anime
├── anime-cards.html
├── anime-list.html
├── create-anime.html
├── edit-anime.html
└── import-anime.html
├── credits.html
├── errors
├── 404.html
├── 405.html
└── 500.html
├── home.html
├── layout.html
├── manga
├── create-manga.html
├── edit-manga.html
├── import-manga.html
├── manga-cards.html
└── manga-list.html
├── more.html
└── settings.html
/.github/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.4.0
4 |
5 | ### New
6 |
7 | - .exe file to run on windows
8 | - Server start message
9 | - Opens site on browser as soon as the script is run
10 |
11 | ### Updated
12 |
13 | - Readme.md
14 | - Cleared roadmap
15 | - Updated how to use guide
16 | - Added a guide to host on pythonanywhere.org
17 | - Removed running from MMDB.cmd/sh
18 |
19 | ### Fixed
20 |
21 | - Update modal not showing whenever a new MMDB version is released
22 |
23 | ### Removed
24 |
25 | - Command files to install pip packages and run MyMangaDataBase
26 |
27 | ## 2.3.0
28 |
29 | ### New
30 |
31 | - Shameless MMDB social promotions :P and setting to remove it
32 | - Image overlay card layout
33 | - Cascading grid layout using [Masonary](https://masonry.desandro.com/) and [imagesLoaded](https://imagesloaded.desandro.com/)
34 | - MyMangaDataBase APIs -
35 | - Add
36 | - Edit
37 | - Delete
38 |
39 | ### Updated
40 |
41 | - Containerised settings.html (Now its more centered)
42 | - Showing AniList API credits on import pages
43 | - Alternativeto url when redirecting from flash message
44 | - "Truncate Title" logic
45 |
46 | ### Fix
47 |
48 | - Corrected wording
49 | - Linting, sorting & removing unused imports
50 |
51 | ## 2.2.1
52 |
53 | ### Fix
54 |
55 | - New version update message not showing
56 | - Metadata from MangaUpdates giving error
57 |
58 | ## 2.2.0
59 |
60 | ### New
61 |
62 | - Search Feature
63 | - Manga
64 | - Anime
65 | - Import user list from
66 | - MangaUpdates (Manga)
67 | - AniList (Manga and Anime)
68 | - 405 Method Not Allowed error page
69 | - Get online metadata from [MangaUpdates](mangaupdates.com)
70 |
71 | ### Updated
72 |
73 | - Extra file check before uploading MAL import file
74 | - Exports and Backup deletion logic
75 |
76 | ### Fixed
77 |
78 | - Card layout not being centered
79 | - Anime info not opening when truncating title in table mode
80 | - Getting error when checking for update when no internet connection
81 | - No image showing on 404 error page
82 |
83 | ## 2.1.0
84 |
85 | ### New
86 |
87 | - Cards Layout
88 |
89 | ### Updated
90 |
91 | - "Star on github" link
92 | - Image links
93 | - Images (github)
94 |
95 | ### Fixed
96 |
97 | - Little bad code
98 |
99 | ## 2.0.0
100 |
101 | ### New
102 |
103 | - Anime Section
104 | - Anime List
105 | - Create Anime
106 | - Import
107 | - MAL
108 | - MMDB
109 | - SVG Icon
110 | - Home Page
111 | - Overviews
112 | - Manga
113 | - Anime
114 | - More Page
115 | - User Interface
116 | - APIs
117 | - Jikan (Use to collect Manga Information)
118 | - OtakuXYZ (Shown on error pages)
119 | - Metadata: Genre
120 | - Themes
121 | - Dark
122 | - Light
123 |
124 | ### Updated
125 |
126 | - Rename database filename from `manga.db` to `database.db`
127 | - Table/List User Interface
128 | - Switeched functions of tags field to genre field
129 | - Genre field is for official tags
130 | - Tags field is for user desired tags
131 | - Chapter Logging
132 | - Before it was used to log chapters based on date and show history all at once
133 | - Now it logs chapters based on title name and shows each title history separetly
134 | - Instructions in readme.txt
135 | - [Website](https://edwinrodger.github.io/MyMangaDataBase/)
136 | - Folder Structure
137 | - Latest Images
138 | - Many Internal Functions
139 |
140 | ### Removed
141 |
142 | - default png image
143 | - Before, the default cover can be png or svg
144 | - Now it is only svg
145 | - Unused errors in create and update/edit forms
146 | - Many functions and routes (some will be added in future updates)
147 | - Ascending and descending sort
148 | - MangaUpdates import etc.
149 |
150 | ## 1.9.0
151 |
152 | ### New
153 |
154 | - Add metadata while creating entry
155 |
156 | ## 1.8.0
157 |
158 | ### New
159 |
160 | - Link to Alternativeto.net
161 | - https://alternativeto.net/software/mymangadatabase/about/
162 | - Routes to error pages
163 | - 404
164 | - 500
165 | - Sorting of Table Heads
166 | - Ascending (Asc)
167 | - Descensing (Desc)
168 | - Weekly Automatic Backups
169 | - Every sunday
170 | - Dashboard
171 | - No. of Manga
172 | - Genre
173 | - Score
174 | - History
175 | - Open Source libraries
176 | - Chapter Logging
177 | - Logs amount of chapters read per title per day
178 | - Updation of manga
179 | - Deletion of manga
180 | - **Added MangaUpdates Import**
181 | - Imports every normal list
182 | - Doesn't support custom lists
183 | - Editable Metadata
184 | - Description
185 | - Author
186 | - Artist
187 | - Tags
188 |
189 | ### Updated
190 |
191 | - `--run-with-{server}` warning messages
192 | - localhost.run
193 | - ngrok
194 | - Libraries
195 | - **Migrate settings file**
196 | - From `ini` to `json`
197 | - Linting
198 | - Table head code
199 | - Rename
200 | - `main/utils.py` -> `main/backup.py`
201 | - Edit page layout
202 | - Import description
203 | - Added steps to import MyAnimeList backup
204 | - Added information about status assignment in MangaUpdates backup
205 |
206 | ### Fixed
207 |
208 | - No indentation in json file while updating setting
209 | - Importing MyAnimeList backup giving server error
210 |
211 | ###### Released On: 01 May 2023
212 |
213 | ---
214 |
215 | ## 1.7.0
216 |
217 | ### New
218 |
219 | - **NEW DEMO SITE!!!** at https://mymangadatabase.pythonanywhere.com/
220 | - An occasional flash message to star MMDB on github and setting to toggle it On or Off
221 | - Arguments -
222 | - `--run-with-ngrok`, Hosts the server on [ngrok](https://ngrok.com/)
223 | - Requires ngrok to be installed on the system
224 | - Requires ngrok account
225 | - Requires authtoken to be configured by ngrok
226 | - `--run-with-localhost`, Hosts the server on [localhost.run](https://localhost.run)
227 | - Load site slowly
228 | - Blueprint: errors, new error pages
229 | - Blueprint: functions, to make simple database operations easier
230 | - `help.md` to help non-technical users with basic things
231 | - Links to github under info section -
232 | - Bug report
233 | - Feature request
234 | - Star MyMangaDataBase
235 | - Packages -
236 | - [sqlite-web](https://github.com/coleifer/sqlite-web) (dev)
237 | - [flask-ngrok2](https://github.com/MohamedAliRashad/flask-ngrok2)
238 | - [pylint](https://pylint.readthedocs.io/en/latest/)
239 | - Rich help panel
240 | - Search by genre
241 | - Three setting sections -
242 | - Defaults
243 | - Column Interface
244 | - Flash Messages
245 |
246 | ### Fixed
247 |
248 | - Overflowing of metadata content from main div on smaller devices
249 | - Typos
250 |
251 | ### Updated
252 |
253 | - Change `Info` to `Help` in navbar
254 | - Changed `sort_func` variable to `status_value`
255 | - Python Packages
256 | - Rerouting to same manga page after updating manga/metadata
257 | - Whole code standards according to PyLint
258 |
259 | ### Deleted
260 |
261 | - Packages
262 | - pillow
263 | - python-dotenv
264 |
265 | ###### Released On: 01 March 2023
266 |
267 | ---
268 |
269 | ## 1.6.0
270 |
271 | ### New
272 |
273 | - Manga Metadata
274 | - Covers
275 | - Description
276 | - Author
277 | - Artist
278 | - Tags
279 | - Update whole database metadata at once or one metadata at a time
280 | - Issue template
281 | - Using yml instead of md
282 | - [SVG icon](../src/static/manga_cover/default.svg)
283 | - App arguments
284 | - `--version`
285 | - `--development`
286 | - `--logging`
287 | - Development environment for devs
288 | - `app_runner.py --development`
289 | - Logger
290 | - Logs in the Apache Combined Log Format
291 | - Settings to customise User Interface
292 | - Configuration in `config.ini` file
293 | - Rich progressbars and help messages
294 | - New Font [Nunito](https://fonts.google.com/specimen/Nunito)
295 |
296 | ### Fixed/Updated
297 |
298 | - Replace png image with svg icon
299 | - default.png -> default.svg
300 | - Skipping check for update when no internet connection
301 | - Updated database for additional metadata
302 | - Added
303 | - Author
304 | - Artist
305 | - Updated
306 | - Default image name in 'cover' column from default.png to default.svg
307 | - Changed rendering file in sort function
308 | - sorted_manga.html -> table.html
309 | - Updated import and export functions
310 | - Showing MMDB version when running the app
311 | - Updated navbar
312 | - Updated ['About MMDB'](https://edwinrodger.github.io/MyMangaDataBase/) page
313 | - Added colors to the messages
314 | - Fixed bug where manga id and date was getting sent to URL after updating the manga
315 | - Random placeholder text in search field everytime the page is loaded
316 | - Change filename
317 | - home.html -> table.html
318 | - manga_id.html -> edit.html
319 | - create_manga.html -> create-manga.html
320 | - Centered text in table
321 | - New and clear routes for same function
322 | - Updated links in readme.txt
323 |
324 | ### Deleted
325 |
326 | - Old github issue files -
327 | - bug_report.md
328 | - feature_request.md
329 | - sorted_manga.html
330 | - Removed all "# type: ignore" (used to hide error less code warning messages)
331 | - Removed .env file
332 |
333 | ###### Released On: 01 Feb 2023
334 |
335 | ---
336 |
337 | ## 1.5.0
338 |
339 | ### New
340 |
341 | - Basic comments and docstrings in code
342 | - Column for cover image (Though it only displays a default picture)
343 | - Delete database route
344 | - Linux support (Can be very buggy because it was tested using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about))
345 |
346 | ### Fixed/Updated
347 | - Better function names
348 | - Changed MMDB export to .json file from .db file
349 | - Drastically decrease the import time
350 | - Default sorting is now alphabetical
351 | - Updated packages
352 | - Updated information messages
353 | - Fixed a bug where score gets reset to zero every time you edit the manga
354 | - Updated pipenv run commands
355 |
356 | ###### Released On: 01/01/2023
357 |
358 | ---
359 |
360 | ## 1.4.0
361 |
362 | ### New
363 |
364 | - MyAnimeList import
365 | - Warning messages when uploading wrong kind of backups
366 | - Search Bar
367 |
368 | ### Fixed/Updated
369 |
370 | - Updated edit manga page
371 | - Changed link for about page
372 | - External pages will now open in another tab
373 | - Updated import messages
374 |
375 | ### Deleted
376 |
377 | - Removed untested code
378 | - About.html
379 |
380 | ---
381 |
382 | ## 1.3.0
383 |
384 | ### New
385 |
386 | - Added repository and author's website
387 | - Added `readme.txt` in zip file
388 | - Added extra page for manga info
389 | - New [Website](https://edwinrodger.github.com/MyMangaDataBase)
390 | - [CODE_OF_CONDUCT.md](https://github.com/EdwinRodger/MyMangaDataBase/blob/main/.github/CODE_OF_CONDUCT.md)
391 | - [CONTRIBUTING.md](https://github.com/EdwinRodger/MyMangaDataBase/blob/main/.github/CONTRIBUTING.md)
392 | - [LICENSE](https://github.com/EdwinRodger/MyMangaDataBase/blob/main/LICENSE)
393 | - [Issue Templates](https://github.com/EdwinRodger/MyMangaDataBase/tree/main/.github/ISSUE_TEMPLATE)
394 |
395 | ### Fixed/Updated
396 |
397 | - Updated How to use and Contribution guide in `README.md`
398 | - Updated software update checker
399 | - Better choice handeling while asking for update
400 |
401 | ### Deleted
402 |
403 | - requirements.txt
404 | - MyMangaDataBase.sh
405 | - MyMangaDataBase.bat
406 |
407 | ###### Released On: 12-11-2022
408 |
409 | ---
410 |
411 | ## 1.2.0
412 |
413 | ### New
414 |
415 | - Added Import and Export for MMDB database
416 |
417 | ### Fixed/Updated
418 |
419 | - Made a working sh file for MacOS and Linux system
420 |
421 | ###### Released On: 5-11-2022
422 |
423 | ---
424 |
425 | ## 1.1.0
426 |
427 | ### New
428 |
429 | 1. Added favicon
430 | 2. Using _Waitress_ module to host application
431 |
432 | ### Fixed/Updated
433 |
434 | 1. Decreased the time to start the app
435 | 2. Date showing when set to 01-01-0001
436 | 3. Did linting
437 | 4. Sorted Imports
438 |
439 | ### Help Wanted
440 |
441 | 1. If you can update `MyMangaDataBase.sh` file to run `app_runner.py` file, do share the code via pull request. It will help other UNIX users too
442 | 2. If there is any problem in `app_runner.py` file in MacOS or Linux, kindly fix that also as I am Windows user and don't know a thing about MacOS and Linux 😅
443 |
444 | ###### Released On: 31-10-2022
445 |
446 | ---
447 |
448 | ## 1.0.0
449 |
450 | ## First Release of MyMangaDataBase
451 |
452 | ###### Released On: 31-10-2022
453 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | Discussion Section.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Welcome to MyMangaDataBase contributing guide
2 |
3 | Thank you for investing your time in contributing to our project! :sparkles:.
4 |
5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
6 |
7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
8 |
9 | ## New contributor guide
10 |
11 | To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:
12 |
13 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
14 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
15 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
16 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
17 |
18 |
19 | ## Getting started
20 |
21 | To contribute to this project, you must have basic understanding of [Python](https://www.python.org/) and [Python Packages](https://pypi.org/)
22 |
23 | ### Issues
24 |
25 | #### Create a new issue
26 |
27 | If you spot a problem within the program, [search if an issue already exists](https://github.com/EdwinRodger/MyMangaDataBase/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/EdwinRodger/MyMangaDataBase/issues/new/choose).
28 |
29 | #### Solve an issue
30 |
31 | Scan through our [existing issues](https://github.com/EdwinRodger/MyMangaDataBase/issues) to find one that interests you. You can narrow down the search using `labels` as filters. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
32 |
33 | ### Make Changes
34 |
35 | #### Make changes in UI
36 |
37 | All the UI is made using HTML, CSS ([Bootstrap](https://getbootstrap.com/docs/5.2/getting-started/introduction/)) and Jinja Templating Engine. You can find all the code for UI in `templates` folder
38 |
39 | #### Make changes in code
40 |
41 | This project follows a simple folder structure. If you want to make change which is related to manga, you can find all the code in `manga` folder. If the change is related to Home page or About Page, you can find it's code in `main` folder.
42 |
43 | ### Commit your update
44 |
45 | Commit the changes once you are happy with them.
46 |
47 | ### Pull Request
48 |
49 | When you're finished with the changes, create a pull request, also known as a PR.
50 | - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
51 | Once you submit your PR, we may ask questions or request additional information.
52 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can make any other changes in your fork, then commit them to your branch.
53 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
54 | - If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
55 |
56 | ### Your PR is merged!
57 |
58 | Congratulations :tada::tada::sparkles:.
59 |
60 | Once your PR is merged, your contributions will be publicly visible on the [MyMangaDataBase Contributors](https://github.com/EdwinRodger/MyMangaDataBase/graphs/contributors).
61 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Issue report
2 | description: Report an issue in MyMangaDataBase
3 | labels: [Bug]
4 | body:
5 |
6 | - type: textarea
7 | id: reproduce-steps
8 | attributes:
9 | label: Steps to reproduce
10 | description: Provide an example of the issue.
11 | placeholder: |
12 | Example:
13 | 1. First step
14 | 2. Second step
15 | 3. Issue here
16 | validations:
17 | required: true
18 |
19 | - type: textarea
20 | id: expected-behavior
21 | attributes:
22 | label: Expected behavior
23 | description: Explain what you should expect to happen.
24 | placeholder: |
25 | Example:
26 | "This should happen..."
27 | validations:
28 | required: true
29 |
30 | - type: textarea
31 | id: actual-behavior
32 | attributes:
33 | label: Actual behavior
34 | description: Explain what actually happens.
35 | placeholder: |
36 | Example:
37 | "This happened instead..."
38 | validations:
39 | required: true
40 |
41 | - type: input
42 | id: MyMangaDataBase-version
43 | attributes:
44 | label: MyMangaDataBase version
45 | description: You can find your MyMangaDataBase version written in folder name.
46 | placeholder: |
47 | Example: "1.5.0"
48 | validations:
49 | required: true
50 |
51 | - type: textarea
52 | id: other-details
53 | attributes:
54 | label: Other details
55 | placeholder: |
56 | Additional details and attachments.
57 |
58 | - type: checkboxes
59 | id: acknowledgements
60 | attributes:
61 | label: Acknowledgements
62 | description: Read this carefully, we will close and ignore your issue if you skimmed through this.
63 | options:
64 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
65 | required: true
66 | - label: I have written a short but informative title.
67 | required: true
68 | - label: I have updated the app to the lastest version.
69 | required: true
70 | - label: I will fill out all of the requested information in this form.
71 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: ⭐ Feature request
2 | description: Suggest a feature to improve MyMangaDataBase
3 | labels: [Feature request]
4 | body:
5 |
6 | - type: textarea
7 | id: feature-description
8 | attributes:
9 | label: Describe your suggested feature
10 | description: How can MyMangaDataBase be improved?
11 | placeholder: |
12 | Example:
13 | "It should work like this..."
14 | validations:
15 | required: true
16 |
17 | - type: textarea
18 | id: other-details
19 | attributes:
20 | label: Other details
21 | placeholder: |
22 | Additional details and attachments.
23 |
24 | - type: checkboxes
25 | id: acknowledgements
26 | attributes:
27 | label: Acknowledgements
28 | description: Read this carefully, we will close and ignore your issue if you skimmed through this.
29 | options:
30 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
31 | required: true
32 | - label: I have written a short but informative title.
33 | required: true
34 | - label: I will fill out all of the requested information in this form.
35 | required: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | .idea/
161 |
162 | # VSCode
163 | .vscode/
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | flask = "*"
8 | waitress = "*"
9 | flask-sqlalchemy = "*"
10 | flask-wtf = "*"
11 | requests = "*"
12 | pillow = "*"
13 | beautifulsoup4 = "*"
14 |
15 | [dev-packages]
16 | black = "*"
17 | pyclean = "*"
18 | isort = "*"
19 |
20 | [requires]
21 | python_version = "3.10"
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MyMangaDataBase
2 |
3 | .png "Home Page")
4 |
5 | One database to keep track of your Anime and Manga.
6 |
7 | MyMangaDataBase (or MMDB for short) is
8 |
9 | - Free
10 | - Private
11 | - Open source
12 | - Self Hosted application to track all your anime and manga.
13 |
14 | Use MMDB to make your own Anime and Manga list without the fear of a certain manga/anime not present in your tracking site's database. MMDB is a self-hosted option which means all the data remains on your device.
15 |
16 | MMDB backend is made using Python(Flask) with HTML, CSS(Bootstrap5) and Jinja used as front-end.
17 |
18 | Try demo at https://mymangadatabase.pythonanywhere.com/
19 |
20 | ## Features
21 |
22 | - Dark Theme
23 | - Editable metadata (genre, description etc.)
24 | - Export to MMDB
25 | - Import from
26 | - AniList (Anime & Manga)
27 | - MangaUpdates (Manga)
28 | - MyAnimeList (Anime & Manga)
29 | - MMDB (Anime & Manga)
30 | - Responsive UI
31 | - Self Hosted
32 | - Sort by status
33 | - Search by genre or tags
34 |
35 | ## Road Map
36 |
37 | - Make APIs
38 |
39 | ## How To Use
40 |
41 | ### For Windows
42 |
43 | 1. Install latest MMDB version from releases page.
44 | 2. Extract and open zip file
45 | 3. Run MyMangaDataBase.exe
46 |
47 | ### For Linux (Tested using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about))
48 |
49 | 1. Install [Python](https://python.org) and [Git](https://git-scm.com) if not already installed
50 | 2. Install pipenv ([How to install](https://github.com/pypa/pipenv?tab=readme-ov-file#installation))
51 | 3. Clone repo or download MyMangaDataBase-{version}-linux from [latest releases](https://github.com/EdwinRodger/MyMangaDataBase/releases/latest)
52 | 4. Run - `cd ./MyMangaDataBase-{version}-linux/MyMangaDataBase-main`
53 | 5. Run - `pipenv install`
54 | 6. Run - `pipenv run ./app.py` or `pipenv run python3 ./app.py`
55 |
56 | ## Host on pythonanywhere.org
57 |
58 | Follow [this amazing video](https://github.com/EdwinRodger/MyMangaDataBase/tree/main/docs/videos) to host your own animanga database on pythonanywhere.org
59 |
60 | (No need to thank me on music used)
61 |
62 | ## Want to Contribute?
63 |
64 | See [CONTRIBUTING.md](.github/CONTRIBUTING.md)
65 |
66 | MyMangaDataBase is made in [python 3.10](https://www.python.org/downloads/release/python-3101/) and can be run on python>=3.8
67 |
68 | You can download the repository by going into 'Code' and then clicking 'Download ZIP' or just click [here](https://github.com/EdwinRodger/MyMangaDataBase/archive/refs/heads/main.zip) to download the same zip file
69 |
70 | If you have [git](https://git-scm.com/) installed on your device, you can clone the github repository by running the command below into your terminal -
71 |
72 | ```git
73 | git clone https://github.com/EdwinRodger/MyMangaDataBase.git
74 | ```
75 |
76 | 1. Run command `python app.py` (Make sure you are in virtual environement).
77 | 2. Open `http://127.0.0.1:6070/` in browser
78 |
79 | ## Debugging
80 |
81 | Pass `super-saiyan` argument while running `app.py`, it will turn on flask debugging.
82 |
83 | ## Extra Stuff
84 |
85 | To run the code perfectly, you can use any python version between 3.8 to 3.10 and this project is made in python version 3.10.1
86 |
87 | I tested MMDB on WSL which is basically linux environment in windows and the program is working fine on my end but I expect it to be buggy or worst, completly broken on actual linux system. If thats the case, open an issue on github and I will try to fix it.
88 |
89 | I used [jikan.moe](https://jikan.moe) api to get manga details. You can find the code [here](https://github.com/EdwinRodger/MyMangaDataBase/blob/48cde5db4b2e033b7164faad06c1a1baef9d2f4a/src/manga/backup.py#L115). To lower the load on server, there is a [1 second sleep](https://github.com/EdwinRodger/MyMangaDataBase/blob/48cde5db4b2e033b7164faad06c1a1baef9d2f4a/src/manga/backup.py#L185) between requests.
90 |
91 | I didn't learned javascript yet and that is why this project doesn't have any JS in it and every function is done in python using routes. If you find any javascript, I most probably copied it from stackoverflow or documentations.
92 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import sys
4 | import webbrowser
5 | from time import strftime
6 |
7 | import waitress
8 | from flask import request
9 |
10 | from src import create_app, db
11 | from src.anime.backup import delete_anime_export
12 | from src.manga.backup import delete_manga_export
13 | from src.settings.routes import create_json_files
14 |
15 | app = create_app()
16 |
17 |
18 | # This custom logging is taken from https://gist.github.com/alexaleluia12/e40f1dfa4ce598c2e958611f67d28966#file-flask_logging_requests-py-L28
19 | @app.after_request
20 | def after_request(response):
21 | timestamp = strftime("[%Y-%b-%d %H:%M]")
22 | logger.info(
23 | "%s %s %s %s %s",
24 | timestamp,
25 | request.method,
26 | request.scheme,
27 | request.full_path,
28 | response.status,
29 | )
30 | return response
31 |
32 |
33 | def checks():
34 | with app.app_context():
35 | db.create_all()
36 | create_json_files()
37 | delete_anime_export()
38 | delete_manga_export()
39 |
40 |
41 | def run():
42 | if sys.argv[-1].lower() == "super-saiyan":
43 | app.run(host="127.0.0.1", port=6070, debug=True)
44 | elif sys.argv[-1].lower() == "checks":
45 | checks()
46 | print("checks done!")
47 | else:
48 | print("Server running on http://127.0.0.1:6070")
49 | webbrowser.open_new_tab("http://127.0.0.1:6070")
50 | waitress.serve(app=app, host="127.0.0.1", port=6070)
51 |
52 |
53 | if __name__ == "__main__":
54 | checks()
55 | with open("json/settings.json", "r") as f:
56 | settings = json.load(f)
57 | logger = logging.getLogger("tdm")
58 | if settings["enable_logging"] == "Yes":
59 | logger.setLevel(logging.INFO)
60 | run()
61 |
--------------------------------------------------------------------------------
/docs/help.md:
--------------------------------------------------------------------------------
1 | # Star MyMangaDataBase
2 |
3 | Starring MyMangaDataBase is very easy, Just click the star icon on top-right of you webpage. That's it!
4 |
5 | 
--------------------------------------------------------------------------------
/docs/images/2023-04-02/hero(2023-04-02).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-04-02/hero(2023-04-02).png
--------------------------------------------------------------------------------
/docs/images/2023-04-02/new-manga(2023-04-02).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-04-02/new-manga(2023-04-02).png
--------------------------------------------------------------------------------
/docs/images/2023-04-02/update-manga(2023-04-02).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-04-02/update-manga(2023-04-02).png
--------------------------------------------------------------------------------
/docs/images/2023-05-01/hero(2023-05-01).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-05-01/hero(2023-05-01).png
--------------------------------------------------------------------------------
/docs/images/2023-05-01/new-manga(2023-05-01).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-05-01/new-manga(2023-05-01).png
--------------------------------------------------------------------------------
/docs/images/2023-05-01/update-manga(2023-05-01).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-05-01/update-manga(2023-05-01).png
--------------------------------------------------------------------------------
/docs/images/2023-06-24/anime-list(2023-06-24).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-06-24/anime-list(2023-06-24).png
--------------------------------------------------------------------------------
/docs/images/2023-06-24/hero(2023-06-24).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-06-24/hero(2023-06-24).png
--------------------------------------------------------------------------------
/docs/images/2023-06-24/manga-list(2023-06-24).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-06-24/manga-list(2023-06-24).png
--------------------------------------------------------------------------------
/docs/images/2023-06-24/update-anime(2023-06-24).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-06-24/update-anime(2023-06-24).png
--------------------------------------------------------------------------------
/docs/images/2023-06-24/update-manga(2023-06-24).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/2023-06-24/update-manga(2023-06-24).png
--------------------------------------------------------------------------------
/docs/images/others/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/images/others/star.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
A personal, simple and strightforward manga database to keep track of all your
37 | Anime and Manga. Your list, Your control! Don't wait for someone to add anime/manga to the
38 | site's
39 | database,
40 | make your own database.
41 |
42 | You can see the demo at https://mymangadatabase.pythonanywhere.com/
44 |
45 |
46 |
47 |
51 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
Features
60 |
61 |
62 |
63 |
64 |
65 |
FOSS
66 |
MyMangaDataBase is Free and Open Source Software built in Python as backend and HTML with
67 | Bootstrap5 as frontend. You can find the source code in the github repository.
68 |
69 |
70 |
71 |
72 |
73 |
Self hosted
74 |
MyMangaDataBase is fully self hosted option. It uses waitress as its hosting server.
77 |
78 |
79 |
80 |
81 |
82 |
Responsive UI
83 |
As MyMangaDataBase is made in Bootstrap5 which is known to be a mobile first CSS framework, MMDB
84 | has decent mobile user interface out of the box.
85 |
86 |
87 |
88 |
89 |
Screenshots
90 |
91 |
92 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
104 |
105 |
106 |
108 |
109 |
110 |
112 |
113 |
114 |
116 |
117 |
118 |
120 |
121 |
122 |
124 |
125 |
126 |
131 |
136 |
137 |
138 |
139 |
140 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/docs/readme.txt:
--------------------------------------------------------------------------------
1 | - Author: EdwinRodger (https://github.com/EdwinRodger/)
2 | - Source: https://github.com/EdwinRodger/MyMangaDataBase
3 | - License: GNU General Public License v3.0 (https://github.com/EdwinRodger/MyMangaDataBase/blob/main/LICENSE)
4 |
5 | Instructions -
6 | To run MyMangaDataBase, simply run `MyMangaDataBase.cmd`(Windows) or `MyMangaDataBase.sh`(Linux) depending on your OS.
7 |
8 | Bugs -
9 | If you found bugs or gliches in the application, you can report it by creating an issue on github.
10 | Follow this link to report bugs - https://github.com/EdwinRodger/MyMangaDataBase/issues/new?assignees=&labels=Bug&template=bug_report.yml
11 |
12 | Feature Request -
13 | If you like to see a new feature in the application, you can ask for it by going to the link - https://github.com/EdwinRodger/MyMangaDataBase/issues/new?assignees=&labels=Feature+request&template=feature_request.yml
--------------------------------------------------------------------------------
/docs/videos/host-on-pythonanywhere.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/docs/videos/host-on-pythonanywhere.mp4
--------------------------------------------------------------------------------
/pythonanywhere.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 |
4 | from src import create_app, db
5 | from src.anime.backup import delete_anime_export
6 | from src.manga.backup import delete_manga_export
7 | from src.settings.routes import create_json_files
8 |
9 | app = create_app()
10 |
11 | def checks():
12 | with app.app_context():
13 | db.create_all()
14 | create_json_files()
15 | delete_anime_export()
16 | delete_manga_export()
17 |
18 | if sys.argv[-1].lower() == "checks":
19 | checks()
20 | print("Checks done!")
21 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -i https://pypi.org/simple
2 | beautifulsoup4==4.12.3; python_full_version >= '3.6.0'
3 | blinker==1.8.2; python_version >= '3.8'
4 | certifi==2024.6.2; python_version >= '3.6'
5 | charset-normalizer==3.3.2; python_full_version >= '3.7.0'
6 | click==8.1.7; python_version >= '3.7'
7 | colorama==0.4.6; platform_system == 'Windows'
8 | flask==3.0.3; python_version >= '3.8'
9 | flask-sqlalchemy==3.1.1; python_version >= '3.8'
10 | flask-wtf==1.2.1; python_version >= '3.8'
11 | greenlet==3.0.3; python_version < '3.13' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))
12 | idna==3.7; python_version >= '3.5'
13 | itsdangerous==2.2.0; python_version >= '3.8'
14 | jinja2==3.1.4; python_version >= '3.7'
15 | markupsafe==2.1.5; python_version >= '3.7'
16 | pillow==10.3.0; python_version >= '3.8'
17 | requests==2.32.3; python_version >= '3.8'
18 | soupsieve==2.5; python_version >= '3.8'
19 | sqlalchemy==2.0.31; python_version >= '3.7'
20 | typing-extensions==4.12.2; python_version >= '3.8'
21 | urllib3==2.2.2; python_version >= '3.8'
22 | waitress==3.0.0; python_full_version >= '3.8.0'
23 | werkzeug==3.0.3; python_version >= '3.8'
24 | wtforms==3.1.2; python_version >= '3.8'
25 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import random
3 |
4 | from flask import Flask
5 | from flask_sqlalchemy import SQLAlchemy
6 |
7 | from src.config import Config
8 |
9 | db = SQLAlchemy()
10 |
11 |
12 | def create_app(config_class=Config):
13 | app = Flask(__name__)
14 | app.config.from_object(config_class)
15 |
16 | db.init_app(app)
17 |
18 | from src.anime.routes import anime
19 | from src.errors.handlers import errors
20 | from src.home.routes import home
21 | from src.manga.routes import manga
22 | from src.settings.routes import settings
23 | # from src.api.anime.routes import anime_API
24 | # from src.api.manga.routes import manga_API
25 |
26 | app.register_blueprint(home)
27 | app.register_blueprint(manga)
28 | app.register_blueprint(anime)
29 | app.register_blueprint(settings)
30 | app.register_blueprint(errors)
31 | # app.register_blueprint(anime_API)
32 | # app.register_blueprint(manga_API)
33 |
34 | from src.anime.forms import AnimeSearchBar
35 | from src.manga.forms import MangaSearchBar
36 |
37 | # Pass Stuff To Layout.html
38 | @app.context_processor
39 | def base():
40 | with open("json/settings.json", "r") as f:
41 | json_settings = json.load(f)
42 | theme = json_settings["theme"]
43 |
44 | from src.models import Anime, Manga
45 |
46 | # Below logic finds all the anime/manga from database, take random title,
47 | # get its title and then send it to search bar as a place holder
48 | manga = Manga.query.order_by(Manga.title.name).all()
49 | anime = Anime.query.order_by(Anime.title.name).all()
50 |
51 | mangacount = len(manga)
52 | animecount = len(anime)
53 |
54 | try:
55 | manga_index = random.randint(0, (mangacount - 1))
56 | except ValueError:
57 | manga_index = 0
58 |
59 | try:
60 | anime_index = random.randint(0, (animecount - 1))
61 | except ValueError:
62 | anime_index = 0
63 |
64 | if manga_index != 0:
65 | manga = manga[manga_index]
66 | manga_title = manga.title
67 | else:
68 | manga_title = "Search"
69 |
70 | if anime_index != 0:
71 | anime = anime[anime_index]
72 | anime_title = anime.title
73 | else:
74 | anime_title = "Search"
75 |
76 | manga_form = MangaSearchBar()
77 | anime_form = AnimeSearchBar()
78 |
79 | return {
80 | "theme": theme,
81 | "manga_navsearch": manga_form,
82 | "manga_title": manga_title,
83 | "anime_navsearch": anime_form,
84 | "anime_title": anime_title,
85 | }
86 |
87 | return app
88 |
--------------------------------------------------------------------------------
/src/anime/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/anime/__init__.py
--------------------------------------------------------------------------------
/src/anime/backup.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import secrets
4 | import time
5 | import xml.etree.ElementTree as ET
6 | from datetime import datetime
7 | from zipfile import ZipFile
8 |
9 | import requests
10 |
11 | from src import db
12 | from src.anime import utils as anime_utils
13 | from src.models import Anime
14 |
15 | today_date = datetime.date(datetime.today())
16 |
17 |
18 | def delete_anime_export():
19 | for backup in os.listdir("."):
20 | # Removing MMDB backup
21 | if backup.startswith("MMDB-Anime-Export"):
22 | os.remove(f"{backup}")
23 | if os.path.exists("anime.json"):
24 | os.remove("anime.json")
25 | if os.path.exists("backup-chapter-log.json"):
26 | os.remove("backup-chapter-log.json")
27 | # Removing MAL backup
28 | if backup.endswith(".xml"):
29 | os.remove(f"{backup}")
30 | # Removing MU backup
31 | if (
32 | backup.startswith("read_")
33 | or backup.startswith("wish_")
34 | or backup.startswith("complete_")
35 | or backup.startswith("unfinished_")
36 | or backup.startswith("hold_")
37 | ):
38 | os.remove(f"{backup}")
39 | # Removing AniList backup
40 | if backup.startswith("gdpr_data") and backup.endswith(".json"):
41 | os.remove(f"{backup}")
42 | for backup in os.listdir("src//"):
43 | # Removing MMDB backup
44 | if backup.startswith("MMDB-Anime-Export"):
45 | os.remove(f"src//{backup}")
46 |
47 |
48 | def export_mmdb_backup():
49 | path = f"src/MMDB-Anime-Export-{today_date}.zip"
50 | anime_list = Anime.query.all()
51 | obj = {}
52 | i = 0
53 | with open("anime.json", "w", encoding="UTF-8") as backup_json_file:
54 | for anime in anime_list:
55 | obj[i] = {
56 | "title": f"{anime.title}",
57 | "start_date": f"{anime.start_date}",
58 | "end_date": f"{anime.end_date}",
59 | "episode": f"{anime.episode}",
60 | "score": f"{anime.score}",
61 | "status": f"{anime.status}",
62 | "cover": f"{anime.cover}",
63 | "description": f"{anime.description}",
64 | "genre": f"{anime.genre}",
65 | "tags": f"{anime.tags}",
66 | "notes": f"{anime.notes}",
67 | }
68 | i += 1
69 | json.dump(obj, backup_json_file, indent=4)
70 | with ZipFile(path, "w") as zipfile:
71 | for root, _, files in os.walk("src/static/anime_cover/"):
72 | for file in files:
73 | zipfile.write(os.path.join(root, file))
74 | zipfile.write("anime.json")
75 | zipfile.write("json/animelogs.json")
76 |
77 |
78 | def import_mmdb_backup(filename):
79 | ZipFile(filename).extractall()
80 | with open("anime.json", "r", encoding="UTF-8") as file:
81 | data = json.load(file)
82 | for _, value in data.items():
83 | # This condition is because in json None is sent as string which,
84 | # here, goes to DB as "None" and shows respective fields with none
85 | if value["tags"] == "None":
86 | tags = None
87 | else:
88 | tags = value["tags"]
89 | if value["genre"] == "None":
90 | genre = None
91 | else:
92 | genre = value["genre"]
93 | if value["notes"] == "None":
94 | notes = None
95 | else:
96 | notes = value["notes"]
97 | anime = Anime(
98 | title=value["title"],
99 | start_date=value["start_date"],
100 | end_date=value["end_date"],
101 | episode=value["episode"],
102 | status=value["status"],
103 | score=value["score"],
104 | cover=value["cover"],
105 | description=value["description"],
106 | genre=genre,
107 | tags=tags,
108 | notes=notes,
109 | )
110 | db.session.add(anime)
111 | db.session.commit()
112 | delete_anime_export()
113 |
114 |
115 | def import_MyAnimeList_anime(filename):
116 | tree = ET.parse(filename)
117 | root = tree.getroot()
118 | total_anime = len(root.findall("anime"))
119 | current_anime = 0
120 |
121 | for anime in root.findall("anime"):
122 | series_title = anime.find("series_title").text
123 | my_watched_episodes = anime.find("my_watched_episodes").text
124 | my_score = anime.find("my_score").text
125 |
126 | my_status = anime.find("my_status").text
127 | if my_status.lower() == "plan to watch":
128 | my_status = "Plan to watch"
129 | if my_status.lower() == "on-hold":
130 | my_status = "On hold"
131 |
132 | my_start_date = anime.find("my_start_date").text
133 | if my_start_date == "0000-00-00":
134 | my_start_date = "0001-01-01"
135 |
136 | my_finish_date = anime.find("my_finish_date").text
137 | if my_finish_date == "0000-00-00":
138 | my_finish_date = "0001-01-01"
139 |
140 | series_animedb_id = anime.find("series_animedb_id").text
141 | response = requests.get(
142 | f"https://api.jikan.moe/v4/anime/{series_animedb_id}/full"
143 | )
144 |
145 | if response.status_code == 200:
146 | data = response.json()
147 |
148 | genre = []
149 | for i in data["data"]["genres"]:
150 | genre.append(i["name"])
151 | genre = ", ".join(genre)
152 |
153 | url = data["data"]["images"]["jpg"]["large_image_url"]
154 | picture = requests.get(url).content
155 | random_hex_name = secrets.token_hex(8)
156 | with open(f"src/static/anime_cover/{random_hex_name}.jpg", "wb") as f:
157 | f.write(picture)
158 |
159 | anime = Anime(
160 | title=series_title,
161 | start_date=my_start_date,
162 | end_date=my_finish_date,
163 | episode=my_watched_episodes,
164 | status=my_status,
165 | score=int(my_score),
166 | cover=f"{random_hex_name}.jpg",
167 | description=data["data"]["synopsis"],
168 | genre=genre,
169 | )
170 | db.session.add(anime)
171 | else:
172 | print(
173 | f"There is an error while getting {series_title}... Skipping this Title!"
174 | )
175 | total_anime -= 1
176 |
177 | current_anime += 1
178 | print(f"Progress: {current_anime}/{total_anime}")
179 |
180 | # Safety measure to not cross Jikan's Rate limit: https://docs.api.jikan.moe/#section/Information/Rate-Limiting
181 | time.sleep(1)
182 |
183 | db.session.commit()
184 | delete_anime_export()
185 |
186 |
187 | def anilist_API(series_id):
188 | query = """
189 | query ($id: Int) { # Define which variables will be used in the query (id)
190 | Media (id: $id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
191 | title {
192 | english
193 | romaji
194 | }
195 | coverImage{
196 | extraLarge
197 | }
198 | description
199 | genres
200 | staff{
201 | edges{
202 | node{
203 | name{
204 | full
205 | }
206 | }
207 | role
208 | }
209 | }
210 | }
211 | }
212 | """
213 |
214 | url = "https://graphql.anilist.co"
215 |
216 | variables = {"id": series_id}
217 |
218 | # Make the HTTP Api request
219 | response = requests.post(url, json={"query": query, "variables": variables})
220 | response_data = response.json()["data"]["Media"]
221 |
222 | if response.status_code == 200:
223 | title = response_data["title"]["english"]
224 | if title == None:
225 | title = response_data["title"]["romaji"]
226 |
227 | cover_url = response_data["coverImage"]["extraLarge"]
228 | cover_image_name = anime_utils.online_image_downloader(cover_url, title)
229 |
230 | description = response_data["description"]
231 | genre = ", ".join(response_data["genres"])
232 |
233 | authors = []
234 | for author in response_data["staff"]["edges"]:
235 | if "Story" in author["role"]:
236 | name = author["node"]["name"]["full"]
237 | authors.append(name)
238 | authors = ", ".join(authors)
239 |
240 | artists = []
241 | for artist in response_data["staff"]["edges"]:
242 | if ("Art") in artist["role"]:
243 | name = artist["node"]["name"]["full"]
244 | artists.append(name)
245 | artists = ", ".join(artists)
246 |
247 | print(f"Metadata fetched successfully: [{series_id}] {title}")
248 | return (
249 | str(title),
250 | str(cover_image_name),
251 | str(description),
252 | str(genre),
253 | str(authors),
254 | str(artists),
255 | )
256 | else:
257 | print("Query failed: ", series_id)
258 | return None
259 |
260 |
261 | def import_anilist_anime(filename):
262 | with open("gdpr_data.json", "r") as f:
263 | data = json.load(f)
264 | for series_type in data["lists"]:
265 | if series_type["series_type"] == 0:
266 | if series_type["started_on"] != 0:
267 | start_date = str(series_type["started_on"])
268 | start_date = datetime.strptime(start_date, "%Y%m%d")
269 | start_date = start_date.strftime("%Y-%m-%d")
270 | else:
271 | start_date = "0001-01-01"
272 | if series_type["finished_on"] != 0:
273 | end_date = str(series_type["finished_on"])
274 | end_date = datetime.strptime(end_date, "%Y%m%d")
275 | end_date = end_date.strftime("%Y-%m-%d")
276 | else:
277 | end_date = "0001-01-01"
278 | if series_type["status"] == 0:
279 | status = "Watching"
280 | elif series_type["status"] == 1:
281 | status = "Plan to watch"
282 | elif series_type["status"] == 2:
283 | status = "Completed"
284 | elif series_type["status"] == 3:
285 | status = "Dropped"
286 | elif series_type["status"] == 4:
287 | status = "On hold"
288 |
289 | try:
290 | metadata = anilist_API(series_type["series_id"])
291 | except Exception as e:
292 | print(f"Error in query: {e},\n\tseries_id: {series_type['series_id']}")
293 |
294 | if metadata is not None:
295 | anime = Anime(
296 | cover=metadata[1],
297 | title=metadata[0],
298 | episode=series_type["progress"],
299 | start_date=start_date,
300 | end_date=end_date,
301 | score=round(series_type["score"] / 10),
302 | status=status,
303 | description=metadata[2],
304 | genre=metadata[3],
305 | )
306 | else:
307 | anime = Anime(
308 | title=series_type["series_id"],
309 | episode=series_type["progress"],
310 | start_date=start_date,
311 | end_date=end_date,
312 | score=round(series_type["score"] / 10),
313 | status=status,
314 | )
315 | # Commiting entries to database
316 | db.session.add(anime)
317 | db.session.commit()
318 | time.sleep(1)
319 |
--------------------------------------------------------------------------------
/src/anime/forms.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from flask_wtf import FlaskForm
4 | from flask_wtf.file import FileAllowed, FileField
5 | from wtforms import (
6 | DateField,
7 | IntegerField,
8 | SelectField,
9 | StringField,
10 | SubmitField,
11 | TextAreaField,
12 | )
13 | from wtforms.validators import DataRequired, Length, Optional
14 |
15 |
16 | class AnimeForm(FlaskForm):
17 | title = StringField("Title", validators=[DataRequired(), Length(min=2)])
18 | cover = FileField("Cover Image", validators=[FileAllowed(["jpg", "png"])])
19 | if platform.system() == "Windows":
20 | start_date = DateField("Start Date")
21 | end_date = DateField("End Date")
22 | else:
23 | # DateField will allow None which is a Linux specific problem, solved using - https://github.com/wtforms/wtforms/issues/489#issuecomment-1382074627
24 | start_date = DateField("Start Date", validators=[Optional()])
25 | end_date = DateField("End Date", validators=[Optional()])
26 | episode = IntegerField("Episode")
27 | score = SelectField("Score", choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
28 | status = SelectField(
29 | "Status",
30 | choices=["Watching", "Completed", "On hold", "Dropped", "Plan to watch"],
31 | )
32 | description = TextAreaField("Description")
33 | genre = StringField("Genre")
34 | tags = StringField("Tags")
35 | notes = TextAreaField("Notes")
36 | submit = SubmitField("Add")
37 | update = SubmitField("Update")
38 |
39 |
40 | class AnimeSearchBar(FlaskForm):
41 | search_field = StringField("Search", validators=[DataRequired(), Length(min=2)])
42 | search_button = SubmitField("Search")
43 |
--------------------------------------------------------------------------------
/src/anime/routes.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 | import platform
4 |
5 | from flask import (
6 | Blueprint,
7 | flash,
8 | redirect,
9 | render_template,
10 | request,
11 | send_file,
12 | url_for,
13 | )
14 | from sqlalchemy import delete
15 |
16 | from src import db
17 | from src.anime.backup import (
18 | export_mmdb_backup,
19 | import_anilist_anime,
20 | import_mmdb_backup,
21 | import_MyAnimeList_anime,
22 | )
23 | from src.anime.forms import AnimeForm, AnimeSearchBar
24 | from src.anime.utils import (
25 | AnimeHistory,
26 | get_layout,
27 | get_settings,
28 | remove_cover,
29 | save_picture,
30 | )
31 | from src.home.utils import mmdb_promotion
32 | from src.models import Anime
33 |
34 | today_date = datetime.date(datetime.today())
35 |
36 | anime = Blueprint("anime", __name__, url_prefix="/anime")
37 |
38 |
39 | @anime.route("/list/all")
40 | def anime_list():
41 | anime_list = Anime.query.order_by(Anime.title.name).all()
42 | settings = get_settings()
43 | layout = get_layout()
44 | return render_template(
45 | f"anime/{layout}",
46 | title="Anime List",
47 | current_section="Anime",
48 | anime_list=anime_list,
49 | sort_function="All",
50 | settings=settings,
51 | )
52 |
53 |
54 | # Add New Anime
55 | @anime.route("/new", methods=["GET", "POST"])
56 | def new_anime():
57 | form = AnimeForm()
58 | if form.validate_on_submit():
59 | # In linux if DateField is None(0001-01-01) it will be set to 0001-01-01
60 | if form.start_date.data == None:
61 | form.start_date.data = "0001-01-01"
62 | if form.end_date.data == None:
63 | form.end_date.data = "0001-01-01"
64 | if form.cover.data:
65 | picture_file = save_picture(form.cover.data)
66 | else:
67 | picture_file = "default-anime.svg"
68 | anime = Anime(
69 | title=form.title.data,
70 | cover=picture_file,
71 | start_date=form.start_date.data,
72 | end_date=form.end_date.data,
73 | episode=form.episode.data,
74 | status=form.status.data,
75 | score=form.score.data,
76 | description=form.description.data,
77 | genre=form.genre.data,
78 | tags=form.tags.data,
79 | notes=form.notes.data,
80 | )
81 | db.session.add(anime)
82 | db.session.commit()
83 | flash(f"{form.title.data} is added!", "success")
84 | return redirect(url_for("anime.anime_list"))
85 | return render_template(
86 | "anime/create-anime.html",
87 | title="New Anime",
88 | form=form,
89 | legend="New Anime",
90 | current_section="Anime",
91 | )
92 |
93 |
94 | # Update a Anime
95 | @anime.route("/edit/", methods=["GET", "POST"])
96 | def edit_anime(anime_id):
97 | anime = Anime.query.get_or_404(anime_id)
98 | form = AnimeForm()
99 | anime_history = AnimeHistory()
100 | old_name = anime.title
101 | history = anime_history.get_history(anime.title)
102 | if form.validate_on_submit():
103 | # In linux if DateField is None(0001-01-01) it will be set to 0001-01-01
104 | if form.start_date.data == None:
105 | form.start_date.data = "0001-01-01"
106 | if form.end_date.data == None:
107 | form.end_date.data = "0001-01-01"
108 | if form.cover.data:
109 | remove_cover(anime.cover)
110 | picture_file = save_picture(form.cover.data)
111 | anime.cover = picture_file
112 | anime.title = form.title.data
113 | anime.start_date = form.start_date.data
114 | anime.end_date = form.end_date.data
115 | anime.episode = form.episode.data
116 | anime.status = form.status.data
117 | anime.score = form.score.data
118 | anime.description = form.description.data
119 | anime.genre = form.genre.data
120 | anime.tags = form.tags.data
121 | anime.notes = form.notes.data
122 | db.session.commit()
123 | anime_history.check_rename(old_name=old_name, new_name=form.title.data)
124 | anime_history.add_episode(anime.title, form.episode.data)
125 | flash("Your anime has been updated!", "success")
126 | elif request.method == "GET":
127 | form.title.data = anime.title
128 | form.start_date.data = datetime.strptime(anime.start_date, "%Y-%m-%d").date()
129 | form.end_date.data = datetime.strptime(anime.end_date, "%Y-%m-%d").date()
130 | form.episode.data = anime.episode
131 | form.status.data = anime.status
132 | form.score.data = str(anime.score)
133 | form.description.data = anime.description
134 | form.genre.data = anime.genre
135 | form.tags.data = anime.tags
136 | form.notes.data = anime.notes
137 | return render_template(
138 | "anime/edit-anime.html",
139 | title=f"Edit {anime.title}",
140 | form=form,
141 | anime=anime,
142 | legend="Update Anime",
143 | current_section="Anime",
144 | history=history,
145 | platform=platform
146 | )
147 |
148 |
149 | # Delete A Anime
150 | @anime.route("/delete/", methods=["POST"])
151 | def delete_anime(anime_id):
152 | anime = Anime.query.get_or_404(anime_id)
153 | anime_history = AnimeHistory()
154 | remove_cover(anime.cover)
155 | db.session.delete(anime)
156 | db.session.commit()
157 | anime_history.clear_history(anime.title)
158 | flash("Your anime has been Obliterated!", "success")
159 | return redirect(url_for("anime.anime_list"))
160 |
161 |
162 | # Sort Anime
163 | @anime.route("/list/", methods=["GET", "POST"])
164 | def sort_anime(sort_function):
165 | anime_list = (
166 | Anime.query.filter_by(status=sort_function).order_by(Anime.title.name).all()
167 | )
168 | settings = get_settings()
169 | layout = get_layout()
170 | return render_template(
171 | f"anime/{layout}",
172 | title=f"{sort_function} Anime",
173 | anime_list=anime_list,
174 | sort_function=sort_function,
175 | current_section="Anime",
176 | settings=settings,
177 | )
178 |
179 |
180 | # Add One episode To The Anime
181 | @anime.route("/add-one-episode/")
182 | def add_one_episode(anime_id):
183 | anime = Anime.query.get_or_404(anime_id)
184 | anime_history = AnimeHistory()
185 | anime.episode = anime.episode + 1
186 | db.session.commit()
187 | anime_history.add_episode(anime.title, anime.episode)
188 | return redirect(url_for("anime.anime_list"))
189 |
190 |
191 | # Searches anime related to given genres in the database
192 | @anime.route("/genre/", methods=["GET"])
193 | def search_genre(genre):
194 | anime_list = Anime.query.filter(Anime.genre.like(f"%{genre}%")).all()
195 | settings = get_settings()
196 | layout = get_layout()
197 | return render_template(
198 | f"anime/{layout}",
199 | title=f"{genre} Genre",
200 | anime_list=anime_list,
201 | current_section="Anime",
202 | settings=settings,
203 | )
204 |
205 |
206 | # Searches anime related to given tags in the database
207 | @anime.route("/tags/", methods=["GET"])
208 | def search_tags(tag):
209 | anime_list = Anime.query.filter(Anime.tags.like(f"%{tag}%")).all()
210 | settings = get_settings()
211 | layout = get_layout()
212 | return render_template(
213 | f"anime/{layout}",
214 | title=f"{tag} Tag",
215 | anime_list=anime_list,
216 | current_section="Anime",
217 | settings=settings,
218 | )
219 |
220 |
221 | # The path for uploading the file
222 | @anime.route("/import", methods=["GET", "POST"])
223 | def import_anime():
224 | return render_template(
225 | "anime/import-anime.html", current_section="Anime", title="Import Anime"
226 | )
227 |
228 |
229 | # Imports backup based on file extension
230 | @anime.route("/import/", methods=["GET", "POST"])
231 | def importbackup(backup):
232 | # check if the method is post
233 | if request.method == "POST":
234 | # get the file from the files object
235 | backup_file = request.files["file"]
236 | # Checking if no file is sent
237 | if backup_file.filename == "":
238 | flash("Choose a file to import!", "danger")
239 | return redirect(url_for("anime.import_anime"))
240 | # If file is sent through MMDB form, checking if file name is correct. If correct, then extracting the import
241 | if (
242 | backup == "MyMangaDataBase"
243 | and backup_file.filename.lower().endswith((".zip"))
244 | and backup_file.filename.startswith(("MMDB-Anime-Export"))
245 | ):
246 | # this will secure the file
247 | backup_file.save(backup_file.filename)
248 | import_mmdb_backup(backup_file.filename)
249 | elif (
250 | backup == "MyAnimeList"
251 | and backup_file.filename.lower().endswith((".xml"))
252 | and backup_file.filename.lower().startswith(("animelist"))
253 | ):
254 | # this will secure the file
255 | backup_file.save(backup_file.filename)
256 | import_MyAnimeList_anime(backup_file.filename)
257 | elif backup == "AniList" and backup_file.filename.lower() == "gdpr_data.json":
258 | backup_file.save(backup_file.filename)
259 | import_anilist_anime(backup_file.filename)
260 | else:
261 | flash("Choose correct file to import!", "danger")
262 | return redirect(url_for("anime.import_anime"))
263 | return redirect(
264 | url_for("anime.anime_list")
265 | ) # Display thsi message after uploading
266 | return redirect(
267 | url_for("anime.import_anime")
268 | ) # Display thsi message after uploading
269 |
270 |
271 | # Downloads MMDB json export file
272 | @anime.route("/export")
273 | def export():
274 | export_mmdb_backup()
275 | return send_file(f"MMDB-Anime-Export-{today_date}.zip")
276 |
277 |
278 | # Delete Database
279 | @anime.route("/delete/database")
280 | def delete_database():
281 | delete_db = delete(Anime).where(Anime.id >= 0)
282 | anime_history = AnimeHistory()
283 | db.session.execute(delete_db)
284 | db.session.commit()
285 | anime_history.clear_all_history()
286 | for root, _, files in os.walk("src/static/anime_cover/"):
287 | for file in files:
288 | # This if block will prevent deletion of default cover image files
289 | if file not in ("default-manga.svg", "default-anime.svg"):
290 | os.remove(os.path.join(root, file))
291 | return redirect(url_for("anime.anime_list"))
292 |
293 |
294 | # Searches anime in the database
295 | @anime.route("/search", methods=["POST", "GET"])
296 | def search_anime():
297 | form = AnimeSearchBar()
298 | settings = get_settings()
299 | layout = get_layout()
300 | if form.validate_on_submit():
301 | anime_list = Anime.query.filter(
302 | Anime.title.like(f"%{form.search_field.data}%")
303 | ).all()
304 | return render_template(
305 | f"anime/{layout}",
306 | title=f"{form.search_field.data} Anime",
307 | animes=anime,
308 | anime_list=anime_list,
309 | current_section="Anime",
310 | settings=settings,
311 | )
312 | anime_list = Anime.query.order_by(Anime.title.name).all()
313 | return render_template(
314 | f"anime/{layout}",
315 | title="Anime List",
316 | current_section="Anime",
317 | anime_list=anime_list,
318 | sort_function="All",
319 | settings=settings,
320 | )
321 |
322 |
323 | @anime.before_request
324 | def before_request():
325 | endpoint = request.endpoint
326 | if endpoint == "anime.anime_list":
327 | mmdb_promotion(anime_list)()
328 |
--------------------------------------------------------------------------------
/src/anime/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import secrets
4 | import shutil
5 | from datetime import datetime
6 |
7 | import requests
8 | from flask import current_app
9 | from PIL import Image
10 |
11 |
12 | def save_picture(form_picture):
13 | random_hex = secrets.token_hex(8)
14 | _, f_ext = os.path.splitext(form_picture.filename)
15 | picture_fn = random_hex + f_ext
16 | picture_path = os.path.join(
17 | current_app.root_path, "static/anime_cover/", picture_fn
18 | )
19 |
20 | i = Image.open(form_picture)
21 | i.save(picture_path)
22 |
23 | return picture_fn
24 |
25 |
26 | def remove_cover(anime_cover):
27 | if not anime_cover == "default-anime.svg":
28 | os.remove(f"src/static/anime_cover/{anime_cover}")
29 |
30 |
31 | class AnimeHistory:
32 | def __init__(self):
33 | with open("json/animelogs.json", "r") as f:
34 | self.history = json.load(f)
35 |
36 | def add_episode(self, anime_name, new_episode_number):
37 | current_date = datetime.now().strftime("%Y-%m-%d")
38 | current_time = datetime.now().strftime("%H:%M:%S")
39 | if anime_name in self.history:
40 | # If user edits anime without updating episode then it creates duplicates of that episode number in logs
41 | # This if else blocks above situation
42 | old_episode_number = self.history[anime_name][-1][
43 | 0
44 | ] # self.history[anime_name][recent entry/episode of anime_name][episode_number]
45 | if old_episode_number != new_episode_number:
46 | self.history[anime_name].append(
47 | (new_episode_number, current_date, current_time)
48 | )
49 | else:
50 | self.history[anime_name] = [
51 | (new_episode_number, current_date, current_time)
52 | ]
53 | self.commit()
54 |
55 | def get_history(self, anime_name):
56 | if anime_name in self.history:
57 | return self.history[anime_name]
58 | else:
59 | return []
60 |
61 | # To change key in dictionary: https://stackoverflow.com/a/16475408
62 | def check_rename(self, old_name, new_name):
63 | if old_name != new_name:
64 | try:
65 | self.history[new_name] = self.history[old_name]
66 | del self.history[old_name]
67 | self.commit()
68 | except:
69 | return
70 |
71 | def clear_history(self, anime_name):
72 | if anime_name in self.history:
73 | del self.history[anime_name]
74 | self.commit()
75 |
76 | def clear_all_history(self):
77 | self.history = {}
78 | self.commit()
79 |
80 | def commit(self):
81 | with open("json/animelogs.json", "w") as f:
82 | json.dump(self.history, f, indent=4)
83 |
84 |
85 | def get_settings():
86 | with open("json/settings.json", "r") as f:
87 | settings = json.load(f)
88 | return settings
89 |
90 |
91 | def get_layout():
92 | with open("json/settings.json", "r") as f:
93 | settings = json.load(f)
94 | layout = settings["layout"]
95 | if layout == "Card" or layout == "Image Overlay Card":
96 | return "anime-cards.html"
97 | else:
98 | return "anime-list.html"
99 |
100 |
101 | def online_image_downloader(img_url, title):
102 | file_name = os.urandom(8).hex()
103 | res = requests.get(img_url, stream=True, timeout=30)
104 | if res.status_code == 200:
105 | with open(f"src/static/anime_cover/{file_name}.jpg", "wb") as image:
106 | shutil.copyfileobj(res.raw, image)
107 | return f"{file_name}.jpg"
108 | print("Image couldn't be retrieved: ", title)
109 | return "default-anime.svg"
110 |
--------------------------------------------------------------------------------
/src/api/anime/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/api/anime/__init__.py
--------------------------------------------------------------------------------
/src/api/anime/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, jsonify
2 |
3 | from src.models import Anime
4 | from src.anime.utils import AnimeHistory
5 |
6 | from src import db
7 |
8 | anime_API = Blueprint("anime_API", __name__, url_prefix="/api/anime")
9 |
10 | @anime_API.route("/add", methods=["POST"])
11 | def add_anime():
12 | data = request.get_json()
13 | try:
14 | anime = Anime(
15 | title=data["title"],
16 | episode=data["episode"],
17 | start_date=data["start_date"],
18 | end_date=data["end_date"],
19 | score=data["score"],
20 | status=data["status"],
21 | description=data["description"],
22 | genre=data["genre"],
23 | tags=data["tags"],
24 | notes=data["notes"],
25 | )
26 | db.session.add(anime)
27 | db.session.commit()
28 | return jsonify({"message": "Data received"}), 200
29 | except Exception as e:
30 | return (
31 | jsonify({"message": "There is some error in data", "error": str(e)}),
32 | 400,
33 | )
34 |
35 | @anime_API.route("/edit", methods=["PUT"])
36 | def edit_anime():
37 | data = request.get_json()
38 | try:
39 | anime = Anime.query.filter_by(title=data["title"]).first()
40 | anime_history = AnimeHistory()
41 | old_name = anime.title
42 | anime.title = data["title"]
43 | anime.episode = data["episode"]
44 | anime.start_date = data["start_date"]
45 | anime.end_date = data["end_date"]
46 | anime.score = data["score"]
47 | anime.status = data["status"]
48 | anime.description = data["description"]
49 | anime.genre = data["genre"]
50 | anime.tags = data["tags"]
51 | anime.notes = data["notes"]
52 | db.session.commit()
53 | anime_history.check_rename(old_name=old_name, new_name=data["title"])
54 | anime_history.add_episode(anime.title, data["episode"])
55 | return jsonify({"message": "Data received"}), 200
56 | except Exception as e:
57 | return (
58 | jsonify({"message": "There is some error in data", "error": str(e)}),
59 | 400,
60 | )
61 |
--------------------------------------------------------------------------------
/src/api/manga/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/api/manga/__init__.py
--------------------------------------------------------------------------------
/src/api/manga/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, jsonify
2 |
3 | from src.models import Manga
4 | from src.manga.utils import MangaHistory
5 |
6 | from src import db
7 |
8 | manga_API = Blueprint("manga_API", __name__, url_prefix="/api/manga")
9 |
10 | @manga_API.route("/add", methods=["POST"])
11 | def add_manga():
12 | data = request.get_json()
13 | try:
14 | manga = Manga(
15 | title=data["title"],
16 | start_date=data["start_date"],
17 | end_date=data["end_date"],
18 | volume=data["volume"],
19 | chapter=data["chapter"],
20 | status=data["status"],
21 | score=data["score"],
22 | description=data["description"],
23 | genre=data["genre"],
24 | tags=data["tags"],
25 | author=data["author"],
26 | artist=data["artist"],
27 | notes=data["notes"],
28 | )
29 | db.session.add(manga)
30 | db.session.commit()
31 | return jsonify({"message": "Data received"}), 200
32 | except Exception as e:
33 | return (
34 | jsonify({"message": "There is some error in data", "error": str(e)}),
35 | 400,
36 | )
37 |
38 | @manga_API.route("/edit", methods=["PUT"])
39 | def edit_manga():
40 | data = request.get_json()
41 | try:
42 | manga = Manga.query.filter_by(title=data["title"]).first()
43 | manga_history = MangaHistory()
44 | old_name = manga.title
45 | manga.title = data["title"]
46 | manga.start_date = data["start_date"]
47 | manga.end_date = data["end_date"]
48 | manga.volume = data["volume"]
49 | manga.chapter = data["chapter"]
50 | manga.status = data["status"]
51 | manga.score = data["score"]
52 | manga.description = data["description"]
53 | manga.genre = data["genre"]
54 | manga.tags = data["tags"]
55 | manga.author = data["author"]
56 | manga.artist = data["artist"]
57 | manga.notes = data["notes"]
58 | db.session.commit()
59 | manga_history.check_rename(old_name=old_name, new_name=data["title"])
60 | manga_history.add_chapter(manga.title, data["chapter"])
61 | return jsonify({"message": "Data received"}), 200
62 | except Exception as e:
63 | return (
64 | jsonify({"message": "There is some error in data", "error": str(e)}),
65 | 400,
66 | )
67 |
--------------------------------------------------------------------------------
/src/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class Config:
5 | SQLALCHEMY_DATABASE_URI = "sqlite:///database.db"
6 | SECRET_KEY = os.urandom(16).hex()
7 |
--------------------------------------------------------------------------------
/src/errors/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/errors/__init__.py
--------------------------------------------------------------------------------
/src/errors/handlers.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | import requests
4 | from flask import Blueprint, render_template
5 |
6 | errors = Blueprint("errors", __name__)
7 |
8 |
9 | # List of reactions for 404 error
10 | reactions_404 = ["facepalm", "mad", "smack"]
11 | # List of reactions for 405 error
12 | reactions_405 = ["no", "bleh"]
13 | # List of reactions for 500 error
14 | reactions_500 = ["nervous", "sorry", "surprised", "sweat", "confused"]
15 | # Base URL for the API
16 | api_base_url = "https://api.otakugifs.xyz/gif"
17 |
18 |
19 | # Function to retrieve the GIF URL for a given reaction
20 | def get_gif_url(reaction):
21 | url = f"{api_base_url}?reaction={reaction}&format=WebP"
22 | try:
23 | response = requests.get(url)
24 | except:
25 | return None
26 | if response.status_code == 200:
27 | data = response.json()
28 | gif_url = data["url"]
29 | return gif_url
30 | return None
31 |
32 |
33 | # Function to get the GIF URL based on the error code and a list of reactions
34 | def gif_url(code):
35 | if code == 404:
36 | reactions = reactions_404
37 | elif code == 405:
38 | reactions = reactions_405
39 | else:
40 | reactions = reactions_500
41 | # Randomly select a reaction from the given list
42 | reaction = random.choice(reactions)
43 | # Get the GIF URL for the selected reaction
44 | gif_url = get_gif_url(reaction)
45 | if gif_url is not None:
46 | return gif_url
47 | return None
48 |
49 |
50 | @errors.app_errorhandler(404)
51 | def error_404(error):
52 | url = gif_url(404)
53 | return render_template("errors/404.html", url=url), 404
54 |
55 |
56 | @errors.app_errorhandler(405)
57 | def error_405(error):
58 | url = gif_url(405)
59 | return render_template("errors/405.html", url=url), 405
60 |
61 |
62 | @errors.app_errorhandler(500)
63 | def error_500(error):
64 | url = gif_url(500)
65 | return render_template("errors/500.html", url=url), 500
66 |
67 |
68 | @errors.route("/errors/")
69 | def err(error):
70 | if error == 404:
71 | url = gif_url(404)
72 | return render_template("errors/404.html", url=url)
73 | if error == 405:
74 | url = gif_url(405)
75 | return render_template("errors/405.html", url=url)
76 | if error == 500:
77 | url = gif_url(500)
78 | return render_template("errors/500.html", url=url)
79 |
--------------------------------------------------------------------------------
/src/home/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/home/__init__.py
--------------------------------------------------------------------------------
/src/home/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, request
2 |
3 | from src.home.utils import (
4 | anime_overview_data,
5 | check_for_update,
6 | manga_overview_data,
7 | mmdb_promotion,
8 | )
9 |
10 | home = Blueprint("home", __name__)
11 |
12 |
13 | @home.route("/")
14 | @home.route("/home")
15 | def homepage():
16 | manga_data = manga_overview_data()
17 | anime_data = anime_overview_data()
18 | show_update_modal = check_for_update()
19 | return render_template(
20 | "home.html",
21 | title="Home",
22 | current_section="Home",
23 | manga_data=manga_data,
24 | anime_data=anime_data,
25 | show_update_modal=show_update_modal,
26 | )
27 |
28 |
29 | @home.route("/more")
30 | def more():
31 | return render_template("more.html", title="More", current_section="More")
32 |
33 |
34 | @home.route("/credits")
35 | def credits():
36 | return render_template("credits.html", title="Credits", current_section="More")
37 |
38 |
39 | @home.before_request
40 | def before_request():
41 | endpoint = request.endpoint
42 | action = {
43 | "home.more": more,
44 | "home.credits": credits,
45 | }.get(endpoint, more)
46 |
47 | mmdb_promotion(action)()
48 |
--------------------------------------------------------------------------------
/src/home/utils.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import os
4 | import random
5 | import statistics
6 | from collections import Counter
7 |
8 | import requests
9 | from flask import flash
10 |
11 | from src.models import Anime, Manga
12 |
13 |
14 | def manga_overview_data():
15 | # Fetch all manga records from the database and order them by title
16 | manga_list = Manga.query.order_by(Manga.title.name).all()
17 |
18 | # Total number of manga
19 | total_manga = len(manga_list)
20 |
21 | # Accumulate the total number of chapters
22 | total_chapters = sum(manga.chapter for manga in manga_list)
23 |
24 | # Initialize variables for total chapters, scores, status, and genre
25 | score = []
26 | status = []
27 | genre = []
28 |
29 | for manga in manga_list:
30 | # Collect the scores of each manga
31 | if manga.score != 0:
32 | score.append(manga.score)
33 |
34 | # Collect the status of each manga
35 | status.append(manga.status)
36 |
37 | # Collect the genre tags of each manga
38 | if manga.genre:
39 | genres = manga.genre.split(", ")
40 | genre.extend(i.strip() for i in genres if i.strip() not in ["N", "o"])
41 |
42 | # Count the occurrence of each score, status, and genre
43 | score_count = Counter(score)
44 | status_count = Counter(status)
45 | genre_count = Counter(genre)
46 |
47 | # Sort the genre count in descending order
48 | genre_count = dict(
49 | sorted(genre_count.items(), key=lambda item: item[1], reverse=True)
50 | )
51 |
52 | # Sort the score count in descending order
53 | score_count = dict(sorted(score_count.items(), reverse=True))
54 |
55 | try:
56 | # Calculate the mean score using the scores
57 | mean_score = statistics.mean(score)
58 | # Rounding mean score to 2 decimal places
59 | mean_score = round(mean_score, 2)
60 | except statistics.StatisticsError:
61 | # Handle the case where there are no scores
62 | mean_score = 0
63 |
64 | # Create a dictionary containing the overview data
65 | manga_overview_data = {
66 | "manga_len": total_manga,
67 | "manga_chapter_len": total_chapters,
68 | "score": score_count,
69 | "status": status_count,
70 | "genre": genre_count,
71 | "mean_score": mean_score,
72 | }
73 |
74 | return manga_overview_data
75 |
76 |
77 | def anime_overview_data():
78 | # Fetch all anime records from the database and order them by title
79 | anime_list = Anime.query.order_by(Anime.title.name).all()
80 |
81 | # Total number of anime
82 | total_anime = len(anime_list)
83 |
84 | # Accumulate the total number of episodes
85 | total_episodes = sum(anime.episode for anime in anime_list)
86 |
87 | # Initialize variables for total episodes, scores, status, and genre
88 | score = []
89 | status = []
90 | genre = []
91 |
92 | for anime in anime_list:
93 | # Collect the scores of each anime
94 | if anime.score != 0:
95 | score.append(anime.score)
96 |
97 | # Collect the status of each anime
98 | status.append(anime.status)
99 |
100 | # Collect the genre tags of each anime
101 | if anime.genre:
102 | genres = anime.genre.split(", ")
103 | genre.extend(i.strip() for i in genres if i.strip() not in ["N", "o"])
104 |
105 | # Count the occurrence of each score, status, and genre
106 | score_count = Counter(score)
107 | status_count = Counter(status)
108 | genre_count = Counter(genre)
109 |
110 | # Sort the genre count in descending order
111 | genre_count = dict(
112 | sorted(genre_count.items(), key=lambda item: item[1], reverse=True)
113 | )
114 |
115 | # Sort the score count in descending order
116 | score_count = dict(sorted(score_count.items(), reverse=True))
117 |
118 | try:
119 | # Calculate the mean score using the scores
120 | mean_score = statistics.mean(score)
121 | # Rounding mean score to 2 decimal places
122 | mean_score = round(mean_score, 2)
123 | except statistics.StatisticsError:
124 | # Handle the case where there are no scores
125 | mean_score = 0
126 |
127 | # Create a dictionary containing the overview data
128 | anime_overview_data = {
129 | "anime_len": total_anime,
130 | "anime_episode_len": total_episodes,
131 | "score": score_count,
132 | "status": status_count,
133 | "genre": genre_count,
134 | "mean_score": mean_score,
135 | }
136 |
137 | return anime_overview_data
138 |
139 |
140 | def check_for_update():
141 | # API URL to fetch the latest tags/releases
142 | url = "https://api.github.com/repos/EdwinRodger/MyMangaDataBase/tags"
143 |
144 | # Fetch the JSON response from the API
145 | try:
146 | response = requests.get(url).json()
147 | except:
148 | return False
149 |
150 | # File path to store the version hash
151 | version_hash_file = "json/versionhash.json"
152 |
153 | # Calculate the hash of the response
154 | current_hash = hashlib.sha224(str(response).encode("utf-8")).hexdigest()
155 |
156 | # Check if the version hash file exists
157 | if not os.path.exists(version_hash_file):
158 | # If the file doesn't exist, create it and store the current hash
159 | with open(version_hash_file, "w", encoding="utf-8") as file:
160 | json.dump({"current_hash": current_hash}, file, indent=4)
161 | else:
162 | # If the file exists, read the stored hash
163 | with open(version_hash_file, "r", encoding="utf-8") as file:
164 | data = json.load(file)
165 |
166 | # Check if the current hash matches the stored hash
167 | if current_hash == data["current_hash"]:
168 | # No update available
169 | return False
170 |
171 | # Update the hash in the file to reflect the latest version
172 | with open(version_hash_file, "w", encoding="utf-8") as file:
173 | json.dump({"current_hash": current_hash}, file, indent=4)
174 |
175 | # An update is available
176 | return True
177 |
178 |
179 | def mmdb_promotion(func):
180 | def wrapper(*args, **kwargs):
181 | with open("json/settings.json", "r") as f:
182 | json_settings = json.load(f)
183 |
184 | if json_settings["mmdb_promotion"] == "Yes":
185 | num = random.randint(1, 25)
186 | if num == 1:
187 | flash(
188 | 'Star MyMangaDataBase on GitHub!',
189 | "info",
190 | )
191 | if num == 2:
192 | flash(
193 | 'Like MyMangaDataBase on AlternativeTo!',
194 | "info",
195 | )
196 |
197 | return func(*args, **kwargs)
198 |
199 | return wrapper
200 |
--------------------------------------------------------------------------------
/src/manga/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/manga/__init__.py
--------------------------------------------------------------------------------
/src/manga/forms.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from flask_wtf import FlaskForm
4 | from flask_wtf.file import FileAllowed, FileField
5 | from wtforms import (
6 | DateField,
7 | IntegerField,
8 | SelectField,
9 | StringField,
10 | SubmitField,
11 | TextAreaField,
12 | )
13 | from wtforms.validators import DataRequired, Length, Optional
14 |
15 |
16 | class MangaForm(FlaskForm):
17 | title = StringField("Title", validators=[DataRequired(), Length(min=2)])
18 | cover = FileField("Cover Image", validators=[FileAllowed(["jpg", "png"])])
19 | if platform.system() == "Windows":
20 | start_date = DateField("Start Date")
21 | end_date = DateField("End Date")
22 | else:
23 | # DateField will allow None which is a Linux specific problem, solved using - https://github.com/wtforms/wtforms/issues/489#issuecomment-1382074627
24 | start_date = DateField("Start Date", validators=[Optional()])
25 | end_date = DateField("End Date", validators=[Optional()])
26 | volume = IntegerField("Volume")
27 | chapter = IntegerField("Chapter")
28 | score = SelectField("Score", choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
29 | status = SelectField(
30 | "Status",
31 | choices=["Reading", "Completed", "On hold", "Dropped", "Plan to read"],
32 | )
33 | description = TextAreaField("Description")
34 | genre = StringField("Genre")
35 | tags = StringField("Tags")
36 | author = StringField("Author")
37 | artist = StringField("Artist")
38 | notes = TextAreaField("Notes")
39 | submit = SubmitField("Add")
40 | update = SubmitField("Update")
41 |
42 |
43 | class MangaSearchBar(FlaskForm):
44 | search_field = StringField("Search", validators=[DataRequired(), Length(min=2)])
45 | search_button = SubmitField("Search")
46 |
--------------------------------------------------------------------------------
/src/manga/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import secrets
4 | import shutil
5 | from datetime import datetime
6 |
7 | import requests
8 | from flask import current_app
9 | from PIL import Image
10 |
11 |
12 | def save_picture(form_picture):
13 | random_hex = secrets.token_hex(8)
14 | _, f_ext = os.path.splitext(form_picture.filename)
15 | picture_fn = random_hex + f_ext
16 | picture_path = os.path.join(
17 | current_app.root_path, "static/manga_cover/", picture_fn
18 | )
19 |
20 | i = Image.open(form_picture)
21 | i.save(picture_path)
22 |
23 | return picture_fn
24 |
25 |
26 | def remove_cover(manga_cover):
27 | if not manga_cover == "default-manga.svg":
28 | os.remove(f"src/static/manga_cover/{manga_cover}")
29 |
30 |
31 | class MangaHistory:
32 | def __init__(self):
33 | with open("json/mangalogs.json", "r") as f:
34 | self.history = json.load(f)
35 |
36 | def add_chapter(self, manga_name, new_chapter_number):
37 | current_date = datetime.now().strftime("%Y-%m-%d")
38 | current_time = datetime.now().strftime("%H:%M:%S")
39 | if manga_name in self.history:
40 | # If user edits manga without updating chapter then it creates duplicates of that chapter number in logs
41 | # This if else blocks above situation
42 | old_chapter_number = self.history[manga_name][-1][
43 | 0
44 | ] # self.history[manga_name][recent entry/chapter of manga_name][chapter_number]
45 | if old_chapter_number != new_chapter_number:
46 | self.history[manga_name].append(
47 | (new_chapter_number, current_date, current_time)
48 | )
49 | else:
50 | self.history[manga_name] = [
51 | (new_chapter_number, current_date, current_time)
52 | ]
53 | self.commit()
54 |
55 | def get_history(self, manga_name):
56 | if manga_name in self.history:
57 | return self.history[manga_name]
58 | else:
59 | return []
60 |
61 | # To change key in dictionary: https://stackoverflow.com/a/16475408
62 | def check_rename(self, old_name, new_name):
63 | if old_name != new_name:
64 | try:
65 | self.history[new_name] = self.history[old_name]
66 | del self.history[old_name]
67 | self.commit()
68 | except:
69 | return
70 |
71 | def clear_history(self, manga_name):
72 | if manga_name in self.history:
73 | del self.history[manga_name]
74 | self.commit()
75 |
76 | def clear_all_history(self):
77 | self.history = {}
78 | self.commit()
79 |
80 | def commit(self):
81 | with open("json/mangalogs.json", "w") as f:
82 | json.dump(self.history, f, indent=4)
83 |
84 |
85 | def get_settings():
86 | with open("json/settings.json", "r") as f:
87 | settings = json.load(f)
88 | return settings
89 |
90 |
91 | def get_layout():
92 | with open("json/settings.json", "r") as f:
93 | settings = json.load(f)
94 | layout = settings["layout"]
95 | if layout == "Card" or layout == "Image Overlay Card":
96 | return "manga-cards.html"
97 | else:
98 | return "manga-list.html"
99 |
100 |
101 | def online_image_downloader(img_url, title):
102 | file_name = os.urandom(8).hex()
103 | res = requests.get(img_url, stream=True, timeout=30)
104 | if res.status_code == 200:
105 | with open(f"src/static/manga_cover/{file_name}.jpg", "wb") as image:
106 | shutil.copyfileobj(res.raw, image)
107 | return f"{file_name}.jpg"
108 | print("Image couldn't be retrieved: ", title)
109 | return "default-manga.svg"
110 |
--------------------------------------------------------------------------------
/src/manga/web_scraper.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from bs4 import BeautifulSoup
3 |
4 | import src.manga.utils as manga_utils
5 |
6 |
7 | # Collects metadata of manga from ManagUpdates which contains description, genres etc.
8 | def manga_metadata(url, title):
9 | # Making a GET request
10 | response = requests.get(url, timeout=30)
11 | # Parsing the HTML
12 | soup = BeautifulSoup(response.content, "html.parser")
13 |
14 | # Checks for divs which contains metadata
15 | # I have divided the page into two sides - left[0] and right[1]
16 | side = soup.find_all("div", class_="col-6 p-2 text")
17 | # Checks left side content for description. As description is first in list
18 | # so no extra variable is used
19 | manga_desc = side[0].find(class_="sContent")
20 | # Check for right side content which contais data of image, genre, author and artist
21 | content = side[1].find_all(class_="sContent")
22 | # Checks all image url on the webpage. It is used for cover of manga.
23 | images = side[1].find_all(class_="img-fluid")
24 |
25 | # Getting Manga Description and removing duplicate and extra stuff
26 | manga_description = manga_desc.text.strip()
27 | if "More..." in manga_description:
28 | manga_description = manga_description.split("More...\n\n\n")
29 | manga_description = " ".join(manga_description[1:]).replace("Less...", "")
30 | # After collecting the image on right side (cover image), we send it to get downloaded.
31 | try:
32 | manga_cover = manga_utils.online_image_downloader(
33 | images[0].get("src").strip(), title
34 | )
35 | except IndexError:
36 | print("Image couldn't be retrieved: ", title)
37 | manga_cover = "default-manga.svg"
38 | # Index 1 on content consists of genre
39 | # .split is to remove last suggestion from the genre i.e. "Search for series of same genre(s)"
40 | manga_genre = content[1].text.split("\xa0")
41 | manga_genre = ", ".join(manga_genre[:-1])
42 | # Index 5 on content consists of author
43 | manga_author = content[5].text.replace("[Add]", "")
44 | # Index 6 on content consists of Artist
45 | manga_artist = content[6].text.replace("[Add]", "")
46 | print("Metadata successfully updated: ", title)
47 | return [
48 | str(manga_artist),
49 | str(manga_author),
50 | str(manga_cover),
51 | str(manga_description),
52 | str(manga_genre),
53 | ]
54 |
55 |
56 | # Searches MangaUpdates for manga title and sends the first recommendation to manga_metadata
57 | # function
58 | def manga_search(title):
59 | # Default search URL with manga title as seach query and Making a GET request
60 | response = requests.get(
61 | f"https://www.mangaupdates.com/search.html?search={title}", timeout=30
62 | )
63 | # Parsing the HTML
64 | soup = BeautifulSoup(response.content, "html.parser")
65 | # Getting div class of "Series Info" -> "Title" -> First Recommendation
66 | series = soup.find_all("div", class_="col-6 py-1 py-md-0 text")
67 | # Getting tag from first recommendation div
68 | try:
69 | atag = series[0].find_all("a")
70 | except IndexError:
71 | print("Manga not found: ", title)
72 | return [
73 | None,
74 | None,
75 | "default-manga.svg",
76 | "Manga not found on MangaUpdates",
77 | None,
78 | ]
79 | # Getting URL from tag's href
80 | url = atag[0].get("href")
81 | # Calling manga_metadata function to return manga metadata
82 | return manga_metadata(url, title)
83 |
--------------------------------------------------------------------------------
/src/models.py:
--------------------------------------------------------------------------------
1 | from src import db
2 |
3 |
4 | class Manga(db.Model):
5 | id = db.Column(db.Integer, primary_key=True, autoincrement=True)
6 | cover = db.Column(db.String, nullable=False, default="default-manga.svg")
7 | title = db.Column(db.String, nullable=False)
8 | start_date = db.Column(db.String)
9 | end_date = db.Column(db.String)
10 | volume = db.Column(db.Integer)
11 | chapter = db.Column(db.Integer)
12 | score = db.Column(db.Integer)
13 | status = db.Column(db.String(20), nullable=False, default="Plan to read")
14 | description = db.Column(db.String)
15 | genre = db.Column(db.String)
16 | tags = db.Column(db.String)
17 | author = db.Column(db.String)
18 | artist = db.Column(db.String)
19 | notes = db.Column(db.String)
20 |
21 | def __repr__(self):
22 | return f"Manga('{self.title}', '{self.start_date}', '{self.end_date}', '{self.score}', '{self.status}', '{self.genre}')"
23 |
24 |
25 | class Anime(db.Model):
26 | id = db.Column(db.Integer, primary_key=True, autoincrement=True)
27 | cover = db.Column(db.String, nullable=False, default="default-anime.svg")
28 | title = db.Column(db.String, nullable=False)
29 | episode = db.Column(db.Integer)
30 | start_date = db.Column(db.String)
31 | end_date = db.Column(db.String)
32 | score = db.Column(db.Integer)
33 | status = db.Column(db.String(20), nullable=False, default="Plan to watch")
34 | description = db.Column(db.String)
35 | genre = db.Column(db.String)
36 | tags = db.Column(db.String)
37 | notes = db.Column(db.String)
38 |
39 | def __repr__(self):
40 | return f"Anime('{self.title}', '{self.start_date}', '{self.end_date}', '{self.score}', '{self.status}', '{self.genre}')"
41 |
--------------------------------------------------------------------------------
/src/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/settings/__init__.py
--------------------------------------------------------------------------------
/src/settings/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import SelectField, SubmitField
3 |
4 |
5 | class SettingsForm(FlaskForm):
6 | theme = SelectField("Theme", choices=["Dark", "Light"])
7 | enable_logging = SelectField("Enable Logging", choices=["Yes", "No"])
8 | truncate_title = SelectField("Truncate Title", choices=["Yes", "No"])
9 | layout = SelectField("Layout", choices=["Table", "Card", "Image Overlay Card"])
10 | mmdb_promotion = SelectField("MMDB Promotion", choices=["Yes", "No"])
11 | save = SubmitField("Save")
12 |
--------------------------------------------------------------------------------
/src/settings/routes.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | from flask import Blueprint, flash, render_template, request
5 |
6 | from src import db
7 | from src.settings.forms import SettingsForm
8 |
9 | settings = Blueprint("settings", __name__, url_prefix="/settings")
10 |
11 |
12 | def create_json_files():
13 | if not os.path.exists("json"):
14 | os.mkdir("json")
15 | if not os.path.exists("json/settings.json"):
16 | with open("json/settings.json", "w") as f:
17 | settings = {
18 | "theme": "Dark",
19 | "enable_logging": "Yes",
20 | "truncate_title": "No",
21 | "layout": "Table",
22 | "mmdb_promotion": "Yes",
23 | }
24 | json.dump(settings, f)
25 | if not os.path.exists("json/mangalogs.json"):
26 | with open("json/mangalogs.json", "w") as f:
27 | json.dump({}, f)
28 | if not os.path.exists("json/animelogs.json"):
29 | with open("json/animelogs.json", "w") as f:
30 | json.dump({}, f)
31 |
32 |
33 | @settings.route("", methods=["POST", "GET"])
34 | def settingspage():
35 | with open("json/settings.json", "r") as f:
36 | json_settings = json.load(f)
37 |
38 | form = SettingsForm()
39 |
40 | if form.validate_on_submit():
41 | # Theme
42 | json_settings["theme"] = form.theme.data
43 | # Logging
44 | json_settings["enable_logging"] = form.enable_logging.data
45 | # Truncate Title
46 | json_settings["truncate_title"] = form.truncate_title.data
47 | # Layout
48 | json_settings["layout"] = form.layout.data
49 | # MMDB Promotion
50 | json_settings["mmdb_promotion"] = form.mmdb_promotion.data
51 |
52 | with open("json/settings.json", "w") as f:
53 | json.dump(json_settings, f, indent=4)
54 | flash("Settings Updated!", "success")
55 | elif request.method == "GET":
56 | # Theme
57 | form.theme.data = json_settings["theme"]
58 | # Enable Logging
59 | form.enable_logging.data = json_settings["enable_logging"]
60 | # Truncate Title
61 | form.truncate_title.data = json_settings["truncate_title"]
62 | # Layout
63 | form.layout.data = json_settings["layout"]
64 | # MMDB Promotion
65 | form.mmdb_promotion.data = json_settings["mmdb_promotion"]
66 | return render_template(
67 | "settings.html",
68 | form=form,
69 | legend="Settings",
70 | title="Settings",
71 | current_section="Settings",
72 | )
73 |
--------------------------------------------------------------------------------
/src/static/anime_cover/default-anime.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/error_assests/404 not found.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/error_assests/404 not found.gif
--------------------------------------------------------------------------------
/src/static/error_assests/405 not allowed.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/error_assests/405 not allowed.jpg
--------------------------------------------------------------------------------
/src/static/error_assests/500 server down.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/error_assests/500 server down.jpg
--------------------------------------------------------------------------------
/src/static/favicon/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following font:
2 |
3 | - Font Title: Leckerli One
4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli"
5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v16/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))
7 |
--------------------------------------------------------------------------------
/src/static/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/static/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/static/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/static/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/src/static/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/src/static/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EdwinRodger/MyMangaDataBase/5a32af67c5089d45d9754b6de92f7ee88b4359ec/src/static/favicon/favicon.ico
--------------------------------------------------------------------------------
/src/static/favicon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/src/static/manga_cover/default-manga.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/anime/anime-cards.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 | {% block secondHeader %}
3 |
4 |
This is a server error which means there is something wrong in MMDB Code. Please issue a bug request
16 | on Github describing how this error happened.