Merge branch 'develop' into additional_comic_fields

This commit is contained in:
Mizaki 2023-05-12 22:41:38 +01:00
commit ddf4407b77
93 changed files with 1476 additions and 1103 deletions

View File

@ -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

View File

@ -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

43
.github/workflows/contributions.yaml vendored Normal file
View File

@ -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

View File

@ -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

9
.mailmap Normal file
View File

@ -0,0 +1,9 @@
Andrew W. Buchanan <buchanan@difference.com>
Davide Romanini <d.romanini@cineca.it> <davide.romanini@gmail.com>
Davide Romanini <d.romanini@cineca.it> <user159033@92-63-141-211.rdns.melbourne.co.uk>
Michael Fitzurka <MichaelFitzurka@users.noreply.github.com> <MichaelFitzurka@github.com>
Timmy Welch <timmy@narnian.us>
beville <beville@users.noreply.github.com> <(no author)@6c5673fe-1810-88d6-992b-cd32ca31540c>
beville <beville@users.noreply.github.com> <beville@6c5673fe-1810-88d6-992b-cd32ca31540c>
beville <beville@users.noreply.github.com> <beville@gmail.com@6c5673fe-1810-88d6-992b-cd32ca31540c>
beville <beville@users.noreply.github.com> <beville@users.noreply.github.com>

View File

@ -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]

View File

@ -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"

16
AUTHORS Normal file
View File

@ -0,0 +1,16 @@
beville <beville@users.noreply.github.com>
Davide Romanini <d.romanini@cineca.it>
fcanc <f.canc@icloud.com>
Alban Seurat <alkpone@alkpone.com>
tlc <tlc@users.noreply.github.com>
Marek Pawlak <francuz14@gmail.com>
Timmy Welch <timmy@narnian.us>
J.P. Cranford <philipcranford4@gmail.com>
thFrgttn <39759781+thFrgttn@users.noreply.github.com>
Andrew W. Buchanan <buchanan@difference.com>
Michael Fitzurka <MichaelFitzurka@users.noreply.github.com>
Richard Haussmann <richard.haussmann@gmail.com>
Mizaki <jinxybob@hotmail.com>
Xavier Jouvenot <x.jouvenot@gmail.com>
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Ben Longman <deck@steamdeck.lan>

View File

