├── .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 | [](https://pypi.org/project/mangadex-downloader)
2 | [](https://pypi.org/project/mangadex-downloader)
3 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------