Merge branch 'develop' into additional_comic_fields
This commit is contained in:
commit
ddf4407b77
6
.flake8
6
.flake8
@ -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
|
60
.github/workflows/build.yaml
vendored
60
.github/workflows/build.yaml
vendored
@ -1,8 +1,8 @@
|
||||
name: CI
|
||||
|
||||
env:
|
||||
PIP: pip
|
||||
PYTHON: python
|
||||
PKG_CONFIG_PATH: /usr/local/opt/icu4c/lib/pkgconfig
|
||||
LC_COLLATE: en_US.UTF-8
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
@ -23,24 +23,18 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: syphar/restore-virtualenv@v1.2
|
||||
id: cache-virtualenv
|
||||
|
||||
- uses: syphar/restore-pip-download-cache@v1
|
||||
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt
|
||||
python -m pip install flake8
|
||||
|
||||
- uses: reviewdog/action-setup@v1
|
||||
with:
|
||||
@ -57,61 +51,45 @@ jobs:
|
||||
os: [ubuntu-latest, macos-10.15, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: syphar/restore-virtualenv@v1.2
|
||||
id: cache-virtualenv
|
||||
|
||||
- uses: syphar/restore-pip-download-cache@v1
|
||||
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install build dependencies
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt
|
||||
python -m pip install --upgrade --upgrade-strategy eager tox
|
||||
|
||||
- name: Install Windows build dependencies
|
||||
run: |
|
||||
choco install -y zip
|
||||
if: runner.os == 'Windows'
|
||||
- name: Install macos dependencies
|
||||
run: |
|
||||
brew install icu4c pkg-config
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
# export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
# export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: Install linux dependencies
|
||||
run: |
|
||||
sudo apt-get install pkg-config libicu-dev
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2
|
||||
# export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
# export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Build and install PyPi packages
|
||||
run: |
|
||||
make clean pydist
|
||||
python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]"
|
||||
|
||||
- name: build
|
||||
run: |
|
||||
make dist
|
||||
python -m tox r -m build
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
if: runner.os != 'Linux' # linux binary currently has a segfault when running on latest fedora
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "${{ format('ComicTagger-{0}', runner.os) }}"
|
||||
path: |
|
||||
dist/*.zip
|
||||
dist/*.AppImage
|
||||
|
||||
- name: PyTest
|
||||
run: |
|
||||
python -m pytest
|
||||
python -m tox r
|
||||
|
43
.github/workflows/contributions.yaml
vendored
Normal file
43
.github/workflows/contributions.yaml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Contributions
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
contrib-readme-job:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_COMMIT_AUTHOR: github-actions[bot]
|
||||
CI_COMMIT_EMAIL: <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
CI_COMMIT_MESSAGE: Update AUTHORS
|
||||
name: A job to automate contrib in readme
|
||||
steps:
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.6
|
||||
with:
|
||||
use_username: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update AUTHORS
|
||||
run: |
|
||||
git config --global log.mailmap true
|
||||
git log --reverse '--format=%aN <%aE>' | cat -n | sort -uk2 | sort -n | cut -f2- >AUTHORS
|
||||
|
||||
- name: Commit and push AUTHORS
|
||||
run: |
|
||||
if ! git diff --exit-code; then
|
||||
git pull
|
||||
git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_COMMIT_EMAIL }}"
|
||||
git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}"
|
||||
git push
|
||||
fi
|
59
.github/workflows/package.yaml
vendored
59
.github/workflows/package.yaml
vendored
@ -1,8 +1,8 @@
|
||||
name: Package
|
||||
|
||||
env:
|
||||
PIP: pip
|
||||
PYTHON: python
|
||||
PKG_CONFIG_PATH: /usr/local/opt/icu4c/lib/pkgconfig
|
||||
LC_COLLATE: en_US.UTF-8
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@ -18,61 +18,40 @@ jobs:
|
||||
os: [ubuntu-latest, macos-10.15, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: syphar/restore-virtualenv@v1.2
|
||||
id: cache-virtualenv
|
||||
|
||||
- uses: syphar/restore-pip-download-cache@v1
|
||||
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install build dependencies
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt
|
||||
python -m pip install --upgrade --upgrade-strategy eager tox
|
||||
|
||||
- name: Install Windows build dependencies
|
||||
run: |
|
||||
choco install -y zip
|
||||
if: runner.os == 'Windows'
|
||||
- name: Install macos dependencies
|
||||
run: |
|
||||
brew install icu4c pkg-config
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
# export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
# export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: Install linux dependencies
|
||||
run: |
|
||||
sudo apt-get install pkg-config libicu-dev
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2
|
||||
# export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
# export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Build, Install and Test PyPi packages
|
||||
run: |
|
||||
make clean pydist
|
||||
python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]"
|
||||
python -m flake8
|
||||
python -m pytest
|
||||
|
||||
- name: "Publish distribution 📦 to PyPI"
|
||||
if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Linux'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: dist
|
||||
|
||||
- name: Build PyInstaller package
|
||||
run: |
|
||||
make dist
|
||||
python -m tox r
|
||||
python -m tox r -m release
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
- name: Get release name
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
@ -88,6 +67,8 @@ jobs:
|
||||
name: "${{ env.release_name }}"
|
||||
prerelease: "${{ contains(github.ref, '-') }}" # alpha-releases should be 1.3.0-alpha.x full releases should be 1.3.0
|
||||
draft: false
|
||||
# upload the single application zip file for each OS and include the wheel built on linux
|
||||
files: |
|
||||
dist/!(*Linux).zip
|
||||
dist/*.zip
|
||||
dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl
|
||||
dist/*.AppImage
|
||||
|
9
.mailmap
Normal file
9
.mailmap
Normal file
@ -0,0 +1,9 @@
|
||||
Andrew W. Buchanan <buchanan@difference.com>
|
||||
Davide Romanini <d.romanini@cineca.it> <davide.romanini@gmail.com>
|
||||
Davide Romanini <d.romanini@cineca.it> <user159033@92-63-141-211.rdns.melbourne.co.uk>
|
||||
Michael Fitzurka <MichaelFitzurka@users.noreply.github.com> <MichaelFitzurka@github.com>
|
||||
Timmy Welch <timmy@narnian.us>
|
||||
beville <beville@users.noreply.github.com> <(no author)@6c5673fe-1810-88d6-992b-cd32ca31540c>
|
||||
beville <beville@users.noreply.github.com> <beville@6c5673fe-1810-88d6-992b-cd32ca31540c>
|
||||
beville <beville@users.noreply.github.com> <beville@gmail.com@6c5673fe-1810-88d6-992b-cd32ca31540c>
|
||||
beville <beville@users.noreply.github.com> <beville@users.noreply.github.com>
|
@ -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]
|
||||
|
59
.travis.yml
59
.travis.yml
@ -1,59 +0,0 @@
|
||||
language: python
|
||||
# Only build tags
|
||||
if: type = pull_request OR tag IS present
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- /^\d+\.\d+\.\d+.*$/
|
||||
env:
|
||||
global:
|
||||
- PYTHON=python3
|
||||
- PIP=pip3
|
||||
- SETUPTOOLS_SCM_PRETEND_VERSION=$TRAVIS_TAG
|
||||
- MAKE=make
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
python: 3.8
|
||||
- name: "Python: 3.7"
|
||||
os: osx
|
||||
language: shell
|
||||
python: 3.7
|
||||
env: PYTHON=python3 PIP="python3 -m pip"
|
||||
cache:
|
||||
- directories:
|
||||
- $HOME/Library/Caches/pip
|
||||
- os: windows
|
||||
language: bash
|
||||
env: PATH=/C/Python37:/C/Python37/Scripts:$PATH MAKE=mingw32-make PIP=pip PYTHON=python
|
||||
before_install:
|
||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install -y python --version 3.7.9; choco install -y mingw zip; fi
|
||||
install:
|
||||
- $PIP install -r requirements_dev.txt
|
||||
- $PIP install -r requirements-GUI.txt
|
||||
- $PIP install -r requirements-CBR.txt
|
||||
script:
|
||||
- if [ "$TRAVIS_OS_NAME" != "linux" ]; then $MAKE dist ; fi
|
||||
|
||||
deploy:
|
||||
- name: "$TRAVIS_TAG"
|
||||
body: Released ComicTagger $TRAVIS_TAG
|
||||
provider: releases
|
||||
skip_cleanup: true
|
||||
api_key:
|
||||
secure: RgohcOJOfLhXXT12bMWaLwOqhe+ClSCYXjYuUJuWK4/E1fdd1xu1ebdQU+MI/R8cZ0Efz3sr2n3NkO/Aa8gN68xEfuF7RVRMm64P9oPrfZgGdsD6H43rU/6kN8bgaDRmCYpLTfXaJ+/gq0x1QDkhWJuceF2BYEGGvL0BvS/TUsLyjVxs8ujTplLyguXHNEv4/7Yz7SBNZZmUHjBuq/y+l8ds3ra9rSgAVAN1tMXoFKJPv+SNNkpTo5WUNMPzBnN041F1rzqHwYDLog2V7Krp9JkXzheRFdAr51/tJBYzEd8AtYVdYvaIvoO6A4PiTZ7MpsmcZZPAWqLQU00UTm/PhT/LVR+7+f8lOBG07RgNNHB+edjDRz3TAuqyuZl9wURWTZKTPuO49TkZMz7Wm0DRNZHvBm1IXLeSG7Tll2YL1+WpZNZg+Dhro2J1QD3vxDXafhMdTCB4z0q5aKpG93IT0p6oXOO0oEGOPZYbA2c5R3SXWSyqd1E1gdhbVjIZr59h++TEf1zz07tvWHqPuAF/Ly/j+dIcY2wj0EzRWaSASWgUpTnMljAkHtWhqDw4GXGDRkRUWRJl1d0/JyVqCeIdRzDQNl8/q7BcO3F1zqr1PgnYdz0lfwWxL1/ekw2vHOJE/GOdkyvX0aJrnaOV338mjJbfGHYv4ESc9ow1kdtIbiU=
|
||||
file_glob: true
|
||||
file: dist/*.zip
|
||||
draft: true
|
||||
on:
|
||||
tags: true
|
||||
condition: $TRAVIS_OS_NAME != "linux"
|
||||
- provider: pypi
|
||||
user: __token__
|
||||
password:
|
||||
secure: h+y5WkE8igf864dnsbGPFvOBkyPkuBYtnDRt+EgxHd71EZnV2YP7ns2Cx12su/SVVDdZCBlmHVtkhl6Jmqy+0rTkSYx+3mlBOqyl8Cj5+BlP/dP7Bdmhs2uLZk2YYL1avbC0A6eoNJFtCkjurnB/jCGE433rvMECWJ5x2HsQTKchCmDAEdAZbRBJrzLFsrIC+6NXW1IJZjd+OojbhLSyVar2Jr32foh6huTcBu/x278V1+zIC/Rwy3W67+3c4aZxYrI47FoYFza0jjFfr3EoSkKYUSByMTIvhWaqB2gIsF0T160jgDd8Lcgej+86ACEuG0v01VE7xoougqlOaJ94eAmapeM7oQXzekSwSAxcK3JQSfgWk/AvPhp07T4pQ8vCZmky6yqvVp1EzfKarTeub1rOnv+qo1znKLrBtOoq6t8pOAeczDdIDs51XT/hxaijpMRCM8vHxN4Kqnc4DY+3KcF7UFyH1ifQJHQe71tLBsM/GnAcJM5/3ykFVGvRJ716p4aa6IoGsdNk6bqlysNh7nURDl+bfm+CDXRkO2jkFwUFNqPHW7JwY6ZFx+b5SM3TzC3obJhfMS7OC37fo2geISOTR0xVie6NvpN6TjNAxFTfDxWJI7yH3Al2w43B3uYDd97WeiN+B+HVWtdaER87IVSRbRqFrRub+V+xrozT0y0=
|
||||
skip_existing: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: $TRAVIS_OS_NAME = "linux"
|
16
AUTHORS
Normal file
16
AUTHORS
Normal file
@ -0,0 +1,16 @@
|
||||
beville <beville@users.noreply.github.com>
|
||||
Davide Romanini <d.romanini@cineca.it>
|
||||
fcanc <f.canc@icloud.com>
|
||||
Alban Seurat <alkpone@alkpone.com>
|
||||
tlc <tlc@users.noreply.github.com>
|
||||
Marek Pawlak <francuz14@gmail.com>
|
||||
Timmy Welch <timmy@narnian.us>
|
||||
J.P. Cranford <philipcranford4@gmail.com>
|
||||
thFrgttn <39759781+thFrgttn@users.noreply.github.com>
|
||||
Andrew W. Buchanan <buchanan@difference.com>
|
||||
Michael Fitzurka <MichaelFitzurka@users.noreply.github.com>
|
||||
Richard Haussmann <richard.haussmann@gmail.com>
|
||||
Mizaki <jinxybob@hotmail.com>
|
||||
Xavier Jouvenot <x.jouvenot@gmail.com>
|
||||
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
Ben Longman <deck@steamdeck.lan>
|
@ -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 ===================
|
||||
```
|
||||
|
@ -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
|
64
Makefile
64
Makefile
@ -1,64 +0,0 @@
|
||||
PIP ?= pip3
|
||||
PYTHON ?= python3
|
||||
VERSION_STR := $(shell $(PYTHON) setup.py --version)
|
||||
|
||||
SITE_PACKAGES := $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
|
||||
PACKAGE_PATH = $(SITE_PACKAGES)/comictagger.egg-link
|
||||
|
||||
VENV := $(shell echo $${VIRTUAL_ENV-venv})
|
||||
PY3 := $(shell command -v $(PYTHON) 2> /dev/null)
|
||||
PYTHON_VENV := $(VENV)/bin/python
|
||||
INSTALL_STAMP := $(VENV)/.install.stamp
|
||||
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
PYTHON_VENV := $(VENV)/Scripts/python.exe
|
||||
OS_VERSION=win-$(PROCESSOR_ARCHITECTURE)
|
||||
APP_NAME=comictagger.exe
|
||||
FINAL_NAME=ComicTagger-$(VERSION_STR)-$(OS_VERSION).exe
|
||||
else ifeq ($(shell uname -s),Darwin)
|
||||
OS_VERSION=osx-$(shell defaults read loginwindow SystemVersionStampAsString)-$(shell uname -m)
|
||||
APP_NAME=ComicTagger.app
|
||||
FINAL_NAME=ComicTagger-$(VERSION_STR)-$(OS_VERSION).app
|
||||
else
|
||||
APP_NAME=comictagger
|
||||
FINAL_NAME=ComicTagger-$(VERSION_STR)-$(shell uname -s)
|
||||
endif
|
||||
|
||||
.PHONY: all clean pydist dist CI check
|
||||
|
||||
all: clean dist
|
||||
|
||||
$(PYTHON_VENV):
|
||||
@if [ -z $(PY3) ]; then echo "Python 3 could not be found."; exit 2; fi
|
||||
$(PY3) -m venv $(VENV)
|
||||
|
||||
clean:
|
||||
find . -maxdepth 4 -type d -name "__pycache__" -print -depth -exec rm -rf {} \;
|
||||
rm -rf $(PACKAGE_PATH) $(INSTALL_STAMP) build dist MANIFEST comictaggerlib/ctversion.py
|
||||
$(MAKE) -C mac clean
|
||||
|
||||
CI: install
|
||||
$(PYTHON_VENV) -m black .
|
||||
$(PYTHON_VENV) -m isort .
|
||||
$(PYTHON_VENV) -m flake8 .
|
||||
$(PYTHON_VENV) -m pytest
|
||||
|
||||
check: install
|
||||
$(PYTHON_VENV) -m black --check .
|
||||
$(PYTHON_VENV) -m isort --check .
|
||||
$(PYTHON_VENV) -m flake8 .
|
||||
$(PYTHON_VENV) -m pytest
|
||||
|
||||
pydist:
|
||||
$(PYTHON_VENV) -m build
|
||||
|
||||
install: $(INSTALL_STAMP)
|
||||
$(INSTALL_STAMP): $(PYTHON_VENV) requirements.txt requirements_dev.txt
|
||||
$(PYTHON_VENV) -m pip install -r requirements_dev.txt
|
||||
$(PYTHON_VENV) -m pip install -e .
|
||||
touch $(INSTALL_STAMP)
|
||||
|
||||
dist:
|
||||
pyinstaller -y comictagger.spec
|
||||
cd dist && zip -m -r $(FINAL_NAME).zip $(APP_NAME)
|
123
README.md
123
README.md
@ -35,7 +35,7 @@ For details, screen-shots, and more, visit [the Wiki](https://github.com/comicta
|
||||
|
||||
### Binaries
|
||||
|
||||
Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases).
|
||||
Windows, Linux and MacOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases).
|
||||
|
||||
Just unzip the archive in any folder and run, no additional installation steps are required.
|
||||
|
||||
@ -47,7 +47,14 @@ A pip package is provided, you can install it with:
|
||||
$ pip3 install comictagger[GUI]
|
||||
```
|
||||
|
||||
There are two optional dependencies GUI and CBR. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI]`
|
||||
There are optional dependencies. You can install the optional dependencies by specifying one or more of them in braces e.g. `comictagger[CBR,GUI]`
|
||||
|
||||
Optional dependencies:
|
||||
1. `ICU`: Ensures that comic pages are supported correctly. This should always be installed. *Currently only exists in the latest alpha release *
|
||||
1. `CBR`: Provides support for CBR/RAR files.
|
||||
1. `GUI`: Installs the GUI.
|
||||
1. `7Z`: Provides support for CB7/7Z files.
|
||||
1. `all`: Installs all of the above optional dependencies.
|
||||
|
||||
### Chocolatey installation (Windows only)
|
||||
|
||||
@ -59,5 +66,113 @@ choco install comictagger
|
||||
|
||||
1. Ensure you have python 3.9 installed
|
||||
2. Clone this repository `git clone https://github.com/comictagger/comictagger.git`
|
||||
3. `pip3 install -r requirements_dev.txt`
|
||||
7. `pip3 install .` or `pip3 install .[GUI]`
|
||||
7. `pip3 install .[ICU]` or `pip3 install .[GUI,ICU]`
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
<!-- readme: beville,davide-romanini,collaborators,contributors -start -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/beville">
|
||||
<img src="https://avatars.githubusercontent.com/u/7294848?v=4" width="100;" alt="beville"/>
|
||||
<br />
|
||||
<sub><b>beville</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/davide-romanini">
|
||||
<img src="https://avatars.githubusercontent.com/u/731199?v=4" width="100;" alt="davide-romanini"/>
|
||||
<br />
|
||||
<sub><b>davide-romanini</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/fcanc">
|
||||
<img src="https://avatars.githubusercontent.com/u/4999486?v=4" width="100;" alt="fcanc"/>
|
||||
<br />
|
||||
<sub><b>fcanc</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/lordwelch">
|
||||
<img src="https://avatars.githubusercontent.com/u/7547075?v=4" width="100;" alt="lordwelch"/>
|
||||
<br />
|
||||
<sub><b>lordwelch</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mizaki">
|
||||
<img src="https://avatars.githubusercontent.com/u/1141189?v=4" width="100;" alt="mizaki"/>
|
||||
<br />
|
||||
<sub><b>mizaki</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MichaelFitzurka">
|
||||
<img src="https://avatars.githubusercontent.com/u/27830765?v=4" width="100;" alt="MichaelFitzurka"/>
|
||||
<br />
|
||||
<sub><b>MichaelFitzurka</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/abuchanan920">
|
||||
<img src="https://avatars.githubusercontent.com/u/368793?v=4" width="100;" alt="abuchanan920"/>
|
||||
<br />
|
||||
<sub><b>abuchanan920</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/AlbanSeurat">
|
||||
<img src="https://avatars.githubusercontent.com/u/500180?v=4" width="100;" alt="AlbanSeurat"/>
|
||||
<br />
|
||||
<sub><b>AlbanSeurat</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rhaussmann">
|
||||
<img src="https://avatars.githubusercontent.com/u/7084007?v=4" width="100;" alt="rhaussmann"/>
|
||||
<br />
|
||||
<sub><b>rhaussmann</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jpcranford">
|
||||
<img src="https://avatars.githubusercontent.com/u/21347202?v=4" width="100;" alt="jpcranford"/>
|
||||
<br />
|
||||
<sub><b>jpcranford</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/PawlakMarek">
|
||||
<img src="https://avatars.githubusercontent.com/u/26022173?v=4" width="100;" alt="PawlakMarek"/>
|
||||
<br />
|
||||
<sub><b>PawlakMarek</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Xav83">
|
||||
<img src="https://avatars.githubusercontent.com/u/6787157?v=4" width="100;" alt="Xav83"/>
|
||||
<br />
|
||||
<sub><b>Xav83</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/thFrgttn">
|
||||
<img src="https://avatars.githubusercontent.com/u/39759781?v=4" width="100;" alt="thFrgttn"/>
|
||||
<br />
|
||||
<sub><b>thFrgttn</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/tlc">
|
||||
<img src="https://avatars.githubusercontent.com/u/19436?v=4" width="100;" alt="tlc"/>
|
||||
<br />
|
||||
<sub><b>tlc</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: beville,davide-romanini,collaborators,contributors -end -->
|
||||
|
@ -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
|
@ -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",
|
||||
)
|
33
build-tools/get_appimage.py
Normal file
33
build-tools/get_appimage.py
Normal 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)
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
47
build-tools/zip_artifacts.py
Normal file
47
build-tools/zip_artifacts.py
Normal 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)
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A class to encapsulate CoMet data"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -141,14 +141,14 @@ class CoMet:
|
||||
md.series = utils.xlate(get("series"))
|
||||
md.title = utils.xlate(get("title"))
|
||||
md.issue = utils.xlate(get("issue"))
|
||||
md.volume = utils.xlate(get("volume"), True)
|
||||
md.volume = utils.xlate_int(get("volume"))
|
||||
md.comments = utils.xlate(get("description"))
|
||||
md.publisher = utils.xlate(get("publisher"))
|
||||
md.language = utils.xlate(get("language"))
|
||||
md.format = utils.xlate(get("format"))
|
||||
md.page_count = utils.xlate(get("pages"), True)
|
||||
md.page_count = utils.xlate_int(get("pages"))
|
||||
md.maturity_rating = utils.xlate(get("rating"))
|
||||
md.price = utils.xlate(get("price"), is_float=True)
|
||||
md.price = utils.xlate_float(get("price"))
|
||||
md.is_version_of = utils.xlate(get("isVersionOf"))
|
||||
md.rights = utils.xlate(get("rights"))
|
||||
md.identifier = utils.xlate(get("identifier"))
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""A class to represent a single comic, be it file or folder of images"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -22,7 +22,6 @@ import shutil
|
||||
import sys
|
||||
from typing import cast
|
||||
|
||||
import natsort
|
||||
import wordninja
|
||||
|
||||
from comicapi import filenamelexer, filenameparser, utils
|
||||
@ -280,7 +279,7 @@ class ComicArchive:
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files = cast(list[str], natsort.os_sorted(files))
|
||||
files = cast(list[str], utils.os_sorted(files))
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
@ -562,13 +561,13 @@ class ComicArchive:
|
||||
)
|
||||
metadata.alternate_number = utils.xlate(p.filename_info["alternate"])
|
||||
metadata.issue = utils.xlate(p.filename_info["issue"])
|
||||
metadata.issue_count = utils.xlate(p.filename_info["issue_count"])
|
||||
metadata.issue_count = utils.xlate_int(p.filename_info["issue_count"])
|
||||
metadata.publisher = utils.xlate(p.filename_info["publisher"])
|
||||
metadata.series = utils.xlate(p.filename_info["series"])
|
||||
metadata.title = utils.xlate(p.filename_info["title"])
|
||||
metadata.volume = utils.xlate(p.filename_info["volume"])
|
||||
metadata.volume_count = utils.xlate(p.filename_info["volume_count"])
|
||||
metadata.year = utils.xlate(p.filename_info["year"])
|
||||
metadata.volume = utils.xlate_int(p.filename_info["volume"])
|
||||
metadata.volume_count = utils.xlate_int(p.filename_info["volume_count"])
|
||||
metadata.year = utils.xlate_int(p.filename_info["year"])
|
||||
|
||||
metadata.scan_info = utils.xlate(p.filename_info["remainder"])
|
||||
metadata.format = "FCBD" if p.filename_info["fcbd"] else None
|
||||
@ -583,11 +582,11 @@ class ComicArchive:
|
||||
if fnp.series:
|
||||
metadata.series = fnp.series
|
||||
if fnp.volume:
|
||||
metadata.volume = utils.xlate(fnp.volume, True)
|
||||
metadata.volume = utils.xlate_int(fnp.volume)
|
||||
if fnp.year:
|
||||
metadata.year = utils.xlate(fnp.year, True)
|
||||
metadata.year = utils.xlate_int(fnp.year)
|
||||
if fnp.issue_count:
|
||||
metadata.issue_count = utils.xlate(fnp.issue_count, True)
|
||||
metadata.issue_count = utils.xlate_int(fnp.issue_count)
|
||||
if fnp.remainder:
|
||||
metadata.scan_info = fnp.remainder
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""A class to encapsulate the ComicBookInfo data"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -85,16 +85,16 @@ class ComicBookInfo:
|
||||
metadata.title = utils.xlate(cbi["title"])
|
||||
metadata.issue = utils.xlate(cbi["issue"])
|
||||
metadata.publisher = utils.xlate(cbi["publisher"])
|
||||
metadata.month = utils.xlate(cbi["publicationMonth"], True)
|
||||
metadata.year = utils.xlate(cbi["publicationYear"], True)
|
||||
metadata.issue_count = utils.xlate(cbi["numberOfIssues"], True)
|
||||
metadata.month = utils.xlate_int(cbi["publicationMonth"])
|
||||
metadata.year = utils.xlate_int(cbi["publicationYear"])
|
||||
metadata.issue_count = utils.xlate_int(cbi["numberOfIssues"])
|
||||
metadata.comments = utils.xlate(cbi["comments"])
|
||||
metadata.genre = utils.xlate(cbi["genre"])
|
||||
metadata.volume = utils.xlate(cbi["volume"], True)
|
||||
metadata.volume_count = utils.xlate(cbi["numberOfVolumes"], True)
|
||||
metadata.volume = utils.xlate_int(cbi["volume"])
|
||||
metadata.volume_count = utils.xlate_int(cbi["numberOfVolumes"])
|
||||
metadata.language = utils.xlate(cbi["language"])
|
||||
metadata.country = utils.xlate(cbi["country"])
|
||||
metadata.critical_rating = utils.xlate(cbi["rating"], True)
|
||||
metadata.critical_rating = utils.xlate_int(cbi["rating"])
|
||||
|
||||
metadata.credits = [
|
||||
Credits(
|
||||
@ -152,16 +152,16 @@ class ComicBookInfo:
|
||||
assign("title", utils.xlate(metadata.title))
|
||||
assign("issue", utils.xlate(metadata.issue))
|
||||
assign("publisher", utils.xlate(metadata.publisher))
|
||||
assign("publicationMonth", utils.xlate(metadata.month, True))
|
||||
assign("publicationYear", utils.xlate(metadata.year, True))
|
||||
assign("numberOfIssues", utils.xlate(metadata.issue_count, True))
|
||||
assign("publicationMonth", utils.xlate_int(metadata.month))
|
||||
assign("publicationYear", utils.xlate_int(metadata.year))
|
||||
assign("numberOfIssues", utils.xlate_int(metadata.issue_count))
|
||||
assign("comments", utils.xlate(metadata.comments))
|
||||
assign("genre", utils.xlate(metadata.genre))
|
||||
assign("volume", utils.xlate(metadata.volume, True))
|
||||
assign("numberOfVolumes", utils.xlate(metadata.volume_count, True))
|
||||
assign("volume", utils.xlate_int(metadata.volume))
|
||||
assign("numberOfVolumes", utils.xlate_int(metadata.volume_count))
|
||||
assign("language", utils.xlate(utils.get_language_from_iso(metadata.language)))
|
||||
assign("country", utils.xlate(metadata.country))
|
||||
assign("rating", utils.xlate(metadata.critical_rating, True))
|
||||
assign("rating", utils.xlate_int(metadata.critical_rating))
|
||||
assign("credits", metadata.credits)
|
||||
assign("tags", list(metadata.tags))
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""A class to encapsulate ComicRack's ComicInfo.xml data"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -190,16 +190,16 @@ class ComicInfoXml:
|
||||
md.series = utils.xlate(get("Series"))
|
||||
md.title = utils.xlate(get("Title"))
|
||||
md.issue = IssueString(utils.xlate(get("Number"))).as_string()
|
||||
md.issue_count = utils.xlate(get("Count"), True)
|
||||
md.volume = utils.xlate(get("Volume"), True)
|
||||
md.issue_count = utils.xlate_int(get("Count"))
|
||||
md.volume = utils.xlate_int(get("Volume"))
|
||||
md.alternate_series = utils.xlate(get("AlternateSeries"))
|
||||
md.alternate_number = IssueString(utils.xlate(get("AlternateNumber"))).as_string()
|
||||
md.alternate_count = utils.xlate(get("AlternateCount"), True)
|
||||
md.alternate_count = utils.xlate_int(get("AlternateCount"))
|
||||
md.comments = utils.xlate(get("Summary"))
|
||||
md.notes = utils.xlate(get("Notes"))
|
||||
md.year = utils.xlate(get("Year"), True)
|
||||
md.month = utils.xlate(get("Month"), True)
|
||||
md.day = utils.xlate(get("Day"), True)
|
||||
md.year = utils.xlate_int(get("Year"))
|
||||
md.month = utils.xlate_int(get("Month"))
|
||||
md.day = utils.xlate_int(get("Day"))
|
||||
md.publisher = utils.xlate(get("Publisher"))
|
||||
md.imprint = utils.xlate(get("Imprint"))
|
||||
md.genre = utils.xlate(get("Genre"))
|
||||
@ -210,12 +210,12 @@ class ComicInfoXml:
|
||||
md.characters = utils.xlate(get("Characters"))
|
||||
md.teams = utils.xlate(get("Teams"))
|
||||
md.locations = utils.xlate(get("Locations"))
|
||||
md.page_count = utils.xlate(get("PageCount"), True)
|
||||
md.page_count = utils.xlate_int(get("PageCount"))
|
||||
md.scan_info = utils.xlate(get("ScanInformation"))
|
||||
md.story_arc = utils.xlate(get("StoryArc"))
|
||||
md.series_group = utils.xlate(get("SeriesGroup"))
|
||||
md.maturity_rating = utils.xlate(get("AgeRating"))
|
||||
md.critical_rating = utils.xlate(get("CommunityRating"), is_float=True)
|
||||
md.critical_rating = utils.xlate_float(get("CommunityRating"))
|
||||
|
||||
tmp = utils.xlate(get("BlackAndWhite"))
|
||||
if tmp is not None and tmp.casefold() in ["yes", "true", "1"]:
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
This should probably be re-written, but, well, it mostly works!
|
||||
"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -5,7 +5,7 @@ tagging schemes and databases, such as ComicVine or GCD. This makes conversion
|
||||
possible, however lossy it might be
|
||||
|
||||
"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -122,7 +122,7 @@ class GenericMetadata:
|
||||
pages: list[ImageMetadata] = dataclasses.field(default_factory=list)
|
||||
|
||||
# Some CoMet-only items
|
||||
price: str | None = None
|
||||
price: float | None = None
|
||||
is_version_of: str | None = None
|
||||
rights: str | None = None
|
||||
identifier: str | None = None
|
||||
|
@ -4,7 +4,7 @@ Class for handling the odd permutations of an 'issue number' that the
|
||||
comics industry throws at us.
|
||||
e.g.: "12", "12.1", "0", "-1", "5AU", "100-2"
|
||||
"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Some generic utilities"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -14,27 +14,50 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Iterable, Mapping
|
||||
from shutil import which # noqa: F401
|
||||
from typing import Any
|
||||
|
||||
import natsort
|
||||
import pycountry
|
||||
import rapidfuzz.fuzz
|
||||
|
||||
import comicapi.data
|
||||
|
||||
try:
|
||||
import icu
|
||||
|
||||
del icu
|
||||
icu_available = True
|
||||
except ImportError:
|
||||
icu_available = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UtilsVars:
|
||||
already_fixed_encoding = False
|
||||
def _custom_key(tup):
|
||||
lst = []
|
||||
for x in natsort.os_sort_keygen()(tup):
|
||||
ret = x
|
||||
if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "":
|
||||
ret = ("a", *x[1:])
|
||||
|
||||
lst.append(ret)
|
||||
return tuple(lst)
|
||||
|
||||
|
||||
def os_sorted(lst: Iterable) -> Iterable:
|
||||
key = _custom_key
|
||||
if icu_available or platform.system() == "Windows":
|
||||
key = natsort.os_sort_keygen()
|
||||
return sorted(lst, key=key)
|
||||
|
||||
|
||||
def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) -> str:
|
||||
@ -45,17 +68,17 @@ def combine_notes(existing_notes: str | None, new_notes: str | None, split: str)
|
||||
return (untouched_notes + "\n" + (new_notes or "")).strip()
|
||||
|
||||
|
||||
def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]:
|
||||
def parse_date_str(date_str: str | None) -> tuple[int | None, int | None, int | None]:
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
if date_str:
|
||||
parts = date_str.split("-")
|
||||
year = xlate(parts[0], True)
|
||||
year = xlate_int(parts[0])
|
||||
if len(parts) > 1:
|
||||
month = xlate(parts[1], True)
|
||||
month = xlate_int(parts[1])
|
||||
if len(parts) > 2:
|
||||
day = xlate(parts[2], True)
|
||||
day = xlate_int(parts[2])
|
||||
return day, month, year
|
||||
|
||||
|
||||
@ -65,9 +88,11 @@ def get_recursive_filelist(pathlist: list[str]) -> list[str]:
|
||||
filelist: list[str] = []
|
||||
for p in pathlist:
|
||||
if os.path.isdir(p):
|
||||
filelist.extend(x for x in glob.glob(f"{p}{os.sep}/**", recursive=True) if not os.path.isdir(x))
|
||||
elif str(p) not in filelist:
|
||||
filelist.append(str(p))
|
||||
for root, _, files in os.walk(p):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root, f))
|
||||
else:
|
||||
filelist.append(p)
|
||||
|
||||
return filelist
|
||||
|
||||
@ -82,23 +107,32 @@ def add_to_path(dirname: str) -> None:
|
||||
os.environ["PATH"] = os.pathsep.join(paths)
|
||||
|
||||
|
||||
def xlate(data: Any, is_int: bool = False, is_float: bool = False) -> Any:
|
||||
def xlate_int(data: Any) -> int | None:
|
||||
data = xlate_float(data)
|
||||
if data is None:
|
||||
return None
|
||||
return int(data)
|
||||
|
||||
|
||||
def xlate_float(data: Any) -> float | None:
|
||||
if data is None or data == "":
|
||||
return None
|
||||
i: str | int | float
|
||||
if isinstance(data, (int, float)):
|
||||
i = data
|
||||
else:
|
||||
i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890.")))
|
||||
if i == "":
|
||||
return None
|
||||
try:
|
||||
return float(i)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def xlate(data: Any) -> str | None:
|
||||
if data is None or data == "":
|
||||
return None
|
||||
if is_int or is_float:
|
||||
i: str | int | float
|
||||
if isinstance(data, (int, float)):
|
||||
i = data
|
||||
else:
|
||||
i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890.")))
|
||||
if i == "":
|
||||
return None
|
||||
try:
|
||||
if is_float:
|
||||
return float(i)
|
||||
return int(float(i))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return str(data)
|
||||
|
||||
|
@ -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()
|
5
comictaggerlib/__main__.py
Normal file
5
comictaggerlib/__main__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from comictaggerlib.main import main
|
||||
|
||||
main()
|
@ -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")
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to select from automated issue matches"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to show ID log and progress"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to confirm and set config for auto-tag"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -49,7 +49,7 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.autotag_ignore_leading_numbers_in_filename)
|
||||
self.cbxRemoveAfterSuccess.setChecked(self.config.autotag_remove_archive_after_successful_match)
|
||||
self.cbxWaitForRateLimit.setChecked(self.config.autotag_wait_and_retry_on_rate_limit)
|
||||
self.cbxAutoImprint.setChecked(self.config.talker_auto_imprint)
|
||||
self.cbxAutoImprint.setChecked(self.config.identifier_auto_imprint)
|
||||
|
||||
nlmt_tip = """<html>The <b>Name Match Ratio Threshold: Auto-Identify</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The lower
|
||||
@ -75,7 +75,7 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.remove_after_success = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.search_string = ""
|
||||
self.name_length_match_tolerance = self.config.talker_series_match_search_thresh
|
||||
self.name_length_match_tolerance = self.config.identifier_series_match_search_thresh
|
||||
self.split_words = self.cbxSplitWords.isChecked()
|
||||
|
||||
def search_string_toggle(self) -> None:
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A class to manage modifying metadata specifically for CBL/CBI"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
"""ComicTagger CLI functions"""
|
||||
#
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -40,15 +40,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CLI:
|
||||
def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]):
|
||||
def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]) -> None:
|
||||
self.config = config
|
||||
self.talkers = talkers
|
||||
self.batch_mode = False
|
||||
|
||||
def current_talker(self) -> ComicTalker:
|
||||
if self.config[0].talker_source in self.talkers:
|
||||
return self.talkers[self.config[0].talker_source]
|
||||
logger.error("Could not find the '%s' talker", self.config[0].talker_source)
|
||||
if self.config.talker_source in self.talkers:
|
||||
return self.talkers[self.config.talker_source]
|
||||
logger.error("Could not find the '%s' talker", self.config.talker_source)
|
||||
raise SystemExit(2)
|
||||
|
||||
def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata:
|
||||
@ -114,7 +114,7 @@ class CLI:
|
||||
ca = match_set.ca
|
||||
md = self.create_local_metadata(ca)
|
||||
ct_md = self.actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"])
|
||||
if self.config.talker_clear_metadata_on_import:
|
||||
if self.config.identifier_clear_metadata_on_import:
|
||||
md = ct_md
|
||||
else:
|
||||
notes = (
|
||||
@ -123,7 +123,7 @@ class CLI:
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if self.config.talker_auto_imprint:
|
||||
if self.config.identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
||||
self.actual_metadata_save(ca, md)
|
||||
@ -427,7 +427,7 @@ class CLI:
|
||||
match_results.fetch_data_failures.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if self.config.talker_clear_metadata_on_import:
|
||||
if self.config.identifier_clear_metadata_on_import:
|
||||
md = ct_md
|
||||
else:
|
||||
notes = (
|
||||
@ -436,7 +436,7 @@ class CLI:
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if self.config.talker_auto_imprint:
|
||||
if self.config.identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
@ -458,18 +458,18 @@ class CLI:
|
||||
return
|
||||
|
||||
new_ext = "" # default
|
||||
if self.config.filename_rename_set_extension_based_on_archive:
|
||||
if self.config.rename_set_extension_based_on_archive:
|
||||
new_ext = ca.extension()
|
||||
|
||||
renamer = FileRenamer(
|
||||
md,
|
||||
platform="universal" if self.config.filename_rename_strict else "auto",
|
||||
platform="universal" if self.config.rename_strict else "auto",
|
||||
replacements=self.config.rename_replacements,
|
||||
)
|
||||
renamer.set_template(self.config.filename_rename_template)
|
||||
renamer.set_issue_zero_padding(self.config.filename_rename_issue_number_padding)
|
||||
renamer.set_smart_cleanup(self.config.filename_rename_use_smart_string_cleanup)
|
||||
renamer.move = self.config.filename_rename_move_to_dir
|
||||
renamer.set_template(self.config.rename_template)
|
||||
renamer.set_issue_zero_padding(self.config.rename_issue_number_padding)
|
||||
renamer.set_smart_cleanup(self.config.rename_use_smart_string_cleanup)
|
||||
renamer.move = self.config.rename_move_to_dir
|
||||
|
||||
try:
|
||||
new_name = renamer.determine_name(ext=new_ext)
|
||||
@ -481,17 +481,13 @@ class CLI:
|
||||
"Please consult the template help in the settings "
|
||||
"and the documentation on the format at "
|
||||
"https://docs.python.org/3/library/string.html#format-string-syntax",
|
||||
self.config.filename_rename_template,
|
||||
self.config.rename_template,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Formatter failure: %s metadata: %s", self.config.filename_rename_template, renamer.metadata
|
||||
)
|
||||
logger.exception("Formatter failure: %s metadata: %s", self.config.rename_template, renamer.metadata)
|
||||
|
||||
folder = get_rename_dir(
|
||||
ca, self.config.filename_rename_dir if self.config.filename_rename_move_to_dir else None
|
||||
)
|
||||
folder = get_rename_dir(ca, self.config.rename_dir if self.config.rename_move_to_dir else None)
|
||||
|
||||
full_path = folder / new_name
|
||||
|
||||
|
@ -4,7 +4,7 @@ Display cover images from either a local archive, or from Comic Vine.
|
||||
TODO: This should be re-factored using subclasses!
|
||||
"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -60,7 +60,7 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
URLMode = 1
|
||||
DataMode = 3
|
||||
|
||||
image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray)
|
||||
image_fetch_complete = QtCore.pyqtSignal(str, QtCore.QByteArray)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -201,7 +201,7 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]:
|
||||
self.load_url()
|
||||
elif self.mode == CoverImageWidget.DataMode:
|
||||
self.cover_remote_fetch_complete(self.imageData)
|
||||
self.cover_remote_fetch_complete("", self.imageData)
|
||||
else:
|
||||
self.load_page()
|
||||
|
||||
@ -238,7 +238,9 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
self.cover_fetcher.fetch(self.url_list[self.imageIndex])
|
||||
|
||||
# called when the image is done loading from internet
|
||||
def cover_remote_fetch_complete(self, image_data: bytes) -> None:
|
||||
def cover_remote_fetch_complete(self, url: str, image_data: bytes) -> None:
|
||||
if url and url not in self.url_list:
|
||||
return
|
||||
img = get_qimage_from_data(image_data)
|
||||
self.current_pixmap = QtGui.QPixmap.fromImage(img)
|
||||
self.set_display_pixmap()
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to edit credits"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""CLI settings for ComicTagger"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -236,7 +236,7 @@ def register_commands(parser: settngs.Manager) -> None:
|
||||
|
||||
def register_commandline_settings(parser: settngs.Manager) -> None:
|
||||
parser.add_group("commands", register_commands, True)
|
||||
parser.add_group("runtime", register_settings)
|
||||
parser.add_persistent_group("runtime", register_settings)
|
||||
|
||||
|
||||
def validate_commandline_settings(
|
||||
|
@ -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:
|
||||
|
@ -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]:
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to confirm and set options for export to zip"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Functions for renaming files based on metadata"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -20,6 +20,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import string
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from pathvalidate import Platform, normalize_platform, sanitize_filename
|
||||
@ -94,8 +95,8 @@ class MetadataFormatter(string.Formatter):
|
||||
def _vformat(
|
||||
self,
|
||||
format_string: str,
|
||||
args: list[Any],
|
||||
kwargs: dict[str, Any],
|
||||
args: Sequence[Any],
|
||||
kwargs: Mapping[str, Any],
|
||||
used_args: set[Any],
|
||||
recursion_depth: int,
|
||||
auto_arg_index: int = 0,
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQt5 widget for managing list of comic archive files"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A class to manage fetching and caching of images by URL"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -41,7 +41,7 @@ class ImageFetcherException(Exception):
|
||||
...
|
||||
|
||||
|
||||
def fetch_complete(image_data: bytes | QtCore.QByteArray) -> None:
|
||||
def fetch_complete(url: str, image_data: bytes | QtCore.QByteArray) -> None:
|
||||
...
|
||||
|
||||
|
||||
@ -79,22 +79,22 @@ class ImageFetcher:
|
||||
# first look in the DB
|
||||
image_data = self.get_image_from_cache(url)
|
||||
# Async for retrieving covers seems to work well
|
||||
if blocking: # if blocking or not qt_available:
|
||||
if blocking or not qt_available:
|
||||
if not image_data:
|
||||
try:
|
||||
image_data = requests.get(url, headers={"user-agent": "comictagger/" + ctversion.version}).content
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
except Exception as e:
|
||||
logger.exception("Fetching url failed: %s")
|
||||
raise ImageFetcherException("Network Error!") from e
|
||||
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
ImageFetcher.image_fetch_complete(url, image_data)
|
||||
return image_data
|
||||
|
||||
if qt_available:
|
||||
# if we found it, just emit the signal asap
|
||||
if image_data:
|
||||
ImageFetcher.image_fetch_complete(QtCore.QByteArray(image_data))
|
||||
ImageFetcher.image_fetch_complete(url, QtCore.QByteArray(image_data))
|
||||
return b""
|
||||
|
||||
# didn't find it. look online
|
||||
@ -110,9 +110,9 @@ class ImageFetcher:
|
||||
image_data = reply.readAll()
|
||||
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
self.add_image_to_cache(reply.request().url().toString(), image_data)
|
||||
|
||||
ImageFetcher.image_fetch_complete(image_data)
|
||||
ImageFetcher.image_fetch_complete(reply.request().url().toString(), image_data)
|
||||
|
||||
def create_image_db(self) -> None:
|
||||
# this will wipe out any existing version
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A class to manage creating image content hashes, and calculate hamming distances"""
|
||||
#
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 widget to display a popup image"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A class to automatically identify a comic archive"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to select specific issue from list"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -106,7 +106,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
# now that the list has been sorted, find the initial record, and
|
||||
# select it
|
||||
if self.initial_id is None:
|
||||
if not self.initial_id:
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to a text file or log"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A python app to (automatically) tag comic archives"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -17,8 +17,12 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import locale
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import settngs
|
||||
@ -48,6 +52,49 @@ except Exception:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def _lang_code_mac() -> str:
|
||||
"""
|
||||
stolen from https://github.com/mu-editor/mu
|
||||
Returns the user's language preference as defined in the Language & Region
|
||||
preference pane in macOS's System Preferences.
|
||||
"""
|
||||
|
||||
# Uses the shell command `defaults read -g AppleLocale` that prints out a
|
||||
# language code to standard output. Assumptions about the command:
|
||||
# - It exists and is in the shell's PATH.
|
||||
# - It accepts those arguments.
|
||||
# - It returns a usable language code.
|
||||
#
|
||||
# Reference documentation:
|
||||
# - The man page for the `defaults` command on macOS.
|
||||
# - The macOS underlying API:
|
||||
# https://developer.apple.com/documentation/foundation/nsuserdefaults.
|
||||
|
||||
lang_detect_command = "defaults read -g AppleLocale"
|
||||
|
||||
status, output = subprocess.getstatusoutput(lang_detect_command)
|
||||
if status == 0:
|
||||
# Command was successful.
|
||||
lang_code = output
|
||||
else:
|
||||
logging.warning("Language detection command failed: %r", output)
|
||||
lang_code = ""
|
||||
|
||||
return lang_code
|
||||
|
||||
|
||||
def configure_locale() -> None:
|
||||
if sys.platform == "darwin" and "LANG" not in os.environ:
|
||||
code = _lang_code_mac()
|
||||
if code != "":
|
||||
os.environ["LANG"] = f"{code}.utf-8"
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
sys.stdout.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined]
|
||||
sys.stderr.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined]
|
||||
sys.stdin.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def update_publishers(config: settngs.Config[settngs.Namespace]) -> None:
|
||||
json_file = config[0].runtime_config.user_config_dir / "publishers.json"
|
||||
if json_file.exists():
|
||||
@ -66,6 +113,7 @@ class App:
|
||||
self.config_load_success = False
|
||||
|
||||
def run(self) -> None:
|
||||
configure_locale()
|
||||
conf = self.initialize()
|
||||
self.initialize_dirs(conf.config)
|
||||
self.load_plugins(conf)
|
||||
@ -119,6 +167,15 @@ class App:
|
||||
# config already loaded
|
||||
error = None
|
||||
|
||||
talkers = ctsettings.talkers
|
||||
del ctsettings.talkers
|
||||
|
||||
if len(talkers) < 1:
|
||||
error = error = (
|
||||
f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details",
|
||||
True,
|
||||
)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
logger.debug("Installed Packages")
|
||||
@ -134,7 +191,10 @@ class App:
|
||||
|
||||
# manage the CV API key
|
||||
# None comparison is used so that the empty string can unset the value
|
||||
if self.config[0].talker_comicvine_cv_api_key is not None or self.config[0].talker_comicvine_cv_url is not None:
|
||||
if not error and (
|
||||
self.config[0].talker_comicvine_comicvine_key is not None
|
||||
or self.config[0].talker_comicvine_comicvine_url is not None
|
||||
):
|
||||
settings_path = self.config[0].runtime_config.user_config_dir / "settings.json"
|
||||
if self.config_load_success:
|
||||
self.manager.save_file(self.config[0], settings_path)
|
||||
@ -150,9 +210,6 @@ class App:
|
||||
True,
|
||||
)
|
||||
|
||||
talkers = ctsettings.talkers
|
||||
del ctsettings.talkers
|
||||
|
||||
if self.config[0].runtime_no_gui:
|
||||
if error and error[1]:
|
||||
print(f"A fatal error occurred please check the log for more information: {error[0]}") # noqa: T201
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to select from automated issue matches"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -11,7 +11,7 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
|
||||
)
|
||||
"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to show pages of a comic archive"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQt5 widget for editing the page list info"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 class to load a page image from a ComicArchive in a background thread"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQt5 dialog to show ID log and progress"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to confirm rename"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -39,7 +39,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
comic_archive_list: list[ComicArchive],
|
||||
data_style: int,
|
||||
config: settngs.Config[settngs.Namespace],
|
||||
talker: ComicTalker,
|
||||
talkers: dict[str, ComicTalker],
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@ -55,7 +55,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
)
|
||||
|
||||
self.config = config
|
||||
self.talker = talker
|
||||
self.talkers = talkers
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
self.rename_list: list[str] = []
|
||||
@ -73,7 +73,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
self.renamer.replacements = self.config[0].rename_replacements
|
||||
|
||||
new_ext = ca.path.suffix # default
|
||||
if self.config[0].filename_rename_set_extension_based_on_archive:
|
||||
if self.config[0].rename_set_extension_based_on_archive:
|
||||
new_ext = ca.extension()
|
||||
|
||||
if md is None:
|
||||
@ -160,7 +160,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modify_settings(self) -> None:
|
||||
settingswin = SettingsWindow(self, self.config, self.talker)
|
||||
settingswin = SettingsWindow(self, self.config, self.talkers)
|
||||
settingswin.setModal(True)
|
||||
settingswin.show_rename_tab()
|
||||
settingswin.exec()
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to select specific series/volume from list"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -103,7 +103,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
series_name: str,
|
||||
issue_number: str,
|
||||
year: int | None,
|
||||
issue_count: int,
|
||||
issue_count: int | None,
|
||||
cover_index_list: list[int],
|
||||
comic_archive: ComicArchive | None,
|
||||
config: settngs.Namespace,
|
||||
@ -151,7 +151,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.progdialog: QtWidgets.QProgressDialog | None = None
|
||||
self.search_thread: SearchThread | None = None
|
||||
|
||||
self.use_filter = self.config.talker_always_use_publisher_filter
|
||||
self.use_filter = self.config.identifier_always_use_publisher_filter
|
||||
|
||||
# Load to retrieve settings
|
||||
self.talker = talker
|
||||
@ -303,7 +303,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
if found_match is not None:
|
||||
self.iddialog.accept()
|
||||
|
||||
self.series_id = utils.xlate(found_match["series_id"])
|
||||
self.series_id = utils.xlate(found_match["series_id"]) or ""
|
||||
self.issue_number = found_match["issue_number"]
|
||||
self.select_by_id()
|
||||
self.show_issues()
|
||||
@ -326,6 +326,8 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.issue_number = selector.issue_number
|
||||
self.issue_id = selector.issue_id
|
||||
self.accept()
|
||||
else:
|
||||
self.imageWidget.update_content()
|
||||
|
||||
def select_by_id(self) -> None:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
@ -336,7 +338,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
def perform_query(self, refresh: bool = False) -> None:
|
||||
self.search_thread = SearchThread(
|
||||
self.talker, self.series_name, refresh, self.literal, self.config.talker_series_match_search_thresh
|
||||
self.talker, self.series_name, refresh, self.literal, self.config.identifier_series_match_search_thresh
|
||||
)
|
||||
self.search_thread.searchComplete.connect(self.search_complete)
|
||||
self.search_thread.progressUpdate.connect(self.search_progress_update)
|
||||
@ -403,7 +405,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
# compare as str in case extra chars ie. '1976?'
|
||||
# - missing (none) values being converted to 'None' - consistent with prior behaviour in v1.2.3
|
||||
# sort by start_year if set
|
||||
if self.config.talker_sort_series_by_year:
|
||||
if self.config.identifier_sort_series_by_year:
|
||||
try:
|
||||
self.ct_search_results = sorted(
|
||||
self.ct_search_results,
|
||||
@ -421,7 +423,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
logger.exception("bad data error sorting results by count_of_issues")
|
||||
|
||||
# move sanitized matches to the front
|
||||
if self.config.talker_exact_series_matches_first:
|
||||
if self.config.identifier_exact_series_matches_first:
|
||||
try:
|
||||
sanitized = utils.sanitize_title(self.series_name, False).casefold()
|
||||
sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold()
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A PyQT4 dialog to enter app settings"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -25,8 +25,10 @@ from typing import Any
|
||||
import settngs
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
import comictaggerlib.ui.talkeruigenerator
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import md_test
|
||||
from comictaggerlib import ctsettings
|
||||
from comictaggerlib.ctversion import version
|
||||
from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements
|
||||
from comictaggerlib.imagefetcher import ImageFetcher
|
||||
@ -131,7 +133,7 @@ Spider-Geddon #1 - New Players; Check In
|
||||
|
||||
class SettingsWindow(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talker: ComicTalker
|
||||
self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talkers: dict[str, ComicTalker]
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@ -142,7 +144,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
)
|
||||
|
||||
self.config = config
|
||||
self.talker = talker
|
||||
self.talkers = talkers
|
||||
self.name = "Settings"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
@ -185,16 +187,21 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.leRenameTemplate.setToolTip(f"<pre>{html.escape(template_tooltip)}</pre>")
|
||||
self.rename_error: Exception | None = None
|
||||
|
||||
self.sources: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs(
|
||||
self.tComicTalkers, self.config, self.talkers
|
||||
)
|
||||
self.connect_signals()
|
||||
self.settings_to_form()
|
||||
self.rename_test()
|
||||
self.dir_test()
|
||||
|
||||
# Set General as start tab
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
|
||||
def connect_signals(self) -> None:
|
||||
self.btnBrowseRar.clicked.connect(self.select_rar)
|
||||
self.btnClearCache.clicked.connect(self.clear_cache)
|
||||
self.btnResetSettings.clicked.connect(self.reset_settings)
|
||||
self.btnTestKey.clicked.connect(self.test_api_key)
|
||||
self.btnTemplateHelp.clicked.connect(self.show_template_help)
|
||||
self.cbxMoveFiles.clicked.connect(self.dir_test)
|
||||
self.leDirectory.textEdited.connect(self.dir_test)
|
||||
@ -223,7 +230,6 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.btnRemoveValueReplacement.clicked.disconnect()
|
||||
self.btnResetSettings.clicked.disconnect()
|
||||
self.btnTemplateHelp.clicked.disconnect()
|
||||
self.btnTestKey.clicked.disconnect()
|
||||
self.cbxChangeExtension.clicked.disconnect()
|
||||
self.cbxComplicatedParser.clicked.disconnect()
|
||||
self.cbxMoveFiles.clicked.disconnect()
|
||||
@ -301,7 +307,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
else:
|
||||
self.leRarExePath.setEnabled(False)
|
||||
self.sbNameMatchIdentifyThresh.setValue(self.config[0].identifier_series_match_identify_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.config[0].talker_series_match_search_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.config[0].identifier_series_match_search_thresh)
|
||||
self.tePublisherFilter.setPlainText("\n".join(self.config[0].identifier_publisher_filter))
|
||||
|
||||
self.cbxCheckForNewVersion.setChecked(self.config[0].general_check_for_new_version)
|
||||
@ -312,16 +318,10 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.cbxRemovePublisher.setChecked(self.config[0].filename_remove_publisher)
|
||||
self.switch_parser()
|
||||
|
||||
self.cbxUseSeriesStartAsVolume.setChecked(self.config[0].talker_comicvine_cv_use_series_start_as_volume)
|
||||
self.cbxClearFormBeforePopulating.setChecked(self.config[0].talker_clear_form_before_populating)
|
||||
self.cbxRemoveHtmlTables.setChecked(self.config[0].talker_comicvine_cv_remove_html_tables)
|
||||
|
||||
self.cbxUseFilter.setChecked(self.config[0].talker_always_use_publisher_filter)
|
||||
self.cbxSortByYear.setChecked(self.config[0].talker_sort_series_by_year)
|
||||
self.cbxExactMatches.setChecked(self.config[0].talker_exact_series_matches_first)
|
||||
|
||||
self.leKey.setText(self.config[0].talker_comicvine_cv_api_key)
|
||||
self.leURL.setText(self.config[0].talker_comicvine_cv_url)
|
||||
self.cbxClearFormBeforePopulating.setChecked(self.config[0].identifier_clear_form_before_populating)
|
||||
self.cbxUseFilter.setChecked(self.config[0].identifier_always_use_publisher_filter)
|
||||
self.cbxSortByYear.setChecked(self.config[0].identifier_sort_series_by_year)
|
||||
self.cbxExactMatches.setChecked(self.config[0].identifier_exact_series_matches_first)
|
||||
|
||||
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary)
|
||||
self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags)
|
||||
@ -349,6 +349,10 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
table.removeRow(i)
|
||||
for row, replacement in enumerate(replacments):
|
||||
self.insertRow(table, row, replacement)
|
||||
|
||||
# Set talker values
|
||||
comictaggerlib.ui.talkeruigenerator.settings_to_talker_form(self.sources, self.config)
|
||||
|
||||
self.connect_signals()
|
||||
|
||||
def get_replacements(self) -> Replacements:
|
||||
@ -418,7 +422,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.config[0].general_check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
|
||||
self.config[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
|
||||
self.config[0].talker_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
|
||||
self.config[0].identifier_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
|
||||
self.config[0].identifier_publisher_filter = [
|
||||
x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip()
|
||||
]
|
||||
@ -428,21 +432,10 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.config[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked()
|
||||
self.config[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked()
|
||||
|
||||
self.config[0].talker_comicvine_cv_use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.config[0].talker_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.config[0].talker_comicvine_cv_remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
|
||||
|
||||
self.config[0].talker_always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.config[0].talker_sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.config[0].talker_exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
|
||||
if self.leKey.text().strip():
|
||||
self.config[0].talker_comicvine_cv_api_key = self.leKey.text().strip()
|
||||
self.talker.api_key = self.config[0].talker_comicvine_cv_api_key
|
||||
|
||||
if self.leURL.text().strip():
|
||||
self.config[0].talker_comicvine_cv_url = self.leURL.text().strip()
|
||||
self.talker.api_url = self.config[0].talker_comicvine_cv_url
|
||||
self.config[0].identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.config[0].identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.config[0].identifier_sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.config[0].identifier_exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
|
||||
self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
@ -464,6 +457,9 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.config[0].rename_strict = self.cbxRenameStrict.isChecked()
|
||||
self.config[0].rename_replacements = self.get_replacements()
|
||||
|
||||
# Read settings from talker tabs
|
||||
comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config)
|
||||
|
||||
self.update_talkers_config()
|
||||
|
||||
settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json")
|
||||
@ -471,9 +467,9 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
def update_talkers_config(self) -> None:
|
||||
cfg = settngs.normalize_config(self.config, True, True)
|
||||
if f"talker_{self.talker.id}" in cfg[0]:
|
||||
self.talker.parse_settings(cfg[0][f"talker_{self.talker.id}"])
|
||||
ctsettings.talkers = self.talkers
|
||||
self.config = ctsettings.plugin.validate_talker_settings(self.config)
|
||||
del ctsettings.talkers
|
||||
|
||||
def select_rar(self) -> None:
|
||||
self.select_file(self.leRarExePath, "RAR")
|
||||
@ -483,12 +479,6 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache()
|
||||
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
|
||||
|
||||
def test_api_key(self) -> None:
|
||||
if self.talker.check_api_key(self.leKey.text().strip(), self.leURL.text().strip()):
|
||||
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
|
||||
|
||||
def reset_settings(self) -> None:
|
||||
self.config = settngs.get_namespace(settngs.defaults(self.config[1]))
|
||||
self.settings_to_form()
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""The main window of the ComicTagger app"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -898,13 +898,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
md.is_empty = False
|
||||
md.alternate_number = IssueString(self.leAltIssueNum.text()).as_string()
|
||||
md.issue = IssueString(self.leIssueNum.text()).as_string()
|
||||
md.issue_count = utils.xlate(self.leIssueCount.text(), True)
|
||||
md.volume = utils.xlate(self.leVolumeNum.text(), True)
|
||||
md.volume_count = utils.xlate(self.leVolumeCount.text(), True)
|
||||
md.month = utils.xlate(self.lePubMonth.text(), True)
|
||||
md.year = utils.xlate(self.lePubYear.text(), True)
|
||||
md.day = utils.xlate(self.lePubDay.text(), True)
|
||||
md.alternate_count = utils.xlate(self.leAltIssueCount.text(), True)
|
||||
md.issue_count = utils.xlate_int(self.leIssueCount.text())
|
||||
md.volume = utils.xlate_int(self.leVolumeNum.text())
|
||||
md.volume_count = utils.xlate_int(self.leVolumeCount.text())
|
||||
md.month = utils.xlate_int(self.lePubMonth.text())
|
||||
md.year = utils.xlate_int(self.lePubYear.text())
|
||||
md.day = utils.xlate_int(self.lePubDay.text())
|
||||
md.alternate_count = utils.xlate_int(self.leAltIssueCount.text())
|
||||
|
||||
md.series = self.leSeries.text()
|
||||
md.title = self.leTitle.text()
|
||||
@ -915,7 +915,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
md.notes = self.teNotes.toPlainText()
|
||||
md.maturity_rating = self.cbMaturityRating.currentText()
|
||||
|
||||
md.critical_rating = utils.xlate(self.dsbCriticalRating.cleanText(), is_float=True)
|
||||
md.critical_rating = utils.xlate_float(self.dsbCriticalRating.cleanText())
|
||||
if md.critical_rating == 0.0:
|
||||
md.critical_rating = None
|
||||
|
||||
@ -1027,9 +1027,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
QtWidgets.QMessageBox.information(self, "Online Search", "Need to enter a series name to search.")
|
||||
return
|
||||
|
||||
year = utils.xlate(self.lePubYear.text(), True)
|
||||
year = utils.xlate_int(self.lePubYear.text())
|
||||
|
||||
issue_count = utils.xlate(self.leIssueCount.text(), True)
|
||||
issue_count = utils.xlate_int(self.leIssueCount.text())
|
||||
|
||||
cover_index_list = self.metadata.get_cover_page_index_list()
|
||||
selector = SeriesSelectionWindow(
|
||||
@ -1071,7 +1071,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if self.config[0].cbl_apply_transform_on_import:
|
||||
new_metadata = CBLTransformer(new_metadata, self.config[0]).apply()
|
||||
|
||||
if self.config[0].talker_clear_form_before_populating:
|
||||
if self.config[0].identifier_clear_form_before_populating:
|
||||
self.clear_form()
|
||||
|
||||
notes = (
|
||||
@ -1354,7 +1354,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
QtWidgets.QMessageBox.warning(self, self.tr("Web Link"), self.tr("Web Link is invalid."))
|
||||
|
||||
def show_settings(self) -> None:
|
||||
settingswin = SettingsWindow(self, self.config, self.current_talker())
|
||||
settingswin = SettingsWindow(self, self.config, self.talkers)
|
||||
settingswin.setModal(True)
|
||||
settingswin.exec()
|
||||
settingswin.result()
|
||||
@ -1785,7 +1785,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if self.config[0].talker_auto_imprint:
|
||||
if self.config[0].identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
||||
if not ca.write_metadata(md, self.save_data_style):
|
||||
@ -2047,7 +2047,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if self.dirty_flag_verification(
|
||||
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
|
||||
):
|
||||
dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.current_talker())
|
||||
dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.talkers)
|
||||
dlg.setModal(True)
|
||||
if dlg.exec() and self.comic_archive is not None:
|
||||
self.fileSelectionList.update_selected_rows()
|
||||
|
@ -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><html><head/><body><p>These settings are for the automatic issue identifier which searches online for matches. </p><p>Hover the mouse over an entry field for more info.</p></body></html></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><html><head/><body><p>These settings are for the automatic issue identifier which searches online for matches. </p><p>Hover the mouse over an entry field for more info.</p></body></html></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><html><head/><body><p>A personal API key from <a href="http://www.comicvine.com/api/"><span style=" text-decoration: underline; color:#0000ff;">Comic Vine</span></a> is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.</p></body></html></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>
|
||||
|
238
comictaggerlib/ui/talkeruigenerator.py
Normal file
238
comictaggerlib/ui/talkeruigenerator.py
Normal 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
|
@ -1,6 +1,6 @@
|
||||
"""Version checker"""
|
||||
#
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""A python class to manage caching of data from Comic Vine"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -411,7 +411,12 @@ class ComicCacher:
|
||||
)
|
||||
|
||||
# now process the results
|
||||
|
||||
credits = []
|
||||
try:
|
||||
for credit in json.loads(row[13]):
|
||||
credits.append(Credit(**credit))
|
||||
except Exception:
|
||||
logger.exception("credits failed")
|
||||
record = ComicIssue(
|
||||
id=row[1],
|
||||
name=row[2],
|
||||
@ -425,7 +430,7 @@ class ComicCacher:
|
||||
alt_image_urls=row[10].strip().splitlines(),
|
||||
characters=row[11].strip().splitlines(),
|
||||
locations=row[12].strip().splitlines(),
|
||||
credits=json.loads(row[13]),
|
||||
credits=credits,
|
||||
teams=row[14].strip().splitlines(),
|
||||
story_arcs=row[15].strip().splitlines(),
|
||||
genres=row[16].strip().splitlines(),
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -21,6 +21,7 @@ import settngs
|
||||
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictalker.resulttypes import ComicIssue, ComicSeries
|
||||
from comictalker.talker_utils import fix_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -107,18 +108,21 @@ class ComicTalker:
|
||||
|
||||
name: str = "Example"
|
||||
id: str = "example"
|
||||
logo_url: str = "https://example.com/logo.png"
|
||||
website: str = "https://example.com/"
|
||||
website: str = "https://example.com"
|
||||
logo_url: str = f"{website}/logo.png"
|
||||
attribution: str = f"Metadata provided by <a href='{website}'>{name}</a>"
|
||||
|
||||
def __init__(self, version: str, cache_folder: pathlib.Path) -> None:
|
||||
self.cache_folder = cache_folder
|
||||
self.version = version
|
||||
self.api_key: str = ""
|
||||
self.api_url: str = ""
|
||||
self.api_key = self.default_api_key = ""
|
||||
self.api_url = self.default_api_url = ""
|
||||
|
||||
def register_settings(self, parser: settngs.Manager) -> None:
|
||||
"""Allows registering settings using the settngs package with an argparse like interface"""
|
||||
"""
|
||||
Allows registering settings using the settngs package with an argparse like interface.
|
||||
The order that settings are declared is the order they will be displayed.
|
||||
"""
|
||||
return None
|
||||
|
||||
def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]:
|
||||
@ -126,12 +130,29 @@ class ComicTalker:
|
||||
settings is a dictionary of settings defined in register_settings.
|
||||
It is only guaranteed that the settings defined in register_settings will be present.
|
||||
"""
|
||||
if settings[f"{self.id}_key"]:
|
||||
self.api_key = settings[f"{self.id}_key"]
|
||||
if settings[f"{self.id}_url"]:
|
||||
self.api_url = fix_url(settings[f"{self.id}_url"])
|
||||
|
||||
settings[f"{self.id}_url"] = self.api_url
|
||||
|
||||
if self.api_key == "":
|
||||
self.api_key = self.default_api_key
|
||||
if self.api_url == "":
|
||||
self.api_url = self.default_api_url
|
||||
return settings
|
||||
|
||||
def check_api_key(self, key: str, url: str) -> bool:
|
||||
def check_api_key(self, url: str, key: str) -> tuple[str, bool]:
|
||||
"""
|
||||
This function should return true if the given api key and url are valid.
|
||||
If the Talker does not use an api key it should validate that the url works.
|
||||
This function should return (msg, True) if the given API key and URL are valid,
|
||||
where msg is a message to display to the user.
|
||||
|
||||
This function should return (msg, False) if the given API key or URL are not valid,
|
||||
where msg is a message to display to the user.
|
||||
|
||||
If the Talker does not use an API key it should validate that the URL works.
|
||||
If the Talker does not use an API key or URL it should check that the source is available.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -146,10 +167,14 @@ class ComicTalker:
|
||||
"""
|
||||
This function should return a list of series that match the given series name
|
||||
according to the source the Talker uses.
|
||||
|
||||
Sanitizing the series name is the responsibility of the talker.
|
||||
|
||||
If `literal` == True then it is requested that no filtering or
|
||||
transformation/sanitizing of the title or results be performed by the talker.
|
||||
|
||||
A sensible amount of results should be returned.
|
||||
|
||||
For example the `ComicVineTalker` stops requesting new pages after the results
|
||||
become too different from the `series_name` by use of the `titles_match` function
|
||||
provided by the `comicapi.utils` module, and only allows a maximum of 5 pages
|
||||
@ -183,7 +208,9 @@ class ComicTalker:
|
||||
"""
|
||||
This function should return a single issue for each series id in
|
||||
the `series_id_list` and it should match the issue_number.
|
||||
|
||||
Preferably it should also only return issues published in the given `year`.
|
||||
|
||||
If there is no year given (`year` == None) or the Talker does not have issue publication info
|
||||
return the results unfiltered.
|
||||
"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -14,7 +14,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import posixpath
|
||||
import re
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
@ -26,6 +28,13 @@ from comictalker.resulttypes import ComicIssue
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fix_url(url: str) -> str:
|
||||
tmp_url = urlsplit(url)
|
||||
# joinurl only works properly if there is a trailing slash
|
||||
tmp_url = tmp_url._replace(path=posixpath.normpath(tmp_url.path) + "/")
|
||||
return tmp_url.geturl()
|
||||
|
||||
|
||||
def map_comic_issue_to_metadata(
|
||||
issue_results: ComicIssue, source: str, remove_html_tables: bool = False, use_year_volume: bool = False
|
||||
) -> GenericMetadata:
|
||||
@ -49,7 +58,7 @@ def map_comic_issue_to_metadata(
|
||||
if issue_results.cover_date:
|
||||
metadata.day, metadata.month, metadata.year = utils.parse_date_str(issue_results.cover_date)
|
||||
elif issue_results.series.start_year:
|
||||
metadata.year = utils.xlate(issue_results.series.start_year, True)
|
||||
metadata.year = utils.xlate_int(issue_results.series.start_year)
|
||||
|
||||
metadata.comments = cleanup_html(issue_results.description, remove_html_tables)
|
||||
if use_year_volume:
|
||||
@ -95,11 +104,11 @@ def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]:
|
||||
year = None
|
||||
if date_str:
|
||||
parts = date_str.split("-")
|
||||
year = utils.xlate(parts[0], True)
|
||||
year = utils.xlate_int(parts[0])
|
||||
if len(parts) > 1:
|
||||
month = utils.xlate(parts[1], True)
|
||||
month = utils.xlate_int(parts[1])
|
||||
if len(parts) > 2:
|
||||
day = utils.xlate(parts[2], True)
|
||||
day = utils.xlate_int(parts[2])
|
||||
return day, month, year
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
ComicVine information source
|
||||
"""
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
# Copyright 2012-2014 ComicTagger Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -22,7 +22,7 @@ import logging
|
||||
import pathlib
|
||||
import time
|
||||
from typing import Any, Callable, Generic, TypeVar
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
import settngs
|
||||
@ -156,78 +156,86 @@ CV_STATUS_RATELIMIT = 107
|
||||
class ComicVineTalker(ComicTalker):
|
||||
name: str = "Comic Vine"
|
||||
id: str = "comicvine"
|
||||
logo_url: str = "https://comicvine.gamespot.com/a/bundles/comicvinesite/images/logo.png"
|
||||
website: str = "https://comicvine.gamespot.com/"
|
||||
website: str = "https://comicvine.gamespot.com"
|
||||
logo_url: str = f"{website}/a/bundles/comicvinesite/images/logo.png"
|
||||
attribution: str = f"Metadata provided by <a href='{website}'>{name}</a>"
|
||||
|
||||
def __init__(self, version: str, cache_folder: pathlib.Path):
|
||||
super().__init__(version, cache_folder)
|
||||
# Default settings
|
||||
self.api_url: str = "https://comicvine.gamespot.com/api"
|
||||
self.api_key: str = "27431e6787042105bd3e47e169a624521f89f3a4"
|
||||
self.default_api_url = self.api_url = f"{self.website}/api/"
|
||||
self.default_api_key = self.api_key = "27431e6787042105bd3e47e169a624521f89f3a4"
|
||||
self.remove_html_tables: bool = False
|
||||
self.use_series_start_as_volume: bool = False
|
||||
self.wait_on_ratelimit: bool = False
|
||||
|
||||
tmp_url = urlsplit(self.api_url)
|
||||
|
||||
# joinurl only works properly if there is a trailing slash
|
||||
if tmp_url.path and tmp_url.path[-1] != "/":
|
||||
tmp_url = tmp_url._replace(path=tmp_url.path + "/")
|
||||
|
||||
self.api_url = tmp_url.geturl()
|
||||
|
||||
# NOTE: This was hardcoded before which is why it isn't in settings
|
||||
self.wait_on_ratelimit_time: int = 20
|
||||
|
||||
def register_settings(self, parser: settngs.Manager) -> None:
|
||||
parser.add_setting("--cv-api-key", help="Use the given Comic Vine API Key.")
|
||||
parser.add_setting("--cv-url", help="Use the given Comic Vine URL.")
|
||||
parser.add_setting("--cv-use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--cv-wait-on-ratelimit", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting(
|
||||
"--cv-use-series-start-as-volume",
|
||||
default=False,
|
||||
action=argparse.BooleanOptionalAction,
|
||||
display_name="Use series start as volume",
|
||||
help="Use the series start year as the volume number",
|
||||
)
|
||||
parser.add_setting(
|
||||
"--cv-wait-on-ratelimit",
|
||||
default=False,
|
||||
action=argparse.BooleanOptionalAction,
|
||||
display_name="Wait on ratelimit",
|
||||
help="Wait when the rate limit is hit",
|
||||
)
|
||||
parser.add_setting(
|
||||
"--cv-remove-html-tables",
|
||||
default=False,
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Removes html tables instead of converting them to text.",
|
||||
display_name="Remove HTML tables",
|
||||
help="Removes html tables instead of converting them to text",
|
||||
)
|
||||
# The empty string being the default allows this setting to be unset, allowing the default to change
|
||||
parser.add_setting(
|
||||
f"--{self.id}-key",
|
||||
default="",
|
||||
display_name="API Key",
|
||||
help=f"Use the given Comic Vine API Key. (default: {self.default_api_key})",
|
||||
)
|
||||
parser.add_setting(
|
||||
f"--{self.id}-url",
|
||||
default="",
|
||||
display_name="API URL",
|
||||
help=f"Use the given Comic Vine URL. (default: {self.default_api_url})",
|
||||
)
|
||||
|
||||
def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]:
|
||||
if settings["cv_api_key"]:
|
||||
self.api_key = settings["cv_api_key"]
|
||||
if settings["cv_url"]:
|
||||
tmp_url = urlsplit(settings["cv_url"])
|
||||
# joinurl only works properly if there is a trailing slash
|
||||
if tmp_url.path and tmp_url.path[-1] != "/":
|
||||
tmp_url = tmp_url._replace(path=tmp_url.path + "/")
|
||||
|
||||
self.api_url = tmp_url.geturl()
|
||||
settings = super().parse_settings(settings)
|
||||
|
||||
self.use_series_start_as_volume = settings["cv_use_series_start_as_volume"]
|
||||
self.wait_on_ratelimit = settings["cv_wait_on_ratelimit"]
|
||||
self.remove_html_tables = settings["cv_remove_html_tables"]
|
||||
return settngs
|
||||
return settings
|
||||
|
||||
def check_api_key(self, key: str, url: str) -> bool:
|
||||
def check_api_key(self, url: str, key: str) -> tuple[str, bool]:
|
||||
url = talker_utils.fix_url(url)
|
||||
if not url:
|
||||
url = self.api_url
|
||||
url = self.default_api_url
|
||||
try:
|
||||
tmp_url = urlsplit(url)
|
||||
if tmp_url.path and tmp_url.path[-1] != "/":
|
||||
tmp_url = tmp_url._replace(path=tmp_url.path + "/")
|
||||
url = tmp_url.geturl()
|
||||
test_url = urljoin(url, "issue/1/")
|
||||
|
||||
cv_response: CVResult = requests.get(
|
||||
test_url,
|
||||
headers={"user-agent": "comictagger/" + self.version},
|
||||
params={"api_key": key, "format": "json", "field_list": "name"},
|
||||
params={"api_key": key or self.default_api_key, "format": "json", "field_list": "name"},
|
||||
).json()
|
||||
|
||||
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
|
||||
return cv_response["status_code"] != 100
|
||||
if cv_response["status_code"] != 100:
|
||||
return "The API key is valid", True
|
||||
else:
|
||||
return "The API key is INVALID!", False
|
||||
except Exception:
|
||||
return False
|
||||
return "Failed to connect to the URL!", False
|
||||
|
||||
def search_for_series(
|
||||
self,
|
||||
@ -385,7 +393,7 @@ class ComicVineTalker(ComicTalker):
|
||||
series_filter += str(vid) + "|"
|
||||
flt = f"volume:{series_filter},issue_number:{issue_number}" # CV uses volume to mean series
|
||||
|
||||
int_year = utils.xlate(year, True)
|
||||
int_year = utils.xlate_int(year)
|
||||
if int_year is not None:
|
||||
flt += f",cover_date:{int_year}-1-1|{int_year + 1}-1-1"
|
||||
|
||||
@ -493,7 +501,7 @@ class ComicVineTalker(ComicTalker):
|
||||
else:
|
||||
image_url = record["image"].get("super_url", "")
|
||||
|
||||
start_year = utils.xlate(record.get("start_year", ""), True)
|
||||
start_year = utils.xlate_int(record.get("start_year", ""))
|
||||
|
||||
aliases = record.get("aliases") or ""
|
||||
|
||||
|
@ -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.
|
@ -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>
|
@ -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"
|
@ -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.
|
50
localefix.py
50
localefix.py
@ -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]
|
@ -1 +0,0 @@
|
||||
py7zr
|
@ -1 +0,0 @@
|
||||
unrar-cffi>=0.2.2
|
@ -1 +0,0 @@
|
||||
PyQt5
|
@ -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
|
@ -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
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
"""Print out a line-by-line list of basic tag info from all comics"""
|
||||
|
||||
# Copyright 2012 Anthony Beville
|
||||
# Copyright 2012 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -4,7 +4,7 @@ Make some tree structures and symbolic links to comic files based on metadata
|
||||
organizing by date and series, in different trees
|
||||
"""
|
||||
|
||||
# Copyright 2012 Anthony Beville
|
||||
# Copyright 2012 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,9 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
"""Moves comic files based on metadata organizing in a tree by Publisher/Series (Volume)"""
|
||||
|
||||
# This script is based on make_links.py by Anthony Beville
|
||||
|
||||
# Copyright 2015 Fabio Cancedda, Anthony Beville
|
||||
# Copyright 2015 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
"""Fix the comic file names using a list of transforms"""
|
||||
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -5,7 +5,7 @@ and deleted. Walks recursively through the given folders. Originals
|
||||
are kept in a sub-folder at the level of the original
|
||||
"""
|
||||
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
"""Reduce the image size of pages in the comic archive"""
|
||||
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""Test archive cover against Comic Vine for a given issue ID
|
||||
"""
|
||||
|
||||
# Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 ComicTagger Authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
282
setup.cfg
Normal file
282
setup.cfg
Normal 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
|
88
setup.py
88
setup.py
@ -1,89 +1,5 @@
|
||||
# Setup file for comictagger python source (no wheels yet)
|
||||
#
|
||||
# An entry point script called "comictagger" will be created
|
||||
#
|
||||
# Currently commented out, an experiment at desktop integration.
|
||||
# It seems that post installation tweaks are broken by wheel files.
|
||||
# Kept here for further research
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
def read(fname: str) -> str:
|
||||
"""
|
||||
Read the contents of a file.
|
||||
Parameters
|
||||
----------
|
||||
fname : str
|
||||
Path to file.
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
File contents.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
install_requires = read("requirements.txt").splitlines()
|
||||
|
||||
# Dynamically determine extra dependencies
|
||||
extras_require = {}
|
||||
extra_req_files = glob.glob("requirements-*.txt")
|
||||
for extra_req_file in extra_req_files:
|
||||
name = os.path.splitext(extra_req_file)[0].replace("requirements-", "", 1)
|
||||
extras_require[name] = read(extra_req_file).splitlines()
|
||||
|
||||
# If there are any extras, add a catch-all case that includes everything.
|
||||
# This assumes that entries in extras_require are lists (not single strings),
|
||||
# and that there are no duplicated packages across the extras.
|
||||
if extras_require:
|
||||
extras_require["all"] = sorted({x for v in extras_require.values() for x in v})
|
||||
|
||||
|
||||
setup(
|
||||
name="comictagger",
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
python_requires=">=3.9",
|
||||
description="A cross-platform GUI/CLI app for writing metadata to comic archives",
|
||||
author="ComicTagger team",
|
||||
author_email="comictagger@gmail.com",
|
||||
url="https://github.com/comictagger/comictagger",
|
||||
packages=find_packages(exclude=["tests", "testing"]),
|
||||
package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]},
|
||||
entry_points={
|
||||
"console_scripts": ["comictagger=comictaggerlib.main:main"],
|
||||
"pyinstaller40": ["hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs"],
|
||||
"comicapi.archiver": [
|
||||
"zip = comicapi.archivers.zip:ZipArchiver",
|
||||
"sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver",
|
||||
"rar = comicapi.archivers.rar:RarArchiver",
|
||||
"folder = comicapi.archivers.folder:FolderArchiver",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Environment :: MacOS X",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Other/Nonlisted Topic",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
],
|
||||
keywords=["comictagger", "comics", "comic", "metadata", "tagging", "tagger"],
|
||||
license="Apache License 2.0",
|
||||
long_description=read("README.md"),
|
||||
long_description_content_type="text/markdown",
|
||||
)
|
||||
setup()
|
||||
|
@ -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 = [
|
||||
|
Loading…
x
Reference in New Issue
Block a user