├── .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 | ![Home Page](docs/images/2023-06-24/hero(2023-06-24).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 | ![Star.png](/docs/images/others/star.png "How to star MMDB") -------------------------------------------------------------------------------- /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 | 5 | 6 | 7 | 8 | 10 | 12 | 13 | MyMangaDataBase by EdwinRodger 14 | 15 | 16 | 17 | 18 | 20 | 21 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 | Hero.png 33 |
34 |
35 |

MyMangaDataBase

36 |

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 | 88 | 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 |
5 |
6 | 7 | 15 | 16 | 59 | 60 |
61 | {{ anime_navsearch.hidden_tag() }} 62 | {{ anime_navsearch.search_field(class="form-control mx-1", placeholder=anime_title) }} 63 | {{ anime_navsearch.search_button(class="btn btn-outline-primary") }} 64 |
65 |
66 |
67 | {% endblock %} 68 | 69 | 70 | 71 | {% block body %} 72 | 73 | {% if settings["layout"] == "Card" %} 74 |
76 | {% for anime in anime_list %} 77 |
78 |
79 |
80 |
81 | Anime Cover 83 |
84 |
85 |
86 | {% if settings["truncate_title"] == "Yes" %} 87 |
89 | {{ anime.title }} 90 |
91 | {% else %} 92 |
94 | {{ anime.title }} 95 |
96 | {% endif %} 97 |

98 | Score: {{ anime.score }} 99 |

100 |

101 | Episodes: 102 | 104 | {{ anime.episode }} 105 | 106 |

107 |

108 | Start Date: 109 | {% if anime.start_date == "0001-01-01" %} 110 | - 111 | {% else %} 112 | {{ anime.start_date }} 113 | {% endif %} 114 |

115 |

116 | End Date: 117 | {% if anime.end_date == "0001-01-01" %} 118 | - 119 | {% else %} 120 | {{ anime.end_date }} 121 | {% endif %} 122 |

123 |

Status: {{ anime.status }}

124 |
125 |
126 |
127 |
128 |
129 | {% endfor %} 130 |
131 | {% else %} 132 |
133 |
134 | {% for anime in anime_list %} 135 |
136 |
137 | Anime Cover 139 |
140 |
141 | {% if settings["truncate_title"] == "Yes" %} 142 |
144 | {{ anime.title }} 145 |
146 | {% else %} 147 |
149 | {{ anime.title }} 150 |
151 | {% endif %} 152 |

153 | Score: {{ anime.score }} 154 |

155 |

156 | Episodes: 157 | 159 | {{ anime.episode }} 160 | 161 |

162 |

163 | Start Date: 164 | {% if anime.start_date == "0001-01-01" %} 165 | - 166 | {% else %} 167 | {{ anime.start_date }} 168 | {% endif %} 169 |

170 |

171 | End Date: 172 | {% if anime.end_date == "0001-01-01" %} 173 | - 174 | {% else %} 175 | {{ anime.end_date }} 176 | {% endif %} 177 |

178 |

Status: {{ anime.status }}

179 |
180 |
181 |
182 |
183 | {% endfor %} 184 |
185 |
186 | {% endif %} 187 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/anime/anime-list.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block secondHeader %} 3 | 4 |
5 |
6 | 7 | 15 | 16 | 59 | 60 |
61 | {{ anime_navsearch.hidden_tag() }} 62 | {{ anime_navsearch.search_field(class="form-control mx-1", placeholder=anime_title) }} 63 | {{ anime_navsearch.search_button(class="btn btn-outline-primary") }} 64 |
65 |
66 |
67 | {% endblock %} 68 | 69 | 70 | 71 | {% block body %} 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {% for anime in anime_list %} 88 | 89 | 90 | 94 | 95 | {% if truncate_title == "Yes" %} 96 | 97 | 103 | {% else %} 104 | 110 | {% endif %} 111 | 112 | {% if anime.episode == 0 %} 113 | 114 | {% else %} 115 | 121 | {% endif %} 122 | {% if anime.start_date == "0001-01-01" %} 123 | 124 | {% else %} 125 | 126 | {% endif %} 127 | {% if anime.end_date == "0001-01-01" %} 128 | 129 | {% else %} 130 | 131 | {% endif %} 132 | 133 | 134 | {% endfor %} 135 | 136 |
CoverTitleScoreEpisodesStart DateEnd DateStatus
91 | Anime Cover 93 | 98 | 100 | {{ anime.title }} 101 | 102 | 105 | 107 | {{ anime.title }} 108 | 109 | {{ anime.score }}- 116 | 118 | {{ anime.episode }} 119 | 120 | -{{ anime.start_date }}-{{ anime.end_date }}{{ anime.status }}
137 |
138 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/anime/create-anime.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
7 | {{ form.hidden_tag() }} 8 |
9 | 10 | {{ legend }} 11 | 12 |
13 | {{ form.title.label(class="form-label") }} 14 | {% if form.title.errors %} 15 | {{ form.title(class="form-control form-control-lg is-invalid") }} 16 |
17 | {% for error in form.title.errors %} 18 | {{ error }} 19 | {% endfor %} 20 |
21 | {% else %} 22 | {{ form.title(class="form-control form-control-lg") }} 23 | {% endif %} 24 |
25 |
26 | {{ form.start_date.label(class="form-label") }} 27 | {% if form.start_date.errors %} 28 | {{ form.start_date(class="form-control form-control-lg is-invalid") }} 29 |
30 | {% for error in form.start_date.errors %} 31 | {{ error }} 32 | {% endfor %} 33 |
34 | {% else %} 35 | {{ form.start_date(class="form-control form-control-lg") }} 36 | {% endif %} 37 |