@ -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<br>
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<br>
black: formats all of the code consistently so there are no surprises<br>
flake8: checks for code quality and style (warns for unused imports and similar issues)<br>
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 ===================
```

View File

@ -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

View File

@ -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)

123
README.md
View File

@ -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
<!-- readme: beville,davide-romanini,collaborators,contributors -start -->
<table>
<tr>
<td align="center">
<a href="https://github.com/beville">
<img src="https://avatars.githubusercontent.com/u/7294848?v=4" width="100;" alt="beville"/>
<br />
<sub><b>beville</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/davide-romanini">
<img src="https://avatars.githubusercontent.com/u/731199?v=4" width="100;" alt="davide-romanini"/>
<br />
<sub><b>davide-romanini</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/fcanc">
<img src="https://avatars.githubusercontent.com/u/4999486?v=4" width="100;" alt="fcanc"/>
<br />
<sub><b>fcanc</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lordwelch">
<img src="https://avatars.githubusercontent.com/u/7547075?v=4" width="100;" alt="lordwelch"/>
<br />
<sub><b>lordwelch</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/mizaki">
<img src="https://avatars.githubusercontent.com/u/1141189?v=4" width="100;" alt="mizaki"/>
<br />
<sub><b>mizaki</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/MichaelFitzurka">
<img src="https://avatars.githubusercontent.com/u/27830765?v=4" width="100;" alt="MichaelFitzurka"/>
<br />
<sub><b>MichaelFitzurka</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/abuchanan920">
<img src="https://avatars.githubusercontent.com/u/368793?v=4" width="100;" alt="abuchanan920"/>
<br />
<sub><b>abuchanan920</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/AlbanSeurat">
<img src="https://avatars.githubusercontent.com/u/500180?v=4" width="100;" alt="AlbanSeurat"/>
<br />
<sub><b>AlbanSeurat</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/rhaussmann">
<img src="https://avatars.githubusercontent.com/u/7084007?v=4" width="100;" alt="rhaussmann"/>
<br />
<sub><b>rhaussmann</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jpcranford">
<img src="https://avatars.githubusercontent.com/u/21347202?v=4" width="100;" alt="jpcranford"/>
<br />
<sub><b>jpcranford</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/PawlakMarek">
<img src="https://avatars.githubusercontent.com/u/26022173?v=4" width="100;" alt="PawlakMarek"/>
<br />
<sub><b>PawlakMarek</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Xav83">
<img src="https://avatars.githubusercontent.com/u/6787157?v=4" width="100;" alt="Xav83"/>
<br />
<sub><b>Xav83</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/thFrgttn">
<img src="https://avatars.githubusercontent.com/u/39759781?v=4" width="100;" alt="thFrgttn"/>
<br />
<sub><b>thFrgttn</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/tlc">
<img src="https://avatars.githubusercontent.com/u/19436?v=4" width="100;" alt="tlc"/>
<br />
<sub><b>tlc</b></sub>
</a>
</td></tr>
</table>
<!-- readme: beville,davide-romanini,collaborators,contributors -end -->

View File

@ -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

View File

@ -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",
)

View File

@ -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)

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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"))

View File

@ -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

View File

@ -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))

View File

@ -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"]:

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1,5 @@
from __future__ import annotations
from comictaggerlib.main import main
main()

View File

@ -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")

View File

@ -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.

View File

@ -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.

View File

@ -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 = """<html>The <b>Name Match Ratio Threshold: Auto-Identify</b> 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:

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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.

View File

@ -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(

View File

@ -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:

View File

@ -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]:

View File

@ -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.

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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()):

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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()

View File

@ -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()

View File

@ -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"<pre>{html.escape(template_tooltip)}</pre>")
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()

View File

@ -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()

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>702</width>
<height>513</height>
<height>559</height>
</rect>
</property>
<property name="windowTitle">
@ -28,7 +28,7 @@
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
<number>3</number>
</property>
<widget class="QWidget" name="tGeneral">
<attribute name="title">
@ -136,24 +136,14 @@
<string>Searching</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These settings are for the automatic issue identifier which searches online for matches. &lt;/p&gt;&lt;p&gt;Hover the mouse over an entry field for more info.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line_2">
<item row="3" column="0">
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="6" column="0">
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
@ -252,6 +242,44 @@
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These settings are for the automatic issue identifier which searches online for matches. &lt;/p&gt;&lt;p&gt;Hover the mouse over an entry field for more info.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxExactMatches">
<property name="text">
<string>Initially show Series Name exact matches first</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="cbxSortByYear">
<property name="text">
<string>Initially sort Series search results by Starting Year instead of No. Issues</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
<property name="text">
<string>Clear Form Before Importing Comic Vine data</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tFilenameParser">
@ -308,197 +336,11 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tComicVine">
<widget class="QWidget" name="tComicTalkers">
<attribute name="title">
<string>Comic Vine</string>
<string>Metadata Sources</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="grpBoxCVTop">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
<property name="text">
<string>Use Series Start Date as Volume</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
<property name="text">
<string>Clear Form Before Importing Comic Vine data</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxRemoveHtmlTables">
<property name="text">
<string>Remove HTML tables from CV summary field</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxSortByYear">
<property name="text">
<string>Initially sort Series search results by Starting Year instead of No. Issues</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxExactMatches">
<property name="text">
<string>Initially show Series Name exact matches first</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="grpBoxKey">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="lblKeyHelp">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A personal API key from &lt;a href=&quot;http://www.comicvine.com/api/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Comic Vine&lt;/span&gt;&lt;/a&gt; is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnTestKey">
<property name="text">
<string>Test Key</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leKey">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="readOnly">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblKey">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Comic Vine API Key</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblURL">
<property name="text">
<string>Comic Vine URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="leURL"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
<layout class="QVBoxLayout" name="verticalLayout_4"/>
</widget>
<widget class="QWidget" name="tCBL">
<attribute name="title">
@ -774,25 +616,16 @@
<property name="text">
<string>Find</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Replacement</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Strict Only</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
</widget>
</item>
@ -819,25 +652,16 @@
<property name="text">
<string>Find</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Replacement</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Strict Only</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
</widget>
</item>

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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(),

View File

@ -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 <a href='{website}'>{name}</a>"
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.
"""

View File

@ -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

View File

@ -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 <a href='{website}'>{name}</a>"
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 ""

View File

@ -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.

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>main.sh</string>
<key>CFBundleIconFile</key>
<string>app.icns</string>
<key>CFBundleIdentifier</key>
<string>org.comictagger.comictagger</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ComicTagger</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>%%CTVERSION%%</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>%%CTVERSION%%</string>
<key>NSAppleScriptEnabled</key>
<string>YES</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@ -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"

View File

@ -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.

View File

@ -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]

View File

@ -1 +0,0 @@
py7zr

View File

@ -1 +0,0 @@
unrar-cffi>=0.2.2

View File

@ -1 +0,0 @@
PyQt5

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

282
setup.cfg Normal file
View File

@ -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

View File

@ -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()

View File

@ -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 = [