diff --git a/.flake8 b/.flake8 deleted file mode 100644 index f2b9c0f..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 120 -extend-ignore = E203, E501, A003 -extend-exclude = venv, scripts, build, dist, comictaggerlib/ctversion.py -per-file-ignores = - comictaggerlib/cli.py: T20 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 337661e..165951a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,8 +1,8 @@ name: CI env: - PIP: pip - PYTHON: python + PKG_CONFIG_PATH: /usr/local/opt/icu4c/lib/pkgconfig + LC_COLLATE: en_US.UTF-8 on: pull_request: push: @@ -23,24 +23,18 @@ jobs: os: [ubuntu-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: syphar/restore-virtualenv@v1.2 - id: cache-virtualenv - - - uses: syphar/restore-pip-download-cache@v1 - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - - name: Install build dependencies run: | - python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt + python -m pip install flake8 - uses: reviewdog/action-setup@v1 with: @@ -57,61 +51,45 @@ jobs: os: [ubuntu-latest, macos-10.15, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: syphar/restore-virtualenv@v1.2 - id: cache-virtualenv - - - uses: syphar/restore-pip-download-cache@v1 - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - - - name: Install build dependencies + - name: Install tox run: | - python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt + python -m pip install --upgrade --upgrade-strategy eager tox - - name: Install Windows build dependencies - run: | - choco install -y zip - if: runner.os == 'Windows' - name: Install macos dependencies run: | brew install icu4c pkg-config - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'macOS' + - name: Install linux dependencies run: | - sudo apt-get install pkg-config libicu-dev - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'Linux' - name: Build and install PyPi packages run: | - make clean pydist - python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]" - - - name: build - run: | - make dist + python -m tox r -m build - name: Archive production artifacts - uses: actions/upload-artifact@v2 - if: runner.os != 'Linux' # linux binary currently has a segfault when running on latest fedora + uses: actions/upload-artifact@v3 with: name: "${{ format('ComicTagger-{0}', runner.os) }}" path: | dist/*.zip + dist/*.AppImage - name: PyTest run: | - python -m pytest + python -m tox r diff --git a/.github/workflows/contributions.yaml b/.github/workflows/contributions.yaml new file mode 100644 index 0000000..df7870b --- /dev/null +++ b/.github/workflows/contributions.yaml @@ -0,0 +1,43 @@ +name: Contributions +on: + push: + branches: + - 'develop' + tags-ignore: + - '**' + +jobs: + contrib-readme-job: + permissions: + contents: write + runs-on: ubuntu-latest + env: + CI_COMMIT_AUTHOR: github-actions[bot] + CI_COMMIT_EMAIL: <41898282+github-actions[bot]@users.noreply.github.com> + CI_COMMIT_MESSAGE: Update AUTHORS + name: A job to automate contrib in readme + steps: + - name: Contribute List + uses: akhilmhdh/contributors-readme-action@v2.3.6 + with: + use_username: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Update AUTHORS + run: | + git config --global log.mailmap true + git log --reverse '--format=%aN <%aE>' | cat -n | sort -uk2 | sort -n | cut -f2- >AUTHORS + + - name: Commit and push AUTHORS + run: | + if ! git diff --exit-code; then + git pull + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ env.CI_COMMIT_EMAIL }}" + git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" + git push + fi diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index eb37ecd..27f82c5 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -1,8 +1,8 @@ name: Package env: - PIP: pip - PYTHON: python + PKG_CONFIG_PATH: /usr/local/opt/icu4c/lib/pkgconfig + LC_COLLATE: en_US.UTF-8 on: push: tags: @@ -18,61 +18,40 @@ jobs: os: [ubuntu-latest, macos-10.15, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: syphar/restore-virtualenv@v1.2 - id: cache-virtualenv - - - uses: syphar/restore-pip-download-cache@v1 - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - - - name: Install build dependencies + - name: Install tox run: | - python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt + python -m pip install --upgrade --upgrade-strategy eager tox - - name: Install Windows build dependencies - run: | - choco install -y zip - if: runner.os == 'Windows' - name: Install macos dependencies run: | brew install icu4c pkg-config - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'macOS' + - name: Install linux dependencies run: | - sudo apt-get install pkg-config libicu-dev - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'Linux' - name: Build, Install and Test PyPi packages run: | - make clean pydist - python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]" - python -m flake8 - python -m pytest - - - name: "Publish distribution 📦 to PyPI" - if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Linux' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - packages_dir: dist - - - name: Build PyInstaller package - run: | - make dist + python -m tox r + python -m tox r -m release + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - name: Get release name if: startsWith(github.ref, 'refs/tags/') @@ -88,6 +67,8 @@ jobs: name: "${{ env.release_name }}" prerelease: "${{ contains(github.ref, '-') }}" # alpha-releases should be 1.3.0-alpha.x full releases should be 1.3.0 draft: false + # upload the single application zip file for each OS and include the wheel built on linux files: | - dist/!(*Linux).zip + dist/*.zip dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl + dist/*.AppImage diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..d5aa596 --- /dev/null +++ b/.mailmap @@ -0,0 +1,9 @@ +Andrew W. Buchanan +Davide Romanini +Davide Romanini +Michael Fitzurka +Timmy Welch +beville <(no author)@6c5673fe-1810-88d6-992b-cd32ca31540c> +beville +beville +beville diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29226fc..fa79b71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,32 +13,32 @@ repos: rev: v2.2.0 hooks: - id: setup-cfg-fmt +- repo: https://github.com/PyCQA/autoflake + rev: v2.1.1 + hooks: + - id: autoflake + args: [-i, --remove-all-unused-imports, --ignore-init-module-imports] +- repo: https://github.com/asottile/pyupgrade + rev: v3.3.2 + hooks: + - id: pyupgrade + args: [--py39-plus] - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort args: [--af,--add-import, 'from __future__ import annotations'] -- repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [--py39-plus] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black -- repo: https://github.com/PyCQA/autoflake - rev: v2.0.1 - hooks: - - id: autoflake - args: [-i, --remove-all-unused-imports, --ignore-init-module-imports] - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, flake8-print] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.2.0 hooks: - id: mypy additional_dependencies: [types-setuptools, types-requests] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 99a9ce0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -language: python -# Only build tags -if: type = pull_request OR tag IS present -branches: - only: - - develop - - /^\d+\.\d+\.\d+.*$/ -env: - global: - - PYTHON=python3 - - PIP=pip3 - - SETUPTOOLS_SCM_PRETEND_VERSION=$TRAVIS_TAG - - MAKE=make -matrix: - include: - - os: linux - python: 3.8 - - name: "Python: 3.7" - os: osx - language: shell - python: 3.7 - env: PYTHON=python3 PIP="python3 -m pip" - cache: - - directories: - - $HOME/Library/Caches/pip - - os: windows - language: bash - env: PATH=/C/Python37:/C/Python37/Scripts:$PATH MAKE=mingw32-make PIP=pip PYTHON=python -before_install: - - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install -y python --version 3.7.9; choco install -y mingw zip; fi -install: - - $PIP install -r requirements_dev.txt - - $PIP install -r requirements-GUI.txt - - $PIP install -r requirements-CBR.txt -script: - - if [ "$TRAVIS_OS_NAME" != "linux" ]; then $MAKE dist ; fi - -deploy: - - name: "$TRAVIS_TAG" - body: Released ComicTagger $TRAVIS_TAG - provider: releases - skip_cleanup: true - api_key: - secure: RgohcOJOfLhXXT12bMWaLwOqhe+ClSCYXjYuUJuWK4/E1fdd1xu1ebdQU+MI/R8cZ0Efz3sr2n3NkO/Aa8gN68xEfuF7RVRMm64P9oPrfZgGdsD6H43rU/6kN8bgaDRmCYpLTfXaJ+/gq0x1QDkhWJuceF2BYEGGvL0BvS/TUsLyjVxs8ujTplLyguXHNEv4/7Yz7SBNZZmUHjBuq/y+l8ds3ra9rSgAVAN1tMXoFKJPv+SNNkpTo5WUNMPzBnN041F1rzqHwYDLog2V7Krp9JkXzheRFdAr51/tJBYzEd8AtYVdYvaIvoO6A4PiTZ7MpsmcZZPAWqLQU00UTm/PhT/LVR+7+f8lOBG07RgNNHB+edjDRz3TAuqyuZl9wURWTZKTPuO49TkZMz7Wm0DRNZHvBm1IXLeSG7Tll2YL1+WpZNZg+Dhro2J1QD3vxDXafhMdTCB4z0q5aKpG93IT0p6oXOO0oEGOPZYbA2c5R3SXWSyqd1E1gdhbVjIZr59h++TEf1zz07tvWHqPuAF/Ly/j+dIcY2wj0EzRWaSASWgUpTnMljAkHtWhqDw4GXGDRkRUWRJl1d0/JyVqCeIdRzDQNl8/q7BcO3F1zqr1PgnYdz0lfwWxL1/ekw2vHOJE/GOdkyvX0aJrnaOV338mjJbfGHYv4ESc9ow1kdtIbiU= - file_glob: true - file: dist/*.zip - draft: true - on: - tags: true - condition: $TRAVIS_OS_NAME != "linux" - - provider: pypi - user: __token__ - password: - secure: h+y5WkE8igf864dnsbGPFvOBkyPkuBYtnDRt+EgxHd71EZnV2YP7ns2Cx12su/SVVDdZCBlmHVtkhl6Jmqy+0rTkSYx+3mlBOqyl8Cj5+BlP/dP7Bdmhs2uLZk2YYL1avbC0A6eoNJFtCkjurnB/jCGE433rvMECWJ5x2HsQTKchCmDAEdAZbRBJrzLFsrIC+6NXW1IJZjd+OojbhLSyVar2Jr32foh6huTcBu/x278V1+zIC/Rwy3W67+3c4aZxYrI47FoYFza0jjFfr3EoSkKYUSByMTIvhWaqB2gIsF0T160jgDd8Lcgej+86ACEuG0v01VE7xoougqlOaJ94eAmapeM7oQXzekSwSAxcK3JQSfgWk/AvPhp07T4pQ8vCZmky6yqvVp1EzfKarTeub1rOnv+qo1znKLrBtOoq6t8pOAeczDdIDs51XT/hxaijpMRCM8vHxN4Kqnc4DY+3KcF7UFyH1ifQJHQe71tLBsM/GnAcJM5/3ykFVGvRJ716p4aa6IoGsdNk6bqlysNh7nURDl+bfm+CDXRkO2jkFwUFNqPHW7JwY6ZFx+b5SM3TzC3obJhfMS7OC37fo2geISOTR0xVie6NvpN6TjNAxFTfDxWJI7yH3Al2w43B3uYDd97WeiN+B+HVWtdaER87IVSRbRqFrRub+V+xrozT0y0= - skip_existing: true - skip_cleanup: true - on: - tags: true - condition: $TRAVIS_OS_NAME = "linux" diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..c526237 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,16 @@ +beville +Davide Romanini +fcanc +Alban Seurat +tlc +Marek Pawlak +Timmy Welch +J.P. Cranford +thFrgttn <39759781+thFrgttn@users.noreply.github.com> +Andrew W. Buchanan +Michael Fitzurka +Richard Haussmann +Mizaki +Xavier Jouvenot +github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> +Ben Longman diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f01cd8..1e9e4ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Please open a [GitHub Pull Request](https://github.com/comictagger/comictagger/p Currently only python 3.9 is supported however 3.10 will probably work if you try it -Those on linux should install `Pillow` from the system package manager if possible and if the GUI and/or the CBR/RAR comicbooks are going to be used `pyqt5` and `unrar-cffi` should be installed from the system package manager +Those on linux should install `Pillow` from the system package manager if possible and if the GUI `pyqt5` should be installed from the system package manager Those on macOS will need to ensure that you are using python3 in x86 mode either by installing an x86 only version of python or using the universal installer and using `python3-intel64` instead of `python3` @@ -50,10 +50,10 @@ Those on macOS will need to ensure that you are using python3 in x86 mode either git clone https://github.com/comictagger/comictagger.git ``` -2. It is preferred to use a virtual env for running from source, adding the `--system-site-packages` allows packages already installed via the system package manager to be used: +2. It is preferred to use a virtual env for running from source: ``` -python3 -m venv --system-site-packages venv +python3 -m venv venv ``` 3. Activate the virtual env: @@ -65,73 +65,34 @@ or if on windows PowerShell . venv/bin/activate.ps1 ``` -4. install dependencies: +4. Install tox: ```bash -pip install -r requirements_dev.txt -r requirements.txt -# if installing optional dependencies -pip install -r requirements-GUI.txt -r requirements-CBR.txt +pip install tox ``` -5. install ComicTagger +5. If you are on an M1 Mac you will need to export two environment variables for tests to pass. ``` -pip install . +export tox_python=python3.9-intel64 +export tox_env=m1env ``` -6. (optionally) run pytest to ensure that their are no failures (xfailed means expected failure) +6. install ComicTagger ``` -$ pytest -============================= test session starts ============================== -platform darwin -- Python 3.9.12, pytest-7.1.1, pluggy-1.0.0 -rootdir: /Users/timmy/build/source/comictagger -collected 61 items - -tests/test_FilenameParser.py ..x......x.xxx.xx....xxxxxx.xx.x..xxxxxxx [ 67%] -tests/test_comicarchive.py x... [ 73%] -tests/test_rename.py ..xxx.xx..XXX.XX [100%] - -================== 27 passed, 29 xfailed, 5 xpassed in 2.68s =================== +tox run -e venv ``` 7. Make your changes -8. run code tools and correct any issues +8. Build to ensure that your changes work: this will produce a binary build in the dist folder ```bash -black . -isort . -flake8 . -pytest +tox run -m build ``` -black: formats all of the code consistently so there are no surprises
+The build runs these formatters and linters automatically + +setup-cfg-fmt: Formats the setup.cfg file +autoflake: Removes unused imports isort: sorts imports so that you can always find where an import is located
+black: formats all of the code consistently so there are no surprises
flake8: checks for code quality and style (warns for unused imports and similar issues)
+mypy: checks the types of variables and functions to catch errors pytest: runs tests for ComicTagger functionality - - -if on mac or linux most of this can be accomplished by running -``` -make install -# or make PYTHON=python3-intel64 install -. venv/bin/activate -make CI -``` -There is also `make check` which will run all of the code tools in a read-only capacity -``` -$ make check -venv/bin/black --check . -All done! ✨ 🍰 ✨ -52 files would be left unchanged. -venv/bin/isort --check . -Skipped 6 files -venv/bin/flake8 . -venv/bin/pytest -============================= test session starts ============================== -platform darwin -- Python 3.9.12, pytest-7.1.1, pluggy-1.0.0 -rootdir: /Users/timmy/build/source/comictagger -collected 61 items - -tests/test_FilenameParser.py ..x......x.xxx.xx....xxxxxx.xx.x..xxxxxxx [ 67%] -tests/test_comicarchive.py x... [ 73%] -tests/test_rename.py ..xxx.xx..XXX.XX [100%] - -================== 27 passed, 29 xfailed, 5 xpassed in 2.68s =================== -``` diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e341d47..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include README.md -include release_notes.txt -include requirements.txt -recursive-include scripts *.py *.txt -recursive-include desktop-integration * -include windows/app.ico -include mac/app.icns diff --git a/Makefile b/Makefile deleted file mode 100644 index 8fe3924..0000000 --- a/Makefile +++ /dev/null @@ -1,64 +0,0 @@ -PIP ?= pip3 -PYTHON ?= python3 -VERSION_STR := $(shell $(PYTHON) setup.py --version) - -SITE_PACKAGES := $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') -PACKAGE_PATH = $(SITE_PACKAGES)/comictagger.egg-link - -VENV := $(shell echo $${VIRTUAL_ENV-venv}) -PY3 := $(shell command -v $(PYTHON) 2> /dev/null) -PYTHON_VENV := $(VENV)/bin/python -INSTALL_STAMP := $(VENV)/.install.stamp - - -ifeq ($(OS),Windows_NT) - PYTHON_VENV := $(VENV)/Scripts/python.exe - OS_VERSION=win-$(PROCESSOR_ARCHITECTURE) - APP_NAME=comictagger.exe - FINAL_NAME=ComicTagger-$(VERSION_STR)-$(OS_VERSION).exe -else ifeq ($(shell uname -s),Darwin) - OS_VERSION=osx-$(shell defaults read loginwindow SystemVersionStampAsString)-$(shell uname -m) - APP_NAME=ComicTagger.app - FINAL_NAME=ComicTagger-$(VERSION_STR)-$(OS_VERSION).app -else - APP_NAME=comictagger - FINAL_NAME=ComicTagger-$(VERSION_STR)-$(shell uname -s) -endif - -.PHONY: all clean pydist dist CI check - -all: clean dist - -$(PYTHON_VENV): - @if [ -z $(PY3) ]; then echo "Python 3 could not be found."; exit 2; fi - $(PY3) -m venv $(VENV) - -clean: - find . -maxdepth 4 -type d -name "__pycache__" -print -depth -exec rm -rf {} \; - rm -rf $(PACKAGE_PATH) $(INSTALL_STAMP) build dist MANIFEST comictaggerlib/ctversion.py - $(MAKE) -C mac clean - -CI: install - $(PYTHON_VENV) -m black . - $(PYTHON_VENV) -m isort . - $(PYTHON_VENV) -m flake8 . - $(PYTHON_VENV) -m pytest - -check: install - $(PYTHON_VENV) -m black --check . - $(PYTHON_VENV) -m isort --check . - $(PYTHON_VENV) -m flake8 . - $(PYTHON_VENV) -m pytest - -pydist: - $(PYTHON_VENV) -m build - -install: $(INSTALL_STAMP) -$(INSTALL_STAMP): $(PYTHON_VENV) requirements.txt requirements_dev.txt - $(PYTHON_VENV) -m pip install -r requirements_dev.txt - $(PYTHON_VENV) -m pip install -e . - touch $(INSTALL_STAMP) - -dist: - pyinstaller -y comictagger.spec - cd dist && zip -m -r $(FINAL_NAME).zip $(APP_NAME) diff --git a/README.md b/README.md index a2b4454..477dc6b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ For details, screen-shots, and more, visit [the Wiki](https://github.com/comicta ### Binaries -Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). +Windows, Linux and MacOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). Just unzip the archive in any folder and run, no additional installation steps are required. @@ -47,7 +47,14 @@ A pip package is provided, you can install it with: $ pip3 install comictagger[GUI] ``` -There are two optional dependencies GUI and CBR. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI]` +There are optional dependencies. You can install the optional dependencies by specifying one or more of them in braces e.g. `comictagger[CBR,GUI]` + +Optional dependencies: +1. `ICU`: Ensures that comic pages are supported correctly. This should always be installed. *Currently only exists in the latest alpha release * +1. `CBR`: Provides support for CBR/RAR files. +1. `GUI`: Installs the GUI. +1. `7Z`: Provides support for CB7/7Z files. +1. `all`: Installs all of the above optional dependencies. ### Chocolatey installation (Windows only) @@ -59,5 +66,113 @@ choco install comictagger 1. Ensure you have python 3.9 installed 2. Clone this repository `git clone https://github.com/comictagger/comictagger.git` - 3. `pip3 install -r requirements_dev.txt` - 7. `pip3 install .` or `pip3 install .[GUI]` + 7. `pip3 install .[ICU]` or `pip3 install .[GUI,ICU]` + + +## Contributors + + + + + + + + + + + + + + + + + + + + +
+ + beville +
+ beville +
+
+ + davide-romanini +
+ davide-romanini +
+
+ + fcanc +
+ fcanc +
+
+ + lordwelch +
+ lordwelch +
+
+ + mizaki +
+ mizaki +
+
+ + MichaelFitzurka +
+ MichaelFitzurka +
+
+ + abuchanan920 +
+ abuchanan920 +
+
+ + AlbanSeurat +
+ AlbanSeurat +
+
+ + rhaussmann +
+ rhaussmann +
+
+ + jpcranford +
+ jpcranford +
+
+ + PawlakMarek +
+ PawlakMarek +
+
+ + Xav83 +
+ Xav83 +
+
+ + thFrgttn +
+ thFrgttn +
+
+ + tlc +
+ tlc +
+
+ diff --git a/desktop-integration/linux/ComicTagger.desktop b/build-tools/ComicTagger.desktop similarity index 92% rename from desktop-integration/linux/ComicTagger.desktop rename to build-tools/ComicTagger.desktop index 1300d91..b34ba7a 100644 --- a/desktop-integration/linux/ComicTagger.desktop +++ b/build-tools/ComicTagger.desktop @@ -3,7 +3,7 @@ Encoding=UTF-8 Name=ComicTagger GenericName=Comic Metadata Editor Comment=A cross-platform GUI/CLI app for writing metadata to comic archives -Exec=%%CTSCRIPT%% %F +Exec=comictagger %F Icon=/usr/local/share/comictagger/app.png Terminal=false Type=Application diff --git a/comictagger.spec b/build-tools/comictagger.spec similarity index 98% rename from comictagger.spec rename to build-tools/comictagger.spec index 413c6e4..8f1f040 100644 --- a/comictagger.spec +++ b/build-tools/comictagger.spec @@ -9,7 +9,7 @@ block_cipher = None a = Analysis( - ["comictagger.py"], + ["../comictaggerlib/__main__.py"], pathex=[], binaries=[], datas=[], @@ -237,5 +237,5 @@ if platform.system() not in ["Windows"]: }, ], }, - bundle_identifier=None, + bundle_identifier="com.comictagger", ) diff --git a/build-tools/get_appimage.py b/build-tools/get_appimage.py new file mode 100644 index 0000000..82b5d0f --- /dev/null +++ b/build-tools/get_appimage.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import argparse +import os +import pathlib +import stat + +import requests + +parser = argparse.ArgumentParser() +parser.add_argument("APPIMAGETOOL", default="build/appimagetool-x86_64.AppImage", type=pathlib.Path, nargs="?") + +opts = parser.parse_args() +opts.APPIMAGETOOL = opts.APPIMAGETOOL.absolute() + + +def urlretrieve(url: str, dest: pathlib.Path) -> None: + resp = requests.get(url) + if resp.status_code == 200: + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(resp.content) + + +if opts.APPIMAGETOOL.exists(): + raise SystemExit(0) + +urlretrieve( + "https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage", opts.APPIMAGETOOL +) +os.chmod(opts.APPIMAGETOOL, stat.S_IRWXU) + +if not opts.APPIMAGETOOL.exists(): + raise SystemExit(1) diff --git a/mac/Makefile b/build-tools/mac/Makefile similarity index 100% rename from mac/Makefile rename to build-tools/mac/Makefile diff --git a/mac/app.icns b/build-tools/mac/app.icns similarity index 100% rename from mac/app.icns rename to build-tools/mac/app.icns diff --git a/mac/make_thin.sh b/build-tools/mac/make_thin.sh similarity index 100% rename from mac/make_thin.sh rename to build-tools/mac/make_thin.sh diff --git a/mac/volume.icns b/build-tools/mac/volume.icns similarity index 100% rename from mac/volume.icns rename to build-tools/mac/volume.icns diff --git a/windows/app.ico b/build-tools/windows/app.ico similarity index 100% rename from windows/app.ico rename to build-tools/windows/app.ico diff --git a/build-tools/zip_artifacts.py b/build-tools/zip_artifacts.py new file mode 100644 index 0000000..300b069 --- /dev/null +++ b/build-tools/zip_artifacts.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import os +import pathlib +import platform +import zipfile + +from comictaggerlib.ctversion import __version__ + +app = "ComicTagger" +exe = app.casefold() +if platform.system() == "Windows": + os_version = f"win-{platform.machine()}" + app_name = f"{exe}.exe" + final_name = f"{app}-{__version__}-{os_version}.exe" +elif platform.system() == "Darwin": + ver = platform.mac_ver() + os_version = f"osx-{ver[0]}-{ver[2]}" + app_name = f"{app}.app" + final_name = f"{app}-{__version__}-{os_version}.app" +else: + app_name = exe + final_name = f"ComicTagger-{__version__}-{platform.system()}" + +path = f"dist/{app_name}" +zip_file = pathlib.Path(f"dist/{final_name}.zip") + + +def addToZip(zf, path, zippath): + if os.path.isfile(path): + zf.write(path, zippath) + elif os.path.isdir(path): + if zippath: + zf.write(path, zippath) + for nm in sorted(os.listdir(path)): + addToZip(zf, os.path.join(path, nm), os.path.join(zippath, nm)) + # else: ignore + + +zip_file.unlink(missing_ok=True) +with zipfile.ZipFile(zip_file, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=8) as zf: + zippath = os.path.basename(path) + if not zippath: + zippath = os.path.basename(os.path.dirname(path)) + if zippath in ("", os.curdir, os.pardir): + zippath = "" + addToZip(zf, path, zippath) diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py index efe58e5..51d1a68 100644 --- a/comicapi/archivers/archiver.py +++ b/comicapi/archivers/archiver.py @@ -25,7 +25,7 @@ class Archiver(Protocol): """ enabled: bool = True - def __init__(self): + def __init__(self) -> None: self.path = pathlib.Path() def get_comment(self) -> str: diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py index 726404a..1dc2df8 100644 --- a/comicapi/archivers/rar.py +++ b/comicapi/archivers/rar.py @@ -12,7 +12,7 @@ import time from comicapi.archivers import Archiver try: - from unrar.cffi import rarfile + import rarfile rar_support = True except ImportError: @@ -22,7 +22,7 @@ except ImportError: logger = logging.getLogger(__name__) if not rar_support: - logger.error("unrar-cffi unavailable") + logger.error("rar unavailable") class RarArchiver(Archiver): @@ -43,7 +43,7 @@ class RarArchiver(Archiver): def get_comment(self) -> str: rarc = self.get_rar_obj() - return rarc.comment.decode("utf-8") if rarc else "" + return (rarc.comment if rarc else "") or "" def set_comment(self, comment: str) -> bool: if rar_support and self.exe: diff --git a/comicapi/comet.py b/comicapi/comet.py index a3de3b2..02e52d0 100644 --- a/comicapi/comet.py +++ b/comicapi/comet.py @@ -1,6 +1,6 @@ """A class to encapsulate CoMet data""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -141,14 +141,14 @@ class CoMet: md.series = utils.xlate(get("series")) md.title = utils.xlate(get("title")) md.issue = utils.xlate(get("issue")) - md.volume = utils.xlate(get("volume"), True) + md.volume = utils.xlate_int(get("volume")) md.comments = utils.xlate(get("description")) md.publisher = utils.xlate(get("publisher")) md.language = utils.xlate(get("language")) md.format = utils.xlate(get("format")) - md.page_count = utils.xlate(get("pages"), True) + md.page_count = utils.xlate_int(get("pages")) md.maturity_rating = utils.xlate(get("rating")) - md.price = utils.xlate(get("price"), is_float=True) + md.price = utils.xlate_float(get("price")) md.is_version_of = utils.xlate(get("isVersionOf")) md.rights = utils.xlate(get("rights")) md.identifier = utils.xlate(get("identifier")) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index e84abd1..ca089a2 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -1,5 +1,5 @@ """A class to represent a single comic, be it file or folder of images""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import shutil import sys from typing import cast -import natsort import wordninja from comicapi import filenamelexer, filenameparser, utils @@ -280,7 +279,7 @@ class ComicArchive: # seems like some archive creators are on Windows, and don't know about case-sensitivity! if sort_list: - files = cast(list[str], natsort.os_sorted(files)) + files = cast(list[str], utils.os_sorted(files)) # make a sub-list of image files self.page_list = [] @@ -562,13 +561,13 @@ class ComicArchive: ) metadata.alternate_number = utils.xlate(p.filename_info["alternate"]) metadata.issue = utils.xlate(p.filename_info["issue"]) - metadata.issue_count = utils.xlate(p.filename_info["issue_count"]) + metadata.issue_count = utils.xlate_int(p.filename_info["issue_count"]) metadata.publisher = utils.xlate(p.filename_info["publisher"]) metadata.series = utils.xlate(p.filename_info["series"]) metadata.title = utils.xlate(p.filename_info["title"]) - metadata.volume = utils.xlate(p.filename_info["volume"]) - metadata.volume_count = utils.xlate(p.filename_info["volume_count"]) - metadata.year = utils.xlate(p.filename_info["year"]) + metadata.volume = utils.xlate_int(p.filename_info["volume"]) + metadata.volume_count = utils.xlate_int(p.filename_info["volume_count"]) + metadata.year = utils.xlate_int(p.filename_info["year"]) metadata.scan_info = utils.xlate(p.filename_info["remainder"]) metadata.format = "FCBD" if p.filename_info["fcbd"] else None @@ -583,11 +582,11 @@ class ComicArchive: if fnp.series: metadata.series = fnp.series if fnp.volume: - metadata.volume = utils.xlate(fnp.volume, True) + metadata.volume = utils.xlate_int(fnp.volume) if fnp.year: - metadata.year = utils.xlate(fnp.year, True) + metadata.year = utils.xlate_int(fnp.year) if fnp.issue_count: - metadata.issue_count = utils.xlate(fnp.issue_count, True) + metadata.issue_count = utils.xlate_int(fnp.issue_count) if fnp.remainder: metadata.scan_info = fnp.remainder diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py index 1ceff44..cad1bb8 100644 --- a/comicapi/comicbookinfo.py +++ b/comicapi/comicbookinfo.py @@ -1,5 +1,5 @@ """A class to encapsulate the ComicBookInfo data""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -85,16 +85,16 @@ class ComicBookInfo: metadata.title = utils.xlate(cbi["title"]) metadata.issue = utils.xlate(cbi["issue"]) metadata.publisher = utils.xlate(cbi["publisher"]) - metadata.month = utils.xlate(cbi["publicationMonth"], True) - metadata.year = utils.xlate(cbi["publicationYear"], True) - metadata.issue_count = utils.xlate(cbi["numberOfIssues"], True) + metadata.month = utils.xlate_int(cbi["publicationMonth"]) + metadata.year = utils.xlate_int(cbi["publicationYear"]) + metadata.issue_count = utils.xlate_int(cbi["numberOfIssues"]) metadata.comments = utils.xlate(cbi["comments"]) metadata.genre = utils.xlate(cbi["genre"]) - metadata.volume = utils.xlate(cbi["volume"], True) - metadata.volume_count = utils.xlate(cbi["numberOfVolumes"], True) + metadata.volume = utils.xlate_int(cbi["volume"]) + metadata.volume_count = utils.xlate_int(cbi["numberOfVolumes"]) metadata.language = utils.xlate(cbi["language"]) metadata.country = utils.xlate(cbi["country"]) - metadata.critical_rating = utils.xlate(cbi["rating"], True) + metadata.critical_rating = utils.xlate_int(cbi["rating"]) metadata.credits = [ Credits( @@ -152,16 +152,16 @@ class ComicBookInfo: assign("title", utils.xlate(metadata.title)) assign("issue", utils.xlate(metadata.issue)) assign("publisher", utils.xlate(metadata.publisher)) - assign("publicationMonth", utils.xlate(metadata.month, True)) - assign("publicationYear", utils.xlate(metadata.year, True)) - assign("numberOfIssues", utils.xlate(metadata.issue_count, True)) + assign("publicationMonth", utils.xlate_int(metadata.month)) + assign("publicationYear", utils.xlate_int(metadata.year)) + assign("numberOfIssues", utils.xlate_int(metadata.issue_count)) assign("comments", utils.xlate(metadata.comments)) assign("genre", utils.xlate(metadata.genre)) - assign("volume", utils.xlate(metadata.volume, True)) - assign("numberOfVolumes", utils.xlate(metadata.volume_count, True)) + assign("volume", utils.xlate_int(metadata.volume)) + assign("numberOfVolumes", utils.xlate_int(metadata.volume_count)) assign("language", utils.xlate(utils.get_language_from_iso(metadata.language))) assign("country", utils.xlate(metadata.country)) - assign("rating", utils.xlate(metadata.critical_rating, True)) + assign("rating", utils.xlate_int(metadata.critical_rating)) assign("credits", metadata.credits) assign("tags", list(metadata.tags)) diff --git a/comicapi/comicinfoxml.py b/comicapi/comicinfoxml.py index 8d97ded..124b6dc 100644 --- a/comicapi/comicinfoxml.py +++ b/comicapi/comicinfoxml.py @@ -1,5 +1,5 @@ """A class to encapsulate ComicRack's ComicInfo.xml data""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -190,16 +190,16 @@ class ComicInfoXml: md.series = utils.xlate(get("Series")) md.title = utils.xlate(get("Title")) md.issue = IssueString(utils.xlate(get("Number"))).as_string() - md.issue_count = utils.xlate(get("Count"), True) - md.volume = utils.xlate(get("Volume"), True) + md.issue_count = utils.xlate_int(get("Count")) + md.volume = utils.xlate_int(get("Volume")) md.alternate_series = utils.xlate(get("AlternateSeries")) md.alternate_number = IssueString(utils.xlate(get("AlternateNumber"))).as_string() - md.alternate_count = utils.xlate(get("AlternateCount"), True) + md.alternate_count = utils.xlate_int(get("AlternateCount")) md.comments = utils.xlate(get("Summary")) md.notes = utils.xlate(get("Notes")) - md.year = utils.xlate(get("Year"), True) - md.month = utils.xlate(get("Month"), True) - md.day = utils.xlate(get("Day"), True) + md.year = utils.xlate_int(get("Year")) + md.month = utils.xlate_int(get("Month")) + md.day = utils.xlate_int(get("Day")) md.publisher = utils.xlate(get("Publisher")) md.imprint = utils.xlate(get("Imprint")) md.genre = utils.xlate(get("Genre")) @@ -210,12 +210,12 @@ class ComicInfoXml: md.characters = utils.xlate(get("Characters")) md.teams = utils.xlate(get("Teams")) md.locations = utils.xlate(get("Locations")) - md.page_count = utils.xlate(get("PageCount"), True) + md.page_count = utils.xlate_int(get("PageCount")) md.scan_info = utils.xlate(get("ScanInformation")) md.story_arc = utils.xlate(get("StoryArc")) md.series_group = utils.xlate(get("SeriesGroup")) md.maturity_rating = utils.xlate(get("AgeRating")) - md.critical_rating = utils.xlate(get("CommunityRating"), is_float=True) + md.critical_rating = utils.xlate_float(get("CommunityRating")) tmp = utils.xlate(get("BlackAndWhite")) if tmp is not None and tmp.casefold() in ["yes", "true", "1"]: diff --git a/comicapi/filenameparser.py b/comicapi/filenameparser.py index 0b9f599..60d3285 100644 --- a/comicapi/filenameparser.py +++ b/comicapi/filenameparser.py @@ -2,7 +2,7 @@ This should probably be re-written, but, well, it mostly works! """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 377f334..e5a4e4b 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -5,7 +5,7 @@ tagging schemes and databases, such as ComicVine or GCD. This makes conversion possible, however lossy it might be """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -122,7 +122,7 @@ class GenericMetadata: pages: list[ImageMetadata] = dataclasses.field(default_factory=list) # Some CoMet-only items - price: str | None = None + price: float | None = None is_version_of: str | None = None rights: str | None = None identifier: str | None = None diff --git a/comicapi/issuestring.py b/comicapi/issuestring.py index 9150246..149e5b1 100644 --- a/comicapi/issuestring.py +++ b/comicapi/issuestring.py @@ -4,7 +4,7 @@ Class for handling the odd permutations of an 'issue number' that the comics industry throws at us. e.g.: "12", "12.1", "0", "-1", "5AU", "100-2" """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/utils.py b/comicapi/utils.py index 7c8a5c9..9cb579c 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -1,5 +1,5 @@ """Some generic utilities""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,27 +14,50 @@ # limitations under the License. from __future__ import annotations -import glob import json import logging import os import pathlib +import platform import unicodedata from collections import defaultdict -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from shutil import which # noqa: F401 from typing import Any +import natsort import pycountry import rapidfuzz.fuzz import comicapi.data +try: + import icu + + del icu + icu_available = True +except ImportError: + icu_available = False + logger = logging.getLogger(__name__) -class UtilsVars: - already_fixed_encoding = False +def _custom_key(tup): + lst = [] + for x in natsort.os_sort_keygen()(tup): + ret = x + if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "": + ret = ("a", *x[1:]) + + lst.append(ret) + return tuple(lst) + + +def os_sorted(lst: Iterable) -> Iterable: + key = _custom_key + if icu_available or platform.system() == "Windows": + key = natsort.os_sort_keygen() + return sorted(lst, key=key) def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) -> str: @@ -45,17 +68,17 @@ def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) return (untouched_notes + "\n" + (new_notes or "")).strip() -def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]: +def parse_date_str(date_str: str | None) -> tuple[int | None, int | None, int | None]: day = None month = None year = None if date_str: parts = date_str.split("-") - year = xlate(parts[0], True) + year = xlate_int(parts[0]) if len(parts) > 1: - month = xlate(parts[1], True) + month = xlate_int(parts[1]) if len(parts) > 2: - day = xlate(parts[2], True) + day = xlate_int(parts[2]) return day, month, year @@ -65,9 +88,11 @@ def get_recursive_filelist(pathlist: list[str]) -> list[str]: filelist: list[str] = [] for p in pathlist: if os.path.isdir(p): - filelist.extend(x for x in glob.glob(f"{p}{os.sep}/**", recursive=True) if not os.path.isdir(x)) - elif str(p) not in filelist: - filelist.append(str(p)) + for root, _, files in os.walk(p): + for f in files: + filelist.append(os.path.join(root, f)) + else: + filelist.append(p) return filelist @@ -82,23 +107,32 @@ def add_to_path(dirname: str) -> None: os.environ["PATH"] = os.pathsep.join(paths) -def xlate(data: Any, is_int: bool = False, is_float: bool = False) -> Any: +def xlate_int(data: Any) -> int | None: + data = xlate_float(data) + if data is None: + return None + return int(data) + + +def xlate_float(data: Any) -> float | None: + if data is None or data == "": + return None + i: str | int | float + if isinstance(data, (int, float)): + i = data + else: + i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890."))) + if i == "": + return None + try: + return float(i) + except ValueError: + return None + + +def xlate(data: Any) -> str | None: if data is None or data == "": return None - if is_int or is_float: - i: str | int | float - if isinstance(data, (int, float)): - i = data - else: - i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890."))) - if i == "": - return None - try: - if is_float: - return float(i) - return int(float(i)) - except ValueError: - return None return str(data) diff --git a/comictagger.py b/comictagger.py deleted file mode 100755 index 17c515d..0000000 --- a/comictagger.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import localefix -from comictaggerlib.main import App - -if __name__ == "__main__": - localefix.configure_locale() - - App().run() diff --git a/comictaggerlib/__main__.py b/comictaggerlib/__main__.py new file mode 100644 index 0000000..9b7d9ab --- /dev/null +++ b/comictaggerlib/__main__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from comictaggerlib.main import main + +main() diff --git a/comictaggerlib/__pyinstaller/hook-comictaggerlib.py b/comictaggerlib/__pyinstaller/hook-comictaggerlib.py index 4143e64..8b7b6c0 100644 --- a/comictaggerlib/__pyinstaller/hook-comictaggerlib.py +++ b/comictaggerlib/__pyinstaller/hook-comictaggerlib.py @@ -1,7 +1,7 @@ from __future__ import annotations -from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import collect_data_files, collect_entry_point -datas = [] +datas, hiddenimports = collect_entry_point("comictagger.talker") datas += collect_data_files("comictaggerlib.ui") datas += collect_data_files("comictaggerlib.graphics") diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 62301bc..2abd2c5 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select from automated issue matches""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/autotagprogresswindow.py b/comictaggerlib/autotagprogresswindow.py index f2486bb..5650f55 100644 --- a/comictaggerlib/autotagprogresswindow.py +++ b/comictaggerlib/autotagprogresswindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to show ID log and progress""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 7e7db5d..efb3c29 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to confirm and set config for auto-tag""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.autotag_ignore_leading_numbers_in_filename) self.cbxRemoveAfterSuccess.setChecked(self.config.autotag_remove_archive_after_successful_match) self.cbxWaitForRateLimit.setChecked(self.config.autotag_wait_and_retry_on_rate_limit) - self.cbxAutoImprint.setChecked(self.config.talker_auto_imprint) + self.cbxAutoImprint.setChecked(self.config.identifier_auto_imprint) nlmt_tip = """The Name Match Ratio Threshold: Auto-Identify is for eliminating automatic search matches that are too long compared to your series name search. The lower @@ -75,7 +75,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.remove_after_success = False self.wait_and_retry_on_rate_limit = False self.search_string = "" - self.name_length_match_tolerance = self.config.talker_series_match_search_thresh + self.name_length_match_tolerance = self.config.identifier_series_match_search_thresh self.split_words = self.cbxSplitWords.isChecked() def search_string_toggle(self) -> None: diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index 4be2171..a1aa714 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -1,6 +1,6 @@ """A class to manage modifying metadata specifically for CBL/CBI""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index e91d0fe..cfca3e0 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -1,7 +1,7 @@ #!/usr/bin/python """ComicTagger CLI functions""" # -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,15 +40,15 @@ logger = logging.getLogger(__name__) class CLI: - def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]): + def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]) -> None: self.config = config self.talkers = talkers self.batch_mode = False def current_talker(self) -> ComicTalker: - if self.config[0].talker_source in self.talkers: - return self.talkers[self.config[0].talker_source] - logger.error("Could not find the '%s' talker", self.config[0].talker_source) + if self.config.talker_source in self.talkers: + return self.talkers[self.config.talker_source] + logger.error("Could not find the '%s' talker", self.config.talker_source) raise SystemExit(2) def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata: @@ -114,7 +114,7 @@ class CLI: ca = match_set.ca md = self.create_local_metadata(ca) ct_md = self.actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"]) - if self.config.talker_clear_metadata_on_import: + if self.config.identifier_clear_metadata_on_import: md = ct_md else: notes = ( @@ -123,7 +123,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.talker_auto_imprint: + if self.config.identifier_auto_imprint: md.fix_publisher() self.actual_metadata_save(ca, md) @@ -427,7 +427,7 @@ class CLI: match_results.fetch_data_failures.append(str(ca.path.absolute())) return - if self.config.talker_clear_metadata_on_import: + if self.config.identifier_clear_metadata_on_import: md = ct_md else: notes = ( @@ -436,7 +436,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.talker_auto_imprint: + if self.config.identifier_auto_imprint: md.fix_publisher() # ok, done building our metadata. time to save @@ -458,18 +458,18 @@ class CLI: return new_ext = "" # default - if self.config.filename_rename_set_extension_based_on_archive: + if self.config.rename_set_extension_based_on_archive: new_ext = ca.extension() renamer = FileRenamer( md, - platform="universal" if self.config.filename_rename_strict else "auto", + platform="universal" if self.config.rename_strict else "auto", replacements=self.config.rename_replacements, ) - renamer.set_template(self.config.filename_rename_template) - renamer.set_issue_zero_padding(self.config.filename_rename_issue_number_padding) - renamer.set_smart_cleanup(self.config.filename_rename_use_smart_string_cleanup) - renamer.move = self.config.filename_rename_move_to_dir + renamer.set_template(self.config.rename_template) + renamer.set_issue_zero_padding(self.config.rename_issue_number_padding) + renamer.set_smart_cleanup(self.config.rename_use_smart_string_cleanup) + renamer.move = self.config.rename_move_to_dir try: new_name = renamer.determine_name(ext=new_ext) @@ -481,17 +481,13 @@ class CLI: "Please consult the template help in the settings " "and the documentation on the format at " "https://docs.python.org/3/library/string.html#format-string-syntax", - self.config.filename_rename_template, + self.config.rename_template, ) return except Exception: - logger.exception( - "Formatter failure: %s metadata: %s", self.config.filename_rename_template, renamer.metadata - ) + logger.exception("Formatter failure: %s metadata: %s", self.config.rename_template, renamer.metadata) - folder = get_rename_dir( - ca, self.config.filename_rename_dir if self.config.filename_rename_move_to_dir else None - ) + folder = get_rename_dir(ca, self.config.rename_dir if self.config.rename_move_to_dir else None) full_path = folder / new_name diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index 07ab9da..c167ca5 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -4,7 +4,7 @@ Display cover images from either a local archive, or from Comic Vine. TODO: This should be re-factored using subclasses! """ # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ class CoverImageWidget(QtWidgets.QWidget): URLMode = 1 DataMode = 3 - image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray) + image_fetch_complete = QtCore.pyqtSignal(str, QtCore.QByteArray) def __init__( self, @@ -201,7 +201,7 @@ class CoverImageWidget(QtWidgets.QWidget): elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]: self.load_url() elif self.mode == CoverImageWidget.DataMode: - self.cover_remote_fetch_complete(self.imageData) + self.cover_remote_fetch_complete("", self.imageData) else: self.load_page() @@ -238,7 +238,9 @@ class CoverImageWidget(QtWidgets.QWidget): self.cover_fetcher.fetch(self.url_list[self.imageIndex]) # called when the image is done loading from internet - def cover_remote_fetch_complete(self, image_data: bytes) -> None: + def cover_remote_fetch_complete(self, url: str, image_data: bytes) -> None: + if url and url not in self.url_list: + return img = get_qimage_from_data(image_data) self.current_pixmap = QtGui.QPixmap.fromImage(img) self.set_display_pixmap() diff --git a/comictaggerlib/crediteditorwindow.py b/comictaggerlib/crediteditorwindow.py index 678e73e..f94c294 100644 --- a/comictaggerlib/crediteditorwindow.py +++ b/comictaggerlib/crediteditorwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to edit credits""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index f6109d8..e9e8376 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -1,6 +1,6 @@ """CLI settings for ComicTagger""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -236,7 +236,7 @@ def register_commands(parser: settngs.Manager) -> None: def register_commandline_settings(parser: settngs.Manager) -> None: parser.add_group("commands", register_commands, True) - parser.add_group("runtime", register_settings) + parser.add_persistent_group("runtime", register_settings) def validate_commandline_settings( diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 3d780f5..7fd8140 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -46,6 +46,43 @@ def identifier(parser: settngs.Manager) -> None: action=AppendAction, help="When enabled filters the listed publishers from all search results", ) + parser.add_setting("--series-match-search-thresh", default=90, type=int) + parser.add_setting( + "--clear-metadata", + default=True, + help="Clears all existing metadata during import, default is to merges metadata.\nMay be used in conjunction with -o, -f and -m.\n\n", + dest="clear_metadata_on_import", + action=argparse.BooleanOptionalAction, + ) + parser.add_setting( + "-a", + "--auto-imprint", + action=argparse.BooleanOptionalAction, + default=False, + help="Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n", + ) + + parser.add_setting( + "--sort-series-by-year", default=True, action=argparse.BooleanOptionalAction, help="Sorts series by year" + ) + parser.add_setting( + "--exact-series-matches-first", + default=True, + action=argparse.BooleanOptionalAction, + help="Puts series that are an exact match at the top of the list", + ) + parser.add_setting( + "--always-use-publisher-filter", + default=False, + action=argparse.BooleanOptionalAction, + help="Enables the publisher filter", + ) + parser.add_setting( + "--clear-form-before-populating", + default=False, + action=argparse.BooleanOptionalAction, + help="Clears all existing metadata when applying metadata from comic source", + ) def dialog(parser: settngs.Manager) -> None: @@ -86,43 +123,6 @@ def filename(parser: settngs.Manager) -> None: def talker(parser: settngs.Manager) -> None: # General settings for talkers parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID") - parser.add_setting("--series-match-search-thresh", default=90, type=int) - parser.add_setting( - "--clear-metadata", - default=True, - help="Clears all existing metadata during import, default is to merges metadata.\nMay be used in conjunction with -o, -f and -m.\n\n", - dest="clear_metadata_on_import", - action=argparse.BooleanOptionalAction, - ) - parser.add_setting( - "-a", - "--auto-imprint", - action=argparse.BooleanOptionalAction, - default=False, - help="Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n", - ) - - parser.add_setting( - "--sort-series-by-year", default=True, action=argparse.BooleanOptionalAction, help="Sorts series by year" - ) - parser.add_setting( - "--exact-series-matches-first", - default=True, - action=argparse.BooleanOptionalAction, - help="Puts series that are an exact match at the top of the list", - ) - parser.add_setting( - "--always-use-publisher-filter", - default=False, - action=argparse.BooleanOptionalAction, - help="Enables the publisher filter", - ) - parser.add_setting( - "--clear-form-before-populating", - default=False, - action=argparse.BooleanOptionalAction, - help="Clears all existing metadata when applying metadata from comic source", - ) def cbl(parser: settngs.Manager) -> None: diff --git a/comictaggerlib/ctsettings/plugin.py b/comictaggerlib/ctsettings/plugin.py index 84fbb90..6debffa 100644 --- a/comictaggerlib/ctsettings/plugin.py +++ b/comictaggerlib/ctsettings/plugin.py @@ -12,23 +12,39 @@ logger = logging.getLogger("comictagger") def archiver(manager: settngs.Manager) -> None: - exe_registered: set[str] = set() for archiver in comicapi.comicarchive.archivers: - if archiver.exe and archiver.exe not in exe_registered: + if archiver.exe: + # add_setting will overwrite anything with the same name. + # So we only end up with one option even if multiple archivers use the same exe. manager.add_setting( - f"--{archiver.exe.replace(' ', '-').replace('_', '-').strip().strip('-')}", + f"--{settngs.sanitize_name(archiver.exe)}", default=archiver.exe, help="Path to the %(default)s executable\n\n", ) - exe_registered.add(archiver.exe) def register_talker_settings(manager: settngs.Manager) -> None: - for talker_name, talker in comictaggerlib.ctsettings.talkers.items(): + for talker_id, talker in comictaggerlib.ctsettings.talkers.items(): + + def api_options(manager: settngs.Manager) -> None: + manager.add_setting( + f"--{talker_id}-key", + default="", + display_name="API Key", + help=f"API Key for {talker.name} (default: {talker.default_api_key})", + ) + manager.add_setting( + f"--{talker_id}-url", + default="", + display_name="URL", + help=f"URL for {talker.name} (default: {talker.default_api_url})", + ) + try: - manager.add_persistent_group("talker_" + talker_name, talker.register_settings, False) + manager.add_persistent_group("talker_" + talker_id, api_options, False) + manager.add_persistent_group("talker_" + talker_id, talker.register_settings, False) except Exception: - logger.exception("Failed to register settings for %s", talker_name) + logger.exception("Failed to register settings for %s", talker_id) def validate_archive_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: @@ -37,11 +53,7 @@ def validate_archive_settings(config: settngs.Config[settngs.Namespace]) -> sett cfg = settngs.normalize_config(config, file=True, cmdline=True, defaults=False) for archiver in comicapi.comicarchive.archivers: exe_name = settngs.sanitize_name(archiver.exe) - if ( - exe_name in cfg[0]["archiver"] - and cfg[0]["archiver"][exe_name] - and cfg[0]["archiver"][exe_name] != archiver.exe - ): + if exe_name in cfg[0]["archiver"] and cfg[0]["archiver"][exe_name]: if os.path.basename(cfg[0]["archiver"][exe_name]) == archiver.exe: comicapi.utils.add_to_path(os.path.dirname(cfg[0]["archiver"][exe_name])) else: @@ -53,15 +65,15 @@ def validate_archive_settings(config: settngs.Config[settngs.Namespace]) -> sett def validate_talker_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: # Apply talker settings from config file cfg = settngs.normalize_config(config, True, True) - for talker_name, talker in list(comictaggerlib.ctsettings.talkers.items()): + for talker_id, talker in list(comictaggerlib.ctsettings.talkers.items()): try: - talker.parse_settings(cfg[0]["talker_" + talker_name]) + cfg[0]["talker_" + talker_id] = talker.parse_settings(cfg[0]["talker_" + talker_id]) except Exception as e: # Remove talker as we failed to apply the settings - del comictaggerlib.ctsettings.talkers[talker_name] + del comictaggerlib.ctsettings.talkers[talker_id] logger.exception("Failed to initialize talker settings: %s", e) - return config + return settngs.get_namespace(cfg) def validate_plugin_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: diff --git a/comictaggerlib/exportwindow.py b/comictaggerlib/exportwindow.py index a3f8f73..595e443 100644 --- a/comictaggerlib/exportwindow.py +++ b/comictaggerlib/exportwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to confirm and set options for export to zip""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py index 103fb6d..8aca69e 100644 --- a/comictaggerlib/filerenamer.py +++ b/comictaggerlib/filerenamer.py @@ -1,6 +1,6 @@ """Functions for renaming files based on metadata""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import logging import os import pathlib import string +from collections.abc import Mapping, Sequence from typing import Any, cast from pathvalidate import Platform, normalize_platform, sanitize_filename @@ -94,8 +95,8 @@ class MetadataFormatter(string.Formatter): def _vformat( self, format_string: str, - args: list[Any], - kwargs: dict[str, Any], + args: Sequence[Any], + kwargs: Mapping[str, Any], used_args: set[Any], recursion_depth: int, auto_arg_index: int = 0, diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index c84b049..a8a6970 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -1,6 +1,6 @@ """A PyQt5 widget for managing list of comic archive files""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 6f41e88..541c647 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -73,12 +73,12 @@ try: return True return super().event(event) -except ImportError as e: +except ImportError: def show_exception_box(log_msg: str) -> None: ... - logger.error(str(e)) + logger.exception("Qt unavailable") qt_available = False diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py index 01163a1..05b1afe 100644 --- a/comictaggerlib/imagefetcher.py +++ b/comictaggerlib/imagefetcher.py @@ -1,6 +1,6 @@ """A class to manage fetching and caching of images by URL""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class ImageFetcherException(Exception): ... -def fetch_complete(image_data: bytes | QtCore.QByteArray) -> None: +def fetch_complete(url: str, image_data: bytes | QtCore.QByteArray) -> None: ... @@ -79,22 +79,22 @@ class ImageFetcher: # first look in the DB image_data = self.get_image_from_cache(url) # Async for retrieving covers seems to work well - if blocking: # if blocking or not qt_available: + if blocking or not qt_available: if not image_data: try: image_data = requests.get(url, headers={"user-agent": "comictagger/" + ctversion.version}).content + # save the image to the cache + self.add_image_to_cache(self.fetched_url, image_data) except Exception as e: logger.exception("Fetching url failed: %s") raise ImageFetcherException("Network Error!") from e - - # save the image to the cache - self.add_image_to_cache(self.fetched_url, image_data) + ImageFetcher.image_fetch_complete(url, image_data) return image_data if qt_available: # if we found it, just emit the signal asap if image_data: - ImageFetcher.image_fetch_complete(QtCore.QByteArray(image_data)) + ImageFetcher.image_fetch_complete(url, QtCore.QByteArray(image_data)) return b"" # didn't find it. look online @@ -110,9 +110,9 @@ class ImageFetcher: image_data = reply.readAll() # save the image to the cache - self.add_image_to_cache(self.fetched_url, image_data) + self.add_image_to_cache(reply.request().url().toString(), image_data) - ImageFetcher.image_fetch_complete(image_data) + ImageFetcher.image_fetch_complete(reply.request().url().toString(), image_data) def create_image_db(self) -> None: # this will wipe out any existing version diff --git a/comictaggerlib/imagehasher.py b/comictaggerlib/imagehasher.py index f8662ff..f30270c 100644 --- a/comictaggerlib/imagehasher.py +++ b/comictaggerlib/imagehasher.py @@ -1,6 +1,6 @@ """A class to manage creating image content hashes, and calculate hamming distances""" # -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/imagepopup.py b/comictaggerlib/imagepopup.py index 1702535..445e4a2 100644 --- a/comictaggerlib/imagepopup.py +++ b/comictaggerlib/imagepopup.py @@ -1,6 +1,6 @@ """A PyQT4 widget to display a popup image""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 43451e4..7d0e3b0 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -1,6 +1,6 @@ """A class to automatically identify a comic archive""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index c5b47ab..f1b9e9c 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select specific issue from list""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -106,7 +106,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): # now that the list has been sorted, find the initial record, and # select it - if self.initial_id is None: + if not self.initial_id: self.twList.selectRow(0) else: for r in range(0, self.twList.rowCount()): diff --git a/comictaggerlib/logwindow.py b/comictaggerlib/logwindow.py index ee66466..8b3bda8 100644 --- a/comictaggerlib/logwindow.py +++ b/comictaggerlib/logwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to a text file or log""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 3e35a1d..ea8b070 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -1,6 +1,6 @@ """A python app to (automatically) tag comic archives""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,12 @@ from __future__ import annotations import argparse import json +import locale +import logging import logging.handlers +import os import signal +import subprocess import sys import settngs @@ -48,6 +52,49 @@ except Exception: logger.setLevel(logging.DEBUG) +def _lang_code_mac() -> str: + """ + stolen from https://github.com/mu-editor/mu + Returns the user's language preference as defined in the Language & Region + preference pane in macOS's System Preferences. + """ + + # Uses the shell command `defaults read -g AppleLocale` that prints out a + # language code to standard output. Assumptions about the command: + # - It exists and is in the shell's PATH. + # - It accepts those arguments. + # - It returns a usable language code. + # + # Reference documentation: + # - The man page for the `defaults` command on macOS. + # - The macOS underlying API: + # https://developer.apple.com/documentation/foundation/nsuserdefaults. + + lang_detect_command = "defaults read -g AppleLocale" + + status, output = subprocess.getstatusoutput(lang_detect_command) + if status == 0: + # Command was successful. + lang_code = output + else: + logging.warning("Language detection command failed: %r", output) + lang_code = "" + + return lang_code + + +def configure_locale() -> None: + if sys.platform == "darwin" and "LANG" not in os.environ: + code = _lang_code_mac() + if code != "": + os.environ["LANG"] = f"{code}.utf-8" + + locale.setlocale(locale.LC_ALL, "") + sys.stdout.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] + sys.stderr.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] + sys.stdin.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] + + def update_publishers(config: settngs.Config[settngs.Namespace]) -> None: json_file = config[0].runtime_config.user_config_dir / "publishers.json" if json_file.exists(): @@ -66,6 +113,7 @@ class App: self.config_load_success = False def run(self) -> None: + configure_locale() conf = self.initialize() self.initialize_dirs(conf.config) self.load_plugins(conf) @@ -119,6 +167,15 @@ class App: # config already loaded error = None + talkers = ctsettings.talkers + del ctsettings.talkers + + if len(talkers) < 1: + error = error = ( + f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details", + True, + ) + signal.signal(signal.SIGINT, signal.SIG_DFL) logger.debug("Installed Packages") @@ -134,7 +191,10 @@ class App: # manage the CV API key # None comparison is used so that the empty string can unset the value - if self.config[0].talker_comicvine_cv_api_key is not None or self.config[0].talker_comicvine_cv_url is not None: + if not error and ( + self.config[0].talker_comicvine_comicvine_key is not None + or self.config[0].talker_comicvine_comicvine_url is not None + ): settings_path = self.config[0].runtime_config.user_config_dir / "settings.json" if self.config_load_success: self.manager.save_file(self.config[0], settings_path) @@ -150,9 +210,6 @@ class App: True, ) - talkers = ctsettings.talkers - del ctsettings.talkers - if self.config[0].runtime_no_gui: if error and error[1]: print(f"A fatal error occurred please check the log for more information: {error[0]}") # noqa: T201 diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index 65432b8..6383eeb 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select from automated issue matches""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/optionalmsgdialog.py b/comictaggerlib/optionalmsgdialog.py index c71d704..df2e267 100644 --- a/comictaggerlib/optionalmsgdialog.py +++ b/comictaggerlib/optionalmsgdialog.py @@ -11,7 +11,7 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question", ) """ # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/pagebrowser.py b/comictaggerlib/pagebrowser.py index 114bee7..567c72e 100644 --- a/comictaggerlib/pagebrowser.py +++ b/comictaggerlib/pagebrowser.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to show pages of a comic archive""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 6ba1461..2f322cf 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -1,6 +1,6 @@ """A PyQt5 widget for editing the page list info""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/pageloader.py b/comictaggerlib/pageloader.py index 8049265..cd38213 100644 --- a/comictaggerlib/pageloader.py +++ b/comictaggerlib/pageloader.py @@ -1,6 +1,6 @@ """A PyQT4 class to load a page image from a ComicArchive in a background thread""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/progresswindow.py b/comictaggerlib/progresswindow.py index 1a2342f..3544157 100644 --- a/comictaggerlib/progresswindow.py +++ b/comictaggerlib/progresswindow.py @@ -1,6 +1,6 @@ """A PyQt5 dialog to show ID log and progress""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 9bed8df..9bc31a4 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to confirm rename""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ class RenameWindow(QtWidgets.QDialog): comic_archive_list: list[ComicArchive], data_style: int, config: settngs.Config[settngs.Namespace], - talker: ComicTalker, + talkers: dict[str, ComicTalker], ) -> None: super().__init__(parent) @@ -55,7 +55,7 @@ class RenameWindow(QtWidgets.QDialog): ) self.config = config - self.talker = talker + self.talkers = talkers self.comic_archive_list = comic_archive_list self.data_style = data_style self.rename_list: list[str] = [] @@ -73,7 +73,7 @@ class RenameWindow(QtWidgets.QDialog): self.renamer.replacements = self.config[0].rename_replacements new_ext = ca.path.suffix # default - if self.config[0].filename_rename_set_extension_based_on_archive: + if self.config[0].rename_set_extension_based_on_archive: new_ext = ca.extension() if md is None: @@ -160,7 +160,7 @@ class RenameWindow(QtWidgets.QDialog): self.twList.setSortingEnabled(True) def modify_settings(self) -> None: - settingswin = SettingsWindow(self, self.config, self.talker) + settingswin = SettingsWindow(self, self.config, self.talkers) settingswin.setModal(True) settingswin.show_rename_tab() settingswin.exec() diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 1707f0f..9a208bb 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select specific series/volume from list""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -103,7 +103,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): series_name: str, issue_number: str, year: int | None, - issue_count: int, + issue_count: int | None, cover_index_list: list[int], comic_archive: ComicArchive | None, config: settngs.Namespace, @@ -151,7 +151,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.progdialog: QtWidgets.QProgressDialog | None = None self.search_thread: SearchThread | None = None - self.use_filter = self.config.talker_always_use_publisher_filter + self.use_filter = self.config.identifier_always_use_publisher_filter # Load to retrieve settings self.talker = talker @@ -303,7 +303,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): if found_match is not None: self.iddialog.accept() - self.series_id = utils.xlate(found_match["series_id"]) + self.series_id = utils.xlate(found_match["series_id"]) or "" self.issue_number = found_match["issue_number"] self.select_by_id() self.show_issues() @@ -326,6 +326,8 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.issue_number = selector.issue_number self.issue_id = selector.issue_id self.accept() + else: + self.imageWidget.update_content() def select_by_id(self) -> None: for r in range(0, self.twList.rowCount()): @@ -336,7 +338,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): def perform_query(self, refresh: bool = False) -> None: self.search_thread = SearchThread( - self.talker, self.series_name, refresh, self.literal, self.config.talker_series_match_search_thresh + self.talker, self.series_name, refresh, self.literal, self.config.identifier_series_match_search_thresh ) self.search_thread.searchComplete.connect(self.search_complete) self.search_thread.progressUpdate.connect(self.search_progress_update) @@ -403,7 +405,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # compare as str in case extra chars ie. '1976?' # - missing (none) values being converted to 'None' - consistent with prior behaviour in v1.2.3 # sort by start_year if set - if self.config.talker_sort_series_by_year: + if self.config.identifier_sort_series_by_year: try: self.ct_search_results = sorted( self.ct_search_results, @@ -421,7 +423,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): logger.exception("bad data error sorting results by count_of_issues") # move sanitized matches to the front - if self.config.talker_exact_series_matches_first: + if self.config.identifier_exact_series_matches_first: try: sanitized = utils.sanitize_title(self.series_name, False).casefold() sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold() diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 68a046e..1b7b356 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to enter app settings""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,8 +25,10 @@ from typing import Any import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic +import comictaggerlib.ui.talkeruigenerator from comicapi import utils from comicapi.genericmetadata import md_test +from comictaggerlib import ctsettings from comictaggerlib.ctversion import version from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements from comictaggerlib.imagefetcher import ImageFetcher @@ -131,7 +133,7 @@ Spider-Geddon #1 - New Players; Check In class SettingsWindow(QtWidgets.QDialog): def __init__( - self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talker: ComicTalker + self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talkers: dict[str, ComicTalker] ) -> None: super().__init__(parent) @@ -142,7 +144,7 @@ class SettingsWindow(QtWidgets.QDialog): ) self.config = config - self.talker = talker + self.talkers = talkers self.name = "Settings" if platform.system() == "Windows": @@ -185,16 +187,21 @@ class SettingsWindow(QtWidgets.QDialog): self.leRenameTemplate.setToolTip(f"
{html.escape(template_tooltip)}
") self.rename_error: Exception | None = None + self.sources: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs( + self.tComicTalkers, self.config, self.talkers + ) self.connect_signals() self.settings_to_form() self.rename_test() self.dir_test() + # Set General as start tab + self.tabWidget.setCurrentIndex(0) + def connect_signals(self) -> None: self.btnBrowseRar.clicked.connect(self.select_rar) self.btnClearCache.clicked.connect(self.clear_cache) self.btnResetSettings.clicked.connect(self.reset_settings) - self.btnTestKey.clicked.connect(self.test_api_key) self.btnTemplateHelp.clicked.connect(self.show_template_help) self.cbxMoveFiles.clicked.connect(self.dir_test) self.leDirectory.textEdited.connect(self.dir_test) @@ -223,7 +230,6 @@ class SettingsWindow(QtWidgets.QDialog): self.btnRemoveValueReplacement.clicked.disconnect() self.btnResetSettings.clicked.disconnect() self.btnTemplateHelp.clicked.disconnect() - self.btnTestKey.clicked.disconnect() self.cbxChangeExtension.clicked.disconnect() self.cbxComplicatedParser.clicked.disconnect() self.cbxMoveFiles.clicked.disconnect() @@ -301,7 +307,7 @@ class SettingsWindow(QtWidgets.QDialog): else: self.leRarExePath.setEnabled(False) self.sbNameMatchIdentifyThresh.setValue(self.config[0].identifier_series_match_identify_thresh) - self.sbNameMatchSearchThresh.setValue(self.config[0].talker_series_match_search_thresh) + self.sbNameMatchSearchThresh.setValue(self.config[0].identifier_series_match_search_thresh) self.tePublisherFilter.setPlainText("\n".join(self.config[0].identifier_publisher_filter)) self.cbxCheckForNewVersion.setChecked(self.config[0].general_check_for_new_version) @@ -312,16 +318,10 @@ class SettingsWindow(QtWidgets.QDialog): self.cbxRemovePublisher.setChecked(self.config[0].filename_remove_publisher) self.switch_parser() - self.cbxUseSeriesStartAsVolume.setChecked(self.config[0].talker_comicvine_cv_use_series_start_as_volume) - self.cbxClearFormBeforePopulating.setChecked(self.config[0].talker_clear_form_before_populating) - self.cbxRemoveHtmlTables.setChecked(self.config[0].talker_comicvine_cv_remove_html_tables) - - self.cbxUseFilter.setChecked(self.config[0].talker_always_use_publisher_filter) - self.cbxSortByYear.setChecked(self.config[0].talker_sort_series_by_year) - self.cbxExactMatches.setChecked(self.config[0].talker_exact_series_matches_first) - - self.leKey.setText(self.config[0].talker_comicvine_cv_api_key) - self.leURL.setText(self.config[0].talker_comicvine_cv_url) + self.cbxClearFormBeforePopulating.setChecked(self.config[0].identifier_clear_form_before_populating) + self.cbxUseFilter.setChecked(self.config[0].identifier_always_use_publisher_filter) + self.cbxSortByYear.setChecked(self.config[0].identifier_sort_series_by_year) + self.cbxExactMatches.setChecked(self.config[0].identifier_exact_series_matches_first) self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary) self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags) @@ -349,6 +349,10 @@ class SettingsWindow(QtWidgets.QDialog): table.removeRow(i) for row, replacement in enumerate(replacments): self.insertRow(table, row, replacement) + + # Set talker values + comictaggerlib.ui.talkeruigenerator.settings_to_talker_form(self.sources, self.config) + self.connect_signals() def get_replacements(self) -> Replacements: @@ -418,7 +422,7 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].general_check_for_new_version = self.cbxCheckForNewVersion.isChecked() self.config[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value() - self.config[0].talker_series_match_search_thresh = self.sbNameMatchSearchThresh.value() + self.config[0].identifier_series_match_search_thresh = self.sbNameMatchSearchThresh.value() self.config[0].identifier_publisher_filter = [ x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip() ] @@ -428,21 +432,10 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked() self.config[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked() - self.config[0].talker_comicvine_cv_use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked() - self.config[0].talker_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() - self.config[0].talker_comicvine_cv_remove_html_tables = self.cbxRemoveHtmlTables.isChecked() - - self.config[0].talker_always_use_publisher_filter = self.cbxUseFilter.isChecked() - self.config[0].talker_sort_series_by_year = self.cbxSortByYear.isChecked() - self.config[0].talker_exact_series_matches_first = self.cbxExactMatches.isChecked() - - if self.leKey.text().strip(): - self.config[0].talker_comicvine_cv_api_key = self.leKey.text().strip() - self.talker.api_key = self.config[0].talker_comicvine_cv_api_key - - if self.leURL.text().strip(): - self.config[0].talker_comicvine_cv_url = self.leURL.text().strip() - self.talker.api_url = self.config[0].talker_comicvine_cv_url + self.config[0].identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() + self.config[0].identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked() + self.config[0].identifier_sort_series_by_year = self.cbxSortByYear.isChecked() + self.config[0].identifier_exact_series_matches_first = self.cbxExactMatches.isChecked() self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() @@ -464,6 +457,9 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].rename_strict = self.cbxRenameStrict.isChecked() self.config[0].rename_replacements = self.get_replacements() + # Read settings from talker tabs + comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config) + self.update_talkers_config() settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json") @@ -471,9 +467,9 @@ class SettingsWindow(QtWidgets.QDialog): QtWidgets.QDialog.accept(self) def update_talkers_config(self) -> None: - cfg = settngs.normalize_config(self.config, True, True) - if f"talker_{self.talker.id}" in cfg[0]: - self.talker.parse_settings(cfg[0][f"talker_{self.talker.id}"]) + ctsettings.talkers = self.talkers + self.config = ctsettings.plugin.validate_talker_settings(self.config) + del ctsettings.talkers def select_rar(self) -> None: self.select_file(self.leRarExePath, "RAR") @@ -483,12 +479,6 @@ class SettingsWindow(QtWidgets.QDialog): ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache() QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.") - def test_api_key(self) -> None: - if self.talker.check_api_key(self.leKey.text().strip(), self.leURL.text().strip()): - QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!") - else: - QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.") - def reset_settings(self) -> None: self.config = settngs.get_namespace(settngs.defaults(self.config[1])) self.settings_to_form() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index d51da19..0363392 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1,6 +1,6 @@ """The main window of the ComicTagger app""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -898,13 +898,13 @@ class TaggerWindow(QtWidgets.QMainWindow): md.is_empty = False md.alternate_number = IssueString(self.leAltIssueNum.text()).as_string() md.issue = IssueString(self.leIssueNum.text()).as_string() - md.issue_count = utils.xlate(self.leIssueCount.text(), True) - md.volume = utils.xlate(self.leVolumeNum.text(), True) - md.volume_count = utils.xlate(self.leVolumeCount.text(), True) - md.month = utils.xlate(self.lePubMonth.text(), True) - md.year = utils.xlate(self.lePubYear.text(), True) - md.day = utils.xlate(self.lePubDay.text(), True) - md.alternate_count = utils.xlate(self.leAltIssueCount.text(), True) + md.issue_count = utils.xlate_int(self.leIssueCount.text()) + md.volume = utils.xlate_int(self.leVolumeNum.text()) + md.volume_count = utils.xlate_int(self.leVolumeCount.text()) + md.month = utils.xlate_int(self.lePubMonth.text()) + md.year = utils.xlate_int(self.lePubYear.text()) + md.day = utils.xlate_int(self.lePubDay.text()) + md.alternate_count = utils.xlate_int(self.leAltIssueCount.text()) md.series = self.leSeries.text() md.title = self.leTitle.text() @@ -915,7 +915,7 @@ class TaggerWindow(QtWidgets.QMainWindow): md.notes = self.teNotes.toPlainText() md.maturity_rating = self.cbMaturityRating.currentText() - md.critical_rating = utils.xlate(self.dsbCriticalRating.cleanText(), is_float=True) + md.critical_rating = utils.xlate_float(self.dsbCriticalRating.cleanText()) if md.critical_rating == 0.0: md.critical_rating = None @@ -1027,9 +1027,9 @@ class TaggerWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.information(self, "Online Search", "Need to enter a series name to search.") return - year = utils.xlate(self.lePubYear.text(), True) + year = utils.xlate_int(self.lePubYear.text()) - issue_count = utils.xlate(self.leIssueCount.text(), True) + issue_count = utils.xlate_int(self.leIssueCount.text()) cover_index_list = self.metadata.get_cover_page_index_list() selector = SeriesSelectionWindow( @@ -1071,7 +1071,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if self.config[0].cbl_apply_transform_on_import: new_metadata = CBLTransformer(new_metadata, self.config[0]).apply() - if self.config[0].talker_clear_form_before_populating: + if self.config[0].identifier_clear_form_before_populating: self.clear_form() notes = ( @@ -1354,7 +1354,7 @@ class TaggerWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.warning(self, self.tr("Web Link"), self.tr("Web Link is invalid.")) def show_settings(self) -> None: - settingswin = SettingsWindow(self, self.config, self.current_talker()) + settingswin = SettingsWindow(self, self.config, self.talkers) settingswin.setModal(True) settingswin.exec() settingswin.result() @@ -1785,7 +1785,7 @@ class TaggerWindow(QtWidgets.QMainWindow): ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config[0].talker_auto_imprint: + if self.config[0].identifier_auto_imprint: md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): @@ -2047,7 +2047,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if self.dirty_flag_verification( "File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?" ): - dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.current_talker()) + dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.talkers) dlg.setModal(True) if dlg.exec() and self.comic_archive is not None: self.fileSelectionList.update_selected_rows() diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index 813c7a8..ee7d80d 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -7,7 +7,7 @@ 0 0 702 - 513 + 559 @@ -28,7 +28,7 @@ - 0 + 3 @@ -136,24 +136,14 @@ Searching - - - - <html><head/><body><p>These settings are for the automatic issue identifier which searches online for matches. </p><p>Hover the mouse over an entry field for more info.</p></body></html> - - - true - - - - - + + Qt::Horizontal - + QFormLayout::AllNonFixedFieldsGrow @@ -252,6 +242,44 @@ + + + + <html><head/><body><p>These settings are for the automatic issue identifier which searches online for matches. </p><p>Hover the mouse over an entry field for more info.</p></body></html> + + + true + + + + + + + Qt::Horizontal + + + + + + + Initially show Series Name exact matches first + + + + + + + Initially sort Series search results by Starting Year instead of No. Issues + + + + + + + Clear Form Before Importing Comic Vine data + + + @@ -308,197 +336,11 @@ - + - Comic Vine + Metadata Sources - - - - - - 0 - 0 - - - - - - - - - Use Series Start Date as Volume - - - - - - - Clear Form Before Importing Comic Vine data - - - - - - - Remove HTML tables from CV summary field - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - - Initially sort Series search results by Starting Year instead of No. Issues - - - - - - - Initially show Series Name exact matches first - - - - - - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - - - - - 0 - 0 - - - - <html><head/><body><p>A personal API key from <a href="http://www.comicvine.com/api/"><span style=" text-decoration: underline; color:#0000ff;">Comic Vine</span></a> is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.</p></body></html> - - - Qt::RichText - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse - - - - - - - Test Key - - - - - - - - 0 - 0 - - - - false - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 200 - 16777215 - - - - Comic Vine API Key - - - - - - - Comic Vine URL - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + @@ -774,25 +616,16 @@ Find - - AlignCenter - Replacement - - AlignCenter - Strict Only - - AlignCenter - @@ -819,25 +652,16 @@ Find - - AlignCenter - Replacement - - AlignCenter - Strict Only - - AlignCenter - diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py new file mode 100644 index 0000000..ff5e5b5 --- /dev/null +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -0,0 +1,238 @@ +from __future__ import annotations + +import argparse +import logging +from functools import partial +from typing import Any, NamedTuple + +import settngs +from PyQt5 import QtCore, QtWidgets + +from comictalker.comictalker import ComicTalker + +logger = logging.getLogger(__name__) + + +class TalkerTab(NamedTuple): + tab: QtWidgets.QWidget + widgets: dict[str, QtWidgets.QWidget] + + +def generate_api_widgets( + talker_id: str, + sources: dict[str, QtWidgets.QWidget], + config: settngs.Config[settngs.Namespace], + layout: QtWidgets.QGridLayout, + talkers: dict[str, ComicTalker], +) -> None: + # *args enforces keyword arguments and allows position arguments to be ignored + def call_check_api(*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker_id: str) -> None: + url = "" + key = "" + if le_key is not None: + key = le_key.text().strip() + if le_url is not None: + url = le_url.text().strip() + + check_text, check_bool = talkers[talker_id].check_api_key(url, key) + if check_bool: + QtWidgets.QMessageBox.information(None, "API Test Success", check_text) + else: + QtWidgets.QMessageBox.warning(None, "API Test Failed", check_text) + + # get the actual config objects in case they have overwritten the default + talker_key = config[1][f"talker_{talker_id}"][1][f"{talker_id}_key"] + talker_url = config[1][f"talker_{talker_id}"][1][f"{talker_id}_url"] + btn_test_row = None + le_key = None + le_url = None + + # only file settings are saved + if talker_key.file: + # record the current row so we know where to add the button + btn_test_row = layout.rowCount() + le_key = generate_textbox(talker_key, layout) + # To enable setting and getting + sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_key"] = le_key + + # only file settings are saved + if talker_url.file: + # record the current row so we know where to add the button + # We overwrite so that the default will be next to the url text box + btn_test_row = layout.rowCount() + le_url = generate_textbox(talker_url, layout) + # To enable setting and getting + sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_url"] = le_url + + # The button row was recorded so we add it + if btn_test_row is not None: + btn = QtWidgets.QPushButton("Test API") + layout.addWidget(btn, btn_test_row, 2) + # partial is used as connect will pass in event information + btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker_id=talker_id)) + + +def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox: + widget = QtWidgets.QCheckBox(option.display_name) + widget.setToolTip(option.help) + layout.addWidget(widget, layout.rowCount(), 0, 1, -1) + + return widget + + +def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QSpinBox: + row = layout.rowCount() + lbl = QtWidgets.QLabel(option.display_name) + lbl.setToolTip(option.help) + layout.addWidget(lbl, row, 0) + widget = QtWidgets.QSpinBox() + widget.setRange(0, 9999) + widget.setToolTip(option.help) + layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft) + + return widget + + +def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QDoubleSpinBox: + row = layout.rowCount() + lbl = QtWidgets.QLabel(option.display_name) + lbl.setToolTip(option.help) + layout.addWidget(lbl, row, 0) + widget = QtWidgets.QDoubleSpinBox() + widget.setRange(0, 9999.99) + widget.setToolTip(option.help) + layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft) + + return widget + + +def generate_textbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QLineEdit: + row = layout.rowCount() + lbl = QtWidgets.QLabel(option.display_name) + lbl.setToolTip(option.help) + layout.addWidget(lbl, row, 0) + widget = QtWidgets.QLineEdit() + widget.setToolTip(option.help) + layout.addWidget(widget, row, 1) + + return widget + + +def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: + # Set the active talker via id in sources combo box + sources["cbx_select_talker"].setCurrentIndex(sources["cbx_select_talker"].findData(config[0].talker_source)) + + for talker in sources["tabs"].items(): + for name, widget in talker[1].widgets.items(): + value = getattr(config[0], name) + value_type = type(value) + try: + if value_type is str: + widget.setText(value) + if value_type is int or value_type is float: + widget.setValue(value) + if value_type is bool: + widget.setChecked(value) + except Exception: + logger.debug("Failed to set value of %s", name) + + +def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: + # Source combo box value + config[0].talker_source = sources["cbx_select_talker"].currentData() + + for tab in sources["tabs"].items(): + for name, widget in tab[1].widgets.items(): + widget_value = None + if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): + widget_value = widget.value() + elif isinstance(widget, QtWidgets.QLineEdit): + widget_value = widget.text().strip() + elif isinstance(widget, QtWidgets.QCheckBox): + widget_value = widget.isChecked() + + setattr(config[0], name, widget_value) + + +def generate_source_option_tabs( + comic_talker_tab: QtWidgets.QWidget, + config: settngs.Config[settngs.Namespace], + talkers: dict[str, ComicTalker], +) -> dict[str, QtWidgets.QWidget]: + """ + Generate GUI tabs and settings for talkers + """ + + # Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget + sources: dict = {"tabs": {}} + + # Tab comes with a QVBoxLayout + comic_talker_tab_layout = comic_talker_tab.layout() + + talker_layout = QtWidgets.QGridLayout() + lbl_select_talker = QtWidgets.QLabel("Metadata Source:") + cbx_select_talker = QtWidgets.QComboBox() + line = QtWidgets.QFrame() + line.setFrameShape(QtWidgets.QFrame.HLine) + line.setFrameShadow(QtWidgets.QFrame.Sunken) + talker_tabs = QtWidgets.QTabWidget() + + talker_layout.addWidget(lbl_select_talker, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(cbx_select_talker, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(line, 1, 0, 1, -1) + talker_layout.addWidget(talker_tabs, 2, 0, 1, -1) + + comic_talker_tab_layout.addLayout(talker_layout) + + # Add combobox to sources for getting and setting talker + sources["cbx_select_talker"] = cbx_select_talker + + # Add source sub tabs to Comic Sources tab + for talker_id, talker_obj in talkers.items(): + # Add source to general tab dropdown list + cbx_select_talker.addItem(talker_obj.name, talker_id) + + tab_name = talker_id + sources["tabs"][tab_name] = TalkerTab(tab=QtWidgets.QWidget(), widgets={}) + layout_grid = QtWidgets.QGridLayout() + + for option in config[1][f"talker_{talker_id}"][1].values(): + if not option.file: + continue + if option.dest in (f"{talker_id}_url", f"{talker_id}_key"): + continue + current_widget = None + if option.action is not None and ( + option.action is argparse.BooleanOptionalAction + or option.type is bool + or option.action == "store_true" + or option.action == "store_false" + ): + current_widget = generate_checkbox(option, layout_grid) + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + elif option.type is int: + current_widget = generate_spinbox(option, layout_grid) + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + elif option.type is float: + current_widget = generate_doublespinbox(option, layout_grid) + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + # option.type of None should be string + elif (option.type is None and option.action is None) or option.type is str: + current_widget = generate_textbox(option, layout_grid) + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + else: + logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") + + # Add talker URL and API key fields + generate_api_widgets(talker_id, sources, config, layout_grid, talkers) + + # Add vertical spacer + vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0) + # Display the new widgets + sources["tabs"][tab_name].tab.setLayout(layout_grid) + + # Add new sub tab to Comic Source tab + talker_tabs.addTab(sources["tabs"][tab_name].tab, talker_obj.name) + + return sources diff --git a/comictaggerlib/versionchecker.py b/comictaggerlib/versionchecker.py index f0da9e2..d696dbb 100644 --- a/comictaggerlib/versionchecker.py +++ b/comictaggerlib/versionchecker.py @@ -1,6 +1,6 @@ """Version checker""" # -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictalker/__init__.py b/comictalker/__init__.py index e5bd095..0e5f6d6 100644 --- a/comictalker/__init__.py +++ b/comictalker/__init__.py @@ -2,8 +2,13 @@ from __future__ import annotations import logging import pathlib +import sys + +if sys.version_info < (3, 10): + from importlib_metadata import entry_points +else: + from importlib.metadata import entry_points -import comictalker.talkers.comicvine from comictalker.comictalker import ComicTalker, TalkerError from comictalker.resulttypes import ComicIssue, ComicSeries @@ -21,11 +26,16 @@ def get_talkers(version: str, cache: pathlib.Path) -> dict[str, ComicTalker]: """Returns all comic talker instances""" talkers: dict[str, ComicTalker] = {} - for talker in [comictalker.talkers.comicvine.ComicVineTalker]: + for talker in entry_points(group="comictagger.talker"): try: - obj = talker(version, cache) - talkers[obj.id] = obj + talker_cls = talker.load() + obj = talker_cls(version, cache) + if obj.id != talker.name: + logger.error("Talker ID must be the same as the entry point name") + continue + talkers[talker.name] = obj + except Exception: - logger.exception("Failed to load talker: %s", "comicvine") - raise TalkerError(source="comicvine", code=4, desc="Failed to initialise talker") + logger.exception("Failed to load talker: %s", talker.name) + return talkers diff --git a/comictalker/comiccacher.py b/comictalker/comiccacher.py index 80bb49d..e61f914 100644 --- a/comictalker/comiccacher.py +++ b/comictalker/comiccacher.py @@ -1,6 +1,6 @@ """A python class to manage caching of data from Comic Vine""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -411,7 +411,12 @@ class ComicCacher: ) # now process the results - + credits = [] + try: + for credit in json.loads(row[13]): + credits.append(Credit(**credit)) + except Exception: + logger.exception("credits failed") record = ComicIssue( id=row[1], name=row[2], @@ -425,7 +430,7 @@ class ComicCacher: alt_image_urls=row[10].strip().splitlines(), characters=row[11].strip().splitlines(), locations=row[12].strip().splitlines(), - credits=json.loads(row[13]), + credits=credits, teams=row[14].strip().splitlines(), story_arcs=row[15].strip().splitlines(), genres=row[16].strip().splitlines(), diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index 88c078d..319dd9b 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -1,4 +1,4 @@ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import settngs from comicapi.genericmetadata import GenericMetadata from comictalker.resulttypes import ComicIssue, ComicSeries +from comictalker.talker_utils import fix_url logger = logging.getLogger(__name__) @@ -107,18 +108,21 @@ class ComicTalker: name: str = "Example" id: str = "example" - logo_url: str = "https://example.com/logo.png" - website: str = "https://example.com/" + website: str = "https://example.com" + logo_url: str = f"{website}/logo.png" attribution: str = f"Metadata provided by {name}" def __init__(self, version: str, cache_folder: pathlib.Path) -> None: self.cache_folder = cache_folder self.version = version - self.api_key: str = "" - self.api_url: str = "" + self.api_key = self.default_api_key = "" + self.api_url = self.default_api_url = "" def register_settings(self, parser: settngs.Manager) -> None: - """Allows registering settings using the settngs package with an argparse like interface""" + """ + Allows registering settings using the settngs package with an argparse like interface. + The order that settings are declared is the order they will be displayed. + """ return None def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: @@ -126,12 +130,29 @@ class ComicTalker: settings is a dictionary of settings defined in register_settings. It is only guaranteed that the settings defined in register_settings will be present. """ + if settings[f"{self.id}_key"]: + self.api_key = settings[f"{self.id}_key"] + if settings[f"{self.id}_url"]: + self.api_url = fix_url(settings[f"{self.id}_url"]) + + settings[f"{self.id}_url"] = self.api_url + + if self.api_key == "": + self.api_key = self.default_api_key + if self.api_url == "": + self.api_url = self.default_api_url return settings - def check_api_key(self, key: str, url: str) -> bool: + def check_api_key(self, url: str, key: str) -> tuple[str, bool]: """ - This function should return true if the given api key and url are valid. - If the Talker does not use an api key it should validate that the url works. + This function should return (msg, True) if the given API key and URL are valid, + where msg is a message to display to the user. + + This function should return (msg, False) if the given API key or URL are not valid, + where msg is a message to display to the user. + + If the Talker does not use an API key it should validate that the URL works. + If the Talker does not use an API key or URL it should check that the source is available. """ raise NotImplementedError @@ -146,10 +167,14 @@ class ComicTalker: """ This function should return a list of series that match the given series name according to the source the Talker uses. + Sanitizing the series name is the responsibility of the talker. + If `literal` == True then it is requested that no filtering or transformation/sanitizing of the title or results be performed by the talker. + A sensible amount of results should be returned. + For example the `ComicVineTalker` stops requesting new pages after the results become too different from the `series_name` by use of the `titles_match` function provided by the `comicapi.utils` module, and only allows a maximum of 5 pages @@ -183,7 +208,9 @@ class ComicTalker: """ This function should return a single issue for each series id in the `series_id_list` and it should match the issue_number. + Preferably it should also only return issues published in the given `year`. + If there is no year given (`year` == None) or the Talker does not have issue publication info return the results unfiltered. """ diff --git a/comictalker/talker_utils.py b/comictalker/talker_utils.py index 19485c2..e81b37c 100644 --- a/comictalker/talker_utils.py +++ b/comictalker/talker_utils.py @@ -1,4 +1,4 @@ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ from __future__ import annotations import logging +import posixpath import re +from urllib.parse import urlsplit from bs4 import BeautifulSoup @@ -26,6 +28,13 @@ from comictalker.resulttypes import ComicIssue logger = logging.getLogger(__name__) +def fix_url(url: str) -> str: + tmp_url = urlsplit(url) + # joinurl only works properly if there is a trailing slash + tmp_url = tmp_url._replace(path=posixpath.normpath(tmp_url.path) + "/") + return tmp_url.geturl() + + def map_comic_issue_to_metadata( issue_results: ComicIssue, source: str, remove_html_tables: bool = False, use_year_volume: bool = False ) -> GenericMetadata: @@ -49,7 +58,7 @@ def map_comic_issue_to_metadata( if issue_results.cover_date: metadata.day, metadata.month, metadata.year = utils.parse_date_str(issue_results.cover_date) elif issue_results.series.start_year: - metadata.year = utils.xlate(issue_results.series.start_year, True) + metadata.year = utils.xlate_int(issue_results.series.start_year) metadata.comments = cleanup_html(issue_results.description, remove_html_tables) if use_year_volume: @@ -95,11 +104,11 @@ def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]: year = None if date_str: parts = date_str.split("-") - year = utils.xlate(parts[0], True) + year = utils.xlate_int(parts[0]) if len(parts) > 1: - month = utils.xlate(parts[1], True) + month = utils.xlate_int(parts[1]) if len(parts) > 2: - day = utils.xlate(parts[2], True) + day = utils.xlate_int(parts[2]) return day, month, year diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 3f7d11e..3dc77ef 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -1,7 +1,7 @@ """ ComicVine information source """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import logging import pathlib import time from typing import Any, Callable, Generic, TypeVar -from urllib.parse import urljoin, urlsplit +from urllib.parse import urljoin import requests import settngs @@ -156,78 +156,86 @@ CV_STATUS_RATELIMIT = 107 class ComicVineTalker(ComicTalker): name: str = "Comic Vine" id: str = "comicvine" - logo_url: str = "https://comicvine.gamespot.com/a/bundles/comicvinesite/images/logo.png" - website: str = "https://comicvine.gamespot.com/" + website: str = "https://comicvine.gamespot.com" + logo_url: str = f"{website}/a/bundles/comicvinesite/images/logo.png" attribution: str = f"Metadata provided by {name}" def __init__(self, version: str, cache_folder: pathlib.Path): super().__init__(version, cache_folder) # Default settings - self.api_url: str = "https://comicvine.gamespot.com/api" - self.api_key: str = "27431e6787042105bd3e47e169a624521f89f3a4" + self.default_api_url = self.api_url = f"{self.website}/api/" + self.default_api_key = self.api_key = "27431e6787042105bd3e47e169a624521f89f3a4" self.remove_html_tables: bool = False self.use_series_start_as_volume: bool = False self.wait_on_ratelimit: bool = False - tmp_url = urlsplit(self.api_url) - - # joinurl only works properly if there is a trailing slash - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") - - self.api_url = tmp_url.geturl() - # NOTE: This was hardcoded before which is why it isn't in settings self.wait_on_ratelimit_time: int = 20 def register_settings(self, parser: settngs.Manager) -> None: - parser.add_setting("--cv-api-key", help="Use the given Comic Vine API Key.") - parser.add_setting("--cv-url", help="Use the given Comic Vine URL.") - parser.add_setting("--cv-use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction) - parser.add_setting("--cv-wait-on-ratelimit", default=False, action=argparse.BooleanOptionalAction) + parser.add_setting( + "--cv-use-series-start-as-volume", + default=False, + action=argparse.BooleanOptionalAction, + display_name="Use series start as volume", + help="Use the series start year as the volume number", + ) + parser.add_setting( + "--cv-wait-on-ratelimit", + default=False, + action=argparse.BooleanOptionalAction, + display_name="Wait on ratelimit", + help="Wait when the rate limit is hit", + ) parser.add_setting( "--cv-remove-html-tables", default=False, action=argparse.BooleanOptionalAction, - help="Removes html tables instead of converting them to text.", + display_name="Remove HTML tables", + help="Removes html tables instead of converting them to text", + ) + # The empty string being the default allows this setting to be unset, allowing the default to change + parser.add_setting( + f"--{self.id}-key", + default="", + display_name="API Key", + help=f"Use the given Comic Vine API Key. (default: {self.default_api_key})", + ) + parser.add_setting( + f"--{self.id}-url", + default="", + display_name="API URL", + help=f"Use the given Comic Vine URL. (default: {self.default_api_url})", ) def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: - if settings["cv_api_key"]: - self.api_key = settings["cv_api_key"] - if settings["cv_url"]: - tmp_url = urlsplit(settings["cv_url"]) - # joinurl only works properly if there is a trailing slash - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") - - self.api_url = tmp_url.geturl() + settings = super().parse_settings(settings) self.use_series_start_as_volume = settings["cv_use_series_start_as_volume"] self.wait_on_ratelimit = settings["cv_wait_on_ratelimit"] self.remove_html_tables = settings["cv_remove_html_tables"] - return settngs + return settings - def check_api_key(self, key: str, url: str) -> bool: + def check_api_key(self, url: str, key: str) -> tuple[str, bool]: + url = talker_utils.fix_url(url) if not url: - url = self.api_url + url = self.default_api_url try: - tmp_url = urlsplit(url) - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") - url = tmp_url.geturl() test_url = urljoin(url, "issue/1/") cv_response: CVResult = requests.get( test_url, headers={"user-agent": "comictagger/" + self.version}, - params={"api_key": key, "format": "json", "field_list": "name"}, + params={"api_key": key or self.default_api_key, "format": "json", "field_list": "name"}, ).json() # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" - return cv_response["status_code"] != 100 + if cv_response["status_code"] != 100: + return "The API key is valid", True + else: + return "The API key is INVALID!", False except Exception: - return False + return "Failed to connect to the URL!", False def search_for_series( self, @@ -385,7 +393,7 @@ class ComicVineTalker(ComicTalker): series_filter += str(vid) + "|" flt = f"volume:{series_filter},issue_number:{issue_number}" # CV uses volume to mean series - int_year = utils.xlate(year, True) + int_year = utils.xlate_int(year) if int_year is not None: flt += f",cover_date:{int_year}-1-1|{int_year + 1}-1-1" @@ -493,7 +501,7 @@ class ComicVineTalker(ComicTalker): else: image_url = record["image"].get("super_url", "") - start_year = utils.xlate(record.get("start_year", ""), True) + start_year = utils.xlate_int(record.get("start_year", "")) aliases = record.get("aliases") or "" diff --git a/desktop-integration/mac/ComicTagger b/desktop-integration/mac/ComicTagger deleted file mode 100644 index 7c79e43..0000000 --- a/desktop-integration/mac/ComicTagger +++ /dev/null @@ -1,4 +0,0 @@ -This file is a placeholder that will automatically be replaced with a symlink to -the local machine's Python framework python binary. - -When pip does an uninstall, it will remove the link. diff --git a/desktop-integration/mac/Info.plist b/desktop-integration/mac/Info.plist deleted file mode 100644 index cdbf493..0000000 --- a/desktop-integration/mac/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - main.sh - CFBundleIconFile - app.icns - CFBundleIdentifier - org.comictagger.comictagger - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ComicTagger - CFBundlePackageType - APPL - CFBundleShortVersionString - %%CTVERSION%% - CFBundleSignature - ???? - CFBundleVersion - %%CTVERSION%% - NSAppleScriptEnabled - YES - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/desktop-integration/mac/main.sh b/desktop-integration/mac/main.sh deleted file mode 100755 index 1d2fe4f..0000000 --- a/desktop-integration/mac/main.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -# This is a lot of hoop-jumping to get the absolute path -# of this script, so that we can use the Symlinked python -# binary to call the CT script. This is all so that the -# Mac menu doesn't say "Python". - -realpath() -{ - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -CTSCRIPT=%%CTSCRIPT%% - -THIS=$(realpath $0) -THIS_FOLDER=$(dirname $THIS) -"$THIS_FOLDER/ComicTagger" "$CTSCRIPT" diff --git a/desktop-integration/windows/ComicTagger-pip.lnk b/desktop-integration/windows/ComicTagger-pip.lnk deleted file mode 100644 index c0cadfa..0000000 --- a/desktop-integration/windows/ComicTagger-pip.lnk +++ /dev/null @@ -1,4 +0,0 @@ -This file is a placeholder that will automatically be replaced with a Windows -shortcut on the user's desktop. - -When pip does an uninstall, it will remove the shortcut file. diff --git a/localefix.py b/localefix.py deleted file mode 100644 index b7bf897..0000000 --- a/localefix.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import locale -import logging -import os -import subprocess -import sys - - -def _lang_code_mac() -> str: - """ - stolen from https://github.com/mu-editor/mu - Returns the user's language preference as defined in the Language & Region - preference pane in macOS's System Preferences. - """ - - # Uses the shell command `defaults read -g AppleLocale` that prints out a - # language code to standard output. Assumptions about the command: - # - It exists and is in the shell's PATH. - # - It accepts those arguments. - # - It returns a usable language code. - # - # Reference documentation: - # - The man page for the `defaults` command on macOS. - # - The macOS underlying API: - # https://developer.apple.com/documentation/foundation/nsuserdefaults. - - lang_detect_command = "defaults read -g AppleLocale" - - status, output = subprocess.getstatusoutput(lang_detect_command) - if status == 0: - # Command was successful. - lang_code = output - else: - logging.warning("Language detection command failed: %r", output) - lang_code = "" - - return lang_code - - -def configure_locale() -> None: - if sys.platform == "darwin" and "LANG" not in os.environ: - code = _lang_code_mac() - if code != "": - os.environ["LANG"] = f"{code}.utf-8" - - locale.setlocale(locale.LC_ALL, "") - sys.stdout.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] - sys.stderr.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] - sys.stdin.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] diff --git a/requirements-7Z.txt b/requirements-7Z.txt deleted file mode 100644 index 014d24e..0000000 --- a/requirements-7Z.txt +++ /dev/null @@ -1 +0,0 @@ -py7zr diff --git a/requirements-CBR.txt b/requirements-CBR.txt deleted file mode 100644 index 0e3a2cd..0000000 --- a/requirements-CBR.txt +++ /dev/null @@ -1 +0,0 @@ -unrar-cffi>=0.2.2 diff --git a/requirements-GUI.txt b/requirements-GUI.txt deleted file mode 100644 index 2d07ca0..0000000 --- a/requirements-GUI.txt +++ /dev/null @@ -1 +0,0 @@ -PyQt5 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a6aefcd..0000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -appdirs==1.4.4 -beautifulsoup4>=4.1 -importlib_metadata>=3.3.0 -natsort>=8.1.0 -pathvalidate -pillow>=9.1.0, <10 -pycountry -#pyicu; sys_platform == 'linux' or sys_platform == 'darwin' -rapidfuzz>=2.12.0 -requests==2.* -settngs==0.5.0 -text2digits -typing_extensions -wordninja diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index ace72dd..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,12 +0,0 @@ -black>=22 -build -flake8==4.* -flake8-black -flake8-encodings -flake8-isort -isort>=5.10 -pyinstaller>=5.6.2 -pytest==7.* -setuptools>=42 -setuptools_scm[toml]>=3.4 -wheel diff --git a/scripts/inventory.py b/scripts/inventory.py index efa33ec..f68fd65 100755 --- a/scripts/inventory.py +++ b/scripts/inventory.py @@ -1,7 +1,7 @@ #!/usr/bin/python """Print out a line-by-line list of basic tag info from all comics""" -# Copyright 2012 Anthony Beville +# Copyright 2012 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/make_links.py b/scripts/make_links.py index dd93120..24fd8dd 100755 --- a/scripts/make_links.py +++ b/scripts/make_links.py @@ -4,7 +4,7 @@ Make some tree structures and symbolic links to comic files based on metadata organizing by date and series, in different trees """ -# Copyright 2012 Anthony Beville +# Copyright 2012 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/move2folder.py b/scripts/move2folder.py index 154afcb..de81cd5 100755 --- a/scripts/move2folder.py +++ b/scripts/move2folder.py @@ -1,9 +1,7 @@ #!/usr/bin/python """Moves comic files based on metadata organizing in a tree by Publisher/Series (Volume)""" -# This script is based on make_links.py by Anthony Beville - -# Copyright 2015 Fabio Cancedda, Anthony Beville +# Copyright 2015 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/name_fixer.py b/scripts/name_fixer.py index fdbfa1d..5117069 100755 --- a/scripts/name_fixer.py +++ b/scripts/name_fixer.py @@ -1,7 +1,7 @@ #!/usr/bin/python """Fix the comic file names using a list of transforms""" -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/remove_ads.py b/scripts/remove_ads.py index 31f4579..934828e 100755 --- a/scripts/remove_ads.py +++ b/scripts/remove_ads.py @@ -5,7 +5,7 @@ and deleted. Walks recursively through the given folders. Originals are kept in a sub-folder at the level of the original """ -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/shrink.py b/scripts/shrink.py index eb74ef3..e618993 100644 --- a/scripts/shrink.py +++ b/scripts/shrink.py @@ -1,7 +1,7 @@ #!/usr/bin/python """Reduce the image size of pages in the comic archive""" -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/validate_cover.py b/scripts/validate_cover.py index 4bb9077..72e185f 100755 --- a/scripts/validate_cover.py +++ b/scripts/validate_cover.py @@ -2,7 +2,7 @@ """Test archive cover against Comic Vine for a given issue ID """ -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..035b7cf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,282 @@ +[metadata] +name = comictagger +description = A cross-platform GUI/CLI app for writing metadata to comic archives +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/comictagger/comictagger +author = ComicTagger team +author_email = comictagger@gmail.com +license = Apache License 2.0 +classifiers = + Development Status :: 4 - Beta + Environment :: Console + Environment :: MacOS X + Environment :: Win32 (MS Windows) + Environment :: X11 Applications :: Qt + Intended Audience :: End Users/Desktop + License :: OSI Approved :: Apache Software License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Topic :: Multimedia :: Graphics + Topic :: Other/Nonlisted Topic + Topic :: Utilities +keywords = + comictagger + comics + comic + metadata + tagging + tagger + +[options] +packages = find: +install_requires = + appdirs==1.4.4 + beautifulsoup4>=4.1 + importlib-metadata>=3.3.0 + natsort>=8.1.0 + pathvalidate + pillow>=9.1.0,<10 + pycountry + rapidfuzz>=2.12.0 + requests==2.* + settngs==0.6.3 + text2digits + typing-extensions>=4.3.0 + wordninja +python_requires = >=3.9 + +[options.packages.find] +exclude = tests; testing + +[options.entry_points] +console_scripts = comictagger=comictaggerlib.main:main +comicapi.archiver = + zip = comicapi.archivers.zip:ZipArchiver + sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver + rar = comicapi.archivers.rar:RarArchiver + folder = comicapi.archivers.folder:FolderArchiver +comictagger.talker = + comicvine = comictalker.talkers.comicvine:ComicVineTalker +pyinstaller40 = + hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs + +[options.extras_require] +7Z = + py7zr +CBR = + rarfile>=4.0 +GUI = + PyQt5 +ICU = + pyicu;sys_platform == 'linux' or sys_platform == 'darwin' +all = + PyQt5 + py7zr + rarfile>=4.0 + pyicu;sys_platform == 'linux' or sys_platform == 'darwin' + +[options.package_data] +comicapi = + data/* +comictaggerlib = + ui/* + graphics/* + +[tox:tox] +env_list = + format + py3.9-{none,gui,7z,cbr,icu,all} +minversion = 4.4.12 +basepython = {env:tox_python:python3.9} + +[testenv] +description = run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest>=7 +extras = + 7z: 7Z + cbr: CBR + gui: GUI + icu: ICU + all: all +commands = + python -m pytest {tty:--color=yes} {posargs} + icu,all: python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu + +[m1env] +description = run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest>=7 + icu,all: pyicu-binary +extras = + 7z: 7Z + cbr: CBR + gui: GUI + all: 7Z,CBR,GUI + +[testenv:py3.9-{icu,all}] +base = {env:tox_env:testenv} + +[testenv:format] +labels = + release + build +skip_install = true +deps = + black>=22 + isort>=5.10 + setup-cfg-fmt + autoflake + pyupgrade +commands = + -setup-cfg-fmt setup.cfg + -python -m autoflake -i --remove-all-unused-imports --ignore-init-module-imports . + -python -m isort --af --add-import 'from __future__ import annotations' . + -python -m black . + +[testenv:lint] +labels = + release +skip_install = true +depends = format +deps = + flake8==4.* + flake8-black + flake8-encodings + flake8-isort + mypy + types-setuptools + types-requests +commands = + python -m flake8 . + -python -m mypy --ignore-missing-imports comicapi comictaggerlib comictalker + +[testenv:clean] +description = Clean development outputs +labels = + release + build +depends = + format + lint +skip_install = true +commands = + -python -c 'import shutil,pathlib; \ + shutil.rmtree("./build/", ignore_errors=True); \ + shutil.rmtree("./dist/", ignore_errors=True); \ + pathlib.Path("./comictaggerlib/ctversion.py").unlink(missing_ok=True); \ + pathlib.Path("comictagger.spec").unlink(missing_ok=True)' + +[testenv:wheel] +description = Generate wheel and tar.gz +labels = + release + build +depends = clean +skip_install = true +deps = + build +commands = + python -m build + +[testenv:pypi-upload] +description = Upload wheel to PyPi +platform = linux +labels = + release +skip_install = true +depends = wheel +deps = + twine +passenv = + TWINE_* +setenv = + TWINE_NON_INTERACTIVE=true +commands = + python -m twine upload dist/*.whl dist/*.tar.gz + +[testenv:pyinstaller] +description = Generate pyinstaller executable +labels = + release + build +base = {env:tox_env:testenv} +depends = + clean + pypi-upload +deps = + pyinstaller>=5.6.2 +extras = + all +commands = + python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu + pyinstaller -y build-tools/comictagger.spec + +[testenv:appimage] +description = Generate appimage executable +skip_install = true +platform = linux +base = {env:tox_env:testenv} +labels = + release + build +depends = + clean + pypi-upload + pyinstaller +deps = + requests +allowlist_externals = + {tox_root}/build/appimagetool-x86_64.AppImage +change_dir = dist +commands_pre = + -python -c 'import shutil; shutil.rmtree("{tox_root}/build/", ignore_errors=True)' + python {tox_root}/build-tools/get_appimage.py {tox_root}/build/appimagetool-x86_64.AppImage +commands = + python -c 'import shutil,pathlib; shutil.copytree("{tox_root}/dist/comictagger/", "{tox_root}/build/appimage", dirs_exist_ok=True); \ + shutil.copy("{tox_root}/comictaggerlib/graphics/app.png", "{tox_root}/build/appimage/app.png"); \ + pathlib.Path("{tox_root}/build/appimage/AppRun").symlink_to("comictagger"); \ + pathlib.Path("{tox_root}/build/appimage/AppRun.desktop").write_text( \ + pathlib.Path("{tox_root}/build-tools/ComicTagger.desktop").read_text() \ + .replace("/usr/local/share/comictagger/app.png", "app") \ + .replace("Exec=comictagger", "Exec={tox_root}/comictagger"))' + {tox_root}/build/appimagetool-x86_64.AppImage {tox_root}/build/appimage + +[testenv:zip_artifacts] +description = Zip release artifacts +labels = + release + build +depends = + wheel + pyinstaller + appimage +commands = + python ./build-tools/zip_artifacts.py + +[testenv:venv] +envdir = venv +deps = + flake8==4.* + flake8-black + flake8-encodings + flake8-isort + mypy + types-setuptools + types-requests + build + pyinstaller>=5.6.2 + +[flake8] +max-line-length = 120 +extend-ignore = E203, E501, A003 +extend-exclude = venv, scripts, build, dist, comictaggerlib/ctversion.py +per-file-ignores = + comictaggerlib/cli.py: T20 diff --git a/setup.py b/setup.py index 291d1bc..a03590f 100644 --- a/setup.py +++ b/setup.py @@ -1,89 +1,5 @@ -# Setup file for comictagger python source (no wheels yet) -# -# An entry point script called "comictagger" will be created -# -# Currently commented out, an experiment at desktop integration. -# It seems that post installation tweaks are broken by wheel files. -# Kept here for further research - from __future__ import annotations -import glob -import os +from setuptools import setup -from setuptools import find_packages, setup - - -def read(fname: str) -> str: - """ - Read the contents of a file. - Parameters - ---------- - fname : str - Path to file. - Returns - ------- - str - File contents. - """ - with open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8") as f: - return f.read() - - -install_requires = read("requirements.txt").splitlines() - -# Dynamically determine extra dependencies -extras_require = {} -extra_req_files = glob.glob("requirements-*.txt") -for extra_req_file in extra_req_files: - name = os.path.splitext(extra_req_file)[0].replace("requirements-", "", 1) - extras_require[name] = read(extra_req_file).splitlines() - -# If there are any extras, add a catch-all case that includes everything. -# This assumes that entries in extras_require are lists (not single strings), -# and that there are no duplicated packages across the extras. -if extras_require: - extras_require["all"] = sorted({x for v in extras_require.values() for x in v}) - - -setup( - name="comictagger", - install_requires=install_requires, - extras_require=extras_require, - python_requires=">=3.9", - description="A cross-platform GUI/CLI app for writing metadata to comic archives", - author="ComicTagger team", - author_email="comictagger@gmail.com", - url="https://github.com/comictagger/comictagger", - packages=find_packages(exclude=["tests", "testing"]), - package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]}, - entry_points={ - "console_scripts": ["comictagger=comictaggerlib.main:main"], - "pyinstaller40": ["hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs"], - "comicapi.archiver": [ - "zip = comicapi.archivers.zip:ZipArchiver", - "sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver", - "rar = comicapi.archivers.rar:RarArchiver", - "folder = comicapi.archivers.folder:FolderArchiver", - ], - }, - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Environment :: Win32 (MS Windows)", - "Environment :: MacOS X", - "Environment :: X11 Applications :: Qt", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", - "Topic :: Utilities", - "Topic :: Other/Nonlisted Topic", - "Topic :: Multimedia :: Graphics", - ], - keywords=["comictagger", "comics", "comic", "metadata", "tagging", "tagger"], - license="Apache License 2.0", - long_description=read("README.md"), - long_description_content_type="text/markdown", -) +setup() diff --git a/tests/utils_test.py b/tests/utils_test.py index ba525ee..43ebc94 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -7,6 +7,46 @@ import pytest import comicapi.utils +def test_os_sorted(): + page_name_list = [ + "cover.jpg", + "Page1.jpeg", + "!cover.jpg", + "page4.webp", + "test/!cover.tar.gz", + "!cover.tar.gz", + "00.jpg", + "ignored.txt", + "page0.jpg", + "test/00.tar.gz", + ".ignored.jpg", + "Page3.gif", + "!cover.tar.gz", + "Page2.png", + "page10.jpg", + "!cover", + ] + + assert comicapi.utils.os_sorted(page_name_list) == [ + "!cover", + "!cover.jpg", + "!cover.tar.gz", + "!cover.tar.gz", # Depending on locale punctuation or numbers might come first (Linux, MacOS) + ".ignored.jpg", + "00.jpg", + "cover.jpg", + "ignored.txt", + "page0.jpg", + "Page1.jpeg", + "Page2.png", + "Page3.gif", + "page4.webp", + "page10.jpg", + "test/!cover.tar.gz", + "test/00.tar.gz", + ] + + def test_recursive_list_with_file(tmp_path) -> None: foo_png = tmp_path / "foo.png" foo_png.write_text("not a png") @@ -27,39 +67,56 @@ def test_recursive_list_with_file(tmp_path) -> None: temp_txt2 = tmp_path / "info2.txt" temp_txt2.write_text("this is here") + glob_in_name = tmp_path / "[e-b]" + glob_in_name.mkdir() + expected_result = {str(foo_png), str(temp_cbr), str(temp_file), str(temp_txt), str(temp_txt2)} - result = set(comicapi.utils.get_recursive_filelist([str(temp_txt2), tmp_path])) + result = set(comicapi.utils.get_recursive_filelist([str(temp_txt2), tmp_path, str(glob_in_name)])) assert result == expected_result xlate_values = [ - ({"data": "", "is_int": False, "is_float": False}, None), - ({"data": None, "is_int": False, "is_float": False}, None), - ({"data": None, "is_int": True, "is_float": False}, None), - ({"data": " ", "is_int": True, "is_float": False}, None), - ({"data": "", "is_int": True, "is_float": False}, None), - ({"data": "9..", "is_int": True, "is_float": False}, None), - ({"data": "9", "is_int": False, "is_float": False}, "9"), - ({"data": 9, "is_int": False, "is_float": False}, "9"), - ({"data": 9, "is_int": True, "is_float": False}, 9), - ({"data": "9", "is_int": True, "is_float": False}, 9), - ({"data": 9.3, "is_int": True, "is_float": False}, 9), - ({"data": "9.3", "is_int": True, "is_float": False}, 9), - ({"data": "9.", "is_int": True, "is_float": False}, 9), - ({"data": " 9 . 3 l", "is_int": True, "is_float": False}, 9), - ({"data": 9, "is_int": False, "is_float": True}, 9.0), - ({"data": "9", "is_int": False, "is_float": True}, 9.0), - ({"data": 9.3, "is_int": False, "is_float": True}, 9.3), - ({"data": "9.3", "is_int": False, "is_float": True}, 9.3), - ({"data": "9.", "is_int": False, "is_float": True}, 9.0), - ({"data": " 9 . 3 l", "is_int": False, "is_float": True}, 9.3), + ("", None), + (None, None), + ("9", "9"), + (9, "9"), +] +xlate_int_values = [ + (None, None), + (" ", None), + ("", None), + ("9..", None), + (9, 9), + ("9", 9), + (9.3, 9), + ("9.3", 9), + ("9.", 9), + (" 9 . 3 l", 9), +] +xlate_float_values = [ + (9, 9.0), + ("9", 9.0), + (9.3, 9.3), + ("9.3", 9.3), + ("9.", 9.0), + (" 9 . 3 l", 9.3), ] @pytest.mark.parametrize("value, result", xlate_values) def test_xlate(value, result): - assert comicapi.utils.xlate(**value) == result + assert comicapi.utils.xlate(value) == result + + +@pytest.mark.parametrize("value, result", xlate_float_values) +def test_xlate_float(value, result): + assert comicapi.utils.xlate_float(value) == result + + +@pytest.mark.parametrize("value, result", xlate_int_values) +def test_xlate_int(value, result): + assert comicapi.utils.xlate_int(value) == result language_values = [