If you don't know Start Date, Enter 01-01-0001 in above field

38 |
39 |
40 | {{ form.end_date.label(class="form-label") }} 41 | {% if form.end_date.errors %} 42 | {{ form.end_date(class="form-control form-control-lg is-invalid") }} 43 |
44 | {% for error in form.end_date.errors %} 45 | {{ error }} 46 | {% endfor %} 47 |
48 | {% else %} 49 | {{ form.end_date(class="form-control form-control-lg") }} 50 | {% endif %} 51 |

If you don't know End Date, Enter 01-01-0001 in above field

52 |
53 |
54 | {{ form.episode.label(class="form-label") }} 55 | {% if form.episode.errors %} 56 | {{ form.episode(class="form-control form-control-lg is-invalid") }} 57 |
58 | {% for error in form.episode.errors %} 59 | {{ error }} 60 | {% endfor %} 61 |
62 | {% else %} 63 | {{ form.episode(class="form-control form-control-lg") }} 64 | {% endif %} 65 |

If you haven't started the anime or the anime doesn't have any episodes, enter 66 | 0 67 | (zero) in above field

68 |
69 |
70 | {{ form.score.label(class="form-label") }} 71 | {% if form.score.errors %} 72 | {{ form.score(class="form-control form-control-lg is-invalid") }} 73 |
74 | {% for error in form.score.errors %} 75 | {{ error }} 76 | {% endfor %} 77 |
78 | {% else %} 79 | {{ form.score(class="form-control form-control-lg") }} 80 | {% endif %} 81 |
82 |
83 | {{ form.status.label(class="form-label") }} 84 | {% if form.status.errors %} 85 | {{ form.status(class="form-control form-control-lg is-invalid") }} 86 |
87 | {% for error in form.status.errors %} 88 | {{ error }} 89 | {% endfor %} 90 |
91 | {% else %} 92 | {{ form.status(class="form-control form-control-lg") }} 93 | {% endif %} 94 |
95 | 96 | Optional 97 | 98 |
99 | 100 | {{ form.description(class="form-control", rows=7) }} 101 |
102 |
103 | 110 | {{ form.genre(class="form-control") }} 111 |

Seprate genre by using a comma (,)

112 |
113 |
114 | 120 | {{ form.tags(class="form-control") }} 121 |

Seprate tags by using a comma (,)

