Compare commits

...

54 Commits

Author SHA1 Message Date
30f1db1c73 Update requirements and Linux build dependencies 2023-04-26 14:46:18 -07:00
ff15bff94c Fix pypi upload 2023-04-25 16:26:05 -07:00
83aabfd9c3 Upgrade pre-commit 2023-04-25 16:11:19 -07:00
d3ff40c249 Only update the image in CoverImageWidget if the url matches the current url
This fixes an issue causing the first issue cover to show when using the auto-identify feature
Fixes #455
2023-04-25 16:00:08 -07:00
c07e1c4168 Add additional typing 2023-04-25 16:00:06 -07:00
1dc93c351d Update settngs to typed version fixes #453 2023-04-25 16:00:04 -07:00
f94c9ef857 Update appimage step
Fix platform case
Remove icu check from appimage step as ComicTagger is not installed
Add appimagetool to allowed commands
Fix appimage paths
2023-04-25 16:00:02 -07:00
14fa70e608 Separate xlate into separate functions based on return type fixes #454 2023-04-25 15:55:27 -07:00
ec65132cf2 Mark mypy as optional 2023-04-23 02:01:41 -07:00
941bbf545f Remove extraneous if 2023-04-23 01:52:56 -07:00
afdb08fa15 Fix package.yaml 2023-04-23 01:49:42 -07:00
c4b7411261 Use tox for building 2023-04-23 01:31:44 -07:00
5b3e9c9026 Switch to rarfile for rar/cbr support 2023-04-23 00:48:13 -07:00
e70c47d12a Make PyICU optional
Update README.md
2023-04-23 00:48:11 -07:00
c1aba269a9 Revert "Make PyICU optional"
This reverts commit bf55037690.
2023-04-22 21:28:14 -07:00
bf55037690 Make PyICU optional
Fix more locale issues
Update README.md
2023-04-18 21:03:50 -07:00
e2dfcc91ce Revert get_recursive_filelist Fixes #449 2023-04-13 20:58:30 -06:00
33796aa475 Fix #447 2023-04-06 10:48:40 -07:00
428879120a Merge branch 'mizaki-talkeruigen_fix' into develop 2023-02-28 11:49:27 -08:00
f0b9bc6c77 Missed name changes from options move 2023-02-28 15:37:52 +00:00
6133b886fb String widget fix-fix 2023-02-28 15:06:59 +00:00
dacd767162 String widget fix 2023-02-28 14:59:58 +00:00
4d90417ecf Update AUTHORS 2023-02-28 06:31:07 +00:00
c3e889279b Fix EOF 2023-02-27 22:30:31 -08:00
9bf998ca9e Remove check_api_url and fix docstrings 2023-02-27 22:29:23 -08:00
5b2a06870a Fix talker settings validation 2023-02-27 22:21:56 -08:00
fca5818874 Merge branch 'mizaki-talker_settings_generator' into develop 2023-02-27 22:20:53 -08:00
eaf0ef2f1b Fix Makefile dependencies
Remove dist/appimage before copy to prevent issues with 2nd run
Add dist/appimagetool target so that the appimage tool is downloaded once
2023-02-27 22:12:12 -08:00
09fb34c5ff Merge branch 'bmfrosty-feature/add-appimage-support' into develop 2023-02-27 22:01:13 -08:00
924467cc57 Add AppImage Support 2023-02-26 22:12:50 -08:00
2611c284b8 Revert "docs(contributor): contrib-readme-action has updated readme"
This reverts commit aba59bdbfe.
2023-02-24 13:23:29 +00:00
b4a3e8c2ee Add missing tool tips to labels
Change metadata select label
Use named tuple for talker tabs
Retrun a string and bool for api check
2023-02-24 00:06:48 +00:00
118429f84c Change source term to metadata
Generate API text field in their own function
API tests return string message of result
Add help to text field lables
2023-02-23 00:42:48 +00:00
8b9332e150 Fix linux build 2023-02-21 20:00:47 -08:00
5b5a483e25 Fix api key test button generation 2023-02-21 00:58:13 +00:00
33ea8da5bc Merge branch 'develop' into talker_settings_generator
# Conflicts:
#	comictaggerlib/settingswindow.py
#	comictalker/talkers/comicvine.py
2023-02-21 00:50:06 +00:00
aba59bdbfe docs(contributor): contrib-readme-action has updated readme 2023-02-21 00:43:46 +00:00
316bd52f21 Use currentData for combo box 2023-02-21 00:42:11 +00:00
59893b1d1c Fix optoin.type ifs 2023-02-21 00:38:13 +00:00
fb83863654 Update plugin settings
Make "runtime" a persistent group, allows normalizing without losing validation
Simplify archiver setting generation
Generate options for setting a url and key for all talkers
Return validated talker settings
Require that the talker id must match the entry point name
Add api_url and api_key as default attributes on talkers
Add default handling of api_url and api_key to register_settings
Update settngs to 0.6.2 to be able to add settings to a group and
  use the display_name attribute
Error if no talkers are loaded
Update talker entry point to comictagger.talker
2023-02-20 16:02:15 -08:00
f131c650fb Merge branch 'mizaki-talker_entry_points' into develop 2023-02-20 14:27:09 -08:00
f439797b03 Use new display_name from settngs. Add source combobox getting and setting and add to sources dict of widgets. 2023-02-20 18:45:39 +00:00
bd5e23f93f Add another test case for format_internal_name 2023-02-20 00:44:51 +00:00
fefb3ce6cd Remove general tab from talker tab and use base tab from settings window. Additional clean up. 2023-02-19 23:33:22 +00:00
a24bd1c719 Generate talker general tab programatically. Move search options to search tab. 2023-02-18 17:16:56 +00:00
02fd8beda8 Use None as parent for api and url message boxes
Rename test_api_key and test_api_url to api_key_btn_connect and api_url_btn_connect
Make separate function to set form values, called in settings_to_form
Change isinstance to is
Call findChildren only once
2023-02-18 01:15:46 +00:00
628dd5e456 Fix actions failure when there are no new contributors 2023-02-17 13:43:41 -08:00
c437532622 Merge branch 'mizaki-cache_role_fix' into develop 2023-02-17 10:21:54 -08:00
0714b94ca1 Restrict contributions updates to only run on pushes to develop 2023-02-17 10:16:21 -08:00
b727b1288d Apply credit datatype to person data from cache 2023-02-15 17:05:14 +00:00
2fde11a704 Test for menu generator format_internal_name 2023-02-14 01:47:32 +00:00
6a6a3320cb Move talker settings menu generator to a separate file 2023-02-14 01:32:56 +00:00
83a8d5d5e1 Generate settings tabs for each talker 2023-02-11 01:18:56 +00:00
4b3b9d8691 Entry points for talkers 2023-02-10 21:16:35 +00:00
66 changed files with 1260 additions and 1059 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

View File

@ -2,7 +2,7 @@ name: Contributions
on:
push:
branches:
- '**'
- 'develop'
tags-ignore:
- '**'
@ -34,8 +34,10 @@ jobs:
- name: Commit and push AUTHORS
run: |
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
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

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"

View File

@ -13,3 +13,4 @@ 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)

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,8 +66,7 @@ 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -378,7 +378,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],
@ -392,7 +397,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(),
complete=bool(row[16]),

View File

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

@ -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:
@ -47,7 +56,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:
@ -82,11 +91,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

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

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