├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── build.yml │ ├── build_pr.yml │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yml ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.optional ├── LICENSE ├── MANIFEST.in ├── README.id.md ├── README.md ├── README.tr.md ├── SECURITY.md ├── assets ├── example_start_cmd.png └── example_usage_executable.png ├── compile.py ├── docs ├── Makefile ├── changelog.md ├── cli_ref │ ├── app_names.md │ ├── auth_cache.md │ ├── chapter_info.md │ ├── cli_options.md │ ├── commands.md │ ├── config.md │ ├── cover.md │ ├── download_tracker.md │ ├── env_vars.md │ ├── file_command.md │ ├── filters.md │ ├── follow_list_library.md │ ├── forums.md │ ├── index.md │ ├── list_library.md │ ├── log_levels.md │ ├── manga_library.md │ ├── oauth.md │ ├── path_placeholders.md │ ├── random.md │ └── seasonal_manga.md ├── cli_usage │ ├── advanced.md │ ├── advanced │ │ ├── auth_cache.md │ │ ├── auto_select_prompt.md │ │ ├── blacklist_group_or_user.md │ │ ├── blacklist_tags.md │ │ ├── chapter_info.md │ │ ├── compression_for_epub_cbz.md │ │ ├── configuration.md │ │ ├── download_in_443_port.md │ │ ├── enable_dns_over_https.md │ │ ├── filename_customization.md │ │ ├── filters.md │ │ ├── followed_mdlist_from_user_library.md │ │ ├── ignore_missing_chapters.md │ │ ├── index.md │ │ ├── manga_from_pipe_input.md │ │ ├── manga_from_scanlator_group.md │ │ ├── manga_from_user_library.md │ │ ├── manga_with_compressed_size.md │ │ ├── manga_with_different_title.md │ │ ├── mdlist_from_user_library.md │ │ ├── requests_timeout.md │ │ ├── scanlator_group_filter.md │ │ ├── setup_proxy.md │ │ ├── show_manga_covers.md │ │ ├── syntax_for_batch_download.md │ │ ├── throttle_requests.md │ │ └── verbose_output.md │ └── index.md ├── conf.py ├── formats.md ├── images │ ├── api_clients.png │ ├── chapter_info.png │ └── post-in-forum-thread.png ├── index.md ├── installation.md ├── make.bat └── migration_v2_v3.md ├── mangadex-dl_x64.spec ├── mangadex-dl_x86.spec ├── mangadex_downloader ├── __init__.py ├── __main__.py ├── artist_and_author.py ├── auth │ ├── __init__.py │ ├── base.py │ ├── legacy.py │ └── oauth2.py ├── chapter.py ├── cli │ ├── __init__.py │ ├── args_parser.py │ ├── auth.py │ ├── command.py │ ├── config.py │ ├── download.py │ ├── update.py │ ├── url.py │ └── utils.py ├── config │ ├── __init__.py │ ├── auth_cache.py │ ├── config.py │ ├── env.py │ └── utils.py ├── cover.py ├── downloader.py ├── errors.py ├── fetcher.py ├── filters.py ├── fonts │ └── GNU FreeFont │ │ ├── AUTHORS │ │ ├── COPYING │ │ ├── CREDITS │ │ ├── README │ │ ├── otf │ │ ├── FreeSans.otf │ │ ├── FreeSansBold.otf │ │ ├── FreeSansBoldOblique.otf │ │ └── FreeSansOblique.otf │ │ └── ttf │ │ ├── FreeSans.ttf │ │ ├── FreeSansBold.ttf │ │ ├── FreeSansBoldOblique.ttf │ │ └── FreeSansOblique.ttf ├── format │ ├── __init__.py │ ├── base.py │ ├── chinfo.py │ ├── comic_book.py │ ├── epub.py │ ├── pdf.py │ ├── placeholders.py │ ├── raw.py │ ├── sevenzip.py │ └── utils.py ├── forums.py ├── group.py ├── images │ └── mangadex-logo.png ├── iterator.py ├── json_op.py ├── language.py ├── main.py ├── manga.py ├── mdlist.py ├── network.py ├── path │ ├── __init__.py │ ├── op.py │ └── placeholders.py ├── progress_bar.py ├── range.py ├── tag.py ├── tracker │ ├── __init__.py │ ├── info_data │ │ ├── __init__.py │ │ ├── legacy.py │ │ └── sqlite.py │ ├── legacy.py │ ├── sql_files │ │ ├── create_ch_info.sql │ │ ├── create_file_info.sql │ │ └── create_img_info.sql │ ├── sql_migrations │ │ ├── 00001_init.py │ │ ├── 00002_add_table_db_info_and_alter_table_file_info.py │ │ ├── __init__.py │ │ └── base.py │ ├── sqlite.py │ └── utils.py ├── update.py ├── user.py └── utils.py ├── requirements-docs.txt ├── requirements-optional.txt ├── requirements.txt ├── ruff.toml ├── run.py ├── seasonal_manga_now.txt └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['mansuf'] 2 | ko_fi: rahmanyusuf 3 | custom: ['https://sociabuzz.com/mansuf/donate'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug issue 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking time to fill this bug report. Before you continue, make sure you have read [github community guidelines](https://docs.github.com/articles/github-community-guidelines). 9 | Please note that this form only for reporting bugs. 10 | - type: textarea 11 | attributes: 12 | label: What happened ? 13 | placeholder: "Explain it (ex: I cannot download these manga)" 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: What did you expect to happen ? 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: OS version 24 | description: What Operating System you're currently using on ? 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: App version 30 | description: You can get it from `mangadex-dl --version` 31 | validations: 32 | required: true 33 | - type: dropdown 34 | attributes: 35 | label: Installation origin 36 | description: Where did you install mangadex-downloader ? 37 | options: 38 | - "PyPI (Python Package Index)" 39 | - Github releases 40 | - "git clone && python setup.py install" 41 | - Other 42 | validations: 43 | required: true 44 | - type: input 45 | attributes: 46 | label: "Installation origin (other sources)" 47 | description: Type in here if you install mangadex-downloader from other source. 48 | - type: textarea 49 | attributes: 50 | label: Reproducible command 51 | placeholder: "Example: mangadex-dl \"insert mangadex url here\" --format pdf" 52 | validations: 53 | required: true 54 | - type: textarea 55 | attributes: 56 | label: Additional context -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest or add a feature 3 | labels: ["enhancement"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: The idea 8 | placeholder: "ex: Add EPUB format" 9 | validations: 10 | required: true 11 | - type: textarea 12 | attributes: 13 | label: Why this feature should be added to the app ? 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/workflows/build_pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request build check 2 | on: 3 | pull_request: 4 | paths: 5 | - '**.py' 6 | - 'requirements.txt' 7 | - 'requirements-optional.txt' 8 | - 'docs/*' 9 | 10 | env: 11 | TEST_TAG: mansuf/mangadex-downloader:test 12 | TEST_OPTIONAL_TAG: mansuf/mangadex-downloader:test-optional 13 | LATEST_TAG: mansuf/mangadex-downloader:latest 14 | LATEST_OPTIONAL_TAG: mansuf/mangadex-downloader:latest-optional 15 | 16 | jobs: 17 | docker: 18 | name: Docker build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v3 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Build and export to Docker 31 | uses: docker/build-push-action@v5 32 | with: 33 | context: . 34 | load: true 35 | tags: ${{ env.TEST_TAG }} 36 | 37 | - name: Build and export to Docker (with optional dependencies) 38 | uses: docker/build-push-action@v5 39 | with: 40 | context: . 41 | file: Dockerfile.optional 42 | load: true 43 | tags: ${{ env.TEST_OPTIONAL_TAG }} 44 | 45 | - name: Test docker image 46 | run: | 47 | docker run --rm ${{ env.TEST_TAG }} --version 48 | 49 | - name: Test 50 | run: | 51 | docker run --rm ${{ env.TEST_OPTIONAL_TAG }} --version 52 | 53 | windows-build: 54 | name: Build app & docs (Windows) 55 | runs-on: windows-latest 56 | strategy: 57 | matrix: 58 | python-version: [ '3.10', '3.11', '3.12', '3.13' ] 59 | 60 | steps: 61 | - name: Clone repo 62 | uses: actions/checkout@v4 63 | 64 | - name: Setup python (x64) 65 | uses: actions/setup-python@v4 66 | with: 67 | python-version: ${{ matrix.python-version }} 68 | architecture: x64 69 | 70 | - name: Setup python (x86) 71 | uses: actions/setup-python@v4 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | architecture: x86 75 | 76 | - name: Install required libraries 77 | run: | 78 | py -${{ matrix.python-version }}-64 -m pip install -U pip 79 | py -${{ matrix.python-version }}-64 -m pip install -U wheel pyinstaller setuptools 80 | py -${{ matrix.python-version }}-64 -m pip install -U .[optional] 81 | py -${{ matrix.python-version }}-64 -m pip install -U .[docs] 82 | 83 | py -${{ matrix.python-version }}-32 -m pip install -U pip 84 | py -${{ matrix.python-version }}-32 -m pip install -U wheel pyinstaller setuptools 85 | py -${{ matrix.python-version }}-32 -m pip install -U .[optional] 86 | py -${{ matrix.python-version }}-32 -m pip install -U .[docs] 87 | 88 | - name: Test imports 89 | run: | 90 | # I..... have no idea for this 91 | mangadex-dl --version 92 | 93 | - name: Get python version 94 | run: | 95 | $PythonVersion = (python --version) 96 | Write-Output "python_version=${PythonVersion}" | Out-File -FilePath $env:GITHUB_ENV -Append 97 | 98 | # Build mangadex-downloader with PyInstaller 99 | # only allow python 3.13 100 | 101 | - name: Compile script 102 | if: ${{ contains(env.python_version, '3.13') }} 103 | run: | 104 | py -${{ matrix.python-version }}-64 -m PyInstaller "mangadex-dl_x64.spec" --distpath "./dist_x64" 105 | py -${{ matrix.python-version }}-32 -m PyInstaller "mangadex-dl_x86.spec" --distpath "./dist_x86" 106 | 107 | - name: Run compiled script 108 | if: ${{ contains(env.python_version, '3.13') }} 109 | run: | 110 | & ".\dist_x64\mangadex-dl_x64\mangadex-dl_x64.exe" --version 111 | & ".\dist_x86\mangadex-dl_x86\mangadex-dl_x86.exe" --version 112 | 113 | - name: Cleanup build 114 | if: contains(env.python_version, '3.13') 115 | run: | 116 | # x86 executable 117 | copy "LICENSE" "dist_x86\mangadex-dl_x86" 118 | copy "README.md" "dist_x86\mangadex-dl_x86" 119 | copy "docs\changelog.md" "dist_x86\mangadex-dl_x86" 120 | echo "mangadex-dl.exe --update" | Out-File -FilePath "dist_x86\mangadex-dl_x86\update.bat" 121 | echo "start cmd" | Out-File -FilePath "dist_x86\mangadex-dl_x86\start cmd.bat" 122 | Rename-Item -Path "dist_x86\mangadex-dl_x86\mangadex-dl_x86.exe" -NewName "mangadex-dl.exe" 123 | Rename-Item -Path "dist_x86\mangadex-dl_x86" -NewName "mangadex-dl" 124 | Compress-Archive -Path "dist_x86\mangadex-dl" -DestinationPath "mangadex-dl_x86.zip" 125 | 126 | # x64 executable 127 | copy "LICENSE" "dist_x64\mangadex-dl_x64" 128 | copy "README.md" "dist_x64\mangadex-dl_x64" 129 | copy "docs\changelog.md" "dist_x64\mangadex-dl_x64" 130 | echo "mangadex-dl.exe --update" | Out-File -FilePath "dist_x64\mangadex-dl_x64\update.bat" 131 | echo "start cmd" | Out-File -FilePath "dist_x64\mangadex-dl_x64\start cmd.bat" 132 | Rename-Item -Path "dist_x64\mangadex-dl_x64\mangadex-dl_x64.exe" -NewName "mangadex-dl.exe" 133 | Rename-Item -Path "dist_x64\mangadex-dl_x64" -NewName "mangadex-dl" 134 | Compress-Archive -Path "dist_x64\mangadex-dl" -DestinationPath "mangadex-dl_x64.zip" 135 | 136 | - name: Upload artifact (x64) 137 | if: contains(env.python_version, '3.13') 138 | uses: actions/upload-artifact@v4 139 | with: 140 | name: mangadex-dl_x64 141 | path: dist_x64/mangadex-dl/ 142 | 143 | - name: Upload artifact (x86) 144 | if: contains(env.python_version, '3.13') 145 | uses: actions/upload-artifact@v4 146 | with: 147 | name: mangadex-dl_x86 148 | path: dist_x86/mangadex-dl/ 149 | 150 | # Only build docs in Python 3.13 151 | 152 | - name: Build docs 153 | if: contains(env.python_version, '3.13') 154 | run: | 155 | cd docs 156 | sphinx-build -M "html" "." "_build" 157 | 158 | 159 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | push: 13 | tags: 14 | - v* 15 | 16 | jobs: 17 | deploy: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.x' 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install build 31 | - name: Build package 32 | run: python -m build 33 | - name: Publish package 34 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 35 | with: 36 | user: __token__ 37 | password: ${{ secrets.PYPI_API_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | test*.py 3 | mangadex_downloader.egg-info/* 4 | docs/_build 5 | *.code-workspace 6 | bugs.txt 7 | ideas.txt 8 | *.db 9 | env 10 | build/ 11 | dist/ 12 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | builder: html 12 | 13 | # Optionally build your docs in additional formats such as PDF 14 | formats: 15 | - pdf 16 | 17 | # Set the OS, Python version and other tools you might need 18 | build: 19 | os: ubuntu-24.04 20 | tools: 21 | python: "3.12" 22 | 23 | # Optionally set the version of Python and requirements required to build your docs 24 | python: 25 | install: 26 | - method: pip 27 | path: . 28 | extra_requirements: 29 | - docs -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python Debugger: Current File", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "program": "test5.py", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruff.configuration": "${workspaceFolder}/ruff.toml", 3 | "[python]": { 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "ms-python.black-formatter", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "explicit" 8 | } 9 | }, 10 | "python.formatting.provider": "none" 11 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome. Make sure you follow [github community guidelines](https://docs.github.com/articles/github-community-guidelines) before opening a issue or pull request. 4 | 5 | ## Reporting a bug 6 | 7 | Spotted a bug in the app ? You can report it to [issue tracker](https://github.com/mansuf/mangadex-downloader/issues). 8 | 9 | Make sure you provide detailed information like: 10 | 11 | - App version info (you can get it from command `mangadex-dl --version`) 12 | - Snippet command (must be reproducible) 13 | - Installation origin (PyPI, [github releases](https://github.com/mansuf/mangadex-downloader/releases), `git clone` thing) 14 | 15 | ## Code contributing 16 | 17 | Want to add new features ? fix a bug ? made changes to help the app better and faster ? Fork the repository and [send the Pull Request](https://github.com/mansuf/mangadex-downloader/pulls). 18 | 19 | **NOTE:** Before sending a pull request, you have to make sure the code that you're writing are compatible with Python 3.10. 20 | Because minimum Python version for developing this app are 3.10 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-alpine 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | # Setup dependencies 7 | RUN apk add --no-cache jpeg-dev zlib-dev build-base python3-dev freetype-dev 8 | 9 | # Install mangadex-downloader 10 | RUN pip install --upgrade pip 11 | RUN pip install . 12 | 13 | WORKDIR /downloads 14 | 15 | ENTRYPOINT [ "mangadex-downloader" ] 16 | 17 | CMD [ "--help" ] -------------------------------------------------------------------------------- /Dockerfile.optional: -------------------------------------------------------------------------------- 1 | FROM python:3.13 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | # Setup rust 7 | RUN apt update && apt install wget 8 | ENV RUSTUP_HOME=/usr/local/rustup \ 9 | CARGO_HOME=/usr/local/cargo \ 10 | PATH=/usr/local/cargo/bin:$PATH \ 11 | RUST_VERSION=1.84.0 12 | RUN wget https://sh.rustup.rs -O rustup.sh 13 | RUN chmod +x ./rustup.sh 14 | RUN ./rustup.sh -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION 15 | RUN chmod -R a+w $RUSTUP_HOME $CARGO_HOME; 16 | RUN rustup --version; 17 | RUN cargo --version; 18 | RUN rustc --version; 19 | 20 | # Install mangadex-downloader 21 | RUN pip install --upgrade pip 22 | RUN pip install .[optional] 23 | 24 | WORKDIR /downloads 25 | 26 | ENTRYPOINT [ "mangadex-downloader" ] 27 | 28 | CMD [ "--help" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present Rahman Yusuf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | include requirements-docs.txt 5 | include requirements-optional.txt 6 | include mangadex_downloader/images/* 7 | recursive-include mangadex_downloader/fonts * 8 | recursive-include mangadex_downloader/tracker/sql_files * -------------------------------------------------------------------------------- /README.id.md: -------------------------------------------------------------------------------- 1 | [![pypi-total-downloads](https://img.shields.io/pypi/dm/mangadex-downloader?label=DOWNLOADS&style=for-the-badge)](https://pypi.org/project/mangadex-downloader) 2 | [![python-ver](https://img.shields.io/pypi/pyversions/mangadex-downloader?style=for-the-badge)](https://pypi.org/project/mangadex-downloader) 3 | [![pypi-release-ver](https://img.shields.io/pypi/v/mangadex-downloader?style=for-the-badge)](https://pypi.org/project/mangadex-downloader) 4 | 5 | # mangadex-downloader 6 | 7 | Sebuah alat antarmuka baris perintah untuk mengunduh manga dari [MangaDex](https://mangadex.org/), 8 | ditulis di bahasa [Python](https://www.python.org/) 9 | 10 | ## Daftar isi 11 | 12 | - [Fitur utama](#fitur-utama) 13 | - [Format yang didukung](#format-yang-didukung) 14 | - [Instalasi](#instalasi) 15 | - [Python Package Index (PyPI)](#instalasi-pypi) 16 | - [Satu paket aplikasi](#instalasi-satu-paket-aplikasi) 17 | - [Versi perkembangan](#instalasi-versi-perkembangan) 18 | - [Pemakaian](#pemakaian) 19 | - [Versi PyPI](#pemakaian-versi-pypi) 20 | - [Versi satu paket aplikasi](#pemakaian-versi-satu-paket-aplikasi) 21 | - [Berkontribusi](#berkontribusi) 22 | - [Donasi](#donasi) 23 | - [Daftar tautan](#tautan) 24 | - [Penafian (disclaimer)](#penafian) 25 | 26 | ## Fitur utama 27 | 28 | - Mengunduh manga, bab, atau daftar manga langung dari MangaDex 29 | - Mengunduh manga atau daftar manga dari pustaka pengguna 30 | - Cari dan unduh tautan MangaDex dari forums MangaDex ([https://forums.mangadex.org/](https://forums.mangadex.org/)) 31 | - Mendukung batch download 32 | - Mendukung tautan lama MangaDex 33 | - Mendukung penyaringan grup scanlation 34 | - Mendukung autentikasi 35 | - Kendalikan berapa banyak bab dan lembar yang anda ingin unduh 36 | - Mendukung gambar yang terkompresi 37 | - Mendukung HTTP / SOCKS proxy 38 | - Mendukung DNS-over-HTTPS 39 | - Mendukung banyak bahasa 40 | - Simpan dalam gambar, EPUB, PDF, Comic Book Archive (.cbz atau .cb7) 41 | 42 | ***Dan kemampuan untuk tidak mengunduh bab oneshot*** 43 | 44 | ## Format yang didukung 45 | 46 | [Baca disini](https://mangadex-dl.mansuf.link/en/latest/formats.html) untuk informasi lebih lanjut. 47 | 48 | ## Instalasi 49 | 50 | Berikut aplikasi yang anda butuhkan: 51 | 52 | - Python versi 3.10.x atau keatas dengan Pip (Jika OS anda adalah Windows, anda bisa mengunduh satu paket aplikasi. 53 | [Lihat instruksi berikut untuk memasangnya](#instalasi-satu-paket-aplikasi)) 54 | 55 | ### Python Package Index (PyPI) 56 | 57 | Menginstalasi mangadex-downloader sangat mudah asalkan anda mempunyai aplikasi yang dibutuhkan 58 | 59 | ```shell 60 | # Untuk Windows 61 | py -3 -m pip install mangadex-downloader 62 | 63 | # Untuk Linux / Mac OS 64 | python3 -m pip install mangadex-downloader 65 | ``` 66 | 67 | Anda juga bisa menginstal dependensi opsional 68 | 69 | - [py7zr](https://pypi.org/project/py7zr/) untuk dukungan cb7 70 | - [orjson](https://pypi.org/project/orjson/) untuk performa maksimal (cepat JSON modul) 71 | - [lxml](https://pypi.org/project/lxml/) untuk dukungan EPUB 72 | 73 | Atau anda bisa menginstal semua opsional dependensi 74 | 75 | ```shell 76 | # Untuk Windows 77 | py -3 -m pip install mangadex-downloader[optional] 78 | 79 | # Untuk Mac OS / Linux 80 | python3 -m pip install mangadex-downloader[optional] 81 | ``` 82 | 83 | Sudah selesai deh, gampang kan ? 84 | 85 | ### Satu paket aplikasi 86 | 87 | **Catatan:** Instalasi ini hanya untuk OS Windows saja. 88 | 89 | Karena ini satu paket aplikasi, Python tidak harus diinstal 90 | 91 | Langkah-langkah: 92 | 93 | - Unduh versi terbaru disini -> [https://github.com/mansuf/mangadex-downloader/releases](https://github.com/mansuf/mangadex-downloader/releases) 94 | - Ekstrak hasil unduhan tersebut 95 | - Selamat, anda telah berhasil menginstal mangadex-downloader. 96 | [Lihat instruksi berikut untuk menjalankan mangadex-downloader](#usage-bundled-executable-version) 97 | 98 | ### Versi perkembangan 99 | 100 | **Catatan:** Anda harus mempunyai git. Jika anda tidak mempunyainya, instal dari sini [https://git-scm.com/](https://git-scm.com/) 101 | 102 | ```shell 103 | git clone https://github.com/mansuf/mangadex-downloader.git 104 | cd mangadex-downloader 105 | python setup.py install # atau "pip install ." 106 | ``` 107 | 108 | ## Pemakaian 109 | 110 | ### Versi PyPI 111 | 112 | ```shell 113 | 114 | mangadex-dl "Masukan tautan MangaDex disini" 115 | # atau 116 | mangadex-downloader "Masukan tautan MangaDex disini" 117 | 118 | # Gunakan ini jika "mangadex-dl" atau "mangadex-downloader" tidak bekerja 119 | 120 | # Untuk Windows 121 | py -3 -m mangadex_downloader "Masukan tautan MangaDex disini" 122 | 123 | # Untuk Linux / Mac OS 124 | python3 -m mangadex_downloader "Masukan tautan MangaDex disini" 125 | ``` 126 | 127 | ### Versi satu paket aplikasi 128 | 129 | - Arahkan ke tempat dimana anda mengunduh mangadex-downloader 130 | - Buka "start cmd.bat" (jangan khawatir ini bukan virus, ini akan membuka command prompt) 131 | 132 | ![example_start_cmd](https://raw.githubusercontent.com/mansuf/mangadex-downloader/main/assets/example_start_cmd.png) 133 | 134 | - Lalu kita bisa memakai mangadex-downlaoder, lihat contoh dibawah: 135 | 136 | ```shell 137 | mangadex-dl.exe "Masukan tautan MangaDex disini" 138 | ``` 139 | 140 | ![example_usage_executable](https://raw.githubusercontent.com/mansuf/mangadex-downloader/main/assets/example_usage_executable.png) 141 | 142 | Untuk lebih banyak contoh tentang pemakaian, 143 | [anda bisa baca disini](https://mangadex-dl.mansuf.link/en/stable/cli_usage/index.html) 144 | 145 | Untuk informasi lebih lanjut tentang opsi CLI, 146 | [Anda bisa baca disini](https://mangadex-dl.mansuf.link/en/stable/cli_ref/index.html) 147 | 148 | ## Berkontribusi 149 | 150 | Lihat [CONTRIBUTING.md](https://github.com/mansuf/mangadex-downloader/blob/main/CONTRIBUTING.md) untuk info lebih lanjut 151 | 152 | ## Donasi 153 | 154 | Jika anda suka dengan project ini, tolong pertimbangkan untuk donasi ke salah satu website ini: 155 | 156 | - [Sociabuzz](https://sociabuzz.com/mansuf/donate) 157 | - [Ko-fi](https://ko-fi.com/rahmanyusuf) 158 | - [Github Sponsor](https://github.com/sponsors/mansuf) 159 | 160 | Setiap jumlah yang diterima akan diapresiasi 💖 161 | 162 | ## Daftar tautan 163 | 164 | - [PyPI](https://pypi.org/project/mangadex-downloader/) 165 | - [Docs](https://mangadex-dl.mansuf.link) 166 | 167 | ## Penafian (disclaimer) 168 | 169 | mangadex-downloader tidak bersangkutan dengan MangaDex. 170 | Dan pengelola yang sekarang ([@mansuf](https://github.com/mansuf)) bukan seorang MangaDex developer -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To make a report, you can email me at security@mansuf.link 6 | 7 | Optionally, you can encrypt your report with GPG, using key [6ABF0FF73964A699](https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x6abf0ff73964a699) 8 | 9 | ``` 10 | -----BEGIN PGP PUBLIC KEY BLOCK----- 11 | Comment: Hostname: 12 | Version: Hockeypuck 2.1.0-189-g15ebf24 13 | 14 | xsFNBGO+YVgBEACxLveBcLAJ5Pp+YGx8hGw6gcdFo21v8uYAGydNR+EBx4oTO/U1 15 | XjsEFF3RIldwlMoA8fPCdi2artCJxMCxy6iA1IJsZEB8Q7RvrWGEWHMcWxhXihAg 16 | Ong/GgqMeXgd3KpHhO3whwJmxHHZi5tyTwf1xfSxdZ+5SX+1B6E7/5pNgH2dlJbb 17 | 8ICi0AjM3yeyhIIKXk/Z/oCCqn81hYHKUfnOIZsPinxrnTlaG2so8eMDJIJZK7hF 18 | xSmB0nvkGxc62rSlBMr1ma6wD8zI0IbmP0I/iyzHD+2TbkWCRVvaeYzGBwHyA63n 19 | rnqJ9O1+VjcHGKueaFgyoiw1IFYPqd1zT7Yqnyxhv/25E9g1u4i+km47XicspSJm 20 | mAQsO8SkpQAHKR3ASfCsSFnDL5M+XwIr3YFrdgZIbn8VqYX5tGUlohFQ9MevkcSc 21 | 3CygvBlxRAG2Saqyd0AjY08TEpmvbH3mLouMHh1Pk7HWXuj4YFWg/DkSwS5SX12s 22 | 4a3WK+v4AT6VgPKcH4F+3sXJ9bB/24F4b6GspXRUc4zPquLtvMVyVm6Y8kPZEiha 23 | TpSqtJU2/XHeRTfohKtXS7WCMtcIElrd6LbOqY/iuxvvf7jOmAIKOmNqAu5wTiRP 24 | i+o2cxbaWHTlRGM/LwyX8zUV8OjtHqktEfR0CoH+Yoa+eelBXY0kNg466wARAQAB 25 | zUpSYWhtYW4gWXVzdWYgKDNyZCBHUEcga2V5IGZvciBnaXRodWIgc2lnbmluZyBj 26 | b21taXRzKSA8bWFuc3VmQG1hbnN1Zi5saW5rPsLBlwQTAQgAQRYhBDpRc6WjBhWY 27 | Zxl2eWq/D/c5ZKaZBQJjvmFYAhsDBQkJZgGABQsJCAcCAiICBhUKCQgLAgQWAgMB 28 | Ah4HAheAAAoJEGq/D/c5ZKaZmrQQAIBZos/RTfGUj6D+lGpp8j+eD/2CwpwDCxGa 29 | 1pg6yxS34kDSwVRQ0taon1s4haYA12ETlcsxPTv8JLJWSBO6WqxFr+J0Wse2fsDd 30 | eLEXdg0rVbNr5VnTpn0NV6A6vn0dwVJHC0muXhgM+pJfNxfIOs/3D6LvCsHizsyE 31 | ZwJmX7Sb/+BOyEKtfvBTMoS9EpWkqueCThU4wkp77iFVfPek/WJmNdMunHDvtHRN 32 | TOzyiAAEqjreRFsoEyC/SEBUZInazVU7DhiTDAhn3ijh+yiYx4N4MPUU7cOKt8KU 33 | DjFUcMPvUlZOWPX7G97eJi1oRpiLhoUcbXxt4j+6BiGGD/zd62HOsGOCe/lEnGtq 34 | 6GvXeeqaMC/d4QuWlsLZyXrPIl3KGmbNdPELH5iToM+60s04WaPnKroSDupjvGcJ 35 | o5yAQeYcPf0cNM4qKr6JllAzExgmkUedl7pKqvOSdE4yo2ap5DV3SX7o+VseCePe 36 | ZyFn4kmZ29JTcclRKerlGbf+UlLev+vwFS5miAEXO3HXoN20NVspSoJPpWhgS+78 37 | QcssyrJy7FZAoerJPAJd+tpvS8Gpi1DFLedlEi5OOTmqU4dXqLsqrhi4cX6X3yrb 38 | hg7M4z1SY8NMysWIs9oOtS8gfFV3WbEaxUKmtnT07ih1aj9sD9UOyz3fPHKykhkf 39 | Im/7id2tzsFNBGO+YVgBEACyx+uElnZF3T95fZhw3AWBMvnCEFPFAZTJt204bNIp 40 | vycxgjPKH0KSohPNeEVIGptpSvPAvMEfXK1ufEtMXS8zU347H0k/wAX5TMcpp3vS 41 | CFbJf5tDZCSUj/HmqHMua+mN6rXOKKlLNkhf0gBgkzFJbfIoPsKwTv5vJqZoX3Xw 42 | i2McWBbUKlOk/Vc0VxYX543oLcGmcNvhQbpKf45ytngEuryWFQd1PUP1I9C3mFgf 43 | T8ltBbxoY2EEPaMp/rPNSZqWtb+tZjPQEN03NhYITOXJQchZdih1hBX6WtLX162v 44 | Rtc56KG2EbhANdtqna9/OMOUm0DUKSicF9gppBfMPJdoog+gwnwUbaoODDm2D1da 45 | 3itF4ps8CwZBK4TfHlm61y32nl+TVZwJAxFF3+oVn7CmHEYsMlS9mOnXBc3gcSEW 46 | T4GFK4o51hFsGBlHSAJCUF/2GpNdmOXGLvRbGQ3JgtQdTuNFXxSEjzsbBXjVkwg3 47 | QiGQIzUQhv4QyN+pv2E8GuCvEFC3lTLLMprNGjEux6KvWjBYnwJ95GT6RQDmeche 48 | wmhaFuBMcrqrNK4iVWJ91PGC5x9NWFv9nnsJIVL0IrWCzyUEo/V1zsiVxNvDbJCu 49 | t5pGzMov2s9Zr8hGvM6ZCCzOTCbuSAD7OsF5Zp6IRTjaBBC9Ryn3lcu3W0kdAsQW 50 | 3wARAQABwsF8BBgBCAAmFiEEOlFzpaMGFZhnGXZ5ar8P9zlkppkFAmO+YVgCGwwF 51 | CQlmAYAACgkQar8P9zlkpplmRQ/+J5+yT3NbnB4X4bo92qY9RWiLz0uAyXaARIg1 52 | lBiLg6mjO3I4e06EVzRPjENYs9Wo0savNLgqHsx2jmlVAz147YJCoUkxQnybrddx 53 | zUNahm4nbmEAciP8nbrKjHvyhdKMfv7StCXTeJrBFH8q4UEgaCqCctN5vfiLVa/A 54 | NKBm6njCGo+KHYk/Dbmg7YCUJZHVAAzw0Y6ZFKrlOOOvo7bQ/xgi2Y2JVGxSomN8 55 | OGxbwtM5ryzJ7SLrQ8MpCsc6bfsg2igDlh1b2AzNPyt50yWpbFnVVtPaVuzu4Yk/ 56 | vooZt7kD7NnLHl5GVF6dx+SdRh84k7hANvcM/gQ6DiAHdqS3UZgDEU4IO+ZQS6ZC 57 | PFtq4VhSKfpnQ+xeWLgNtpq/ocKJ8xNBgGtVaAMfg5Auwvco4QBine3xpolC3FUP 58 | aqp5BVzeeTmOKqa5J5B6OxKV18DOUf0eWPzZaxxmJi7evKl+xSnIVRWW0VquK9gE 59 | vSTHOyBlyyMtfx+LAomYBh5kO/3Mf+wk3PxGhSUCvQUK3QTSebcQnHB9sqaaSxpQ 60 | ZY2D3nTzBFYDpViM0DegWHqNmLwptUwv21sWTGNZHBsl+csbohcSlrFVL/3JfimQ 61 | MV0gHsQ357XyFy+5u9sRKpOxgfAEam0b7YNCq8ZmB/jZhs+H0d0tO6F52UcssUsB 62 | bAmWc6A= 63 | =dgk/ 64 | -----END PGP PUBLIC KEY BLOCK----- 65 | ``` 66 | -------------------------------------------------------------------------------- /assets/example_start_cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/assets/example_start_cmd.png -------------------------------------------------------------------------------- /assets/example_usage_executable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/assets/example_usage_executable.png -------------------------------------------------------------------------------- /compile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from subprocess import run 3 | 4 | output_one_folder = 'mangadex-dl' 5 | 6 | run([ 7 | 'pyinstaller', 8 | 'run.py', 9 | '-n', 10 | output_one_folder 11 | ]) -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | test-build: 16 | @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 17 | @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 18 | sudo cp -r "./_build/html" "/var/www" 19 | 20 | .PHONY: help test-build Makefile 21 | 22 | # Catch-all target: route all unknown targets to Sphinx using the new 23 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 24 | %: Makefile 25 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 26 | -------------------------------------------------------------------------------- /docs/cli_ref/app_names.md: -------------------------------------------------------------------------------- 1 | # Application names 2 | 3 | ## For PyPI users 4 | 5 | - `mangadex-dl` 6 | - `mangadex-downloader` 7 | 8 | ````{note} 9 | If none of above doesn't work use this 10 | 11 | ```shell 12 | # For Windows 13 | py -3 -m mangadex_downloader 14 | 15 | # For Linux 16 | python3 -m mangadex_downloader 17 | ``` 18 | ```` 19 | 20 | ## For bundled executable users 21 | 22 | It depend to the filename actually, 23 | by default the executable is named `mangdex-dl.exe`. 24 | 25 | You can execute the application without ".exe". For example: 26 | 27 | ```sh 28 | # With .exe 29 | mangadex-dl.exe "insert manga url here" 30 | 31 | # Without .exe 32 | mangadex-dl "insert manga url here" 33 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/auth_cache.md: -------------------------------------------------------------------------------- 1 | # Authentication cache 2 | 3 | Reuse authentication tokens for later use. These tokens stored in same place as [config](./config) is stored. 4 | You don't have to use `--login` again with this, just run the app and you will be automatically logged in. 5 | 6 | ```{warning} 7 | You must enable config in order to use authentication cache. 8 | ``` 9 | 10 | ## Syntax command 11 | 12 | ```shell 13 | mangadex-dl "login_cache:" 14 | ``` 15 | 16 | ## Available sub commands 17 | 18 | ```{option} purge 19 | Invalidate and purge cached authentication tokens 20 | ``` 21 | 22 | ```{option} show 23 | Show expiration time cached authentication tokens 24 | ``` 25 | 26 | ````{option} show_unsafe 27 | ```{warning} 28 | You should not use this command, 29 | because it exposing your auth tokens to terminal screen. 30 | Use this if you know what are you doing. 31 | ``` 32 | 33 | Show cached authentication tokens 34 | ```` 35 | 36 | ## Example usage commands 37 | 38 | ### Enable authentication cache 39 | 40 | ```shell 41 | mangadex-dl "conf:login_cache=1" 42 | 43 | # You must login first in order to get cached 44 | mangadex-dl "tamamo no koi" --login -s 45 | 46 | # After that you won't need to use --login anymore 47 | mangadex-dl "another manga lmao" -s 48 | ``` 49 | 50 | ### Invalidate and purge cached authentication tokens 51 | 52 | ```shell 53 | mangadex-dl "login_cache:purge" 54 | ``` 55 | 56 | ### Show expiration time session token and refresh token 57 | 58 | ```shell 59 | mangadex-dl "login_cache:show" 60 | 61 | # or 62 | 63 | mangadex-dl "login_cache" 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/cli_ref/chapter_info.md: -------------------------------------------------------------------------------- 1 | # Chapter info (cover) 2 | 3 | ![chapter info](../images/chapter_info.png) 4 | 5 | This is called chapter info (some people would call this "cover"). 6 | This gives you information what chapter currently you reading on. 7 | 8 | This applied to this formats 9 | 10 | - `raw-volume` 11 | - `raw-single` 12 | - `cbz-volume` 13 | - `cbz-single` 14 | - `cb7-volume` 15 | - `cb7-single` 16 | - `pdf-volume` 17 | - `pdf-single` 18 | 19 | ```{note} 20 | any `epub` formats doesn't create chapter info, 21 | because obviously there is "Table of Contents" feature in EPUB (if an e-reader support it). 22 | ``` 23 | 24 | You can enable chapter info creation by giving `--use-chapter-cover` or `-ucc` to the app 25 | 26 | ```shell 27 | mangadex-dl "URL" -f pdf-volume --use-chapter-cover 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/cli_ref/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Here is a list of available commands that you can execute in mangadex-downloader. 4 | Most of them are for to show and download manga or lists. 5 | This command can be executed through `URL` parameter, see syntax below 6 | 7 | ## Syntax 8 | 9 | ```sh 10 | # Command without argument 11 | mangadex-dl "command" 12 | 13 | # Command with argument 14 | mangadex-dl "command:arg" 15 | 16 | # Command with multiple arguments 17 | mangadex-dl "command:arg1, arg2, arg3" 18 | ``` 19 | 20 | ## Available commands 21 | 22 | ```{option} random 23 | Show 5 of random manga and select to download 24 | 25 | For more information, see {doc}`./random` 26 | ``` 27 | 28 | ````{option} library STATUS 29 | ```{note} 30 | This command require authentication 31 | ``` 32 | Show list of saved manga from logged in user 33 | 34 | For more information, see {doc}`manga_library` 35 | ```` 36 | 37 | ````{option} list USER-ID 38 | ```{note} 39 | Argument `USER-ID` are optional. 40 | You must login if you didn't use `USER-ID` argument 41 | ``` 42 | Show list of saved MDLists from logged in user 43 | 44 | For more info, see {doc}`./list_library` 45 | ```` 46 | 47 | ````{option} followed-list 48 | ```{note} 49 | This command require authentication 50 | ``` 51 | Show list of followed MDLists from logged in user 52 | 53 | For more info, see {doc}`./follow_list_library` 54 | ```` 55 | 56 | ```{option} group GROUP-ID 57 | Show and download list of manga from a group that have uploaded scanlated chapters 58 | ``` 59 | 60 | ````{option} file PATH_TO_FILE 61 | ```{note} 62 | Path file can be offline or online location. 63 | If you're using file from online location, it only support HTTP(s) method. 64 | ``` 65 | Download list of manga, chapters or lists from a file 66 | 67 | For more info, see {doc}`./file_command` 68 | ```` 69 | 70 | ````{option} seasonal SEASON 71 | ```{note} 72 | Argument `SEASON` are optional 73 | ``` 74 | Select and download seasonal manga 75 | 76 | For more info, see {doc}`./seasonal_manga` 77 | ```` 78 | 79 | ```{option} conf CONFIG_KEY=CONFIG_VALUE 80 | Modify or show config 81 | 82 | For more info, see {doc}`./config` 83 | ``` 84 | 85 | ```{option} login_cache SUBCOMMAND 86 | Modify or show cached authentication tokens expiration time 87 | 88 | For more info, see {doc}`./auth_cache` 89 | ``` 90 | 91 | ## Example usage 92 | 93 | ### Random manga command 94 | 95 | ```sh 96 | mangadex-dl "random" 97 | ``` 98 | 99 | ### File command 100 | 101 | ```sh 102 | # Offline location 103 | mangadex-dl "file:/home/user/mymanga/urls.txt" 104 | 105 | # Online location 106 | mangadex-dl "file:https://raw.githubusercontent.com/mansuf/md-test-urls/main/urls.txt" 107 | ``` 108 | 109 | ### Modify and show configs 110 | 111 | ```sh 112 | # Show all configs 113 | mangadex-dl "conf" 114 | 115 | # Show `save_as` config value 116 | mangadex-dl "conf:save_as" 117 | 118 | # Change `dns_over_https` config value 119 | mangadex-dl "conf:dns_over_https=google" 120 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/cover.md: -------------------------------------------------------------------------------- 1 | # Cover command 2 | 3 | This command will show list of covers which you can choose and download it. 4 | 5 | ```{note} 6 | This command will download covers only. The manga is not downloaded at all. 7 | The covers will be stored in folder under manga title name (ex: "Official "Test" Manga") 8 | ``` 9 | 10 | ## Syntax 11 | 12 | It support following values: 13 | 14 | - Full manga URL (https://mangadex.org/title/...) 15 | - Full cover manga URL (https://mangadex.org/covers/...) 16 | - Manga id only (f9c33607-9180-4ba6-b85c-e4b5faee7192) 17 | 18 | ### Original quality 19 | 20 | ```sh 21 | mangadex-dl "cover:manga_id_or_full_url" 22 | ``` 23 | 24 | ### 512px quality 25 | 26 | ```sh 27 | mangadex-dl "cover-512px:manga_id_or_full_url" 28 | ``` 29 | 30 | ### 256px quality 31 | 32 | ```sh 33 | mangadex-dl "cover-256px:manga_id_or_full_url" 34 | ``` 35 | 36 | ## Example usage 37 | 38 | ```sh 39 | # Original quality (manga id only) 40 | mangadex-dl "cover:f9c33607-9180-4ba6-b85c-e4b5faee7192" 41 | 42 | # Original quality (full manga URL) 43 | mangadex-dl "cover:https://mangadex.org/title/f9c33607-9180-4ba6-b85c-e4b5faee7192/official-test-manga" 44 | 45 | # Original quality (full cover manga URL) 46 | mangadex-dl "cover:https://mangadex.org/covers/f9c33607-9180-4ba6-b85c-e4b5faee7192/c18da525-e34f-4128-a696-4477b6ce6827.png" 47 | 48 | 49 | # 512px quality (manga id only) 50 | mangadex-dl "cover-512px:f9c33607-9180-4ba6-b85c-e4b5faee7192" 51 | 52 | # 512px quality (full manga URL) 53 | mangadex-dl "cover-512px:https://mangadex.org/title/f9c33607-9180-4ba6-b85c-e4b5faee7192/official-test-manga" 54 | 55 | # 512px quality (full cover manga URL) 56 | mangadex-dl "cover-512px:https://mangadex.org/covers/f9c33607-9180-4ba6-b85c-e4b5faee7192/c18da525-e34f-4128-a696-4477b6ce6827.png" 57 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/download_tracker.md: -------------------------------------------------------------------------------- 1 | # Download tracker 2 | 3 | Every time you download a manga, chapter or list. 4 | The application will write `download.db` file into manga folder. 5 | But what does it do ? does it dangerous ? the file seems suspicious. 6 | 7 | Worry not, the file is not dangerous. It's called download tracker, 8 | it will track chapters and images every time you download. 9 | So next time you run the application, it will check what chapters has been downloaded 10 | and if the application found chapters that has not been downloaded yet, 11 | the application will download them all. 12 | 13 | Download tracker is designed to avoid rate-limit frequently from MangaDex API on some formats. 14 | Also it check latest chapters on any `volume` and `single` formats. 15 | So let's say you already downloaded `Volume 1`, but there is new chapter on `Volume 1`. 16 | The application will re-download `Volume 1`. 17 | 18 | ## Verify downloaded chapters and images 19 | 20 | The download tracker can verify downloaded chapters and images on all formats. 21 | Previously, the application only verify images which only available to `raw` formats (raw, raw-volume, raw-single). 22 | With this, the application will know what chapters and images is corrupted or missing 23 | and will re-download the corrupted or missing chapters and images. 24 | 25 | ## Cool features, is there a way to turn it off ? 26 | 27 | You can, use `--no-track` to turn off download tracker feature. 28 | 29 | ```sh 30 | mangadex-dl "insert MangaDex URL here" --no-track 31 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/env_vars.md: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | 3 | ```{option} MANGADEXDL_CONFIG_ENABLED [1 or 0, true or false] 4 | Set this `1` or `true` to enable config, `0` or `false` to disable config. 5 | ``` 6 | 7 | ```{option} MANGADEXDL_CONFIG_PATH 8 | A directory to store config and authentication cache. 9 | ``` 10 | 11 | ```{option} MANGADEXDL_ZIP_COMPRESSION_TYPE 12 | Set zip compression type for any `cbz` and `epub` formats, 13 | by default it set to `stored` 14 | 15 | Must be one of: 16 | 17 | - stored 18 | - deflated 19 | - bzip2 20 | - lzma 21 | 22 | For more information, see https://docs.python.org/3/library/zipfile.html#zipfile.ZIP_STORED 23 | ``` 24 | 25 | ````{option} MANGADEXDL_ZIP_COMPRESSION_LEVEL 26 | Set zip compression level for any `cbz` and `epub` formats. 27 | 28 | ```{note} 29 | Zip compression type `stored` or `lzma` has no effect 30 | ``` 31 | 32 | levels: 33 | 34 | - deflated : 0-9 35 | - bzip2 : 1-9 36 | 37 | For more information about each levels zip compression, 38 | see https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile 39 | ```` 40 | 41 | ````{option} MANGADEXDL_GROUP_BLACKLIST [VALUE1, VALUE2, ...] 42 | Add groups to blacklist. 43 | This to prevent chapter being downloaded from blacklisted groups. 44 | 45 | Value must be file path, uuid, MangaDex url containing uuid. 46 | Multiple values is supported (separated by comma) 47 | 48 | **Example usage (from a file)** 49 | 50 | ```shell 51 | # inside of blocked_groups.txt 52 | 53 | https://mangadex.org/group/4197198b-c99b-41ae-ad21-8e6ecc10aa49/no-group-scanlation 54 | https://mangadex.org/group/0047632b-1390-493d-ad7c-ac6bb9288f05/ateteenplus 55 | https://mangadex.org/group/1715d32d-0bf0-46e2-b8ad-a64386523038/afterlife-scans 56 | ``` 57 | 58 | ``` 59 | # For Windows 60 | set MANGADEXDL_GROUP_BLACKLIST=blocked_groups.txt 61 | 62 | # For Linux / Mac OS 63 | export MANGADEXDL_GROUP_BLACKLIST=blocked_groups.txt 64 | ``` 65 | 66 | **Example usage (uuid)** 67 | 68 | ```shell 69 | # For Windows 70 | set MANGADEXDL_GROUP_BLACKLIST=4197198b-c99b-41ae-ad21-8e6ecc10aa49, 0047632b-1390-493d-ad7c-ac6bb9288f05 71 | 72 | # For Linux / Mac OS 73 | export MANGADEXDL_GROUP_BLACKLIST=4197198b-c99b-41ae-ad21-8e6ecc10aa49, 0047632b-1390-493d-ad7c-ac6bb9288f05 74 | ``` 75 | ```` 76 | 77 | ````{option} MANGADEXDL_USER_BLACKLIST [VALUE1, VALUE2, ...] 78 | Add users to blacklist. 79 | This to prevent chapter being downloaded from blacklisted users. 80 | 81 | ```{note} 82 | Group blacklisting takes priority over user blacklisting 83 | ``` 84 | 85 | Value must be file path, uuid, MangaDex url containing uuid. 86 | Multiple values is supported (separated by comma) 87 | 88 | **Example usage (from a file)** 89 | 90 | ```shell 91 | # inside of blocked_users.txt 92 | 93 | https://mangadex.org/user/f8cc4f8a-e596-4618-ab05-ef6572980bbf/tristan9 94 | https://mangadex.org/user/81304b72-005d-4e62-bea6-4cb65869f7da/bravedude8 95 | ``` 96 | 97 | ``` 98 | # For Windows 99 | set MANGADEXDL_USER_BLACKLIST=blocked_users.txt 100 | 101 | # For Linux / Mac OS 102 | export MANGADEXDL_USER_BLACKLIST=blocked_users.txt 103 | ``` 104 | 105 | **Example usage (uuid)** 106 | 107 | ```shell 108 | # For Windows 109 | set MANGADEXDL_USER_BLACKLIST=1c4d814e-b1c1-4b75-8a69-f181bb4e57a9, f8cc4f8a-e596-4618-ab05-ef6572980bbf 110 | 111 | # For Linux / Mac OS 112 | export MANGADEXDL_USER_BLACKLIST=1c4d814e-b1c1-4b75-8a69-f181bb4e57a9, f8cc4f8a-e596-4618-ab05-ef6572980bbf 113 | ``` 114 | ```` 115 | 116 | ````{option} MANGADEXDL_TAGS_BLACKLIST [VALUE1, VALUE2, ...] 117 | Add tags to blacklist. 118 | This to prevent manga being downloaded if it's contain one or more blacklisted tags. 119 | 120 | Value must be file path, keyword, uuid, MangaDex url containing uuid. 121 | Multiple values is supported (separated by comma) 122 | 123 | **Example usage (from a file)** 124 | 125 | ```shell 126 | # inside of blocked_tags.txt 127 | 128 | boys' love 129 | girls' love 130 | https://mangadex.org/tag/b29d6a3d-1569-4e7a-8caf-7557bc92cd5d/gore 131 | ``` 132 | 133 | ``` 134 | # For Windows 135 | set MANGADEXDL_TAGS_BLACKLIST=blocked_tags.txt 136 | 137 | # For Linux / Mac OS 138 | export MANGADEXDL_TAGS_BLACKLIST=blocked_tags.txt 139 | ``` 140 | 141 | **Example usage (keyword)** 142 | 143 | ```shell 144 | # For Windows 145 | set MANGADEXDL_TAGS_BLACKLIST=gore, girls' love 146 | 147 | # For Linux / Mac OS 148 | export MANGADEXDL_TAGS_BLACKLIST=gore, girls' love 149 | ``` 150 | 151 | **Example usage (uuid)** 152 | 153 | ```shell 154 | # For Windows 155 | set MANGADEXDL_TAGS_BLACKLIST=b29d6a3d-1569-4e7a-8caf-7557bc92cd5d, a3c67850-4684-404e-9b7f-c69850ee5da6 156 | 157 | # For Linux / Mac OS 158 | export MANGADEXDL_TAGS_BLACKLIST=b29d6a3d-1569-4e7a-8caf-7557bc92cd5d, a3c67850-4684-404e-9b7f-c69850ee5da6 159 | ``` 160 | ```` -------------------------------------------------------------------------------- /docs/cli_ref/file_command.md: -------------------------------------------------------------------------------- 1 | # File command (batch download command) 2 | 3 | ## Syntax 4 | 5 | ```shell 6 | mangadex-dl "file:" 7 | ``` 8 | 9 | ## Arguments 10 | 11 | ```{option} location 12 | A valid local or web (http, https) file location 13 | ``` 14 | 15 | ## Example usage 16 | 17 | ### Batch download from local file 18 | 19 | ```shell 20 | mangadex-dl "file:/etc/my-manga/lists-urls.txt" 21 | ``` 22 | 23 | ### Batch download from web URL 24 | 25 | ```shell 26 | mangadex-dl "file:https://raw.githubusercontent.com/mansuf/md-test-urls/main/urls.txt" 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/cli_ref/filters.md: -------------------------------------------------------------------------------- 1 | # Filters 2 | 3 | Currently filters can be used in: 4 | 5 | - Search manga (`mangadex-dl "title manga" -s`) 6 | - Random manga (`mangadex-dl "random"`) 7 | 8 | ## Syntax 9 | 10 | It's accessible from `-ft` or `--filter` option 11 | 12 | ```shell 13 | mangadex-dl -s -ft "KEY=VALUE" 14 | ``` 15 | 16 | It also support multiple values separated by commas 17 | 18 | ```shell 19 | mangadex-dl "random" -ft "KEY=VALUE1,VALUE2,VALUE3" 20 | ``` 21 | 22 | ```{note} 23 | random manga has limited filters, here a list of available filters for random manga. 24 | 25 | - content_rating 26 | - included_tags 27 | - included_tags_mode 28 | - excluded_tags 29 | - excluded_tags_mode 30 | 31 | ``` 32 | 33 | ## Available filters 34 | 35 | ```{option} authors [VALUE1, VALUE2, ...] 36 | Authors of manga 37 | 38 | Value must be valid uuid or MangaDex url containing uuid. 39 | ``` 40 | 41 | ```{option} artists [VALUE1, VALUE2, ...] 42 | Artists of manga 43 | 44 | Value must be valid uuid or MangaDex url containing uuid. 45 | ``` 46 | 47 | ```{option} author_or_artist [VALUE] 48 | An Author OR an Artist within Manga 49 | 50 | Value must be valid uuid or MangaDex url containing uuid. 51 | ``` 52 | 53 | ```{option} year [INTEGER] 54 | Year of release 55 | ``` 56 | 57 | ```{option} included_tags [VALUE1, VALUE2, ...] 58 | Value must be valid keyword or uuid or MangaDex url containing uuid. 59 | To see all available tags in MangaDex -> https://mangadex.org/tag/ 60 | ``` 61 | 62 | ```{option} included_tags_mode [OR, AND] 63 | ``` 64 | 65 | ```{option} excluded_tags [VALUE1, VALUE2, ...] 66 | Value must be valid keyword or uuid or MangaDex url containing uuid. 67 | To see all available tags in MangaDex -> https://mangadex.org/tag/ 68 | ``` 69 | 70 | ```{option} excluded_tags_mode [OR, AND] 71 | ``` 72 | 73 | ```{option} status [VALUE1, VALUE2, ...] 74 | Must be one of: 75 | 76 | - ongoing 77 | - completed 78 | - hiatus 79 | - cancelled 80 | ``` 81 | 82 | ```{option} original_language [VALUE1, VALUE2, ...] 83 | Must be one of valid languages returned from `mangadex-dl --list-languages` 84 | ``` 85 | 86 | ```{option} excluded_original_language [VALUE1, VALUE2, ...] 87 | Must be one of valid languages returned from `mangadex-dl --list-languages` 88 | ``` 89 | 90 | ```{option} available_translated_language [VALUE1, VALUE2, ...] 91 | Must be one of valid languages returned from `mangadex-dl --list-languages` 92 | ``` 93 | 94 | ```{option} publication_demographic [VALUE1, VALUE2, ...] 95 | Must be one of: 96 | 97 | - shounen 98 | - shoujo 99 | - josei 100 | - seinen 101 | - none 102 | ``` 103 | 104 | ```{option} content_rating [VALUE1, VALUE2, ...] 105 | Must be one of: 106 | 107 | - safe 108 | - suggestive 109 | - erotica 110 | - pornographic 111 | ``` 112 | 113 | ```{option} created_at_since [DATETIME] 114 | value must matching format `%Y-%m-%dT%H:%M:%S` 115 | ``` 116 | 117 | ```{option} updated_at_since [DATETIME] 118 | value must matching format `%Y-%m-%dT%H:%M:%S` 119 | ``` 120 | 121 | ```{option} has_available_chapters [1 or 0, true or false] 122 | ``` 123 | 124 | ```{option} order[title] [asc or ascending, desc or descending] 125 | ``` 126 | 127 | ```{option} order[year] [asc or ascending, desc or descending] 128 | ``` 129 | 130 | ```{option} order[createdAt] [asc or ascending, desc or descending] 131 | ``` 132 | 133 | ```{option} order[updatedAt] [asc or ascending, desc or descending] 134 | ``` 135 | 136 | ```{option} order[latestUploadedChapter] [asc or ascending, desc or descending] 137 | ``` 138 | 139 | ```{option} order[followedCount] [asc or ascending, desc or descending] 140 | ``` 141 | 142 | ```{option} order[relevance] [asc or ascending, desc or descending] 143 | ``` 144 | 145 | ```{option} order[rating] [asc or ascending, desc or descending] 146 | ``` 147 | 148 | ## Example usage 149 | 150 | Search manga with content rating erotica and status completed 151 | 152 | ```shell 153 | mangadex-dl -s -ft "original_language=Japanese" -ft "content_rating=erotica" -ft "status=completed" 154 | ``` 155 | 156 | Search manhwa with "highest rating" order 157 | 158 | ```shell 159 | mangadex-dl -s -ft "original_language=Korean" -ft "order[rating]=descending" 160 | ``` 161 | 162 | Random manga with oneshot tags but without yuri and yaoi tags 163 | 164 | ```shell 165 | mangadex-dl "random" -ft "included_tags=oneshot" -ft "excluded_tags=boys' love, girls' love" 166 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/follow_list_library.md: -------------------------------------------------------------------------------- 1 | # Followed list library command 2 | 3 | Show all followed MangaDex lists from logged in user. You will be prompted to select which list want to download. 4 | 5 | ```{note} 6 | You must login in order to use this command. Otherwise it will not work. 7 | ``` 8 | 9 | ## Syntax 10 | 11 | ```shell 12 | mangadex-dl "followed-list" --login 13 | ``` 14 | 15 | ## Example usage 16 | 17 | ```shell 18 | # User will be prompted to select which list wants to download 19 | # And then save it as pdf format 20 | mangadex-dl "followed-list" --login --save-as pdf 21 | ``` 22 | 23 | Output 24 | 25 | ```shell 26 | =============================================== 27 | List of followed MDlist from user "..." 28 | =============================================== 29 | (1). ... 30 | (2). ... 31 | (3). ... 32 | (4). ... 33 | (5). ... 34 | (6). ... 35 | (7). ... 36 | (8). ... 37 | (9). ... 38 | 39 | type "next" to show next results 40 | type "previous" to show previous results 41 | type "preview NUMBER" to show more details about selected result. For example: "preview 2" 42 | => 43 | # .... 44 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/forums.md: -------------------------------------------------------------------------------- 1 | # Forums 2 | 3 | Imagine, you seeing a list of manga in some MangaDex forums thread and you want to download them all. 4 | Surely you copy each URLs from the thread and paste them into mangadex-downloader. 5 | You must be tired copy paste them all right ? and you wasted your time doing that. 6 | 7 | Worry not, you can download all of them directly from mangadex-downloader itself ! 8 | 9 | **Wow, it so cool. How ?** 10 | 11 | Just copy the forum thread url and paste it into mangadex-downloader 12 | 13 | ```sh 14 | mangadex-dl "https://forums.mangadex.org/threads/whats-your-top-3-manga.1082493/" 15 | ``` 16 | 17 | That's it, you will be prompted to select which manga, chapter, or list you wanna download. 18 | If you don't wanna be prompted and just wanna download them all, you can use `--input-pos` option. 19 | 20 | ```sh 21 | # "*" means all 22 | mangadex-dl "https://forums.mangadex.org/threads/whats-your-top-3-manga.1082493/" --input-pos "*" 23 | ``` 24 | 25 | ## Specific post in a forum thread 26 | 27 | mangadex-downloader can find MangaDex URLs to a specific post in forum thread 28 | if the URL containing post-id. Let me give you an example: 29 | 30 | Let's say you want to download list of manga from this post only. 31 | 32 | ![post in a forum thread](../images/post-in-forum-thread.png) 33 | 34 | Move your mouse to number sign (#10) on top right corner, 35 | right click on your mouse, copy link address and paste it to mangadex-downloader. 36 | 37 | ```sh 38 | mangadex-dl "https://forums.mangadex.org/threads/whats-your-top-3-manga.1082493/#post-16636005" 39 | ``` 40 | 41 | Notice there is `#post-16636005` at the end of URL ? 42 | Those are called post-id in MangaDex forums. 43 | mangadex-downloader will only find MangaDex URLs on that post only, not the entire thread. 44 | 45 | ## Legacy MangaDex forum thread URL 46 | 47 | You can use old MangaDex forum thread URL to mangadex-downloader. 48 | Just copy the URL, paste it and run it ! 49 | 50 | ```sh 51 | mangadex-dl "https://mangadex.org/thread/430211" 52 | ``` 53 | 54 | ## Note 55 | 56 | mangadex-downloader only shows results if the thread containing valid MangaDex urls. 57 | -------------------------------------------------------------------------------- /docs/cli_ref/index.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | ```{toctree} 4 | :glob: 5 | 6 | ./* 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/cli_ref/list_library.md: -------------------------------------------------------------------------------- 1 | # List library command 2 | 3 | Show all saved MangaDex lists from logged in user or from another user. You will be prompted to select which list want to download. 4 | 5 | ## Syntax 6 | 7 | ```shell 8 | mangadex-dl "list:" 9 | ``` 10 | 11 | If `` is given, it will show all public MangaDex lists from that user. 12 | Otherwise it will show all MangaDex lists from logged in user. 13 | 14 | You can give just the id or full URL to ``. 15 | 16 | ```{note} 17 | Authentication is required if `` is not given. 18 | ``` 19 | 20 | ## Example usage 21 | 22 | Show all MangaDex lists (private and public) from logged in user 23 | 24 | ```shell 25 | mangadex-dl "list" --login 26 | ``` 27 | 28 | Show all public MangaDex lists from another user 29 | 30 | ```shell 31 | # MangaDex lists from user "BraveDude8" (one of MangaDex moderators) 32 | mangadex-dl "list:https://mangadex.org/user/81304b72-005d-4e62-bea6-4cb65869f7da" 33 | # or 34 | mangadex-dl "list:81304b72-005d-4e62-bea6-4cb65869f7da" 35 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/log_levels.md: -------------------------------------------------------------------------------- 1 | # Logging levels 2 | 3 | mangadex-downloader are using python logging from standard library, 4 | for more information you can read it here -> https://docs.python.org/3/library/logging.html#logging-levels 5 | 6 | Logging levels are determined by numeric values. 7 | The table are showed below: 8 | 9 | | Level | Numeric value | 10 | | ----- | ------------- | 11 | | CRITICAL | 50 | 12 | | ERROR | 40 | 13 | | WARNING | 30 | 14 | | INFO | 20 | 15 | | DEBUG | 10 | 16 | | NOTSET | 0 | 17 | 18 | Example formula for logging levels: 19 | 20 | If you set logging level to WARNING (which numeric value is 30), 21 | all logs that has WARNING level and above (ERROR and CRITICAL) will be visible. 22 | 23 | Same goes for INFO and DEBUG. 24 | 25 | If you set logging level to INFO (which numeric value is 20), 26 | all logs that has INFO level and above (WARNING, ERROR, CRITICAL) will be visible. 27 | 28 | ```{note} 29 | If you set logging level to `NOTSET`, all logs will be not visible at all. 30 | ``` 31 | 32 | ## Syntax 33 | 34 | Accessible from `--log-level` option 35 | 36 | ## Example usage 37 | 38 | ```sh 39 | # DEBUG (verbose) output 40 | mangadex-dl "Insert MangaDex URL here" --log-level "DEBUG" 41 | 42 | # WARNING output 43 | mangadex-dl "Insert MangaDex URL here" --log-level "WARNING" 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/cli_ref/manga_library.md: -------------------------------------------------------------------------------- 1 | # Manga library command 2 | 3 | Show all saved mangas from logged in user. You will be prompted to select which manga want to download. 4 | 5 | ```{note} 6 | You must login in order to use this command. Otherwise it will not work. 7 | ``` 8 | 9 | ## Syntax 10 | 11 | ```shell 12 | mangadex-dl "library:" --login 13 | ``` 14 | 15 | If `` is given, it will filter manga library based on reading status. 16 | If not, then it will show all manga in the library. 17 | 18 | ## Statuses 19 | 20 | ```{option} reading 21 | ``` 22 | 23 | ```{option} on_hold 24 | ``` 25 | 26 | ```{option} plan_to_read 27 | ``` 28 | 29 | ```{option} dropped 30 | ``` 31 | 32 | ```{option} re_reading 33 | ``` 34 | 35 | ```{option} completed 36 | ``` 37 | 38 | ```{option} help 39 | Show all available statuses 40 | ``` 41 | 42 | ## Example usage 43 | 44 | ### Show all manga in user library 45 | 46 | ```shell 47 | # User will be prompted to select which manga wants to download 48 | # And then save it as pdf format 49 | mangadex-dl "library" --login --save-as pdf 50 | ``` 51 | 52 | ### Show manga with reading status "Plan to read" in user library 53 | 54 | ```shell 55 | mangadex-dl "library:plan_to_read" --login 56 | ``` -------------------------------------------------------------------------------- /docs/cli_ref/oauth.md: -------------------------------------------------------------------------------- 1 | # OAuth (New Authentication System) 2 | 3 | MangaDex are now switching to new authentication system called OAuth 2.0 and because of this 4 | the legacy authentication may be deprecated soon. 5 | 6 | ## The process 7 | 8 | Previously, when you login to mangadex-downloader you will be prompted to input 9 | username and password, then the application will send request to MangaDex server 10 | that you're trying to login into your account via mangadex-downloader. After that, 11 | MangaDex server acknowledge the request and then send the authentication tokens (access token and refresh token), 12 | this token (access token) is used to send request to restricted endpoints that require login, 13 | the token is expired within 15 minutes after you successfully logged in. However, we have another token called 14 | refresh token that is to refresh access token when it's expired. mangadex-downloader will send refresh access token request 15 | to MangaDex server using refresh token and then we get the new access token. The refresh token (to my knowledge) is expired 16 | within 1 month. So if you're using authentication cache in mangadex-downloader you still can login to the app via refresh token 17 | 18 | Now, MangaDex has this new authentication system called OAuth 2.0 19 | (if you don't know what that is, search in google or other search engine), in short it's more secure authentication system. 20 | 21 | If you're trying to login in mangadex-downloader using new authentication system, you will be prompted 4 inputs: 22 | 23 | - username 24 | - password 25 | - API Client ID 26 | - API Client Secret 27 | 28 | Example usage: 29 | 30 | ```sh 31 | mangadex-dl "URL" --login --login-method "oauth2" --login-username "username" --login-password "password" --login-api-id "API Client ID" --login-api-secret "API Client Secret" 32 | ``` 33 | 34 | ```{note} 35 | You must set `--login-method` to `oauth2` to use new authentication system, otherwise it will use legacy auth system. 36 | ``` 37 | 38 | What is this additional input `API Client ID` and `API Client Secret` ? well that is additional credential information 39 | required to login to MangaDex new authentication system. You can get it from `API Clients` section in MangaDex user settings. 40 | Text that startswith "personal-client-..." is the `API Client ID` and you can get `API Client Secret` from `Get Secret` button 41 | 42 | ![chapter info](../images/api_clients.png) 43 | 44 | ## It seems really complicated can i just enter username and password like the old days ? 45 | 46 | Well you can, and you can do it now on MangaDex website. 47 | But this feature is not implemented yet for third-party applications such as mangadex-downloader. 48 | I'm pretty sure the MangaDex devs team is working to release this feature. 49 | 50 | The process for this method is the application will open a browser visiting MangaDex url prompting you to input username and password, 51 | and then MangaDex send authentication tokens back to mangadex-downloader. -------------------------------------------------------------------------------- /docs/cli_ref/random.md: -------------------------------------------------------------------------------- 1 | # Random manga 2 | 3 | ## Syntax 4 | 5 | ```shell 6 | mangadex-dl "random" 7 | ``` 8 | 9 | With filter 10 | 11 | ```shell 12 | mangadex-dl "random" -ft "KEY=VALUE" 13 | ``` 14 | 15 | For more information about filters, see {doc}`./filters` 16 | 17 | ## Example usage 18 | 19 | ```shell 20 | mangadex-dl "random" 21 | ``` 22 | 23 | Random manga with oneshot tags 24 | 25 | ```shell 26 | mangadex-dl "random" -ft "included_tags=oneshot" 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/cli_ref/seasonal_manga.md: -------------------------------------------------------------------------------- 1 | # Seasonal manga 2 | 3 | ## Syntax 4 | 5 | ``` 6 | mangadex-dl "seasonal:" 7 | ``` 8 | 9 | If `` is given, it will show seasonal manga based on given season. 10 | Otherwise it will show current seasonal manga. 11 | 12 | If you want to see all available seasons, 13 | type `list` in `` argument 14 | 15 | ```shell 16 | mangadex-dl "seasonal:list" 17 | ``` 18 | 19 | ```{note} 20 | Current seasonal manga is retrieved from 21 | https://github.com/mansuf/mangadex-downloader/blob/main/seasonal_manga_now.txt. 22 | If you think this is out of update, 23 | please open a issue [here](https://github.com/mansuf/mangadex-downloader/issues) 24 | ``` 25 | 26 | ## Example usage 27 | 28 | Get current seasonal manga 29 | 30 | ```shell 31 | mangadex-dl "seasonal" 32 | ``` 33 | 34 | Get `Seasonal: Fall 2020` manga 35 | 36 | ```shell 37 | mangadex-dl "seasonal:fall 2020" 38 | ``` 39 | 40 | Get all available seasons 41 | 42 | ```shell 43 | mangadex-dl "seasonal:list" 44 | ``` -------------------------------------------------------------------------------- /docs/cli_usage/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced usage 2 | 3 | Moved to {doc}`./advanced/index` 4 | 5 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/auth_cache.md: -------------------------------------------------------------------------------- 1 | # Authentication cache 2 | 3 | mangadex-downloader support authentication cache, which mean you can reuse your previous login session in mangadex-downloader 4 | without re-login. 5 | 6 | ```{note} 7 | This authentication cache is stored in same place as where [config](#configuration) is stored. 8 | ``` 9 | 10 | You have to enable [config](#configuration) in order to get working. 11 | 12 | If you enabled authentication cache for the first time, you must login in order to get cached. 13 | 14 | ```shell 15 | mangadex-dl "https://mangadex.org/title/..." --login --login-cache 16 | 17 | # or 18 | 19 | mangadex-dl "conf:login_cache=true" 20 | mangadex-dl "https://mangadex.org/title/..." --login 21 | ``` 22 | 23 | After this command, you no longer need to use `--login` option, 24 | use `--login` option if you want to update user login. 25 | 26 | ```shell 27 | # Let's say user "abc123" is already cached 28 | # And you want to change cached user to "def9090" 29 | mangadex-dl "https://mangadex.org/title/..." --login 30 | ``` 31 | 32 | For more information, you can see here -> {doc}`../../cli_ref/auth_cache` 33 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/auto_select_prompt.md: -------------------------------------------------------------------------------- 1 | # Auto select choices from selectable prompt command (list, library, followed-list) 2 | 3 | In case you didn't want to be prompted, you can use this feature ! 4 | 5 | ```shell 6 | # Automatically select position 1 7 | mangadex-dl "insert keyword here" -s --input-pos "1" 8 | 9 | # Select all 10 | mangadex-dl "insert keyword here" -s --input-pos "*" 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/blacklist_group_or_user.md: -------------------------------------------------------------------------------- 1 | # Blacklist a group or user 2 | 3 | Sometimes you don't like the chapter that this user or group upload it. 4 | You can use this feature to prevent the chapter being downloaded. 5 | 6 | ## Group 7 | 8 | ```shell 9 | # For Windows 10 | set MANGADEXDL_GROUP_BLACKLIST=4197198b-c99b-41ae-ad21-8e6ecc10aa49, 0047632b-1390-493d-ad7c-ac6bb9288f05 11 | 12 | # For Linux / Mac OS 13 | export MANGADEXDL_GROUP_BLACKLIST=4197198b-c99b-41ae-ad21-8e6ecc10aa49, 0047632b-1390-493d-ad7c-ac6bb9288f05 14 | ``` 15 | 16 | ## User 17 | 18 | ```shell 19 | # For Windows 20 | set MANGADEXDL_USER_BLACKLIST=1c4d814e-b1c1-4b75-8a69-f181bb4e57a9, f8cc4f8a-e596-4618-ab05-ef6572980bbf 21 | 22 | # For Linux / Mac OS 23 | export MANGADEXDL_USER_BLACKLIST=1c4d814e-b1c1-4b75-8a69-f181bb4e57a9, f8cc4f8a-e596-4618-ab05-ef6572980bbf 24 | ``` 25 | 26 | For more information, see {doc}`../../cli_ref/env_vars` 27 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/blacklist_tags.md: -------------------------------------------------------------------------------- 1 | # Blacklist one or more tags 2 | 3 | Sometimes you don't like manga that has **some** tags. You can use this feature to prevent the manga being downloaded. 4 | 5 | ```shell 6 | # For Windows 7 | set MANGADEXDL_TAGS_BLACKLIST=gore, sexual violence, oneshot 8 | 9 | # For Linux / Mac OS 10 | export MANGADEXDL_TAGS_BLACKLIST=gore, sexual violence, oneshot 11 | ``` 12 | 13 | For more information, see {doc}`../../cli_ref/env_vars` 14 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/chapter_info.md: -------------------------------------------------------------------------------- 1 | # Enable chapter info creation (or "covers") 2 | 3 | In case you want this image appeared in the beginning of every chapters. 4 | 5 | ![chapter info](../..//images/chapter_info.png) 6 | 7 | You can use `--use-chapter-cover` to enable it. 8 | 9 | ```{note} 10 | It only works for any `volume` and `single` format 11 | ``` 12 | 13 | ```shell 14 | mangadex-dl "insert URL here" --use-chapter-cover -f pdf-volume 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/compression_for_epub_cbz.md: -------------------------------------------------------------------------------- 1 | # Enable compression for epub and cbz formats 2 | 3 | By default, the application didn't enable compression for cbz and epub formats. 4 | In order to enable compression you must use 2 environment variables 5 | 6 | ```sh 7 | # For Linux / Mac OS 8 | export MANGADEXDL_ZIP_COMPRESSION_TYPE=deflated 9 | export MANGADEXDL_ZIP_COMPRESSION_LEVEL=9 10 | ``` 11 | 12 | ```batch 13 | :: For Windows 14 | set MANGADEXDL_ZIP_COMPRESSION_TYPE=deflated 15 | set MANGADEXDL_ZIP_COMPRESSION_LEVEL=9 16 | ``` 17 | 18 | For more information, see: 19 | 20 | - [MANGADEXDL_ZIP_COMPRESSION_TYPE](https://mangadex-dl.mansuf.link/en/stable/cli_ref/env_vars.html#cmdoption-arg-MANGADEXDL_ZIP_COMPRESSION_TYPE) 21 | - [MANGADEXDL_ZIP_COMPRESSION_LEVEL](https://mangadex-dl.mansuf.link/en/stable/cli_ref/env_vars.html#cmdoption-arg-MANGADEXDL_ZIP_COMPRESSION_LEVEL) 22 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | mangadex-downloader support local config stored in local disk. You must set `MANGADEXDL_CONFIG_ENABLED` to `1` or `true` in order to get working. 4 | 5 | ```shell 6 | # For Windows 7 | set MANGADEXDL_CONFIG_ENABLED=1 8 | 9 | # For Linux / Mac OS 10 | export MANGADEXDL_CONFIG_ENABLED=1 11 | ``` 12 | 13 | These config are stored in local user directory (`~/.mangadex-dl`). If you want to change location to store these config, you can set `MANGADEXDL_CONFIG_PATH` to another path. 14 | 15 | ```{note} 16 | If new path is doesn't exist, the app will create folder to that location. 17 | ``` 18 | 19 | ```shell 20 | # For Windows 21 | set MANGADEXDL_CONFIG_PATH=D:\myconfig\here\lmao 22 | 23 | # For Linux / Mac OS 24 | export MANGADEXDL_CONFIG_PATH="/etc/mangadex-dl/config" 25 | ``` 26 | 27 | Example usage 28 | 29 | ```shell 30 | mangadex-dl "conf:save_as=pdf" 31 | # Successfully changed config save_as from 'raw' to 'pdf' 32 | 33 | mangadex-dl "conf:use_chapter_title=1" 34 | # Successfully changed config use_chapter_title from 'False' to 'True' 35 | ``` 36 | 37 | For more information, you can see -> {doc}`../../cli_ref/config` 38 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/download_in_443_port.md: -------------------------------------------------------------------------------- 1 | # Download manga, chapter, or list in forced HTTPS 443 port 2 | 3 | To prevent school/office network blocking traffic to non-standard ports. You can use `--force-https` or `-fh` option 4 | 5 | For example: 6 | 7 | ```shell 8 | mangadex-dl "https://mangadex.org/title/..." --force-https 9 | ``` -------------------------------------------------------------------------------- /docs/cli_usage/advanced/enable_dns_over_https.md: -------------------------------------------------------------------------------- 1 | # Enable DNS-over-HTTPS 2 | 3 | mangadex-downloader support DoH (DNS-over-HTTPS). 4 | You can use it in case your router or ISP being not friendly to MangaDex server. 5 | 6 | Example usage 7 | 8 | ```shell 9 | mangadex-dl "https://mangadex.org/title/..." --dns-over-https cloudflare 10 | ``` 11 | 12 | If you're looking for all available providers, [see here](https://requests-doh.mansuf.link/en/stable/doh_providers.html) 13 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/filename_customization.md: -------------------------------------------------------------------------------- 1 | # Filename customization 2 | 3 | Starting v3.0.0, mangadex-downloader support customize filename based on your preference. 4 | Also it support placeholders ! 5 | 6 | ## Available options 7 | 8 | The usage is depends which format you're using. 9 | 10 | - `--filename-chapter` for any chapter format (cbz, pdf, epub, etc) 11 | - `--filename-volume` for any volume format (cbz-volume, pdf-volume, etc) 12 | - `--filename-single` for any single format (cbz-single, pdf-single) 13 | 14 | ## Example usage 15 | 16 | ### Chapter format 17 | 18 | ```sh 19 | mangadex-dl "URL" -f cbz --filename-chapter "{manga.title} Ch. {chapter.chapter}{file_ext}" 20 | ``` 21 | 22 | ### Volume format 23 | 24 | ```sh 25 | mangadex-dl "URL" -f cbz-volume --filename-volume "{manga.title} Vol. {chapter.volume}{file_ext}" 26 | ``` 27 | 28 | ### Single format 29 | 30 | ```sh 31 | mangadex-dl "URL" -f cbz-single --filename-single "{manga.title} All Chapters{file_ext}" 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/filters.md: -------------------------------------------------------------------------------- 1 | # Filters 2 | 3 | mangadex-downloader support filters. These filters applied to search and random manga. 4 | 5 | Example usage (Search manga) 6 | 7 | ```shell 8 | # Search manhwa with status completed and ongoing, with tags "Comedy" and "Slice of life" 9 | mangadex-dl -s -ft "status=completed,ongoing" -ft "original_language=Korean" -ft "included_tags=comedy, slice of life" 10 | 11 | # or 12 | 13 | mangadex-dl -s -ft "status=completed,ongoing" -ft "original_language=Korean" -ft "included_tags=4d32cc48-9f00-4cca-9b5a-a839f0764984, e5301a23-ebd9-49dd-a0cb-2add944c7fe9" 14 | ``` 15 | 16 | Example usage (Random manga) 17 | 18 | ```shell 19 | # Search manga with tags "Comedy" and "Slice of life" 20 | mangadex-dl "random" -ft "included_tags=comedy, slice of life" 21 | 22 | # or 23 | 24 | mangadex-dl "random" -ft "included_tags=4d32cc48-9f00-4cca-9b5a-a839f0764984, e5301a23-ebd9-49dd-a0cb-2add944c7fe9" 25 | ``` 26 | 27 | For more information about syntax and available filters, see {doc}`../../cli_ref/filters` 28 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/followed_mdlist_from_user_library.md: -------------------------------------------------------------------------------- 1 | # Download MangaDex followed list from logged in user library 2 | 3 | ```{warning} 4 | This method require authentication 5 | ``` 6 | 7 | You can download MangaDex followed list from logged in user library. Just type `followed-list`, login, and select mdlist you want to download. 8 | 9 | ```shell 10 | mangadex-dl "followed-list" --login 11 | # You will be prompted to input username and password for login to MangaDex 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/ignore_missing_chapters.md: -------------------------------------------------------------------------------- 1 | # Ignore missing chapters 2 | 3 | You want to perform update manga after moving downloaded chapters 4 | to another place but mangadex-downloader keeps downloading the missing chapters. 5 | How to avoid this ? 6 | 7 | Worry not the `--ignore-missing-chapters` is your option. 8 | 9 | Simple add `--ignore-missing-chapters` to the CLI arguments and the missing chapters 10 | won't be downloaded anymore 11 | 12 | ```{warning} 13 | This option cannot be used with `--no-track` option 14 | ``` 15 | 16 | Example usage: 17 | 18 | ```sh 19 | # We try to perform clean download 20 | # Meaning that, the manga is not downloaded yet 21 | mangadex-dl "insert URL here" -f cbz 22 | 23 | # After that the manga is downloaded. 24 | # And you moving the chapters to somewhere else 25 | # and now the downloaded chapters are gone moved to another place 26 | ... 27 | 28 | # If you want to update it, 29 | # but do not want to re-download the missing chapters 30 | # simply add --ignore-missing-chapters option 31 | mangadex-dl "The same URL you downloaded" -f cbz --ignore-missing-chapters 32 | ``` 33 | 34 | And done, the missing chapters won't be downloaded anymore. 35 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/index.md: -------------------------------------------------------------------------------- 1 | # Advanced usage 2 | 3 | ```{toctree} 4 | :glob: 5 | 6 | ./* 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/manga_from_pipe_input.md: -------------------------------------------------------------------------------- 1 | # Download manga, chapter, or list from pipe input 2 | 3 | mangadex-downloader support pipe input. You can use it by adding `-pipe` option. 4 | 5 | ```shell 6 | echo "https://mangadex.org/title/..." | mangadex-dl -pipe 7 | ``` 8 | 9 | Multiple lines input also supported. 10 | 11 | ```shell 12 | # For Linux / Mac OS 13 | cat "urls.txt" | mangadex-dl -pipe 14 | 15 | # For Windows 16 | type "urls.txt" | mangadex-dl -pipe 17 | ``` 18 | 19 | Also, you can use another options when using pipe 20 | 21 | ```shell 22 | echo "https://mangadex.org/title/..." | mangadex-dl -pipe --path "/home/myuser" --cover "512px" 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/manga_from_scanlator_group.md: -------------------------------------------------------------------------------- 1 | # Download manga from scanlator group 2 | 3 | You can download manga from your favorite scanlator groups !. Just type `group:`, and then choose which manga you want to download. 4 | 5 | ```shell 6 | # "Tonikaku scans" group 7 | mangadex-dl "group:063cf1b0-9e25-495b-b234-296579a34496" 8 | ``` 9 | 10 | You can also give the full URL if you want to 11 | 12 | ```shell 13 | mangadex-dl "group:https://mangadex.org/group/063cf1b0-9e25-495b-b234-296579a34496/tonikaku-scans?tab=titles" 14 | ``` 15 | 16 | This was equal to these command if you use search with filters 17 | 18 | ```shell 19 | mangadex-dl -s -ft "group=063cf1b0-9e25-495b-b234-296579a34496" 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/manga_from_user_library.md: -------------------------------------------------------------------------------- 1 | # Download manga from logged in user library 2 | 3 | ```{warning} 4 | This method require authentication 5 | ``` 6 | 7 | mangadex-downloader support download from user library. Just type `library`, login, and select which manga you want to download. 8 | 9 | For example: 10 | 11 | ```shell 12 | mangadex-dl "library" --login 13 | # You will be prompted to input username and password for login to MangaDex 14 | ``` 15 | 16 | You can also apply filter to it ! 17 | 18 | ```shell 19 | # List all mangas with "Reading" status from user library 20 | mangadex-dl "library:reading" --login 21 | 22 | # List all mangas with "Plan to read" status from user library 23 | mangadex-dl "library:plan_to_read" --login 24 | ``` 25 | 26 | To list all available filters type `library:help` 27 | 28 | ```shell 29 | mangadex-dl "library:help" 30 | # ... 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/manga_with_compressed_size.md: -------------------------------------------------------------------------------- 1 | # Download manga with compressed size images 2 | 3 | If you have limited plan or metered network, you can download manga, chapter, or list with compressed size. 4 | And yes, this may reduce the quality. But hey, at least it saved you from huge amount of bytes 5 | 6 | Example Usage: 7 | 8 | ```shell 9 | mangadex-dl "https://mangadex.org/title/..." --use-compressed-image 10 | 11 | # or 12 | 13 | mangadex-dl "https://mangadex.org/title/..." -uci 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/manga_with_different_title.md: -------------------------------------------------------------------------------- 1 | # Download a manga with different title 2 | 3 | mangadex-downloader also support multi titles manga, which mean you can choose between different titles in different languages ! 4 | 5 | Example usage: 6 | 7 | ```shell 8 | mangadex-dl "https://mangadex.org/title/..." --use-alt-details 9 | # Manga "..." has alternative titles, please choose one 10 | # (1). [English]: ... 11 | # (2). [Japanese]: ... 12 | # (3). [Indonesian]: ... 13 | # => 14 | ``` 15 | 16 | ```{warning} 17 | When you already downloaded a manga, but you wanna download it again with different title. It will re-download the whole manga. 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/mdlist_from_user_library.md: -------------------------------------------------------------------------------- 1 | # Download MangaDex list from logged in user library 2 | 3 | ```{warning} 4 | This method require authentication 5 | ``` 6 | 7 | You can download MangaDex list from logged in user library. Just type `list`, login, and select mdlist you want to download. 8 | 9 | For example: 10 | 11 | ```shell 12 | mangadex-dl "list" --login 13 | # You will be prompted to input username and password for login to MangaDex 14 | ``` 15 | 16 | Also, you can download mdlist from another user. It will only fetch all public list only. 17 | 18 | ```{note} 19 | Authentication is not required when download MangaDex list from another user. 20 | ``` 21 | 22 | For example: 23 | 24 | ```shell 25 | mangadex-dl "list:give_the_user_id_here" 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/requests_timeout.md: -------------------------------------------------------------------------------- 1 | # Set timeout for each HTTP(s) requests 2 | 3 | In case if you don't have patience 😁 4 | 5 | ```shell 6 | # Set timeout for 2 seconds for each HTTP(s) requests 7 | mangadex-dl "https://mangadex.org/title/..." --timeout 2 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/scanlator_group_filter.md: -------------------------------------------------------------------------------- 1 | # Scanlator group filtering 2 | 3 | You can download chapters only from 1 scanlation group, by using `--group` option 4 | 5 | ```shell 6 | # This will download all chapters from group "Tonikaku scans" only 7 | mangadex-dl "https://mangadex.org/title/..." --group "https://mangadex.org/group/063cf1b0-9e25-495b-b234-296579a34496/tonikaku-scans" 8 | ``` 9 | 10 | You can download all same chapters with different groups, by using `--group` option with value "all" 11 | 12 | ```shell 13 | # This will download all chapters, regardless of scanlation groups 14 | mangadex-dl "https://mangadex.org/title/..." --group "all" 15 | ``` 16 | 17 | ```{warning} 18 | You cannot use `--group all` and `--no-group-name` together. It will throw error, if you're trying to do it 19 | ``` 20 | 21 | Also, you can use user as filter in `--group` option. 22 | 23 | For example: 24 | 25 | ```shell 26 | mangadex-dl "https://mangadex.org/title/..." --group "https://mangadex.org/user/..." 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/setup_proxy.md: -------------------------------------------------------------------------------- 1 | # Setup proxy 2 | 3 | ```shell 4 | # HTTP proxy 5 | mangadex-dl "https://mangadex.org/title/..." --proxy "http://127.0.0.1" 6 | 7 | # SOCKS proxy 8 | mangadex-dl "https://mangadex.org/title/..." --proxy "socks://127.0.0.1" 9 | ``` 10 | 11 | mangadex-downloader support proxy from environments 12 | 13 | ```shell 14 | # For Linux / Mac OS 15 | export http_proxy="http://127.0.0.1" 16 | export https_proxy="http://127.0.0.1" 17 | 18 | # For Windows 19 | set http_proxy=http://127.0.0.1 20 | set https_proxy=http://127.0.0.1 21 | 22 | mangadex-dl "insert mangadex url here" --proxy-env 23 | ``` 24 | 25 | ```{warning} 26 | You cannot use `--proxy` and `--proxy-env` together. It will throw error, if you're trying to do it 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/show_manga_covers.md: -------------------------------------------------------------------------------- 1 | # Show list of manga covers and download it 2 | 3 | Wanna download the cover only ? I got you 4 | 5 | ```shell 6 | # Manga id only 7 | mangadex-dl "cover:manga_id" 8 | 9 | # Full manga URL 10 | mangadex-dl "cover:https://mangadex.org/title/..." 11 | 12 | # Full cover manga URL 13 | mangadex-dl "cover:https://mangadex.org/covers/..." 14 | ``` 15 | 16 | Don't wanna get prompted ? Use `--input-pos` option ! 17 | 18 | ```sh 19 | # Automatically select choice 1 20 | mangadex-dl "cover:https://mangadex.org/title/..." --input-pos 1 21 | 22 | # Automatically select all choices 23 | mangadex-dl "cover:https://mangadex.org/title/..." --input-pos "*" 24 | ``` 25 | 26 | ```{note} 27 | This will download covers in original quality. 28 | If you want to use different quality, use command `cover-512px` for 512px quality 29 | and `cover-256px` for 256px quality. 30 | ``` 31 | 32 | For more information, see {doc}`../../cli_ref/cover` 33 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/syntax_for_batch_download.md: -------------------------------------------------------------------------------- 1 | # Special syntax for batch download 2 | 3 | To avoid conflict filenames with reserved names (such as: `list`, `library`, `followed-list`) in `URL` argument, 4 | you can use special syntax for batch download 5 | 6 | For example: 7 | 8 | ```shell 9 | mangadex-dl "file:/home/manga/urls.txt" 10 | 11 | mangadex-dl "file:list" 12 | ``` 13 | 14 | For more information, see {doc}`../../cli_ref/file_command` 15 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/throttle_requests.md: -------------------------------------------------------------------------------- 1 | # Throttling requests 2 | 3 | If you worried about being blocked by MangaDex if you download too much, you can use this feature to throttle requests. 4 | 5 | Example usage: 6 | 7 | ```shell 8 | # Delay requests for each 1.5 seconds 9 | mangadex-dl "https://mangadex.org/title/..." --delay-requests 1.5 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/cli_usage/advanced/verbose_output.md: -------------------------------------------------------------------------------- 1 | # Enable verbose output / change logging level 2 | 3 | Starting v2.10.0, you can enable verbose output from `--log-level` with value `DEBUG`. 4 | 5 | ```sh 6 | mangadex-dl "insert MangaDex URL here" --log-level "DEBUG" 7 | ``` 8 | 9 | Change logging level to warning. 10 | 11 | ```{note} 12 | This level will only show output if the levels are warning, error and critical 13 | 14 | For more information, see {doc}`../../cli_ref/log_levels` 15 | ``` 16 | 17 | ```sh 18 | mangadex-dl "insert MangaDex URL here" --log-level "WARNING" 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import re 15 | import sys 16 | sys.path.insert(0, os.path.abspath('..')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'mangadex-downloader' 22 | copyright = '2021 - present, Rahman Yusuf' 23 | author = 'mansuf' 24 | 25 | # Find version without importing it 26 | regex_version = re.compile(r'[0-9]{1}.[0-9]{1,2}.[0-9]{1,3}') 27 | with open('../mangadex_downloader/__init__.py', 'r') as r: 28 | _version = regex_version.search(r.read()) 29 | 30 | if _version is None: 31 | raise RuntimeError('version is not set') 32 | 33 | version = _version.group() 34 | 35 | # The full version, including alpha/beta/rc tags 36 | release = version 37 | 38 | 39 | # -- General configuration --------------------------------------------------- 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = [ 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.extlinks', 47 | 'sphinx.ext.napoleon', 48 | 'sphinx.ext.intersphinx', 49 | 'myst_parser' 50 | ] 51 | 52 | myst_enable_extensions = [ 53 | 'dollarmath', 54 | 'linkify' 55 | ] 56 | 57 | myst_linkify_fuzzy_links=False 58 | 59 | myst_heading_anchors = 3 60 | 61 | source_suffix = { 62 | '.rst': 'restructuredtext', 63 | '.md': 'markdown', 64 | } 65 | 66 | # No typing in docs 67 | autodoc_member_order = 'bysource' 68 | autodoc_typehints = 'none' 69 | 70 | # Add any paths that contain templates here, relative to this directory. 71 | templates_path = ['_templates'] 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This pattern also affects html_static_path and html_extra_path. 76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 77 | 78 | # Intersphinx mapping 79 | intersphinx_mapping = { 80 | 'python': ('https://docs.python.org/3', None), 81 | } 82 | 83 | # -- Options for HTML output ------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | # Intersphinx mapping 89 | intersphinx_mapping = { 90 | 'python': ('https://docs.python.org/3', None), 91 | } 92 | 93 | html_theme = 'furo' 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | html_static_path = ['_static'] 99 | 100 | html_title = project -------------------------------------------------------------------------------- /docs/images/api_clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/docs/images/api_clients.png -------------------------------------------------------------------------------- /docs/images/chapter_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/docs/images/chapter_info.png -------------------------------------------------------------------------------- /docs/images/post-in-forum-thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/docs/images/post-in-forum-thread.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # mangadex-downloader 2 | 3 | A command-line tool to download manga from [MangaDex](https://mangadex.org/), written in [Python](https://www.python.org/). 4 | 5 | ## Key features 6 | 7 | - Download manga, cover manga, chapter, or list directly from MangaDex 8 | - Download manga or list from user library 9 | - Find and download MangaDex URLs from MangaDex forums ([https://forums.mangadex.org/](https://forums.mangadex.org/)) 10 | - Download manga in each chapters, each volumes, or wrap all chapters into single file 11 | - Search (with filters) and download manga 12 | - Filter chapters with scalantion groups or users 13 | - Manga tags, groups, and users blacklist support 14 | - Batch download support 15 | - Authentication (with cache) support 16 | - Control how many chapters and pages you want to download 17 | - Multi languages support 18 | - Legacy MangaDex url support 19 | - Save as raw images, EPUB, PDF, Comic Book Archive (.cbz or .cb7) 20 | - Respect API rate limit 21 | 22 | ## Getting started 23 | 24 | - Installation: {doc}`installation` 25 | - Basic usage: {doc}`./cli_usage/index` 26 | - Advanced usage: {doc}`./cli_usage/advanced` 27 | 28 | ## Manuals 29 | 30 | - Available formats: {doc}`./formats` 31 | - CLI Options: {doc}`./cli_ref/cli_options` 32 | - Commands: {doc}`./cli_ref/commands` 33 | 34 | To see all available manuals, see {doc}`cli_ref/index` 35 | 36 | ```{toctree} 37 | :maxdepth: 2 38 | :hidden: 39 | 40 | installation 41 | formats 42 | cli_usage/index 43 | cli_usage/advanced/index 44 | cli_ref/index 45 | ``` 46 | 47 | ```{toctree} 48 | :hidden: 49 | :caption: Development 50 | 51 | migration_v2_v3 52 | changelog 53 | Github repository 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Stable version 4 | 5 | ### With PyPI 6 | 7 | ```shell 8 | # For Windows 9 | py -3 -m pip install mangadex-downloader 10 | 11 | # For Linux / Mac OS 12 | python3 -m pip install mangadex-downloader 13 | ``` 14 | 15 | ### Compiled app (for Windows only) 16 | 17 | Go to latest release in https://github.com/mansuf/mangadex-downloader/releases and download it. 18 | 19 | **NOTE**: According to [`pyinstaller`](https://github.com/pyinstaller/pyinstaller) it should support Windows 7, 20 | but its recommended to use it on Windows 8+. 21 | 22 | 23 | ## Development version 24 | 25 | ```{warning} 26 | This version is not stable and may crash during run. 27 | ``` 28 | 29 | ### With PyPI & Git 30 | 31 | **NOTE:** You must have git installed. If you don't have it, install it from here https://git-scm.com/. 32 | 33 | ```shell 34 | # For Windows 35 | py -3 -m pip install git+https://github.com/mansuf/mangadex-downloader.git 36 | 37 | # For Linux / Mac OS 38 | python3 -m pip install git+https://github.com/mansuf/mangadex-downloader.git 39 | ``` 40 | 41 | ### With Git only 42 | 43 | **NOTE:** You must have git installed. If you don't have it, install it from here https://git-scm.com/. 44 | 45 | ```shell 46 | git clone https://github.com/mansuf/mangadex-downloader.git 47 | cd mangadex-downloader 48 | python setup.py install 49 | ``` -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/migration_v2_v3.md: -------------------------------------------------------------------------------- 1 | # Migration guide from v2 to v3 2 | 3 | Here's list of things that you should know before start using v3 4 | 5 | ## `--path` option become absolute path and support placeholders 6 | 7 | In v2 or lower, if you set `--path` with value `mymanga/some_kawaii_manga`. 8 | The manga and the chapters will be stored under directory `mymanga/some_kawaii_manga/NameOfTheManga`. 9 | 10 | ```sh 11 | mangadex-dl "URL" --path "mymanga/some_kawaii_manga" 12 | 13 | # If you see download directory, you will see this: 14 | 📂mymanga 15 | ┗ 📂some_kawaii_manga 16 | ┃ ┗ 📂NameOfTheManga 17 | ┃ ┃ ┣ 📂Vol. 1 Ch. 1 18 | ┃ ┃ ┃ ┣ 📜00.png 19 | ┃ ┃ ┃ ┣ 📜01.png 20 | ┃ ┃ ┃ ┗ 📜++.png 21 | ┃ ┃ ┣ 📜cover.jpg 22 | ┃ ┃ ┗ 📜download.db 23 | ``` 24 | 25 | Now, if you set `--path` with value `mymanga/some_kawaii_manga`. 26 | The manga and the chapters will be stored under directory `mymanga/some_kawaii_manga`. 27 | 28 | ```sh 29 | mangadex-dl "URL" --path "mymanga/some_kawaii_manga" 30 | 31 | # If you see download directory, you will see this: 32 | 📂mymanga 33 | ┗ 📂some_kawaii_manga 34 | ┃ ┣ 📂Vol. 1 Ch. 1 35 | ┃ ┃ ┣ 📜00.png 36 | ┃ ┃ ┣ 📜01.png 37 | ┃ ┃ ┗ 📜++.png 38 | ┃ ┣ 📜cover.jpg 39 | ┃ ┗ 📜download.db 40 | ``` 41 | 42 | If you comfortable with old behaviour, you can use placeholders 43 | 44 | ```sh 45 | mangadex-dl "URL" --path "mymanga/some_kawaii_manga/{manga.title}" 46 | ``` 47 | 48 | See more placeholders in {doc}`./cli_ref/path_placeholders` 49 | 50 | ## `No volume` will get separated into chapters format 51 | 52 | ```{note} 53 | This change only affect any `volume` formats, 54 | `chapters` and `single` formats doesn't get affected by this change 55 | ``` 56 | 57 | Now, if a manga that doesn’t have no volume, 58 | it will get separated (chapters format) rather than being merged into single file called No volume.cbz (example). 59 | However if you prefer old behaviour (merge no volume chapters into single file) you can use --create-no-volume. 60 | 61 | For example: 62 | 63 | ### v2 and lower 64 | 65 | ```sh 66 | mangadex-dl "URL" --save-as "raw-volume" 67 | 68 | # If you see download directory, you will see this: 69 | 📂mymanga 70 | ┗ 📂some_spicy_manga 71 | ┃ ┣ 📂No Volume 72 | ┃ ┃ ┣ 📜00.png 73 | ┃ ┃ ┗ 📜24.png 74 | ┃ ┣ 📂Volume. 1 75 | ┃ ┃ ┣ 📜00.png 76 | ┃ ┃ ┗ 📜24.png 77 | ┃ ┣ 📜cover.jpg 78 | ┃ ┗ 📜download.db 79 | ``` 80 | 81 | ### v3 and upper 82 | 83 | ```sh 84 | mangadex-dl "URL" --save-as "raw-volume" 85 | 86 | # If you see download directory, you will see this: 87 | 📂mymanga 88 | ┗ 📂some_spicy_manga 89 | ┃ ┣ 📂Chapter 1 90 | ┃ ┃ ┣ 📜00.png 91 | ┃ ┃ ┗ 📜24.png 92 | ┃ ┣ 📂Chapter 2 93 | ┃ ┃ ┣ 📜00.png 94 | ┃ ┃ ┗ 📜24.png 95 | ┃ ┣ 📂Volume. 1 96 | ┃ ┃ ┣ 📜00.png 97 | ┃ ┃ ┗ 📜24.png 98 | ┃ ┣ 📜cover.jpg 99 | ┃ ┗ 📜download.db 100 | ``` 101 | 102 | If you prefer old behaviour like v2 and lower, you can use `--create-no-volume` 103 | 104 | ```sh 105 | mangadex-dl "URL" --save-as "raw-volume" --create-no-volume 106 | ``` 107 | 108 | ## Dropped support for Python v3.8 and v3.9 109 | 110 | Since Python v3.8 already reached End-of-life (EOL), 111 | i have no intention to continue developing it and Python 3.9 is almost reached End-of-life too. 112 | 113 | Minimum Python version for installing mangadex-downloader is 3.10 and upper. 114 | -------------------------------------------------------------------------------- /mangadex-dl_x64.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['run.py'], 9 | pathex=[], 10 | binaries=[], 11 | datas=[ 12 | ('mangadex_downloader/fonts', 'mangadex_downloader/fonts'), 13 | ('mangadex_downloader/images', 'mangadex_downloader/images'), 14 | ('mangadex_downloader/tracker/sql_files', 'mangadex_downloader/tracker/sql_files'), 15 | ('mangadex_downloader/tracker/sql_migrations', 'mangadex_downloader/tracker/sql_migrations'), 16 | ], 17 | hiddenimports=[], 18 | hookspath=[], 19 | hooksconfig={}, 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False, 26 | ) 27 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 28 | 29 | exe = EXE( 30 | pyz, 31 | a.scripts, 32 | [], 33 | exclude_binaries=True, 34 | name='mangadex-dl_x64', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | console=True, 40 | disable_windowed_traceback=False, 41 | argv_emulation=False, 42 | target_arch=None, 43 | codesign_identity=None, 44 | entitlements_file=None, 45 | ) 46 | coll = COLLECT( 47 | exe, 48 | a.binaries, 49 | a.zipfiles, 50 | a.datas, 51 | strip=False, 52 | upx=True, 53 | upx_exclude=[], 54 | name='mangadex-dl_x64', 55 | ) 56 | -------------------------------------------------------------------------------- /mangadex-dl_x86.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['run.py'], 9 | pathex=[], 10 | binaries=[], 11 | datas=[ 12 | ('mangadex_downloader/fonts', 'mangadex_downloader/fonts'), 13 | ('mangadex_downloader/images', 'mangadex_downloader/images'), 14 | ('mangadex_downloader/tracker/sql_files', 'mangadex_downloader/tracker/sql_files'), 15 | ('mangadex_downloader/tracker/sql_migrations', 'mangadex_downloader/tracker/sql_migrations'), 16 | ], 17 | hiddenimports=[], 18 | hookspath=[], 19 | hooksconfig={}, 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False, 26 | ) 27 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 28 | 29 | exe = EXE( 30 | pyz, 31 | a.scripts, 32 | [], 33 | exclude_binaries=True, 34 | name='mangadex-dl_x86', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | console=True, 40 | disable_windowed_traceback=False, 41 | argv_emulation=False, 42 | target_arch=None, 43 | codesign_identity=None, 44 | entitlements_file=None, 45 | ) 46 | coll = COLLECT( 47 | exe, 48 | a.binaries, 49 | a.zipfiles, 50 | a.datas, 51 | strip=False, 52 | upx=True, 53 | upx_exclude=[], 54 | name='mangadex-dl_x86', 55 | ) 56 | -------------------------------------------------------------------------------- /mangadex_downloader/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A command-Line tool to download manga from MangaDex, written in Python 3 | """ 4 | 5 | # fmt: off 6 | __version__ = "3.1.4" 7 | __description__ = "A Command-line tool to download manga from MangaDex, written in Python" 8 | __author__ = "Rahman Yusuf" 9 | __author_email__ = "danipart4@gmail.com" 10 | __license__ = "MIT" 11 | __repository__ = "mansuf/mangadex-downloader" 12 | __url_repository__ = "https://github.com" 13 | # fmt: on 14 | 15 | import logging 16 | 17 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 18 | -------------------------------------------------------------------------------- /mangadex_downloader/__main__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from mangadex_downloader.cli import main 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /mangadex_downloader/artist_and_author.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .fetcher import get_author 24 | 25 | 26 | class _Base: 27 | def __init__(self, _id=None, data=None): 28 | if not data: 29 | self.data = get_author(_id)["data"] 30 | else: 31 | self.data = data 32 | 33 | self.id = self.data["id"] 34 | 35 | attr = self.data["attributes"] 36 | 37 | # Name 38 | self.name = attr.get("name") 39 | 40 | # Profile photo 41 | self.image = attr.get("imageUrl") 42 | 43 | # The rest of values 44 | for key, value in attr.items(): 45 | setattr(self, key, value) 46 | 47 | 48 | class Artist(_Base): 49 | pass 50 | 51 | 52 | class Author(_Base): 53 | pass 54 | -------------------------------------------------------------------------------- /mangadex_downloader/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .oauth2 import OAuth2 # noqa: F401 2 | from .legacy import LegacyAuth # noqa: F401 3 | -------------------------------------------------------------------------------- /mangadex_downloader/auth/base.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class MangaDexAuthBase: 25 | """Base auth class for MangaDex API""" 26 | 27 | def __init__(self, session): 28 | self.session = session 29 | 30 | def login(self, username, email, password, **kwargs): 31 | """Login to MangaDex""" 32 | pass 33 | 34 | def logout(self): 35 | """Logout from MangaDex 36 | 37 | NOTE: this method only revoke `session_token` and `refresh_token` 38 | """ 39 | pass 40 | 41 | def update_token(self, session=None, refresh=None): 42 | """Update token internally for this class""" 43 | pass 44 | 45 | def refresh_token(self): 46 | """Get new `session_token` using `refresh_token`""" 47 | pass 48 | 49 | def check_login(self): 50 | """Check if login session is still active""" 51 | pass 52 | -------------------------------------------------------------------------------- /mangadex_downloader/auth/legacy.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | 25 | from .base import MangaDexAuthBase 26 | from ..errors import LoginFailed 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | 31 | class LegacyAuth(MangaDexAuthBase): 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | 35 | self.token = { 36 | "token": { 37 | "session": None, 38 | "refresh": None, 39 | } 40 | } 41 | 42 | def _make_ready_token(self, token): 43 | return { 44 | "session": token["token"]["session"], 45 | "refresh": token["token"]["refresh"], 46 | } 47 | 48 | def update_token(self, session=None, refresh=None): 49 | if session: 50 | self.token["token"]["session"] = session 51 | 52 | if refresh: 53 | self.token["token"]["refresh"] = refresh 54 | 55 | def login(self, username, email, password, **kwargs): 56 | if not username and not email: 57 | raise LoginFailed('at least provide "username" or "email" to login') 58 | 59 | # Raise error if password length are less than 8 characters 60 | if len(password) < 8: 61 | raise LoginFailed("password length must be more than 8 characters") 62 | 63 | url = f"{self.session.base_url}/auth/login" 64 | data = {"password": password} 65 | 66 | if username: 67 | data["username"] = username 68 | if email: 69 | data["email"] = email 70 | 71 | # Begin to log in 72 | r = self.session.post(url, json=data) 73 | if r.status_code == 401: 74 | result = r.json() 75 | err = result["errors"][0]["detail"] 76 | log.error("Login to MangaDex failed, reason: %s" % err) 77 | raise LoginFailed(err) 78 | 79 | self.token = r.json() 80 | 81 | return self._make_ready_token(self.token) 82 | 83 | def logout(self): 84 | self.session.post(f"{self.session.base_url}/auth/logout") 85 | 86 | self.token = None 87 | 88 | def check_login(self): 89 | url = f"{self.session.base_url}/auth/check" 90 | r = self.session.get(url) 91 | 92 | return r.json()["isAuthenticated"] 93 | 94 | def refresh_token(self): 95 | url = f"{self.session.base_url}/auth/refresh" 96 | r = self.session.post(url, json={"token": self.token["token"]["refresh"]}) 97 | result = r.json() 98 | 99 | if r.status_code != 200: 100 | raise LoginFailed( 101 | "Refresh token failed, reason: %s" % result["errors"][0]["detail"] 102 | ) 103 | 104 | self.token = result 105 | 106 | return self._make_ready_token(self.token) 107 | -------------------------------------------------------------------------------- /mangadex_downloader/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import traceback 4 | from .update import check_update 5 | from .args_parser import get_args 6 | from .url import build_url 7 | from .utils import ( 8 | cleanup_app, 9 | setup_logging, 10 | setup_network, 11 | register_keyboardinterrupt_handler, 12 | sys_argv, 13 | ) 14 | from .config import build_config 15 | from .auth import login_with_err_handler, logout_with_err_handler 16 | from .download import download 17 | 18 | from ..errors import MangaDexException 19 | from ..format import deprecated_formats 20 | from ..utils import queueworker_active_threads 21 | 22 | _deprecated_opts = { 23 | # I know this isn't deprecated 24 | # But i need the warning feature, hehe 25 | "range": "--range is disabled, because it's broken and need to rework", 26 | } 27 | 28 | 29 | def check_deprecated_options(log, args): 30 | for arg, msg in _deprecated_opts.items(): 31 | deprecated = getattr(args, arg) 32 | if deprecated: 33 | log.warning(msg) 34 | 35 | 36 | def check_deprecated_formats(log, args): 37 | if args.save_as in deprecated_formats: 38 | log.warning( 39 | f"format `{args.save_as}` is deprecated, " 40 | "please use `raw` or `cbz` format with `--write-tachiyomi-info` instead" 41 | ) 42 | 43 | 44 | def check_conflict_options(args, parser): 45 | if args.ignore_missing_chapters and args.no_track: 46 | parser.error("--ignore-missing-chapters cannot be used when --no-track is set") 47 | 48 | if args.group and args.no_group_name: 49 | raise MangaDexException("--group cannot be used together with --no-group-name") 50 | 51 | if args.start_chapter is not None and args.end_chapter is not None: 52 | if args.start_chapter > args.end_chapter: 53 | raise MangaDexException("--start-chapter cannot be more than --end-chapter") 54 | 55 | if args.start_chapter < 0 and args.end_chapter >= 0: 56 | raise MangaDexException( 57 | "--end-chapter cannot be positive number while --start-chapter is negative number" 58 | ) 59 | 60 | if args.start_page is not None and args.end_page is not None: 61 | if args.start_page > args.end_page: 62 | raise MangaDexException("--start-page cannot be more than --end-page") 63 | 64 | if args.start_page < 0 and args.end_page >= 0: 65 | raise MangaDexException( 66 | "--end-page cannot be positive number while --start-page is negative number" 67 | ) 68 | 69 | 70 | def _main(argv): 71 | parser = None 72 | try: 73 | # Signal handler 74 | register_keyboardinterrupt_handler() 75 | 76 | # Get command-line arguments 77 | parser, args = get_args(argv) 78 | 79 | # Setup logging 80 | log = setup_logging( 81 | "mangadex_downloader", True if args.log_level == "DEBUG" else False 82 | ) 83 | 84 | # Check deprecated 85 | check_deprecated_options(log, args) 86 | check_deprecated_formats(log, args) 87 | 88 | # Check conflict options 89 | check_conflict_options(args, parser) 90 | 91 | # Parse config 92 | build_config(parser, args) 93 | 94 | # Setup network 95 | setup_network(args) 96 | 97 | # Login 98 | login_with_err_handler(args) 99 | 100 | # Building url 101 | build_url(parser, args) 102 | 103 | # Download the manga 104 | download(args) 105 | 106 | # Logout when it's finished 107 | logout_with_err_handler(args) 108 | 109 | # Check update 110 | check_update() 111 | 112 | # library error 113 | except MangaDexException as e: 114 | err_msg = str(e) 115 | return parser, 1, err_msg 116 | 117 | # Other exception 118 | except Exception as e: 119 | traceback.print_exception(type(e), e, e.__traceback__, file=sys.stderr) 120 | return parser, 2, None 121 | 122 | else: 123 | # We're done here 124 | return parser, 0, None 125 | 126 | 127 | def main(argv=None): 128 | _argv = sys_argv if argv is None else argv 129 | 130 | # Notes for exit code 131 | # 0 Means it has no error 132 | # 1 is library error (at least we can handle it) 133 | # 2 is an error that we cannot handle (usually from another library or Python itself) 134 | 135 | if "--run-forever" in [i.lower() for i in _argv]: 136 | while True: 137 | args_parser, exit_code, err_msg = _main(_argv) 138 | 139 | if exit_code == 2: 140 | # Hard error 141 | # an error that we cannot handle 142 | # exit the application 143 | break 144 | 145 | # Shutdown worker threads 146 | # to prevent infinite worker threads 147 | for worker_thread in queueworker_active_threads: 148 | worker_thread.shutdown(blocking=True, blocking_timeout=3) 149 | 150 | time.sleep(5) 151 | else: 152 | args_parser, exit_code, err_msg = _main(_argv) 153 | 154 | cleanup_app() 155 | 156 | if args_parser is not None and exit_code > 0 and err_msg: 157 | # It has error message, exit with .error() 158 | args_parser.error(err_msg) 159 | 160 | # There is no error during execution 161 | # or an error occurred during parsing arguments 162 | # or another error that the program itself cannot handle it 163 | sys.exit(exit_code) 164 | -------------------------------------------------------------------------------- /mangadex_downloader/cli/config.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import sys 25 | 26 | from ..utils import get_key_value 27 | from ..config import ( 28 | config, 29 | _conf, 30 | config_enabled, 31 | set_config_from_cli_opts, 32 | reset_config, 33 | get_all_configs, 34 | ) 35 | from ..config.utils import ConfigTypeError 36 | 37 | log = logging.getLogger(__name__) 38 | 39 | 40 | def build_config_from_url_arg(parser, urls): 41 | if not urls.startswith("conf"): 42 | return 43 | 44 | if not config_enabled: 45 | parser.error( 46 | "Config is not enabled, " 47 | "you must set MANGADEXDL_CONFIG_ENABLED=1 in your env" 48 | ) 49 | 50 | for url in urls.splitlines(): 51 | val = url.strip() 52 | # Just ignore it if empty lines 53 | if not val: 54 | continue 55 | # Invalid config 56 | elif not val.startswith("conf"): 57 | continue 58 | 59 | # Split from "conf:config_key=config_value" 60 | # to ["conf", "config_key=config_value"] 61 | _, conf = get_key_value(val, sep=":") 62 | 63 | # Split string from "config_key=config_value" 64 | # to ["config_key", "config_value"] 65 | conf_key, conf_value = get_key_value(conf) 66 | 67 | if not conf_key: 68 | for name, value in get_all_configs(): 69 | print(f"Config {name!r} is set to {value!r}") 70 | continue 71 | 72 | # Reset config (if detected) 73 | if conf_key.startswith("reset"): 74 | try: 75 | reset_config(conf_value) 76 | except AttributeError: 77 | parser.error(f"Config {conf_key!r} is not exist") 78 | 79 | if conf_value: 80 | print(f"Successfully reset config {conf_value!r}") 81 | else: 82 | # Reset all configs 83 | print("Successfully reset all configs") 84 | 85 | continue 86 | 87 | try: 88 | previous_value = getattr(config, conf_key) 89 | except AttributeError: 90 | parser.error(f"Config {conf_key!r} is not exist") 91 | 92 | if not conf_value: 93 | print(f"Config {conf_key!r} is set to {previous_value!r}") 94 | continue 95 | 96 | try: 97 | setattr(config, conf_key, conf_value) 98 | except ConfigTypeError as e: 99 | parser.error(str(e)) 100 | 101 | conf_value = getattr(config, conf_key) 102 | 103 | print( 104 | f"Successfully changed config {conf_key} " 105 | f"from {previous_value!r} to {conf_value!r}" 106 | ) 107 | 108 | # Changing config require users to input config in URL argument 109 | # If the app is not exited, the app will continue and throwing error 110 | # because of invalid URL given 111 | sys.exit(0) 112 | 113 | 114 | def build_config(parser, args): 115 | build_config_from_url_arg(parser, args.URL) 116 | 117 | if not config_enabled and args.login_cache: 118 | parser.error( 119 | "You must set MANGADEXDL_CONFIG_ENABLED=1 in your env " 120 | "in order to enable login caching" 121 | ) 122 | 123 | # Automatically set config.login_cache to True 124 | # if args.login_cache is True and config.login_cache is False 125 | if not config.login_cache and args.login_cache: 126 | config.login_cache = args.login_cache 127 | 128 | # ====================== 129 | # Compatibility configs 130 | # ====================== 131 | 132 | # Print all config to debug 133 | if config_enabled: 134 | log.debug(f"Loaded config from path {_conf.path!r} = {_conf._data}") 135 | 136 | set_config_from_cli_opts(args) 137 | 138 | log.debug(f"Loaded config from cli args = {_conf._data}") 139 | -------------------------------------------------------------------------------- /mangadex_downloader/cli/download.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import traceback 25 | 26 | from ..errors import MangaDexException, ChapterNotFound 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | 31 | def download(args): 32 | for url in args.URL: 33 | try: 34 | url(args, args.type) 35 | except ChapterNotFound as e: 36 | # Do not show traceback for "chapter not found" errors 37 | log.error(e) 38 | except MangaDexException as e: 39 | # The error already explained 40 | log.error(e) 41 | traceback.print_exception(type(e), e, e.__traceback__) 42 | continue 43 | -------------------------------------------------------------------------------- /mangadex_downloader/cli/update.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import sys 25 | 26 | from ..update import check_version 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | 31 | def check_update(): 32 | log.debug("Checking update...") 33 | try: 34 | latest_version = check_version() 35 | except Exception: 36 | sys.exit(1) 37 | 38 | if latest_version: 39 | log.info( 40 | f"There is new version mangadex-downloader ! ({latest_version})), " 41 | "you should update it with '--update' option" 42 | ) 43 | else: 44 | log.debug("No update found") 45 | -------------------------------------------------------------------------------- /mangadex_downloader/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_cache import * # noqa: F403 2 | from .config import * # noqa: F403 3 | from .env import * # noqa: F403 4 | -------------------------------------------------------------------------------- /mangadex_downloader/config/env.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import zipfile 24 | import os 25 | from pathlib import Path 26 | 27 | from .utils import ( 28 | validate_bool, 29 | validate_dummy, 30 | validate_zip_compression_type, 31 | validate_int, 32 | validate_blacklist, 33 | validate_tag, 34 | load_env, 35 | LazyLoadEnv, 36 | ConfigTypeError, 37 | ) 38 | from ..errors import MangaDexException 39 | 40 | __all__ = ("env", "base_path", "config_enabled", "init") 41 | 42 | 43 | class EnvironmentVariables: 44 | # 4 values of tuple 45 | # ( 46 | # key_env: string, 47 | # default_value: Any, 48 | # validator_function: Callable, 49 | # lazy_loading: boolean 50 | # ) 51 | _vars = [ 52 | [ 53 | "config_enabled", 54 | False, 55 | validate_bool, 56 | False, 57 | ], 58 | [ 59 | "config_path", 60 | None, 61 | validate_dummy, 62 | False, 63 | ], 64 | [ 65 | "zip_compression_type", 66 | zipfile.ZIP_STORED, 67 | validate_zip_compression_type, 68 | False, 69 | ], 70 | [ 71 | "zip_compression_level", 72 | None, 73 | validate_int, 74 | False, 75 | ], 76 | [ 77 | "user_blacklist", 78 | tuple(), 79 | validate_blacklist, 80 | False, 81 | ], 82 | [ 83 | "group_blacklist", 84 | tuple(), 85 | validate_blacklist, 86 | False, 87 | ], 88 | [ 89 | "tags_blacklist", 90 | tuple(), 91 | lambda x: validate_blacklist(x, validate_tag), 92 | # We need to use lazy loading for env MANGADEXDL_TAGS_BLACKLIST 93 | # to prevent "circular imports" problem when using `requestsMangaDexSession`. 94 | # Previously, it was using `requests.Session` 95 | # which is not respecting rate limit system from MangaDex API 96 | True, 97 | ], 98 | ] 99 | 100 | def __init__(self): 101 | self.data = {} 102 | 103 | for key, default_value, validator, lazy_loading in self._vars: 104 | env_key = f"MANGADEXDL_{key.upper()}" 105 | env_value = os.environ.get(env_key) 106 | if env_value is not None: 107 | if lazy_loading: 108 | self.data[key] = LazyLoadEnv(env_key, env_value, validator) 109 | continue 110 | 111 | self.data[key] = load_env(env_key, env_value, validator) 112 | else: 113 | self.data[key] = default_value 114 | 115 | def read(self, name): 116 | try: 117 | value = self.data[name] 118 | except KeyError: 119 | # This should not happened 120 | # unless user is hacking in the internal API 121 | raise MangaDexException(f'environment variable "{name}" is not exist') 122 | 123 | if not isinstance(value, LazyLoadEnv): 124 | return value 125 | 126 | self.data[name] = value.load() 127 | return self.data[name] 128 | 129 | 130 | _env_orig = EnvironmentVariables() 131 | 132 | 133 | class EnvironmentVariablesProxy: 134 | def __getattr__(self, name): 135 | return _env_orig.read(name) 136 | 137 | def __setattr__(self, name, value): 138 | raise NotImplementedError 139 | 140 | 141 | # Allow library to get values from attr easily 142 | env = EnvironmentVariablesProxy() 143 | 144 | _env_dir = env.config_path 145 | base_path = Path(_env_dir) if _env_dir is not None else (Path.home() / ".mangadex-dl") 146 | 147 | _env_conf_enabled = env.config_enabled 148 | try: 149 | config_enabled = validate_bool(_env_conf_enabled) 150 | except ConfigTypeError: 151 | raise MangaDexException( 152 | "Failed to load env MANGADEXDL_CONFIG_ENABLED, " 153 | f"value {_env_conf_enabled!r} is not valid boolean value" 154 | ) 155 | 156 | 157 | def init(): 158 | # Create config directory 159 | try: 160 | base_path.mkdir(exist_ok=True, parents=True) 161 | except Exception as e: 162 | raise MangaDexException( 163 | f"Failed to create config folder in '{base_path}', " 164 | f"reason: {e}. Make sure you have permission to read & write in that directory " 165 | "or you can set MANGADEXDL_CONFIG_DIR to another path " 166 | "or you can disable config with MANGADEXDL_CONFIG_ENABLED=0" 167 | ) from None 168 | -------------------------------------------------------------------------------- /mangadex_downloader/cover.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .fetcher import get_cover_art 24 | from .language import get_language 25 | from .utils import convert_int_or_float 26 | 27 | cover_qualities = [ 28 | "original", 29 | "512px", 30 | "256px", 31 | ] 32 | 33 | valid_cover_types = cover_qualities + ["none"] 34 | 35 | default_cover_type = "original" 36 | 37 | 38 | class CoverArt: 39 | def __init__(self, cover_id=None, data=None): 40 | if not data: 41 | self.data = get_cover_art(cover_id)["data"] 42 | else: 43 | self.data = data 44 | 45 | self.id = self.data["id"] 46 | attr = self.data["attributes"] 47 | 48 | # Description 49 | self.description = attr["description"] 50 | 51 | # File cover 52 | self.file = attr["fileName"] 53 | 54 | # Locale 55 | self.locale = get_language(attr["locale"]) 56 | 57 | # Manga and user id 58 | self.manga_id = None 59 | self.user_id = None 60 | try: 61 | rels = self.data["relationships"] 62 | for rel in rels: 63 | if rel["type"] == "manga": 64 | self.manga_id = rel["id"] 65 | elif rel["type"] == "user": 66 | self.user_id = rel["id"] 67 | except KeyError: 68 | # There is no relationships in API data 69 | pass 70 | 71 | def __str__(self) -> str: 72 | from .config import config 73 | 74 | msg = f"Cover volume {self.volume}" 75 | if config.language == "all" or config.volume_cover_language == "all": 76 | msg += f" in {self.locale.name} language" 77 | 78 | return msg 79 | 80 | @property 81 | def volume(self): 82 | vol = self.data["attributes"]["volume"] 83 | if vol is not None: 84 | # As far as i know 85 | # Volume manga are integer numbers, not float 86 | try: 87 | return convert_int_or_float(vol) 88 | except ValueError: 89 | pass 90 | 91 | # Weird af volume name 92 | # Example: https://api.mangadex.org/manga/485a777b-e395-4ab1-b262-2a87f53e23c0/aggregate 93 | # (Take a look volume "3Cxx") 94 | return vol 95 | 96 | # No volume 97 | return vol 98 | -------------------------------------------------------------------------------- /mangadex_downloader/errors.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import __repository__, __url_repository__ 24 | 25 | 26 | class UnhandledException(Exception): 27 | """Some errors that the application are unable to handle""" 28 | 29 | def __init__(self, msg): 30 | super().__init__( 31 | str(msg) 32 | + f". Please report this issue to {__url_repository__}/{__repository__}/issues" 33 | ) 34 | 35 | 36 | class MangaDexException(Exception): 37 | """Base exception for MangaDex errors""" 38 | 39 | pass 40 | 41 | 42 | class UnhandledHTTPError(MangaDexException): 43 | """Raised when we unable to handle HTTP errors""" 44 | 45 | pass 46 | 47 | 48 | class HTTPException(MangaDexException): 49 | """HTTP errors""" 50 | 51 | def __init__(self, *args: object, resp=None) -> None: 52 | self.response = resp 53 | super().__init__(*args) 54 | 55 | 56 | class ChapterNotFound(MangaDexException): 57 | """Raised when selected manga has no chapters""" 58 | 59 | pass 60 | 61 | 62 | class InvalidPlaceholders(MangaDexException): 63 | """Raised when filename or directory placeholders is invalid""" 64 | 65 | pass 66 | 67 | 68 | class InvalidMangaDexList(MangaDexException): 69 | """Raised when invalid MangaDex list is found""" 70 | 71 | pass 72 | 73 | 74 | class InvalidManga(MangaDexException): 75 | """Raised when invalid manga is found""" 76 | 77 | pass 78 | 79 | 80 | class InvalidURL(MangaDexException): 81 | """Raised when given mangadex url is invalid""" 82 | 83 | pass 84 | 85 | 86 | class LoginFailed(MangaDexException): 87 | """Raised when login is failed""" 88 | 89 | pass 90 | 91 | 92 | class AlreadyLoggedIn(MangaDexException): 93 | """Raised when user try login but already logged in""" 94 | 95 | pass 96 | 97 | 98 | class NotLoggedIn(MangaDexException): 99 | """Raised when user try to logout when user are not logged in""" 100 | 101 | pass 102 | 103 | 104 | class InvalidFormat(MangaDexException): 105 | """Raised when invalid format is given""" 106 | 107 | pass 108 | 109 | 110 | class UserNotFound(MangaDexException): 111 | """Raised when user are not found in MangaDex""" 112 | 113 | pass 114 | 115 | 116 | class GroupNotFound(MangaDexException): 117 | """Raised when scanlator group are not found in MangaDex""" 118 | 119 | pass 120 | -------------------------------------------------------------------------------- /mangadex_downloader/fetcher.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | from functools import lru_cache 25 | from .errors import ( 26 | ChapterNotFound, 27 | GroupNotFound, 28 | InvalidManga, 29 | InvalidMangaDexList, 30 | MangaDexException, 31 | UserNotFound, 32 | ) 33 | from .network import Net, base_url, origin_url 34 | from .utils import validate_url 35 | 36 | log = logging.getLogger(__name__) 37 | 38 | 39 | def get_manga(manga_id): 40 | url = "{0}/manga/{1}".format(base_url, manga_id) 41 | params = {"includes[]": ["author", "artist", "cover_art"]} 42 | r = Net.mangadex.get(url, params=params) 43 | if r.status_code == 404: 44 | raise InvalidManga('Manga "%s" cannot be found' % manga_id) 45 | return r.json() 46 | 47 | 48 | def get_legacy_id(_type, _id): 49 | supported_types = ["manga", "chapter", "title"] 50 | 51 | # Alias for title 52 | if _type == "manga": 53 | _type = "title" 54 | 55 | if _type not in supported_types: 56 | raise MangaDexException('"%s" is not supported type' % _type) 57 | 58 | # Normally, this can be done from API url. 59 | # But, somehow the API endpoint (/legacy/mapping) 60 | # throwing server error (500) in response. We will use this, until the API gets fixed. 61 | # NOTE: The error only applied to "chapter" type, "manga" type is working fine. 62 | url = "{0}/{1}/{2}".format(origin_url, _type, _id) 63 | 64 | # The process is by sending request to "mangadex.org" (not "api.mangadex.org"), 65 | # if it gets redirected, the legacy id is exist. 66 | # Otherwise the legacy id is not found in MangaDex database 67 | r = Net.mangadex.get(url, allow_redirects=False) 68 | 69 | if r.status_code >= 300: 70 | # Redirected request, the legacy id is exist 71 | location_url = r.headers.get("location") 72 | 73 | # Get the new id 74 | url = validate_url(location_url) 75 | else: 76 | # 200 status code, the legacy id is not exist. 77 | # Raise error based on type url 78 | if _type == "title": 79 | raise InvalidManga('Manga "%s" cannot be found' % _id) 80 | elif _type == "chapter": 81 | raise ChapterNotFound("Chapter %s cannot be found" % _id) 82 | 83 | return url 84 | 85 | 86 | @lru_cache(maxsize=1048) 87 | def get_author(author_id): 88 | url = "{0}/author/{1}".format(base_url, author_id) 89 | r = Net.mangadex.get(url) 90 | return r.json() 91 | 92 | 93 | @lru_cache(maxsize=1048) 94 | def get_user(user_id): 95 | url = "{0}/user/{1}".format(base_url, user_id) 96 | r = Net.mangadex.get(url) 97 | if r.status_code == 404: 98 | raise UserNotFound(f"user {user_id} cannot be found") 99 | return r.json() 100 | 101 | 102 | @lru_cache(maxsize=1048) 103 | def get_cover_art(cover_id): 104 | url = "{0}/cover/{1}".format(base_url, cover_id) 105 | r = Net.mangadex.get(url) 106 | return r.json() 107 | 108 | 109 | def get_chapter(chapter_id): 110 | url = "{0}/chapter/{1}".format(base_url, chapter_id) 111 | params = {"includes[]": ["scanlation_group", "user", "manga"]} 112 | r = Net.mangadex.get(url, params=params) 113 | if r.status_code == 404: 114 | raise ChapterNotFound("Chapter %s cannot be found" % chapter_id) 115 | return r.json() 116 | 117 | 118 | def get_list(list_id): 119 | url = "{0}/list/{1}".format(base_url, list_id) 120 | r = Net.mangadex.get(url) 121 | if r.status_code == 404: 122 | raise InvalidMangaDexList("List %s cannot be found" % list_id) 123 | return r.json() 124 | 125 | 126 | @lru_cache(maxsize=1048) 127 | def get_group(group_id): 128 | url = "{0}/group/{1}".format(base_url, group_id) 129 | r = Net.mangadex.get(url) 130 | if r.status_code == 404: 131 | raise GroupNotFound(f"Scanlator group {group_id} cannot be found") 132 | return r.json() 133 | 134 | 135 | def get_all_chapters(manga_id, lang): 136 | url = "{0}/manga/{1}/aggregate".format(base_url, manga_id) 137 | r = Net.mangadex.get(url, params={"translatedLanguage[]": [lang]}) 138 | return r.json() 139 | 140 | 141 | def get_chapter_images(chapter_id, force_https=False): 142 | url = "{0}/at-home/server/{1}".format(base_url, chapter_id) 143 | r = Net.mangadex.get(url, params={"forcePort443": force_https}) 144 | return r.json() 145 | 146 | 147 | def get_bulk_chapters(chap_ids): 148 | url = "{0}/chapter".format(base_url) 149 | includes = ["scanlation_group", "user"] 150 | content_ratings = ["safe", "suggestive", "erotica", "pornographic"] 151 | params = { 152 | "ids[]": chap_ids, 153 | "limit": 100, 154 | "includes[]": includes, 155 | "contentRating[]": content_ratings, 156 | } 157 | r = Net.mangadex.get(url, params=params) 158 | return r.json() 159 | 160 | 161 | def get_unread_chapters(manga_id): 162 | url = f"{base_url}/manga/{manga_id}/read" 163 | r = Net.mangadex.get(url) 164 | return r.json() 165 | -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/README: -------------------------------------------------------------------------------- 1 | -*-text-*- 2 | GNU FreeFont 3 | 4 | The GNU FreeFont project aims to provide a useful set of free scalable 5 | (i.e., OpenType) fonts covering as much as possible of the ISO 10646/Unicode 6 | UCS (Universal Character Set). 7 | 8 | Statement of Purpose 9 | -------------------- 10 | 11 | The practical reason for putting glyphs together in a single font face is 12 | to conveniently mix symbols and characters from different writing systems, 13 | without having to switch fonts. 14 | 15 | Coverage 16 | -------- 17 | 18 | FreeFont covers the following character ranges 19 | * Latin, Cyrillic, and Arabic, with supplements for many languages 20 | * Greek, Hebrew, Armenian, Georgian, Thaana, Syriac 21 | * Devanagari, Bengali, Gujarati, Gurmukhi, Sinhala, Tamil, Malayalam 22 | * Thai, Tai Le, Kayah Li, Hanunóo, Buginese 23 | * Cherokee, Unified Canadian Aboriginal Syllabics 24 | * Ethiopian, Tifnagh, Vai, Osmanya, Coptic 25 | * Glagolitic, Gothic, Runic, Ugaritic, Old Persian, Phoenician, Old Italic 26 | * Braille, International Phonetic Alphabet 27 | * currency symbols, general punctuation and diacritical marks, dingbats 28 | * mathematical symbols, including much of the TeX repertoire of symbols 29 | * technical symbols: APL, OCR, arrows, 30 | * geometrical shapes, box drawing 31 | * musical symbols, gaming symbols, miscellaneous symbols 32 | etc. 33 | For more detail see 34 | 35 | Editing 36 | ------- 37 | 38 | The free outline font editor, George Williams' FontForge 39 | is used for editing the fonts. 40 | 41 | Design Issues 42 | ------------- 43 | 44 | Which font shapes should be made? Historical style terms like Renaissance 45 | or Baroque letterforms cannot be applied beyond Latin/Cyrillic/Greek 46 | scripts to any greater extent than Kufi or Nashki can be applied beyond 47 | Arabic script; "italic" is strictly meaningful only for Latin letters, 48 | although many scripts such as Cyrillic have a history with "cursive" and 49 | many others with "oblique" faces. 50 | 51 | However, most modern writing systems have typographic formulations for 52 | contrasting uniform and modulated character stroke widths, and since the 53 | advent of the typewriter, most have developed a typographic style with 54 | uniform-width characters. 55 | 56 | Accordingly, the FreeFont family has one monospaced - FreeMono - and two 57 | proportional faces (one with uniform stroke - FreeSans - and one with 58 | modulated stroke - FreeSerif). 59 | 60 | The point of having characters from different writing systems in one font 61 | is that mixed text should look good, and so each FreeFont face contains 62 | characters of similar style and weight. 63 | 64 | Licensing 65 | --------- 66 | 67 | Free UCS scalable fonts is free software; you can redistribute it and/or 68 | modify it under the terms of the GNU General Public License as published 69 | by the Free Software Foundation; either version 3 of the License, or 70 | (at your option) any later version. 71 | 72 | The fonts are distributed in the hope that they will be useful, but 73 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 74 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 75 | for more details. 76 | 77 | You should have received a copy of the GNU General Public License along 78 | with this program; if not, write to the Free Software Foundation, Inc., 79 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 80 | 81 | As a special exception, if you create a document which uses this font, and 82 | embed this font or unaltered portions of this font into the document, this 83 | font does not by itself cause the resulting document to be covered by the 84 | GNU General Public License. This exception does not however invalidate any 85 | other reasons why the document might be covered by the GNU General Public 86 | License. If you modify this font, you may extend this exception to your 87 | version of the font, but you are not obligated to do so. If you do not 88 | wish to do so, delete this exception statement from your version. 89 | 90 | Files and their suffixes 91 | ------------------------ 92 | 93 | The files with .sfd (Spline Font Database) are in FontForge's native format. 94 | They may be used to modify the fonts. 95 | 96 | TrueType fonts are the files with the .ttf (TrueType Font) suffix. These 97 | are ready to use in Linux/Unix, on Apple Mac OS, and on Microsoft Windows 98 | systems. 99 | 100 | OpenType fonts (with suffix .otf) are preferred for use on Linux/Unix, 101 | but *not* for recent Microsoft Windows systems. 102 | See the INSTALL file for more information. 103 | 104 | Web Open Font Format files (with suffix .woff) are for use in Web sites. 105 | See the webfont_guidelines.txt for further information. 106 | 107 | Further information 108 | ------------------- 109 | 110 | Home page of GNU FreeFont: 111 | http://www.gnu.org/software/freefont/ 112 | 113 | More information is at the main project page of Free UCS scalable fonts: 114 | http://savannah.gnu.org/projects/freefont/ 115 | 116 | To report problems with GNU FreeFont, it is best to obtain a Savannah 117 | account and post reports using that account on 118 | https://savannah.gnu.org/bugs/ 119 | 120 | Public discussions about GNU FreeFont may be posted to the mailing list 121 | freefont-bugs@gnu.org 122 | 123 | -------------------------------------------------------------------------- 124 | Original author: Primoz Peterlin 125 | Current administrator: Steve White 126 | 127 | $Id: README,v 1.10 2011-06-12 07:14:12 Stevan_White Exp $ 128 | -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/otf/FreeSans.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/otf/FreeSans.otf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/otf/FreeSansBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/otf/FreeSansBold.otf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/otf/FreeSansBoldOblique.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/otf/FreeSansBoldOblique.otf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/otf/FreeSansOblique.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/otf/FreeSansOblique.otf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSans.ttf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSansBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSansBold.ttf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSansBoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSansBoldOblique.ttf -------------------------------------------------------------------------------- /mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSansOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/fonts/GNU FreeFont/ttf/FreeSansOblique.ttf -------------------------------------------------------------------------------- /mangadex_downloader/format/__init__.py: -------------------------------------------------------------------------------- 1 | from .raw import Raw, RawSingle, RawVolume 2 | from .pdf import PDF, PDFSingle, PDFVolume 3 | from .comic_book import ComicBookArchive, ComicBookArchiveSingle, ComicBookArchiveVolume 4 | from .sevenzip import SevenZip, SevenZipSingle, SevenZipVolume 5 | from .epub import Epub, EpubSingle, EpubVolume 6 | from ..errors import InvalidFormat 7 | 8 | formats = { 9 | "raw": Raw, 10 | "raw-volume": RawVolume, 11 | "raw-single": RawSingle, 12 | "pdf": PDF, 13 | "pdf-volume": PDFVolume, 14 | "pdf-single": PDFSingle, 15 | "cbz": ComicBookArchive, 16 | "cbz-volume": ComicBookArchiveVolume, 17 | "cbz-single": ComicBookArchiveSingle, 18 | "cb7": SevenZip, 19 | "cb7-volume": SevenZipVolume, 20 | "cb7-single": SevenZipSingle, 21 | "epub": Epub, 22 | "epub-volume": EpubVolume, 23 | "epub-single": EpubSingle, 24 | } 25 | 26 | deprecated_formats = [] 27 | 28 | default_save_as_format = "raw" 29 | 30 | 31 | def get_format(fmt): 32 | try: 33 | return formats[fmt] 34 | except KeyError: 35 | raise InvalidFormat( 36 | "invalid save_as format, available are: %s" % set(formats.keys()) 37 | ) 38 | -------------------------------------------------------------------------------- /mangadex_downloader/format/chinfo.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import sys 24 | import textwrap 25 | from pathlib import Path 26 | 27 | from ..utils import get_cover_art_url 28 | from ..network import Net 29 | 30 | from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter 31 | 32 | # Font related 33 | rgb_white = (255, 255, 255) 34 | font_family = "arial.ttf" 35 | 36 | # Text positioning 37 | text_align = "left" 38 | 39 | base_path = Path(__file__).parent.parent.resolve() 40 | font_path = base_path / "fonts/GNU FreeFont" 41 | if sys.platform == "win32": 42 | # According to GNUFreeFont README page 43 | # https://ftp.gnu.org/gnu/freefont/README 44 | # it is recommended to use "ttf" files for Windows 45 | font_ext = ".ttf" 46 | font_path /= "ttf" 47 | else: 48 | # Otherwise just use "otf" files for other OS 49 | font_ext = ".otf" 50 | font_path /= "otf" 51 | 52 | fonts = { 53 | "default": font_path / f"FreeSans{font_ext}", 54 | "bold": font_path / f"FreeSansBold{font_ext}", 55 | "bold_italic": font_path / f"FreeSansBoldOblique{font_ext}", 56 | "italic": font_path / f"FreeSansOblique{font_ext}", 57 | } 58 | 59 | 60 | def load_font(type, size): 61 | font = fonts[type] 62 | loc = str(font.resolve()) 63 | return ImageFont.truetype(loc, size) 64 | 65 | 66 | def textwrap_newlines(text, width): 67 | """This function is designed to split with newlines instead of list""" 68 | new_text = "" 69 | result = textwrap.wrap(text, width) 70 | for word in result: 71 | new_text += word + "\n" 72 | 73 | return new_text 74 | 75 | 76 | def draw_multiline_text(font, image, text, width_pos, height_pos, split_size): 77 | new_text = textwrap_newlines(text, width=split_size) 78 | draw = ImageDraw.Draw(image) 79 | draw.multiline_text( 80 | xy=(width_pos, height_pos), 81 | text=new_text, 82 | fill=rgb_white, 83 | font=font, 84 | align="left", 85 | ) 86 | return draw.multiline_textbbox( 87 | (width_pos, height_pos), new_text, font, align="left" 88 | ) 89 | 90 | 91 | def get_chapter_info(manga, cover, chapter): 92 | cover_url = get_cover_art_url(manga.id, cover, "original") 93 | r = Net.mangadex.get(cover_url, stream=True) 94 | image = Image.open(r.raw) 95 | image = image.convert("RGBA") 96 | 97 | # resize image to fixed 1000px width (keeping aspect ratio) 98 | # so font sizes and text heights match for all covers 99 | aspect_ratio = image.height / image.width 100 | new_width = 1000 101 | new_height = new_width * aspect_ratio 102 | 103 | image = image.resize((int(new_width), int(new_height)), Image.Resampling.LANCZOS) 104 | 105 | # apply blur and darken filters 106 | image = image.filter(ImageFilter.GaussianBlur(6)) 107 | image = ImageEnhance.Brightness(image).enhance(0.3) 108 | 109 | title_text = chapter.manga_title 110 | if len(title_text) > 85: 111 | title_font = load_font("bold", size=80) 112 | else: 113 | title_font = load_font("bold", size=90) 114 | title_bbox = draw_multiline_text( 115 | font=title_font, 116 | image=image, 117 | text=title_text, 118 | width_pos=40, 119 | height_pos=40, 120 | split_size=20, 121 | ) 122 | 123 | chinfo_font = load_font("default", size=45) 124 | chinfo_text = chapter.simple_name 125 | if chapter.title: 126 | chinfo_text += f" - {chapter.title}" 127 | chinfo_bbox = draw_multiline_text( 128 | font=chinfo_font, 129 | image=image, 130 | text=chinfo_text, 131 | width_pos=40, 132 | height_pos=title_bbox[3] + 40, 133 | split_size=40, 134 | ) 135 | 136 | scanlatedby_font = load_font("italic", size=30) 137 | scanlatedby_text = "Scanlated by:" 138 | scanlatedby_bbox = draw_multiline_text( 139 | font=scanlatedby_font, 140 | image=image, 141 | text=scanlatedby_text, 142 | width_pos=40, 143 | height_pos=chinfo_bbox[3] + 30, 144 | split_size=40, 145 | ) 146 | 147 | group_bbox = None 148 | for group in chapter.groups: 149 | group_font = load_font("bold", size=50) 150 | group_text = group.name 151 | if group_bbox is None: 152 | height_pos = scanlatedby_bbox[3] + 15 153 | else: 154 | height_pos = group_bbox[3] + 5 155 | 156 | group_bbox = draw_multiline_text( 157 | font=group_font, 158 | image=image, 159 | text=group_text, 160 | width_pos=40, 161 | height_pos=height_pos, 162 | split_size=30, 163 | ) 164 | 165 | logo = base_path / "images/mangadex-logo.png" 166 | logo_image = Image.open(logo) 167 | logo_image = logo_image.convert("RGBA").resize((120, 120)) 168 | image.alpha_composite( 169 | im=logo_image, dest=(40, (image.height - (logo_image.height + 30))) 170 | ) 171 | 172 | return image 173 | -------------------------------------------------------------------------------- /mangadex_downloader/format/placeholders.py: -------------------------------------------------------------------------------- 1 | # Utility for formats placeholders 2 | 3 | # the concept: 4 | # - create 2 class inherited from list (array mutable) 5 | # - create 3 attributes (first, last, current) inside each classes 6 | # - Use the class for placeholders object for volume and single formats 7 | # - profit 8 | 9 | 10 | class _Base(list): 11 | def __init__(self, *args, **kwargs): 12 | self.first = None 13 | self.last = None 14 | 15 | super().__init__(*args, **kwargs) 16 | 17 | 18 | # For `volume` placeholder in volume format 19 | class VolumePlaceholder: 20 | def __init__(self, value): 21 | self.__value = value 22 | self.chapters = VolumeChaptersPlaceholder() 23 | 24 | def __str__(self) -> str: 25 | return str(self.__value) 26 | 27 | 28 | # For `volume.chapters` placeholder in volume format 29 | class VolumeChaptersPlaceholder(_Base): 30 | pass 31 | 32 | 33 | # For `chapters` placeholder in single format 34 | class SingleChaptersPlaceholder(_Base): 35 | pass 36 | -------------------------------------------------------------------------------- /mangadex_downloader/format/sevenzip.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import os 25 | 26 | from .base import ConvertedChaptersFormat, ConvertedVolumesFormat, ConvertedSingleFormat 27 | from .utils import get_chapter_info, get_volume_cover 28 | from ..utils import create_directory 29 | from ..progress_bar import progress_bar_manager as pbm 30 | 31 | try: 32 | import py7zr 33 | except ImportError: 34 | PY7ZR_OK = False 35 | else: 36 | PY7ZR_OK = True 37 | 38 | 39 | class py7zrNotInstalled(Exception): 40 | """Raised when py7zr is not installed""" 41 | 42 | pass 43 | 44 | 45 | log = logging.getLogger(__name__) 46 | 47 | 48 | class SevenZipFile: 49 | file_ext = ".cb7" 50 | 51 | def check_dependecies(self): 52 | if not PY7ZR_OK: 53 | raise py7zrNotInstalled("py7zr is not installed") 54 | 55 | def convert(self, images, path): 56 | pbm.set_convert_total(len(images)) 57 | progress_bar = pbm.get_convert_pb(recreate=not pbm.stacked) 58 | 59 | for im_path in images: 60 | with py7zr.SevenZipFile( 61 | path, "a" if os.path.exists(path) else "w" 62 | ) as zip_obj: 63 | zip_obj.write(im_path, im_path.name) 64 | progress_bar.update(1) 65 | 66 | def check_write_chapter_info(self, path, target): 67 | if not os.path.exists(path): 68 | return True 69 | 70 | with py7zr.SevenZipFile(path, "r") as zip_obj: 71 | return target not in zip_obj.getnames() 72 | 73 | def insert_ch_info_img(self, images, chapter, path, count): 74 | """Insert chapter info (cover) image""" 75 | img_name = count.get() + ".png" 76 | img_path = path / img_name 77 | 78 | if self.config.use_chapter_cover: 79 | get_chapter_info(self.manga, chapter, img_path) 80 | images.append(img_path) 81 | count.increase() 82 | 83 | def insert_vol_cover_img(self, images, volume, path, count): 84 | """Insert volume cover""" 85 | img_name = count.get() + ".png" 86 | img_path = path / img_name 87 | 88 | if self.config.use_volume_cover: 89 | get_volume_cover(self.manga, volume, img_path, self.replace) 90 | images.append(img_path) 91 | count.increase() 92 | 93 | 94 | class SevenZip(ConvertedChaptersFormat, SevenZipFile): 95 | def on_finish(self, file_path, chapter, images): 96 | self.worker.submit(lambda: self.convert(images, file_path)) 97 | 98 | 99 | class SevenZipVolume(ConvertedVolumesFormat, SevenZipFile): 100 | def __init__(self, *args, **kwargs): 101 | super().__init__(*args, **kwargs) 102 | 103 | # See `PDFVolume.__init__()` why i did this 104 | self.images = [] 105 | 106 | def on_prepare(self, file_path, volume, count): 107 | self.images.clear() 108 | 109 | volume_name = self.get_volume_name(volume) 110 | self.volume_path = create_directory(volume_name, self.path) 111 | 112 | self.insert_vol_cover_img(self.images, volume, self.volume_path, count) 113 | 114 | def on_iter_chapter(self, file_path, chapter, count): 115 | self.insert_ch_info_img(self.images, chapter, self.volume_path, count) 116 | 117 | def on_received_images(self, file_path, chapter, images): 118 | self.images.extend(images) 119 | 120 | def on_convert(self, file_path, volume, images): 121 | self.worker.submit(lambda: self.convert(self.images, file_path)) 122 | 123 | 124 | class SevenZipSingle(ConvertedSingleFormat, SevenZipFile): 125 | def __init__(self, *args, **kwargs): 126 | # See `PDFVolume.__init__()` why i did this 127 | self.images = [] 128 | 129 | self.images_path = None 130 | 131 | super().__init__(*args, **kwargs) 132 | 133 | def on_prepare(self, file_path, base_path): 134 | self.images.clear() 135 | 136 | self.images_path = base_path 137 | 138 | def on_iter_chapter(self, file_path, chapter, count): 139 | self.insert_ch_info_img(self.images, chapter, self.images_path, count) 140 | 141 | def on_received_images(self, file_path, chapter, images): 142 | self.images.extend(images) 143 | return super().on_received_images(file_path, chapter, images) 144 | 145 | def on_finish(self, file_path, images): 146 | self.worker.submit(lambda: self.convert(self.images, file_path)) 147 | -------------------------------------------------------------------------------- /mangadex_downloader/group.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .fetcher import get_group 24 | from .utils import get_local_attr 25 | 26 | 27 | class Group: 28 | def __init__(self, group_id=None, data=None): 29 | if not data: 30 | self.data = get_group(group_id)["data"] 31 | else: 32 | self.data = data 33 | 34 | self.id = self.data["id"] 35 | attr = self.data["attributes"] 36 | 37 | # Name 38 | self.name = attr["name"] 39 | 40 | # Alternative names 41 | self.alt_names = [get_local_attr(i) for i in attr["altNames"]] 42 | 43 | # is it locked ? 44 | self.locked = attr["locked"] 45 | 46 | # Website 47 | self.url = attr["website"] 48 | 49 | # description 50 | self.description = attr["description"] 51 | -------------------------------------------------------------------------------- /mangadex_downloader/images/mangadex-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/images/mangadex-logo.png -------------------------------------------------------------------------------- /mangadex_downloader/json_op.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # This module containing JSON operations 24 | # where the library use `json` or `orjson` (if available) streamlined 25 | # without using different behaviour each libraries 26 | # ex: dumped JSON (orjson) -> bytes 27 | # ex: dumped JSON (json) -> str 28 | 29 | import json 30 | import chardet 31 | from typing import Union 32 | 33 | try: 34 | import orjson 35 | except ImportError: 36 | ORJSON_OK = False 37 | else: 38 | ORJSON_OK = True 39 | 40 | __all__ = ("loads", "dumps", "JSONDecodeError", "JSONEncodeError") 41 | 42 | # List of errors 43 | if ORJSON_OK: 44 | JSONDecodeError = orjson.JSONDecodeError 45 | JSONEncodeError = orjson.JSONEncodeError 46 | else: 47 | JSONDecodeError = json.JSONDecodeError 48 | JSONEncodeError = TypeError 49 | 50 | 51 | def _get_encoding(content): 52 | data = bytearray(content) 53 | detector = chardet.UniversalDetector() 54 | while data: 55 | feed = data[:4096] 56 | detector.feed(feed) 57 | if detector.done: 58 | break 59 | 60 | del data[:4096] 61 | 62 | if not detector.done: 63 | detector.close() 64 | 65 | return detector.result["encoding"] 66 | 67 | 68 | def loads(content: Union[str, bytes]) -> dict: 69 | """Take a bytes or str parameter will result a loaded dict JSON""" 70 | if ORJSON_OK: 71 | return orjson.loads(content) 72 | 73 | return json.loads(content) 74 | 75 | 76 | def dumps(content: dict, convert_str=True) -> Union[str, bytes]: 77 | """Take a dict parameter will result in str or bytes 78 | (str, if param `convert_str` is True, else bytes)""" 79 | dumped = None 80 | if ORJSON_OK: 81 | dumped = orjson.dumps(content) 82 | else: 83 | dumped = json.dumps(content) 84 | 85 | if convert_str and ORJSON_OK: 86 | # Do technique convert str from bytes 87 | # because by default, orjson is returning bytes data 88 | 89 | # Get encoding 90 | encoding = _get_encoding(dumped) 91 | 92 | # Begin the decoding 93 | return dumped.decode(encoding) 94 | elif not convert_str and not ORJSON_OK: 95 | return dumped.encode("utf-8") 96 | 97 | # Return the data as-is 98 | return dumped 99 | -------------------------------------------------------------------------------- /mangadex_downloader/language.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from enum import Enum 24 | 25 | 26 | # Adapted from https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt 27 | class Language(Enum): 28 | """List of MangaDex languages""" 29 | 30 | # The reason why in the end of each variables here 31 | # has "#:", because to showed up in sphinx documentation. 32 | English = "en" #: 33 | Japanese = "ja" #: 34 | Polish = "pl" #: 35 | SerboCroatian = "sh" #: 36 | Dutch = "nl" #: 37 | Italian = "it" #: 38 | Russian = "ru" #: 39 | German = "de" #: 40 | Hungarian = "hu" #: 41 | French = "fr" #: 42 | Finnish = "fi" #: 43 | Vietnamese = "vi" #: 44 | Greek = "el" #: 45 | Bulgarian = "bg" #: 46 | SpanishSpain = "es" #: 47 | PortugueseBrazil = "pt-br" #: 48 | PortuguesePortugal = "pt" #: 49 | Swedish = "sv" #: 50 | Arabic = "ar" #: 51 | Danish = "da" #: 52 | ChineseSimplified = "zh" #: 53 | Bengali = "bn" #: 54 | Romanian = "ro" #: 55 | Czech = "cs" #: 56 | Mongolian = "mn" #: 57 | Turkish = "tr" #: 58 | Indonesian = "id" #: 59 | Korean = "ko" #: 60 | SpanishLTAM = "es-la" #: 61 | Persian = "fa" #: 62 | Malay = "ms" #: 63 | Thai = "th" #: 64 | Catalan = "ca" #: 65 | Filipino = "tl" #: 66 | ChineseTraditional = "zh-hk" #: 67 | Ukrainian = "uk" #: 68 | Burmese = "my" #: 69 | Lithuanian = "lt" #: 70 | Hebrew = "he" #: 71 | Hindi = "hi" #: 72 | Norwegian = "no" #: 73 | Nepali = "ne" #: 74 | Kazakh = "kk" #: 75 | Tamil = "ta" #: 76 | Azerbaijani = "az" #: 77 | Slovak = "sk" #: 78 | Georgian = "ka" #: 79 | 80 | # Other language 81 | Other = None #: 82 | 83 | # While all languages above is valid languages, 84 | # this one is not actually a language 85 | # it's just alias for all languages 86 | All = "all" 87 | 88 | 89 | class RomanizedLanguage(Enum): 90 | RomanizedJapanese = "ja-ro" 91 | RomanizedKorean = "ko-ro" 92 | RomanizedChinese = "zh-ro" 93 | 94 | 95 | def get_language(lang): 96 | try: 97 | return Language[lang] 98 | except KeyError: 99 | pass 100 | return Language(lang) 101 | 102 | 103 | def get_details_language(lang): 104 | # Retrieve base languages first 105 | try: 106 | return get_language(lang) 107 | except ValueError: 108 | pass 109 | 110 | # Retrieve romanized language 111 | try: 112 | return RomanizedLanguage[lang] 113 | except KeyError: 114 | pass 115 | return RomanizedLanguage(lang) 116 | -------------------------------------------------------------------------------- /mangadex_downloader/mdlist.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .fetcher import get_list 24 | 25 | 26 | # Why "MangaDexList" ? why not "List" ? 27 | # to prevent typing.List conflict 28 | class MangaDexList: 29 | def __init__(self, _id=None, data=None): 30 | if _id is not None: 31 | data = get_list(_id)["data"] 32 | 33 | self.id = data.get("id") 34 | self.data = data 35 | 36 | attr = data["attributes"] 37 | 38 | self.name = attr.get("name") 39 | 40 | self.visibility = attr.get("visibility") 41 | 42 | def total(self) -> int: 43 | """Return total manga in the list""" 44 | rels = self.data["relationships"] 45 | 46 | count = 0 47 | for rel in rels: 48 | _type = rel["type"] 49 | 50 | if _type == "manga": 51 | count += 1 52 | 53 | return count 54 | 55 | def __str__(self) -> str: 56 | return f"MDList: {self.name} ({self.total()} total)" 57 | 58 | def __repr__(self) -> str: 59 | return f"MDList: {self.name} ({self.total()} total)" 60 | 61 | def iter_manga(self): 62 | """Yield :class:`Manga` from a list""" 63 | # "Circular imports" problem 64 | from .iterator import IteratorMangaFromList 65 | 66 | return IteratorMangaFromList(data=self.data.copy()) 67 | -------------------------------------------------------------------------------- /mangadex_downloader/path/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/path/__init__.py -------------------------------------------------------------------------------- /mangadex_downloader/path/op.py: -------------------------------------------------------------------------------- 1 | from .placeholders import Placeholder, create_placeholders_kwargs 2 | 3 | 4 | def get_filename(manga, obj, file_ext, format="chapter"): 5 | from ..config import config 6 | 7 | # Append `file_ext` to placeholders 8 | attributes = Placeholder.get_allowed_attributes() 9 | 10 | cli_option = f"--filename-{format}" 11 | 12 | fmt_kwargs = create_placeholders_kwargs(manga, attributes, cli_option=cli_option) 13 | fmt_kwargs["file_ext"] = Placeholder( 14 | obj=file_ext, 15 | name="file_ext", 16 | allowed_attributes=attributes, 17 | cli_option=cli_option, 18 | ) 19 | 20 | p = Placeholder( 21 | obj=obj, 22 | name=format.capitalize(), 23 | allowed_attributes=attributes, 24 | cli_option=cli_option, 25 | ) 26 | 27 | if format == "volume": 28 | # to bypass error "you must use attribute in placeholder bla bla bla" 29 | p.has_attr = False 30 | 31 | # Special treatment for single format 32 | # Because `chapters` is the only placeholders that can be used 33 | # for single format 34 | if format == "single": 35 | fmt_kwargs["chapters"] = p 36 | else: 37 | fmt_kwargs[format] = p 38 | 39 | return getattr(config, f"filename_{format}").format(**fmt_kwargs) 40 | 41 | 42 | def get_path(manga): 43 | from ..config import config 44 | 45 | attributes = Placeholder.get_allowed_attributes( 46 | file_ext=False, chapter=False, volume=False 47 | ) 48 | fmt_kwargs = create_placeholders_kwargs( 49 | manga, attributes=attributes, cli_option="--path" 50 | ) 51 | 52 | return config.path.format(**fmt_kwargs) 53 | 54 | 55 | def get_manga_info_filepath(manga): 56 | from ..config import config 57 | 58 | attributes = Placeholder.get_allowed_attributes( 59 | file_ext=False, chapter=False, volume=False 60 | ) 61 | fmt_kwargs = create_placeholders_kwargs( 62 | manga, attributes=attributes, cli_option="--manga-info-filepath" 63 | ) 64 | 65 | # Workaround for "mihon" manga info format 66 | # If we do not do this, the file extension will end up as ".mihon" instead of ".json" 67 | if config.manga_info_format == "mihon": 68 | manga_info_format = "json" 69 | else: 70 | manga_info_format = config.manga_info_format 71 | 72 | # Because this is modified placeholders 73 | # in order for get_manga_info_filepath() to work 74 | # We need to include some attributes before creating placeholders 75 | # to prevent crashing (KeyError) when initialize Placeholder class 76 | attributes["manga_info_format"] = None 77 | attributes["download_path"] = None 78 | 79 | fmt_kwargs["manga_info_format"] = Placeholder( 80 | obj=manga_info_format, 81 | name="manga_info_format", 82 | allowed_attributes=attributes, 83 | ) 84 | fmt_kwargs["download_path"] = Placeholder( 85 | obj=get_path(manga), name="download_path", allowed_attributes=attributes 86 | ) 87 | 88 | return config.manga_info_filepath.format(**fmt_kwargs) 89 | -------------------------------------------------------------------------------- /mangadex_downloader/path/placeholders.py: -------------------------------------------------------------------------------- 1 | from pathvalidate import sanitize_filename 2 | from ..utils import comma_separated_text 3 | from ..language import Language as L, get_language 4 | from ..network import Net 5 | from ..errors import InvalidPlaceholders 6 | 7 | 8 | class Language: 9 | def __init__(self, lang: L): 10 | if isinstance(lang, str): 11 | lang = get_language(lang) 12 | 13 | self.simple = lang.value 14 | self.full = lang.name 15 | 16 | 17 | def _get_volume(x): 18 | if x is None: 19 | return "No volume" 20 | 21 | return f"Vol. {x}" 22 | 23 | 24 | def _get_or_unknown(x): 25 | if not x: 26 | return "Unknown" 27 | 28 | return x 29 | 30 | 31 | def _split_text(text): 32 | return comma_separated_text(text, use_bracket=False) 33 | 34 | 35 | class Placeholder: 36 | def __init__(self, obj, name=None, allowed_attributes=None, cli_option=None): 37 | self.obj = obj 38 | self.has_attr = True 39 | self.cli_option = cli_option 40 | self.obj_name = name if name else obj.__class__.__name__ 41 | 42 | if allowed_attributes is not None: 43 | attr = allowed_attributes 44 | else: 45 | attr = self.get_allowed_attributes() 46 | 47 | attr = attr[self.obj_name] 48 | 49 | if not isinstance(attr, dict): 50 | # It's not dict type 51 | # see `Format` in self.get_allowed_attributes() 52 | # var "attr" in this case is modifier function 53 | value = attr(obj) if attr is not None else obj 54 | 55 | setattr(self, self.obj_name, value) 56 | self.has_attr = False 57 | return 58 | 59 | if obj is None: 60 | # We can't do anything if origin object is null 61 | return 62 | 63 | # Copy "allowed" origin attributes to this class 64 | for attr, modifier in attr.items(): 65 | value = getattr(obj, attr) 66 | 67 | if modifier: 68 | value = modifier(value) 69 | 70 | setattr(self, attr, value) 71 | 72 | def __getitem__(self, index): 73 | return self.obj.__getitem__(index) 74 | 75 | def __str__(self): 76 | if self.has_attr: 77 | raise InvalidPlaceholders( 78 | "You must use attribute for placeholder " 79 | f"{self.obj_name!r} in {self.cli_option!r} option" 80 | ) 81 | return self.obj.__str__() 82 | 83 | @classmethod 84 | def get_allowed_attributes( 85 | cls, 86 | manga=True, 87 | chapter=True, 88 | volume=True, 89 | single=True, 90 | user=True, 91 | language=True, 92 | format=True, 93 | file_ext=True, 94 | ): 95 | attr = {} 96 | 97 | if manga: 98 | attr["Manga"] = { 99 | # Allowed attribute and modifier (function) 100 | "title": sanitize_filename, 101 | "id": None, 102 | "alternative_titles": None, 103 | "description": sanitize_filename, 104 | "authors": _split_text, 105 | "artists": _split_text, 106 | "cover": None, 107 | "genres": _split_text, 108 | "status": None, 109 | "content_rating": None, 110 | "tags": lambda x: _split_text([i.name for i in x]), 111 | } 112 | 113 | if chapter: 114 | attr["Chapter"] = { 115 | "chapter": _get_or_unknown, 116 | "volume": _get_or_unknown, 117 | "title": sanitize_filename, 118 | "pages": None, 119 | "language": None, 120 | "name": None, 121 | "simple_name": None, 122 | "groups_name": sanitize_filename, 123 | "manga_title": sanitize_filename, 124 | } 125 | 126 | if user: 127 | attr["User"] = {"id": None, "name": None} 128 | 129 | if language: 130 | attr["Language"] = {"simple": None, "full": None} 131 | 132 | if format: 133 | attr["Format"] = None 134 | 135 | if file_ext: 136 | attr["file_ext"] = None 137 | 138 | if volume: 139 | attr["Volume"] = {"chapters": None} 140 | 141 | if single: 142 | attr["Single"] = {"first": None, "last": None} 143 | 144 | return attr 145 | 146 | 147 | def create_placeholders_kwargs(manga, attributes=None, cli_option=None): 148 | from ..config import config 149 | 150 | return { 151 | "manga": Placeholder( 152 | obj=manga, allowed_attributes=attributes, cli_option=cli_option 153 | ), 154 | "user": Placeholder( 155 | obj=Net.mangadex.user, 156 | name="User", 157 | allowed_attributes=attributes, 158 | cli_option=cli_option, 159 | ), 160 | "language": Placeholder( 161 | obj=Language(manga.chapters.language), 162 | name="Language", 163 | allowed_attributes=attributes, 164 | cli_option=cli_option, 165 | ), 166 | "format": Placeholder( 167 | obj=config.save_as, 168 | name="Format", 169 | allowed_attributes=attributes, 170 | cli_option=cli_option, 171 | ), 172 | } 173 | -------------------------------------------------------------------------------- /mangadex_downloader/tag.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from functools import lru_cache 24 | from typing import List 25 | 26 | from .network import Net, base_url 27 | from .utils import get_local_attr 28 | 29 | 30 | class Tag: 31 | def __init__(self, data): 32 | self.id = data["id"] 33 | 34 | attr = data["attributes"] 35 | 36 | self.name = get_local_attr(attr["name"]) 37 | self.description = get_local_attr(attr["description"]) 38 | self.group = attr["group"] 39 | 40 | def __repr__(self) -> str: 41 | return self.name 42 | 43 | 44 | @lru_cache(maxsize=4096) 45 | def get_all_tags() -> List[Tag]: 46 | tags = [] 47 | r = Net.mangadex.get(f"{base_url}/manga/tag") 48 | data = r.json() 49 | 50 | for item in data["data"]: 51 | tags.append(Tag(item)) 52 | 53 | return tags 54 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | from tqdm import tqdm 25 | from pathlib import Path 26 | 27 | from .legacy import DownloadTrackerJSON, FileInfo, ChapterInfo, ImageInfo 28 | from .sqlite import DownloadTrackerSQLite 29 | 30 | from ..utils import delete_file 31 | 32 | log = logging.getLogger(__name__) 33 | 34 | 35 | def _migrate_legacy_tracker_raw( 36 | legacy_tracker: DownloadTrackerJSON, 37 | new_tracker: DownloadTrackerSQLite, 38 | manga_id: str, 39 | path: Path, 40 | progress_bar: tqdm, 41 | ): 42 | fmt = legacy_tracker.format 43 | 44 | fi: FileInfo 45 | for fi in legacy_tracker.data["files"]: 46 | # File info 47 | new_tracker.add_file_info( 48 | name=fi.name, manga_id=manga_id, ch_id=fi.id, hash=fi.hash 49 | ) 50 | 51 | if "single" in fmt or "volume" in fmt: 52 | # Chapter info 53 | ci_data = [] 54 | ci: ChapterInfo 55 | for ci in fi.chapters: 56 | ci_data.append((ci.name, ci.id, fi.name)) 57 | new_tracker.add_chapters_info(ci_data) 58 | 59 | # Image info 60 | ii_data = [] 61 | ii: ImageInfo 62 | for ii in fi.images: 63 | ii_data.append((ii.name, ii.hash, ii.chapter_id, fi.name)) 64 | new_tracker.add_images_info(ii_data) 65 | 66 | new_tracker.toggle_complete(fi.name, True) 67 | progress_bar.update(1) 68 | 69 | delete_file(legacy_tracker.file) 70 | 71 | 72 | def _migrate_legacy_tracker_any( 73 | legacy_tracker: DownloadTrackerJSON, 74 | new_tracker: DownloadTrackerSQLite, 75 | manga_id: str, 76 | path: Path, 77 | progress_bar: tqdm, 78 | ): 79 | fmt = legacy_tracker.format 80 | 81 | fi: FileInfo 82 | for fi in legacy_tracker.data["files"]: 83 | # File info 84 | new_tracker.add_file_info( 85 | name=fi.name, manga_id=manga_id, ch_id=fi.id, hash=fi.hash 86 | ) 87 | 88 | if "single" in fmt or "volume" in fmt: 89 | # Chapter info 90 | ci_data = [] 91 | ci: ChapterInfo 92 | for ci in fi.chapters: 93 | ci_data.append((ci.name, ci.id, fi.name)) 94 | new_tracker.add_chapters_info(ci_data) 95 | 96 | new_tracker.toggle_complete(fi.name, True) 97 | progress_bar.update(1) 98 | 99 | delete_file(legacy_tracker.file) 100 | 101 | 102 | def _migrate_legacy_tracker(fmt, path): 103 | from ..chapter import Chapter 104 | 105 | new_tracker = DownloadTrackerSQLite(fmt, path) 106 | legacy_tracker = DownloadTrackerJSON(fmt, path) 107 | 108 | if legacy_tracker.empty: 109 | # We don't wanna migrate if it's empty 110 | # Just delete the old tracker file 111 | delete_file(legacy_tracker.file) 112 | del legacy_tracker 113 | 114 | return new_tracker 115 | 116 | log.info("Legacy download tracker detected, migrating to new one...") 117 | log.warning( 118 | "Do not turn it off while migrating " 119 | "or the migration will be failed and download tracker data will be lost" 120 | ) 121 | progress_bar = tqdm( 122 | total=len(legacy_tracker.data["files"]), unit="files", desc="progress" 123 | ) 124 | 125 | manga_id = None 126 | chapter_id = None 127 | 128 | # Since we only want to get manga id from old tracker 129 | # (because the old tracker doesn't have manga id) 130 | # we just fetch from single chapter id to prevent rate-limited from the API 131 | fi = legacy_tracker.data["files"][0] 132 | if "single" in fmt or "volume" in fmt: 133 | # Any `single` or `volume` formats 134 | # (raw-single, raw-volume, etc) 135 | for chapter_info in fi.chapters: 136 | chapter_id = chapter_info.id 137 | break 138 | else: 139 | # Any `chapter` formats 140 | # (raw, pdf, epub, etc) 141 | chapter_id = fi.id 142 | 143 | chapter = Chapter(_id=chapter_id) 144 | manga_id = chapter.manga_id 145 | 146 | args_migrate = (legacy_tracker, new_tracker, manga_id, path, progress_bar) 147 | 148 | # Begin migrating 149 | if "raw" in fmt: 150 | _migrate_legacy_tracker_raw(*args_migrate) 151 | else: 152 | _migrate_legacy_tracker_any(*args_migrate) 153 | 154 | return new_tracker 155 | 156 | 157 | def get_tracker(fmt, path): 158 | legacy_path = DownloadTrackerJSON.get_tracker_path(fmt, path) 159 | if legacy_path.exists(): 160 | return _migrate_legacy_tracker(fmt, path) 161 | 162 | return DownloadTrackerSQLite(fmt, path) 163 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/info_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/tracker/info_data/__init__.py -------------------------------------------------------------------------------- /mangadex_downloader/tracker/info_data/legacy.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Union, List 24 | from dataclasses import dataclass 25 | 26 | 27 | class BaseInfo: 28 | """Base info for download tracker in JSON format""" 29 | 30 | @property 31 | def data(self): 32 | """Return data that is ready to compare""" 33 | raise NotImplementedError 34 | 35 | def __eq__(self, o) -> bool: 36 | if not isinstance(o, BaseInfo): 37 | raise NotImplementedError 38 | 39 | return self.data == o.data 40 | 41 | 42 | @dataclass 43 | class ImageInfo(BaseInfo): 44 | name: str 45 | hash: str 46 | chapter_id: str 47 | 48 | @property 49 | def data(self): 50 | return {"name": self.name, "hash": self.hash, "chapter_id": self.chapter_id} 51 | 52 | 53 | @dataclass 54 | class ChapterInfo(BaseInfo): 55 | name: str 56 | id: str 57 | 58 | @property 59 | def data(self): 60 | return {"name": self.name, "id": self.id} 61 | 62 | def __eq__(self, o) -> bool: 63 | if isinstance(o, str): 64 | return self.id == o 65 | 66 | return super().__eq__(o) 67 | 68 | 69 | @dataclass 70 | class FileInfo(BaseInfo): 71 | name: str 72 | id: str 73 | hash: str 74 | completed: str 75 | images: Union[None, List[ImageInfo]] 76 | chapters: Union[None, List[ChapterInfo]] 77 | 78 | def __post_init__(self): 79 | if self.images is not None: 80 | self.images = [ImageInfo(**i) for i in self.images] 81 | 82 | if self.chapters is not None: 83 | self.chapters = [ChapterInfo(**i) for i in self.chapters] 84 | 85 | @property 86 | def data(self): 87 | return { 88 | "name": self.name, 89 | "id": self.id, 90 | "hash": self.hash, 91 | "completed": self.completed, 92 | "images": self.images, 93 | "chapters": self.chapters, 94 | } 95 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/info_data/sqlite.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from typing import Union, List 24 | from datetime import datetime 25 | from dataclasses import dataclass 26 | 27 | 28 | @dataclass 29 | class ImageInfo: 30 | name: str 31 | hash: str 32 | chapter_id: str 33 | 34 | def __eq__(self, o): 35 | if not isinstance(o, ImageInfo): 36 | raise NotImplementedError 37 | 38 | return self.name == o.name and self.chapter_id == o.chapter_id 39 | 40 | 41 | @dataclass 42 | class ChapterInfo: 43 | name: str 44 | id: str 45 | 46 | def __eq__(self, o): 47 | compare = None 48 | 49 | if isinstance(o, ChapterInfo): 50 | compare = self.name == o.name and self.id == o.id 51 | elif isinstance(o, str): 52 | compare = self.id == o 53 | else: 54 | raise NotImplementedError 55 | 56 | return compare 57 | 58 | 59 | class FileInfoCompletedField: 60 | def __init__(self, *args): 61 | pass 62 | 63 | def __set_name__(self, owner, name): 64 | self._name = "_" + name 65 | 66 | def __get__(self, obj, type): 67 | if obj is None: 68 | # By default file_info.completed is False 69 | # because the download is not completed (just started) 70 | return False 71 | 72 | val = getattr(obj, self._name) 73 | 74 | if not val: 75 | return 0 76 | else: 77 | return 1 78 | 79 | def __set__(self, obj, value): 80 | if not isinstance(value, bool): 81 | raise ValueError("value must be boolean type") 82 | 83 | setattr(obj, self._name, value) 84 | 85 | 86 | class FileInfoDatetimeField: 87 | def __init__(self, *args): 88 | pass 89 | 90 | def __set_name__(self, owner, name): 91 | self._name = "_" + name 92 | 93 | def __get__(self, obj, type): 94 | val = getattr(obj, self._name) 95 | 96 | return val.isoformat() 97 | 98 | def __set__(self, obj, value): 99 | val = datetime.fromisoformat(value) 100 | 101 | setattr(obj, self._name, val) 102 | 103 | 104 | @dataclass 105 | class FileInfo: 106 | name: str 107 | manga_id: str 108 | ch_id: str 109 | hash: str 110 | last_download_time: datetime 111 | completed: int 112 | volume: int 113 | images: Union[None, List[ImageInfo]] 114 | chapters: Union[None, List[ChapterInfo]] 115 | 116 | @classmethod 117 | def dummy(cls): 118 | """Create dummy FileInfo for all formats if --no-track is used""" 119 | return cls() 120 | 121 | def __post_init__(self): 122 | if self.images is not None: 123 | self.images = [ImageInfo(*(i[0], i[1], i[2])) for i in self.images] 124 | 125 | if self.chapters is not None: 126 | self.chapters = [ChapterInfo(*(i[0], i[1])) for i in self.chapters] 127 | 128 | if self.last_download_time is not None: 129 | self.last_download_time = datetime.fromisoformat(self.last_download_time) 130 | 131 | def __eq__(self, o): 132 | if not isinstance(o, FileInfo): 133 | raise NotImplementedError 134 | 135 | return ( 136 | self.name == o.name 137 | and self.manga_id == o.manga_id 138 | and self.ch_id == o.ch_id 139 | ) 140 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_files/create_ch_info.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Use unsanitized input on SQL query is dangerous. 3 | But here's the thing, python `sqlite3.Cursor.executescript` 4 | doesn't support parameters, so we cannot add variables to the query. 5 | Also `sqlite3.Cursor.execute` and `sqlite3.Cursor.executemany` 6 | only support single-line query, so we have no choice to use 7 | `str.format_map` and the only input to the SQL query 8 | is just file format name (raw, cbz, etc) 9 | */ 10 | CREATE TABLE IF NOT EXISTS "ch_info_{format}" ( 11 | "name" TEXT NOT NULL, 12 | "id" TEXT NOT NULL, 13 | "fi_name" TEXT NOT NULL, 14 | FOREIGN KEY("fi_name") REFERENCES "file_info_{format}"("name") ON DELETE CASCADE 15 | ); -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_files/create_file_info.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Use unsanitized input on SQL query is dangerous. 3 | But here's the thing, python `sqlite3.Cursor.executescript` 4 | doesn't support parameters, so we cannot add variables to the query. 5 | Also `sqlite3.Cursor.execute` and `sqlite3.Cursor.executemany` 6 | only support single-line query, so we have no choice to use 7 | `str.format_map` and the only input to the SQL query 8 | is just file format name (raw, cbz, etc) 9 | */ 10 | CREATE TABLE IF NOT EXISTS "file_info_{format}" ( 11 | "name" TEXT NOT NULL, 12 | "manga_id" TEXT NOT NULL, 13 | "ch_id" TEXT, 14 | "hash" TEXT, 15 | "last_download_time" TEXT, 16 | "completed" INTEGER NOT NULL, 17 | PRIMARY KEY("name") 18 | ); -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_files/create_img_info.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Use unsanitized input on SQL query is dangerous. 3 | But here's the thing, python `sqlite3.Cursor.executescript` 4 | doesn't support parameters, so we cannot add variables to the query. 5 | Also `sqlite3.Cursor.execute` and `sqlite3.Cursor.executemany` 6 | only support single-line query, so we have no choice to use 7 | `str.format_map` and the only input to the SQL query 8 | is just file format name (raw, cbz, etc) 9 | */ 10 | CREATE TABLE IF NOT EXISTS "img_info_{format}" ( 11 | "name" TEXT NOT NULL, 12 | "hash" TEXT NOT NULL, 13 | "chapter_id" TEXT NOT NULL, 14 | "fi_name" TEXT NOT NULL, 15 | FOREIGN KEY("fi_name") REFERENCES "file_info_{format}"("name") ON DELETE CASCADE 16 | ); -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_migrations/00001_init.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sqlite3 3 | from pathlib import Path 4 | from .base import SQLMigration 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | class Migration(SQLMigration): 10 | file = __file__ 11 | 12 | def check_if_migrate_is_possible(self) -> bool: 13 | cursor = self.db.cursor() 14 | fmt = self.get_format() 15 | 16 | missing_ch_info = False 17 | try: 18 | cursor.execute(f"SELECT * FROM ch_info_{fmt}") 19 | except sqlite3.OperationalError: 20 | missing_ch_info = True 21 | 22 | missing_file_info = False 23 | try: 24 | cursor.execute(f"SELECT * FROM file_info_{fmt}") 25 | except sqlite3.OperationalError: 26 | missing_file_info = True 27 | 28 | missing_img_info = False 29 | try: 30 | cursor.execute(f"SELECT * FROM img_info_{fmt}") 31 | except sqlite3.OperationalError: 32 | missing_img_info = True 33 | 34 | return any([missing_img_info, missing_ch_info, missing_file_info]) 35 | 36 | def migrate(self): 37 | 38 | sqlfiles_base_path = Path(__file__).parent.parent.resolve() 39 | sql_commands = { 40 | i: (sqlfiles_base_path / "sql_files" / f"{i}.sql").read_text() 41 | for i in ["create_file_info", "create_ch_info", "create_img_info"] 42 | } 43 | cursor = self.db.cursor() 44 | 45 | for cmd_name, cmd_script in sql_commands.items(): 46 | cmd_script = cmd_script.format_map({"format": self.get_format()}) 47 | 48 | cursor.execute(cmd_script) 49 | 50 | self.db.commit() 51 | cursor.close() 52 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_migrations/00002_add_table_db_info_and_alter_table_file_info.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sqlite3 3 | from .base import ( 4 | SQLMigration, 5 | ) 6 | from ...manga import Manga 7 | from ... import __version__ 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class Migration(SQLMigration): 13 | new_version = 1 14 | file = __file__ 15 | 16 | def __init__(self, *args, **kwargs): 17 | super().__init__(*args, **kwargs) 18 | 19 | # I cannot read config values from class attributes 20 | # that would trigger recursive import error 21 | fmt = self.get_format() 22 | self.migrate_columns[f"file_info_{fmt}"] = ["volume"] 23 | self.migrate_values[f"file_info_{fmt}"] = ["volume", int] 24 | 25 | self.migrate_tables = ["db_info"] 26 | 27 | # Return: 28 | # {file_name: volume_manga} 29 | def _get_values_for_volume_column(self): 30 | from ...config import config 31 | 32 | # File info cursor 33 | fi_cursor = self.db.cursor() 34 | fi_cursor.execute(f"SELECT * FROM file_info_{self.get_format()}") 35 | 36 | values = {} 37 | for fi_data in fi_cursor.fetchall(): 38 | manga_id = fi_data[1] 39 | fi_name = fi_data[0] 40 | manga = Manga(_id=manga_id) 41 | manga.fetch_chapters(lang=config.language, all_chapters=True) 42 | 43 | chapter_iterator = manga.chapters.iter() 44 | volumes = {} 45 | for chapter, images in chapter_iterator: 46 | try: 47 | volumes[chapter.volume] 48 | except KeyError: 49 | volumes[chapter.volume] = [(chapter, images)] 50 | else: 51 | volumes[chapter.volume].append((chapter, images)) 52 | 53 | for volume, chapters in volumes.items(): 54 | 55 | ch_info_cursor = self.db.cursor() 56 | ch_info_cursor.execute( 57 | f"SELECT id, fi_name FROM ch_info_{self.get_format()} WHERE fi_name = ?", 58 | (fi_name,), 59 | ) 60 | ch_info_data = ch_info_cursor.fetchall() 61 | chapter_ids = [i[0] for i in ch_info_data] 62 | 63 | if not ch_info_data: 64 | continue 65 | 66 | # Get: file name 67 | ch_info_fi_name_ref = ch_info_data[0][1] 68 | 69 | if not any([i.id in chapter_ids for i, _ in chapters]): 70 | continue 71 | 72 | values[ch_info_fi_name_ref] = volume 73 | ch_info_cursor.close() 74 | 75 | fi_cursor.close() 76 | return values 77 | 78 | def _update_volume_values(self, cursor, values): 79 | for filename, volume in values.items(): 80 | cursor.execute( 81 | f"UPDATE file_info_{self.get_format()} SET volume = ? WHERE name = ?", 82 | (volume, filename), 83 | ) 84 | self.db.commit() 85 | 86 | def _is_version_missing(self, cursor): 87 | # Ensure the db_version is exist in db_info table 88 | try: 89 | cursor.execute( 90 | "SELECT db_version FROM db_info WHERE app_name = 'mangadex-downloader'" 91 | ) 92 | except sqlite3.OperationalError: 93 | # This only evaluated to boolean 94 | missing_version = True 95 | else: 96 | missing_version = False 97 | 98 | return missing_version 99 | 100 | def check_if_migrate_is_possible(self) -> bool: 101 | cursor = self.db.cursor() 102 | missing_tables = self.get_missing_tables() 103 | missing_columns = self.get_missing_columns() 104 | missing_version = self._is_version_missing(cursor) 105 | 106 | cursor.close() 107 | 108 | return any([missing_columns, missing_tables, missing_version]) 109 | 110 | def migrate(self): 111 | cursor = self.db.cursor() 112 | missing_tables = self.get_missing_tables() 113 | missing_columns = self.get_missing_columns() 114 | missing_values = self.get_missing_values() 115 | missing_version = self._is_version_missing(cursor) 116 | volume_values = self._get_values_for_volume_column() 117 | 118 | # Migrate tables first 119 | if missing_tables: 120 | cursor.execute( 121 | 'CREATE TABLE "db_info" ("app_name" TEXT,"app_version" TEXT, "db_version" INTEGER);' 122 | ) 123 | self.db.commit() 124 | 125 | if missing_version: 126 | cursor.execute( 127 | 'INSERT INTO db_info ("app_name", "app_version", "db_version") VALUES (?,?,?)', 128 | ("mangadex-downloader", __version__, self.new_version), 129 | ) 130 | self.db.commit() 131 | 132 | # Then migrate the columns 133 | if missing_columns: 134 | cursor.execute( 135 | f"ALTER TABLE file_info_{self.get_format()} ADD COLUMN volume INTEGER;" 136 | ) 137 | self.db.commit() 138 | 139 | self._update_volume_values(cursor, volume_values) 140 | 141 | # Ensure that the values are not null 142 | for table, column_name, column_data_type in missing_values: 143 | cursor.execute(f"SELECT {column_name} FROM {table}") 144 | 145 | values = cursor.fetchall() 146 | # Verify it 147 | if any([not isinstance(i, column_data_type) for i in values]): 148 | self._update_volume_values(cursor, volume_values) 149 | 150 | if self.db.in_transaction: 151 | self.db.commit() 152 | 153 | cursor.close() 154 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sqlite3 3 | import logging 4 | from .base import migration_files 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | def _iter_migrate_cls(db: sqlite3.Connection): 10 | for _, file in sorted(migration_files.items()): 11 | 12 | migrate_lib = importlib.import_module( 13 | "." + file.replace(".py", ""), "mangadex_downloader.tracker.sql_migrations" 14 | ) 15 | yield migrate_lib.Migration(db), file 16 | 17 | 18 | def check_if_there_is_migrations(db): 19 | possible_migrations = [] 20 | for migrate_cls, _ in _iter_migrate_cls(db): 21 | check = migrate_cls.check_if_migrate_is_possible() 22 | possible_migrations.append(check) 23 | 24 | return any(possible_migrations) 25 | 26 | 27 | def migrate(db: sqlite3.Connection): 28 | for migrate_cls, file in _iter_migrate_cls(db): 29 | if not migrate_cls.check_if_migrate_is_possible(): 30 | continue 31 | 32 | log.debug(f"Applying download tracker database migration {file}...") 33 | migrate_cls.migrate() 34 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/sql_migrations/base.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import logging 3 | import glob 4 | import re 5 | import sys 6 | from pathlib import Path 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def _get_migration_files(): 12 | migration_files = {} 13 | 14 | root_dir = Path(__file__).parent.resolve() 15 | if sys.version_info.major == 3 and sys.version_info.minor <= 9: 16 | files = glob.glob(str(root_dir) + "/*") 17 | else: 18 | files = glob.glob("*", root_dir=root_dir) 19 | 20 | for file in files: 21 | result = re.match(r"(?P[0-9]{1,}).{1,}\.py", file) 22 | if not result: 23 | continue 24 | 25 | migrate_id = int(result.group("migrate_id")) 26 | migration_files[migrate_id] = file 27 | 28 | return migration_files 29 | 30 | 31 | # Format: 32 | # {migrate_id: migration_file.py} 33 | migration_files = _get_migration_files() 34 | 35 | # Fix #136 #134 #133 36 | # Migration is not applied properly because of the order of the migration files 37 | migration_files = sorted(migration_files.items(), key=lambda x: x[0]) 38 | migration_files = dict(migration_files) 39 | 40 | 41 | class SQLMigration: 42 | """Base class for SQL Migration""" 43 | 44 | new_version = None 45 | file = None 46 | 47 | def __init__(self, db: sqlite3.Connection): 48 | # Base checking before migrations 49 | # subclasses must implement this 50 | self.migrate_tables = [] 51 | 52 | # Format: 53 | # {table_name: [column_name1, column_name2, ...]} 54 | self.migrate_columns = {} 55 | 56 | # Format: 57 | # {table_name: [column_name, column_data_type]} 58 | # This to make sure that column value is not null 59 | self.migrate_values = {} 60 | 61 | self.db = db 62 | 63 | def check_if_migrate_is_possible(self) -> bool: 64 | return True 65 | 66 | def migrate(self): 67 | """Subclasses must implement this""" 68 | raise NotImplementedError 69 | 70 | def get_format(self): 71 | from ...config import config 72 | 73 | return config.save_as.replace("-", "_") 74 | 75 | def get_current_version(self): 76 | cursor = self.db.cursor() 77 | 78 | try: 79 | cursor.execute( 80 | "SELECT version FROM db_info WHERE app_name = 'mangadex-downloader'" 81 | ) 82 | except sqlite3.OperationalError: 83 | return 0 84 | 85 | version = cursor.fetchone() 86 | cursor.close() 87 | 88 | return version 89 | 90 | def get_missing_tables(self): 91 | missing_tables = [] 92 | cursor = self.db.cursor() 93 | 94 | for table in self.migrate_tables: 95 | try: 96 | cursor.execute(f"SELECT * FROM {table}") 97 | except sqlite3.OperationalError: 98 | missing_tables.append(table) 99 | 100 | cursor.close() 101 | return missing_tables 102 | 103 | def get_missing_columns(self): 104 | missing_columns = [] 105 | cursor = self.db.cursor() 106 | 107 | for table, columns in self.migrate_columns.items(): 108 | cursor.execute(f"PRAGMA table_info('{table}')") 109 | 110 | column_names = [i[1] for i in cursor.fetchall()] 111 | 112 | for column in columns: 113 | if column not in column_names: 114 | missing_columns.append(column) 115 | 116 | cursor.close() 117 | return missing_columns 118 | 119 | def get_missing_values(self): 120 | """Ensure that the values are not null 121 | 122 | Return: Tuple[str, str, Any] 123 | Note: data type "Any" depends on what "column_data_type" is 124 | """ 125 | missing_values = [] 126 | cursor = self.db.cursor() 127 | 128 | for table, (column_name, column_data_type) in self.migrate_values.items(): 129 | try: 130 | cursor.execute(f"SELECT {column_name} FROM {table}") 131 | except sqlite3.OperationalError: 132 | # No such column 133 | missing_values.append((table, column_name, column_data_type)) 134 | 135 | data = cursor.fetchall() 136 | for value in data: 137 | if not isinstance(value[0], column_data_type): 138 | missing_values.append((table, column_name, column_data_type)) 139 | 140 | cursor.close() 141 | return missing_values 142 | -------------------------------------------------------------------------------- /mangadex_downloader/tracker/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mansuf/mangadex-downloader/867ad2f9f1b6392833008abf6015cff830429ed1/mangadex_downloader/tracker/utils.py -------------------------------------------------------------------------------- /mangadex_downloader/user.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022-present Rahman Yusuf 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .fetcher import get_user 24 | 25 | 26 | class User: 27 | def __init__(self, user_id=None, data=None): 28 | if data is None: 29 | self.data = get_user(user_id)["data"] 30 | else: 31 | self.data = data 32 | 33 | self.id = self.data["id"] 34 | attr = self.data["attributes"] 35 | 36 | self.name = attr["username"] 37 | self.roles = attr["roles"] 38 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | furo 2 | myst-parser[linkify] -------------------------------------------------------------------------------- /requirements-optional.txt: -------------------------------------------------------------------------------- 1 | py7zr==0.22.0 2 | orjson==3.10.15 3 | lxml==5.3.0 4 | Authlib -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests-doh==1.0.0 2 | requests[socks] 3 | tqdm 4 | pathvalidate 5 | packaging 6 | pyjwt 7 | beautifulsoup4 8 | Pillow==11.1.0 9 | chardet -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 92 2 | 3 | target-version = "py310" -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # This was used to run mangadex-downloader for compiled app 2 | # Because we cannot compile __main__.py directly (i don't know why, the errors are confusing) 3 | 4 | from mangadex_downloader.cli import main 5 | 6 | if __name__ == "__main__": 7 | main() -------------------------------------------------------------------------------- /seasonal_manga_now.txt: -------------------------------------------------------------------------------- 1 | https://mangadex.org/list/4be9338a-3402-4f98-b467-43fb56663927/seasonal-fall-2022 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | # Root directory 6 | # (README.md, mangadex_downloader/__init__.py) 7 | HERE = pathlib.Path(__file__).parent 8 | README = (HERE / "README.md").read_text() 9 | init_file = (HERE / "mangadex_downloader/__init__.py").read_text() 10 | 11 | 12 | def get_version(): 13 | """Get version of the app""" 14 | re_version = r"__version__ = \"([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.{0,})\"" 15 | _version = re.search(re_version, init_file) 16 | 17 | if _version is None: 18 | raise RuntimeError("Version is not set") 19 | 20 | return _version.group(1) 21 | 22 | 23 | def get_value_var(var_name): 24 | """Get value of `__{var_name}__` from `mangadex_downloader/__init__.py`""" 25 | var = f"__{var_name}__" 26 | regex = '%s = "(.{1,})"' % var 27 | 28 | found = re.search(regex, init_file) 29 | 30 | if found is None: 31 | raise RuntimeError(f'{var} is not set in "mangadex_downloader/__init__.py"') 32 | 33 | return found.group(1) 34 | 35 | 36 | def get_requirements(): 37 | """Return tuple of library needed for app to run""" 38 | main = [] 39 | try: 40 | with open("./requirements.txt", "r") as r: 41 | main = r.read().splitlines() 42 | except FileNotFoundError: 43 | raise RuntimeError("requirements.txt is needed to build mangadex-downloader") 44 | 45 | if not main: 46 | raise RuntimeError("requirements.txt have no necessary libraries inside of it") 47 | 48 | docs = [] 49 | try: 50 | with open("./requirements-docs.txt", "r") as r: 51 | docs = r.read().splitlines() 52 | except FileNotFoundError: 53 | # There is no docs requirements 54 | # Developers can ignore this error and continue to install without any problem. 55 | # However, this is needed if developers want to create documentation in readthedocs.org or local device. 56 | pass 57 | 58 | optional = [] 59 | try: 60 | with open("./requirements-optional.txt", "r") as r: 61 | optional = r.read().splitlines() 62 | except FileNotFoundError: 63 | raise RuntimeError( 64 | "requirements-optional.txt is needed to build mangadex-downloader" 65 | ) 66 | 67 | if not optional: 68 | raise RuntimeError( 69 | "requirements-optional.txt have no necessary libraries inside of it" 70 | ) 71 | 72 | return main, {"docs": docs, "optional": optional} 73 | 74 | 75 | # Get requirements needed to build app 76 | requires_main, extras_require = get_requirements() 77 | 78 | # Get all modules 79 | packages = find_packages(".") 80 | 81 | # Get repository 82 | repo = get_value_var("repository") 83 | 84 | # Finally run main setup 85 | setup( 86 | name="mangadex-downloader", 87 | packages=packages, 88 | version=get_version(), 89 | license=get_value_var("license"), 90 | description=get_value_var("description"), 91 | long_description=README, 92 | long_description_content_type="text/markdown", 93 | author=get_value_var("author"), 94 | author_email=get_value_var("author_email"), 95 | url=f"https://github.com/{repo}", 96 | download_url=f"https://github.com/{repo}/releases", 97 | keywords=["mangadex"], 98 | install_requires=requires_main, 99 | extras_require=extras_require, 100 | entry_points={ 101 | "console_scripts": [ 102 | "mangadex-downloader=mangadex_downloader.__main__:main", 103 | "mangadex-dl=mangadex_downloader.__main__:main", 104 | ] 105 | }, 106 | classifiers=[ 107 | "Development Status :: 5 - Production/Stable", 108 | "Intended Audience :: End Users/Desktop", 109 | "License :: OSI Approved :: MIT License", 110 | "Programming Language :: Python :: 3 :: Only", 111 | "Programming Language :: Python :: 3", 112 | "Programming Language :: Python :: 3.10", 113 | "Programming Language :: Python :: 3.11", 114 | "Programming Language :: Python :: 3.12", 115 | "Programming Language :: Python :: 3.13", 116 | ], 117 | python_requires=">=3.10", 118 | include_package_data=True, 119 | ) 120 | --------------------------------------------------------------------------------