122 |
123 |
124 | 125 | {{ form.notes(class="form-control", rows=7) }} 126 |
127 |
128 | 129 | {{ form.cover(class="form-control") }} 130 | {% if form.cover.errors %} 131 | {% for error in form.cover.errors %} 132 | {{ error }}
133 | {% endfor %} 134 | {% endif %} 135 |
136 |
137 |
138 | {{ form.submit(class="btn btn-outline-info") }} 139 |
140 |
141 |
142 |
143 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/anime/edit-anime.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | 5 | {{ legend }} 6 |
7 |
8 |
9 | {{ form.hidden_tag() }} 10 |
11 |
12 | Anime Cover 14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | {% if form.title.errors %} 22 | {{ form.title(class="form-control is-invalid") }} 23 |
24 | {% for error in form.title.errors %} 25 | {{ error }} 26 | {% endfor %} 27 |
28 | {% else %} 29 | {{ form.title(class="form-control") }} 30 | {% endif %} 31 |
32 |
33 | 34 | {% if form.start_date.errors %} 35 | {{ form.start_date(class="form-control is-invalid") }} 36 |
37 | {% for error in form.start_date.errors %} 38 | {{ error }} 39 | {% endfor %} 40 |
41 | {% else %} 42 | {{ form.start_date(class="form-control") }} 43 | {% endif %} 44 | {% if platform.system() == "Windows" %} 45 |

If you don't know Start Date, Enter 01-01-0001 in above field.

46 | {% else %} 47 |

If you don't know Start Date, Leave the above field empty

48 | {% endif %} 49 |
50 |
51 | 52 | {% if form.end_date.errors %} 53 | {{ form.end_date(class="form-control is-invalid") }} 54 |
55 | {% for error in form.end_date.errors %} 56 | {{ error }} 57 | {% endfor %} 58 |
59 | {% else %} 60 | {{ form.end_date(class="form-control") }} 61 | {% endif %} 62 | {% if platform.system() == "Windows" %} 63 |

If you don't know End Date, Enter 01-01-0001 in above field.

64 | {% else %} 65 |

If you don't know End Date, Leave the above field empty

66 | {% endif %} 67 |
68 |
69 | 73 | {% if form.episode.errors %} 74 | {{ form.episode(class="form-control is-invalid") }} 75 |
76 | {% for error in form.episode.errors %} 77 | {{ error }} 78 | {% endfor %} 79 |
80 | {% else %} 81 | {{ form.episode(class="form-control") }} 82 | {% endif %} 83 |

If you haven't started the anime or the anime doesn't have any episodes, 84 | enter 0 (zero) in above field

85 |
86 |
87 | 88 | {{ form.score(class="form-control") }} 89 |
90 |
91 | 92 | {{ form.status(class="form-control") }} 93 |
94 | 95 | Optional 96 | 97 |
98 | 99 | {{ form.description(class="form-control", rows=7) }} 100 |
101 |
102 | 109 | {{ form.genre(class="form-control") }} 110 |

Seprate genre by using a comma (,)

111 |
112 |
113 | 119 | {{ form.tags(class="form-control") }} 120 |

Seprate tags by using a comma (,)

121 |
122 |
123 | 124 | {{ form.notes(class="form-control", rows=7) }} 125 |
126 |
127 | 128 | {{ form.cover(class="form-control") }} 129 | {% if form.cover.errors %} 130 | {% for error in form.cover.errors %} 131 | {{ error }}
132 | {% endfor %} 133 | {% endif %} 134 |
135 |
136 | 137 | {{ form.update(class="btn btn-primary px-auto") }} 138 | 139 | Go Home 140 | 141 | 143 |
144 |
145 |
146 |
147 |
148 | 149 | 171 | 189 |
190 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/anime/import-anime.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
7 |
8 |
9 |
MMDB Anime Import
10 |

Imports " MMDB-Anime-Export-{Date}.zip " file

11 |
12 | 13 | 15 |
16 |

This wil overwrite all anime episode logs!

17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
MyAnimeList Anime Import
27 |

Imports " animelist_{uuid}_-_{user_id}.xml " file

28 |
29 | 30 | 32 |
33 |

Only import .xml file!

34 |

NOTE:

