diff --git a/.woodpecker/test_release.yml b/.woodpecker/test_release.yml index ef64caa..50409cc 100644 --- a/.woodpecker/test_release.yml +++ b/.woodpecker/test_release.yml @@ -26,7 +26,7 @@ pipeline: branch: master event: pull_request commands: - - python3 -m hatch build --clean + - just test_build # create release-notes test-create-release-notes: diff --git a/.woodpecker/test_tox_amd64.yml b/.woodpecker/test_tox_amd64.yml index 342fe15..f365a7c 100644 --- a/.woodpecker/test_tox_amd64.yml +++ b/.woodpecker/test_tox_amd64.yml @@ -26,4 +26,4 @@ pipeline: branch: master event: pull_request commands: - - python3 -m tox + - just test_tox diff --git a/.woodpecker/test_tox_arm64.yml b/.woodpecker/test_tox_arm64.yml index 87a9a20..a203c1d 100644 --- a/.woodpecker/test_tox_arm64.yml +++ b/.woodpecker/test_tox_arm64.yml @@ -29,4 +29,4 @@ pipeline: - grep -v img2pdf contrib/requirements_dev.txt > contrib/requirements_dev_arm64.txt - rm -f contrib/requirements_dev.txt - mv contrib/requirements_dev_arm64.txt contrib/requirements_dev.txt - - python3 -m tox + - just test_tox diff --git a/.woodpecker/tests.yml b/.woodpecker/tests.yml index 72c95eb..23c31b4 100644 --- a/.woodpecker/tests.yml +++ b/.woodpecker/tests.yml @@ -17,51 +17,29 @@ pipeline: image: cr.44net.ch/ci-plugins/tests pull: true commands: - - shfmt -d -i 4 -bn -ci -sr . + - just test_shfmt # check code style - python test-black: image: cr.44net.ch/ci-plugins/tests pull: true commands: - - python3 -m black --check --diff . - - # check imports - python - test-isort: - image: cr.44net.ch/ci-plugins/tests - pull: true - commands: - - python3 -m isort --check-only --diff . - - # check unused and missing imports - python - test-autoflake: - image: cr.44net.ch/ci-plugins/tests - pull: true - commands: - - python3 -m autoflake --remove-all-unused-imports -r -v mangadlp/ - - python3 -m autoflake --check --remove-all-unused-imports -r -v mangadlp/ + - just test_black # check static typing - python - test-mypy: + test-pyright: image: cr.44net.ch/ci-plugins/tests pull: true commands: - - python3 -m mypy --install-types --non-interactive mangadlp/ + - just install_deps + - just test_pyright - # mccabe, pycodestyle, pyflakes tests - python - test-pylama: + # ruff test - python + test-ruff: image: cr.44net.ch/ci-plugins/tests pull: true commands: - - python3 -m pylama mangadlp/ - - # pylint test - python - test-pylint: - image: cr.44net.ch/ci-plugins/tests - pull: true - commands: - - python3 -m pip install -r requirements.txt - - python3 -m pylint --fail-under 9 mangadlp/ + - just test_ruff # test mkdocs generation test-mkdocs: @@ -72,14 +50,14 @@ pipeline: - cd docs || exit 1 - python3 -m mkdocs build --strict - # test code with different python versions - python + # test code with pytest - python test-tox-pytest: when: event: [ push ] image: cr.44net.ch/ci-plugins/tests pull: true commands: - - python3 -m tox -e basic + - just test_pytest # generate coverage report - python test-tox-coverage: @@ -89,7 +67,7 @@ pipeline: image: cr.44net.ch/ci-plugins/tests pull: true commands: - - python3 -m tox -e coverage + - just test_coverage # analyse code with sonarqube and upload it sonarqube-analysis: diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a88c7..a06ac4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,442 +7,458 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -- Add support for more sites +- Add support for more sites + +## [2.3.1] - 2023-03-12 + +### Added + +- Added TypedDicts for type checkers and type annotation + +### Fixed + +- Fixed some typos in the README + +### Changed + +- Switched from pylint/pylama/isort/autoflake to ruff +- Switched from mypy to pyright and added strict type checking +- Updated the api template ## [2.3.0] - 2023-02-15 ### Added -- Metadata is now added to each chapter. Schema - standard: [https://anansi-project.github.io/docs/comicinfo/schemas/v2.0](https://anansi-project.github.io/docs/comicinfo/schemas/v2.0) -- Added `xmltodict` as a package requirement -- Cache now also saves the manga title -- New tests -- More typo annotations for function, compatible with python3.8 -- File format checker if you use the MangaDLP class directly +- Metadata is now added to each chapter. Schema + standard: [https://anansi-project.github.io/docs/comicinfo/schemas/v2.0](https://anansi-project.github.io/docs/comicinfo/schemas/v2.0) +- Added `xmltodict` as a package requirement +- Cache now also saves the manga title +- New tests +- More typo annotations for function, compatible with python3.8 +- File format checker if you use the MangaDLP class directly ### Fixed -- API template typos -- Some useless type annotations +- API template typos +- Some useless type annotations ### Changed -- Simplified the chapter info generation -- Updated the license year -- Updated the API template -- Updated the API detection and removed it from the MangaDLP class +- Simplified the chapter info generation +- Updated the license year +- Updated the API template +- Updated the API detection and removed it from the MangaDLP class ## [2.2.20] - 2023-02-12 ### Fixed -- Script now doesn't exit if multiple mangas were requested and one had an error +- Script now doesn't exit if multiple mangas were requested and one had an error ## [2.2.19] - 2023-02-11 ### Added -- First version of the chapter cache (very basic functionality) +- First version of the chapter cache (very basic functionality) ### Fixed -- Fixed all exception re-raises to include the original stack trace +- Fixed all exception re-raises to include the original stack trace ### Changed -- Simplified chapter download loop +- Simplified chapter download loop ## [2.2.18] - 2023-01-21 ### Fixed -- Fixed manga titles on non english language -- Fixed title & filename fixing to not use `ascii` but `uft8` +- Fixed manga titles on non english language +- Fixed title & filename fixing to not use `ascii` but `uft8` ### Added -- Fallback title to english of none was found in requested language -- More debug logs -- More tests +- Fallback title to english of none was found in requested language +- More debug logs +- More tests ### Changed -- Now uses the first found alt-title. Before it was the last -- Removed `sys.exit` in the api +- Now uses the first found alt-title. Before it was the last +- Removed `sys.exit` in the api ## [2.2.17] - 2023-01-15 ### Fixed -- Set a timeout of 10 seconds for the api requests +- Set a timeout of 10 seconds for the api requests ### Added -- `--name-format` and `--name-format-none` flags to add a custom naming scheme for the downloaded files. See - docs: https://manga-dlp.ivn.sh/download/ -- More debug log messages -- More tests for the custom naming scheme -- More type hints +- `--name-format` and `--name-format-none` flags to add a custom naming scheme for the downloaded files. See + docs: https://manga-dlp.ivn.sh/download/ +- More debug log messages +- More tests for the custom naming scheme +- More type hints ### Changed -- Make `--format` a `click.Choice` option -- In the `--format` option the leading dot is now invalid. `--format .cbz` -> `--format cbz` -- Changed empty values from the api from None to an empty string -- Minor code readability improvements +- Make `--format` a `click.Choice` option +- In the `--format` option the leading dot is now invalid. `--format .cbz` -> `--format cbz` +- Changed empty values from the api from None to an empty string +- Minor code readability improvements ## [2.2.16] - 2022-12-30 ### Fixed -- Log level is now fixed and should not default to 0 -- Docker schedule should now work again +- Log level is now fixed and should not default to 0 +- Docker schedule should now work again ### Changed -- Integrate logging logs to loguru via custom sink -- Simplify docker shell scripts +- Integrate logging logs to loguru via custom sink +- Simplify docker shell scripts ## [2.2.15] - 2022-12-29 ### Added -- `--warn` and `--loglevel` flags +- `--warn` and `--loglevel` flags ### Removed -- Remove `--lean` and `--verbose` flags and remove custom log levels +- Remove `--lean` and `--verbose` flags and remove custom log levels ### Changed -- Move from standard library logging to [loguru](https://loguru.readthedocs.io/en/stable/index.html) -- Move from standard library argparse to [click](https://click.palletsprojects.com/en/8.1.x/) +- Move from standard library logging to [loguru](https://loguru.readthedocs.io/en/stable/index.html) +- Move from standard library argparse to [click](https://click.palletsprojects.com/en/8.1.x/) ## [2.2.14] - 2022-10-06 ### Changed -- Changed logging format to ISO 8601 -- Small logging corrections +- Changed logging format to ISO 8601 +- Small logging corrections ## [2.2.13] - 2022-08-15 ### Added -- Option to run custom hooks before and after each chapter/manga download -- _Tests for the new hooks_ -- _Docs for the new hooks_ -- _Tests for mkdocs generation_ +- Option to run custom hooks before and after each chapter/manga download +- _Tests for the new hooks_ +- _Docs for the new hooks_ +- _Tests for mkdocs generation_ ### Changed -- Verbose and Debug logging now have a space as a seperator between log level-name and log-level -- APIs now have an attribute with their name (for the hooks) - `api.api_name` -- Docs moved to Cloudflare pages (generated with mkdocs) +- Verbose and Debug logging now have a space as a seperator between log level-name and log-level +- APIs now have an attribute with their name (for the hooks) - `api.api_name` +- Docs moved to Cloudflare pages (generated with mkdocs) ## [2.1.12] - 2022-07-25 ### Fixed -- Image publishing with `hatch` on pypi should now work again -- The schedule fixer for the new `.sh` schedule should now work correctly +- Image publishing with `hatch` on pypi should now work again +- The schedule fixer for the new `.sh` schedule should now work correctly ### Added -- More CI tests: `pylint`, `pylama` and `autoflake` -- New function in `get_release_notes.sh` to get the latest version -- Docstrings for `MangaDLP` and the api module `Mangadex` +- More CI tests: `pylint`, `pylama` and `autoflake` +- New function in `get_release_notes.sh` to get the latest version +- Docstrings for `MangaDLP` and the api module `Mangadex` ### Changed -- CI workflow is now faster and runs natively on arm64 (before it was buildx/emulation) -- `Pylint`/`pylama` code improvements -- Version management is now done with `hatch` (in `__about__.py`) +- CI workflow is now faster and runs natively on arm64 (before it was buildx/emulation) +- `Pylint`/`pylama` code improvements +- Version management is now done with `hatch` (in `__about__.py`) ## [2.1.11] - 2022-07-18 ### Fixed -- The `--read` option now filters empty lines, so it will not generate an error anymore -- An error which was caused by the interactive input method when you did not specify a chapter or to list them -- Some typos +- The `--read` option now filters empty lines, so it will not generate an error anymore +- An error which was caused by the interactive input method when you did not specify a chapter or to list them +- Some typos ### Added -- Options to configure the default schedule in the docker container via environment variables -- Section the the docker [README.md](docker/README.md) for the new environment variables -- `autoflake` test in `justfile` -- Some more things which get logged +- Options to configure the default schedule in the docker container via environment variables +- Section the the docker [README.md](docker/README.md) for the new environment variables +- `autoflake` test in `justfile` +- Some more things which get logged ### Changed -- **BREAKING**: renamed the default schedule from `daily` to `daily.sh`. Don't forget to fix your bind-mounts to - overwrite - the default schedule -- Added the `.sh` suffix to the s6 init scripts for better compatibility -- Adjusted the new logging implementation. It shows now more info about the module the log is from, and some other - improvements +- **BREAKING**: renamed the default schedule from `daily` to `daily.sh`. Don't forget to fix your bind-mounts to + overwrite + the default schedule +- Added the `.sh` suffix to the s6 init scripts for better compatibility +- Adjusted the new logging implementation. It shows now more info about the module the log is from, and some other + improvements ## [2.1.10] - 2022-07-14 ### Fixed -- Removed some unused files +- Removed some unused files ### Added -- `logger.py` for all log related settings and functions +- `logger.py` for all log related settings and functions ### Changed -- Logging of output. The script now uses the `logging` library +- Logging of output. The script now uses the `logging` library ## [2.1.9] - 2022-06-26 ### Fixed -- Timeouts in tests, due to api limitations. Now added a wait time between tests -- Pytest path +- Timeouts in tests, due to api limitations. Now added a wait time between tests +- Pytest path ### Added -- `--lean` flag for less output -- [justfile](https://github.com/casey/just) for setting up a dev environment and testing the code -- [asdf](https://github.com/asdf-vm/asdf) for version management -- Dev requirements in [contrib/requirements_dev.txt](contrib/requirements_dev.txt) -- `README` in [contrib](contrib) +- `--lean` flag for less output +- [justfile](https://github.com/casey/just) for setting up a dev environment and testing the code +- [asdf](https://github.com/asdf-vm/asdf) for version management +- Dev requirements in [contrib/requirements_dev.txt](contrib/requirements_dev.txt) +- `README` in [contrib](contrib) ### Changed -- Handling of verbosity and logging. Now there are 4 types of verbosity: `normal`, `lean`, `verbose` and `debug` -- CI/CD pipeline for testing and releases -- Coverage testing now also done with `tox` -- Default verbosity of docker container is now `--lean` -- Reorganised [pyproject.toml](pyproject.toml) +- Handling of verbosity and logging. Now there are 4 types of verbosity: `normal`, `lean`, `verbose` and `debug` +- CI/CD pipeline for testing and releases +- Coverage testing now also done with `tox` +- Default verbosity of docker container is now `--lean` +- Reorganised [pyproject.toml](pyproject.toml) ## [2.1.8] - 2022-06-22 ### Fixed -- Interactive input +- Interactive input ## [2.1.7] - 2022-06-22 ### Added -- tox version testing -- New pre-release tests -- Build info's with hatch -- [Pypi](https://pypi.org/project/manga-dlp/) build with hatch -- Pypi section in `README.md` -- [Snyk](https://app.snyk.io/org/olofvndrhr-t6h/project/aae9609d-a4e4-41f8-b1ac-f2561b2ad4e3) test results - in `README.md` +- tox version testing +- New pre-release tests +- Build info's with hatch +- [Pypi](https://pypi.org/project/manga-dlp/) build with hatch +- Pypi section in `README.md` +- [Snyk](https://app.snyk.io/org/olofvndrhr-t6h/project/aae9609d-a4e4-41f8-b1ac-f2561b2ad4e3) test results + in `README.md` ### Changed -- Moved code from `manga-dlp.py` to `input.py` for uniformity -- The default entrypoint is now `mangadlp.input:main` +- Moved code from `manga-dlp.py` to `input.py` for uniformity +- The default entrypoint is now `mangadlp.input:main` ## [2.1.6] - 2022-06-21 ### Fixed -- Docker labels are now working -- Global variables are now fully uppercase -- Some errors with static types +- Docker labels are now working +- Global variables are now fully uppercase +- Some errors with static types ### Added -- bump2version config for releases -- More tests with: `mypy` and `isort` -- New issue templates +- bump2version config for releases +- More tests with: `mypy` and `isort` +- New issue templates ### Changed -- Release workflow now is based on configuration files -- Switched from `setup.py` to `pyproject.toml` -- `README.md` now has sorted badges -- Imports are now sorted with `isort` -- Static types are now checked with `mypy` -- Release note generation is now simplified +- Release workflow now is based on configuration files +- Switched from `setup.py` to `pyproject.toml` +- `README.md` now has sorted badges +- Imports are now sorted with `isort` +- Static types are now checked with `mypy` +- Release note generation is now simplified ## [2.1.5] - 2022-06-18 ### Fixed -- Image names now have a suffix, as some comic readers have problems with no - suffix [fixes issue #2] +- Image names now have a suffix, as some comic readers have problems with no + suffix [fixes issue #2] ### Added -- `--format` section in the README +- `--format` section in the README ## [2.1.4] - 2022-05-29 ### Fixed -- Docker container now works again -- Fixed cron in docker container +- Docker container now works again +- Fixed cron in docker container ### Changed -- Docker container scheduling is now more practical +- Docker container scheduling is now more practical ## [2.1.3] - 2022-05-29 ### Fixed -- Error-chapters and skipped-chapters list are now shown again -- The Interactive input version now matches `--version` +- Error-chapters and skipped-chapters list are now shown again +- The Interactive input version now matches `--version` ### Added -- Ability to list chapters with interactive input +- Ability to list chapters with interactive input ### Changed -- Replace `exit()` with `sys.exit()` -- Renamed class methods to not look like dunder methods -- Script execution moved from `os.system()` to `subprocess.call()` +- Replace `exit()` with `sys.exit()` +- Renamed class methods to not look like dunder methods +- Script execution moved from `os.system()` to `subprocess.call()` ## [2.1.2] - 2022-05-20 ### Fixed -- List chapters when none were specified -- Typos +- List chapters when none were specified +- Typos ### Added -- Ability to download whole volumes +- Ability to download whole volumes ### Changed -- Moved processing of list with links to input.py -- Updated README for volume and chapter selection +- Moved processing of list with links to input.py +- Updated README for volume and chapter selection ## [2.1.1] - 2022-05-18 ### Fixed -- Progress bar on verbose output -- Sonarqube link for CI -- A few typos -- Removed unnecessary escapes from file rename regex +- Progress bar on verbose output +- Sonarqube link for CI +- A few typos +- Removed unnecessary escapes from file rename regex ### Added -- API template +- API template ### Changed -- Updated docker baseimage -- Rewrote app.py to a class +- Updated docker baseimage +- Rewrote app.py to a class ## [2.1.0] - 2022-05-16 ### Fixed -- Detection of files. Now it will skip them again +- Detection of files. Now it will skip them again ### Added -- Ability to save the chapters as pdf (only on amd64/x86) -- New output formats: rar, zip -- Progress bar to show image download -- Interactive input if no command line flags are given -- Better KeyboardInterrupt handling -- Better error handling -- Removed duplicate code +- Ability to save the chapters as pdf (only on amd64/x86) +- New output formats: rar, zip +- Progress bar to show image download +- Interactive input if no command line flags are given +- Better KeyboardInterrupt handling +- Better error handling +- Removed duplicate code ### Changed -- How the variables are used inside the script -- Variables have now the same name as in other scripts (mostly) -- Better retrying when a task fails +- How the variables are used inside the script +- Variables have now the same name as in other scripts (mostly) +- Better retrying when a task fails ## [2.0.8] - 2022-05-13 ### Changed -- Rewrote parts of script to be easier to maintain -- Moved the input script to the base folder -- Moved all arguments to a class -- Docker container creation +- Rewrote parts of script to be easier to maintain +- Moved the input script to the base folder +- Moved all arguments to a class +- Docker container creation ## [2.0.7] - 2022-05-13 ### Changed -- Changed CI/CD Platform from Drone-CI to Woodpecker-CI -- Release title is now only the version +- Changed CI/CD Platform from Drone-CI to Woodpecker-CI +- Release title is now only the version ## [2.0.6] - 2022-05-11 ### Fixed -- Filenames on windows (ntfs). Removed double quote from file and folder names +- Filenames on windows (ntfs). Removed double quote from file and folder names ## [2.0.5] - 2022-05-11 ### Fixed -- Better error handling on "KeyboardInterrupt" -- Release notes now fixed +- Better error handling on "KeyboardInterrupt" +- Release notes now fixed ### Added -- New test cases +- New test cases ## [2.0.4] - 2022-05-10 ### Added -- New test cases for more coverage -- Github release -- Updated docker baseimage +- New test cases for more coverage +- Github release +- Updated docker baseimage ## [2.0.3] - 2022-05-10 ### Fixed -- Test cases now work again -- Sonarqube settings +- Test cases now work again +- Sonarqube settings ### Added -- Coverage report in sonarqube -- Gitea release +- Coverage report in sonarqube +- Gitea release ## [2.0.2] - 2022-05-09 ### Fixed -- Restart failed api requests -- Added wait time for image gathering, as to stop api rate limiting from mangadex -- "--wait" options now works properly again +- Restart failed api requests +- Added wait time for image gathering, as to stop api rate limiting from mangadex +- "--wait" options now works properly again ## [2.0.1] - 2022-05-09 ### Fixed -- Regex for removing illegal characters in the filenames now doesn't remove quotes -- Updated docker baseimage and fixed the mangadlp tag in it -- Update license for 2022 +- Regex for removing illegal characters in the filenames now doesn't remove quotes +- Updated docker baseimage and fixed the mangadlp tag in it +- Update license for 2022 ### Added -- Quick start section in README -- Preperation for pypi +- Quick start section in README +- Preperation for pypi ## [2.0.0] - 2022-05-09 ### Fixed -- Support for new mangadex api +- Support for new mangadex api ### Changed -- Code is now formatted with [black](https://github.com/psf/black) -- Now also supports just the uuid for managex (not a full link) +- Code is now formatted with [black](https://github.com/psf/black) +- Now also supports just the uuid for managex (not a full link) diff --git a/README.md b/README.md index f569f1d..84e1950 100644 --- a/README.md +++ b/README.md @@ -19,30 +19,42 @@ Code Analysis Meta [![Code style](https://img.shields.io/badge/code%20style-black-black)](https://github.com/psf/black) -[![Linter](https://img.shields.io/badge/linter-pylint-yellowgreen)](https://pylint.pycqa.org/en/latest/) -[![Types](https://img.shields.io/badge/types-mypy-blue)](https://github.com/python/mypy) -[![Imports](https://img.shields.io/badge/imports-isort-ef8336.svg)](https://github.com/pycqa/isort) +[![Linter](https://img.shields.io/badge/linter-ruff-red)](https://github.com/charliermarsh/ruff) +[![Types](https://img.shields.io/badge/types-pyright-blue)](https://github.com/microsoft/pyright) [![Tests](https://img.shields.io/badge/tests-pytest%20%7C%20tox-yellow)](https://github.com/pytest-dev/pytest/) [![Coverage](https://img.shields.io/badge/coverage-coveragepy-green)](https://github.com/nedbat/coveragepy) [![License](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://snyk.io/learn/what-is-mit-license/) [![Compatibility](https://img.shields.io/pypi/pyversions/manga-dlp)](https://pypi.org/project/manga-dlp/) + --- ## Description A manga download script written in python. It only supports [mangadex.org](https://mangadex.org/) for now. But support -for other sites is planned. +for other sites is _planned™_. Before downloading a new chapter, the script always checks if there is already a chapter with the same name in the download directory. If found the chapter is skipped. So you can run the script on a schedule to only download new chapters without any additional setup. The default behaiviour is to pack the images to a [cbz archive](https://en.wikipedia.org/wiki/Comic_book_archive). If -you just want the folder with all the pictures use the flag `--nocbz`. +you just want the folder with all the pictures use the flag `--format ""`. ## _Currently_ Supported sites -- [Mangadex.org](https://mangadex.org/) +- [Mangadex.org](https://mangadex.org/) + +## Features (not complete) + +- Metadata support with [ComicInfo.xml](https://anansi-project.github.io/docs/comicinfo/intro) +- Json caching +- Custom hooks after/before each download +- Custom chapter name format +- Volume support +- Multiple archive formats supported (cbz,cbr,zip,none) +- Language selection +- Download all chapters directly +- And others... ## Usage @@ -124,20 +136,20 @@ verbosity: [mutually_exclusive] For suggestions for improvement, just open a pull request. -If you want to add support for a new site, there is an api [template file](./contrib/api_template.py) which you can use. -And more infos and tools in the contrib [README.md](contrib/README.md) +If you want to add support for a new site, there is an api [template file](contrib/api_template.py) which you can use. +And more infos and tools are in the contrib [README.md](contrib/README.md) -Otherwise, you can open am issue with the name of the site which you want support for. (not guaranteed to be -implemented) +Otherwise, you can open an issue with the name of the site which you want support for (not guaranteed to be +implemented). If you encounter any bugs, also just open an issue with a description of the problem. ## TODO's -- Make docker container for easy distribution - --> [Dockerhub](https://hub.docker.com/repository/docker/olofvndrhr/manga-dlp) -- Automate release - --> Done with woodpecker-ci -- Make pypi package - --> Done with release [2.1.7](https://pypi.org/project/manga-dlp/) -- Add more supported sites +- Make docker container for easy distribution + --> [Dockerhub](https://hub.docker.com/r/olofvndrhr/manga-dlp) +- Automate release + --> Done with woodpecker-ci +- Make pypi package + --> Done with release [2.1.7](https://pypi.org/project/manga-dlp/) +- Add more supported sites diff --git a/contrib/api_template.py b/contrib/api_template.py index aa95174..ea02391 100644 --- a/contrib/api_template.py +++ b/contrib/api_template.py @@ -1,9 +1,14 @@ +from typing import Dict, List, Union + +from mangadlp.types import ChapterData,ComicInfo + # api template for manga-dlp class YourAPI: """Your API Class. - Get infos for a manga from example.org + + Get infos for a manga from example.org. Args: url_uuid (str): URL or UUID of the manga @@ -22,10 +27,8 @@ class YourAPI: api_base_url = "https://api.mangadex.org" img_base_url = "https://uploads.mangadex.org" - def __init__(self, url_uuid, language, forcevol): - """ - get infos to initiate class - """ + def __init__(self, url_uuid: str, language: str, forcevol: bool): + """get infos to initiate class.""" self.api_name = "Your API Name" self.url_uuid = url_uuid @@ -36,22 +39,24 @@ class YourAPI: self.manga_uuid = "abc" self.manga_title = "abc" self.chapter_list = ["1", "2", "2.1", "5", "10"] - self.manga_chapter_data = { # example data + self.manga_chapter_data: Dict[str, ChapterData] = { # example data "1": { "uuid": "abc", "volume": "1", "chapter": "1", "name": "test", + "pages" 2, }, "2": { "uuid": "abc", "volume": "1", "chapter": "2", "name": "test", + "pages": 45, }, } # or with --forcevol - self.manga_chapter_data = { + self.manga_chapter_data: Dict[str, ChapterData] = { "1:1": { "uuid": "abc", "volume": "1", @@ -66,9 +71,8 @@ class YourAPI: }, } - def get_chapter_images(chapter: str, download_wait: float) -> list: - """ - Get chapter images as a list (full links) + def get_chapter_images(self, chapter: str, wait_time: float) -> List[str]: + """Get chapter images as a list (full links). Args: chapter: The chapter number (chapter data index) @@ -77,7 +81,6 @@ class YourAPI: Returns: The list of urls of the page images """ - # example return [ "https://abc.def/image/123.png", @@ -85,10 +88,10 @@ class YourAPI: "https://abc.def/image/12345.png", ] - def create_metadata(self, chapter: str) -> dict: - """ - Get metadata with correct keys for ComicInfo.xml - Provide as much metadata as possible. empty/false values will be ignored + def create_metadata(self, chapter: str) -> ComicInfo: + """Get metadata with correct keys for ComicInfo.xml. + + Provide as much metadata as possible. empty/false values will be ignored. Args: chapter: The chapter number (chapter data index) @@ -96,7 +99,6 @@ class YourAPI: Returns: The metadata as a dict """ - # metadata types. have to be valid # {key: (type, default value, valid values)} { @@ -155,7 +157,7 @@ class YourAPI: # example return { - "Volume": "abc", + "Volume": 1, "LanguageISO": "en", "Title": "test", } diff --git a/contrib/requirements_dev.txt b/contrib/requirements_dev.txt index a2aa1f6..680db66 100644 --- a/contrib/requirements_dev.txt +++ b/contrib/requirements_dev.txt @@ -14,9 +14,7 @@ hatchling>=1.11.0 pytest>=7.0.0 coverage>=6.3.1 black>=22.1.0 -isort>=5.10.0 -pylint>=2.13.0 mypy>=0.940 tox>=3.24.5 -autoflake>=1.4 -pylama>=8.3.8 +ruff>=0.0.247 +pyright>=1.1.294 diff --git a/docs/pages/index.md b/docs/pages/index.md index efca1e0..e7cac2d 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -17,30 +17,42 @@ Code Analysis Meta [![Code style](https://img.shields.io/badge/code%20style-black-black)](https://github.com/psf/black) -[![Linter](https://img.shields.io/badge/linter-pylint-yellowgreen)](https://pylint.pycqa.org/en/latest/) -[![Types](https://img.shields.io/badge/types-mypy-blue)](https://github.com/python/mypy) -[![Imports](https://img.shields.io/badge/imports-isort-ef8336.svg)](https://github.com/pycqa/isort) +[![Linter](https://img.shields.io/badge/linter-ruff-red)](https://github.com/charliermarsh/ruff) +[![Types](https://img.shields.io/badge/types-pyright-blue)](https://github.com/microsoft/pyright) [![Tests](https://img.shields.io/badge/tests-pytest%20%7C%20tox-yellow)](https://github.com/pytest-dev/pytest/) [![Coverage](https://img.shields.io/badge/coverage-coveragepy-green)](https://github.com/nedbat/coveragepy) [![License](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://snyk.io/learn/what-is-mit-license/) [![Compatibility](https://img.shields.io/pypi/pyversions/manga-dlp)](https://pypi.org/project/manga-dlp/) + --- ## Description A manga download script written in python. It only supports [mangadex.org](https://mangadex.org/) for now. But support -for other sites is planned. +for other sites is _planned™_. Before downloading a new chapter, the script always checks if there is already a chapter with the same name in the download directory. If found the chapter is skipped. So you can run the script on a schedule to only download new chapters without any additional setup. The default behaiviour is to pack the images to a [cbz archive](https://en.wikipedia.org/wiki/Comic_book_archive). If -you just want the folder with all the pictures use the flag `--nocbz`. +you just want the folder with all the pictures use the flag `--format ""`. ## _Currently_ Supported sites -- [Mangadex.org](https://mangadex.org/) +- [Mangadex.org](https://mangadex.org/) + +## Features (not complete) + +- Metadata support with [ComicInfo.xml](https://anansi-project.github.io/docs/comicinfo/intro) +- Json caching +- Custom hooks after/before each download +- Custom chapter name format +- Volume support +- Multiple archive formats supported (cbz,cbr,zip,none) +- Language selection +- Download all chapters directly +- And others... ## Usage @@ -82,7 +94,7 @@ mangadlp # call script directly ### With docker -See the docker [README](docker/) +See the docker [README](https://manga-dlp.ivn.sh/docker/) ## Options @@ -122,22 +134,20 @@ verbosity: [mutually_exclusive] For suggestions for improvement, just open a pull request. -If you want to add support for a new site, there is an -api [template file](https://github.com/olofvndrhr/manga-dlp/blob/master/contrib/api_template.py) which you can use. -And more infos and tools in the -contrib [README.md](https://github.com/olofvndrhr/manga-dlp/blob/master/contrib/README.md) +If you want to add support for a new site, there is an api [template file](https://github.com/olofvndrhr/manga-dlp/tree/master/contrib/api_template.py) which you can use. +And more infos and tools are in the contrib [README.md](https://github.com/olofvndrhr/manga-dlp/tree/master/contrib/README.md) -Otherwise, you can open am issue with the name of the site which you want support for. (not guaranteed to be -implemented) +Otherwise, you can open an issue with the name of the site which you want support for (not guaranteed to be +implemented). If you encounter any bugs, also just open an issue with a description of the problem. ## TODO's -- Make docker container for easy distribution - --> [Dockerhub](https://hub.docker.com/repository/docker/olofvndrhr/manga-dlp) -- Automate release - --> Done with woodpecker-ci -- Make pypi package - --> Done with release [2.1.7](https://pypi.org/project/manga-dlp/) -- Add more supported sites +- Make docker container for easy distribution + --> [Dockerhub](https://hub.docker.com/r/olofvndrhr/manga-dlp) +- Automate release + --> Done with woodpecker-ci +- Make pypi package + --> Done with release [2.1.7](https://pypi.org/project/manga-dlp/) +- Add more supported sites diff --git a/justfile b/justfile index cd35d4d..c8e5ee7 100755 --- a/justfile +++ b/justfile @@ -68,45 +68,39 @@ create_venv: @python3 -m venv venv install_deps: + @echo "installing dependencies" + @pip3 install -r requirements.txt + +install_deps_dev: @echo "installing dependencies" @pip3 install -r contrib/requirements_dev.txt test_shfmt: - @find . -type f \( -name "**.sh" -and -not -path "./venv/*" -and -not -path "./.tox/*" \) -exec shfmt -d -i 4 -bn -ci -sr "{}" \+; + @find . -type f \( -name "**.sh" -and -not -path "./.**" -and -not -path "./venv**" \) -exec shfmt -d -i 4 -bn -ci -sr "{}" \+; test_black: - @python3 -m black --check --diff . + @python3 -m black --check --diff mangadlp/ -test_isort: - @python3 -m isort --check-only --diff . +test_pyright: + @python3 -m pyright mangadlp/ -test_mypy: - @python3 -m mypy --install-types --non-interactive mangadlp/ +test_ruff: + @python3 -m ruff --diff mangadlp/ + +test_ci_conf: + @woodpecker-cli lint .woodpecker/ test_pytest: @python3 -m tox -e basic -test_autoflake: - @python3 -m autoflake --remove-all-unused-imports -r -v mangadlp/ - @python3 -m autoflake --check --remove-all-unused-imports -r -v mangadlp/ - -test_pylama: - @python3 -m pylama --options tox.ini mangadlp/ - -test_pylint: - @python3 -m pylint --fail-under 9 mangadlp/ +test_coverage: + @python3 -m tox -e coverage test_tox: @python3 -m tox -test_tox_coverage: - @python3 -m tox -e coverage - test_build: - @python3 -m hatch build - -test_ci_conf: - @woodpecker-cli lint .woodpecker/ + @python3 -m hatch build --clean test_docker_build: @docker build . -f docker/Dockerfile.amd64 -t manga-dlp:test @@ -123,11 +117,8 @@ lint: -just test_ci_conf just test_shfmt just test_black - just test_isort - just test_mypy - just test_autoflake - just test_pylama - just test_pylint + just test_pyright + just test_ruff @echo -e "\n\033[0;32m=== ALL DONE ===\033[0m\n" tests: @@ -135,11 +126,8 @@ tests: -just test_ci_conf just test_shfmt just test_black - just test_isort - just test_mypy - just test_autoflake - just test_pylama - just test_pylint + just test_pyright + just test_ruff just test_pytest @echo -e "\n\033[0;32m=== ALL DONE ===\033[0m\n" @@ -148,13 +136,10 @@ tests_full: -just test_ci_conf just test_shfmt just test_black - just test_isort - just test_mypy - just test_autoflake - just test_pylama - just test_pylint + just test_pyright + just test_ruff just test_build just test_tox - just test_tox_coverage + just test_coverage just test_docker_build @echo -e "\n\033[0;32m=== ALL DONE ===\033[0m\n" diff --git a/mangadlp/__about__.py b/mangadlp/__about__.py index 55e4709..3a5935a 100644 --- a/mangadlp/__about__.py +++ b/mangadlp/__about__.py @@ -1 +1 @@ -__version__ = "2.3.0" +__version__ = "2.3.1" diff --git a/mangadlp/api/mangadex.py b/mangadlp/api/mangadex.py index 12abf93..1d7b046 100644 --- a/mangadlp/api/mangadex.py +++ b/mangadlp/api/mangadex.py @@ -1,15 +1,18 @@ import re from time import sleep +from typing import Any, Dict, List import requests from loguru import logger as log from mangadlp import utils +from mangadlp.types import ChapterData, ComicInfo class Mangadex: """Mangadex API Class. - Get infos for a manga from mangadex.org + + Get infos for a manga from mangadex.org. Args: url_uuid (str): URL or UUID of the manga @@ -64,10 +67,10 @@ class Mangadex: log.error("No valid UUID found") raise exc - return uuid + return uuid # pyright:ignore # make initial request - def get_manga_data(self) -> dict: + def get_manga_data(self) -> Dict[str, Any]: log.debug(f"Getting manga data for: {self.manga_uuid}") counter = 1 while counter <= 3: @@ -84,12 +87,14 @@ class Mangadex: counter += 1 else: break + + response_body: Dict[str, Dict[str, Any]] = response.json() # pyright:ignore # check if manga exists - if response.json()["result"] != "ok": + if response_body["result"] != "ok": # type:ignore log.error("Manga not found") raise KeyError - return response.json()["data"] + return response_body["data"] # get the title of the manga (and fix the filename) def get_manga_title(self) -> str: @@ -111,7 +116,7 @@ class Mangadex: if item.get(self.language): alt_title = item break - title = alt_title[self.language] + title = alt_title[self.language] # pyright:ignore except (KeyError, UnboundLocalError): log.warning( "Manga title also not found in alt titles. Falling back to english title" @@ -132,7 +137,7 @@ class Mangadex: timeout=10, ) try: - total_chapters = r.json()["total"] + total_chapters: int = r.json()["total"] except Exception as exc: log.error( "Error retrieving the chapters list. Did you specify a valid language code?" @@ -146,13 +151,13 @@ class Mangadex: return total_chapters # get chapter data like name, uuid etc - def get_chapter_data(self) -> dict: + def get_chapter_data(self) -> Dict[str, ChapterData]: log.debug(f"Getting chapter data for: {self.manga_uuid}") api_sorting = "order[chapter]=asc&order[volume]=asc" # check for chapters in specified lang total_chapters = self.check_chapter_lang() - chapter_data = {} + chapter_data: dict[str, ChapterData] = {} last_volume, last_chapter = ("", "") offset = 0 while offset < total_chapters: # if more than 500 chapters @@ -160,8 +165,9 @@ class Mangadex: f"{self.api_base_url}/manga/{self.manga_uuid}/feed?{api_sorting}&limit=500&offset={offset}&{self.api_additions}", timeout=10, ) - for chapter in r.json()["data"]: - attributes: dict = chapter["attributes"] + response_body: Dict[str, Any] = r.json() + for chapter in response_body["data"]: + attributes: Dict[str, Any] = chapter["attributes"] # chapter infos from feed chapter_num: str = attributes.get("chapter") or "" chapter_vol: str = attributes.get("volume") or "" @@ -203,7 +209,7 @@ class Mangadex: return chapter_data # get images for the chapter (mangadex@home) - def get_chapter_images(self, chapter: str, wait_time: float) -> list: + def get_chapter_images(self, chapter: str, wait_time: float) -> List[str]: log.debug(f"Getting chapter images for: {self.manga_uuid}") athome_url = f"{self.api_base_url}/at-home/server" chapter_uuid = self.manga_chapter_data[chapter]["uuid"] @@ -237,11 +243,11 @@ class Mangadex: if api_error: return [] - chapter_hash = api_data["chapter"]["hash"] - chapter_img_data = api_data["chapter"]["data"] + chapter_hash = api_data["chapter"]["hash"] # pyright:ignore + chapter_img_data = api_data["chapter"]["data"] # pyright:ignore # get list of image urls - image_urls = [] + image_urls: List[str] = [] for image in chapter_img_data: image_urls.append(f"{self.img_base_url}/data/{chapter_hash}/{image}") @@ -250,9 +256,9 @@ class Mangadex: return image_urls # create list of chapters - def create_chapter_list(self) -> list: + def create_chapter_list(self) -> List[str]: log.debug(f"Creating chapter list for: {self.manga_uuid}") - chapter_list = [] + chapter_list: List[str] = [] for data in self.manga_chapter_data.values(): chapter_number: str = data["chapter"] volume_number: str = data["volume"] @@ -263,15 +269,15 @@ class Mangadex: return chapter_list - def create_metadata(self, chapter: str) -> dict: + def create_metadata(self, chapter: str) -> ComicInfo: log.info("Creating metadata from api") chapter_data = self.manga_chapter_data[chapter] try: - volume = int(chapter_data.get("volume")) + volume = int(chapter_data["volume"]) except (ValueError, TypeError): volume = None - metadata = { + metadata: ComicInfo = { "Volume": volume, "Number": chapter_data.get("chapter"), "PageCount": chapter_data.get("pages"), diff --git a/mangadlp/app.py b/mangadlp/app.py index 1fb58a8..6ee44c0 100644 --- a/mangadlp/app.py +++ b/mangadlp/app.py @@ -1,7 +1,7 @@ import re import shutil from pathlib import Path -from typing import Any, Union +from typing import Any, Dict, List, Tuple, Union from loguru import logger as log @@ -10,11 +10,12 @@ from mangadlp.api.mangadex import Mangadex from mangadlp.cache import CacheDB from mangadlp.hooks import run_hook from mangadlp.metadata import write_metadata +from mangadlp.types import ChapterData from mangadlp.utils import get_file_format def match_api(url_uuid: str) -> type: - """Match the correct api class from a string + """Match the correct api class from a string. Args: url_uuid: url/uuid to check @@ -22,9 +23,8 @@ def match_api(url_uuid: str) -> type: Returns: The class of the API to use """ - # apis to check - apis: list[tuple[str, re.Pattern, type]] = [ + apis: List[Tuple[str, re.Pattern[str], type]] = [ ( "mangadex.org", re.compile( @@ -53,6 +53,7 @@ def match_api(url_uuid: str) -> type: class MangaDLP: """Download Mangas from supported sites. + After initialization, start the script with the function get_manga(). Args: @@ -108,7 +109,7 @@ class MangaDLP: self.chapter_post_hook_cmd = chapter_post_hook_cmd self.cache_path = cache_path self.add_metadata = add_metadata - self.hook_infos: dict = {} + self.hook_infos: Dict[str, Any] = {} # prepare everything self._prepare() @@ -226,7 +227,7 @@ class MangaDLP: skipped_chapters: list[Any] = [] error_chapters: list[Any] = [] for chapter in chapters_to_download: - if self.cache_path and chapter in cached_chapters: + if self.cache_path and chapter in cached_chapters: # pyright:ignore log.info(f"Chapter '{chapter}' is in cache. Skipping download") continue @@ -240,7 +241,7 @@ class MangaDLP: skipped_chapters.append(chapter) # update cache if self.cache_path: - cache.add_chapter(chapter) + cache.add_chapter(chapter) # pyright:ignore continue except Exception: # skip download/packing due to an error @@ -273,7 +274,7 @@ class MangaDLP: # update cache if self.cache_path: - cache.add_chapter(chapter) + cache.add_chapter(chapter) # pyright:ignore # start chapter post hook run_hook( @@ -310,7 +311,7 @@ class MangaDLP: # once called per chapter def get_chapter(self, chapter: str) -> Path: # get chapter infos - chapter_infos: dict = self.api.manga_chapter_data[chapter] + chapter_infos: ChapterData = self.api.manga_chapter_data[chapter] log.debug(f"Chapter infos: {chapter_infos}") # get image urls for chapter @@ -352,7 +353,7 @@ class MangaDLP: log.debug(f"Filename: '{chapter_filename}'") # set download path for chapter (image folder) - chapter_path = self.manga_path / chapter_filename + chapter_path: Path = self.manga_path / chapter_filename # set archive path with file format chapter_archive_path = Path(f"{chapter_path}{self.file_format}") diff --git a/mangadlp/cache.py b/mangadlp/cache.py index a2077b5..1781dba 100644 --- a/mangadlp/cache.py +++ b/mangadlp/cache.py @@ -26,12 +26,14 @@ class CacheDB: if not self.db_data.get(self.db_key): self.db_data[self.db_key] = {} - self.db_uuid_data: dict = self.db_data[self.db_key] + self.db_uuid_data = self.db_data[self.db_key] if not self.db_uuid_data.get("name"): self.db_uuid_data.update({"name": self.name}) self._write_db() - self.db_uuid_chapters: list = self.db_uuid_data.get("chapters") or [] + self.db_uuid_chapters: List[str] = ( + self.db_uuid_data.get("chapters") or [] # type:ignore + ) def _prepare_db(self) -> None: if self.db_path.exists(): @@ -44,11 +46,11 @@ class CacheDB: log.error("Can't create db-file") raise exc - def _read_db(self) -> Dict[str, dict]: + def _read_db(self) -> Dict[str, Dict[str, Union[str, List[str]]]]: log.info(f"Reading cache-db: {self.db_path}") try: db_txt = self.db_path.read_text(encoding="utf8") - db_dict: dict[str, dict] = json.loads(db_txt) + db_dict: Dict[str, Dict[str, Union[str, List[str]]]] = json.loads(db_txt) except Exception as exc: log.error("Can't load cache-db") raise exc @@ -73,7 +75,7 @@ class CacheDB: raise exc -def sort_chapters(chapters: list) -> List[str]: +def sort_chapters(chapters: List[str]) -> List[str]: try: sorted_list = sorted(chapters, key=float) except Exception: diff --git a/mangadlp/cli.py b/mangadlp/cli.py index b62e1cc..998e966 100644 --- a/mangadlp/cli.py +++ b/mangadlp/cli.py @@ -1,5 +1,6 @@ import sys from pathlib import Path +from typing import Any, List import click from click_option_group import ( @@ -15,7 +16,7 @@ from mangadlp.logger import prepare_logger # read in the list of links from a file -def readin_list(_ctx, _param, value) -> list: +def readin_list(_ctx: click.Context, _param: str, value: str) -> List[str]: if not value: return [] @@ -38,8 +39,8 @@ def readin_list(_ctx, _param, value) -> list: @click.help_option() @click.version_option(version=__version__, package_name="manga-dlp") # manga selection -@optgroup.group("source", cls=RequiredMutuallyExclusiveOptionGroup) -@optgroup.option( +@optgroup.group("source", cls=RequiredMutuallyExclusiveOptionGroup) # type: ignore +@optgroup.option( # type: ignore "-u", "--url", "--uuid", @@ -49,19 +50,19 @@ def readin_list(_ctx, _param, value) -> list: show_default=True, help="URL or UUID of the manga", ) -@optgroup.option( +@optgroup.option( # type: ignore "--read", "read_mangas", is_eager=True, callback=readin_list, - type=click.Path(exists=True, dir_okay=False), + type=click.Path(exists=True, dir_okay=False, path_type=str), default=None, show_default=True, help="Path of file with manga links to download. One per line", ) # logging options -@optgroup.group("verbosity", cls=MutuallyExclusiveOptionGroup) -@optgroup.option( +@optgroup.group("verbosity", cls=MutuallyExclusiveOptionGroup) # type: ignore +@optgroup.option( # type: ignore "--loglevel", "verbosity", type=int, @@ -69,7 +70,7 @@ def readin_list(_ctx, _param, value) -> list: show_default=True, help="Custom log level", ) -@optgroup.option( +@optgroup.option( # type: ignore "--warn", "verbosity", flag_value=30, @@ -77,7 +78,7 @@ def readin_list(_ctx, _param, value) -> list: show_default=True, help="Only log warnings and higher", ) -@optgroup.option( +@optgroup.option( # type: ignore "--debug", "verbosity", flag_value=10, @@ -227,12 +228,8 @@ def readin_list(_ctx, _param, value) -> list: help="Enable/disable creation of metadata via ComicInfo.xml", ) @click.pass_context -def main(ctx: click.Context, **kwargs) -> None: - """ - Script to download mangas from various sites - - """ - +def main(ctx: click.Context, **kwargs: Any) -> None: + """Script to download mangas from various sites.""" url_uuid: str = kwargs.pop("url_uuid") read_mangas: list[str] = kwargs.pop("read_mangas") verbosity: int = kwargs.pop("verbosity") diff --git a/mangadlp/downloader.py b/mangadlp/downloader.py index 3826531..4630dd6 100644 --- a/mangadlp/downloader.py +++ b/mangadlp/downloader.py @@ -2,7 +2,7 @@ import logging import shutil from pathlib import Path from time import sleep -from typing import Union +from typing import List, Union import requests from loguru import logger as log @@ -12,7 +12,7 @@ from mangadlp import utils # download images def download_chapter( - image_urls: list, + image_urls: List[str], chapter_path: Union[str, Path], download_wait: float, ) -> None: @@ -48,8 +48,8 @@ def download_chapter( # write image try: with image_path.open("wb") as file: - r.raw.decode_content = True - shutil.copyfileobj(r.raw, file) + r.raw.decode_content = True # pyright:ignore + shutil.copyfileobj(r.raw, file) # pyright:ignore except Exception as exc: log.error("Can't write file") raise exc diff --git a/mangadlp/hooks.py b/mangadlp/hooks.py index 31c702a..db3d50e 100644 --- a/mangadlp/hooks.py +++ b/mangadlp/hooks.py @@ -1,11 +1,15 @@ import os import subprocess +from typing import Any from loguru import logger as log -def run_hook(command: str, hook_type: str, **kwargs) -> int: - """ +def run_hook(command: str, hook_type: str, **kwargs: Any) -> int: + """Run a command. + + Run a command with subprocess.run and add kwargs to the environment. + Args: command (str): command to run hook_type (str): type of the hook @@ -14,7 +18,6 @@ def run_hook(command: str, hook_type: str, **kwargs) -> int: Returns: exit_code (int): exit code of command """ - # check if hook commands are empty if not command or command == "None": log.debug(f"Hook '{hook_type}' empty. Not running") diff --git a/mangadlp/logger.py b/mangadlp/logger.py index 576baa5..f0b0897 100644 --- a/mangadlp/logger.py +++ b/mangadlp/logger.py @@ -1,5 +1,6 @@ import logging import sys +from typing import Any, Dict from loguru import logger @@ -8,11 +9,9 @@ LOGURU_FMT = "{time:%Y-%m-%dT%H:%M:%S%z} | [{level: <7}] [{name: # from loguru docs class InterceptHandler(logging.Handler): - """ - Intercept python logging messages and log them via loguru.logger - """ + """Intercept python logging messages and log them via loguru.logger.""" - def emit(self, record): + def emit(self, record: Any) -> None: # Get corresponding Loguru level if it exists try: level = logger.level(record.levelname).name @@ -21,8 +20,8 @@ class InterceptHandler(logging.Handler): # Find caller from where originated the logged message frame, depth = logging.currentframe(), 2 - while frame.f_code.co_filename == logging.__file__: - frame = frame.f_back + while frame.f_code.co_filename == logging.__file__: # pyright:ignore + frame = frame.f_back # type: ignore depth += 1 logger.opt(depth=depth, exception=record.exc_info).log( @@ -32,7 +31,7 @@ class InterceptHandler(logging.Handler): # init logger with format and log level def prepare_logger(loglevel: int = 20) -> None: - config: dict = { + config: Dict[str, Any] = { "handlers": [ { "sink": sys.stdout, diff --git a/mangadlp/metadata.py b/mangadlp/metadata.py index 343a3cc..2bd9701 100644 --- a/mangadlp/metadata.py +++ b/mangadlp/metadata.py @@ -1,14 +1,18 @@ from pathlib import Path -from typing import Any, Dict, Tuple +from typing import Any, Dict, List, Tuple, Union import xmltodict from loguru import logger as log +from mangadlp.types import ComicInfo + METADATA_FILENAME = "ComicInfo.xml" METADATA_TEMPLATE = Path("mangadlp/metadata/ComicInfo_v2.0.xml") # define metadata types, defaults and valid values. an empty list means no value check # {key: (type, default value, valid values)} -METADATA_TYPES: Dict[str, Tuple[type, Any, list]] = { +METADATA_TYPES: Dict[ + str, Tuple[Any, Union[str, int, None], List[Union[str, int, None]]] +] = { "Title": (str, None, []), "Series": (str, None, []), "Number": (str, None, []), @@ -59,10 +63,10 @@ METADATA_TYPES: Dict[str, Tuple[type, Any, list]] = { } -def validate_metadata(metadata_in: dict) -> Dict[str, dict]: +def validate_metadata(metadata_in: ComicInfo) -> Dict[str, ComicInfo]: log.info("Validating metadata") - metadata_valid: dict[str, dict] = {"ComicInfo": {}} + metadata_valid: dict[str, ComicInfo] = {"ComicInfo": {}} for key, value in METADATA_TYPES.items(): metadata_type, metadata_default, metadata_validation = value @@ -75,7 +79,7 @@ def validate_metadata(metadata_in: dict) -> Dict[str, dict]: # check if metadata key is available try: - md_to_check = metadata_in[key] + md_to_check: Union[str, int, None] = metadata_in[key] except KeyError: continue # check if provided metadata item is empty @@ -84,7 +88,7 @@ def validate_metadata(metadata_in: dict) -> Dict[str, dict]: # check if metadata type is correct log.debug(f"Key:{key} -> value={type(md_to_check)} -> check={metadata_type}") - if not isinstance(md_to_check, metadata_type): # noqa + if not isinstance(md_to_check, metadata_type): log.warning( f"Metadata has wrong type: {key}:{metadata_type} -> {md_to_check}" ) @@ -104,8 +108,8 @@ def validate_metadata(metadata_in: dict) -> Dict[str, dict]: return metadata_valid -def write_metadata(chapter_path: Path, metadata: dict) -> None: - if metadata["Format"] == "pdf": +def write_metadata(chapter_path: Path, metadata: ComicInfo) -> None: + if metadata["Format"] == "pdf": # pyright:ignore log.warning("Can't add metadata for pdf format. Skipping") return diff --git a/mangadlp/types.py b/mangadlp/types.py new file mode 100644 index 0000000..b924bf4 --- /dev/null +++ b/mangadlp/types.py @@ -0,0 +1,50 @@ +from typing import Optional, TypedDict + + +class ComicInfo(TypedDict, total=False): + """ComicInfo.xml basic types. + + Validation is done via metadata.validate_metadata() + All valid types and values are specified in metadata.METADATA_TYPES + """ + + Title: Optional[str] + Series: Optional[str] + Number: Optional[str] + Count: Optional[int] + Volume: Optional[int] + AlternateSeries: Optional[str] + AlternateNumber: Optional[str] + AlternateCount: Optional[int] + Summary: Optional[str] + Notes: Optional[str] + Year: Optional[int] + Month: Optional[int] + Day: Optional[int] + Writer: Optional[str] + Colorist: Optional[str] + Publisher: Optional[str] + Genre: Optional[str] + Web: Optional[str] + PageCount: Optional[int] + LanguageISO: Optional[str] + Format: Optional[str] + BlackAndWhite: Optional[str] + Manga: Optional[str] + ScanInformation: Optional[str] + SeriesGroup: Optional[str] + AgeRating: Optional[str] + CommunityRating: Optional[int] + + +class ChapterData(TypedDict): + """Basic chapter-data types. + + All values have to be provided. + """ + + uuid: str + volume: str + chapter: str + name: str + pages: int diff --git a/mangadlp/utils.py b/mangadlp/utils.py index d305a45..0d3c31e 100644 --- a/mangadlp/utils.py +++ b/mangadlp/utils.py @@ -24,7 +24,7 @@ def make_archive(chapter_path: Path, file_format: str) -> None: def make_pdf(chapter_path: Path) -> None: try: - import img2pdf # pylint: disable=import-outside-toplevel + import img2pdf # pylint: disable=import-outside-toplevel # pyright:ignore except Exception as exc: log.error("Cant import img2pdf. Please install it first") raise exc @@ -34,14 +34,14 @@ def make_pdf(chapter_path: Path) -> None: for file in chapter_path.iterdir(): images.append(str(file)) try: - pdf_path.write_bytes(img2pdf.convert(images)) + pdf_path.write_bytes(img2pdf.convert(images)) # pyright:ignore except Exception as exc: log.error("Can't create '.pdf' archive") raise exc # create a list of chapters -def get_chapter_list(chapters: str, available_chapters: list) -> List[str]: +def get_chapter_list(chapters: str, available_chapters: List[str]) -> List[str]: # check if there are available chapter chapter_list: list[str] = [] for chapter in chapters.split(","): diff --git a/pyproject.toml b/pyproject.toml index 9762d3c..2e99004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling>=1.11.0"] +requires = ["hatchling>=1.11.0"] build-backend = "hatchling.build" [project] @@ -9,14 +9,8 @@ description = "A cli manga downloader" readme = "README.md" license = "MIT" requires-python = ">=3.8" -authors = [ - { name = "Ivan Schaller", email = "ivan@schaller.sh" }, -] -keywords = [ - "manga", - "downloader", - "mangadex", -] +authors = [{ name = "Ivan Schaller", email = "ivan@schaller.sh" }] +keywords = ["manga", "downloader", "mangadex"] classifiers = [ "License :: OSI Approved :: MIT License", "Natural Language :: English", @@ -30,17 +24,17 @@ dependencies = [ "loguru>=0.6.0", "click>=8.1.3", "click-option-group>=0.5.5", - "xmltodict>=0.13.0" + "xmltodict>=0.13.0", ] [project.urls] Homepage = "https://github.com/olofvndrhr/manga-dlp" -History = "https://github.com/olofvndrhr/manga-dlp/commits/master" -Tracker = "https://github.com/olofvndrhr/manga-dlp/issues" -Source = "https://github.com/olofvndrhr/manga-dlp" +History = "https://github.com/olofvndrhr/manga-dlp/commits/master" +Tracker = "https://github.com/olofvndrhr/manga-dlp/issues" +Source = "https://github.com/olofvndrhr/manga-dlp" [project.scripts] -mangadlp = "mangadlp.cli:main" +mangadlp = "mangadlp.cli:main" manga-dlp = "mangadlp.cli:main" [tool.hatch.version] @@ -69,44 +63,86 @@ dependencies = [ "pytest>=7.0.0", "coverage>=6.3.1", "black>=22.1.0", - "isort>=5.10.0", - "pylint>=2.13.0", "mypy>=0.940", "tox>=3.24.5", - "autoflake>=1.4", - "pylama>=8.3.8", + "ruff>=0.0.247", ] -[tool.isort] -py_version = 39 -skip_gitignore = true -line_length = 88 -profile = "black" -multi_line_output = 3 -include_trailing_comma = true -use_parentheses = true +# pyright -[tool.mypy] -python_version = "3.9" -disallow_untyped_defs = false -follow_imports = "normal" -ignore_missing_imports = true -warn_no_return = false -warn_unused_ignores = true -show_error_context = true -show_column_numbers = true -show_error_codes = true -pretty = true -no_implicit_optional = false +[tool.pyright] +typeCheckingMode = "strict" +pythonVersion = "3.9" +reportUnnecessaryTypeIgnoreComment = true +reportShadowedImports = true +reportUnusedExpression = true +reportMatchNotExhaustive = true +# venvPath = "." +# venv = "venv" + +# ruff + +[tool.ruff] +target-version = "py39" +select = [ + "E", # pycodetyle err + "W", # pycodetyle warn + "D", # pydocstyle + "C90", # mccabe + "I", # isort + "PLE", # pylint err + "PLW", # pylint warn + "PLC", # pylint convention + "PLR", # pylint refactor + "F", # pyflakes + "RUF", # ruff specific +] +line-length = 88 +fix = true +show-fixes = true +format = "grouped" +ignore-init-module-imports = true +respect-gitignore = true +ignore = ["E501", "D103", "D100", "D102", "PLR2004"] +exclude = [ + ".direnv", + ".git", + ".mypy_cache", + ".ruff_cache", + ".svn", + ".venv", + "venv", + "__pypackages__", + "build", + "dist", + "venv", +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["D104"] + +[tool.ruff.pylint] +max-args = 10 + +[tool.ruff.mccabe] +max-complexity = 10 + +[tool.ruff.pydocstyle] +convention = "google" + +[tool.ruff.pycodestyle] +max-doc-length = 88 + +# pytest [tool.pytest.ini_options] -pythonpath = [ - "." -] +pythonpath = ["."] + +# coverage [tool.coverage.run] -source = ["mangadlp"] -branch = true +source = ["mangadlp"] +branch = true command_line = "-m pytest --exitfirst" [tool.coverage.report] @@ -127,12 +163,3 @@ exclude_lines = [ "@(abc.)?abstractmethod", ] ignore_errors = true - -[tool.pylint.main] -py-version = "3.9" - -[tool.pylint.logging] -logging-modules = ["logging", "loguru"] -disable = "C0301, C0114, C0116, W0703, R0902, R0913, E0401, W1203" -good-names = "r" -logging-format-style = "new" diff --git a/tests/test_04_input.py b/tests/test_04_input.py index 093558d..a4e4636 100644 --- a/tests/test_04_input.py +++ b/tests/test_04_input.py @@ -16,8 +16,6 @@ def test_read_and_url(): def test_no_read_and_url(): - url_uuid = "https://mangadex.org/title/0aea9f43-d4a9-4bf7-bebc-550a512f9b95/shikimori-s-not-just-a-cutie" - link_file = "tests/testfile.txt" language = "en" chapters = "1" file_format = "cbz" @@ -30,7 +28,6 @@ def test_no_read_and_url(): def test_no_chaps(): url_uuid = "https://mangadex.org/title/0aea9f43-d4a9-4bf7-bebc-550a512f9b95/shikimori-s-not-just-a-cutie" language = "en" - chapters = "" file_format = "cbz" download_path = "tests" command_args = f"-u {url_uuid} -l {language} --path {download_path} --format {file_format} --debug" diff --git a/tests/test_05_hooks.py b/tests/test_05_hooks.py index 8eb75ac..31ee074 100644 --- a/tests/test_05_hooks.py +++ b/tests/test_05_hooks.py @@ -40,7 +40,7 @@ def test_manga_pre_hook(wait_10s): manga_pre_hook, ] script_path = "manga-dlp.py" - command = ["python3", script_path] + command_args + command = ["python3", script_path, *command_args] assert subprocess.call(command) == 0 assert hook_file.is_file() @@ -72,7 +72,7 @@ def test_manga_post_hook(wait_10s): manga_post_hook, ] script_path = "manga-dlp.py" - command = ["python3", script_path] + command_args + command = ["python3", script_path, *command_args] assert subprocess.call(command) == 0 assert hook_file.is_file() @@ -104,7 +104,7 @@ def test_chapter_pre_hook(wait_10s): chapter_pre_hook, ] script_path = "manga-dlp.py" - command = ["python3", script_path] + command_args + command = ["python3", script_path, *command_args] assert subprocess.call(command) == 0 assert hook_file.is_file() @@ -136,7 +136,7 @@ def test_chapter_post_hook(wait_10s): chapter_post_hook, ] script_path = "manga-dlp.py" - command = ["python3", script_path] + command_args + command = ["python3", script_path, *command_args] assert subprocess.call(command) == 0 assert hook_file.is_file() @@ -176,7 +176,7 @@ def test_all_hooks(wait_10s): chapter_post_hook, ] script_path = "manga-dlp.py" - command = ["python3", script_path] + command_args + command = ["python3", script_path, *command_args] assert subprocess.call(command) == 0 assert Path("tests/manga-pre2.txt").is_file() diff --git a/tests/test_06_cache.py b/tests/test_06_cache.py index a3e065b..c46c96b 100644 --- a/tests/test_06_cache.py +++ b/tests/test_06_cache.py @@ -6,7 +6,7 @@ from mangadlp.cache import CacheDB def test_cache_creation(): cache_file = Path("cache.json") - cache = CacheDB(cache_file, "abc", "en", "test") + CacheDB(cache_file, "abc", "en", "test") assert cache_file.exists() cache_file.unlink() diff --git a/tests/test_07_metadata.py b/tests/test_07_metadata.py index 33ff523..c67b1fe 100644 --- a/tests/test_07_metadata.py +++ b/tests/test_07_metadata.py @@ -133,7 +133,7 @@ def test_metadata_chapter_validity(wait_20s): schema = xmlschema.XMLSchema("mangadlp/metadata/ComicInfo_v2.0.xsd") script_path = "manga-dlp.py" - command = ["python3", script_path] + command_args + command = ["python3", script_path, *command_args] assert subprocess.call(command) == 0 assert metadata_path.is_file() diff --git a/tests/test_11_api_mangadex.py b/tests/test_11_api_mangadex.py index 38ace21..41b34cf 100644 --- a/tests/test_11_api_mangadex.py +++ b/tests/test_11_api_mangadex.py @@ -56,7 +56,7 @@ def test_alt_title_fallback(): forcevol = False test = Mangadex(url_uuid, language, forcevol) - assert test.manga_title == "Iruma à l’école des démons" + assert test.manga_title == "Iruma à l’école des démons" # noqa def test_chapter_infos(): @@ -206,7 +206,6 @@ def test_get_chapter_images(): test = Mangadex(url_uuid, language, forcevol) img_base_url = "https://uploads.mangadex.org" chapter_hash = "0752bc5db298beff6b932b9151dd8437" - chapter_uuid = "e86ec2c4-c5e4-4710-bfaa-7604f00939c7" chapter_num = "1" test_list = [ f"{img_base_url}/data/{chapter_hash}/x1-0deb4c9bfedd5be49e0a90cfb17cf343888239898c9e7451d569c0b3ea2971f4.jpg", diff --git a/tox.ini b/tox.ini index 22d02c8..a32eeb8 100644 --- a/tox.ini +++ b/tox.ini @@ -24,8 +24,3 @@ commands = coverage erase coverage run coverage xml -i - -[pylama] -format = pycodestyle -linters = mccabe,pycodestyle,pyflakes -ignore = E501,C901,C0301