35 |
    36 |
  • 37 | Each entry will take between 1 to 2 seconds to get imported to avoid hitting 38 | 40 | api rate limit. 41 | 42 |
  • 43 |
  • 44 | In Short - the bigger the list, more the time taken to import. 45 |
  • 46 |
  • 47 | During import process, please wait until the page is redirected to 48 | Anime List page. 49 |
  • 50 |
  • You can see the progress on server terminal (^▽^)
  • 51 |
52 |
53 |
54 |
55 |
56 |
57 |
59 |
60 |
61 |
AniList Import
62 |

Imports " gdpr_data.json " file

63 |
64 | 65 | 67 |
68 |

Uses AniList API to get metadata

69 |
70 |
71 |
72 |
73 |
74 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/credits.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block body %} 3 |
4 | 45 |
46 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% if url %} 5 | 404 Not Found GIF 6 | {% else %} 7 | 404 Not Found JPG 9 | {% endif %} 10 |
11 |
12 |
13 |
14 |

Oops. Page Not Found (404)

15 |

Check the URL properly... BAKA!

16 |
17 |
18 |
19 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/errors/405.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% if url %} 5 | 405 Method Not Aloowed GIF 6 | {% else %} 7 | 405 not allowed JPG 9 | {% endif %} 10 |
11 |
12 |
13 |
14 |

Method Not Allowed (405)

15 |

Nope!

16 |
17 |
18 |
19 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% if url %} 5 | 500 Server Down GIF 6 | {% else %} 7 | 500 Server Down JPG 9 | {% endif %} 10 |
11 |
12 |
13 |
14 |

Internal Server Error (500)

15 |

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.

19 |
20 |
21 |
22 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block body %} 3 |
4 |
5 | 6 |
7 |

Manga Overview

8 |
9 |
10 |
11 |

{{ manga_data["manga_len"] }}

12 |

Total Manga

13 |
14 |
15 |

{{ manga_data["manga_chapter_len"] }}

16 |

Chapter Read

17 |
18 |
19 |

{{ manga_data["mean_score"] }}

20 |

Mean Score

21 |
22 |
23 |
24 | 25 |

Status

26 | {% for key, value in manga_data["status"].items() %} 27 |

Total No. of {{ key }} Manga: {{ value }}

28 | {% endfor %} 29 |
30 |
31 | 32 |

Genres

33 | {% for key, value in manga_data["genre"].items() %} 34 |

35 | {{ key 37 | }}: {{ value }} 38 |

39 | {% endfor %} 40 |
41 |
42 | 43 |

Score

44 | {% for key, value in manga_data["score"].items() %} 45 |

{{ key }}: {{ value }}

46 | {% endfor %} 47 |
48 |
49 | 50 |
51 |

Anime Overview

52 |
53 |
54 |
55 |

{{ anime_data["anime_len"] }}

56 |

Total Anime

57 |
58 |
59 |

{{ anime_data["anime_episode_len"] }}

60 |

Episodes Watched

61 |
62 |
63 |

{{ anime_data["mean_score"] }}

64 |

Mean Score

65 |
66 |
67 |
68 | 69 |

Status

70 | {% for key, value in anime_data["status"].items() %} 71 |

Total No. of {{ key }} Anime: {{ value }}

72 | {% endfor %} 73 |
74 |
75 | 76 |

Genres

77 | {% for key, value in anime_data["genre"].items() %} 78 |

{{ 80 | key }}: {{ value 81 | }}

82 | {% endfor %} 83 |
84 |
85 | 86 |

Score

87 | {% for key, value in anime_data["score"].items() %} 88 |

{{ key }}: {{ value }}

89 | {% endfor %} 90 |
91 |
92 |
93 |
94 | 95 | 96 | {% if show_update_modal %} 97 | 107 | {% endif %} 108 | 109 | 127 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | {% if theme == "Dark" %} 3 | 4 | {% else %} 5 | 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% if title %} 14 | {{ title }} 15 | {% else %} 16 | MyMangaDataBase 17 | {% endif %} 18 | 19 | 21 | 23 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 51 | 52 | 53 | 98 | {% block secondHeader %}{% endblock secondHeader %} 99 | 100 | {% with messages = get_flashed_messages(with_categories=true) %} 101 | {% if messages %} 102 | {% for category, messages in messages %} 103 |
104 | {{ messages | safe }} 105 |
106 | {% endfor %} 107 | {% endif %} 108 | {% endwith %} 109 |
110 | {% block body %}{% endblock body %} 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 133 | 134 | -------------------------------------------------------------------------------- /src/templates/manga/create-manga.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
7 | {{ form.hidden_tag() }} 8 |
9 | 10 | {{ legend }} 11 | 12 |
13 | {{ form.title.label(class="form-label") }} 14 | {% if form.title.errors %} 15 | {{ form.title(class="form-control form-control-lg is-invalid") }} 16 |
17 | {% for error in form.title.errors %} 18 | {{ error }} 19 | {% endfor %} 20 |
21 | {% else %} 22 | {{ form.title(class="form-control form-control-lg") }} 23 | {% endif %} 24 |
25 |
26 | {{ form.start_date.label(class="form-label") }} 27 | {% if form.start_date.errors %} 28 | {{ form.start_date(class="form-control form-control-lg is-invalid") }} 29 |
30 | {% for error in form.start_date.errors %} 31 | {{ error }} 32 | {% endfor %} 33 |
34 | {% else %} 35 | {{ form.start_date(class="form-control form-control-lg") }} 36 | {% endif %} 37 |

If you don't know Start Date, Enter 01-01-0001 in above field

38 |
39 |
40 | {{ form.end_date.label(class="form-label") }} 41 | {% if form.end_date.errors %} 42 | {{ form.end_date(class="form-control form-control-lg is-invalid") }} 43 |
44 | {% for error in form.end_date.errors %} 45 | {{ error }} 46 | {% endfor %} 47 |
48 | {% else %} 49 | {{ form.end_date(class="form-control form-control-lg") }} 50 | {% endif %} 51 |

If you don't know End Date, Enter 01-01-0001 in above field

52 |
53 |
54 | {{ form.volume.label(class="form-label") }} 55 | {% if form.volume.errors %} 56 | {{ form.volume(class="form-control form-control-lg is-invalid") }} 57 |
58 | {% for error in form.volume.errors %} 59 | {{ error }} 60 | {% endfor %} 61 |
62 | {% else %} 63 | {{ form.volume(class="form-control form-control-lg") }} 64 | {% endif %} 65 |

If you haven't started the manga or the manga doesn't have any volumes, enter 66 | 0 67 | (zero) in above field

68 |
69 |
70 | {{ form.chapter.label(class="form-label") }} 71 | {% if form.chapter.errors %} 72 | {{ form.chapter(class="form-control form-control-lg is-invalid") }} 73 |
74 | {% for error in form.chapter.errors %} 75 | {{ error }} 76 | {% endfor %} 77 |
78 | {% else %} 79 | {{ form.chapter(class="form-control form-control-lg") }} 80 | {% endif %} 81 |

If you haven't started the manga, enter 0 (zero) in above field

82 |
83 |
84 | {{ form.score.label(class="form-label") }} 85 | {% if form.score.errors %} 86 | {{ form.score(class="form-control form-control-lg is-invalid") }} 87 |
88 | {% for error in form.score.errors %} 89 | {{ error }} 90 | {% endfor %} 91 |
92 | {% else %} 93 | {{ form.score(class="form-control form-control-lg") }} 94 | {% endif %} 95 |
96 |
97 | {{ form.status.label(class="form-label") }} 98 | {% if form.status.errors %} 99 | {{ form.status(class="form-control form-control-lg is-invalid") }} 100 |
101 | {% for error in form.status.errors %} 102 | {{ error }} 103 | {% endfor %} 104 |
105 | {% else %} 106 | {{ form.status(class="form-control form-control-lg") }} 107 | {% endif %} 108 |
109 | 110 | Optional 111 | 112 |
113 | 114 | {{ form.description(class="form-control", rows=7) }} 115 |
116 |
117 | 118 | {{ form.artist(class="form-control") }} 119 |
120 |
121 | 122 | {{ form.author(class="form-control") }} 123 |
124 |
125 | 132 | {{ form.genre(class="form-control") }} 133 |

Seprate genre by using a comma (,)

134 |
135 |
136 | 142 | {{ form.tags(class="form-control") }} 143 |

Seprate tags by using a comma (,)

144 |
145 |
146 | 147 | {{ form.notes(class="form-control", rows=7) }} 148 |
149 |
150 | 151 | {{ form.cover(class="form-control") }} 152 | {% if form.cover.errors %} 153 | {% for error in form.cover.errors %} 154 | {{ error }}
155 | {% endfor %} 156 | {% endif %} 157 |
158 |
159 |
160 | {{ form.submit(class="btn btn-outline-info") }} 161 |
162 |
163 |
164 |
165 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/manga/edit-manga.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | 5 | {{ legend }} 6 |
7 |
8 |
9 | {{ form.hidden_tag() }} 10 |
11 |
12 | Manga Cover 14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | {% if form.title.errors %} 22 | {{ form.title(class="form-control is-invalid") }} 23 |
24 | {% for error in form.title.errors %} 25 | {{ error }} 26 | {% endfor %} 27 |
28 | {% else %} 29 | {{ form.title(class="form-control") }} 30 | {% endif %} 31 |
32 |
33 | 34 | {% if form.start_date.errors %} 35 | {{ form.start_date(class="form-control is-invalid") }} 36 |
37 | {% for error in form.start_date.errors %} 38 | {{ error }} 39 | {% endfor %} 40 |
41 | {% else %} 42 | {{ form.start_date(class="form-control") }} 43 | {% endif %} 44 | {% if platform.system() == "Windows" %} 45 |

If you don't know Start Date, Enter 01-01-0001 in above field.

46 | {% else %} 47 |

If you don't know Start Date, Leave the above field empty

48 | {% endif %} 49 |
50 |
51 | 52 | {% if form.end_date.errors %} 53 | {{ form.end_date(class="form-control is-invalid") }} 54 |
55 | {% for error in form.end_date.errors %} 56 | {{ error }} 57 | {% endfor %} 58 |
59 | {% else %} 60 | {{ form.end_date(class="form-control") }} 61 | {% endif %} 62 | {% if platform.system() == "Windows" %} 63 |

If you don't know End Date, Enter 01-01-0001 in above field.

64 | {% else %} 65 |

If you don't know End Date, Leave the above field empty

66 | {% endif %} 67 |
68 |
69 | 70 | {% if form.volume.errors %} 71 | {{ form.volume(class="form-control is-invalid") }} 72 |
73 | {% for error in form.volume.errors %} 74 | {{ error }} 75 | {% endfor %} 76 |
77 | {% else %} 78 | {{ form.volume(class="form-control") }} 79 | {% endif %} 80 |

If you haven't started the manga or the manga doesn't have any volumes, 81 | enter 0 (zero) in above field

82 |
83 |
84 | 88 | {% if form.chapter.errors %} 89 | {{ form.chapter(class="form-control is-invalid") }} 90 |
91 | {% for error in form.chapter.errors %} 92 | {{ error }} 93 | {% endfor %} 94 |
95 | {% else %} 96 | {{ form.chapter(class="form-control") }} 97 | {% endif %} 98 |

If you haven't started the manga, enter 0 (zero) in above field

99 |
100 |
101 | 102 | {{ form.score(class="form-control") }} 103 |
104 |
105 | 106 | {{ form.status(class="form-control") }} 107 |
108 | 109 | Optional 110 | 111 |
112 | 113 | {{ form.description(class="form-control", rows=7) }} 114 |
115 | 116 |
117 | 118 | {{ form.artist(class="form-control") }} 119 |
120 |
121 | 122 | {{ form.author(class="form-control") }} 123 |
124 |
125 | 132 | {{ form.genre(class="form-control") }} 133 |

Seprate genre by using a comma (,)

134 |
135 |
136 | 142 | {{ form.tags(class="form-control") }} 143 |

Seprate tags by using a comma (,)

144 |
145 |
146 | 147 | {{ form.notes(class="form-control", rows=7) }} 148 |
149 |
150 | 151 | {{ form.cover(class="form-control") }} 152 | {% if form.cover.errors %} 153 | {% for error in form.cover.errors %} 154 | {{ error }}
155 | {% endfor %} 156 | {% endif %} 157 |
158 |
159 | 160 | {{ form.update(class="btn btn-primary px-auto") }} 161 | 162 | Get Metadata From MangaUpdates 163 | 164 | Go Home 165 | 166 | 168 |
169 |
170 |
171 |
172 |
173 | 174 | 196 | 214 |
215 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/manga/import-manga.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
7 |
8 |
9 |
MMDB Manga Import
10 |

Imports " MMDB-Manga-Export-{Date}.zip " file

11 |
12 | 13 | 15 |
16 |

This wil overwrite all manga chapter logs!

17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
MyAnimeList Manga Import
27 |

Imports " mangalist_{uuid}_-_{user_id}.xml " file

28 |
29 | 30 | 32 |
33 |

Only import .xml file!

34 |

NOTE:

35 |
    36 |
  • 37 | Each entry will take between 1 to 2 seconds to get imported to avoid hitting 38 | 40 | api rate limit. 41 | 42 |
  • 43 |
  • 44 | In Short - the bigger the list, more the time taken to import. 45 |
  • 46 |
  • 47 | During import process, please wait until the page is redirected to 48 | Manga List page. 49 |
  • 50 |
  • You can see the progress on server terminal (^▽^)
  • 51 |
52 |
53 |
54 |
55 |
56 |
57 |
59 |
60 |
61 |
MangaUpdates Import
62 |

Imports " {status}_{Date}.txt " file

63 |
64 | 65 | 67 |
68 |

Custom lists are not supported.

69 |
70 |
71 |
72 |
73 |
74 |
76 |
77 |
78 |
AniList Import
79 |

Imports " gdpr_data.json " file

80 |
81 | 82 | 84 |
85 |

Uses AniList API to get metadata

86 |
87 |
88 |
89 |
90 |
91 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/manga/manga-cards.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block secondHeader %} 3 | 4 |
5 |
6 | 7 | 15 | 16 | 59 | 60 |
61 | {{ manga_navsearch.hidden_tag() }} 62 | {{ manga_navsearch.search_field(class="form-control mx-1", placeholder=manga_title) }} 63 | {{ manga_navsearch.search_button(class="btn btn-outline-primary") }} 64 |
65 |
66 |
67 | {% endblock %} 68 | 69 | 70 | 71 | {% block body %} 72 | 73 | {% if settings["layout"] == "Card" %} 74 |
75 | {% for manga in manga_list %} 76 |
77 |
78 |
79 |
80 | Anime Cover 82 |
83 |
84 |
85 | {% if settings["truncate_title"] == "Yes" %} 86 |
88 | {{ manga.title }} 89 |
90 | {% else %} 91 |
93 | {{ manga.title }} 94 |
95 | {% endif %} 96 |

97 | Score: {{ manga.score }} 98 |

99 |
100 |

101 | Volumes: 102 | 104 | {{ manga.volume }} 105 | 106 |

107 |   108 |

109 | Chapters: 110 | 112 | {{ manga.chapter }} 113 | 114 |

115 |
116 |

117 | Start Date: 118 | {% if manga.start_date == "0001-01-01" %} 119 | - 120 | {% else %} 121 | {{ manga.start_date }} 122 | {% endif %} 123 |

124 |

125 | End Date: 126 | {% if manga.end_date == "0001-01-01" %} 127 | - 128 | {% else %} 129 | {{ manga.end_date }} 130 | {% endif %} 131 |

132 |

Status: {{ manga.status }}

133 |
134 |
135 |
136 |
137 |
138 | {% endfor %} 139 |
140 | {% else %} 141 |
142 |
143 | {% for manga in manga_list %} 144 |
145 |
146 | Anime Cover 148 |
149 |
150 | {% if settings["truncate_title"] == "Yes" %} 151 |
153 | {{ manga.title }} 154 |
155 | {% else %} 156 |
158 | {{ manga.title }} 159 |
160 | {% endif %} 161 |

162 | Score: {{ manga.score }} 163 |

164 |
165 |

166 | Volumes: 167 | 169 | {{ manga.volume }} 170 | 171 |

172 |   173 |

174 | Chapters: 175 | 177 | {{ manga.chapter }} 178 | 179 |

180 |
181 |

182 | Start Date: 183 | {% if manga.start_date == "0001-01-01" %} 184 | - 185 | {% else %} 186 | {{ manga.start_date }} 187 | {% endif %} 188 |

189 |

190 | End Date: 191 | {% if manga.end_date == "0001-01-01" %} 192 | - 193 | {% else %} 194 | {{ manga.end_date }} 195 | {% endif %} 196 |

197 |

Status: {{ manga.status }}

198 |
199 |
200 |
201 |
202 | {% endfor %} 203 |
204 |
205 | {% endif %} 206 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/manga/manga-list.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block secondHeader %} 3 | 4 |
5 |
6 | 7 | 15 | 16 | 59 | 60 |
61 | {{ manga_navsearch.hidden_tag() }} 62 | {{ manga_navsearch.search_field(class="form-control mx-1", placeholder=manga_title) }} 63 | {{ manga_navsearch.search_button(class="btn btn-outline-primary") }} 64 |
65 |
66 |
67 | {% endblock %} 68 | 69 | 70 | 71 | {% block body %} 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {% for manga in manga_list %} 89 | 90 | 91 | 95 | 96 | {% if settings["truncate_title"] == "Yes" %} 97 | 98 | 108 | {% if manga.score == 0 %} 109 | 110 | {% else %} 111 | 112 | {% endif %} 113 | {% if manga.volume == 0 %} 114 | 115 | {% else %} 116 | 122 | {% endif %} 123 | {% if manga.chapter == 0 %} 124 | 125 | {% else %} 126 | 132 | {% endif %} 133 | {% if manga.start_date == "0001-01-01" %} 134 | 135 | {% else %} 136 | 137 | {% endif %} 138 | {% if manga.end_date == "0001-01-01" %} 139 | 140 | {% else %} 141 | 142 | {% endif %} 143 | 144 | 145 | {% endfor %} 146 | 147 |
CoverTitleScoreVolumeChapterStart DateEnd DateStatus
92 | Manga Cover 94 | 100 | {% else %} 101 | 102 | {% endif %} 103 | 105 | {{ manga.title }} 106 | 107 | -{{ manga.score }}- 117 | 119 | {{ manga.volume }} 120 | 121 | - 127 | 129 | {{ manga.chapter }} 130 | 131 | -{{ manga.start_date }}-{{ manga.end_date }}{{ manga.status }}
148 |
149 | {% endblock body %} -------------------------------------------------------------------------------- /src/templates/more.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block body %} 3 |
4 |

5 | 6 | 8 | About MMDB 9 | 10 |

11 |
12 |

13 | 14 | 15 | Author 16 | 17 |

18 |
19 |

20 | 21 | 23 | Source Code 24 | 25 |

26 |
27 |

28 | 29 | 31 | Changelog 32 | 33 |

34 |
35 |

36 | 37 | 40 | Report A Bug 41 | 42 |

43 |
44 |

45 | 46 | 49 | Request A Feature 50 | 51 |

52 |
53 |

54 | 55 | 58 | Star On Github 59 | 60 |

61 |
62 |

63 | 64 | 66 | Like On AlternativeTo 67 | 68 |

69 |
70 |

71 | 72 | 73 | Credits 74 | 75 |

76 |
77 |
78 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |

Settings

5 |
6 | {{ form.hidden_tag() }} 7 |
8 |
9 |
10 |
11 | 12 | {{ form.theme(class="form-control") }} 13 |
14 |
15 | 16 | {{ form.enable_logging(class="form-control") }} 17 |

Logging will change when you start server next time

18 |
19 |
20 | 21 | {{ form.truncate_title(class="form-control") }} 22 |
23 |
24 | 25 | {{ form.layout(class="form-control") }} 26 |
27 |
28 | 29 | {{ form.mmdb_promotion(class="form-control") }} 30 |
31 | 32 |
33 | {{ form.save(class="btn btn-outline-primary px-auto") }} 34 |
35 |
36 |
37 |
38 |
39 |
40 | {% endblock body %} --------------------------------------------------------------------------------