Compare commits
6 Commits
1.4.4-alph
...
1.1.16-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0484d05462 | ||
|
|
0766bf7064 | ||
|
|
7e8fc143fd | ||
|
|
43a913294e | ||
|
|
70e28c7863 | ||
|
|
14713d8ad0 |
4
.flake8
4
.flake8
@@ -1,4 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
extend-ignore = E203, E501, E722
|
||||
extend-exclude = venv, scripts, build, dist
|
||||
63
.github/workflows/build.yaml
vendored
63
.github/workflows/build.yaml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
os: [ubuntu-latest, macos-10.15, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install -r requirements_dev.txt
|
||||
python3 -m pip install -r requirements.txt
|
||||
for requirement in requirements-*.txt; do
|
||||
python3 -m pip install -r "$requirement"
|
||||
done
|
||||
shell: bash
|
||||
- name: Install Windows dependencies
|
||||
run: |
|
||||
choco install -y zip
|
||||
if: runner.os == 'Windows'
|
||||
- name: build
|
||||
run: |
|
||||
make pydist
|
||||
make dist
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
if: runner.os != 'Linux' # linux binary currently has a segfault when running on latest fedora
|
||||
with:
|
||||
name: "${{ format('ComicTagger-{0}', runner.os) }}"
|
||||
path: dist/*.zip
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: "${{ contains(github.ref, '-') }}" # alpha-releases should be 1.3.0-alpha.x full releases should be 1.3.0
|
||||
draft: true
|
||||
files: dist/*.zip
|
||||
- name: "Publish distribution 📦 to PyPI"
|
||||
if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Linux'
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: piprelease
|
||||
160
.gitignore
vendored
160
.gitignore
vendored
@@ -1,157 +1,3 @@
|
||||
# generated by setuptools_scm
|
||||
ctversion.py
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
|
||||
### Other editors
|
||||
.*.swp
|
||||
nbproject/
|
||||
.vscode
|
||||
|
||||
comictaggerlib/_version.py
|
||||
*.exe
|
||||
*.zip
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
/.idea/
|
||||
/nbproject/
|
||||
*.pyc
|
||||
|
||||
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"
|
||||
137
CONTRIBUTING.md
137
CONTRIBUTING.md
@@ -1,137 +0,0 @@
|
||||
# How to contribute
|
||||
|
||||
If your not sure what you can do or you need to ask a question or just want to talk about ComicTagger head over to the [discussions tab](https://github.com/comictagger/comictagger/discussions/categories/general) and start a discussion
|
||||
|
||||
## Tests
|
||||
|
||||
We have tests written using pytest! Some of them even pass! If you are contributing code any tests you can write are appreciated.
|
||||
|
||||
A great place to start is extending the tests that are already made.
|
||||
|
||||
For example the file tests/filenames.py has lists of filenames to be parsed in the format:
|
||||
```py
|
||||
pytest.param(
|
||||
"Star Wars - War of the Bounty Hunters - IG-88 (2021) (Digital) (Kileko-Empire).cbz",
|
||||
"number ends series, no-issue",
|
||||
{
|
||||
"issue": "",
|
||||
"series": "Star Wars - War of the Bounty Hunters - IG-88",
|
||||
"volume": "",
|
||||
"year": "2021",
|
||||
"remainder": "(Digital) (Kileko-Empire)",
|
||||
"issue_count": "",
|
||||
},
|
||||
marks=pytest.mark.xfail,
|
||||
)
|
||||
```
|
||||
|
||||
A test consists of 3-4 parts
|
||||
1. The filename to be parsed
|
||||
2. The reason it might fail
|
||||
3. What the result of parsing the filename should be
|
||||
4. `marks=pytest.mark.xfail` This marks the test as expected to fail
|
||||
|
||||
If you are not comfortable creating a pull request you can [open an issue](https://github.com/comictagger/comictagger/issues/new/choose) or [start a discussion](https://github.com/comictagger/comictagger/discussions/new)
|
||||
|
||||
## Submitting changes
|
||||
|
||||
Please open a [GitHub Pull Request](https://github.com/comictagger/comictagger/pull/new/develop) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). When you send a pull request, we will love you forever if you include tests. We can always use more test coverage. Please run the code tools below and make sure all of your commits are atomic (one feature per commit).
|
||||
|
||||
## Contributing Code
|
||||
|
||||
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 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`
|
||||
|
||||
1. Clone the repository
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
python3 -m venv --system-site-packages venv
|
||||
```
|
||||
|
||||
3. Activate the virtual env:
|
||||
```
|
||||
. venv/bin/activate
|
||||
```
|
||||
or if on windows PowerShell
|
||||
```
|
||||
. venv/bin/activate.ps1
|
||||
```
|
||||
|
||||
4. install dependencies:
|
||||
```bash
|
||||
pip install -r requirements_dev.txt -r requirements.txt
|
||||
# if installing optional dependencies
|
||||
pip install -r requirements-GUI.txt -r requirements-CBR.txt
|
||||
```
|
||||
|
||||
5. install ComicTagger
|
||||
```
|
||||
pip install .
|
||||
```
|
||||
|
||||
6. (optionall) run pytest to ensure that their are no failures (xfailed means expected failure)
|
||||
```
|
||||
$ 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 ===================
|
||||
```
|
||||
|
||||
7. Make your changes
|
||||
8. run code tools and correct any issues
|
||||
```bash
|
||||
black .
|
||||
isort .
|
||||
flake8 .
|
||||
pytest
|
||||
```
|
||||
|
||||
black: formats all of the code consistently so there are no surprises<br>
|
||||
isort: sorts imports so that you can always find where an import is located<br>
|
||||
flake8: checks for code quality and style (warns for unused imports and similar issues)<br>
|
||||
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 +1,4 @@
|
||||
include README.md
|
||||
include README.txt
|
||||
include release_notes.txt
|
||||
include requirements.txt
|
||||
recursive-include scripts *.py *.txt
|
||||
recursive-include desktop-integration *
|
||||
include windows/app.ico
|
||||
include mac/app.icns
|
||||
recursive-include scripts *.py *.txt
|
||||
130
Makefile
130
Makefile
@@ -1,85 +1,61 @@
|
||||
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-$(VERSION_STR).dist-info
|
||||
|
||||
VENV := $(shell echo $${VIRTUAL_ENV-venv})
|
||||
PY3 := $(shell command -v $(PYTHON) 2> /dev/null)
|
||||
PYTHON_VENV := $(VENV)/bin/python
|
||||
INSTALL_STAMP := $(VENV)/.install.stamp
|
||||
INSTALL_GUI_STAMP := $(VENV)/.install-GUI.stamp
|
||||
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
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)
|
||||
endif
|
||||
|
||||
.PHONY: all clean pydist upload dist CI check run
|
||||
|
||||
all: clean dist
|
||||
|
||||
$(PYTHON_VENV):
|
||||
@if [ -z $(PY3) ]; then echo "Python 3 could not be found."; exit 2; fi
|
||||
$(PY3) -m venv --system-site-packages $(VENV)
|
||||
TAGGER_BASE ?= $(HOME)/Dropbox/tagger/comictagger
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
|
||||
PASSWORD := $(shell cat $(TAGGER_BASE)/project_password.txt)
|
||||
UPLOAD_TOOL := $(TAGGER_BASE)/google/googlecode_upload.py
|
||||
all: clean
|
||||
|
||||
clean:
|
||||
find . -type d -name "__pycache__" | xargs rm -rf {};
|
||||
rm -rf $(INSTALL_STAMP)
|
||||
rm -rf *~ *.pyc *.pyo
|
||||
rm -rf scripts/*.pyc
|
||||
cd comictaggerlib; rm -f *~ *.pyc *.pyo
|
||||
rm -rf dist MANIFEST
|
||||
$(MAKE) -C mac clean
|
||||
rm -rf *.deb
|
||||
rm -rf logdict*.log
|
||||
make -C mac clean
|
||||
make -C windows clean
|
||||
rm -rf build
|
||||
rm comictaggerlib/ctversion.py
|
||||
|
||||
CI: ins
|
||||
black .
|
||||
isort .
|
||||
flake8 .
|
||||
pytest
|
||||
|
||||
check: install
|
||||
$(VENV)/bin/black --check .
|
||||
$(VENV)/bin/isort --check .
|
||||
$(VENV)/bin/flake8 .
|
||||
$(VENV)/bin/pytest
|
||||
|
||||
pydist: CI
|
||||
make clean
|
||||
mkdir -p piprelease
|
||||
rm -f comictagger-$(VERSION_STR).zip
|
||||
$(PYTHON) setup.py sdist --formats=gztar
|
||||
mv dist/comictagger-$(VERSION_STR).tar.gz piprelease
|
||||
rm -rf comictagger.egg-info dist
|
||||
pydist:
|
||||
mkdir -p release
|
||||
rm -f release/*.zip
|
||||
python setup.py sdist --formats=zip #,gztar
|
||||
mv dist/comictagger-$(VERSION_STR).zip release
|
||||
@echo When satisfied with release, do this:
|
||||
@echo make svn_tag
|
||||
|
||||
remove_test_install:
|
||||
sudo rm -rf /usr/local/bin/comictagger.py
|
||||
sudo rm -rf /usr/local/lib/python2.7/dist-packages/comictagger*
|
||||
|
||||
#deb:
|
||||
# fpm -s python -t deb \
|
||||
# -n 'comictagger' \
|
||||
# --category 'utilities' \
|
||||
# --maintainer 'comictagger@gmail.com' \
|
||||
# --after-install debian_scripts/after_install.sh \
|
||||
# --before-remove debian_scripts/before_remove.sh \
|
||||
# -d 'python >= 2.6' \
|
||||
# -d 'python < 2.8' \
|
||||
# -d 'python-imaging' \
|
||||
# -d 'python-bs4' \
|
||||
# --deb-suggests 'rar' \
|
||||
# --deb-suggests 'unrar-free' \
|
||||
# --python-install-bin /usr/share/comictagger \
|
||||
# --python-install-lib /usr/share/comictagger \
|
||||
# setup.py
|
||||
#
|
||||
# # For now, don't require PyQt, since command-line is available without it
|
||||
# #-d 'python-qt4 >= 4.8'
|
||||
|
||||
upload:
|
||||
$(PYTHON) setup.py register
|
||||
$(PYTHON) setup.py sdist --formats=gztar upload
|
||||
#$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Source" -l Featured,Type-Source -u beville -w $(PASSWORD) "release/comictagger-$(VERSION_STR).zip"
|
||||
#$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Mac OS X" -l Featured,Type-Archive -u beville -w $(PASSWORD) "release/ComicTagger-$(VERSION_STR).dmg"
|
||||
#$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Windows" -l Featured,Type-Installer -u beville -w $(PASSWORD) "release/ComicTagger v$(VERSION_STR).exe"
|
||||
python setup.py register
|
||||
python setup.py sdist --formats=zip upload
|
||||
|
||||
svn_tag:
|
||||
svn copy https://comictagger.googlecode.com/svn/trunk \
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
||||
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)
|
||||
|
||||
install-GUI: $(INSTALL_GUI_STAMP)
|
||||
$(INSTALL_GUI_STAMP): requirements-GUI.txt
|
||||
$(PYTHON_VENV) -m pip install -r requirements-GUI.txt
|
||||
touch $(INSTALL_GUI_STAMP)
|
||||
|
||||
ins: $(PACKAGE_PATH)
|
||||
$(PACKAGE_PATH):
|
||||
$(PIP) install -e .
|
||||
|
||||
dist: CI
|
||||
pyinstaller -y comictagger.spec
|
||||
cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME)
|
||||
|
||||
103
README.md
103
README.md
@@ -1,50 +1,53 @@
|
||||
[](https://github.com/comictagger/comictagger/actions/workflows/build.yaml)
|
||||
[](https://gitter.im/comictagger/community)
|
||||
[](https://groups.google.com/forum/#!forum/comictagger)
|
||||
[](https://twitter.com/comictagger)
|
||||
[](https://www.facebook.com/ComicTagger-139615369550787/)
|
||||
|
||||
# ComicTagger
|
||||
|
||||
ComicTagger is a **multi-platform** app for **writing metadata to digital comics**, written in Python and PyQt.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
* Runs on macOS, Microsoft Windows, and Linux systems
|
||||
* Get comic information from [Comic Vine](https://comicvine.gamespot.com/)
|
||||
* **Automatic issue matching** using advanced image processing techniques
|
||||
* **Batch processing** in the GUI for tagging hundreds or more comics at a time
|
||||
* Support for **ComicRack** and **ComicBookLover** tagging formats
|
||||
* Native full support for **CBZ** digital comics
|
||||
* Native read only support for **CBR** digital comics: full support enabled installing additional [rar tools](https://www.rarlab.com/download.htm)
|
||||
* Command line interface (CLI) enabling **custom scripting** and **batch operations on large collections**
|
||||
|
||||
For details, screen-shots, and more, visit [the Wiki](https://github.com/comictagger/comictagger/wiki)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### Binaries
|
||||
|
||||
Windows 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.
|
||||
|
||||
### PIP installation
|
||||
|
||||
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]`
|
||||
|
||||
### From source
|
||||
|
||||
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]`
|
||||
This is a fork derived from google code:
|
||||
|
||||
https://code.google.com/p/comictagger/
|
||||
|
||||
|
||||
Changes in this fork:
|
||||
- using different unrar library https://pypi.python.org/pypi/unrar/. The previous one used unrar.dll on windows and
|
||||
hackish wrapping of unrar command on linux, while this new one should use unrarlib on both platforms. From my tests
|
||||
it is more stable and faster. *Requires unrarlib availability, check unrar module documentation for more
|
||||
information*.
|
||||
- extracted core libraries in its own package comicapi, shared in a new repository using git subtree for better
|
||||
alignment with comicstreamer
|
||||
- support for *day of month* field in the GUI
|
||||
- merge of changes from fcanc fork
|
||||
|
||||
Todo:
|
||||
- more tests in non-linux platforms
|
||||
- repackage for simple user installation
|
||||
|
||||
Follows original readme:
|
||||
|
||||
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
|
||||
|
||||
Features:
|
||||
|
||||
* Runs on Mac OSX, Microsoft Windows, and Linux systems
|
||||
* Communicates with an online database (Comic Vine) for acquiring metadata
|
||||
* Uses image processing to automatically match a given archive with the correct issue data
|
||||
* Batch processing in the GUI for tagging hundreds or more comics at a time
|
||||
* Reads and writes multiple tagging schemes ( ComicBookLover and ComicRack, with more planned).
|
||||
* Reads and writes RAR and Zip archives (external tools needed for writing RAR)
|
||||
* Command line interface (CLI) on all platforms (including Windows), which supports batch operations, and which can be
|
||||
used in native scripts for complex operations. For example, to recursively scrape and tag all archives in a folder
|
||||
comictagger.py -R -s -o -f -t cr -v -i --nooverwrite /path/to/comics/
|
||||
|
||||
For details, screen-shots, release notes, and more, visit http://code.google.com/p/comictagger/
|
||||
|
||||
Requires:
|
||||
|
||||
* python 2.6 or 2.7
|
||||
* configparser
|
||||
* python imaging (PIL) >= 1.1.6
|
||||
* beautifulsoup > 4.1
|
||||
|
||||
Optional requirement (for GUI):
|
||||
|
||||
* pyqt4
|
||||
|
||||
Install and run:
|
||||
|
||||
* ComicTagger can be run directly from this directory, using the launcher script "comictagger.py"
|
||||
|
||||
* To install on your system use: "python setup.py install". Take note in the output where comictagger.py goes!
|
||||
|
||||
18
comicapi/UnRAR2/UnRARDLL/license.txt
Normal file
18
comicapi/UnRAR2/UnRARDLL/license.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
The unrar.dll library is freeware. This means:
|
||||
|
||||
1. All copyrights to RAR and the unrar.dll are exclusively
|
||||
owned by the author - Alexander Roshal.
|
||||
|
||||
2. The unrar.dll library may be used in any software to handle RAR
|
||||
archives without limitations free of charge.
|
||||
|
||||
3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS".
|
||||
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT
|
||||
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS,
|
||||
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING
|
||||
OR MISUSING THIS SOFTWARE.
|
||||
|
||||
Thank you for your interest in RAR and unrar.dll.
|
||||
|
||||
|
||||
Alexander L. Roshal
|
||||
BIN
comicapi/UnRAR2/UnRARDLL/unrar.dll
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/unrar.dll
Normal file
Binary file not shown.
140
comicapi/UnRAR2/UnRARDLL/unrar.h
Normal file
140
comicapi/UnRAR2/UnRARDLL/unrar.h
Normal file
@@ -0,0 +1,140 @@
|
||||
#ifndef _UNRAR_DLL_
|
||||
#define _UNRAR_DLL_
|
||||
|
||||
#define ERAR_END_ARCHIVE 10
|
||||
#define ERAR_NO_MEMORY 11
|
||||
#define ERAR_BAD_DATA 12
|
||||
#define ERAR_BAD_ARCHIVE 13
|
||||
#define ERAR_UNKNOWN_FORMAT 14
|
||||
#define ERAR_EOPEN 15
|
||||
#define ERAR_ECREATE 16
|
||||
#define ERAR_ECLOSE 17
|
||||
#define ERAR_EREAD 18
|
||||
#define ERAR_EWRITE 19
|
||||
#define ERAR_SMALL_BUF 20
|
||||
#define ERAR_UNKNOWN 21
|
||||
#define ERAR_MISSING_PASSWORD 22
|
||||
|
||||
#define RAR_OM_LIST 0
|
||||
#define RAR_OM_EXTRACT 1
|
||||
#define RAR_OM_LIST_INCSPLIT 2
|
||||
|
||||
#define RAR_SKIP 0
|
||||
#define RAR_TEST 1
|
||||
#define RAR_EXTRACT 2
|
||||
|
||||
#define RAR_VOL_ASK 0
|
||||
#define RAR_VOL_NOTIFY 1
|
||||
|
||||
#define RAR_DLL_VERSION 4
|
||||
|
||||
#ifdef _UNIX
|
||||
#define CALLBACK
|
||||
#define PASCAL
|
||||
#define LONG long
|
||||
#define HANDLE void *
|
||||
#define LPARAM long
|
||||
#define UINT unsigned int
|
||||
#endif
|
||||
|
||||
struct RARHeaderData
|
||||
{
|
||||
char ArcName[260];
|
||||
char FileName[260];
|
||||
unsigned int Flags;
|
||||
unsigned int PackSize;
|
||||
unsigned int UnpSize;
|
||||
unsigned int HostOS;
|
||||
unsigned int FileCRC;
|
||||
unsigned int FileTime;
|
||||
unsigned int UnpVer;
|
||||
unsigned int Method;
|
||||
unsigned int FileAttr;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
};
|
||||
|
||||
|
||||
struct RARHeaderDataEx
|
||||
{
|
||||
char ArcName[1024];
|
||||
wchar_t ArcNameW[1024];
|
||||
char FileName[1024];
|
||||
wchar_t FileNameW[1024];
|
||||
unsigned int Flags;
|
||||
unsigned int PackSize;
|
||||
unsigned int PackSizeHigh;
|
||||
unsigned int UnpSize;
|
||||
unsigned int UnpSizeHigh;
|
||||
unsigned int HostOS;
|
||||
unsigned int FileCRC;
|
||||
unsigned int FileTime;
|
||||
unsigned int UnpVer;
|
||||
unsigned int Method;
|
||||
unsigned int FileAttr;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Reserved[1024];
|
||||
};
|
||||
|
||||
|
||||
struct RAROpenArchiveData
|
||||
{
|
||||
char *ArcName;
|
||||
unsigned int OpenMode;
|
||||
unsigned int OpenResult;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
};
|
||||
|
||||
struct RAROpenArchiveDataEx
|
||||
{
|
||||
char *ArcName;
|
||||
wchar_t *ArcNameW;
|
||||
unsigned int OpenMode;
|
||||
unsigned int OpenResult;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Flags;
|
||||
unsigned int Reserved[32];
|
||||
};
|
||||
|
||||
enum UNRARCALLBACK_MESSAGES {
|
||||
UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD
|
||||
};
|
||||
|
||||
typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2);
|
||||
|
||||
typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode);
|
||||
typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData);
|
||||
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData);
|
||||
int PASCAL RARCloseArchive(HANDLE hArcData);
|
||||
int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData);
|
||||
int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData);
|
||||
int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName);
|
||||
int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName);
|
||||
void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData);
|
||||
void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc);
|
||||
void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc);
|
||||
void PASCAL RARSetPassword(HANDLE hArcData,char *Password);
|
||||
int PASCAL RARGetDllVersion();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
BIN
comicapi/UnRAR2/UnRARDLL/unrar.lib
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/unrar.lib
Normal file
Binary file not shown.
606
comicapi/UnRAR2/UnRARDLL/unrardll.txt
Normal file
606
comicapi/UnRAR2/UnRARDLL/unrardll.txt
Normal file
@@ -0,0 +1,606 @@
|
||||
|
||||
UnRAR.dll Manual
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
UnRAR.dll is a 32-bit Windows dynamic-link library which provides
|
||||
file extraction from RAR archives.
|
||||
|
||||
|
||||
Exported functions
|
||||
|
||||
====================================================================
|
||||
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Open RAR archive and allocate memory structures
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
ArchiveData Points to RAROpenArchiveData structure
|
||||
|
||||
struct RAROpenArchiveData
|
||||
{
|
||||
char *ArcName;
|
||||
UINT OpenMode;
|
||||
UINT OpenResult;
|
||||
char *CmtBuf;
|
||||
UINT CmtBufSize;
|
||||
UINT CmtSize;
|
||||
UINT CmtState;
|
||||
};
|
||||
|
||||
Structure fields:
|
||||
|
||||
ArcName
|
||||
Input parameter which should point to zero terminated string
|
||||
containing the archive name.
|
||||
|
||||
OpenMode
|
||||
Input parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
RAR_OM_LIST
|
||||
Open archive for reading file headers only.
|
||||
|
||||
RAR_OM_EXTRACT
|
||||
Open archive for testing and extracting files.
|
||||
|
||||
RAR_OM_LIST_INCSPLIT
|
||||
Open archive for reading file headers only. If you open an archive
|
||||
in such mode, RARReadHeader[Ex] will return all file headers,
|
||||
including those with "file continued from previous volume" flag.
|
||||
In case of RAR_OM_LIST such headers are automatically skipped.
|
||||
So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will
|
||||
get several file header records for same file if file is split between
|
||||
volumes. For such files only the last file header record will contain
|
||||
the correct file CRC and if you wish to get the correct packed size,
|
||||
you need to sum up packed sizes of all parts.
|
||||
|
||||
OpenResult
|
||||
Output parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
0 Success
|
||||
ERAR_NO_MEMORY Not enough memory to initialize data structures
|
||||
ERAR_BAD_DATA Archive header broken
|
||||
ERAR_BAD_ARCHIVE File is not valid RAR archive
|
||||
ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers
|
||||
ERAR_EOPEN File open error
|
||||
|
||||
CmtBuf
|
||||
Input parameter which should point to the buffer for archive
|
||||
comments. Maximum comment size is limited to 64Kb. Comment text is
|
||||
zero terminated. If the comment text is larger than the buffer
|
||||
size, the comment text will be truncated. If CmtBuf is set to
|
||||
NULL, comments will not be read.
|
||||
|
||||
CmtBufSize
|
||||
Input parameter which should contain size of buffer for archive
|
||||
comments.
|
||||
|
||||
CmtSize
|
||||
Output parameter containing size of comments actually read into the
|
||||
buffer, cannot exceed CmtBufSize.
|
||||
|
||||
CmtState
|
||||
Output parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
0 comments not present
|
||||
1 Comments read completely
|
||||
ERAR_NO_MEMORY Not enough memory to extract comments
|
||||
ERAR_BAD_DATA Broken comment
|
||||
ERAR_UNKNOWN_FORMAT Unknown comment format
|
||||
ERAR_SMALL_BUF Buffer too small, comments not completely read
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
Archive handle or NULL in case of error
|
||||
|
||||
|
||||
========================================================================
|
||||
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData)
|
||||
========================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure
|
||||
allowing to specify Unicode archive name and returning information
|
||||
about archive flags.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
ArchiveData Points to RAROpenArchiveDataEx structure
|
||||
|
||||
struct RAROpenArchiveDataEx
|
||||
{
|
||||
char *ArcName;
|
||||
wchar_t *ArcNameW;
|
||||
unsigned int OpenMode;
|
||||
unsigned int OpenResult;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Flags;
|
||||
unsigned int Reserved[32];
|
||||
};
|
||||
|
||||
Structure fields:
|
||||
|
||||
ArcNameW
|
||||
Input parameter which should point to zero terminated Unicode string
|
||||
containing the archive name or NULL if Unicode name is not specified.
|
||||
|
||||
Flags
|
||||
Output parameter. Combination of bit flags.
|
||||
|
||||
Possible values
|
||||
|
||||
0x0001 - Volume attribute (archive volume)
|
||||
0x0002 - Archive comment present
|
||||
0x0004 - Archive lock attribute
|
||||
0x0008 - Solid attribute (solid archive)
|
||||
0x0010 - New volume naming scheme ('volname.partN.rar')
|
||||
0x0020 - Authenticity information present
|
||||
0x0040 - Recovery record present
|
||||
0x0080 - Block headers are encrypted
|
||||
0x0100 - First volume (set only by RAR 3.0 and later)
|
||||
|
||||
Reserved[32]
|
||||
Reserved for future use. Must be zero.
|
||||
|
||||
Information on other structure fields and function return values
|
||||
is available above, in RAROpenArchive function description.
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARCloseArchive(HANDLE hArcData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Close RAR archive and release allocated memory. It must be called when
|
||||
archive processing is finished, even if the archive processing was stopped
|
||||
due to an error.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
0 Success
|
||||
ERAR_ECLOSE Archive close error
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARReadHeader(HANDLE hArcData,
|
||||
struct RARHeaderData *HeaderData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Read header of file in archive.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
HeaderData
|
||||
It should point to RARHeaderData structure:
|
||||
|
||||
struct RARHeaderData
|
||||
{
|
||||
char ArcName[260];
|
||||
char FileName[260];
|
||||
UINT Flags;
|
||||
UINT PackSize;
|
||||
UINT UnpSize;
|
||||
UINT HostOS;
|
||||
UINT FileCRC;
|
||||
UINT FileTime;
|
||||
UINT UnpVer;
|
||||
UINT Method;
|
||||
UINT FileAttr;
|
||||
char *CmtBuf;
|
||||
UINT CmtBufSize;
|
||||
UINT CmtSize;
|
||||
UINT CmtState;
|
||||
};
|
||||
|
||||
Structure fields:
|
||||
|
||||
ArcName
|
||||
Output parameter which contains a zero terminated string of the
|
||||
current archive name. May be used to determine the current volume
|
||||
name.
|
||||
|
||||
FileName
|
||||
Output parameter which contains a zero terminated string of the
|
||||
file name in OEM (DOS) encoding.
|
||||
|
||||
Flags
|
||||
Output parameter which contains file flags:
|
||||
|
||||
0x01 - file continued from previous volume
|
||||
0x02 - file continued on next volume
|
||||
0x04 - file encrypted with password
|
||||
0x08 - file comment present
|
||||
0x10 - compression of previous files is used (solid flag)
|
||||
|
||||
bits 7 6 5
|
||||
|
||||
0 0 0 - dictionary size 64 Kb
|
||||
0 0 1 - dictionary size 128 Kb
|
||||
0 1 0 - dictionary size 256 Kb
|
||||
0 1 1 - dictionary size 512 Kb
|
||||
1 0 0 - dictionary size 1024 Kb
|
||||
1 0 1 - dictionary size 2048 KB
|
||||
1 1 0 - dictionary size 4096 KB
|
||||
1 1 1 - file is directory
|
||||
|
||||
Other bits are reserved.
|
||||
|
||||
PackSize
|
||||
Output parameter means packed file size or size of the
|
||||
file part if file was split between volumes.
|
||||
|
||||
UnpSize
|
||||
Output parameter - unpacked file size.
|
||||
|
||||
HostOS
|
||||
Output parameter - operating system used for archiving:
|
||||
|
||||
0 - MS DOS;
|
||||
1 - OS/2.
|
||||
2 - Win32
|
||||
3 - Unix
|
||||
|
||||
FileCRC
|
||||
Output parameter which contains unpacked file CRC. In case of file parts
|
||||
split between volumes only the last part contains the correct CRC
|
||||
and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode.
|
||||
|
||||
FileTime
|
||||
Output parameter - contains date and time in standard MS DOS format.
|
||||
|
||||
UnpVer
|
||||
Output parameter - RAR version needed to extract file.
|
||||
It is encoded as 10 * Major version + minor version.
|
||||
|
||||
Method
|
||||
Output parameter - packing method.
|
||||
|
||||
FileAttr
|
||||
Output parameter - file attributes.
|
||||
|
||||
CmtBuf
|
||||
File comments support is not implemented in the new DLL version yet.
|
||||
Now CmtState is always 0.
|
||||
|
||||
/*
|
||||
* Input parameter which should point to the buffer for file
|
||||
* comments. Maximum comment size is limited to 64Kb. Comment text is
|
||||
* a zero terminated string in OEM encoding. If the comment text is
|
||||
* larger than the buffer size, the comment text will be truncated.
|
||||
* If CmtBuf is set to NULL, comments will not be read.
|
||||
*/
|
||||
|
||||
CmtBufSize
|
||||
Input parameter which should contain size of buffer for archive
|
||||
comments.
|
||||
|
||||
CmtSize
|
||||
Output parameter containing size of comments actually read into the
|
||||
buffer, should not exceed CmtBufSize.
|
||||
|
||||
CmtState
|
||||
Output parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
0 Absent comments
|
||||
1 Comments read completely
|
||||
ERAR_NO_MEMORY Not enough memory to extract comments
|
||||
ERAR_BAD_DATA Broken comment
|
||||
ERAR_UNKNOWN_FORMAT Unknown comment format
|
||||
ERAR_SMALL_BUF Buffer too small, comments not completely read
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
0 Success
|
||||
ERAR_END_ARCHIVE End of archive
|
||||
ERAR_BAD_DATA File header broken
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARReadHeaderEx(HANDLE hArcData,
|
||||
struct RARHeaderDataEx *HeaderData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Similar to RARReadHeader, but uses RARHeaderDataEx structure,
|
||||
containing information about Unicode file names and 64 bit file sizes.
|
||||
|
||||
struct RARHeaderDataEx
|
||||
{
|
||||
char ArcName[1024];
|
||||
wchar_t ArcNameW[1024];
|
||||
char FileName[1024];
|
||||
wchar_t FileNameW[1024];
|
||||
unsigned int Flags;
|
||||
unsigned int PackSize;
|
||||
unsigned int PackSizeHigh;
|
||||
unsigned int UnpSize;
|
||||
unsigned int UnpSizeHigh;
|
||||
unsigned int HostOS;
|
||||
unsigned int FileCRC;
|
||||
unsigned int FileTime;
|
||||
unsigned int UnpVer;
|
||||
unsigned int Method;
|
||||
unsigned int FileAttr;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Reserved[1024];
|
||||
};
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARProcessFile(HANDLE hArcData,
|
||||
int Operation,
|
||||
char *DestPath,
|
||||
char *DestName)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Performs action and moves the current position in the archive to
|
||||
the next file. Extract or test the current file from the archive
|
||||
opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set,
|
||||
then a call to this function will simply skip the archive position
|
||||
to the next file.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
Operation
|
||||
File operation.
|
||||
|
||||
Possible values
|
||||
|
||||
RAR_SKIP Move to the next file in the archive. If the
|
||||
archive is solid and RAR_OM_EXTRACT mode was set
|
||||
when the archive was opened, the current file will
|
||||
be processed - the operation will be performed
|
||||
slower than a simple seek.
|
||||
|
||||
RAR_TEST Test the current file and move to the next file in
|
||||
the archive. If the archive was opened with
|
||||
RAR_OM_LIST mode, the operation is equal to
|
||||
RAR_SKIP.
|
||||
|
||||
RAR_EXTRACT Extract the current file and move to the next file.
|
||||
If the archive was opened with RAR_OM_LIST mode,
|
||||
the operation is equal to RAR_SKIP.
|
||||
|
||||
|
||||
DestPath
|
||||
This parameter should point to a zero terminated string containing the
|
||||
destination directory to which to extract files to. If DestPath is equal
|
||||
to NULL, it means extract to the current directory. This parameter has
|
||||
meaning only if DestName is NULL.
|
||||
|
||||
DestName
|
||||
This parameter should point to a string containing the full path and name
|
||||
to assign to extracted file or it can be NULL to use the default name.
|
||||
If DestName is defined (not NULL), it overrides both the original file
|
||||
name saved in the archive and path specigied in DestPath setting.
|
||||
|
||||
Both DestPath and DestName must be in OEM encoding. If necessary,
|
||||
use CharToOem to convert text to OEM before passing to this function.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
0 Success
|
||||
ERAR_BAD_DATA File CRC error
|
||||
ERAR_BAD_ARCHIVE Volume is not valid RAR archive
|
||||
ERAR_UNKNOWN_FORMAT Unknown archive format
|
||||
ERAR_EOPEN Volume open error
|
||||
ERAR_ECREATE File create error
|
||||
ERAR_ECLOSE File close error
|
||||
ERAR_EREAD Read error
|
||||
ERAR_EWRITE Write error
|
||||
|
||||
|
||||
Note: if you wish to cancel extraction, return -1 when processing
|
||||
UCM_PROCESSDATA callback message.
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARProcessFileW(HANDLE hArcData,
|
||||
int Operation,
|
||||
wchar_t *DestPath,
|
||||
wchar_t *DestName)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Unicode version of RARProcessFile. It uses Unicode DestPath
|
||||
and DestName parameters, other parameters and return values
|
||||
are the same as in RARProcessFile.
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetCallback(HANDLE hArcData,
|
||||
int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2),
|
||||
LPARAM UserData);
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Set a user-defined callback function to process Unrar events.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
CallbackProc
|
||||
It should point to a user-defined callback function.
|
||||
|
||||
The function will be passed four parameters:
|
||||
|
||||
|
||||
msg Type of event. Described below.
|
||||
|
||||
UserData User defined value passed to RARSetCallback.
|
||||
|
||||
P1 and P2 Event dependent parameters. Described below.
|
||||
|
||||
|
||||
Possible events
|
||||
|
||||
UCM_CHANGEVOLUME Process volume change.
|
||||
|
||||
P1 Points to the zero terminated name
|
||||
of the next volume.
|
||||
|
||||
P2 The function call mode:
|
||||
|
||||
RAR_VOL_ASK Required volume is absent. The function should
|
||||
prompt user and return a positive value
|
||||
to retry or return -1 value to terminate
|
||||
operation. The function may also specify a new
|
||||
volume name, placing it to the address specified
|
||||
by P1 parameter.
|
||||
|
||||
RAR_VOL_NOTIFY Required volume is successfully opened.
|
||||
This is a notification call and volume name
|
||||
modification is not allowed. The function should
|
||||
return a positive value to continue or -1
|
||||
to terminate operation.
|
||||
|
||||
UCM_PROCESSDATA Process unpacked data. It may be used to read
|
||||
a file while it is being extracted or tested
|
||||
without actual extracting file to disk.
|
||||
Return a positive value to continue process
|
||||
or -1 to cancel the archive operation
|
||||
|
||||
P1 Address pointing to the unpacked data.
|
||||
Function may refer to the data but must not
|
||||
change it.
|
||||
|
||||
P2 Size of the unpacked data. It is guaranteed
|
||||
only that the size will not exceed the maximum
|
||||
dictionary size (4 Mb in RAR 3.0).
|
||||
|
||||
UCM_NEEDPASSWORD DLL needs a password to process archive.
|
||||
This message must be processed if you wish
|
||||
to be able to handle archives with encrypted
|
||||
file names. It can be also used as replacement
|
||||
of RARSetPassword function even for usual
|
||||
encrypted files with non-encrypted names.
|
||||
|
||||
P1 Address pointing to the buffer for a password.
|
||||
You need to copy a password here.
|
||||
|
||||
P2 Size of the password buffer.
|
||||
|
||||
|
||||
UserData
|
||||
User data passed to callback function.
|
||||
|
||||
Other functions of UnRAR.dll should not be called from the callback
|
||||
function.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
None
|
||||
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetChangeVolProc(HANDLE hArcData,
|
||||
int PASCAL (*ChangeVolProc)(char *ArcName,int Mode));
|
||||
====================================================================
|
||||
|
||||
Obsoleted, use RARSetCallback instead.
|
||||
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetProcessDataProc(HANDLE hArcData,
|
||||
int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size))
|
||||
====================================================================
|
||||
|
||||
Obsoleted, use RARSetCallback instead.
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetPassword(HANDLE hArcData,
|
||||
char *Password);
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Set a password to decrypt files.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
Password
|
||||
It should point to a string containing a zero terminated password.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
None
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARGetDllVersion();
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Returns API version.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
None.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
Returns an integer value denoting UnRAR.dll API version, which is also
|
||||
defined in unrar.h as RAR_DLL_VERSION. API version number is incremented
|
||||
only in case of noticeable changes in UnRAR.dll API. Do not confuse it
|
||||
with version of UnRAR.dll stored in DLL resources, which is incremented
|
||||
with every DLL rebuild.
|
||||
|
||||
If RARGetDllVersion() returns a value lower than UnRAR.dll which your
|
||||
application was designed for, it may indicate that DLL version is too old
|
||||
and it will fail to provide all necessary functions to your application.
|
||||
|
||||
This function is absent in old versions of UnRAR.dll, so it is safer
|
||||
to use LoadLibrary and GetProcAddress to access this function.
|
||||
|
||||
80
comicapi/UnRAR2/UnRARDLL/whatsnew.txt
Normal file
80
comicapi/UnRAR2/UnRARDLL/whatsnew.txt
Normal file
@@ -0,0 +1,80 @@
|
||||
List of unrar.dll API changes. We do not include performance and reliability
|
||||
improvements into this list, but this library and RAR/UnRAR tools share
|
||||
the same source code. So the latest version of unrar.dll usually contains
|
||||
same decompression algorithm changes as the latest UnRAR version.
|
||||
============================================================================
|
||||
|
||||
-- 18 January 2008
|
||||
|
||||
all LONG parameters of CallbackProc function were changed
|
||||
to LPARAM type for 64 bit mode compatibility.
|
||||
|
||||
|
||||
-- 12 December 2007
|
||||
|
||||
Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive.
|
||||
|
||||
|
||||
-- 14 August 2007
|
||||
|
||||
Added NoCrypt\unrar_nocrypt.dll without decryption code for those
|
||||
applications where presence of encryption or decryption code is not
|
||||
allowed because of legal restrictions.
|
||||
|
||||
|
||||
-- 14 December 2006
|
||||
|
||||
Added ERAR_MISSING_PASSWORD error type. This error is returned
|
||||
if empty password is specified for encrypted file.
|
||||
|
||||
|
||||
-- 12 June 2003
|
||||
|
||||
Added RARProcessFileW function, Unicode version of RARProcessFile
|
||||
|
||||
|
||||
-- 9 August 2002
|
||||
|
||||
Added RAROpenArchiveEx function allowing to specify Unicode archive
|
||||
name and get archive flags.
|
||||
|
||||
|
||||
-- 24 January 2002
|
||||
|
||||
Added RARReadHeaderEx function allowing to read Unicode file names
|
||||
and 64 bit file sizes.
|
||||
|
||||
|
||||
-- 23 January 2002
|
||||
|
||||
Added ERAR_UNKNOWN error type (it is used for all errors which
|
||||
do not have special ERAR code yet) and UCM_NEEDPASSWORD callback
|
||||
message.
|
||||
|
||||
Unrar.dll automatically opens all next volumes not only when extracting,
|
||||
but also in RAR_OM_LIST mode.
|
||||
|
||||
|
||||
-- 27 November 2001
|
||||
|
||||
RARSetChangeVolProc and RARSetProcessDataProc are replaced by
|
||||
the single callback function installed with RARSetCallback.
|
||||
Unlike old style callbacks, the new function accepts the user defined
|
||||
parameter. Unrar.dll still supports RARSetChangeVolProc and
|
||||
RARSetProcessDataProc for compatibility purposes, but if you write
|
||||
a new application, better use RARSetCallback.
|
||||
|
||||
File comments support is not implemented in the new DLL version yet.
|
||||
Now CmtState is always 0.
|
||||
|
||||
|
||||
-- 13 August 2001
|
||||
|
||||
Added RARGetDllVersion function, so you may distinguish old unrar.dll,
|
||||
which used C style callback functions and the new one with PASCAL callbacks.
|
||||
|
||||
|
||||
-- 10 May 2001
|
||||
|
||||
Callback functions in RARSetChangeVolProc and RARSetProcessDataProc
|
||||
use PASCAL style call convention now.
|
||||
1
comicapi/UnRAR2/UnRARDLL/x64/readme.txt
Normal file
1
comicapi/UnRAR2/UnRARDLL/x64/readme.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is x64 version of unrar.dll.
|
||||
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll
Normal file
Binary file not shown.
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib
Normal file
Binary file not shown.
177
comicapi/UnRAR2/__init__.py
Normal file
177
comicapi/UnRAR2/__init__.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""
|
||||
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
|
||||
|
||||
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
|
||||
stable and foolproof.
|
||||
Notice that it has INCOMPATIBLE interface.
|
||||
|
||||
It enables reading and unpacking of archives created with the
|
||||
RAR/WinRAR archivers. There is a low-level interface which is very
|
||||
similar to the C interface provided by UnRAR. There is also a
|
||||
higher level interface which makes some common operations easier.
|
||||
"""
|
||||
|
||||
__version__ = '0.99.3'
|
||||
|
||||
try:
|
||||
WindowsError
|
||||
in_windows = True
|
||||
except NameError:
|
||||
in_windows = False
|
||||
|
||||
if in_windows:
|
||||
from windows import RarFileImplementation
|
||||
else:
|
||||
from unix import RarFileImplementation
|
||||
|
||||
|
||||
import fnmatch, time, weakref
|
||||
|
||||
class RarInfo(object):
|
||||
"""Represents a file header in an archive. Don't instantiate directly.
|
||||
Use only to obtain information about file.
|
||||
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.
|
||||
USE METHODS OF RarFile CLASS INSTEAD.
|
||||
|
||||
Properties:
|
||||
index - index of file within the archive
|
||||
filename - name of the file in the archive including path (if any)
|
||||
datetime - file date/time as a struct_time suitable for time.strftime
|
||||
isdir - True if the file is a directory
|
||||
size - size in bytes of the uncompressed file
|
||||
comment - comment associated with the file
|
||||
|
||||
Note - this is not currently intended to be a Python file-like object.
|
||||
"""
|
||||
|
||||
def __init__(self, rarfile, data):
|
||||
self.rarfile = weakref.proxy(rarfile)
|
||||
self.index = data['index']
|
||||
self.filename = data['filename']
|
||||
self.isdir = data['isdir']
|
||||
self.size = data['size']
|
||||
self.datetime = data['datetime']
|
||||
self.comment = data['comment']
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
try :
|
||||
arcName = self.rarfile.archiveName
|
||||
except ReferenceError:
|
||||
arcName = "[ARCHIVE_NO_LONGER_LOADED]"
|
||||
return '<RarInfo "%s" in "%s">' % (self.filename, arcName)
|
||||
|
||||
class RarFile(RarFileImplementation):
|
||||
|
||||
def __init__(self, archiveName, password=None):
|
||||
"""Instantiate the archive.
|
||||
|
||||
archiveName is the name of the RAR file.
|
||||
password is used to decrypt the files in the archive.
|
||||
|
||||
Properties:
|
||||
comment - comment associated with the archive
|
||||
|
||||
>>> print RarFile('test.rar').comment
|
||||
This is a test.
|
||||
"""
|
||||
self.archiveName = archiveName
|
||||
RarFileImplementation.init(self, password)
|
||||
|
||||
def __del__(self):
|
||||
self.destruct()
|
||||
|
||||
def infoiter(self):
|
||||
"""Iterate over all the files in the archive, generating RarInfos.
|
||||
|
||||
>>> import os
|
||||
>>> for fileInArchive in RarFile('test.rar').infoiter():
|
||||
... print os.path.split(fileInArchive.filename)[-1],
|
||||
... print fileInArchive.isdir,
|
||||
... print fileInArchive.size,
|
||||
... print fileInArchive.comment,
|
||||
... print tuple(fileInArchive.datetime)[0:5],
|
||||
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)
|
||||
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59
|
||||
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01
|
||||
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47
|
||||
"""
|
||||
for params in RarFileImplementation.infoiter(self):
|
||||
yield RarInfo(self, params)
|
||||
|
||||
def infolist(self):
|
||||
"""Return a list of RarInfos, descripting the contents of the archive."""
|
||||
return list(self.infoiter())
|
||||
|
||||
def read_files(self, condition='*'):
|
||||
"""Read specific files from archive into memory.
|
||||
If "condition" is a list of numbers, then return files which have those positions in infolist.
|
||||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||
and returns boolean True (extract) or False (skip).
|
||||
If "condition" is omitted, all files are returned.
|
||||
|
||||
Returns list of tuples (RarInfo info, str contents)
|
||||
"""
|
||||
checker = condition2checker(condition)
|
||||
return RarFileImplementation.read_files(self, checker)
|
||||
|
||||
|
||||
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
|
||||
"""Extract specific files from archive to disk.
|
||||
|
||||
If "condition" is a list of numbers, then extract files which have those positions in infolist.
|
||||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||
and returns either boolean True (extract) or boolean False (skip).
|
||||
DEPRECATED: If "condition" callback returns string (only supported for Windows) -
|
||||
that string will be used as a new name to save the file under.
|
||||
If "condition" is omitted, all files are extracted.
|
||||
|
||||
"path" is a directory to extract to
|
||||
"withSubpath" flag denotes whether files are extracted with their full path in the archive.
|
||||
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
|
||||
|
||||
Returns list of RarInfos for extracted files."""
|
||||
checker = condition2checker(condition)
|
||||
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
|
||||
|
||||
def condition2checker(condition):
|
||||
"""Converts different condition types to callback"""
|
||||
if type(condition) in [str, unicode]:
|
||||
def smatcher(info):
|
||||
return fnmatch.fnmatch(info.filename, condition)
|
||||
return smatcher
|
||||
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]:
|
||||
def imatcher(info):
|
||||
return info.index in condition
|
||||
return imatcher
|
||||
elif callable(condition):
|
||||
return condition
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
30
comicapi/UnRAR2/rar_exceptions.py
Normal file
30
comicapi/UnRAR2/rar_exceptions.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||
|
||||
|
||||
class ArchiveHeaderBroken(Exception): pass
|
||||
class InvalidRARArchive(Exception): pass
|
||||
class FileOpenError(Exception): pass
|
||||
class IncorrectRARPassword(Exception): pass
|
||||
class InvalidRARArchiveUsage(Exception): pass
|
||||
138
comicapi/UnRAR2/test_UnRAR2.py
Normal file
138
comicapi/UnRAR2/test_UnRAR2.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import os, sys
|
||||
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
|
||||
|
||||
def cleanup(dir='test'):
|
||||
for path, dirs, files in os.walk(dir):
|
||||
for fn in files:
|
||||
os.remove(os.path.join(path, fn))
|
||||
for dir in dirs:
|
||||
os.removedirs(os.path.join(path, dir))
|
||||
|
||||
|
||||
# basic test
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test.rar')
|
||||
rarc.infolist()
|
||||
assert rarc.comment == "This is a test."
|
||||
for info in rarc.infoiter():
|
||||
saveinfo = info
|
||||
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
|
||||
break
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert os.path.exists('test'+os.sep+'this.py')
|
||||
del rarc
|
||||
assert (str(saveinfo)=="""<RarInfo "test" in "[ARCHIVE_NO_LONGER_LOADED]">""")
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test.rar').extract()
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar matching the wildcard *.txt
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt')
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert not os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
|
||||
# check the name and size of each file, extracting small ones
|
||||
cleanup()
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
assert archive.comment == 'This is a test.'
|
||||
archive.extract(lambda rarinfo: rarinfo.size <= 1024)
|
||||
for rarinfo in archive.infoiter():
|
||||
if rarinfo.size <= 1024 and not rarinfo.isdir:
|
||||
assert rarinfo.size == os.stat(rarinfo.filename).st_size
|
||||
assert file('test'+os.sep+'test.txt', 'rt').read() == 'This is only a test.'
|
||||
assert not os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
|
||||
# extract this.py, overriding it's destination
|
||||
cleanup('test2')
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
archive.extract('*.py', 'test2', False)
|
||||
assert os.path.exists('test2'+os.sep+'this.py')
|
||||
cleanup('test2')
|
||||
|
||||
|
||||
# extract test.txt to memory
|
||||
cleanup()
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
entries = UnRAR2.RarFile('test.rar').read_files('*test.txt')
|
||||
assert len(entries)==1
|
||||
assert entries[0][0].filename.endswith('test.txt')
|
||||
assert entries[0][1]=='This is only a test.'
|
||||
|
||||
|
||||
# extract all the files in test.rar with overwriting
|
||||
cleanup()
|
||||
fo = open('test'+os.sep+'test.txt',"wt")
|
||||
fo.write("blah")
|
||||
fo.close()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt')
|
||||
assert open('test'+os.sep+'test.txt',"rt").read()!="blah"
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar without overwriting
|
||||
cleanup()
|
||||
fo = open('test'+os.sep+'test.txt',"wt")
|
||||
fo.write("blahblah")
|
||||
fo.close()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt', overwrite = False)
|
||||
assert open('test'+os.sep+'test.txt',"rt").read()=="blahblah"
|
||||
cleanup()
|
||||
|
||||
# list big file in an archive
|
||||
list(UnRAR2.RarFile('test_nulls.rar').infoiter())
|
||||
|
||||
# extract files from an archive with protected files
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected")
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
try:
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract()
|
||||
except IncorrectRARPassword:
|
||||
errored = True
|
||||
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
assert errored
|
||||
cleanup()
|
||||
|
||||
# extract files from an archive with protected headers
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
try:
|
||||
UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract()
|
||||
except IncorrectRARPassword:
|
||||
errored = True
|
||||
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
assert errored
|
||||
cleanup()
|
||||
|
||||
# make sure docstring examples are working
|
||||
import doctest
|
||||
doctest.testmod(UnRAR2)
|
||||
|
||||
# update documentation
|
||||
import pydoc
|
||||
pydoc.writedoc(UnRAR2)
|
||||
|
||||
# cleanup
|
||||
try:
|
||||
os.remove('__init__.pyc')
|
||||
except:
|
||||
pass
|
||||
218
comicapi/UnRAR2/unix.py
Normal file
218
comicapi/UnRAR2/unix.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Unix version uses unrar command line executable
|
||||
|
||||
import subprocess
|
||||
import gc
|
||||
|
||||
import os, os.path
|
||||
import time, re
|
||||
|
||||
from rar_exceptions import *
|
||||
|
||||
class UnpackerNotInstalled(Exception): pass
|
||||
|
||||
rar_executable_cached = None
|
||||
rar_executable_version = None
|
||||
|
||||
def call_unrar(params):
|
||||
"Calls rar/unrar command line executable, returns stdout pipe"
|
||||
global rar_executable_cached
|
||||
if rar_executable_cached is None:
|
||||
for command in ('unrar', 'rar'):
|
||||
try:
|
||||
subprocess.Popen([command], stdout=subprocess.PIPE)
|
||||
rar_executable_cached = command
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
if rar_executable_cached is None:
|
||||
raise UnpackerNotInstalled("No suitable RAR unpacker installed")
|
||||
|
||||
assert type(params) == list, "params must be list"
|
||||
args = [rar_executable_cached] + params
|
||||
try:
|
||||
gc.disable() # See http://bugs.python.org/issue1336
|
||||
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
global rar_executable_version
|
||||
self.password = password
|
||||
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', []).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
raise FileOpenError
|
||||
if line.find("CRC failed")>=0:
|
||||
raise IncorrectRARPassword
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not (line.startswith('UNRAR')):
|
||||
line = source.next()
|
||||
signature = line
|
||||
# The code below is mighty flaky
|
||||
# and will probably crash on localized versions of RAR
|
||||
# but I see no safe way to rewrite it using a CLI tool
|
||||
if signature.startswith("UNRAR 4"):
|
||||
rar_executable_version = 4
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:] # strip out "Comment:" part
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
else:
|
||||
self.comment = None
|
||||
elif signature.startswith("UNRAR 5"):
|
||||
rar_executable_version = 5
|
||||
line = source.next()
|
||||
while not line.startswith('Archive:'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
self.comment = '\n'.join(accum[:-1]).strip()
|
||||
else:
|
||||
self.comment = None
|
||||
else:
|
||||
raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: "
|
||||
+ signature.split(" ")[1])
|
||||
|
||||
|
||||
def escaped_password(self):
|
||||
return '-' if self.password == None else self.password
|
||||
|
||||
|
||||
def call(self, cmd, options=[], files=[]):
|
||||
options2 = options + ['p'+self.escaped_password()]
|
||||
soptions = ['-'+x for x in options2]
|
||||
return call_unrar([cmd]+soptions+['--',self.archiveName]+files)
|
||||
|
||||
def infoiter(self):
|
||||
|
||||
command = "v" if rar_executable_version == 4 else "l"
|
||||
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
raise FileOpenError
|
||||
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not line.startswith('-----------'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
if line.startswith("CRC failed") or line.startswith("Checksum error"):
|
||||
raise IncorrectRARPassword
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
i = 0
|
||||
re_spaces = re.compile(r"\s+")
|
||||
if rar_executable_version == 4:
|
||||
while not line.startswith('-----------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
# asterisks mark password-encrypted files
|
||||
data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files
|
||||
fields = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(fields[0])
|
||||
attr = fields[5]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
elif rar_executable_version == 5:
|
||||
while not line.startswith('-----------'):
|
||||
fields = line.strip().lstrip("*").split()
|
||||
data = {}
|
||||
data['index'] = i
|
||||
data['filename'] = " ".join(fields[4:])
|
||||
data['size'] = int(fields[1])
|
||||
attr = fields[0]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
i += 1
|
||||
line = source.next()
|
||||
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if checkres==True and not info.isdir:
|
||||
pipe = self.call('p', ['inul'], [info.filename]).stdout
|
||||
res.append((info, pipe.read()))
|
||||
return res
|
||||
|
||||
|
||||
def extract(self, checker, path, withSubpath, overwrite):
|
||||
res = []
|
||||
command = 'x'
|
||||
if not withSubpath:
|
||||
command = 'e'
|
||||
options = []
|
||||
if overwrite:
|
||||
options.append('o+')
|
||||
else:
|
||||
options.append('o-')
|
||||
if not path.endswith(os.sep):
|
||||
path += os.sep
|
||||
names = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if type(checkres) in [str, unicode]:
|
||||
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
|
||||
if checkres==True and not info.isdir:
|
||||
names.append(info.filename)
|
||||
res.append(info)
|
||||
names.append(path)
|
||||
proc = self.call(command, options, names)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
|
||||
raise IncorrectRARPassword
|
||||
return res
|
||||
|
||||
def destruct(self):
|
||||
pass
|
||||
|
||||
|
||||
309
comicapi/UnRAR2/windows.py
Normal file
309
comicapi/UnRAR2/windows.py
Normal file
@@ -0,0 +1,309 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||
|
||||
from __future__ import generators
|
||||
|
||||
import ctypes, ctypes.wintypes
|
||||
import os, os.path, sys
|
||||
import Queue
|
||||
import time
|
||||
|
||||
from rar_exceptions import *
|
||||
|
||||
ERAR_END_ARCHIVE = 10
|
||||
ERAR_NO_MEMORY = 11
|
||||
ERAR_BAD_DATA = 12
|
||||
ERAR_BAD_ARCHIVE = 13
|
||||
ERAR_UNKNOWN_FORMAT = 14
|
||||
ERAR_EOPEN = 15
|
||||
ERAR_ECREATE = 16
|
||||
ERAR_ECLOSE = 17
|
||||
ERAR_EREAD = 18
|
||||
ERAR_EWRITE = 19
|
||||
ERAR_SMALL_BUF = 20
|
||||
ERAR_UNKNOWN = 21
|
||||
|
||||
RAR_OM_LIST = 0
|
||||
RAR_OM_EXTRACT = 1
|
||||
|
||||
RAR_SKIP = 0
|
||||
RAR_TEST = 1
|
||||
RAR_EXTRACT = 2
|
||||
|
||||
RAR_VOL_ASK = 0
|
||||
RAR_VOL_NOTIFY = 1
|
||||
|
||||
RAR_DLL_VERSION = 3
|
||||
|
||||
# enum UNRARCALLBACK_MESSAGES
|
||||
UCM_CHANGEVOLUME = 0
|
||||
UCM_PROCESSDATA = 1
|
||||
UCM_NEEDPASSWORD = 2
|
||||
|
||||
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
|
||||
dll_name = "unrar.dll"
|
||||
if architecture_bits == 64:
|
||||
dll_name = "x64\\unrar64.dll"
|
||||
|
||||
|
||||
try:
|
||||
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
|
||||
except WindowsError:
|
||||
unrar = ctypes.WinDLL(dll_name)
|
||||
|
||||
|
||||
class RAROpenArchiveDataEx(ctypes.Structure):
|
||||
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
|
||||
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||
|
||||
_fields_ = [
|
||||
('ArcName', ctypes.c_char_p),
|
||||
('ArcNameW', ctypes.c_wchar_p),
|
||||
('OpenMode', ctypes.c_uint),
|
||||
('OpenResult', ctypes.c_uint),
|
||||
('_CmtBuf', ctypes.c_voidp),
|
||||
('CmtBufSize', ctypes.c_uint),
|
||||
('CmtSize', ctypes.c_uint),
|
||||
('CmtState', ctypes.c_uint),
|
||||
('Flags', ctypes.c_uint),
|
||||
('Reserved', ctypes.c_uint*32),
|
||||
]
|
||||
|
||||
class RARHeaderDataEx(ctypes.Structure):
|
||||
def __init__(self):
|
||||
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||
|
||||
_fields_ = [
|
||||
('ArcName', ctypes.c_char*1024),
|
||||
('ArcNameW', ctypes.c_wchar*1024),
|
||||
('FileName', ctypes.c_char*1024),
|
||||
('FileNameW', ctypes.c_wchar*1024),
|
||||
('Flags', ctypes.c_uint),
|
||||
('PackSize', ctypes.c_uint),
|
||||
('PackSizeHigh', ctypes.c_uint),
|
||||
('UnpSize', ctypes.c_uint),
|
||||
('UnpSizeHigh', ctypes.c_uint),
|
||||
('HostOS', ctypes.c_uint),
|
||||
('FileCRC', ctypes.c_uint),
|
||||
('FileTime', ctypes.c_uint),
|
||||
('UnpVer', ctypes.c_uint),
|
||||
('Method', ctypes.c_uint),
|
||||
('FileAttr', ctypes.c_uint),
|
||||
('_CmtBuf', ctypes.c_voidp),
|
||||
('CmtBufSize', ctypes.c_uint),
|
||||
('CmtSize', ctypes.c_uint),
|
||||
('CmtState', ctypes.c_uint),
|
||||
('Reserved', ctypes.c_uint*1024),
|
||||
]
|
||||
|
||||
def DosDateTimeToTimeTuple(dosDateTime):
|
||||
"""Convert an MS-DOS format date time to a Python time tuple.
|
||||
"""
|
||||
dosDate = dosDateTime >> 16
|
||||
dosTime = dosDateTime & 0xffff
|
||||
day = dosDate & 0x1f
|
||||
month = (dosDate >> 5) & 0xf
|
||||
year = 1980 + (dosDate >> 9)
|
||||
second = 2*(dosTime & 0x1f)
|
||||
minute = (dosTime >> 5) & 0x3f
|
||||
hour = dosTime >> 11
|
||||
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))
|
||||
|
||||
def _wrap(restype, function, argtypes):
|
||||
result = function
|
||||
result.argtypes = argtypes
|
||||
result.restype = restype
|
||||
return result
|
||||
|
||||
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])
|
||||
|
||||
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])
|
||||
|
||||
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])
|
||||
|
||||
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
|
||||
def RARSetPassword(*args, **kwargs):
|
||||
_RARSetPassword(*args, **kwargs)
|
||||
|
||||
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])
|
||||
|
||||
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])
|
||||
|
||||
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
|
||||
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])
|
||||
|
||||
|
||||
|
||||
RARExceptions = {
|
||||
ERAR_NO_MEMORY : MemoryError,
|
||||
ERAR_BAD_DATA : ArchiveHeaderBroken,
|
||||
ERAR_BAD_ARCHIVE : InvalidRARArchive,
|
||||
ERAR_EOPEN : FileOpenError,
|
||||
}
|
||||
|
||||
class PassiveReader:
|
||||
"""Used for reading files to memory"""
|
||||
def __init__(self, usercallback = None):
|
||||
self.buf = []
|
||||
self.ucb = usercallback
|
||||
|
||||
def _callback(self, msg, UserData, P1, P2):
|
||||
if msg == UCM_PROCESSDATA:
|
||||
data = (ctypes.c_char*P2).from_address(P1).raw
|
||||
if self.ucb!=None:
|
||||
self.ucb(data)
|
||||
else:
|
||||
self.buf.append(data)
|
||||
return 1
|
||||
|
||||
def get_result(self):
|
||||
return ''.join(self.buf)
|
||||
|
||||
class RarInfoIterator(object):
|
||||
def __init__(self, arc):
|
||||
self.arc = arc
|
||||
self.index = 0
|
||||
self.headerData = RARHeaderDataEx()
|
||||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||
if self.res==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
self.arc.lockStatus = "locked"
|
||||
self.arc.needskip = False
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if self.index>0:
|
||||
if self.arc.needskip:
|
||||
RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
|
||||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||
|
||||
if self.res:
|
||||
raise StopIteration
|
||||
self.arc.needskip = True
|
||||
|
||||
data = {}
|
||||
data['index'] = self.index
|
||||
data['filename'] = self.headerData.FileName
|
||||
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
|
||||
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
|
||||
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
|
||||
if self.headerData.CmtState == 1:
|
||||
data['comment'] = self.headerData.CmtBuf.value
|
||||
else:
|
||||
data['comment'] = None
|
||||
self.index += 1
|
||||
return data
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.arc.lockStatus = "finished"
|
||||
|
||||
def generate_password_provider(password):
|
||||
def password_provider_callback(msg, UserData, P1, P2):
|
||||
if msg == UCM_NEEDPASSWORD and password!=None:
|
||||
(ctypes.c_char*P2).from_address(P1).value = password
|
||||
return 1
|
||||
return password_provider_callback
|
||||
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
self.password = password
|
||||
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
|
||||
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
|
||||
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
|
||||
RARSetCallback(self._handle, self.c_callback, 1)
|
||||
|
||||
if archiveData.OpenResult != 0:
|
||||
raise RARExceptions[archiveData.OpenResult]
|
||||
|
||||
if archiveData.CmtState == 1:
|
||||
self.comment = archiveData.CmtBuf.value
|
||||
else:
|
||||
self.comment = None
|
||||
|
||||
if password:
|
||||
RARSetPassword(self._handle, password)
|
||||
|
||||
self.lockStatus = "ready"
|
||||
|
||||
|
||||
|
||||
def destruct(self):
|
||||
if self._handle and RARCloseArchive:
|
||||
RARCloseArchive(self._handle)
|
||||
|
||||
def make_sure_ready(self):
|
||||
if self.lockStatus == "locked":
|
||||
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
|
||||
if self.lockStatus == "finished":
|
||||
self.destruct()
|
||||
self.init(self.password)
|
||||
|
||||
def infoiter(self):
|
||||
self.make_sure_ready()
|
||||
return RarInfoIterator(self)
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
if checker(info) and not info.isdir:
|
||||
reader = PassiveReader()
|
||||
c_callback = UNRARCALLBACK(reader._callback)
|
||||
RARSetCallback(self._handle, c_callback, 1)
|
||||
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
|
||||
if tmpres==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
self.needskip = False
|
||||
res.append((info, reader.get_result()))
|
||||
return res
|
||||
|
||||
|
||||
def extract(self, checker, path, withSubpath, overwrite):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if checkres!=False and not info.isdir:
|
||||
if checkres==True:
|
||||
fn = info.filename
|
||||
if not withSubpath:
|
||||
fn = os.path.split(fn)[-1]
|
||||
target = os.path.join(path, fn)
|
||||
else:
|
||||
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
|
||||
target = checkres
|
||||
if overwrite or (not os.path.exists(target)):
|
||||
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
|
||||
if tmpres==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
|
||||
self.needskip = False
|
||||
res.append(info)
|
||||
return res
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__author__ = "dromanin"
|
||||
__author__ = 'dromanin'
|
||||
|
||||
@@ -14,125 +14,176 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
#from datetime import datetime
|
||||
#from pprint import pprint
|
||||
#import zipfile
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
|
||||
|
||||
class CoMet:
|
||||
|
||||
writer_synonyms = ["writer", "plotter", "scripter"]
|
||||
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
|
||||
inker_synonyms = ["inker", "artist", "finishes"]
|
||||
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
|
||||
letterer_synonyms = ["letterer"]
|
||||
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
|
||||
editor_synonyms = ["editor"]
|
||||
writer_synonyms = ['writer', 'plotter', 'scripter']
|
||||
penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns']
|
||||
inker_synonyms = ['inker', 'artist', 'finishes']
|
||||
colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer']
|
||||
letterer_synonyms = ['letterer']
|
||||
cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist']
|
||||
editor_synonyms = ['editor']
|
||||
|
||||
def metadata_from_string(self, string):
|
||||
def metadataFromString(self, string):
|
||||
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
return self.convertXMLToMetadata(tree)
|
||||
|
||||
def string_from_metadata(self, metadata):
|
||||
def stringFromMetadata(self, metadata):
|
||||
|
||||
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
|
||||
tree = self.convert_metadata_to_xml(metadata)
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
return header + ET.tostring(tree.getroot())
|
||||
|
||||
def convert_metadata_to_xml(self, metadata):
|
||||
def indent(self, elem, level=0):
|
||||
# for making the XML output readable
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
self.indent(elem, level + 1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def convertMetadataToXML(self, filename, metadata):
|
||||
|
||||
# shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
# build a tree structure
|
||||
root = ET.Element("comet")
|
||||
root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/"
|
||||
root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
|
||||
root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/"
|
||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib[
|
||||
'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
|
||||
|
||||
# helper func
|
||||
def assign(comet_entry, md_entry):
|
||||
if md_entry is not None:
|
||||
ET.SubElement(root, comet_entry).text = str(md_entry)
|
||||
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
|
||||
|
||||
# title is manditory
|
||||
if md.title is None:
|
||||
md.title = ""
|
||||
assign("title", md.title)
|
||||
assign("series", md.series)
|
||||
assign("issue", md.issue) # must be int??
|
||||
assign("volume", md.volume)
|
||||
assign("description", md.comments)
|
||||
assign("publisher", md.publisher)
|
||||
assign("pages", md.page_count)
|
||||
assign("format", md.format)
|
||||
assign("language", md.language)
|
||||
assign("rating", md.maturity_rating)
|
||||
assign("price", md.price)
|
||||
assign("isVersionOf", md.is_version_of)
|
||||
assign("rights", md.rights)
|
||||
assign("identifier", md.identifier)
|
||||
assign("lastMark", md.last_mark)
|
||||
assign("genre", md.genre) # TODO repeatable
|
||||
assign('title', md.title)
|
||||
assign('series', md.series)
|
||||
assign('issue', md.issue) # must be int??
|
||||
assign('volume', md.volume)
|
||||
assign('description', md.comments)
|
||||
assign('publisher', md.publisher)
|
||||
assign('pages', md.pageCount)
|
||||
assign('format', md.format)
|
||||
assign('language', md.language)
|
||||
assign('rating', md.maturityRating)
|
||||
assign('price', md.price)
|
||||
assign('isVersionOf', md.isVersionOf)
|
||||
assign('rights', md.rights)
|
||||
assign('identifier', md.identifier)
|
||||
assign('lastMark', md.lastMark)
|
||||
assign('genre', md.genre) # TODO repeatable
|
||||
|
||||
if md.characters is not None:
|
||||
char_list = [c.strip() for c in md.characters.split(",")]
|
||||
char_list = [c.strip() for c in md.characters.split(',')]
|
||||
for c in char_list:
|
||||
assign("character", c)
|
||||
assign('character', c)
|
||||
|
||||
if md.manga is not None and md.manga == "YesAndRightToLeft":
|
||||
assign("readingDirection", "rtl")
|
||||
assign('readingDirection', "rtl")
|
||||
|
||||
date_str = ""
|
||||
if md.year is not None:
|
||||
date_str = str(md.year).zfill(4)
|
||||
if md.month is not None:
|
||||
date_str += "-" + str(md.month).zfill(2)
|
||||
assign("date", date_str)
|
||||
assign('date', date_str)
|
||||
|
||||
assign("coverImage", md.cover_image)
|
||||
assign('coverImage', md.coverImage)
|
||||
|
||||
# need to specially process the credits, since they are structured
|
||||
# differently than CIX
|
||||
credit_writer_list = list()
|
||||
credit_penciller_list = list()
|
||||
credit_inker_list = list()
|
||||
credit_colorist_list = list()
|
||||
credit_letterer_list = list()
|
||||
credit_cover_list = list()
|
||||
credit_editor_list = list()
|
||||
|
||||
# loop thru credits, and build a list for each role that CoMet supports
|
||||
for credit in metadata.credits:
|
||||
|
||||
if credit["role"].lower() in set(self.writer_synonyms):
|
||||
ET.SubElement(root, "writer").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.writer_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'writer').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
if credit["role"].lower() in set(self.penciller_synonyms):
|
||||
ET.SubElement(root, "penciller").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.penciller_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'penciller').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
if credit["role"].lower() in set(self.inker_synonyms):
|
||||
ET.SubElement(root, "inker").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.inker_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'inker').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
if credit["role"].lower() in set(self.colorist_synonyms):
|
||||
ET.SubElement(root, "colorist").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.colorist_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'colorist').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
if credit["role"].lower() in set(self.letterer_synonyms):
|
||||
ET.SubElement(root, "letterer").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.letterer_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'letterer').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
if credit["role"].lower() in set(self.cover_synonyms):
|
||||
ET.SubElement(root, "coverDesigner").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.cover_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'coverDesigner').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
if credit["role"].lower() in set(self.editor_synonyms):
|
||||
ET.SubElement(root, "editor").text = str(credit["person"])
|
||||
if credit['role'].lower() in set(self.editor_synonyms):
|
||||
ET.SubElement(
|
||||
root,
|
||||
'editor').text = u"{0}".format(
|
||||
credit['person'])
|
||||
|
||||
ET.indent(root)
|
||||
# self pretty-print
|
||||
self.indent(root)
|
||||
|
||||
# wrap it in an ElementTree instance, and save as XML
|
||||
tree = ET.ElementTree(root)
|
||||
return tree
|
||||
|
||||
def convert_xml_to_metadata(self, tree):
|
||||
def convertXMLToMetadata(self, tree):
|
||||
|
||||
root = tree.getroot()
|
||||
|
||||
if root.tag != "comet":
|
||||
raise "1"
|
||||
if root.tag != 'comet':
|
||||
raise 1
|
||||
return None
|
||||
|
||||
metadata = GenericMetadata()
|
||||
md = metadata
|
||||
@@ -142,85 +193,84 @@ class CoMet:
|
||||
node = root.find(tag)
|
||||
if node is not None:
|
||||
return node.text
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
md.series = xlate("series")
|
||||
md.title = xlate("title")
|
||||
md.issue = xlate("issue")
|
||||
md.volume = xlate("volume")
|
||||
md.comments = xlate("description")
|
||||
md.publisher = xlate("publisher")
|
||||
md.language = xlate("language")
|
||||
md.format = xlate("format")
|
||||
md.page_count = xlate("pages")
|
||||
md.maturity_rating = xlate("rating")
|
||||
md.price = xlate("price")
|
||||
md.is_version_of = xlate("isVersionOf")
|
||||
md.rights = xlate("rights")
|
||||
md.identifier = xlate("identifier")
|
||||
md.last_mark = xlate("lastMark")
|
||||
md.genre = xlate("genre") # TODO - repeatable field
|
||||
md.series = xlate('series')
|
||||
md.title = xlate('title')
|
||||
md.issue = xlate('issue')
|
||||
md.volume = xlate('volume')
|
||||
md.comments = xlate('description')
|
||||
md.publisher = xlate('publisher')
|
||||
md.language = xlate('language')
|
||||
md.format = xlate('format')
|
||||
md.pageCount = xlate('pages')
|
||||
md.maturityRating = xlate('rating')
|
||||
md.price = xlate('price')
|
||||
md.isVersionOf = xlate('isVersionOf')
|
||||
md.rights = xlate('rights')
|
||||
md.identifier = xlate('identifier')
|
||||
md.lastMark = xlate('lastMark')
|
||||
md.genre = xlate('genre') # TODO - repeatable field
|
||||
|
||||
date = xlate("date")
|
||||
date = xlate('date')
|
||||
if date is not None:
|
||||
parts = date.split("-")
|
||||
parts = date.split('-')
|
||||
if len(parts) > 0:
|
||||
md.year = parts[0]
|
||||
if len(parts) > 1:
|
||||
md.month = parts[1]
|
||||
|
||||
md.cover_image = xlate("coverImage")
|
||||
md.coverImage = xlate('coverImage')
|
||||
|
||||
reading_direction = xlate("readingDirection")
|
||||
if reading_direction is not None and reading_direction == "rtl":
|
||||
readingDirection = xlate('readingDirection')
|
||||
if readingDirection is not None and readingDirection == "rtl":
|
||||
md.manga = "YesAndRightToLeft"
|
||||
|
||||
# loop for character tags
|
||||
char_list = []
|
||||
for n in root:
|
||||
if n.tag == "character":
|
||||
if n.tag == 'character':
|
||||
char_list.append(n.text.strip())
|
||||
md.characters = utils.list_to_string(char_list)
|
||||
md.characters = utils.listToString(char_list)
|
||||
|
||||
# Now extract the credit info
|
||||
for n in root:
|
||||
if any(
|
||||
[
|
||||
n.tag == "writer",
|
||||
n.tag == "penciller",
|
||||
n.tag == "inker",
|
||||
n.tag == "colorist",
|
||||
n.tag == "letterer",
|
||||
n.tag == "editor",
|
||||
]
|
||||
):
|
||||
metadata.add_credit(n.text.strip(), n.tag.title())
|
||||
if (n.tag == 'writer' or
|
||||
n.tag == 'penciller' or
|
||||
n.tag == 'inker' or
|
||||
n.tag == 'colorist' or
|
||||
n.tag == 'letterer' or
|
||||
n.tag == 'editor'
|
||||
):
|
||||
metadata.addCredit(n.text.strip(), n.tag.title())
|
||||
|
||||
if n.tag == "coverDesigner":
|
||||
metadata.add_credit(n.text.strip(), "Cover")
|
||||
if n.tag == 'coverDesigner':
|
||||
metadata.addCredit(n.text.strip(), "Cover")
|
||||
|
||||
metadata.is_empty = False
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
|
||||
# verify that the string actually contains CoMet data in XML format
|
||||
def validate_string(self, string):
|
||||
def validateString(self, string):
|
||||
try:
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
root = tree.getroot()
|
||||
if root.tag != "comet":
|
||||
if root.tag != 'comet':
|
||||
raise Exception
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def write_to_external_file(self, filename, metadata):
|
||||
def writeToExternalFile(self, filename, metadata):
|
||||
|
||||
tree = self.convert_metadata_to_xml(metadata)
|
||||
tree.write(filename, encoding="utf-8")
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
# ET.dump(tree)
|
||||
tree.write(filename, encoding='utf-8')
|
||||
|
||||
def read_from_external_file(self, filename):
|
||||
def readFromExternalFile(self, filename):
|
||||
|
||||
tree = ET.parse(filename)
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
return self.convertXMLToMetadata(tree)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,42 +15,48 @@
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
#import zipfile
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
#import ctversion
|
||||
|
||||
|
||||
class ComicBookInfo:
|
||||
def metadata_from_string(self, string):
|
||||
|
||||
cbi_container = json.loads(str(string, "utf-8"))
|
||||
def metadataFromString(self, string):
|
||||
|
||||
cbi_container = json.loads(unicode(string, 'utf-8'))
|
||||
|
||||
metadata = GenericMetadata()
|
||||
|
||||
cbi = defaultdict(lambda: None, cbi_container["ComicBookInfo/1.0"])
|
||||
cbi = cbi_container['ComicBookInfo/1.0']
|
||||
|
||||
metadata.series = utils.xlate(cbi["series"])
|
||||
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.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.language = utils.xlate(cbi["language"])
|
||||
metadata.country = utils.xlate(cbi["country"])
|
||||
metadata.critical_rating = utils.xlate(cbi["rating"])
|
||||
# helper func
|
||||
# If item is not in CBI, return None
|
||||
def xlate(cbi_entry):
|
||||
if cbi_entry in cbi:
|
||||
return cbi[cbi_entry]
|
||||
else:
|
||||
return None
|
||||
|
||||
metadata.credits = cbi["credits"]
|
||||
metadata.tags = cbi["tags"]
|
||||
metadata.series = xlate('series')
|
||||
metadata.title = xlate('title')
|
||||
metadata.issue = xlate('issue')
|
||||
metadata.publisher = xlate('publisher')
|
||||
metadata.month = xlate('publicationMonth')
|
||||
metadata.year = xlate('publicationYear')
|
||||
metadata.issueCount = xlate('numberOfIssues')
|
||||
metadata.comments = xlate('comments')
|
||||
metadata.credits = xlate('credits')
|
||||
metadata.genre = xlate('genre')
|
||||
metadata.volume = xlate('volume')
|
||||
metadata.volumeCount = xlate('numberOfVolumes')
|
||||
metadata.language = xlate('language')
|
||||
metadata.country = xlate('country')
|
||||
metadata.criticalRating = xlate('rating')
|
||||
metadata.tags = xlate('tags')
|
||||
|
||||
# make sure credits and tags are at least empty lists and not None
|
||||
if metadata.credits is None:
|
||||
@@ -58,20 +64,26 @@ class ComicBookInfo:
|
||||
if metadata.tags is None:
|
||||
metadata.tags = []
|
||||
|
||||
# need the language string to be ISO
|
||||
# need to massage the language string to be ISO
|
||||
if metadata.language is not None:
|
||||
metadata.language = utils.get_language(metadata.language)
|
||||
# reverse look-up
|
||||
pattern = metadata.language
|
||||
metadata.language = None
|
||||
for key in utils.getLanguageDict():
|
||||
if utils.getLanguageDict()[key] == pattern.encode('utf-8'):
|
||||
metadata.language = key
|
||||
break
|
||||
|
||||
metadata.is_empty = False
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
|
||||
def string_from_metadata(self, metadata):
|
||||
def stringFromMetadata(self, metadata):
|
||||
|
||||
cbi_container = self.create_json_dictionary(metadata)
|
||||
cbi_container = self.createJSONDictionary(metadata)
|
||||
return json.dumps(cbi_container)
|
||||
|
||||
def validate_string(self, string):
|
||||
def validateString(self, string):
|
||||
"""Verify that the string actually contains CBI data in JSON format"""
|
||||
|
||||
try:
|
||||
@@ -79,45 +91,54 @@ class ComicBookInfo:
|
||||
except:
|
||||
return False
|
||||
|
||||
return "ComicBookInfo/1.0" in cbi_container
|
||||
return ('ComicBookInfo/1.0' in cbi_container)
|
||||
|
||||
def create_json_dictionary(self, metadata):
|
||||
def createJSONDictionary(self, metadata):
|
||||
"""Create the dictionary that we will convert to JSON text"""
|
||||
|
||||
cbi = {}
|
||||
cbi_container = {
|
||||
"appID": "ComicTagger/" + "1.0.0",
|
||||
"lastModified": str(datetime.now()),
|
||||
"ComicBookInfo/1.0": cbi,
|
||||
} # TODO: ctversion.version,
|
||||
cbi = dict()
|
||||
cbi_container = {'appID': 'ComicTagger/' + '1.0.0', # ctversion.version,
|
||||
'lastModified': str(datetime.now()),
|
||||
'ComicBookInfo/1.0': cbi}
|
||||
|
||||
# helper func
|
||||
def assign(cbi_entry, md_entry):
|
||||
if md_entry is not None or isinstance(md_entry, str) and md_entry != "":
|
||||
if md_entry is not None:
|
||||
cbi[cbi_entry] = md_entry
|
||||
|
||||
assign("series", utils.xlate(metadata.series))
|
||||
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("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("language", utils.xlate(utils.get_language_from_iso(metadata.language)))
|
||||
assign("country", utils.xlate(metadata.country))
|
||||
assign("rating", utils.xlate(metadata.critical_rating))
|
||||
assign("credits", metadata.credits)
|
||||
assign("tags", metadata.tags)
|
||||
# helper func
|
||||
def toInt(s):
|
||||
i = None
|
||||
if type(s) in [str, unicode, int]:
|
||||
try:
|
||||
i = int(s)
|
||||
except ValueError:
|
||||
pass
|
||||
return i
|
||||
|
||||
assign('series', metadata.series)
|
||||
assign('title', metadata.title)
|
||||
assign('issue', metadata.issue)
|
||||
assign('publisher', metadata.publisher)
|
||||
assign('publicationMonth', toInt(metadata.month))
|
||||
assign('publicationYear', toInt(metadata.year))
|
||||
assign('numberOfIssues', toInt(metadata.issueCount))
|
||||
assign('comments', metadata.comments)
|
||||
assign('genre', metadata.genre)
|
||||
assign('volume', toInt(metadata.volume))
|
||||
assign('numberOfVolumes', toInt(metadata.volumeCount))
|
||||
assign('language', utils.getLanguageFromISO(metadata.language))
|
||||
assign('country', metadata.country)
|
||||
assign('rating', metadata.criticalRating)
|
||||
assign('credits', metadata.credits)
|
||||
assign('tags', metadata.tags)
|
||||
|
||||
return cbi_container
|
||||
|
||||
def write_to_external_file(self, filename, metadata):
|
||||
def writeToExternalFile(self, filename, metadata):
|
||||
|
||||
cbi_container = self.create_json_dictionary(metadata)
|
||||
cbi_container = self.createJSONDictionary(metadata)
|
||||
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(cbi_container, indent=4))
|
||||
f = open(filename, 'w')
|
||||
f.write(json.dumps(cbi_container, indent=4))
|
||||
f.close
|
||||
|
||||
@@ -14,27 +14,26 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
#from datetime import datetime
|
||||
#from pprint import pprint
|
||||
#import zipfile
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
|
||||
|
||||
class ComicInfoXml:
|
||||
|
||||
writer_synonyms = ["writer", "plotter", "scripter"]
|
||||
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
|
||||
inker_synonyms = ["inker", "artist", "finishes"]
|
||||
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
|
||||
letterer_synonyms = ["letterer"]
|
||||
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
|
||||
editor_synonyms = ["editor"]
|
||||
writer_synonyms = ['writer', 'plotter', 'scripter']
|
||||
penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns']
|
||||
inker_synonyms = ['inker', 'artist', 'finishes']
|
||||
colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer']
|
||||
letterer_synonyms = ['letterer']
|
||||
cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist']
|
||||
editor_synonyms = ['editor']
|
||||
|
||||
def get_parseable_credits(self):
|
||||
def getParseableCredits(self):
|
||||
parsable_credits = []
|
||||
parsable_credits.extend(self.writer_synonyms)
|
||||
parsable_credits.extend(self.penciller_synonyms)
|
||||
@@ -45,231 +44,247 @@ class ComicInfoXml:
|
||||
parsable_credits.extend(self.editor_synonyms)
|
||||
return parsable_credits
|
||||
|
||||
def metadata_from_string(self, string):
|
||||
def metadataFromString(self, string):
|
||||
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
return self.convertXMLToMetadata(tree)
|
||||
|
||||
def string_from_metadata(self, metadata, xml=None):
|
||||
tree = self.convert_metadata_to_xml(self, metadata, xml)
|
||||
tree_str = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode()
|
||||
return tree_str
|
||||
def stringFromMetadata(self, metadata):
|
||||
|
||||
def convert_metadata_to_xml(self, filename, metadata, xml=None):
|
||||
header = '<?xml version="1.0"?>\n'
|
||||
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
return header + ET.tostring(tree.getroot())
|
||||
|
||||
def indent(self, elem, level=0):
|
||||
# for making the XML output readable
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
self.indent(elem, level + 1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def convertMetadataToXML(self, filename, metadata):
|
||||
|
||||
# shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
if xml:
|
||||
root = ET.ElementTree(ET.fromstring(xml)).getroot()
|
||||
else:
|
||||
# build a tree structure
|
||||
root = ET.Element("ComicInfo")
|
||||
root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema"
|
||||
# build a tree structure
|
||||
root = ET.Element("ComicInfo")
|
||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
|
||||
# helper func
|
||||
|
||||
def assign(cix_entry, md_entry):
|
||||
if md_entry is not None and md_entry:
|
||||
et_entry = root.find(cix_entry)
|
||||
if et_entry is not None:
|
||||
et_entry.text = str(md_entry)
|
||||
else:
|
||||
ET.SubElement(root, cix_entry).text = str(md_entry)
|
||||
else:
|
||||
et_entry = root.find(cix_entry)
|
||||
if et_entry is not None:
|
||||
root.remove(et_entry)
|
||||
if md_entry is not None:
|
||||
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
|
||||
|
||||
assign("Title", md.title)
|
||||
assign("Series", md.series)
|
||||
assign("Number", md.issue)
|
||||
assign("Count", md.issue_count)
|
||||
assign("Volume", md.volume)
|
||||
assign("AlternateSeries", md.alternate_series)
|
||||
assign("AlternateNumber", md.alternate_number)
|
||||
assign("StoryArc", md.story_arc)
|
||||
assign("SeriesGroup", md.series_group)
|
||||
assign("AlternateCount", md.alternate_count)
|
||||
assign("Summary", md.comments)
|
||||
assign("Notes", md.notes)
|
||||
assign("Year", md.year)
|
||||
assign("Month", md.month)
|
||||
assign("Day", md.day)
|
||||
assign('Title', md.title)
|
||||
assign('Series', md.series)
|
||||
assign('Number', md.issue)
|
||||
assign('Count', md.issueCount)
|
||||
assign('Volume', md.volume)
|
||||
assign('AlternateSeries', md.alternateSeries)
|
||||
assign('AlternateNumber', md.alternateNumber)
|
||||
assign('StoryArc', md.storyArc)
|
||||
assign('SeriesGroup', md.seriesGroup)
|
||||
assign('AlternateCount', md.alternateCount)
|
||||
assign('Summary', md.comments)
|
||||
assign('Notes', md.notes)
|
||||
assign('Year', md.year)
|
||||
assign('Month', md.month)
|
||||
assign('Day', md.day)
|
||||
|
||||
# need to specially process the credits, since they are structured
|
||||
# differently than CIX
|
||||
credit_writer_list = []
|
||||
credit_penciller_list = []
|
||||
credit_inker_list = []
|
||||
credit_colorist_list = []
|
||||
credit_letterer_list = []
|
||||
credit_cover_list = []
|
||||
credit_editor_list = []
|
||||
credit_writer_list = list()
|
||||
credit_penciller_list = list()
|
||||
credit_inker_list = list()
|
||||
credit_colorist_list = list()
|
||||
credit_letterer_list = list()
|
||||
credit_cover_list = list()
|
||||
credit_editor_list = list()
|
||||
|
||||
# first, loop thru credits, and build a list for each role that CIX
|
||||
# supports
|
||||
for credit in metadata.credits:
|
||||
|
||||
if credit["role"].lower() in set(self.writer_synonyms):
|
||||
credit_writer_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.writer_synonyms):
|
||||
credit_writer_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
if credit["role"].lower() in set(self.penciller_synonyms):
|
||||
credit_penciller_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.penciller_synonyms):
|
||||
credit_penciller_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
if credit["role"].lower() in set(self.inker_synonyms):
|
||||
credit_inker_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.inker_synonyms):
|
||||
credit_inker_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
if credit["role"].lower() in set(self.colorist_synonyms):
|
||||
credit_colorist_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.colorist_synonyms):
|
||||
credit_colorist_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
if credit["role"].lower() in set(self.letterer_synonyms):
|
||||
credit_letterer_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.letterer_synonyms):
|
||||
credit_letterer_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
if credit["role"].lower() in set(self.cover_synonyms):
|
||||
credit_cover_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.cover_synonyms):
|
||||
credit_cover_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
if credit["role"].lower() in set(self.editor_synonyms):
|
||||
credit_editor_list.append(credit["person"].replace(",", ""))
|
||||
if credit['role'].lower() in set(self.editor_synonyms):
|
||||
credit_editor_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
# second, convert each list to string, and add to XML struct
|
||||
assign("Writer", utils.list_to_string(credit_writer_list))
|
||||
if len(credit_writer_list) > 0:
|
||||
node = ET.SubElement(root, 'Writer')
|
||||
node.text = utils.listToString(credit_writer_list)
|
||||
|
||||
assign("Penciller", utils.list_to_string(credit_penciller_list))
|
||||
if len(credit_penciller_list) > 0:
|
||||
node = ET.SubElement(root, 'Penciller')
|
||||
node.text = utils.listToString(credit_penciller_list)
|
||||
|
||||
assign("Inker", utils.list_to_string(credit_inker_list))
|
||||
if len(credit_inker_list) > 0:
|
||||
node = ET.SubElement(root, 'Inker')
|
||||
node.text = utils.listToString(credit_inker_list)
|
||||
|
||||
assign("Colorist", utils.list_to_string(credit_colorist_list))
|
||||
if len(credit_colorist_list) > 0:
|
||||
node = ET.SubElement(root, 'Colorist')
|
||||
node.text = utils.listToString(credit_colorist_list)
|
||||
|
||||
assign("Letterer", utils.list_to_string(credit_letterer_list))
|
||||
if len(credit_letterer_list) > 0:
|
||||
node = ET.SubElement(root, 'Letterer')
|
||||
node.text = utils.listToString(credit_letterer_list)
|
||||
|
||||
assign("CoverArtist", utils.list_to_string(credit_cover_list))
|
||||
if len(credit_cover_list) > 0:
|
||||
node = ET.SubElement(root, 'CoverArtist')
|
||||
node.text = utils.listToString(credit_cover_list)
|
||||
|
||||
assign("Editor", utils.list_to_string(credit_editor_list))
|
||||
if len(credit_editor_list) > 0:
|
||||
node = ET.SubElement(root, 'Editor')
|
||||
node.text = utils.listToString(credit_editor_list)
|
||||
|
||||
assign("Publisher", md.publisher)
|
||||
assign("Imprint", md.imprint)
|
||||
assign("Genre", md.genre)
|
||||
assign("Web", md.web_link)
|
||||
assign("PageCount", md.page_count)
|
||||
assign("LanguageISO", md.language)
|
||||
assign("Format", md.format)
|
||||
assign("AgeRating", md.maturity_rating)
|
||||
assign("CommunityRating", md.community_rating)
|
||||
assign("BlackAndWhite", "Yes" if md.black_and_white else None)
|
||||
assign("Manga", md.manga)
|
||||
assign("Characters", md.characters)
|
||||
assign("Teams", md.teams)
|
||||
assign("Locations", md.locations)
|
||||
assign("ScanInformation", md.scan_info)
|
||||
assign('Publisher', md.publisher)
|
||||
assign('Imprint', md.imprint)
|
||||
assign('Genre', md.genre)
|
||||
assign('Web', md.webLink)
|
||||
assign('PageCount', md.pageCount)
|
||||
assign('LanguageISO', md.language)
|
||||
assign('Format', md.format)
|
||||
assign('AgeRating', md.maturityRating)
|
||||
if md.blackAndWhite is not None and md.blackAndWhite:
|
||||
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
|
||||
assign('Manga', md.manga)
|
||||
assign('Characters', md.characters)
|
||||
assign('Teams', md.teams)
|
||||
assign('Locations', md.locations)
|
||||
assign('ScanInformation', md.scanInfo)
|
||||
|
||||
# loop and add the page entries under pages node
|
||||
pages_node = root.find("Pages")
|
||||
if pages_node is not None:
|
||||
pages_node.clear()
|
||||
else:
|
||||
pages_node = ET.SubElement(root, "Pages")
|
||||
if len(md.pages) > 0:
|
||||
pages_node = ET.SubElement(root, 'Pages')
|
||||
for page_dict in md.pages:
|
||||
page_node = ET.SubElement(pages_node, 'Page')
|
||||
page_node.attrib = page_dict
|
||||
|
||||
for page_dict in md.pages:
|
||||
page = page_dict
|
||||
if "Image" in page:
|
||||
page["Image"] = str(page["Image"])
|
||||
page_node = ET.SubElement(pages_node, "Page")
|
||||
page_node.attrib = dict(sorted(page_dict.items()))
|
||||
|
||||
ET.indent(root)
|
||||
# self pretty-print
|
||||
self.indent(root)
|
||||
|
||||
# wrap it in an ElementTree instance, and save as XML
|
||||
tree = ET.ElementTree(root)
|
||||
return tree
|
||||
|
||||
def convert_xml_to_metadata(self, tree):
|
||||
def convertXMLToMetadata(self, tree):
|
||||
|
||||
root = tree.getroot()
|
||||
|
||||
if root.tag != "ComicInfo":
|
||||
raise "1"
|
||||
if root.tag != 'ComicInfo':
|
||||
raise 1
|
||||
return None
|
||||
|
||||
def get(name):
|
||||
tag = root.find(name)
|
||||
if tag is None:
|
||||
metadata = GenericMetadata()
|
||||
md = metadata
|
||||
|
||||
# Helper function
|
||||
def xlate(tag):
|
||||
node = root.find(tag)
|
||||
if node is not None:
|
||||
return node.text
|
||||
else:
|
||||
return None
|
||||
return tag.text
|
||||
|
||||
md = GenericMetadata()
|
||||
md.series = xlate('Series')
|
||||
md.title = xlate('Title')
|
||||
md.issue = xlate('Number')
|
||||
md.issueCount = xlate('Count')
|
||||
md.volume = xlate('Volume')
|
||||
md.alternateSeries = xlate('AlternateSeries')
|
||||
md.alternateNumber = xlate('AlternateNumber')
|
||||
md.alternateCount = xlate('AlternateCount')
|
||||
md.comments = xlate('Summary')
|
||||
md.notes = xlate('Notes')
|
||||
md.year = xlate('Year')
|
||||
md.month = xlate('Month')
|
||||
md.day = xlate('Day')
|
||||
md.publisher = xlate('Publisher')
|
||||
md.imprint = xlate('Imprint')
|
||||
md.genre = xlate('Genre')
|
||||
md.webLink = xlate('Web')
|
||||
md.language = xlate('LanguageISO')
|
||||
md.format = xlate('Format')
|
||||
md.manga = xlate('Manga')
|
||||
md.characters = xlate('Characters')
|
||||
md.teams = xlate('Teams')
|
||||
md.locations = xlate('Locations')
|
||||
md.pageCount = xlate('PageCount')
|
||||
md.scanInfo = xlate('ScanInformation')
|
||||
md.storyArc = xlate('StoryArc')
|
||||
md.seriesGroup = xlate('SeriesGroup')
|
||||
md.maturityRating = xlate('AgeRating')
|
||||
|
||||
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.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.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.publisher = utils.xlate(get("Publisher"))
|
||||
md.imprint = utils.xlate(get("Imprint"))
|
||||
md.genre = utils.xlate(get("Genre"))
|
||||
md.web_link = utils.xlate(get("Web"))
|
||||
md.language = utils.xlate(get("LanguageISO"))
|
||||
md.format = utils.xlate(get("Format"))
|
||||
md.manga = utils.xlate(get("Manga"))
|
||||
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.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.community_rating = utils.xlate(get("CommunityRating"))
|
||||
|
||||
tmp = utils.xlate(get("BlackAndWhite"))
|
||||
tmp = xlate('BlackAndWhite')
|
||||
md.blackAndWhite = False
|
||||
if tmp is not None and tmp.lower() in ["yes", "true", "1"]:
|
||||
md.black_and_white = True
|
||||
md.blackAndWhite = True
|
||||
# Now extract the credit info
|
||||
for n in root:
|
||||
if any(
|
||||
[
|
||||
n.tag == "Writer",
|
||||
n.tag == "Penciller",
|
||||
n.tag == "Inker",
|
||||
n.tag == "Colorist",
|
||||
n.tag == "Letterer",
|
||||
n.tag == "Editor",
|
||||
]
|
||||
):
|
||||
if (n.tag == 'Writer' or
|
||||
n.tag == 'Penciller' or
|
||||
n.tag == 'Inker' or
|
||||
n.tag == 'Colorist' or
|
||||
n.tag == 'Letterer' or
|
||||
n.tag == 'Editor'
|
||||
):
|
||||
if n.text is not None:
|
||||
for name in n.text.split(","):
|
||||
md.add_credit(name.strip(), n.tag)
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit(name.strip(), n.tag)
|
||||
|
||||
if n.tag == "CoverArtist":
|
||||
if n.tag == 'CoverArtist':
|
||||
if n.text is not None:
|
||||
for name in n.text.split(","):
|
||||
md.add_credit(name.strip(), "Cover")
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit(name.strip(), "Cover")
|
||||
|
||||
# parse page data now
|
||||
pages_node = root.find("Pages")
|
||||
if pages_node is not None:
|
||||
for page in pages_node:
|
||||
if "Image" in page.attrib:
|
||||
page.attrib["Image"] = int(page.attrib["Image"])
|
||||
md.pages.append(page.attrib)
|
||||
metadata.pages.append(page.attrib)
|
||||
# print page.attrib
|
||||
|
||||
md.is_empty = False
|
||||
metadata.isEmpty = False
|
||||
|
||||
return md
|
||||
return metadata
|
||||
|
||||
def write_to_external_file(self, filename, metadata, xml=None):
|
||||
def writeToExternalFile(self, filename, metadata):
|
||||
|
||||
tree = self.convert_metadata_to_xml(self, metadata, xml)
|
||||
tree.write(filename, encoding="utf-8", xml_declaration=True)
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
# ET.dump(tree)
|
||||
tree.write(filename, encoding='utf-8')
|
||||
|
||||
def read_from_external_file(self, filename):
|
||||
def readFromExternalFile(self, filename):
|
||||
|
||||
tree = ET.parse(filename)
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
return self.convertXMLToMetadata(tree)
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
import calendar
|
||||
import os
|
||||
import unicodedata
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
Error = auto() # Error occurred; value is text of error
|
||||
EOF = auto()
|
||||
Text = auto() # Text
|
||||
LeftParen = auto() # '(' inside action
|
||||
Number = auto() # Simple number
|
||||
IssueNumber = auto() # Preceded by a # Symbol
|
||||
RightParen = auto() # ')' inside action
|
||||
Space = auto() # Run of spaces separating arguments
|
||||
Dot = auto()
|
||||
LeftBrace = auto()
|
||||
RightBrace = auto()
|
||||
LeftSBrace = auto()
|
||||
RightSBrace = auto()
|
||||
Symbol = auto()
|
||||
Skip = auto() # __ or -- no title, issue or series information beyond
|
||||
Operator = auto()
|
||||
Calendar = auto()
|
||||
InfoSpecifier = auto() # Specifies type of info e.g. v1 for 'volume': 1
|
||||
ArchiveType = auto()
|
||||
Honorific = auto()
|
||||
Keywords = auto()
|
||||
FCBD = auto()
|
||||
ComicType = auto()
|
||||
Publisher = auto()
|
||||
C2C = auto()
|
||||
|
||||
|
||||
braces = [
|
||||
ItemType.LeftBrace,
|
||||
ItemType.LeftParen,
|
||||
ItemType.LeftSBrace,
|
||||
ItemType.RightBrace,
|
||||
ItemType.RightParen,
|
||||
ItemType.RightSBrace,
|
||||
]
|
||||
|
||||
eof = chr(0)
|
||||
|
||||
key = {
|
||||
"fcbd": ItemType.FCBD,
|
||||
"freecomicbookday": ItemType.FCBD,
|
||||
"cbr": ItemType.ArchiveType,
|
||||
"cbz": ItemType.ArchiveType,
|
||||
"cbt": ItemType.ArchiveType,
|
||||
"cb7": ItemType.ArchiveType,
|
||||
"rar": ItemType.ArchiveType,
|
||||
"zip": ItemType.ArchiveType,
|
||||
"tar": ItemType.ArchiveType,
|
||||
"7z": ItemType.ArchiveType,
|
||||
"annual": ItemType.ComicType,
|
||||
"book": ItemType.ComicType,
|
||||
"volume": ItemType.InfoSpecifier,
|
||||
"vol.": ItemType.InfoSpecifier,
|
||||
"vol": ItemType.InfoSpecifier,
|
||||
"v": ItemType.InfoSpecifier,
|
||||
"of": ItemType.InfoSpecifier,
|
||||
"dc": ItemType.Publisher,
|
||||
"marvel": ItemType.Publisher,
|
||||
"covers": ItemType.InfoSpecifier,
|
||||
"c2c": ItemType.C2C,
|
||||
"mr": ItemType.Honorific,
|
||||
"ms": ItemType.Honorific,
|
||||
"mrs": ItemType.Honorific,
|
||||
"dr": ItemType.Honorific,
|
||||
}
|
||||
|
||||
|
||||
class Item:
|
||||
def __init__(self, typ: ItemType, pos: int, val: str):
|
||||
self.typ: ItemType = typ
|
||||
self.pos: int = pos
|
||||
self.val: str = val
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.val}: index: {self.pos}: {self.typ}"
|
||||
|
||||
|
||||
class Lexer:
|
||||
def __init__(self, string):
|
||||
self.input: str = string # The string being scanned
|
||||
self.state = None # The next lexing function to enter
|
||||
self.pos: int = -1 # Current position in the input
|
||||
self.start: int = 0 # Start position of this item
|
||||
self.lastPos: int = 0 # Position of most recent item returned by nextItem
|
||||
self.paren_depth: int = 0 # Nesting depth of ( ) exprs
|
||||
self.brace_depth: int = 0 # Nesting depth of { }
|
||||
self.sbrace_depth: int = 0 # Nesting depth of [ ]
|
||||
self.items = []
|
||||
|
||||
# Next returns the next rune in the input.
|
||||
def get(self) -> str:
|
||||
if int(self.pos) >= len(self.input) - 1:
|
||||
self.pos += 1
|
||||
return eof
|
||||
|
||||
self.pos += 1
|
||||
return self.input[self.pos]
|
||||
|
||||
# Peek returns but does not consume the next rune in the input.
|
||||
def peek(self) -> str:
|
||||
if int(self.pos) >= len(self.input) - 1:
|
||||
return eof
|
||||
|
||||
return self.input[self.pos + 1]
|
||||
|
||||
def backup(self):
|
||||
self.pos -= 1
|
||||
|
||||
# Emit passes an item back to the client.
|
||||
def emit(self, t: ItemType):
|
||||
self.items.append(Item(t, self.start, self.input[self.start : self.pos + 1]))
|
||||
self.start = self.pos + 1
|
||||
|
||||
# Ignore skips over the pending input before this point.
|
||||
def ignore(self):
|
||||
self.start = self.pos
|
||||
|
||||
# Accept consumes the next rune if it's from the valid se:
|
||||
def accept(self, valid: str):
|
||||
if self.get() in valid:
|
||||
return True
|
||||
|
||||
self.backup()
|
||||
return False
|
||||
|
||||
# AcceptRun consumes a run of runes from the valid set.
|
||||
def accept_run(self, valid: str):
|
||||
while self.get() in valid:
|
||||
pass
|
||||
|
||||
self.backup()
|
||||
|
||||
# Errorf returns an error token and terminates the scan by passing
|
||||
# Back a nil pointer that will be the next state, terminating self.nextItem.
|
||||
def errorf(self, message: str):
|
||||
self.items.append(Item(ItemType.Error, self.start, message))
|
||||
|
||||
# NextItem returns the next item from the input.
|
||||
# Called by the parser, not in the lexing goroutine.
|
||||
# def next_item(self) -> Item:
|
||||
# item: Item = self.items.get()
|
||||
# self.lastPos = item.pos
|
||||
# return item
|
||||
|
||||
def scan_number(self):
|
||||
digits = "0123456789"
|
||||
|
||||
self.accept_run(digits)
|
||||
if self.accept("."):
|
||||
if self.accept(digits):
|
||||
self.accept_run(digits)
|
||||
else:
|
||||
self.backup()
|
||||
if self.accept("s"):
|
||||
if not self.accept("t"):
|
||||
self.backup()
|
||||
elif self.accept("nr"):
|
||||
if not self.accept("d"):
|
||||
self.backup()
|
||||
elif self.accept("t"):
|
||||
if not self.accept("h"):
|
||||
self.backup()
|
||||
|
||||
return True
|
||||
|
||||
# Runs the state machine for the lexer.
|
||||
def run(self):
|
||||
self.state = lex_filename
|
||||
while self.state is not None:
|
||||
self.state = self.state(self)
|
||||
|
||||
|
||||
# Scans the elements inside action delimiters.
|
||||
def lex_filename(lex: Lexer):
|
||||
r = lex.get()
|
||||
if r == eof:
|
||||
if lex.paren_depth != 0:
|
||||
return lex.errorf("unclosed left paren")
|
||||
|
||||
if lex.brace_depth != 0:
|
||||
return lex.errorf("unclosed left paren")
|
||||
lex.emit(ItemType.EOF)
|
||||
return None
|
||||
elif is_space(r):
|
||||
if r == "_" and lex.peek() == "_":
|
||||
lex.get()
|
||||
lex.emit(ItemType.Skip)
|
||||
else:
|
||||
return lex_space
|
||||
elif r == ".":
|
||||
r = lex.peek()
|
||||
if r < "0" or "9" < r:
|
||||
lex.emit(ItemType.Dot)
|
||||
return lex_filename
|
||||
|
||||
lex.backup()
|
||||
return lex_number
|
||||
elif r == "'":
|
||||
r = lex.peek()
|
||||
if r in "0123456789":
|
||||
return lex_number
|
||||
lex.emit(ItemType.Text) # TODO: Change to Text
|
||||
elif "0" <= r <= "9":
|
||||
lex.backup()
|
||||
return lex_number
|
||||
elif r == "#":
|
||||
if "0" <= lex.peek() <= "9":
|
||||
return lex_number
|
||||
lex.emit(ItemType.Symbol)
|
||||
elif is_operator(r):
|
||||
if r == "-" and lex.peek() == "-":
|
||||
lex.get()
|
||||
lex.emit(ItemType.Skip)
|
||||
else:
|
||||
return lex_operator
|
||||
elif is_alpha_numeric(r):
|
||||
lex.backup()
|
||||
return lex_text
|
||||
elif r == "(":
|
||||
lex.emit(ItemType.LeftParen)
|
||||
lex.paren_depth += 1
|
||||
elif r == ")":
|
||||
lex.emit(ItemType.RightParen)
|
||||
lex.paren_depth -= 1
|
||||
if lex.paren_depth < 0:
|
||||
return lex.errorf("unexpected right paren " + r)
|
||||
|
||||
elif r == "{":
|
||||
lex.emit(ItemType.LeftBrace)
|
||||
lex.brace_depth += 1
|
||||
elif r == "}":
|
||||
lex.emit(ItemType.RightBrace)
|
||||
lex.brace_depth -= 1
|
||||
if lex.brace_depth < 0:
|
||||
return lex.errorf("unexpected right brace " + r)
|
||||
|
||||
elif r == "[":
|
||||
lex.emit(ItemType.LeftSBrace)
|
||||
lex.sbrace_depth += 1
|
||||
elif r == "]":
|
||||
lex.emit(ItemType.RightSBrace)
|
||||
lex.sbrace_depth -= 1
|
||||
if lex.sbrace_depth < 0:
|
||||
return lex.errorf("unexpected right brace " + r)
|
||||
elif is_symbol(r):
|
||||
# L.backup()
|
||||
lex.emit(ItemType.Symbol)
|
||||
else:
|
||||
return lex.errorf("unrecognized character in action: " + r)
|
||||
|
||||
return lex_filename
|
||||
|
||||
|
||||
def lex_operator(lex: Lexer):
|
||||
lex.accept_run("-|:;")
|
||||
lex.emit(ItemType.Operator)
|
||||
return lex_filename
|
||||
|
||||
|
||||
# LexSpace scans a run of space characters.
|
||||
# One space has already been seen.
|
||||
def lex_space(lex: Lexer):
|
||||
while is_space(lex.peek()):
|
||||
lex.get()
|
||||
|
||||
lex.emit(ItemType.Space)
|
||||
return lex_filename
|
||||
|
||||
|
||||
# Lex_text scans an alphanumeric.
|
||||
def lex_text(lex: Lexer):
|
||||
while True:
|
||||
r = lex.get()
|
||||
if is_alpha_numeric(r):
|
||||
if r.isnumeric(): # E.g. v1
|
||||
word = lex.input[lex.start : lex.pos]
|
||||
if word.lower() in key and key[word.lower()] == ItemType.InfoSpecifier:
|
||||
lex.backup()
|
||||
lex.emit(key[word.lower()])
|
||||
return lex_filename
|
||||
else:
|
||||
if r == "'" and lex.peek() == "s":
|
||||
lex.get()
|
||||
else:
|
||||
lex.backup()
|
||||
word = lex.input[lex.start : lex.pos + 1]
|
||||
if word.lower() == "vol" and lex.peek() == ".":
|
||||
lex.get()
|
||||
word = lex.input[lex.start : lex.pos + 1]
|
||||
|
||||
if word.lower() in key:
|
||||
lex.emit(key[word.lower()])
|
||||
elif cal(word):
|
||||
lex.emit(ItemType.Calendar)
|
||||
else:
|
||||
lex.emit(ItemType.Text)
|
||||
break
|
||||
|
||||
return lex_filename
|
||||
|
||||
|
||||
def cal(value: str):
|
||||
month_abbr = [i for i, x in enumerate(calendar.month_abbr) if x == value.title()]
|
||||
month_name = [i for i, x in enumerate(calendar.month_name) if x == value.title()]
|
||||
day_abbr = [i for i, x in enumerate(calendar.day_abbr) if x == value.title()]
|
||||
day_name = [i for i, x in enumerate(calendar.day_name) if x == value.title()]
|
||||
return set(month_abbr + month_name + day_abbr + day_name)
|
||||
|
||||
|
||||
def lex_number(lex: Lexer):
|
||||
if not lex.scan_number():
|
||||
return lex.errorf("bad number syntax: " + lex.input[lex.start : lex.pos])
|
||||
# Complex number logic removed. Messes with math operations without space
|
||||
|
||||
if lex.input[lex.start] == "#":
|
||||
lex.emit(ItemType.IssueNumber)
|
||||
elif not lex.input[lex.pos].isdigit():
|
||||
# Assume that 80th is just text and not a number
|
||||
lex.emit(ItemType.Text)
|
||||
else:
|
||||
lex.emit(ItemType.Number)
|
||||
|
||||
return lex_filename
|
||||
|
||||
|
||||
def is_space(character: str):
|
||||
return character in "_ \t"
|
||||
|
||||
|
||||
# IsAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
def is_alpha_numeric(character: str):
|
||||
return character.isalpha() or character.isnumeric()
|
||||
|
||||
|
||||
def is_operator(character: str):
|
||||
return character in "-|:;/\\"
|
||||
|
||||
|
||||
def is_symbol(character: str):
|
||||
return unicodedata.category(character)[0] in "PS"
|
||||
|
||||
|
||||
def Lex(filename: str):
|
||||
lex = Lexer(string=os.path.basename(filename))
|
||||
lex.run()
|
||||
return lex
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,12 +20,7 @@ possible, however lossy it might be
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import List, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import utils
|
||||
|
||||
|
||||
class PageType:
|
||||
@@ -47,36 +42,24 @@ class PageType:
|
||||
Other = "Other"
|
||||
Deleted = "Deleted"
|
||||
|
||||
|
||||
class ImageMetadata(TypedDict, total=False):
|
||||
Type: str
|
||||
Bookmark: str
|
||||
DoublePage: bool
|
||||
Image: int
|
||||
ImageSize: str
|
||||
ImageHeight: str
|
||||
ImageWidth: str
|
||||
|
||||
|
||||
class CreditMetadata(TypedDict):
|
||||
person: str
|
||||
role: str
|
||||
primary: bool
|
||||
"""
|
||||
class PageInfo:
|
||||
Image = 0
|
||||
Type = PageType.Story
|
||||
DoublePage = False
|
||||
ImageSize = 0
|
||||
Key = ""
|
||||
ImageWidth = 0
|
||||
ImageHeight = 0
|
||||
"""
|
||||
|
||||
|
||||
class GenericMetadata:
|
||||
writer_synonyms = ["writer", "plotter", "scripter"]
|
||||
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
|
||||
inker_synonyms = ["inker", "artist", "finishes"]
|
||||
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
|
||||
letterer_synonyms = ["letterer"]
|
||||
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
|
||||
editor_synonyms = ["editor"]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.is_empty = True
|
||||
self.tag_origin = None
|
||||
self.isEmpty = True
|
||||
self.tagOrigin = None
|
||||
|
||||
self.series = None
|
||||
self.issue = None
|
||||
@@ -85,48 +68,47 @@ class GenericMetadata:
|
||||
self.month = None
|
||||
self.year = None
|
||||
self.day = None
|
||||
self.issue_count = None
|
||||
self.issueCount = None
|
||||
self.volume = None
|
||||
self.genre = None
|
||||
self.language = None # 2 letter iso code
|
||||
self.comments = None # use same way as Summary in CIX
|
||||
|
||||
self.volume_count = None
|
||||
self.critical_rating = None
|
||||
self.volumeCount = None
|
||||
self.criticalRating = None
|
||||
self.country = None
|
||||
|
||||
self.alternate_series = None
|
||||
self.alternate_number = None
|
||||
self.alternate_count = None
|
||||
self.alternateSeries = None
|
||||
self.alternateNumber = None
|
||||
self.alternateCount = None
|
||||
self.imprint = None
|
||||
self.notes = None
|
||||
self.web_link = None
|
||||
self.webLink = None
|
||||
self.format = None
|
||||
self.manga = None
|
||||
self.black_and_white = None
|
||||
self.page_count = None
|
||||
self.maturity_rating = None
|
||||
self.community_rating = None
|
||||
self.blackAndWhite = None
|
||||
self.pageCount = None
|
||||
self.maturityRating = None
|
||||
|
||||
self.story_arc = None
|
||||
self.series_group = None
|
||||
self.scan_info = None
|
||||
self.storyArc = None
|
||||
self.seriesGroup = None
|
||||
self.scanInfo = None
|
||||
|
||||
self.characters = None
|
||||
self.teams = None
|
||||
self.locations = None
|
||||
|
||||
self.credits: List[CreditMetadata] = []
|
||||
self.tags: List[str] = []
|
||||
self.pages: List[ImageMetadata] = []
|
||||
self.credits = list()
|
||||
self.tags = list()
|
||||
self.pages = list()
|
||||
|
||||
# Some CoMet-only items
|
||||
self.price = None
|
||||
self.is_version_of = None
|
||||
self.isVersionOf = None
|
||||
self.rights = None
|
||||
self.identifier = None
|
||||
self.last_mark = None
|
||||
self.cover_image = None
|
||||
self.lastMark = None
|
||||
self.coverImage = None
|
||||
|
||||
def overlay(self, new_md):
|
||||
"""Overlay a metadata object on this one
|
||||
@@ -142,37 +124,35 @@ class GenericMetadata:
|
||||
else:
|
||||
setattr(self, cur, new)
|
||||
|
||||
new_md: GenericMetadata
|
||||
if not new_md.is_empty:
|
||||
self.is_empty = False
|
||||
if not new_md.isEmpty:
|
||||
self.isEmpty = False
|
||||
|
||||
assign("series", new_md.series)
|
||||
assign('series', new_md.series)
|
||||
assign("issue", new_md.issue)
|
||||
assign("issue_count", new_md.issue_count)
|
||||
assign("issueCount", new_md.issueCount)
|
||||
assign("title", new_md.title)
|
||||
assign("publisher", new_md.publisher)
|
||||
assign("day", new_md.day)
|
||||
assign("month", new_md.month)
|
||||
assign("year", new_md.year)
|
||||
assign("volume", new_md.volume)
|
||||
assign("volume_count", new_md.volume_count)
|
||||
assign("volumeCount", new_md.volumeCount)
|
||||
assign("genre", new_md.genre)
|
||||
assign("language", new_md.language)
|
||||
assign("country", new_md.country)
|
||||
assign("critical_rating", new_md.critical_rating)
|
||||
assign("alternate_series", new_md.alternate_series)
|
||||
assign("alternate_number", new_md.alternate_number)
|
||||
assign("alternate_count", new_md.alternate_count)
|
||||
assign("criticalRating", new_md.criticalRating)
|
||||
assign("alternateSeries", new_md.alternateSeries)
|
||||
assign("alternateNumber", new_md.alternateNumber)
|
||||
assign("alternateCount", new_md.alternateCount)
|
||||
assign("imprint", new_md.imprint)
|
||||
assign("web_link", new_md.web_link)
|
||||
assign("webLink", new_md.webLink)
|
||||
assign("format", new_md.format)
|
||||
assign("manga", new_md.manga)
|
||||
assign("black_and_white", new_md.black_and_white)
|
||||
assign("maturity_rating", new_md.maturity_rating)
|
||||
assign("community_rating", new_md.community_rating)
|
||||
assign("story_arc", new_md.story_arc)
|
||||
assign("series_group", new_md.series_group)
|
||||
assign("scan_info", new_md.scan_info)
|
||||
assign("blackAndWhite", new_md.blackAndWhite)
|
||||
assign("maturityRating", new_md.maturityRating)
|
||||
assign("storyArc", new_md.storyArc)
|
||||
assign("seriesGroup", new_md.seriesGroup)
|
||||
assign("scanInfo", new_md.scanInfo)
|
||||
assign("characters", new_md.characters)
|
||||
assign("teams", new_md.teams)
|
||||
assign("locations", new_md.locations)
|
||||
@@ -180,12 +160,12 @@ class GenericMetadata:
|
||||
assign("notes", new_md.notes)
|
||||
|
||||
assign("price", new_md.price)
|
||||
assign("is_version_of", new_md.is_version_of)
|
||||
assign("isVersionOf", new_md.isVersionOf)
|
||||
assign("rights", new_md.rights)
|
||||
assign("identifier", new_md.identifier)
|
||||
assign("last_mark", new_md.last_mark)
|
||||
assign("lastMark", new_md.lastMark)
|
||||
|
||||
self.overlay_credits(new_md.credits)
|
||||
self.overlayCredits(new_md.credits)
|
||||
# TODO
|
||||
|
||||
# not sure if the tags and pages should broken down, or treated
|
||||
@@ -199,132 +179,132 @@ class GenericMetadata:
|
||||
if len(new_md.pages) > 0:
|
||||
assign("pages", new_md.pages)
|
||||
|
||||
def overlay_credits(self, new_credits):
|
||||
def overlayCredits(self, new_credits):
|
||||
for c in new_credits:
|
||||
primary = bool("primary" in c and c["primary"])
|
||||
if 'primary' in c and c['primary']:
|
||||
primary = True
|
||||
else:
|
||||
primary = False
|
||||
|
||||
# Remove credit role if person is blank
|
||||
if c["person"] == "":
|
||||
if c['person'] == "":
|
||||
for r in reversed(self.credits):
|
||||
if r["role"].lower() == c["role"].lower():
|
||||
if r['role'].lower() == c['role'].lower():
|
||||
self.credits.remove(r)
|
||||
# otherwise, add it!
|
||||
else:
|
||||
self.add_credit(c["person"], c["role"], primary)
|
||||
self.addCredit(c['person'], c['role'], primary)
|
||||
|
||||
def set_default_page_list(self, count):
|
||||
def setDefaultPageList(self, count):
|
||||
# generate a default page list, with the first page marked as the cover
|
||||
for i in range(count):
|
||||
page_dict = ImageMetadata(Image=i)
|
||||
page_dict = dict()
|
||||
page_dict['Image'] = str(i)
|
||||
if i == 0:
|
||||
page_dict["Type"] = PageType.FrontCover
|
||||
page_dict['Type'] = PageType.FrontCover
|
||||
self.pages.append(page_dict)
|
||||
|
||||
def get_archive_page_index(self, pagenum):
|
||||
def getArchivePageIndex(self, pagenum):
|
||||
# convert the displayed page number to the page index of the file in
|
||||
# the archive
|
||||
if pagenum < len(self.pages):
|
||||
return int(self.pages[pagenum]["Image"])
|
||||
return int(self.pages[pagenum]['Image'])
|
||||
else:
|
||||
return 0
|
||||
|
||||
return 0
|
||||
|
||||
def get_cover_page_index_list(self):
|
||||
def getCoverPageIndexList(self):
|
||||
# return a list of archive page indices of cover pages
|
||||
coverlist = []
|
||||
for p in self.pages:
|
||||
if "Type" in p and p["Type"] == PageType.FrontCover:
|
||||
coverlist.append(int(p["Image"]))
|
||||
if 'Type' in p and p['Type'] == PageType.FrontCover:
|
||||
coverlist.append(int(p['Image']))
|
||||
|
||||
if len(coverlist) == 0:
|
||||
coverlist.append(0)
|
||||
|
||||
return coverlist
|
||||
|
||||
def add_credit(self, person, role, primary=False):
|
||||
def addCredit(self, person, role, primary=False):
|
||||
|
||||
credit: CreditMetadata = {"person": person, "role": role, "primary": primary}
|
||||
credit = dict()
|
||||
credit['person'] = person
|
||||
credit['role'] = role
|
||||
if primary:
|
||||
credit['primary'] = primary
|
||||
|
||||
# look to see if it's not already there...
|
||||
found = False
|
||||
for c in self.credits:
|
||||
if c["person"].lower() == person.lower() and c["role"].lower() == role.lower():
|
||||
if (c['person'].lower() == person.lower() and
|
||||
c['role'].lower() == role.lower()):
|
||||
# no need to add it. just adjust the "primary" flag as needed
|
||||
c["primary"] = primary
|
||||
c['primary'] = primary
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
self.credits.append(credit)
|
||||
|
||||
def get_primary_credit(self, role):
|
||||
primary = ""
|
||||
for credit in self.credits:
|
||||
if (primary == "" and credit["role"].lower() == role.lower()) or (
|
||||
credit["role"].lower() == role.lower() and credit["primary"]
|
||||
):
|
||||
primary = credit["person"]
|
||||
return primary
|
||||
|
||||
def __str__(self):
|
||||
vals = []
|
||||
if self.is_empty:
|
||||
if self.isEmpty:
|
||||
return "No metadata"
|
||||
|
||||
def add_string(tag, val):
|
||||
if val is not None and str(val) != "":
|
||||
if val is not None and u"{0}".format(val) != "":
|
||||
vals.append((tag, val))
|
||||
|
||||
def add_attr_string(tag):
|
||||
val = getattr(self, tag)
|
||||
add_string(tag, getattr(self, tag))
|
||||
|
||||
add_attr_string("series")
|
||||
add_attr_string("issue")
|
||||
add_attr_string("issue_count")
|
||||
add_attr_string("issueCount")
|
||||
add_attr_string("title")
|
||||
add_attr_string("publisher")
|
||||
add_attr_string("year")
|
||||
add_attr_string("month")
|
||||
add_attr_string("day")
|
||||
add_attr_string("volume")
|
||||
add_attr_string("volume_count")
|
||||
add_attr_string("volumeCount")
|
||||
add_attr_string("genre")
|
||||
add_attr_string("language")
|
||||
add_attr_string("country")
|
||||
add_attr_string("critical_rating")
|
||||
add_attr_string("alternate_series")
|
||||
add_attr_string("alternate_number")
|
||||
add_attr_string("alternate_count")
|
||||
add_attr_string("criticalRating")
|
||||
add_attr_string("alternateSeries")
|
||||
add_attr_string("alternateNumber")
|
||||
add_attr_string("alternateCount")
|
||||
add_attr_string("imprint")
|
||||
add_attr_string("web_link")
|
||||
add_attr_string("webLink")
|
||||
add_attr_string("format")
|
||||
add_attr_string("manga")
|
||||
|
||||
add_attr_string("price")
|
||||
add_attr_string("is_version_of")
|
||||
add_attr_string("isVersionOf")
|
||||
add_attr_string("rights")
|
||||
add_attr_string("identifier")
|
||||
add_attr_string("last_mark")
|
||||
add_attr_string("lastMark")
|
||||
|
||||
if self.black_and_white:
|
||||
add_attr_string("black_and_white")
|
||||
add_attr_string("maturity_rating")
|
||||
add_attr_string("community_rating")
|
||||
add_attr_string("story_arc")
|
||||
add_attr_string("series_group")
|
||||
add_attr_string("scan_info")
|
||||
if self.blackAndWhite:
|
||||
add_attr_string("blackAndWhite")
|
||||
add_attr_string("maturityRating")
|
||||
add_attr_string("storyArc")
|
||||
add_attr_string("seriesGroup")
|
||||
add_attr_string("scanInfo")
|
||||
add_attr_string("characters")
|
||||
add_attr_string("teams")
|
||||
add_attr_string("locations")
|
||||
add_attr_string("comments")
|
||||
add_attr_string("notes")
|
||||
|
||||
add_string("tags", utils.list_to_string(self.tags))
|
||||
add_string("tags", utils.listToString(self.tags))
|
||||
|
||||
for c in self.credits:
|
||||
primary = ""
|
||||
if "primary" in c and c["primary"]:
|
||||
if 'primary' in c and c['primary']:
|
||||
primary = " [P]"
|
||||
add_string("credit", c["role"] + ": " + c["person"] + primary)
|
||||
add_string("credit", c['role'] + ": " + c['person'] + primary)
|
||||
|
||||
# find the longest field name
|
||||
flen = 0
|
||||
@@ -334,99 +314,8 @@ class GenericMetadata:
|
||||
|
||||
# format the data nicely
|
||||
outstr = ""
|
||||
fmt_str = "{0: <" + str(flen) + "} {1}\n"
|
||||
fmt_str = u"{0: <" + str(flen) + "} {1}\n"
|
||||
for i in vals:
|
||||
outstr += fmt_str.format(i[0] + ":", i[1])
|
||||
|
||||
return outstr
|
||||
|
||||
|
||||
md_test = GenericMetadata()
|
||||
|
||||
md_test.is_empty = False
|
||||
md_test.tag_origin = None
|
||||
md_test.series = "Cory Doctorow's Futuristic Tales of the Here and Now"
|
||||
md_test.issue = "1"
|
||||
md_test.title = "Anda's Game"
|
||||
md_test.publisher = "IDW Publishing"
|
||||
md_test.month = 10
|
||||
md_test.year = 2007
|
||||
md_test.day = 1
|
||||
md_test.issue_count = 6
|
||||
md_test.volume = 1
|
||||
md_test.genre = "Sci-Fi"
|
||||
md_test.language = "en"
|
||||
md_test.comments = (
|
||||
"For 12-year-old Anda, getting paid real money to kill the characters of players who were cheating in her favorite online "
|
||||
"computer game was a win-win situation. Until she found out who was paying her, and what those characters meant to the "
|
||||
"livelihood of children around the world."
|
||||
)
|
||||
md_test.volume_count = None
|
||||
md_test.critical_rating = None
|
||||
md_test.country = None
|
||||
md_test.alternate_series = "Tales"
|
||||
md_test.alternate_number = "2"
|
||||
md_test.alternate_count = 7
|
||||
md_test.imprint = "craphound.com"
|
||||
md_test.notes = "Tagged with ComicTagger 1.3.2a5 using info from Comic Vine on 2022-04-16 15:52:26. [Issue ID 140529]"
|
||||
md_test.web_link = "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/"
|
||||
md_test.format = "Series"
|
||||
md_test.manga = "No"
|
||||
md_test.black_and_white = None
|
||||
md_test.page_count = 24
|
||||
md_test.maturity_rating = "Everyone 10+"
|
||||
md_test.community_rating = "3.0"
|
||||
md_test.story_arc = "Here and Now"
|
||||
md_test.series_group = "Futuristic Tales"
|
||||
md_test.scan_info = "(CC BY-NC-SA 3.0)"
|
||||
md_test.characters = "Anda"
|
||||
md_test.teams = "Fahrenheit"
|
||||
md_test.locations = "lonely cottage "
|
||||
md_test.credits = [
|
||||
{"person": "Dara Naraghi", "role": "Writer"},
|
||||
{"person": "Esteve Polls", "role": "Penciller"},
|
||||
{"person": "Esteve Polls", "role": "Inker"},
|
||||
{"person": "Neil Uyetake", "role": "Letterer"},
|
||||
{"person": "Sam Kieth", "role": "Cover"},
|
||||
{"person": "Ted Adams", "role": "Editor"},
|
||||
]
|
||||
md_test.tags = []
|
||||
md_test.pages = [
|
||||
{"Image": 0, "ImageHeight": "1280", "ImageSize": "195977", "ImageWidth": "800", "Type": PageType.FrontCover},
|
||||
{"Image": 1, "ImageHeight": "2039", "ImageSize": "611993", "ImageWidth": "1327"},
|
||||
{"Image": 2, "ImageHeight": "2039", "ImageSize": "783726", "ImageWidth": "1327"},
|
||||
{"Image": 3, "ImageHeight": "2039", "ImageSize": "679584", "ImageWidth": "1327"},
|
||||
{"Image": 4, "ImageHeight": "2039", "ImageSize": "788179", "ImageWidth": "1327"},
|
||||
{"Image": 5, "ImageHeight": "2039", "ImageSize": "864433", "ImageWidth": "1327"},
|
||||
{"Image": 6, "ImageHeight": "2039", "ImageSize": "765606", "ImageWidth": "1327"},
|
||||
{"Image": 7, "ImageHeight": "2039", "ImageSize": "876427", "ImageWidth": "1327"},
|
||||
{"Image": 8, "ImageHeight": "2039", "ImageSize": "852622", "ImageWidth": "1327"},
|
||||
{"Image": 9, "ImageHeight": "2039", "ImageSize": "800205", "ImageWidth": "1327"},
|
||||
{"Image": 10, "ImageHeight": "2039", "ImageSize": "746243", "ImageWidth": "1326"},
|
||||
{"Image": 11, "ImageHeight": "2039", "ImageSize": "718062", "ImageWidth": "1327"},
|
||||
{"Image": 12, "ImageHeight": "2039", "ImageSize": "532179", "ImageWidth": "1326"},
|
||||
{"Image": 13, "ImageHeight": "2039", "ImageSize": "686708", "ImageWidth": "1327"},
|
||||
{"Image": 14, "ImageHeight": "2039", "ImageSize": "641907", "ImageWidth": "1327"},
|
||||
{"Image": 15, "ImageHeight": "2039", "ImageSize": "805388", "ImageWidth": "1327"},
|
||||
{"Image": 16, "ImageHeight": "2039", "ImageSize": "668927", "ImageWidth": "1326"},
|
||||
{"Image": 17, "ImageHeight": "2039", "ImageSize": "710605", "ImageWidth": "1327"},
|
||||
{"Image": 18, "ImageHeight": "2039", "ImageSize": "761398", "ImageWidth": "1326"},
|
||||
{"Image": 19, "ImageHeight": "2039", "ImageSize": "743807", "ImageWidth": "1327"},
|
||||
{"Image": 20, "ImageHeight": "2039", "ImageSize": "552911", "ImageWidth": "1326"},
|
||||
{"Image": 21, "ImageHeight": "2039", "ImageSize": "556827", "ImageWidth": "1327"},
|
||||
{"Image": 22, "ImageHeight": "2039", "ImageSize": "675078", "ImageWidth": "1326"},
|
||||
{
|
||||
"Bookmark": "Interview",
|
||||
"Image": "23",
|
||||
"ImageHeight": "2032",
|
||||
"ImageSize": "800965",
|
||||
"ImageWidth": "1338",
|
||||
"Type": PageType.Letters,
|
||||
},
|
||||
]
|
||||
md_test.price = None
|
||||
md_test.is_version_of = None
|
||||
md_test.rights = None
|
||||
md_test.identifier = None
|
||||
md_test.last_mark = None
|
||||
md_test.cover_image = None
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# coding=utf-8
|
||||
"""Support for mixed digit/string type Issue field
|
||||
|
||||
Class for handling the odd permutations of an 'issue number' that the
|
||||
@@ -19,13 +20,13 @@ comics industry throws at us.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#import utils
|
||||
#import math
|
||||
#import re
|
||||
|
||||
|
||||
class IssueString:
|
||||
|
||||
def __init__(self, text):
|
||||
|
||||
# break up the issue number string into 2 parts: the numeric and suffix string.
|
||||
@@ -37,13 +38,16 @@ class IssueString:
|
||||
if text is None:
|
||||
return
|
||||
|
||||
text = str(text)
|
||||
if isinstance(text, int):
|
||||
text = str(text)
|
||||
|
||||
if len(text) == 0:
|
||||
return
|
||||
|
||||
text = unicode(text)
|
||||
|
||||
# skip the minus sign if it's first
|
||||
if text[0] == "-":
|
||||
if text[0] == '-':
|
||||
start = 1
|
||||
else:
|
||||
start = 0
|
||||
@@ -75,7 +79,7 @@ class IssueString:
|
||||
idx = 0
|
||||
|
||||
part1 = text[0:idx]
|
||||
part2 = text[idx : len(text)]
|
||||
part2 = text[idx:len(text)]
|
||||
|
||||
if part1 != "":
|
||||
self.num = float(part1)
|
||||
@@ -83,7 +87,9 @@ class IssueString:
|
||||
else:
|
||||
self.suffix = text
|
||||
|
||||
def as_string(self, pad=0):
|
||||
# print "num: {0} suf: {1}".format(self.num, self.suffix)
|
||||
|
||||
def asString(self, pad=0):
|
||||
# return the float, left side zero-padded, with suffix attached
|
||||
if self.num is None:
|
||||
return self.suffix
|
||||
@@ -101,9 +107,9 @@ class IssueString:
|
||||
|
||||
# create padding
|
||||
padding = ""
|
||||
length = len(str(num_int))
|
||||
if length < pad:
|
||||
padding = "0" * (pad - length)
|
||||
l = len(str(num_int))
|
||||
if l < pad:
|
||||
padding = "0" * (pad - l)
|
||||
|
||||
num_s = padding + num_s
|
||||
if negative:
|
||||
@@ -111,16 +117,16 @@ class IssueString:
|
||||
|
||||
return num_s
|
||||
|
||||
def as_float(self):
|
||||
def asFloat(self):
|
||||
# return the float, with no suffix
|
||||
if self.suffix == "½":
|
||||
if self.suffix == u"½":
|
||||
if self.num is not None:
|
||||
return self.num + 0.5
|
||||
|
||||
return 0.5
|
||||
return self.num + .5
|
||||
else:
|
||||
return .5
|
||||
return self.num
|
||||
|
||||
def as_int(self):
|
||||
def asInt(self):
|
||||
# return the int version of the float
|
||||
if self.num is None:
|
||||
return None
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# coding=utf-8
|
||||
"""Some generic utilities"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
@@ -14,19 +15,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
|
||||
import pycountry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import os
|
||||
import re
|
||||
import platform
|
||||
import locale
|
||||
import codecs
|
||||
|
||||
|
||||
class UtilsVars:
|
||||
@@ -55,19 +49,26 @@ def fix_output_encoding():
|
||||
def get_recursive_filelist(pathlist):
|
||||
"""Get a recursive list of of all files under all path items in the list"""
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
filelist = []
|
||||
for p in pathlist:
|
||||
# if path is a folder, walk it recursively, and all files underneath
|
||||
if not isinstance(p, str):
|
||||
if isinstance(p, str):
|
||||
# make sure string is unicode
|
||||
p = p.decode(filename_encoding) # , 'replace')
|
||||
elif not isinstance(p, unicode):
|
||||
# it's probably a QString
|
||||
p = str(p)
|
||||
p = unicode(p)
|
||||
|
||||
if os.path.isdir(p):
|
||||
for root, _, files in os.walk(p):
|
||||
for root, dirs, files in os.walk(p):
|
||||
for f in files:
|
||||
if not isinstance(f, str):
|
||||
if isinstance(f, str):
|
||||
# make sure string is unicode
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
elif not isinstance(f, unicode):
|
||||
# it's probably a QString
|
||||
f = str(f)
|
||||
f = unicode(f)
|
||||
filelist.append(os.path.join(root, f))
|
||||
else:
|
||||
filelist.append(p)
|
||||
@@ -75,26 +76,28 @@ def get_recursive_filelist(pathlist):
|
||||
return filelist
|
||||
|
||||
|
||||
def list_to_string(lst):
|
||||
def listToString(l):
|
||||
string = ""
|
||||
if lst is not None:
|
||||
for item in lst:
|
||||
if l is not None:
|
||||
for item in l:
|
||||
if len(string) > 0:
|
||||
string += ", "
|
||||
string += item
|
||||
return string
|
||||
|
||||
|
||||
def add_to_path(dirname):
|
||||
def addtopath(dirname):
|
||||
if dirname is not None and dirname != "":
|
||||
|
||||
# verify that path doesn't already contain the given dirname
|
||||
tmpdirname = re.escape(dirname)
|
||||
pattern = r"(^|{sep}){dir}({sep}|$)".format(dir=tmpdirname, sep=os.pathsep)
|
||||
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format(
|
||||
dir=tmpdirname,
|
||||
sep=os.pathsep)
|
||||
|
||||
match = re.search(pattern, os.environ["PATH"])
|
||||
match = re.search(pattern, os.environ['PATH'])
|
||||
if not match:
|
||||
os.environ["PATH"] = dirname + os.pathsep + os.environ["PATH"]
|
||||
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
|
||||
|
||||
|
||||
def which(program):
|
||||
@@ -103,7 +106,7 @@ def which(program):
|
||||
def is_exe(fpath):
|
||||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
||||
|
||||
fpath, _ = os.path.split(program)
|
||||
fpath, fname = os.path.split(program)
|
||||
if fpath:
|
||||
if is_exe(program):
|
||||
return program
|
||||
@@ -116,109 +119,474 @@ def which(program):
|
||||
return None
|
||||
|
||||
|
||||
def xlate(data, is_int=False):
|
||||
if data is None or data == "":
|
||||
return None
|
||||
if is_int:
|
||||
i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890"), "1234567890")))
|
||||
if i == "0":
|
||||
return "0"
|
||||
if i == "":
|
||||
return None
|
||||
return int(i)
|
||||
|
||||
return str(data)
|
||||
|
||||
|
||||
def remove_articles(text):
|
||||
def removearticles(text):
|
||||
text = text.lower()
|
||||
articles = [
|
||||
"&",
|
||||
"a",
|
||||
"am",
|
||||
"an",
|
||||
"and",
|
||||
"as",
|
||||
"at",
|
||||
"be",
|
||||
"but",
|
||||
"by",
|
||||
"for",
|
||||
"if",
|
||||
"is",
|
||||
"issue",
|
||||
"it",
|
||||
"it's",
|
||||
"its",
|
||||
"itself",
|
||||
"of",
|
||||
"or",
|
||||
"so",
|
||||
"the",
|
||||
"the",
|
||||
"with",
|
||||
]
|
||||
new_text = ""
|
||||
for word in text.split(" "):
|
||||
articles = ['and', 'the', 'a', '&', 'issue']
|
||||
newText = ''
|
||||
for word in text.split(' '):
|
||||
if word not in articles:
|
||||
new_text += word + " "
|
||||
newText += word + ' '
|
||||
|
||||
new_text = new_text[:-1]
|
||||
newText = newText[:-1]
|
||||
|
||||
return new_text
|
||||
# now get rid of some other junk
|
||||
newText = newText.replace(":", "")
|
||||
newText = newText.replace(",", "")
|
||||
newText = newText.replace("-", " ")
|
||||
|
||||
# since the CV API changed, searches for series names with periods
|
||||
# now explicitly require the period to be in the search key,
|
||||
# so the line below is removed (for now)
|
||||
#newText = newText.replace(".", "")
|
||||
|
||||
def sanitize_title(text):
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
# this will probably cause issues with titles in other character sets e.g. chinese, japanese
|
||||
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
|
||||
# comicvine keeps apostrophes a part of the word
|
||||
text = text.replace("'", "")
|
||||
text = text.replace('"', "")
|
||||
# comicvine ignores punctuation and accents
|
||||
text = re.sub(r"[^A-Za-z0-9]+", " ", text)
|
||||
# remove extra space and articles and all lower case
|
||||
text = remove_articles(text).lower().strip()
|
||||
|
||||
return text
|
||||
return newText
|
||||
|
||||
|
||||
def unique_file(file_name):
|
||||
counter = 1
|
||||
# returns ('/path/file', '.ext')
|
||||
file_name_parts = os.path.splitext(file_name)
|
||||
while True:
|
||||
if not os.path.lexists(file_name):
|
||||
return file_name
|
||||
file_name = file_name_parts[0] + " (" + str(counter) + ")" + file_name_parts[1]
|
||||
file_name = file_name_parts[
|
||||
0] + ' (' + str(counter) + ')' + file_name_parts[1]
|
||||
counter += 1
|
||||
|
||||
|
||||
languages = defaultdict(lambda: None)
|
||||
# -o- coding: utf-8 -o-
|
||||
# ISO639 python dict
|
||||
# official list in http://www.loc.gov/standards/iso639-2/php/code_list.php
|
||||
|
||||
countries = defaultdict(lambda: None)
|
||||
|
||||
for c in pycountry.countries:
|
||||
if "alpha_2" in c._fields:
|
||||
countries[c.alpha_2] = c.name
|
||||
|
||||
for lng in pycountry.languages:
|
||||
if "alpha_2" in lng._fields:
|
||||
languages[lng.alpha_2] = lng.name
|
||||
lang_dict = {
|
||||
'ab': 'Abkhaz',
|
||||
'aa': 'Afar',
|
||||
'af': 'Afrikaans',
|
||||
'ak': 'Akan',
|
||||
'sq': 'Albanian',
|
||||
'am': 'Amharic',
|
||||
'ar': 'Arabic',
|
||||
'an': 'Aragonese',
|
||||
'hy': 'Armenian',
|
||||
'as': 'Assamese',
|
||||
'av': 'Avaric',
|
||||
'ae': 'Avestan',
|
||||
'ay': 'Aymara',
|
||||
'az': 'Azerbaijani',
|
||||
'bm': 'Bambara',
|
||||
'ba': 'Bashkir',
|
||||
'eu': 'Basque',
|
||||
'be': 'Belarusian',
|
||||
'bn': 'Bengali',
|
||||
'bh': 'Bihari',
|
||||
'bi': 'Bislama',
|
||||
'bs': 'Bosnian',
|
||||
'br': 'Breton',
|
||||
'bg': 'Bulgarian',
|
||||
'my': 'Burmese',
|
||||
'ca': 'Catalan; Valencian',
|
||||
'ch': 'Chamorro',
|
||||
'ce': 'Chechen',
|
||||
'ny': 'Chichewa; Chewa; Nyanja',
|
||||
'zh': 'Chinese',
|
||||
'cv': 'Chuvash',
|
||||
'kw': 'Cornish',
|
||||
'co': 'Corsican',
|
||||
'cr': 'Cree',
|
||||
'hr': 'Croatian',
|
||||
'cs': 'Czech',
|
||||
'da': 'Danish',
|
||||
'dv': 'Divehi; Maldivian;',
|
||||
'nl': 'Dutch',
|
||||
'dz': 'Dzongkha',
|
||||
'en': 'English',
|
||||
'eo': 'Esperanto',
|
||||
'et': 'Estonian',
|
||||
'ee': 'Ewe',
|
||||
'fo': 'Faroese',
|
||||
'fj': 'Fijian',
|
||||
'fi': 'Finnish',
|
||||
'fr': 'French',
|
||||
'ff': 'Fula',
|
||||
'gl': 'Galician',
|
||||
'ka': 'Georgian',
|
||||
'de': 'German',
|
||||
'el': 'Greek, Modern',
|
||||
'gn': 'Guaraní',
|
||||
'gu': 'Gujarati',
|
||||
'ht': 'Haitian',
|
||||
'ha': 'Hausa',
|
||||
'he': 'Hebrew (modern)',
|
||||
'hz': 'Herero',
|
||||
'hi': 'Hindi',
|
||||
'ho': 'Hiri Motu',
|
||||
'hu': 'Hungarian',
|
||||
'ia': 'Interlingua',
|
||||
'id': 'Indonesian',
|
||||
'ie': 'Interlingue',
|
||||
'ga': 'Irish',
|
||||
'ig': 'Igbo',
|
||||
'ik': 'Inupiaq',
|
||||
'io': 'Ido',
|
||||
'is': 'Icelandic',
|
||||
'it': 'Italian',
|
||||
'iu': 'Inuktitut',
|
||||
'ja': 'Japanese',
|
||||
'jv': 'Javanese',
|
||||
'kl': 'Kalaallisut',
|
||||
'kn': 'Kannada',
|
||||
'kr': 'Kanuri',
|
||||
'ks': 'Kashmiri',
|
||||
'kk': 'Kazakh',
|
||||
'km': 'Khmer',
|
||||
'ki': 'Kikuyu, Gikuyu',
|
||||
'rw': 'Kinyarwanda',
|
||||
'ky': 'Kirghiz, Kyrgyz',
|
||||
'kv': 'Komi',
|
||||
'kg': 'Kongo',
|
||||
'ko': 'Korean',
|
||||
'ku': 'Kurdish',
|
||||
'kj': 'Kwanyama, Kuanyama',
|
||||
'la': 'Latin',
|
||||
'lb': 'Luxembourgish',
|
||||
'lg': 'Luganda',
|
||||
'li': 'Limburgish',
|
||||
'ln': 'Lingala',
|
||||
'lo': 'Lao',
|
||||
'lt': 'Lithuanian',
|
||||
'lu': 'Luba-Katanga',
|
||||
'lv': 'Latvian',
|
||||
'gv': 'Manx',
|
||||
'mk': 'Macedonian',
|
||||
'mg': 'Malagasy',
|
||||
'ms': 'Malay',
|
||||
'ml': 'Malayalam',
|
||||
'mt': 'Maltese',
|
||||
'mi': 'Māori',
|
||||
'mr': 'Marathi (Marāṭhī)',
|
||||
'mh': 'Marshallese',
|
||||
'mn': 'Mongolian',
|
||||
'na': 'Nauru',
|
||||
'nv': 'Navajo, Navaho',
|
||||
'nb': 'Norwegian Bokmål',
|
||||
'nd': 'North Ndebele',
|
||||
'ne': 'Nepali',
|
||||
'ng': 'Ndonga',
|
||||
'nn': 'Norwegian Nynorsk',
|
||||
'no': 'Norwegian',
|
||||
'ii': 'Nuosu',
|
||||
'nr': 'South Ndebele',
|
||||
'oc': 'Occitan',
|
||||
'oj': 'Ojibwe, Ojibwa',
|
||||
'cu': 'Old Church Slavonic',
|
||||
'om': 'Oromo',
|
||||
'or': 'Oriya',
|
||||
'os': 'Ossetian, Ossetic',
|
||||
'pa': 'Panjabi, Punjabi',
|
||||
'pi': 'Pāli',
|
||||
'fa': 'Persian',
|
||||
'pl': 'Polish',
|
||||
'ps': 'Pashto, Pushto',
|
||||
'pt': 'Portuguese',
|
||||
'qu': 'Quechua',
|
||||
'rm': 'Romansh',
|
||||
'rn': 'Kirundi',
|
||||
'ro': 'Romanian, Moldavan',
|
||||
'ru': 'Russian',
|
||||
'sa': 'Sanskrit (Saṁskṛta)',
|
||||
'sc': 'Sardinian',
|
||||
'sd': 'Sindhi',
|
||||
'se': 'Northern Sami',
|
||||
'sm': 'Samoan',
|
||||
'sg': 'Sango',
|
||||
'sr': 'Serbian',
|
||||
'gd': 'Scottish Gaelic',
|
||||
'sn': 'Shona',
|
||||
'si': 'Sinhala, Sinhalese',
|
||||
'sk': 'Slovak',
|
||||
'sl': 'Slovene',
|
||||
'so': 'Somali',
|
||||
'st': 'Southern Sotho',
|
||||
'es': 'Spanish; Castilian',
|
||||
'su': 'Sundanese',
|
||||
'sw': 'Swahili',
|
||||
'ss': 'Swati',
|
||||
'sv': 'Swedish',
|
||||
'ta': 'Tamil',
|
||||
'te': 'Telugu',
|
||||
'tg': 'Tajik',
|
||||
'th': 'Thai',
|
||||
'ti': 'Tigrinya',
|
||||
'bo': 'Tibetan',
|
||||
'tk': 'Turkmen',
|
||||
'tl': 'Tagalog',
|
||||
'tn': 'Tswana',
|
||||
'to': 'Tonga',
|
||||
'tr': 'Turkish',
|
||||
'ts': 'Tsonga',
|
||||
'tt': 'Tatar',
|
||||
'tw': 'Twi',
|
||||
'ty': 'Tahitian',
|
||||
'ug': 'Uighur, Uyghur',
|
||||
'uk': 'Ukrainian',
|
||||
'ur': 'Urdu',
|
||||
'uz': 'Uzbek',
|
||||
've': 'Venda',
|
||||
'vi': 'Vietnamese',
|
||||
'vo': 'Volapük',
|
||||
'wa': 'Walloon',
|
||||
'cy': 'Welsh',
|
||||
'wo': 'Wolof',
|
||||
'fy': 'Western Frisian',
|
||||
'xh': 'Xhosa',
|
||||
'yi': 'Yiddish',
|
||||
'yo': 'Yoruba',
|
||||
'za': 'Zhuang, Chuang',
|
||||
'zu': 'Zulu',
|
||||
}
|
||||
|
||||
|
||||
def get_language_from_iso(iso: str):
|
||||
return languages[iso]
|
||||
countries = [
|
||||
('AF', 'Afghanistan'),
|
||||
('AL', 'Albania'),
|
||||
('DZ', 'Algeria'),
|
||||
('AS', 'American Samoa'),
|
||||
('AD', 'Andorra'),
|
||||
('AO', 'Angola'),
|
||||
('AI', 'Anguilla'),
|
||||
('AQ', 'Antarctica'),
|
||||
('AG', 'Antigua And Barbuda'),
|
||||
('AR', 'Argentina'),
|
||||
('AM', 'Armenia'),
|
||||
('AW', 'Aruba'),
|
||||
('AU', 'Australia'),
|
||||
('AT', 'Austria'),
|
||||
('AZ', 'Azerbaijan'),
|
||||
('BS', 'Bahamas'),
|
||||
('BH', 'Bahrain'),
|
||||
('BD', 'Bangladesh'),
|
||||
('BB', 'Barbados'),
|
||||
('BY', 'Belarus'),
|
||||
('BE', 'Belgium'),
|
||||
('BZ', 'Belize'),
|
||||
('BJ', 'Benin'),
|
||||
('BM', 'Bermuda'),
|
||||
('BT', 'Bhutan'),
|
||||
('BO', 'Bolivia'),
|
||||
('BA', 'Bosnia And Herzegowina'),
|
||||
('BW', 'Botswana'),
|
||||
('BV', 'Bouvet Island'),
|
||||
('BR', 'Brazil'),
|
||||
('BN', 'Brunei Darussalam'),
|
||||
('BG', 'Bulgaria'),
|
||||
('BF', 'Burkina Faso'),
|
||||
('BI', 'Burundi'),
|
||||
('KH', 'Cambodia'),
|
||||
('CM', 'Cameroon'),
|
||||
('CA', 'Canada'),
|
||||
('CV', 'Cape Verde'),
|
||||
('KY', 'Cayman Islands'),
|
||||
('CF', 'Central African Rep'),
|
||||
('TD', 'Chad'),
|
||||
('CL', 'Chile'),
|
||||
('CN', 'China'),
|
||||
('CX', 'Christmas Island'),
|
||||
('CC', 'Cocos Islands'),
|
||||
('CO', 'Colombia'),
|
||||
('KM', 'Comoros'),
|
||||
('CG', 'Congo'),
|
||||
('CK', 'Cook Islands'),
|
||||
('CR', 'Costa Rica'),
|
||||
('CI', 'Cote D`ivoire'),
|
||||
('HR', 'Croatia'),
|
||||
('CU', 'Cuba'),
|
||||
('CY', 'Cyprus'),
|
||||
('CZ', 'Czech Republic'),
|
||||
('DK', 'Denmark'),
|
||||
('DJ', 'Djibouti'),
|
||||
('DM', 'Dominica'),
|
||||
('DO', 'Dominican Republic'),
|
||||
('TP', 'East Timor'),
|
||||
('EC', 'Ecuador'),
|
||||
('EG', 'Egypt'),
|
||||
('SV', 'El Salvador'),
|
||||
('GQ', 'Equatorial Guinea'),
|
||||
('ER', 'Eritrea'),
|
||||
('EE', 'Estonia'),
|
||||
('ET', 'Ethiopia'),
|
||||
('FK', 'Falkland Islands (Malvinas)'),
|
||||
('FO', 'Faroe Islands'),
|
||||
('FJ', 'Fiji'),
|
||||
('FI', 'Finland'),
|
||||
('FR', 'France'),
|
||||
('GF', 'French Guiana'),
|
||||
('PF', 'French Polynesia'),
|
||||
('TF', 'French S. Territories'),
|
||||
('GA', 'Gabon'),
|
||||
('GM', 'Gambia'),
|
||||
('GE', 'Georgia'),
|
||||
('DE', 'Germany'),
|
||||
('GH', 'Ghana'),
|
||||
('GI', 'Gibraltar'),
|
||||
('GR', 'Greece'),
|
||||
('GL', 'Greenland'),
|
||||
('GD', 'Grenada'),
|
||||
('GP', 'Guadeloupe'),
|
||||
('GU', 'Guam'),
|
||||
('GT', 'Guatemala'),
|
||||
('GN', 'Guinea'),
|
||||
('GW', 'Guinea-bissau'),
|
||||
('GY', 'Guyana'),
|
||||
('HT', 'Haiti'),
|
||||
('HN', 'Honduras'),
|
||||
('HK', 'Hong Kong'),
|
||||
('HU', 'Hungary'),
|
||||
('IS', 'Iceland'),
|
||||
('IN', 'India'),
|
||||
('ID', 'Indonesia'),
|
||||
('IR', 'Iran'),
|
||||
('IQ', 'Iraq'),
|
||||
('IE', 'Ireland'),
|
||||
('IL', 'Israel'),
|
||||
('IT', 'Italy'),
|
||||
('JM', 'Jamaica'),
|
||||
('JP', 'Japan'),
|
||||
('JO', 'Jordan'),
|
||||
('KZ', 'Kazakhstan'),
|
||||
('KE', 'Kenya'),
|
||||
('KI', 'Kiribati'),
|
||||
('KP', 'Korea (North)'),
|
||||
('KR', 'Korea (South)'),
|
||||
('KW', 'Kuwait'),
|
||||
('KG', 'Kyrgyzstan'),
|
||||
('LA', 'Laos'),
|
||||
('LV', 'Latvia'),
|
||||
('LB', 'Lebanon'),
|
||||
('LS', 'Lesotho'),
|
||||
('LR', 'Liberia'),
|
||||
('LY', 'Libya'),
|
||||
('LI', 'Liechtenstein'),
|
||||
('LT', 'Lithuania'),
|
||||
('LU', 'Luxembourg'),
|
||||
('MO', 'Macau'),
|
||||
('MK', 'Macedonia'),
|
||||
('MG', 'Madagascar'),
|
||||
('MW', 'Malawi'),
|
||||
('MY', 'Malaysia'),
|
||||
('MV', 'Maldives'),
|
||||
('ML', 'Mali'),
|
||||
('MT', 'Malta'),
|
||||
('MH', 'Marshall Islands'),
|
||||
('MQ', 'Martinique'),
|
||||
('MR', 'Mauritania'),
|
||||
('MU', 'Mauritius'),
|
||||
('YT', 'Mayotte'),
|
||||
('MX', 'Mexico'),
|
||||
('FM', 'Micronesia'),
|
||||
('MD', 'Moldova'),
|
||||
('MC', 'Monaco'),
|
||||
('MN', 'Mongolia'),
|
||||
('MS', 'Montserrat'),
|
||||
('MA', 'Morocco'),
|
||||
('MZ', 'Mozambique'),
|
||||
('MM', 'Myanmar'),
|
||||
('NA', 'Namibia'),
|
||||
('NR', 'Nauru'),
|
||||
('NP', 'Nepal'),
|
||||
('NL', 'Netherlands'),
|
||||
('AN', 'Netherlands Antilles'),
|
||||
('NC', 'New Caledonia'),
|
||||
('NZ', 'New Zealand'),
|
||||
('NI', 'Nicaragua'),
|
||||
('NE', 'Niger'),
|
||||
('NG', 'Nigeria'),
|
||||
('NU', 'Niue'),
|
||||
('NF', 'Norfolk Island'),
|
||||
('MP', 'Northern Mariana Islands'),
|
||||
('NO', 'Norway'),
|
||||
('OM', 'Oman'),
|
||||
('PK', 'Pakistan'),
|
||||
('PW', 'Palau'),
|
||||
('PA', 'Panama'),
|
||||
('PG', 'Papua New Guinea'),
|
||||
('PY', 'Paraguay'),
|
||||
('PE', 'Peru'),
|
||||
('PH', 'Philippines'),
|
||||
('PN', 'Pitcairn'),
|
||||
('PL', 'Poland'),
|
||||
('PT', 'Portugal'),
|
||||
('PR', 'Puerto Rico'),
|
||||
('QA', 'Qatar'),
|
||||
('RE', 'Reunion'),
|
||||
('RO', 'Romania'),
|
||||
('RU', 'Russian Federation'),
|
||||
('RW', 'Rwanda'),
|
||||
('KN', 'Saint Kitts And Nevis'),
|
||||
('LC', 'Saint Lucia'),
|
||||
('VC', 'St Vincent/Grenadines'),
|
||||
('WS', 'Samoa'),
|
||||
('SM', 'San Marino'),
|
||||
('ST', 'Sao Tome'),
|
||||
('SA', 'Saudi Arabia'),
|
||||
('SN', 'Senegal'),
|
||||
('SC', 'Seychelles'),
|
||||
('SL', 'Sierra Leone'),
|
||||
('SG', 'Singapore'),
|
||||
('SK', 'Slovakia'),
|
||||
('SI', 'Slovenia'),
|
||||
('SB', 'Solomon Islands'),
|
||||
('SO', 'Somalia'),
|
||||
('ZA', 'South Africa'),
|
||||
('ES', 'Spain'),
|
||||
('LK', 'Sri Lanka'),
|
||||
('SH', 'St. Helena'),
|
||||
('PM', 'St.Pierre'),
|
||||
('SD', 'Sudan'),
|
||||
('SR', 'Suriname'),
|
||||
('SZ', 'Swaziland'),
|
||||
('SE', 'Sweden'),
|
||||
('CH', 'Switzerland'),
|
||||
('SY', 'Syrian Arab Republic'),
|
||||
('TW', 'Taiwan'),
|
||||
('TJ', 'Tajikistan'),
|
||||
('TZ', 'Tanzania'),
|
||||
('TH', 'Thailand'),
|
||||
('TG', 'Togo'),
|
||||
('TK', 'Tokelau'),
|
||||
('TO', 'Tonga'),
|
||||
('TT', 'Trinidad And Tobago'),
|
||||
('TN', 'Tunisia'),
|
||||
('TR', 'Turkey'),
|
||||
('TM', 'Turkmenistan'),
|
||||
('TV', 'Tuvalu'),
|
||||
('UG', 'Uganda'),
|
||||
('UA', 'Ukraine'),
|
||||
('AE', 'United Arab Emirates'),
|
||||
('UK', 'United Kingdom'),
|
||||
('US', 'United States'),
|
||||
('UY', 'Uruguay'),
|
||||
('UZ', 'Uzbekistan'),
|
||||
('VU', 'Vanuatu'),
|
||||
('VA', 'Vatican City State'),
|
||||
('VE', 'Venezuela'),
|
||||
('VN', 'Viet Nam'),
|
||||
('VG', 'Virgin Islands (British)'),
|
||||
('VI', 'Virgin Islands (U.S.)'),
|
||||
('EH', 'Western Sahara'),
|
||||
('YE', 'Yemen'),
|
||||
('YU', 'Yugoslavia'),
|
||||
('ZR', 'Zaire'),
|
||||
('ZM', 'Zambia'),
|
||||
('ZW', 'Zimbabwe')
|
||||
]
|
||||
|
||||
|
||||
def get_language(string):
|
||||
if string is None:
|
||||
def getLanguageDict():
|
||||
return lang_dict
|
||||
|
||||
|
||||
def getLanguageFromISO(iso):
|
||||
if iso is None:
|
||||
return None
|
||||
|
||||
lang = get_language_from_iso(string)
|
||||
|
||||
if lang is None:
|
||||
try:
|
||||
return pycountry.languages.lookup(string).name
|
||||
except:
|
||||
return None
|
||||
return lang
|
||||
else:
|
||||
return lang_dict[iso]
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import localefix
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# we are running in a bundle
|
||||
frozen = 'ever so'
|
||||
bundle_dir = sys._MEIPASS
|
||||
else:
|
||||
# we are running in a normal Python environment
|
||||
bundle_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# setup libunrar
|
||||
if not os.environ.get("UNRAR_LIB_PATH", None):
|
||||
os.environ["UNRAR_LIB_PATH"] = bundle_dir + "/libunrar.so"
|
||||
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
if __name__ == "__main__":
|
||||
localefix.configure_locale()
|
||||
if __name__ == '__main__':
|
||||
ctmain()
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
import platform
|
||||
from os.path import join
|
||||
from comictaggerlib import ctversion
|
||||
|
||||
enable_console = False
|
||||
binaries = []
|
||||
block_cipher = None
|
||||
|
||||
if platform.system() == "Windows":
|
||||
enable_console = True
|
||||
|
||||
a = Analysis(['comictagger.py'],
|
||||
binaries=binaries,
|
||||
datas=[('comictaggerlib/ui/*.ui', 'ui'), ('comictaggerlib/graphics', 'graphics')],
|
||||
hiddenimports=['PIL'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
# single file setup
|
||||
exclude_binaries=False,
|
||||
name='comictagger',
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=enable_console,
|
||||
icon="windows/app.ico" )
|
||||
|
||||
app = BUNDLE(exe,
|
||||
name='ComicTagger.app',
|
||||
icon='mac/app.icns',
|
||||
info_plist={
|
||||
'NSHighResolutionCapable': 'True',
|
||||
'NSRequiresAquaSystemAppearance': 'False',
|
||||
'CFBundleDisplayName': 'ComicTagger',
|
||||
'CFBundleShortVersionString': ctversion.version,
|
||||
'CFBundleVersion': ctversion.version
|
||||
},
|
||||
bundle_identifier=None)
|
||||
@@ -14,57 +14,55 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
#import sys
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.resulttypes import MultipleMatch
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#from comicvinetalker import ComicVineTalker
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, match_set_list: List[MultipleMatch], style, fetch_func, settings):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent, match_set_list, style, fetch_func):
|
||||
super(AutoTagMatchWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("matchselectionwindow.ui"), self)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.current_match_set: Optional[MultipleMatch] = None
|
||||
|
||||
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
reduce_widget_font_size(self.twList)
|
||||
reduce_widget_font_size(self.teDescription, 1)
|
||||
reduceWidgetFontSize(self.twList)
|
||||
reduceWidgetFontSize(self.teDescription, 1)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.skipButton = QtWidgets.QPushButton("Skip to Next")
|
||||
self.buttonBox.addButton(self.skipButton, QtWidgets.QDialogButtonBox.ButtonRole.ActionRole)
|
||||
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setText("Accept and Write Tags")
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
|
||||
self.buttonBox.addButton(
|
||||
self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(
|
||||
"Accept and Write Tags")
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
@@ -72,35 +70,37 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
self.current_match_set_idx = 0
|
||||
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed)
|
||||
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
|
||||
self.skipButton.clicked.connect(self.skip_to_next)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.skipButton.clicked.connect(self.skipToNext)
|
||||
|
||||
self.update_data()
|
||||
self.updateData()
|
||||
|
||||
def update_data(self):
|
||||
def updateData(self):
|
||||
|
||||
self.current_match_set = self.match_set_list[self.current_match_set_idx]
|
||||
self.current_match_set = self.match_set_list[
|
||||
self.current_match_set_idx]
|
||||
|
||||
if self.current_match_set_idx + 1 == len(self.match_set_list):
|
||||
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).setDisabled(True)
|
||||
self.skipButton.setText("Skip")
|
||||
self.buttonBox.button(
|
||||
QtGui.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
# self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.skipButton.setText(self.tr("Skip"))
|
||||
|
||||
self.set_cover_image()
|
||||
self.populate_table()
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle(
|
||||
"Select correct match or skip ({} of {}): {}".format(
|
||||
u"Select correct match or skip ({0} of {1}): {2}".format(
|
||||
self.current_match_set_idx + 1,
|
||||
len(self.match_set_list),
|
||||
os.path.split(path)[1],
|
||||
)
|
||||
os.path.split(path)[1])
|
||||
)
|
||||
|
||||
def populate_table(self):
|
||||
def populateTable(self):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
@@ -111,139 +111,135 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
for match in self.current_match_set.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match["series"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match["publisher"] is not None:
|
||||
item_text = str(match["publisher"])
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = "Unknown"
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
month_str = ""
|
||||
year_str = "????"
|
||||
if match["month"] is not None:
|
||||
month_str = f"-{int(match['month']):02d}"
|
||||
if match["year"] is not None:
|
||||
year_str = str(match["year"])
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match["issue_title"]
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
def cell_double_clicked(self, r, c):
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
|
||||
def current_item_changed(self, curr, prev):
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.altCoverWidget.set_issue_id(self.current_match()["issue_id"])
|
||||
if self.current_match()["description"] is None:
|
||||
self.altCoverWidget.setIssueID(self.currentMatch()['issue_id'])
|
||||
if self.currentMatch()['description'] is None:
|
||||
self.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(self.current_match()["description"])
|
||||
self.teDescription.setText(self.currentMatch()['description'])
|
||||
|
||||
def set_cover_image(self):
|
||||
def setCoverImage(self):
|
||||
ca = self.current_match_set.ca
|
||||
self.archiveCoverWidget.set_archive(ca)
|
||||
self.archiveCoverWidget.setArchive(ca)
|
||||
|
||||
def current_match(self):
|
||||
def currentMatch(self):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
match = self.twList.item(row, 0).data(
|
||||
QtCore.Qt.UserRole).toPyObject()[0]
|
||||
return match
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.save_match()
|
||||
self.saveMatch()
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len(self.match_set_list):
|
||||
# no more items
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
else:
|
||||
self.update_data()
|
||||
self.updateData()
|
||||
|
||||
def skip_to_next(self):
|
||||
def skipToNext(self):
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len(self.match_set_list):
|
||||
# no more items
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QtGui.QDialog.reject(self)
|
||||
else:
|
||||
self.update_data()
|
||||
self.updateData()
|
||||
|
||||
def reject(self):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
reply = QtGui.QMessageBox.question(
|
||||
self,
|
||||
"Cancel Matching",
|
||||
"Are you sure you wish to cancel the matching process?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
self.tr("Cancel Matching"),
|
||||
self.tr("Are you sure you wish to cancel the matching process?"),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QtGui.QDialog.reject(self)
|
||||
|
||||
def save_match(self):
|
||||
def saveMatch(self):
|
||||
|
||||
match = self.current_match()
|
||||
match = self.currentMatch()
|
||||
ca = self.current_match_set.ca
|
||||
|
||||
md = ca.read_metadata(self.style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
)
|
||||
md = ca.readMetadata(self.style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.fetch_func(match)
|
||||
if cv_md is None:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Network Issue", "Could not connect to Comic Vine to get issue details!"
|
||||
)
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr(
|
||||
"Could not connect to Comic Vine to get issue details!"))
|
||||
return
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay(cv_md)
|
||||
success = ca.write_metadata(md, self.style)
|
||||
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
success = ca.writeMetadata(md, self.style)
|
||||
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
QtWidgets.QMessageBox.warning(self, "Write Error", "Saving the tags to the archive seemed to fail!")
|
||||
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr(
|
||||
"Saving the tags to the archive seemed to fail!"))
|
||||
|
||||
@@ -14,57 +14,56 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagProgressWindow(QtWidgets.QDialog):
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("autotagprogresswindow.ui"), self)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('autotagprogresswindow.ui'), self)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
|
||||
self.testCoverWidget = CoverImageWidget(
|
||||
self.testCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtGui.QGridLayout(self.testCoverContainer)
|
||||
gridlayout.addWidget(self.testCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.isdone = False
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
reduce_widget_font_size(self.textEdit)
|
||||
reduceWidgetFontSize(self.textEdit)
|
||||
|
||||
def set_archive_image(self, img_data):
|
||||
self.set_cover_image(img_data, self.archiveCoverWidget)
|
||||
def setArchiveImage(self, img_data):
|
||||
self.setCoverImage(img_data, self.archiveCoverWidget)
|
||||
|
||||
def set_test_image(self, img_data):
|
||||
self.set_cover_image(img_data, self.testCoverWidget)
|
||||
def setTestImage(self, img_data):
|
||||
self.setCoverImage(img_data, self.testCoverWidget)
|
||||
|
||||
def set_cover_image(self, img_data, widget):
|
||||
widget.set_image_data(img_data)
|
||||
def setCoverImage(self, img_data, widget):
|
||||
widget.setImageData(img_data)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
||||
@@ -14,105 +14,114 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
#from settingswindow import SettingsWindow
|
||||
#from filerenamer import FileRenamer
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, settings, msg):
|
||||
super().__init__(parent)
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("autotagstartwindow.ui"), self)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('autotagstartwindow.ui'), self)
|
||||
self.label.setText(msg)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.cbxDontUseYear.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.leNameLengthMatchTolerance.setText(str(self.settings.id_length_delta_thresh))
|
||||
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxDontUseYear.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
|
||||
QtCore.Qt.Unchecked)
|
||||
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.leNameLengthMatchTolerance.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.leSearchString.setEnabled(False)
|
||||
|
||||
if self.settings.save_on_low_confidence:
|
||||
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.dont_use_year_when_identifying:
|
||||
self.cbxDontUseYear.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxDontUseYear.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.assume_1_if_no_issue_num:
|
||||
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.ignore_leading_numbers_in_filename:
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
|
||||
QtCore.Qt.Checked)
|
||||
if self.settings.remove_archive_after_successful_match:
|
||||
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.wait_and_retry_on_rate_limit:
|
||||
self.cbxWaitForRateLimit.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxWaitForRateLimit.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
nlmt_tip = """ <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
|
||||
nlmtTip = (
|
||||
""" <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The higher
|
||||
it is, the more likely to have a good match, but each search will take longer and
|
||||
use more bandwidth. Too low, and only the very closest lexical matches will be
|
||||
explored.</html>"""
|
||||
explored.</html>""")
|
||||
|
||||
self.leNameLengthMatchTolerance.setToolTip(nlmt_tip)
|
||||
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
|
||||
|
||||
ss_tip = """<html>
|
||||
ssTip = (
|
||||
"""<html>
|
||||
The <b>series search string</b> specifies the search string to be used for all selected archives.
|
||||
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
|
||||
should be from the same series.
|
||||
</html>"""
|
||||
self.leSearchString.setToolTip(ss_tip)
|
||||
self.cbxSpecifySearchString.setToolTip(ss_tip)
|
||||
)
|
||||
self.leSearchString.setToolTip(ssTip)
|
||||
self.cbxSpecifySearchString.setToolTip(ssTip)
|
||||
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthMatchTolerance.setValidator(validator)
|
||||
|
||||
self.cbxSpecifySearchString.stateChanged.connect(self.search_string_toggle)
|
||||
self.cbxSpecifySearchString.stateChanged.connect(
|
||||
self.searchStringToggle)
|
||||
|
||||
self.auto_save_on_low = False
|
||||
self.dont_use_year = False
|
||||
self.assume_issue_one = False
|
||||
self.ignore_leading_digits_in_filename = False
|
||||
self.remove_after_success = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.search_string = None
|
||||
self.name_length_match_tolerance = self.settings.id_length_delta_thresh
|
||||
self.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
self.ignoreLeadingDigitsInFilename = False
|
||||
self.removeAfterSuccess = False
|
||||
self.waitAndRetryOnRateLimit = False
|
||||
self.searchString = None
|
||||
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
|
||||
|
||||
def search_string_toggle(self):
|
||||
def searchStringToggle(self):
|
||||
enable = self.cbxSpecifySearchString.isChecked()
|
||||
self.leSearchString.setEnabled(enable)
|
||||
|
||||
def accept(self):
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.auto_save_on_low = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dont_use_year = self.cbxDontUseYear.isChecked()
|
||||
self.assume_issue_one = self.cbxAssumeIssueOne.isChecked()
|
||||
self.ignore_leading_digits_in_filename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
|
||||
self.remove_after_success = self.cbxRemoveAfterSuccess.isChecked()
|
||||
self.name_length_match_tolerance = int(self.leNameLengthMatchTolerance.text())
|
||||
self.wait_and_retry_on_rate_limit = self.cbxWaitForRateLimit.isChecked()
|
||||
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
|
||||
self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
|
||||
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
|
||||
self.nameLengthMatchTolerance = int(
|
||||
self.leNameLengthMatchTolerance.text())
|
||||
self.waitAndRetryOnRateLimit = self.cbxWaitForRateLimit.isChecked()
|
||||
|
||||
# persist some settings
|
||||
self.settings.save_on_low_confidence = self.auto_save_on_low
|
||||
self.settings.dont_use_year_when_identifying = self.dont_use_year
|
||||
self.settings.assume_1_if_no_issue_num = self.assume_issue_one
|
||||
self.settings.ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
|
||||
self.settings.remove_archive_after_successful_match = self.remove_after_success
|
||||
self.settings.wait_and_retry_on_rate_limit = self.wait_and_retry_on_rate_limit
|
||||
self.settings.save_on_low_confidence = self.autoSaveOnLow
|
||||
self.settings.dont_use_year_when_identifying = self.dontUseYear
|
||||
self.settings.assume_1_if_no_issue_num = self.assumeIssueOne
|
||||
self.settings.ignore_leading_numbers_in_filename = self.ignoreLeadingDigitsInFilename
|
||||
self.settings.remove_archive_after_successful_match = self.removeAfterSuccess
|
||||
self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.search_string = str(self.leSearchString.text())
|
||||
if len(self.search_string) == 0:
|
||||
self.search_string = None
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
||||
self.searchString = None
|
||||
|
||||
@@ -14,15 +14,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
#import os
|
||||
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#import utils
|
||||
|
||||
|
||||
class CBLTransformer:
|
||||
def __init__(self, metadata: GenericMetadata, settings):
|
||||
|
||||
def __init__(self, metadata, settings):
|
||||
self.metadata = metadata
|
||||
self.settings = settings
|
||||
|
||||
@@ -34,36 +33,36 @@ class CBLTransformer:
|
||||
|
||||
def add_string_list_to_tags(str_list):
|
||||
if str_list is not None and str_list != "":
|
||||
items = [s.strip() for s in str_list.split(",")]
|
||||
items = [s.strip() for s in str_list.split(',')]
|
||||
for item in items:
|
||||
append_to_tags_if_unique(item)
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
|
||||
# helper
|
||||
def set_lone_primary(role_list):
|
||||
def setLonePrimary(role_list):
|
||||
lone_credit = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
if c["role"].lower() in role_list:
|
||||
if c['role'].lower() in role_list:
|
||||
count += 1
|
||||
lone_credit = c
|
||||
if count > 1:
|
||||
lone_credit = None
|
||||
break
|
||||
if lone_credit is not None:
|
||||
lone_credit["primary"] = True
|
||||
lone_credit['primary'] = True
|
||||
return lone_credit, count
|
||||
|
||||
# need to loop three times, once for 'writer', 'artist', and then
|
||||
# 'penciler' if no artist
|
||||
set_lone_primary(["writer"])
|
||||
c, count = set_lone_primary(["artist"])
|
||||
setLonePrimary(['writer'])
|
||||
c, count = setLonePrimary(['artist'])
|
||||
if c is None and count == 0:
|
||||
c, count = set_lone_primary(["penciler", "penciller"])
|
||||
c, count = setLonePrimary(['penciler', 'penciller'])
|
||||
if c is not None:
|
||||
c["primary"] = False
|
||||
self.metadata.add_credit(c["person"], "Artist", True)
|
||||
c['primary'] = False
|
||||
self.metadata.addCredit(c['person'], 'Artist', True)
|
||||
|
||||
if self.settings.copy_characters_to_tags:
|
||||
add_string_list_to_tags(self.metadata.characters)
|
||||
@@ -75,7 +74,7 @@ class CBLTransformer:
|
||||
add_string_list_to_tags(self.metadata.locations)
|
||||
|
||||
if self.settings.copy_storyarcs_to_tags:
|
||||
add_string_list_to_tags(self.metadata.story_arc)
|
||||
add_string_list_to_tags(self.metadata.storyArc)
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.metadata.notes is not None:
|
||||
@@ -87,12 +86,12 @@ class CBLTransformer:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.metadata.web_link is not None:
|
||||
if self.metadata.webLink is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.web_link not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.web_link
|
||||
if self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.webLink
|
||||
|
||||
return self.metadata
|
||||
|
||||
@@ -16,34 +16,60 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import os
|
||||
from pprint import pprint
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib.cbltransformer import CBLTransformer
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from comictaggerlib.filerenamer import FileRenamer
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
import json
|
||||
#import signal
|
||||
#import traceback
|
||||
#import time
|
||||
#import platform
|
||||
#import locale
|
||||
#import codecs
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options
|
||||
from comicarchive import ComicArchive, MetaDataStyle
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
import utils
|
||||
|
||||
|
||||
class MultipleMatch():
|
||||
|
||||
def __init__(self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
|
||||
class OnlineMatchResults():
|
||||
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
|
||||
def actual_issue_data_fetch(match, settings, opts):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
comic_vine = ComicVineTalker()
|
||||
comic_vine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
|
||||
cv_md = comic_vine.fetch_issue_data(match["volume_id"], match["issue_number"], settings)
|
||||
comicVine = ComicVineTalker()
|
||||
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
|
||||
cv_md = comicVine.fetchIssueData(
|
||||
match['volume_id'], match['issue_number'], settings)
|
||||
except ComicVineTalkerException:
|
||||
logger.exception("Network error while getting issue details. Save aborted")
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
@@ -52,96 +78,100 @@ def actual_issue_data_fetch(match, settings, opts):
|
||||
return cv_md
|
||||
|
||||
|
||||
def actual_metadata_save(ca: ComicArchive, opts, md):
|
||||
def actual_metadata_save(ca, opts, md):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.write_metadata(md, opts.data_style):
|
||||
logger.error("The tag save seemed to fail!")
|
||||
if not ca.writeMetadata(md, opts.data_style):
|
||||
print >> sys.stderr, "The tag save seemed to fail!"
|
||||
return False
|
||||
|
||||
print("Save complete.")
|
||||
logger.info("Save complete.")
|
||||
else:
|
||||
print >> sys.stderr, "Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
logger.info("dry-run option was set, so nothing was written")
|
||||
print("dry-run option was set, so nothing was written")
|
||||
print >> sys.stderr, "dry-run option was set, so nothing was written"
|
||||
else:
|
||||
logger.info("dry-run option was set, so nothing was written, but here is the final set of tags:")
|
||||
print("dry-run option was set, so nothing was written, but here is the final set of tags:")
|
||||
print(f"{md}")
|
||||
print >> sys.stderr, "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print(u"{0}".format(md))
|
||||
return True
|
||||
|
||||
|
||||
def display_match_set_for_choice(label, match_set: MultipleMatch, opts, settings):
|
||||
print(f"{match_set.ca.path} -- {label}:")
|
||||
def display_match_set_for_choice(label, match_set, opts, settings):
|
||||
print(u"{0} -- {1}:".format(match_set.filename, label))
|
||||
|
||||
# sort match list by year
|
||||
match_set.matches.sort(key=lambda k: k["year"])
|
||||
match_set.matches.sort(key=lambda k: k['year'])
|
||||
|
||||
for (counter, m) in enumerate(match_set.matches):
|
||||
counter += 1
|
||||
print(
|
||||
" {}. {} #{} [{}] ({}/{}) - {}".format(
|
||||
u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
|
||||
counter,
|
||||
m["series"],
|
||||
m["issue_number"],
|
||||
m["publisher"],
|
||||
m["month"],
|
||||
m["year"],
|
||||
m["issue_title"],
|
||||
)
|
||||
)
|
||||
m['series'],
|
||||
m['issue_number'],
|
||||
m['publisher'],
|
||||
m['month'],
|
||||
m['year'],
|
||||
m['issue_title']))
|
||||
if opts.interactive:
|
||||
while True:
|
||||
i = input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(
|
||||
1, len(match_set.matches) + 1)) or i == 's':
|
||||
break
|
||||
if i != "s":
|
||||
if i != 's':
|
||||
i = int(i) - 1
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = match_set.ca
|
||||
md = create_local_metadata(opts, ca, ca.has_metadata(opts.data_style), settings)
|
||||
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings, opts)
|
||||
ca = ComicArchive(
|
||||
match_set.filename,
|
||||
settings.rar_exe_path,
|
||||
ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
md = create_local_metadata(
|
||||
opts, ca, ca.hasMetadata(opts.data_style))
|
||||
cv_md = actual_issue_data_fetch(
|
||||
match_set.matches[int(i)], settings, opts)
|
||||
md.overlay(cv_md)
|
||||
actual_metadata_save(ca, opts, md)
|
||||
|
||||
|
||||
def post_process_matches(match_results: OnlineMatchResults, opts, settings):
|
||||
def post_process_matches(match_results, opts, settings):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len(match_results.good_matches) > 0:
|
||||
if len(match_results.goodMatches) > 0:
|
||||
print("\nSuccessful matches:\n------------------")
|
||||
for f in match_results.good_matches:
|
||||
for f in match_results.goodMatches:
|
||||
print(f)
|
||||
|
||||
if len(match_results.no_matches) > 0:
|
||||
if len(match_results.noMatches) > 0:
|
||||
print("\nNo matches:\n------------------")
|
||||
for f in match_results.no_matches:
|
||||
for f in match_results.noMatches:
|
||||
print(f)
|
||||
|
||||
if len(match_results.write_failures) > 0:
|
||||
if len(match_results.writeFailures) > 0:
|
||||
print("\nFile Write Failures:\n------------------")
|
||||
for f in match_results.write_failures:
|
||||
for f in match_results.writeFailures:
|
||||
print(f)
|
||||
|
||||
if len(match_results.fetch_data_failures) > 0:
|
||||
if len(match_results.fetchDataFailures) > 0:
|
||||
print("\nNetwork Data Fetch Failures:\n------------------")
|
||||
for f in match_results.fetch_data_failures:
|
||||
for f in match_results.fetchDataFailures:
|
||||
print(f)
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
# just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len(match_results.multiple_matches) > 0:
|
||||
print("\nArchives with multiple high-confidence matches:\n------------------")
|
||||
for match_set in match_results.multiple_matches:
|
||||
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings)
|
||||
if len(match_results.multipleMatches) > 0:
|
||||
print(
|
||||
"\nArchives with multiple high-confidence matches:\n------------------")
|
||||
for match_set in match_results.multipleMatches:
|
||||
display_match_set_for_choice(
|
||||
"Multiple high-confidence matches", match_set, opts, settings)
|
||||
|
||||
if len(match_results.low_confidence_matches) > 0:
|
||||
if len(match_results.lowConfidenceMatches) > 0:
|
||||
print("\nArchives with low-confidence matches:\n------------------")
|
||||
for match_set in match_results.low_confidence_matches:
|
||||
for match_set in match_results.lowConfidenceMatches:
|
||||
if len(match_set.matches) == 1:
|
||||
label = "Single low-confidence match"
|
||||
else:
|
||||
@@ -152,32 +182,31 @@ def post_process_matches(match_results: OnlineMatchResults, opts, settings):
|
||||
|
||||
def cli_mode(opts, settings):
|
||||
if len(opts.file_list) < 1:
|
||||
logger.error("You must specify at least one filename. Use the -h option for more info")
|
||||
print >> sys.stderr, "You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
if isinstance(f, str):
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli(f, opts, settings, match_results)
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches(match_results, opts, settings)
|
||||
|
||||
|
||||
def create_local_metadata(opts, ca: ComicArchive, has_desired_tags, settings):
|
||||
def create_local_metadata(opts, ca, has_desired_tags):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.set_default_page_list(ca.get_number_of_pages())
|
||||
md.setDefaultPageList(ca.getNumberOfPages())
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata(opts.data_style)
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay(
|
||||
ca.metadata_from_filename(
|
||||
settings.complicated_parser, settings.remove_c2c, settings.remove_fcbd, settings.remove_publisher
|
||||
)
|
||||
)
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.read_metadata(opts.data_style)
|
||||
md.overlay(ca.metadataFromFilename())
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
@@ -186,54 +215,61 @@ def create_local_metadata(opts, ca: ComicArchive, has_desired_tags, settings):
|
||||
return md
|
||||
|
||||
|
||||
def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults):
|
||||
def process_file_cli(filename, opts, settings, match_results):
|
||||
|
||||
batch_mode = len(opts.file_list) > 1
|
||||
|
||||
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.get_graphic("nocover.png"))
|
||||
ca = ComicArchive(
|
||||
filename,
|
||||
settings.rar_exe_path,
|
||||
ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
|
||||
if not os.path.lexists(filename):
|
||||
logger.error("Cannot find %s", filename)
|
||||
print >> sys.stderr, "Cannot find " + filename
|
||||
return
|
||||
|
||||
if not ca.seems_to_be_a_comic_archive():
|
||||
logger.error("Sorry, but %s is not a comic archive!", filename)
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print >> sys.stderr, "Sorry, but " + \
|
||||
filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
if not ca.is_writable() and (opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
|
||||
logger.error("This archive is not writable for that tag type")
|
||||
# if not ca.isWritableForStyle(opts.data_style) and (opts.delete_tags or
|
||||
# opts.save_tags or opts.rename_file):
|
||||
if not ca.isWritable() and (
|
||||
opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
|
||||
print >> sys.stderr, "This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [False, False, False]
|
||||
if ca.has_cix():
|
||||
if ca.hasCIX():
|
||||
has[MetaDataStyle.CIX] = True
|
||||
if ca.has_cbi():
|
||||
if ca.hasCBI():
|
||||
has[MetaDataStyle.CBI] = True
|
||||
if ca.has_comet():
|
||||
if ca.hasCoMet():
|
||||
has[MetaDataStyle.COMET] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.get_number_of_pages()
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
|
||||
if batch_mode:
|
||||
brief = f"{ca.path}: "
|
||||
brief = u"{0}: ".format(filename)
|
||||
|
||||
if ca.is_sevenzip():
|
||||
brief += "7Z archive "
|
||||
elif ca.is_zip():
|
||||
if ca.isZip():
|
||||
brief += "ZIP archive "
|
||||
elif ca.is_rar():
|
||||
elif ca.isRar():
|
||||
brief += "RAR archive "
|
||||
elif ca.is_folder():
|
||||
elif ca.isFolder():
|
||||
brief += "Folder archive "
|
||||
|
||||
brief += f"({page_count: >3} pages)"
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
|
||||
if not (has[MetaDataStyle.CBI] or has[
|
||||
MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
|
||||
brief += "none "
|
||||
else:
|
||||
if has[MetaDataStyle.CBI]:
|
||||
@@ -244,86 +280,101 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print(brief)
|
||||
print brief
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print()
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if has[MetaDataStyle.CIX]:
|
||||
print("--------- ComicRack tags ---------")
|
||||
if opts.raw:
|
||||
print(ca.read_raw_cix())
|
||||
print(
|
||||
u"{0}".format(
|
||||
unicode(
|
||||
ca.readRawCIX(),
|
||||
errors='ignore')))
|
||||
else:
|
||||
print(ca.read_cix())
|
||||
print(u"{0}".format(ca.readCIX()))
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if has[MetaDataStyle.CBI]:
|
||||
print("------- ComicBookLover tags -------")
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.read_raw_cbi()))
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print(ca.read_cbi())
|
||||
print(u"{0}".format(ca.readCBI()))
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[MetaDataStyle.COMET]:
|
||||
print("----------- CoMet tags -----------")
|
||||
if opts.raw:
|
||||
print(ca.read_raw_comet())
|
||||
print(u"{0}".format(ca.readRawCoMet()))
|
||||
else:
|
||||
print(ca.read_comet())
|
||||
print(u"{0}".format(ca.readCoMet()))
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[opts.data_style]
|
||||
if has[opts.data_style]:
|
||||
if not opts.dryrun:
|
||||
if not ca.remove_metadata(opts.data_style):
|
||||
print(f"{filename}: Tag removal seemed to fail!")
|
||||
if not ca.removeMetadata(opts.data_style):
|
||||
print(u"{0}: Tag removal seemed to fail!".format(filename))
|
||||
else:
|
||||
print(f"{filename}: Removed {style_name} tags.")
|
||||
print(
|
||||
u"{0}: Removed {1} tags.".format(filename, style_name))
|
||||
else:
|
||||
print(f"{filename}: dry-run. {style_name} tags not removed")
|
||||
print(
|
||||
u"{0}: dry-run. {1} tags not removed".format(filename, style_name))
|
||||
else:
|
||||
print(f"{filename}: This archive doesn't have {style_name} tags to remove.")
|
||||
print(u"{0}: This archive doesn't have {1} tags to remove.".format(
|
||||
filename, style_name))
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[opts.data_style]
|
||||
if opts.no_overwrite and has[opts.data_style]:
|
||||
print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.")
|
||||
print(u"{0}: Already has {1} tags. Not overwriting.".format(
|
||||
filename, dst_style_name))
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.")
|
||||
print(
|
||||
u"{0}: Destination and source are same: {1}. Nothing to do.".format(
|
||||
filename,
|
||||
dst_style_name))
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[opts.copy_source]
|
||||
if has[opts.copy_source]:
|
||||
if not opts.dryrun:
|
||||
md = ca.read_metadata(opts.copy_source)
|
||||
md = ca.readMetadata(opts.copy_source)
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer(md, settings).apply()
|
||||
|
||||
if not ca.write_metadata(md, opts.data_style):
|
||||
print(f"{filename}: Tag copy seemed to fail!")
|
||||
if not ca.writeMetadata(md, opts.data_style):
|
||||
print(u"{0}: Tag copy seemed to fail!".format(filename))
|
||||
else:
|
||||
print(f"{filename}: Copied {src_style_name} tags to {dst_style_name}.")
|
||||
print(u"{0}: Copied {1} tags to {2} .".format(
|
||||
filename, src_style_name, dst_style_name))
|
||||
else:
|
||||
print(f"{filename}: dry-run. {src_style_name} tags not copied")
|
||||
print(
|
||||
u"{0}: dry-run. {1} tags not copied".format(filename, src_style_name))
|
||||
else:
|
||||
print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.")
|
||||
print(u"{0}: This archive doesn't have {1} tags to copy.".format(
|
||||
filename, src_style_name))
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
if opts.no_overwrite and has[opts.data_style]:
|
||||
print(f"{filename}: Already has {MetaDataStyle.name[opts.data_style]} tags. Not overwriting.")
|
||||
print(u"{0}: Already has {1} tags. Not overwriting.".format(
|
||||
filename, MetaDataStyle.name[opts.data_style]))
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print(f"Processing {ca.path}...")
|
||||
print(u"Processing {0}...".format(filename))
|
||||
|
||||
md = create_local_metadata(opts, ca, has[opts.data_style], settings)
|
||||
md = create_local_metadata(opts, ca, has[opts.data_style])
|
||||
if md.issue is None or md.issue == "":
|
||||
if opts.assume_issue_is_one_if_not_set:
|
||||
md.issue = "1"
|
||||
@@ -333,17 +384,19 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
comic_vine = ComicVineTalker()
|
||||
comic_vine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
|
||||
cv_md = comic_vine.fetch_issue_data_by_issue_id(opts.issue_id, settings)
|
||||
comicVine = ComicVineTalker()
|
||||
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
|
||||
cv_md = comicVine.fetchIssueDataByIssueID(
|
||||
opts.issue_id, settings)
|
||||
except ComicVineTalkerException:
|
||||
logger.exception("Network error while getting issue details. Save aborted")
|
||||
match_results.fetch_data_failures.append(ca.path)
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
if cv_md is None:
|
||||
logger.error("No match for ID %s was found.", opts.issue_id)
|
||||
match_results.no_matches.append(ca.path)
|
||||
print >> sys.stderr, "No match for ID {0} was found.".format(
|
||||
opts.issue_id)
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
@@ -351,21 +404,21 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
else:
|
||||
ii = IssueIdentifier(ca, settings)
|
||||
|
||||
if md is None or md.is_empty:
|
||||
logger.error("No metadata given to search online with!")
|
||||
match_results.no_matches.append(ca.path)
|
||||
if md is None or md.isEmpty:
|
||||
print >> sys.stderr, "No metadata given to search online with!"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
def myoutput(text):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.default_write_output(text)
|
||||
IssueIdentifier.defaultWriteOutput(text)
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.set_additional_metadata(md)
|
||||
ii.only_use_additional_meta_data = True
|
||||
ii.wait_and_retry_on_rate_limit = opts.wait_and_retry_on_rate_limit
|
||||
ii.set_output_function(myoutput)
|
||||
ii.cover_page_index = md.get_cover_page_index_list()[0]
|
||||
ii.setAdditionalMetadata(md)
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.waitAndRetryOnRateLimit = opts.wait_and_retry_on_rate_limit
|
||||
ii.setOutputFunction(myoutput)
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
@@ -374,37 +427,40 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.result_no_matches:
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.result_found_match_but_bad_cover_score:
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.result_found_match_but_not_first_page:
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage:
|
||||
found_match = True
|
||||
elif result == ii.result_multiple_matches_with_bad_image_scores:
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.result_one_good_match:
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.result_multiple_good_matches:
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
if low_confidence:
|
||||
logger.error("Online search: Multiple low confidence matches. Save aborted")
|
||||
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
|
||||
print >> sys.stderr, "Online search: Multiple low confidence matches. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(
|
||||
MultipleMatch(filename, matches))
|
||||
return
|
||||
else:
|
||||
print >> sys.stderr, "Online search: Multiple good matches. Save aborted"
|
||||
match_results.multipleMatches.append(
|
||||
MultipleMatch(filename, matches))
|
||||
return
|
||||
|
||||
logger.error("Online search: Multiple good matches. Save aborted")
|
||||
match_results.multiple_matches.append(MultipleMatch(ca, matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
logger.error("Online search: Low confidence match. Save aborted")
|
||||
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
|
||||
print >> sys.stderr, "Online search: Low confidence match. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(
|
||||
MultipleMatch(filename, matches))
|
||||
return
|
||||
if not found_match:
|
||||
logger.error("Online search: No match found. Save aborted")
|
||||
match_results.no_matches.append(ca.path)
|
||||
print >> sys.stderr, "Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
# we got here, so we have a single match
|
||||
@@ -412,95 +468,79 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings, opts)
|
||||
if cv_md is None:
|
||||
match_results.fetch_data_failures.append(ca.path)
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
md.overlay(cv_md)
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save(ca, opts, md):
|
||||
match_results.write_failures.append(ca.path)
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
match_results.good_matches.append(ca.path)
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = f"{ca.path}: "
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[opts.data_style]
|
||||
else:
|
||||
use_tags = False
|
||||
|
||||
md = create_local_metadata(opts, ca, use_tags, settings)
|
||||
md = create_local_metadata(opts, ca, use_tags)
|
||||
|
||||
if md.series is None:
|
||||
logger.error(msg_hdr + "Can't rename without series name")
|
||||
print >> sys.stderr, msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.is_sevenzip():
|
||||
new_ext = ".cb7"
|
||||
elif ca.is_zip():
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.is_rar():
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer(md, platform="universal" if settings.rename_strict else "auto")
|
||||
renamer.set_template(settings.rename_template)
|
||||
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
|
||||
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
|
||||
renamer.move = settings.rename_move_dir
|
||||
renamer = FileRenamer(md)
|
||||
renamer.setTemplate(settings.rename_template)
|
||||
renamer.setIssueZeroPadding(settings.rename_issue_number_padding)
|
||||
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
|
||||
|
||||
try:
|
||||
new_name = renamer.determine_name(ext=new_ext)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
msg_hdr + "Invalid format string!\n"
|
||||
"Your rename template is invalid!\n\n"
|
||||
"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"
|
||||
)
|
||||
new_name = renamer.determineName(filename, ext=new_ext)
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print >> sys.stderr, msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(filename))
|
||||
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
|
||||
folder = settings.rename_dir.strip()
|
||||
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
|
||||
|
||||
if os.path.join(folder, new_name) == os.path.abspath(filename):
|
||||
print(msg_hdr + "Filename is already good!", file=sys.stderr)
|
||||
return
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
|
||||
os.rename(filename, new_abs_path)
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print(f"renamed '{os.path.basename(ca.path)}' -> '{new_name}' {suffix}")
|
||||
print(
|
||||
u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix))
|
||||
|
||||
elif opts.export_to_zip:
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = f"{ca.path}: "
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if not ca.is_rar():
|
||||
logger.error(msg_hdr + "Archive is not a RAR.")
|
||||
if not ca.isRar():
|
||||
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
|
||||
return
|
||||
|
||||
rar_file = os.path.abspath(os.path.abspath(filename))
|
||||
new_file = os.path.splitext(rar_file)[0] + ".cbz"
|
||||
|
||||
if opts.abort_export_on_conflict and os.path.lexists(new_file):
|
||||
print(msg_hdr + f"{os.path.split(new_file)[1]} already exists in the that folder.")
|
||||
print msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1])
|
||||
return
|
||||
|
||||
new_file = utils.unique_file(os.path.join(new_file))
|
||||
@@ -508,13 +548,14 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
delete_success = False
|
||||
export_success = False
|
||||
if not opts.dryrun:
|
||||
if ca.export_as_zip(new_file):
|
||||
if ca.exportAsZip(new_file):
|
||||
export_success = True
|
||||
if opts.delete_rar_after_export:
|
||||
try:
|
||||
os.unlink(rar_file)
|
||||
except:
|
||||
logger.exception(msg_hdr + "Error deleting original RAR after export")
|
||||
print >> sys.stderr, msg_hdr + \
|
||||
"Error deleting original RAR after export"
|
||||
delete_success = False
|
||||
else:
|
||||
delete_success = True
|
||||
@@ -523,18 +564,21 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
|
||||
if os.path.lexists(new_file):
|
||||
os.remove(new_file)
|
||||
else:
|
||||
msg = msg_hdr + f"Dry-run: Would try to create {os.path.split(new_file)[1]}"
|
||||
if opts.delete_after_zip_export:
|
||||
msg += " and delete orginal."
|
||||
msg = msg_hdr + \
|
||||
u"Dry-run: Would try to create {0}".format(
|
||||
os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export:
|
||||
msg += u" and delete orginal."
|
||||
print(msg)
|
||||
return
|
||||
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}"
|
||||
msg += u"Archive exported successfully to: {0}".format(
|
||||
os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export and delete_success:
|
||||
msg += " (Original deleted) "
|
||||
msg += u" (Original deleted) "
|
||||
else:
|
||||
msg += "Archive failed to export!"
|
||||
msg += u"Archive failed to export!"
|
||||
|
||||
print(msg)
|
||||
|
||||
1
comictaggerlib/comet.py
Normal file
1
comictaggerlib/comet.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comet import *
|
||||
1
comictaggerlib/comicarchive.py
Normal file
1
comictaggerlib/comicarchive.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comicarchive import *
|
||||
1
comictaggerlib/comicbookinfo.py
Normal file
1
comictaggerlib/comicbookinfo.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comicbookinfo import *
|
||||
1
comictaggerlib/comicinfoxml.py
Normal file
1
comictaggerlib/comicinfoxml.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comicinfoxml import *
|
||||
@@ -14,39 +14,40 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sqlite3 as lite
|
||||
import os
|
||||
import datetime
|
||||
#import sys
|
||||
#from pprint import pprint
|
||||
|
||||
from comicapi import utils
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import ctversion
|
||||
from settings import ComicTaggerSettings
|
||||
import utils
|
||||
|
||||
|
||||
class ComicVineCacher:
|
||||
|
||||
def __init__(self):
|
||||
self.settings_folder = ComicTaggerSettings.get_settings_folder()
|
||||
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
|
||||
self.db_file = os.path.join(self.settings_folder, "cv_cache.db")
|
||||
self.version_file = os.path.join(self.settings_folder, "cache_version.txt")
|
||||
self.version_file = os.path.join(
|
||||
self.settings_folder, "cache_version.txt")
|
||||
|
||||
# verify that cache is from same version as this one
|
||||
data = ""
|
||||
try:
|
||||
with open(self.version_file, "rb") as f:
|
||||
data = f.read().decode("utf-8")
|
||||
with open(self.version_file, 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
if data != ctversion.version:
|
||||
self.clear_cache()
|
||||
self.clearCache()
|
||||
|
||||
if not os.path.exists(self.db_file):
|
||||
self.create_cache_db()
|
||||
|
||||
def clear_cache(self):
|
||||
def clearCache(self):
|
||||
try:
|
||||
os.unlink(self.db_file)
|
||||
except:
|
||||
@@ -59,131 +60,133 @@ class ComicVineCacher:
|
||||
def create_cache_db(self):
|
||||
|
||||
# create the version file
|
||||
with open(self.version_file, "w", encoding="utf-8") as f:
|
||||
with open(self.version_file, 'w') as f:
|
||||
f.write(ctversion.version)
|
||||
|
||||
# this will wipe out any existing version
|
||||
open(self.db_file, "wb").close()
|
||||
open(self.db_file, 'w').close()
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
# create tables
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
# name,id,start_year,publisher,image,description,count_of_issues
|
||||
cur.execute(
|
||||
"CREATE TABLE VolumeSearchCache("
|
||||
+ "search_term TEXT,"
|
||||
+ "id INT,"
|
||||
+ "name TEXT,"
|
||||
+ "start_year INT,"
|
||||
+ "publisher TEXT,"
|
||||
+ "count_of_issues INT,"
|
||||
+ "image_url TEXT,"
|
||||
+ "description TEXT,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime'))) "
|
||||
)
|
||||
"CREATE TABLE VolumeSearchCache(" +
|
||||
"search_term TEXT," +
|
||||
"id INT," +
|
||||
"name TEXT," +
|
||||
"start_year INT," +
|
||||
"publisher TEXT," +
|
||||
"count_of_issues INT," +
|
||||
"image_url TEXT," +
|
||||
"description TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime'))) ")
|
||||
|
||||
cur.execute(
|
||||
"CREATE TABLE Volumes("
|
||||
+ "id INT,"
|
||||
+ "name TEXT,"
|
||||
+ "publisher TEXT,"
|
||||
+ "count_of_issues INT,"
|
||||
+ "start_year INT,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "PRIMARY KEY (id))"
|
||||
)
|
||||
"CREATE TABLE Volumes(" +
|
||||
"id INT," +
|
||||
"name TEXT," +
|
||||
"publisher TEXT," +
|
||||
"count_of_issues INT," +
|
||||
"start_year INT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id))")
|
||||
|
||||
cur.execute(
|
||||
"CREATE TABLE AltCovers("
|
||||
+ "issue_id INT,"
|
||||
+ "url_list TEXT,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "PRIMARY KEY (issue_id))"
|
||||
)
|
||||
"CREATE TABLE AltCovers(" +
|
||||
"issue_id INT," +
|
||||
"url_list TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (issue_id))")
|
||||
|
||||
cur.execute(
|
||||
"CREATE TABLE Issues("
|
||||
+ "id INT,"
|
||||
+ "volume_id INT,"
|
||||
+ "name TEXT,"
|
||||
+ "issue_number TEXT,"
|
||||
+ "super_url TEXT,"
|
||||
+ "thumb_url TEXT,"
|
||||
+ "cover_date TEXT,"
|
||||
+ "site_detail_url TEXT,"
|
||||
+ "description TEXT,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "PRIMARY KEY (id))"
|
||||
)
|
||||
"CREATE TABLE Issues(" +
|
||||
"id INT," +
|
||||
"volume_id INT," +
|
||||
"name TEXT," +
|
||||
"issue_number TEXT," +
|
||||
"super_url TEXT," +
|
||||
"thumb_url TEXT," +
|
||||
"cover_date TEXT," +
|
||||
"site_detail_url TEXT," +
|
||||
"description TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id))")
|
||||
|
||||
def add_search_results(self, search_term, cv_search_results):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM VolumeSearchCache WHERE search_term = ?", [search_term.lower()])
|
||||
cur.execute(
|
||||
"DELETE FROM VolumeSearchCache WHERE search_term = ?", [
|
||||
search_term.lower()])
|
||||
|
||||
# now add in new results
|
||||
for record in cv_search_results:
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
if record["publisher"] is None:
|
||||
if record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = record["publisher"]["name"]
|
||||
pub_name = record['publisher']['name']
|
||||
|
||||
if record["image"] is None:
|
||||
if record['image'] is None:
|
||||
url = ""
|
||||
else:
|
||||
url = record["image"]["super_url"]
|
||||
url = record['image']['super_url']
|
||||
|
||||
cur.execute(
|
||||
"INSERT INTO VolumeSearchCache "
|
||||
+ "(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) "
|
||||
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
search_term.lower(),
|
||||
record["id"],
|
||||
record["name"],
|
||||
record["start_year"],
|
||||
"INSERT INTO VolumeSearchCache " +
|
||||
"(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) " +
|
||||
"VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(search_term.lower(),
|
||||
record['id'],
|
||||
record['name'],
|
||||
record['start_year'],
|
||||
pub_name,
|
||||
record["count_of_issues"],
|
||||
record['count_of_issues'],
|
||||
url,
|
||||
record["description"],
|
||||
),
|
||||
)
|
||||
record['description']))
|
||||
|
||||
def get_search_results(self, search_term):
|
||||
|
||||
results = []
|
||||
results = list()
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# purge stale search results
|
||||
a_day_ago = datetime.datetime.today() - datetime.timedelta(days=1)
|
||||
cur.execute("DELETE FROM VolumeSearchCache WHERE timestamp < ?", [str(a_day_ago)])
|
||||
cur.execute(
|
||||
"DELETE FROM VolumeSearchCache WHERE timestamp < ?", [
|
||||
str(a_day_ago)])
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
|
||||
cur.execute(
|
||||
"SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
|
||||
rows = cur.fetchall()
|
||||
# now process the results
|
||||
for record in rows:
|
||||
result = {
|
||||
"id": record[1],
|
||||
"name": record[2],
|
||||
"start_year": record[3],
|
||||
"count_of_issues": record[5],
|
||||
"description": record[7],
|
||||
"publisher": {"name": record[4]},
|
||||
"image": {"super_url": record[6]},
|
||||
}
|
||||
|
||||
result = dict()
|
||||
result['id'] = record[1]
|
||||
result['name'] = record[2]
|
||||
result['start_year'] = record[3]
|
||||
result['publisher'] = dict()
|
||||
result['publisher']['name'] = record[4]
|
||||
result['count_of_issues'] = record[5]
|
||||
result['image'] = dict()
|
||||
result['image']['super_url'] = record[6]
|
||||
result['description'] = record[7]
|
||||
|
||||
results.append(result)
|
||||
|
||||
@@ -194,41 +197,50 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [issue_id])
|
||||
|
||||
url_list_str = utils.list_to_string(url_list)
|
||||
url_list_str = utils.listToString(url_list)
|
||||
# now add in new record
|
||||
cur.execute("INSERT INTO AltCovers (issue_id, url_list) VALUES(?, ?)", (issue_id, url_list_str))
|
||||
cur.execute("INSERT INTO AltCovers " +
|
||||
"(issue_id, url_list) " +
|
||||
"VALUES(?, ?)",
|
||||
(issue_id,
|
||||
url_list_str)
|
||||
)
|
||||
|
||||
def get_alt_covers(self, issue_id):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change
|
||||
# much....
|
||||
a_month_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||
cur.execute("DELETE FROM AltCovers WHERE timestamp < ?", [str(a_month_ago)])
|
||||
a_month_ago = datetime.datetime.today() - \
|
||||
datetime.timedelta(days=30)
|
||||
cur.execute(
|
||||
"DELETE FROM AltCovers WHERE timestamp < ?", [
|
||||
str(a_month_ago)])
|
||||
|
||||
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
|
||||
cur.execute(
|
||||
"SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
url_list_str = row[0]
|
||||
if len(url_list_str) == 0:
|
||||
return []
|
||||
raw_list = url_list_str.split(",")
|
||||
url_list = []
|
||||
for item in raw_list:
|
||||
url_list.append(str(item).strip())
|
||||
return url_list
|
||||
else:
|
||||
url_list_str = row[0]
|
||||
if len(url_list_str) == 0:
|
||||
return []
|
||||
raw_list = url_list_str.split(",")
|
||||
url_list = []
|
||||
for item in raw_list:
|
||||
url_list.append(str(item).strip())
|
||||
return url_list
|
||||
|
||||
def add_volume_info(self, cv_volume_record):
|
||||
|
||||
@@ -240,25 +252,26 @@ class ComicVineCacher:
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
if cv_volume_record["publisher"] is None:
|
||||
if cv_volume_record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = cv_volume_record["publisher"]["name"]
|
||||
pub_name = cv_volume_record['publisher']['name']
|
||||
|
||||
data = {
|
||||
"name": cv_volume_record["name"],
|
||||
"name": cv_volume_record['name'],
|
||||
"publisher": pub_name,
|
||||
"count_of_issues": cv_volume_record["count_of_issues"],
|
||||
"start_year": cv_volume_record["start_year"],
|
||||
"timestamp": timestamp,
|
||||
"count_of_issues": cv_volume_record['count_of_issues'],
|
||||
"start_year": cv_volume_record['start_year'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert(cur, "volumes", "id", cv_volume_record["id"], data)
|
||||
self.upsert(cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
|
||||
def add_volume_issues_info(self, volume_id, cv_volume_issues):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
@@ -266,18 +279,19 @@ class ComicVineCacher:
|
||||
# add in issues
|
||||
|
||||
for issue in cv_volume_issues:
|
||||
|
||||
data = {
|
||||
"volume_id": volume_id,
|
||||
"name": issue["name"],
|
||||
"issue_number": issue["issue_number"],
|
||||
"site_detail_url": issue["site_detail_url"],
|
||||
"cover_date": issue["cover_date"],
|
||||
"super_url": issue["image"]["super_url"],
|
||||
"thumb_url": issue["image"]["thumb_url"],
|
||||
"description": issue["description"],
|
||||
"timestamp": timestamp,
|
||||
"name": issue['name'],
|
||||
"issue_number": issue['issue_number'],
|
||||
"site_detail_url": issue['site_detail_url'],
|
||||
"cover_date": issue['cover_date'],
|
||||
"super_url": issue['image']['super_url'],
|
||||
"thumb_url": issue['image']['thumb_url'],
|
||||
"description": issue['description'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert(cur, "issues", "id", issue["id"], data)
|
||||
self.upsert(cur, "issues", "id", issue['id'], data)
|
||||
|
||||
def get_volume_info(self, volume_id):
|
||||
|
||||
@@ -286,64 +300,72 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale volume info
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
|
||||
cur.execute(
|
||||
"DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [volume_id])
|
||||
cur.execute(
|
||||
"SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?",
|
||||
[volume_id])
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None:
|
||||
return result
|
||||
|
||||
result = dict()
|
||||
|
||||
# since ID is primary key, there is only one row
|
||||
result = {
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"count_of_issues": row[3],
|
||||
"start_year": row[4],
|
||||
"issues": [],
|
||||
"publisher": {"name": row[2]},
|
||||
}
|
||||
result['id'] = row[0]
|
||||
result['name'] = row[1]
|
||||
result['publisher'] = dict()
|
||||
result['publisher']['name'] = row[2]
|
||||
result['count_of_issues'] = row[3]
|
||||
result['start_year'] = row[4]
|
||||
result['issues'] = list()
|
||||
|
||||
return result
|
||||
|
||||
def get_volume_issues_info(self, volume_id):
|
||||
|
||||
result = None
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change
|
||||
# much....
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
cur.execute(
|
||||
"DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
results = []
|
||||
results = list()
|
||||
|
||||
cur.execute(
|
||||
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?",
|
||||
[volume_id],
|
||||
)
|
||||
[volume_id])
|
||||
rows = cur.fetchall()
|
||||
|
||||
# now process the results
|
||||
for row in rows:
|
||||
record = {
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"issue_number": row[2],
|
||||
"site_detail_url": row[3],
|
||||
"cover_date": row[4],
|
||||
"image": {"super_url": row[5], "thumb_url": row[6]},
|
||||
"description": row[7],
|
||||
}
|
||||
record = dict()
|
||||
|
||||
record['id'] = row[0]
|
||||
record['name'] = row[1]
|
||||
record['issue_number'] = row[2]
|
||||
record['site_detail_url'] = row[3]
|
||||
record['cover_date'] = row[4]
|
||||
record['image'] = dict()
|
||||
record['image']['super_url'] = row[5]
|
||||
record['image']['thumb_url'] = row[6]
|
||||
record['description'] = row[7]
|
||||
|
||||
results.append(record)
|
||||
|
||||
@@ -352,13 +374,19 @@ class ComicVineCacher:
|
||||
|
||||
return results
|
||||
|
||||
def add_issue_select_details(self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url):
|
||||
def add_issue_select_details(
|
||||
self,
|
||||
issue_id,
|
||||
image_url,
|
||||
thumb_image_url,
|
||||
cover_date,
|
||||
site_detail_url):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
@@ -366,7 +394,7 @@ class ComicVineCacher:
|
||||
"thumb_url": thumb_image_url,
|
||||
"cover_date": cover_date,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert(cur, "issues", "id", issue_id, data)
|
||||
|
||||
@@ -375,23 +403,25 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [issue_id])
|
||||
cur.execute(
|
||||
"SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?",
|
||||
[issue_id])
|
||||
row = cur.fetchone()
|
||||
|
||||
details = {}
|
||||
details = dict()
|
||||
if row is None or row[0] is None:
|
||||
details["image_url"] = None
|
||||
details["thumb_image_url"] = None
|
||||
details["cover_date"] = None
|
||||
details["site_detail_url"] = None
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['cover_date'] = None
|
||||
details['site_detail_url'] = None
|
||||
|
||||
else:
|
||||
details["image_url"] = row[0]
|
||||
details["thumb_image_url"] = row[1]
|
||||
details["cover_date"] = row[2]
|
||||
details["site_detail_url"] = row[3]
|
||||
details['image_url'] = row[0]
|
||||
details['thumb_image_url'] = row[1]
|
||||
details['cover_date'] = row[2]
|
||||
details['site_detail_url'] = row[3]
|
||||
|
||||
return details
|
||||
|
||||
@@ -403,8 +433,10 @@ class ComicVineCacher:
|
||||
TODO: should the cursor be created here, and not up the stack?
|
||||
"""
|
||||
|
||||
ins_count = len(data) + 1
|
||||
|
||||
keys = ""
|
||||
vals = []
|
||||
vals = list()
|
||||
ins_slots = ""
|
||||
set_slots = ""
|
||||
|
||||
@@ -427,8 +459,11 @@ class ComicVineCacher:
|
||||
ins_slots += ", ?"
|
||||
condition = pkname + " = ?"
|
||||
|
||||
sql_ins = f"INSERT OR IGNORE INTO {tablename} ({keys}) VALUES ({ins_slots})"
|
||||
sql_ins = ("INSERT OR IGNORE INTO " + tablename +
|
||||
" (" + keys + ") " +
|
||||
" VALUES (" + ins_slots + ")")
|
||||
cur.execute(sql_ins, vals)
|
||||
|
||||
sql_upd = f"UPDATE {tablename} SET {set_slots} WHERE {condition}"
|
||||
sql_upd = ("UPDATE " + tablename +
|
||||
" SET " + set_slots + " WHERE " + condition)
|
||||
cur.execute(sql_upd, vals)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 widget to display cover images
|
||||
"""A PyQt4 widget to display cover images
|
||||
|
||||
Display cover images from either a local archive, or from Comic Vine.
|
||||
TODO: This should be re-factored using subclasses!
|
||||
@@ -18,113 +18,81 @@ TODO: This should be re-factored using subclasses!
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker
|
||||
from comictaggerlib.imagefetcher import ImageFetcher
|
||||
from comictaggerlib.imagepopup import ImagePopup
|
||||
from comictaggerlib.pageloader import PageLoader
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import get_qimage_from_data, reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from pageloader import PageLoader
|
||||
from imagepopup import ImagePopup
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, getQImageFromData
|
||||
#from genericmetadata import GenericMetadata, PageType
|
||||
#from comicarchive import MetaDataStyle
|
||||
#import utils
|
||||
|
||||
|
||||
def clickable(widget):
|
||||
"""Allow a label to be clickable"""
|
||||
"""# Allow a label to be clickable"""
|
||||
|
||||
class Filter(QtCore.QObject):
|
||||
class Filter(QObject):
|
||||
|
||||
dblclicked = QtCore.pyqtSignal()
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QtCore.QEvent.Type.MouseButtonDblClick:
|
||||
if event.type() == QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
flt = Filter(widget)
|
||||
widget.installEventFilter(flt)
|
||||
return flt.dblclicked
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
|
||||
|
||||
class Signal(QtCore.QObject):
|
||||
alt_url_list_fetch_complete = QtCore.pyqtSignal(list)
|
||||
url_fetch_complete = QtCore.pyqtSignal(str, str)
|
||||
image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray)
|
||||
class CoverImageWidget(QWidget):
|
||||
|
||||
def __init__(self, list_fetch, url_fetch, image_fetch):
|
||||
super().__init__()
|
||||
self.alt_url_list_fetch_complete.connect(list_fetch)
|
||||
self.url_fetch_complete.connect(url_fetch)
|
||||
self.image_fetch_complete.connect(image_fetch)
|
||||
|
||||
def emit_list(self, url_list: list):
|
||||
self.alt_url_list_fetch_complete.emit(url_list)
|
||||
|
||||
def emit_url(self, image_url: str, thumb_url: str):
|
||||
self.url_fetch_complete.emit(image_url, thumb_url)
|
||||
|
||||
def emit_image(self, image_data: QtCore.QByteArray):
|
||||
self.image_fetch_complete.emit(image_data)
|
||||
|
||||
|
||||
class CoverImageWidget(QtWidgets.QWidget):
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
DataMode = 3
|
||||
|
||||
def __init__(self, parent, mode, expand_on_click=True):
|
||||
super().__init__(parent)
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("coverimagewidget.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self)
|
||||
|
||||
reduce_widget_font_size(self.label)
|
||||
|
||||
self.sig = Signal(
|
||||
self.alt_cover_url_list_fetch_complete, self.primary_url_fetch_complete, self.cover_remote_fetch_complete
|
||||
)
|
||||
reduceWidgetFontSize(self.label)
|
||||
|
||||
self.mode = mode
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.page_loader = None
|
||||
self.showControls = True
|
||||
|
||||
self.current_pixmap = QtGui.QPixmap()
|
||||
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
|
||||
self.btnRight.setIcon(
|
||||
QIcon(ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
self.comic_archive = None
|
||||
self.issue_id = None
|
||||
self.cover_fetcher = None
|
||||
self.url_list = []
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = None
|
||||
self.imageIndex = -1
|
||||
self.imageCount = 1
|
||||
self.imageData = None
|
||||
|
||||
self.btnLeft.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("left.png")))
|
||||
self.btnRight.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("right.png")))
|
||||
|
||||
self.btnLeft.clicked.connect(self.decrement_image)
|
||||
self.btnRight.clicked.connect(self.increment_image)
|
||||
self.btnLeft.clicked.connect(self.decrementImage)
|
||||
self.btnRight.clicked.connect(self.incrementImage)
|
||||
self.resetWidget()
|
||||
if expand_on_click:
|
||||
clickable(self.lblImage).connect(self.show_popup)
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
else:
|
||||
self.lblImage.setToolTip("")
|
||||
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
def reset_widget(self):
|
||||
def resetWidget(self):
|
||||
self.comic_archive = None
|
||||
self.issue_id = None
|
||||
self.comicVine = None
|
||||
self.cover_fetcher = None
|
||||
self.url_list = []
|
||||
if self.page_loader is not None:
|
||||
@@ -135,52 +103,54 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
self.imageData = None
|
||||
|
||||
def clear(self):
|
||||
self.reset_widget()
|
||||
self.update_content()
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
def increment_image(self):
|
||||
def incrementImage(self):
|
||||
self.imageIndex += 1
|
||||
if self.imageIndex == self.imageCount:
|
||||
self.imageIndex = 0
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
def decrement_image(self):
|
||||
def decrementImage(self):
|
||||
self.imageIndex -= 1
|
||||
if self.imageIndex == -1:
|
||||
self.imageIndex = self.imageCount - 1
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
def set_archive(self, ca: ComicArchive, page=0):
|
||||
def setArchive(self, ca, page=0):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.reset_widget()
|
||||
self.resetWidget()
|
||||
self.comic_archive = ca
|
||||
self.imageIndex = page
|
||||
self.imageCount = ca.get_number_of_pages()
|
||||
self.update_content()
|
||||
self.imageCount = ca.getNumberOfPages()
|
||||
self.updateContent()
|
||||
|
||||
def set_url(self, url):
|
||||
def setURL(self, url):
|
||||
if self.mode == CoverImageWidget.URLMode:
|
||||
self.reset_widget()
|
||||
self.update_content()
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.url_list = [url]
|
||||
self.imageIndex = 0
|
||||
self.imageCount = 1
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
def set_issue_id(self, issue_id):
|
||||
def setIssueID(self, issue_id):
|
||||
if self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.reset_widget()
|
||||
self.update_content()
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.issue_id = issue_id
|
||||
|
||||
comic_vine = ComicVineTalker()
|
||||
comic_vine.url_fetch_complete = self.sig.emit_url
|
||||
comic_vine.async_fetch_issue_cover_urls(int(self.issue_id))
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect(
|
||||
self.primaryUrlFetchComplete)
|
||||
self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id))
|
||||
|
||||
def set_image_data(self, image_data):
|
||||
def setImageData(self, image_data):
|
||||
if self.mode == CoverImageWidget.DataMode:
|
||||
self.reset_widget()
|
||||
self.resetWidget()
|
||||
|
||||
if image_data is None:
|
||||
self.imageIndex = -1
|
||||
@@ -188,54 +158,56 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
self.imageIndex = 0
|
||||
self.imageData = image_data
|
||||
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
def primary_url_fetch_complete(self, primary_url, thumb_url):
|
||||
def primaryUrlFetchComplete(self, primary_url, thumb_url, issue_id):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
self.imageCount = len(self.url_list)
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
# defer the alt cover search
|
||||
QtCore.QTimer.singleShot(1, self.start_alt_cover_search)
|
||||
QTimer.singleShot(1, self.startAltCoverSearch)
|
||||
|
||||
def start_alt_cover_search(self):
|
||||
def startAltCoverSearch(self):
|
||||
|
||||
# now we need to get the list of alt cover URLs
|
||||
self.label.setText("Searching for alt. covers...")
|
||||
|
||||
# page URL should already be cached, so no need to defer
|
||||
comic_vine = ComicVineTalker()
|
||||
issue_page_url = comic_vine.fetch_issue_page_url(self.issue_id)
|
||||
comic_vine.alt_url_list_fetch_complete = self.sig.emit_list
|
||||
comic_vine.async_fetch_alternate_cover_urls(int(self.issue_id), issue_page_url)
|
||||
self.comicVine = ComicVineTalker()
|
||||
issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id)
|
||||
self.comicVine.altUrlListFetchComplete.connect(
|
||||
self.altCoverUrlListFetchComplete)
|
||||
self.comicVine.asyncFetchAlternateCoverURLs(
|
||||
int(self.issue_id), issue_page_url)
|
||||
|
||||
def alt_cover_url_list_fetch_complete(self, url_list):
|
||||
def altCoverUrlListFetchComplete(self, url_list, issue_id):
|
||||
if len(url_list) > 0:
|
||||
self.url_list.extend(url_list)
|
||||
self.imageCount = len(self.url_list)
|
||||
self.update_controls()
|
||||
self.updateControls()
|
||||
|
||||
def set_page(self, pagenum):
|
||||
def setPage(self, pagenum):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.imageIndex = pagenum
|
||||
self.update_content()
|
||||
self.updateContent()
|
||||
|
||||
def update_content(self):
|
||||
self.update_image()
|
||||
self.update_controls()
|
||||
def updateContent(self):
|
||||
self.updateImage()
|
||||
self.updateControls()
|
||||
|
||||
def update_image(self):
|
||||
def updateImage(self):
|
||||
if self.imageIndex == -1:
|
||||
self.load_default()
|
||||
self.loadDefault()
|
||||
elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]:
|
||||
self.load_url()
|
||||
self.loadURL()
|
||||
elif self.mode == CoverImageWidget.DataMode:
|
||||
self.cover_remote_fetch_complete(self.imageData)
|
||||
self.coverRemoteFetchComplete(self.imageData, 0)
|
||||
else:
|
||||
self.load_page()
|
||||
self.loadPage()
|
||||
|
||||
def update_controls(self):
|
||||
def updateControls(self):
|
||||
if not self.showControls or self.mode == CoverImageWidget.DataMode:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
@@ -256,47 +228,71 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.label.setText("")
|
||||
elif self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.label.setText(f"Cover {self.imageIndex + 1} (of {self.imageCount})")
|
||||
self.label.setText(
|
||||
"Cover {0} (of {1})".format(
|
||||
self.imageIndex + 1,
|
||||
self.imageCount))
|
||||
else:
|
||||
self.label.setText(f"Page {self.imageIndex + 1} (of {self.imageCount})")
|
||||
self.label.setText(
|
||||
"Page {0} (of {1})".format(
|
||||
self.imageIndex + 1,
|
||||
self.imageCount))
|
||||
|
||||
def load_url(self):
|
||||
self.load_default()
|
||||
def loadURL(self):
|
||||
self.loadDefault()
|
||||
self.cover_fetcher = ImageFetcher()
|
||||
self.cover_fetcher.image_fetch_complete = self.sig.emit_image
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete)
|
||||
self.cover_fetcher.fetch(self.url_list[self.imageIndex])
|
||||
#print("ATB cover fetch started...")
|
||||
|
||||
# called when the image is done loading from internet
|
||||
def cover_remote_fetch_complete(self, image_data):
|
||||
img = get_qimage_from_data(image_data)
|
||||
self.current_pixmap = QtGui.QPixmap.fromImage(img)
|
||||
self.set_display_pixmap()
|
||||
def coverRemoteFetchComplete(self, image_data, issue_id):
|
||||
img = getQImageFromData(image_data)
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap(0, 0)
|
||||
#print("ATB cover fetch complete!")
|
||||
|
||||
def load_page(self):
|
||||
def loadPage(self):
|
||||
if self.comic_archive is not None:
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
|
||||
self.page_loader.loadComplete.connect(self.page_load_complete)
|
||||
self.page_loader.loadComplete.connect(self.pageLoadComplete)
|
||||
self.page_loader.start()
|
||||
|
||||
def page_load_complete(self, image_data):
|
||||
img = get_qimage_from_data(image_data)
|
||||
self.current_pixmap = QtGui.QPixmap.fromImage(img)
|
||||
self.set_display_pixmap()
|
||||
def pageLoadComplete(self, img):
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap(0, 0)
|
||||
self.page_loader = None
|
||||
|
||||
def load_default(self):
|
||||
self.current_pixmap = QtGui.QPixmap(ComicTaggerSettings.get_graphic("nocover.png"))
|
||||
self.set_display_pixmap()
|
||||
def loadDefault(self):
|
||||
self.current_pixmap = QPixmap(
|
||||
ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
#print("loadDefault called")
|
||||
self.setDisplayPixmap(0, 0)
|
||||
|
||||
def resizeEvent(self, resize_event):
|
||||
if self.current_pixmap is not None:
|
||||
self.set_display_pixmap()
|
||||
delta_w = resize_event.size().width() - \
|
||||
resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - \
|
||||
resize_event.oldSize().height()
|
||||
# print "ATB resizeEvent deltas", resize_event.size().width(),
|
||||
# resize_event.size().height()
|
||||
self.setDisplayPixmap(delta_w, delta_h)
|
||||
|
||||
def set_display_pixmap(self):
|
||||
def setDisplayPixmap(self, delta_w, delta_h):
|
||||
"""The deltas let us know what the new width and height of the label will be"""
|
||||
|
||||
#new_h = self.frame.height() + delta_h
|
||||
#new_w = self.frame.width() + delta_w
|
||||
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
# print "ATB self.frame", self.frame.width(), self.frame.height()
|
||||
# print "ATB self.", self.width(), self.height()
|
||||
|
||||
#frame_w = new_w
|
||||
#frame_h = new_h
|
||||
|
||||
new_h = self.frame.height()
|
||||
new_w = self.frame.width()
|
||||
frame_w = self.frame.width()
|
||||
@@ -305,18 +301,25 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
new_h = max(new_h, 0)
|
||||
new_w = max(new_w, 0)
|
||||
if new_h < 0:
|
||||
new_h = 0
|
||||
if new_w < 0:
|
||||
new_w = 0
|
||||
|
||||
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
# print "ATB self.frame", frame_w, frame_h
|
||||
# print "ATB new size", new_w, new_h
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
|
||||
scaled_pixmap = self.current_pixmap.scaled(
|
||||
new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap(scaled_pixmap)
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move(int((frame_w - img_w) / 2), int((frame_h - img_h) / 2))
|
||||
self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2)
|
||||
|
||||
def show_popup(self):
|
||||
ImagePopup(self, self.current_pixmap)
|
||||
def showPopup(self):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
|
||||
@@ -14,24 +14,23 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class CreditEditorWindow(QtWidgets.QDialog):
|
||||
class CreditEditorWindow(QtGui.QDialog):
|
||||
|
||||
ModeEdit = 0
|
||||
ModeNew = 1
|
||||
|
||||
def __init__(self, parent, mode, role, name, primary):
|
||||
super().__init__(parent)
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("crediteditorwindow.ui"), self)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('crediteditorwindow.ui'), self)
|
||||
|
||||
self.mode = mode
|
||||
|
||||
@@ -64,33 +63,34 @@ class CreditEditorWindow(QtWidgets.QDialog):
|
||||
self.cbRole.setCurrentIndex(i)
|
||||
|
||||
if primary:
|
||||
self.cbPrimary.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbPrimary.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
self.cbRole.currentIndexChanged.connect(self.role_changed)
|
||||
self.cbRole.editTextChanged.connect(self.role_changed)
|
||||
self.cbRole.currentIndexChanged.connect(self.roleChanged)
|
||||
self.cbRole.editTextChanged.connect(self.roleChanged)
|
||||
|
||||
self.update_primary_button()
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def update_primary_button(self):
|
||||
enabled = self.current_role_can_be_primary()
|
||||
def updatePrimaryButton(self):
|
||||
enabled = self.currentRoleCanBePrimary()
|
||||
self.cbPrimary.setEnabled(enabled)
|
||||
|
||||
def current_role_can_be_primary(self):
|
||||
def currentRoleCanBePrimary(self):
|
||||
role = self.cbRole.currentText()
|
||||
if str(role).lower() == "writer" or str(role).lower() == "artist":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return False
|
||||
def roleChanged(self, s):
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def role_changed(self, s):
|
||||
self.update_primary_button()
|
||||
|
||||
def get_credits(self):
|
||||
primary = self.current_role_can_be_primary() and self.cbPrimary.isChecked()
|
||||
def getCredits(self):
|
||||
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
|
||||
return self.cbRole.currentText(), self.leName.text(), primary
|
||||
|
||||
def accept(self):
|
||||
if self.cbRole.currentText() == "" or self.leName.text() == "":
|
||||
QtWidgets.QMessageBox.warning(self, "Whoops", "You need to enter both role and name for a credit.")
|
||||
QtGui.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
|
||||
"You need to enter both role and name for a credit."))
|
||||
else:
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
3
comictaggerlib/ctversion.py
Normal file
3
comictaggerlib/ctversion.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# This file should contain only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version = "1.1.16-beta-rc2"
|
||||
@@ -14,14 +14,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
#from settingswindow import SettingsWindow
|
||||
#from filerenamer import FileRenamer
|
||||
#import utils
|
||||
|
||||
|
||||
class ExportConflictOpts:
|
||||
@@ -30,21 +30,21 @@ class ExportConflictOpts:
|
||||
createUnique = 3
|
||||
|
||||
|
||||
class ExportWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent, settings, msg):
|
||||
super().__init__(parent)
|
||||
class ExportWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("exportwindow.ui"), self)
|
||||
def __init__(self, parent, settings, msg):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui'), self)
|
||||
self.label.setText(msg)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.cbxAddToList.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxDeleteOriginal.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxAddToList.setCheckState(QtCore.Qt.Checked)
|
||||
self.radioDontCreate.setChecked(True)
|
||||
|
||||
self.deleteOriginal = False
|
||||
@@ -52,7 +52,7 @@ class ExportWindow(QtWidgets.QDialog):
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
|
||||
def accept(self):
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
|
||||
self.addToList = self.cbxAddToList.isChecked()
|
||||
@@ -60,3 +60,5 @@ class ExportWindow(QtWidgets.QDialog):
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
elif self.radioCreateNew.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.createUnique
|
||||
# else:
|
||||
# self.fileConflictBehavior = ExportConflictOpts.overwrite
|
||||
|
||||
1
comictaggerlib/filenameparser.py
Normal file
1
comictaggerlib/filenameparser.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.filenameparser import *
|
||||
@@ -14,166 +14,143 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import calendar
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import string
|
||||
import sys
|
||||
import re
|
||||
import datetime
|
||||
|
||||
from pathvalidate import sanitize_filename
|
||||
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetadataFormatter(string.Formatter):
|
||||
def __init__(self, smart_cleanup=False, platform="auto"):
|
||||
super().__init__()
|
||||
self.smart_cleanup = smart_cleanup
|
||||
self.platform = platform
|
||||
|
||||
def format_field(self, value, format_spec):
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
return super().format_field(value, format_spec)
|
||||
|
||||
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0):
|
||||
if recursion_depth < 0:
|
||||
raise ValueError("Max string recursion exceeded")
|
||||
result = []
|
||||
lstrip = False
|
||||
for literal_text, field_name, format_spec, conversion in self.parse(format_string):
|
||||
|
||||
# output the literal text
|
||||
if literal_text:
|
||||
if lstrip:
|
||||
literal_text = literal_text.lstrip("-_)}]#")
|
||||
if self.smart_cleanup:
|
||||
lspace = literal_text[0].isspace() if literal_text else False
|
||||
rspace = literal_text[-1].isspace() if literal_text else False
|
||||
literal_text = " ".join(literal_text.split())
|
||||
if literal_text == "":
|
||||
literal_text = " "
|
||||
else:
|
||||
if lspace:
|
||||
literal_text = " " + literal_text
|
||||
if rspace:
|
||||
literal_text += " "
|
||||
result.append(literal_text)
|
||||
|
||||
lstrip = False
|
||||
# if there's a field, output it
|
||||
if field_name is not None and field_name != "":
|
||||
field_name = field_name.lower()
|
||||
# this is some markup, find the object and do the formatting
|
||||
|
||||
# handle arg indexing when empty field_names are given.
|
||||
if field_name == "":
|
||||
if auto_arg_index is False:
|
||||
raise ValueError("cannot switch from manual field specification to automatic field numbering")
|
||||
field_name = str(auto_arg_index)
|
||||
auto_arg_index += 1
|
||||
elif field_name.isdigit():
|
||||
if auto_arg_index:
|
||||
raise ValueError("cannot switch from manual field specification to automatic field numbering")
|
||||
# disable auto arg incrementing, if it gets used later on, then an exception will be raised
|
||||
auto_arg_index = False
|
||||
|
||||
# given the field_name, find the object it references
|
||||
# and the argument it came from
|
||||
obj, arg_used = self.get_field(field_name, args, kwargs)
|
||||
used_args.add(arg_used)
|
||||
|
||||
# do any conversion on the resulting object
|
||||
obj = self.convert_field(obj, conversion)
|
||||
|
||||
# expand the format spec, if needed
|
||||
format_spec, auto_arg_index = self._vformat(
|
||||
format_spec, args, kwargs, used_args, recursion_depth - 1, auto_arg_index=auto_arg_index
|
||||
)
|
||||
|
||||
# format the object and append to the result
|
||||
fmt_obj = self.format_field(obj, format_spec)
|
||||
if fmt_obj == "" and len(result) > 0 and self.smart_cleanup:
|
||||
lstrip = True
|
||||
if result:
|
||||
result[-1] = result[-1].rstrip("-_({[#")
|
||||
if self.smart_cleanup:
|
||||
fmt_obj = " ".join(fmt_obj.split())
|
||||
fmt_obj = sanitize_filename(fmt_obj, platform=self.platform)
|
||||
result.append(fmt_obj)
|
||||
|
||||
return "".join(result), auto_arg_index
|
||||
import utils
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class FileRenamer:
|
||||
def __init__(self, metadata, platform="auto"):
|
||||
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
|
||||
|
||||
def __init__(self, metadata):
|
||||
self.setMetadata(metadata)
|
||||
self.setTemplate(
|
||||
"%series% v%volume% #%issue% (of %issuecount%) (%year%)")
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
self.metadata = metadata
|
||||
self.move = False
|
||||
self.platform = platform
|
||||
|
||||
def set_metadata(self, metadata: GenericMetadata):
|
||||
self.metadata = metadata
|
||||
def setMetadata(self, metadata):
|
||||
self.metdata = metadata
|
||||
|
||||
def set_issue_zero_padding(self, count):
|
||||
def setIssueZeroPadding(self, count):
|
||||
self.issue_zero_padding = count
|
||||
|
||||
def set_smart_cleanup(self, on):
|
||||
def setSmartCleanup(self, on):
|
||||
self.smart_cleanup = on
|
||||
|
||||
def set_template(self, template: str):
|
||||
def setTemplate(self, template):
|
||||
self.template = template
|
||||
|
||||
def determine_name(self, ext):
|
||||
class Default(dict):
|
||||
def __missing__(self, key):
|
||||
return "{" + key + "}"
|
||||
def replaceToken(self, text, value, token):
|
||||
# helper func
|
||||
def isToken(word):
|
||||
return (word[0] == "%" and word[-1:] == "%")
|
||||
|
||||
md = self.metadata
|
||||
|
||||
# padding for issue
|
||||
md.issue = IssueString(md.issue).as_string(pad=self.issue_zero_padding)
|
||||
|
||||
template = self.template
|
||||
|
||||
new_name = ""
|
||||
|
||||
fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform)
|
||||
md_dict = vars(md)
|
||||
for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
|
||||
md_dict[role] = md.get_primary_credit(role)
|
||||
|
||||
if (isinstance(md.month, int) or isinstance(md.month, str) and md.month.isdigit()) and 0 < int(md.month) < 13:
|
||||
md_dict["month_name"] = calendar.month_name[int(md.month)]
|
||||
md_dict["month_abbr"] = calendar.month_abbr[int(md.month)]
|
||||
if value is not None:
|
||||
return text.replace(token, unicode(value))
|
||||
else:
|
||||
md_dict["month_name"] = ""
|
||||
md_dict["month_abbr"] = ""
|
||||
if self.smart_cleanup:
|
||||
# smart cleanup means we want to remove anything appended to token if it's empty
|
||||
# (e.g "#%issue%" or "v%volume%")
|
||||
# (TODO: This could fail if there is more than one token appended together, I guess)
|
||||
text_list = text.split()
|
||||
|
||||
for Component in pathlib.PureWindowsPath(template).parts:
|
||||
if (
|
||||
self.platform.lower() in ["universal", "windows"] or sys.platform.lower() in ["windows"]
|
||||
) and self.smart_cleanup:
|
||||
# colons get special treatment
|
||||
Component = Component.replace(": ", " - ")
|
||||
Component = Component.replace(":", "-")
|
||||
# special case for issuecount, remove preceding non-token word,
|
||||
# as in "...(of %issuecount%)..."
|
||||
if token == '%issuecount%':
|
||||
for idx, word in enumerate(text_list):
|
||||
if token in word and not isToken(text_list[idx - 1]):
|
||||
text_list[idx - 1] = ""
|
||||
|
||||
new_basename = sanitize_filename(
|
||||
fmt.vformat(Component, args=None, kwargs=Default(md_dict)), platform=self.platform
|
||||
).strip()
|
||||
new_name = os.path.join(new_name, new_basename)
|
||||
text_list = [x for x in text_list if token not in x]
|
||||
return " ".join(text_list)
|
||||
else:
|
||||
return text.replace(token, "")
|
||||
|
||||
def determineName(self, filename, ext=None):
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
preferred_encoding = utils.get_actual_preferred_encoding()
|
||||
|
||||
# print(u"{0}".format(md))
|
||||
|
||||
new_name = self.replaceToken(new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken(new_name, md.volume, '%volume%')
|
||||
|
||||
if md.issue is not None:
|
||||
issue_str = u"{0}".format(
|
||||
IssueString(md.issue).asString(pad=self.issue_zero_padding))
|
||||
else:
|
||||
issue_str = None
|
||||
new_name = self.replaceToken(new_name, issue_str, '%issue%')
|
||||
|
||||
new_name = self.replaceToken(new_name, md.issueCount, '%issuecount%')
|
||||
new_name = self.replaceToken(new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken(new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken(new_name, md.title, '%title%')
|
||||
new_name = self.replaceToken(new_name, md.month, '%month%')
|
||||
month_name = None
|
||||
if md.month is not None:
|
||||
if (isinstance(md.month, str) and md.month.isdigit()) or isinstance(
|
||||
md.month, int):
|
||||
if int(md.month) in range(1, 13):
|
||||
dt = datetime.datetime(1970, int(md.month), 1, 0, 0)
|
||||
month_name = dt.strftime(
|
||||
u"%B".encode(preferred_encoding)).decode(preferred_encoding)
|
||||
new_name = self.replaceToken(new_name, month_name, '%month_name%')
|
||||
|
||||
new_name = self.replaceToken(new_name, md.genre, '%genre%')
|
||||
new_name = self.replaceToken(new_name, md.language, '%language_code%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.criticalRating, '%criticalrating%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateSeries, '%alternateseries%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateNumber, '%alternatenumber%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateCount, '%alternatecount%')
|
||||
new_name = self.replaceToken(new_name, md.imprint, '%imprint%')
|
||||
new_name = self.replaceToken(new_name, md.format, '%format%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.maturityRating, '%maturityrating%')
|
||||
new_name = self.replaceToken(new_name, md.storyArc, '%storyarc%')
|
||||
new_name = self.replaceToken(new_name, md.seriesGroup, '%seriesgroup%')
|
||||
new_name = self.replaceToken(new_name, md.scanInfo, '%scaninfo%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
# remove empty braces,brackets, parentheses
|
||||
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name)
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name)
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
|
||||
new_name = re.sub("(\s--)+", " --", new_name)
|
||||
new_name = re.sub("(\s-)+", " -", new_name)
|
||||
|
||||
# remove dash or double dash at end of line
|
||||
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
|
||||
|
||||
# remove duplicate spaces (again!)
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
|
||||
new_name += ext
|
||||
new_basename += ext
|
||||
|
||||
# remove padding
|
||||
md.issue = IssueString(md.issue).as_string()
|
||||
if self.move:
|
||||
return new_name.strip()
|
||||
return new_basename.strip()
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(" :", " -")
|
||||
new_name = new_name.replace(": ", " - ")
|
||||
new_name = new_name.replace(":", "-")
|
||||
new_name = new_name.replace("?", "")
|
||||
|
||||
return new_name
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""A PyQt5 widget for managing list of comic archive files"""
|
||||
# coding=utf-8
|
||||
"""A PyQt4 widget for managing list of comic archive files"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
@@ -14,33 +15,42 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import platform
|
||||
import os
|
||||
from typing import List
|
||||
#import os
|
||||
#import sys
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from optionalmsgdialog import OptionalMessageDialog
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
|
||||
import utils
|
||||
#from comicarchive import MetaDataStyle
|
||||
#from genericmetadata import GenericMetadata, PageType
|
||||
|
||||
|
||||
class FileTableWidgetItem(QtWidgets.QTableWidgetItem):
|
||||
class FileTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.data(QtCore.Qt.ItemDataRole.UserRole) < other.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
return (self.data(Qt.UserRole).toBool() <
|
||||
other.data(Qt.UserRole).toBool())
|
||||
|
||||
|
||||
class FileInfo:
|
||||
def __init__(self, ca: ComicArchive):
|
||||
self.ca: ComicArchive = ca
|
||||
class FileInfo():
|
||||
|
||||
def __init__(self, ca):
|
||||
self.ca = ca
|
||||
|
||||
|
||||
class FileSelectionList(QtWidgets.QWidget):
|
||||
selectionChanged = QtCore.pyqtSignal(QtCore.QVariant)
|
||||
listCleared = QtCore.pyqtSignal()
|
||||
class FileSelectionList(QWidget):
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
listCleared = pyqtSignal()
|
||||
|
||||
fileColNum = 0
|
||||
CRFlagColNum = 1
|
||||
@@ -50,77 +60,92 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
folderColNum = 5
|
||||
dataColNum = fileColNum
|
||||
|
||||
def __init__(self, parent, settings, dirty_flag_verification):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent, settings):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("fileselectionlist.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui'), self)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
reduce_widget_font_size(self.twList)
|
||||
reduceWidgetFontSize(self.twList)
|
||||
|
||||
self.twList.setColumnCount(6)
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
|
||||
#self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
|
||||
# self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
|
||||
|
||||
self.currentItem = None
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.dirty_flag = False
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.modifiedFlag = False
|
||||
|
||||
select_all_action = QtWidgets.QAction("Select All", self)
|
||||
remove_action = QtWidgets.QAction("Remove Selected Items", self)
|
||||
self.separator = QtWidgets.QAction("", self)
|
||||
selectAllAction = QAction("Select All", self)
|
||||
removeAction = QAction("Remove Selected Items", self)
|
||||
self.separator = QAction("", self)
|
||||
self.separator.setSeparator(True)
|
||||
|
||||
select_all_action.setShortcut("Ctrl+A")
|
||||
remove_action.setShortcut("Ctrl+X")
|
||||
selectAllAction.setShortcut('Ctrl+A')
|
||||
removeAction.setShortcut('Ctrl+X')
|
||||
|
||||
select_all_action.triggered.connect(self.select_all)
|
||||
remove_action.triggered.connect(self.remove_selection)
|
||||
selectAllAction.triggered.connect(self.selectAll)
|
||||
removeAction.triggered.connect(self.removeSelection)
|
||||
|
||||
self.addAction(select_all_action)
|
||||
self.addAction(remove_action)
|
||||
self.addAction(selectAllAction)
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
|
||||
self.dirty_flag_verification = dirty_flag_verification
|
||||
|
||||
def get_sorting(self) -> (int, int):
|
||||
def getSorting(self):
|
||||
col = self.twList.horizontalHeader().sortIndicatorSection()
|
||||
order = self.twList.horizontalHeader().sortIndicatorOrder()
|
||||
return int(col), int(order)
|
||||
return col, order
|
||||
|
||||
def set_sorting(self, col: int, order: QtCore.Qt.SortOrder):
|
||||
self.twList.horizontalHeader().setSortIndicator(col, order)
|
||||
def setSorting(self, col, order):
|
||||
col = self.twList.horizontalHeader().setSortIndicator(col, order)
|
||||
|
||||
def add_app_action(self, action):
|
||||
self.insertAction(QtWidgets.QAction(), action)
|
||||
def addAppAction(self, action):
|
||||
self.insertAction(None, action)
|
||||
|
||||
def set_modified_flag(self, modified):
|
||||
self.dirty_flag = modified
|
||||
def setModifiedFlag(self, modified):
|
||||
self.modifiedFlag = modified
|
||||
|
||||
def select_all(self):
|
||||
self.twList.setRangeSelected(QtWidgets.QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), True)
|
||||
def selectAll(self):
|
||||
self.twList.setRangeSelected(
|
||||
QTableWidgetSelectionRange(
|
||||
0,
|
||||
0,
|
||||
self.twList.rowCount() -
|
||||
1,
|
||||
5),
|
||||
True)
|
||||
|
||||
def deselect_all(self):
|
||||
self.twList.setRangeSelected(QtWidgets.QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), False)
|
||||
def deselectAll(self):
|
||||
self.twList.setRangeSelected(
|
||||
QTableWidgetSelectionRange(
|
||||
0,
|
||||
0,
|
||||
self.twList.rowCount() -
|
||||
1,
|
||||
5),
|
||||
False)
|
||||
|
||||
def remove_archive_list(self, ca_list):
|
||||
def removeArchiveList(self, ca_list):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for ca in ca_list:
|
||||
for row in range(self.twList.rowCount()):
|
||||
row_ca = self.get_archive_by_row(row)
|
||||
row_ca = self.getArchiveByRow(row)
|
||||
if row_ca == ca:
|
||||
self.twList.removeRow(row)
|
||||
break
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def get_archive_by_row(self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
def getArchiveByRow(self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
|
||||
Qt.UserRole).toPyObject()
|
||||
return fi.ca
|
||||
|
||||
def get_current_archive(self):
|
||||
return self.get_archive_by_row(self.twList.currentRow())
|
||||
def getCurrentArchive(self):
|
||||
return self.getArchiveByRow(self.twList.currentRow())
|
||||
|
||||
def remove_selection(self):
|
||||
def removeSelection(self):
|
||||
row_list = []
|
||||
for item in self.twList.selectedItems():
|
||||
if item.column() == 0:
|
||||
@@ -130,71 +155,87 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
return
|
||||
|
||||
if self.twList.currentRow() in row_list:
|
||||
if not self.dirty_flag_verification(
|
||||
"Remove Archive", "If you close this archive, data in the form will be lost. Are you sure?"
|
||||
):
|
||||
if not self.modifiedFlagVerification(
|
||||
"Remove Archive",
|
||||
"If you close this archive, data in the form will be lost. Are you sure?"):
|
||||
return
|
||||
|
||||
row_list.sort()
|
||||
row_list.reverse()
|
||||
|
||||
self.twList.currentItemChanged.disconnect(self.current_item_changed_cb)
|
||||
self.twList.currentItemChanged.disconnect(self.currentItemChangedCB)
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for i in row_list:
|
||||
self.twList.removeRow(i)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
# since on a removal, we select row 0, make sure callback occurs if
|
||||
# we're already there
|
||||
if self.twList.currentRow() == 0:
|
||||
self.current_item_changed_cb(self.twList.currentItem(), None)
|
||||
self.currentItemChangedCB(self.twList.currentItem(), None)
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
|
||||
def add_path_list(self, pathlist):
|
||||
def addPathList(self, pathlist):
|
||||
|
||||
filelist = utils.get_recursive_filelist(pathlist)
|
||||
# we now have a list of files to add
|
||||
|
||||
# Prog dialog on Linux flakes out for small range, so scale up
|
||||
progdialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(filelist), parent=self)
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle("Adding Files")
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
||||
progdialog.setMinimumDuration(300)
|
||||
center_window_on_parent(progdialog)
|
||||
# progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.setWindowModality(Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
first_added = None
|
||||
firstAdded = None
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx, f in enumerate(filelist):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx + 1)
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
center_window_on_parent(progdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
row = self.add_path_item(f)
|
||||
if first_added is None and row is not None:
|
||||
first_added = row
|
||||
centerWindowOnParent(progdialog)
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem(f)
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
progdialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
progdialog.close()
|
||||
if (self.settings.show_no_unrar_warning and
|
||||
self.settings.unrar_exe_path == "" and
|
||||
self.settings.rar_exe_path == "" and
|
||||
platform.system() != "Windows"):
|
||||
for f in filelist:
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
if ext == ".rar" or ext == ".cbr":
|
||||
checked = OptionalMessageDialog.msg(self, "No unrar tool",
|
||||
"""
|
||||
It looks like you've tried to open at least one CBR or RAR file.<br><br>
|
||||
In order for ComicTagger to read this kind of file, you will have to configure
|
||||
the location of the unrar tool in the settings. Until then, ComicTagger
|
||||
will not be able recognize these kind of files.
|
||||
"""
|
||||
)
|
||||
self.settings.show_no_unrar_warning = not checked
|
||||
break
|
||||
|
||||
if first_added is not None:
|
||||
self.twList.selectRow(first_added)
|
||||
if firstAdded is not None:
|
||||
self.twList.selectRow(firstAdded)
|
||||
else:
|
||||
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "File Open", "Selected file doesn't seem to be a comic archive."
|
||||
)
|
||||
QMessageBox.information(self, self.tr("File Open"), self.tr(
|
||||
"Selected file doesn't seem to be a comic archive."))
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "File/Folder Open", "No readable comic archives were found.")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self.tr("File/Folder Open"),
|
||||
self.tr("No comic archives were found."))
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
@@ -209,78 +250,84 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
if self.twList.columnWidth(FileSelectionList.folderColNum) > 200:
|
||||
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
|
||||
|
||||
def is_list_dupe(self, path):
|
||||
def isListDupe(self, path):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.get_archive_by_row(r)
|
||||
ca = self.getArchiveByRow(r)
|
||||
if ca.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def get_current_list_row(self, path):
|
||||
def getCurrentListRow(self, path):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.get_archive_by_row(r)
|
||||
ca = self.getArchiveByRow(r)
|
||||
if ca.path == path:
|
||||
return r
|
||||
r = r + 1
|
||||
|
||||
return -1
|
||||
|
||||
def add_path_item(self, path):
|
||||
path = str(path)
|
||||
def addPathItem(self, path):
|
||||
path = unicode(path)
|
||||
path = os.path.abspath(path)
|
||||
# print "processing", path
|
||||
|
||||
if self.is_list_dupe(path):
|
||||
return self.get_current_list_row(path)
|
||||
if self.isListDupe(path):
|
||||
return self.getCurrentListRow(path)
|
||||
|
||||
ca = ComicArchive(path, self.settings.rar_exe_path, ComicTaggerSettings.get_graphic("nocover.png"))
|
||||
ca = ComicArchive(
|
||||
path,
|
||||
self.settings.rar_exe_path,
|
||||
ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
|
||||
if ca.seems_to_be_a_comic_archive():
|
||||
if ca.seemsToBeAComicArchive():
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow(row)
|
||||
|
||||
fi = FileInfo(ca)
|
||||
|
||||
filename_item = QtWidgets.QTableWidgetItem()
|
||||
folder_item = QtWidgets.QTableWidgetItem()
|
||||
filename_item = QTableWidgetItem()
|
||||
folder_item = QTableWidgetItem()
|
||||
cix_item = FileTableWidgetItem()
|
||||
cbi_item = FileTableWidgetItem()
|
||||
readonly_item = FileTableWidgetItem()
|
||||
type_item = QtWidgets.QTableWidgetItem()
|
||||
type_item = QTableWidgetItem()
|
||||
|
||||
filename_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
filename_item.setData(QtCore.Qt.ItemDataRole.UserRole, fi)
|
||||
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
|
||||
filename_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
filename_item.setData(Qt.UserRole, fi)
|
||||
self.twList.setItem(
|
||||
row, FileSelectionList.fileColNum, filename_item)
|
||||
|
||||
folder_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
|
||||
folder_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.twList.setItem(
|
||||
row, FileSelectionList.folderColNum, folder_item)
|
||||
|
||||
type_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
type_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
|
||||
|
||||
cix_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
cix_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
|
||||
|
||||
cbi_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
cbi_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
|
||||
|
||||
readonly_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
readonly_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
|
||||
readonly_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
readonly_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(
|
||||
row, FileSelectionList.readonlyColNum, readonly_item)
|
||||
|
||||
self.update_row(row)
|
||||
self.updateRow(row)
|
||||
|
||||
return row
|
||||
return -1
|
||||
|
||||
def update_row(self, row):
|
||||
fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
def updateRow(self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
|
||||
Qt.UserRole).toPyObject()
|
||||
|
||||
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
|
||||
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
|
||||
@@ -291,93 +338,119 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[0]
|
||||
folder_item.setText(item_text)
|
||||
folder_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
folder_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[1]
|
||||
filename_item.setText(item_text)
|
||||
filename_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
filename_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
if fi.ca.is_sevenzip():
|
||||
item_text = "7Z"
|
||||
elif fi.ca.is_zip():
|
||||
if fi.ca.isZip():
|
||||
item_text = "ZIP"
|
||||
elif fi.ca.is_rar():
|
||||
elif fi.ca.isRar():
|
||||
item_text = "RAR"
|
||||
else:
|
||||
item_text = ""
|
||||
type_item.setText(item_text)
|
||||
type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
type_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
if fi.ca.has_cix():
|
||||
cix_item.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
|
||||
if fi.ca.hasCIX():
|
||||
cix_item.setCheckState(Qt.Checked)
|
||||
cix_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
|
||||
cix_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
cix_item.setData(Qt.UserRole, False)
|
||||
cix_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if fi.ca.has_cbi():
|
||||
cbi_item.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
|
||||
if fi.ca.hasCBI():
|
||||
cbi_item.setCheckState(Qt.Checked)
|
||||
cbi_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
|
||||
cbi_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
cbi_item.setData(Qt.UserRole, False)
|
||||
cbi_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if not fi.ca.is_writable():
|
||||
readonly_item.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
|
||||
if not fi.ca.isWritable():
|
||||
readonly_item.setCheckState(Qt.Checked)
|
||||
readonly_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
|
||||
readonly_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
readonly_item.setData(Qt.UserRole, False)
|
||||
readonly_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
# Reading these will force them into the ComicArchive's cache
|
||||
fi.ca.read_cix()
|
||||
fi.ca.has_cbi()
|
||||
fi.ca.readCIX()
|
||||
fi.ca.hasCBI()
|
||||
|
||||
def get_selected_archive_list(self) -> List[ComicArchive]:
|
||||
ca_list: List[ComicArchive] = []
|
||||
def getSelectedArchiveList(self):
|
||||
ca_list = []
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if item.isSelected():
|
||||
fi: FileInfo = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
if self.twList.isItemSelected(item):
|
||||
fi = item.data(Qt.UserRole).toPyObject()
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
return ca_list
|
||||
|
||||
def update_current_row(self):
|
||||
self.update_row(self.twList.currentRow())
|
||||
def updateCurrentRow(self):
|
||||
self.updateRow(self.twList.currentRow())
|
||||
|
||||
def update_selected_rows(self):
|
||||
def updateSelectedRows(self):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if item.isSelected():
|
||||
self.update_row(r)
|
||||
if self.twList.isItemSelected(item):
|
||||
self.updateRow(r)
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def current_item_changed_cb(self, curr, prev):
|
||||
def currentItemChangedCB(self, curr, prev):
|
||||
|
||||
new_idx = curr.row()
|
||||
old_idx = -1
|
||||
if prev is not None:
|
||||
old_idx = prev.row()
|
||||
#print("old {0} new {1}".format(old_idx, new_idx))
|
||||
|
||||
if old_idx == new_idx:
|
||||
return
|
||||
|
||||
# don't allow change if modified
|
||||
if prev is not None and new_idx != old_idx:
|
||||
if not self.dirty_flag_verification(
|
||||
"Change Archive", "If you change archives now, data in the form will be lost. Are you sure?"
|
||||
):
|
||||
self.twList.currentItemChanged.disconnect(self.current_item_changed_cb)
|
||||
if not self.modifiedFlagVerification(
|
||||
"Change Archive",
|
||||
"If you change archives now, data in the form will be lost. Are you sure?"):
|
||||
self.twList.currentItemChanged.disconnect(
|
||||
self.currentItemChangedCB)
|
||||
self.twList.setCurrentItem(prev)
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
|
||||
self.twList.currentItemChanged.connect(
|
||||
self.currentItemChangedCB)
|
||||
# Need to defer this revert selection, for some reason
|
||||
QtCore.QTimer.singleShot(1, self.revert_selection)
|
||||
QTimer.singleShot(1, self.revertSelection)
|
||||
return
|
||||
|
||||
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
self.selectionChanged.emit(QtCore.QVariant(fi))
|
||||
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(
|
||||
Qt.UserRole).toPyObject()
|
||||
self.selectionChanged.emit(QVariant(fi))
|
||||
|
||||
def revert_selection(self):
|
||||
def revertSelection(self):
|
||||
self.twList.selectRow(self.twList.currentRow())
|
||||
|
||||
def modifiedFlagVerification(self, title, desc):
|
||||
if self.modifiedFlag:
|
||||
reply = QMessageBox.question(self,
|
||||
self.tr(title),
|
||||
self.tr(desc),
|
||||
QMessageBox.Yes, QMessageBox.No)
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Attempt to use a special checkbox widget in the cell.
|
||||
# Couldn't figure out how to disable it with "enabled" colors
|
||||
#w = QWidget()
|
||||
#cb = QCheckBox(w)
|
||||
# cb.setCheckState(Qt.Checked)
|
||||
#layout = QHBoxLayout()
|
||||
# layout.addWidget(cb)
|
||||
# layout.setAlignment(Qt.AlignHCenter)
|
||||
# layout.setMargin(2)
|
||||
# w.setLayout(layout)
|
||||
#self.twList.setCellWidget(row, 2, w)
|
||||
|
||||
1
comictaggerlib/genericmetadata.py
Normal file
1
comictaggerlib/genericmetadata.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.genericmetadata import *
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -14,61 +14,67 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3 as lite
|
||||
import os
|
||||
import datetime
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
import urllib
|
||||
import ssl
|
||||
#import urllib2
|
||||
|
||||
try:
|
||||
from PyQt5 import QtCore, QtNetwork
|
||||
|
||||
qt_available = True
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
from PyQt4 import QtGui
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
class QByteArray():
|
||||
pass
|
||||
|
||||
class pyqtSignal():
|
||||
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
def emit(a, b, c):
|
||||
pass
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class ImageFetcherException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def fetch_complete(this, image_data):
|
||||
...
|
||||
class ImageFetcher(QObject):
|
||||
|
||||
|
||||
class ImageFetcher:
|
||||
|
||||
image_fetch_complete = fetch_complete
|
||||
fetchComplete = pyqtSignal(QByteArray, int)
|
||||
|
||||
def __init__(self):
|
||||
QObject.__init__(self)
|
||||
|
||||
self.settings_folder = ComicTaggerSettings.get_settings_folder()
|
||||
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
|
||||
self.db_file = os.path.join(self.settings_folder, "image_url_cache.db")
|
||||
self.cache_folder = os.path.join(self.settings_folder, "image_cache")
|
||||
|
||||
self.user_data = None
|
||||
self.fetched_url = ""
|
||||
|
||||
if not os.path.exists(self.db_file):
|
||||
self.create_image_db()
|
||||
|
||||
if qt_available:
|
||||
self.nam = QtNetwork.QNetworkAccessManager()
|
||||
# always use a tls context for urlopen
|
||||
self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
||||
|
||||
def clear_cache(self):
|
||||
def clearCache(self):
|
||||
os.unlink(self.db_file)
|
||||
if os.path.isdir(self.cache_folder):
|
||||
shutil.rmtree(self.cache_folder)
|
||||
|
||||
def fetch(self, url, blocking=False):
|
||||
def fetch(self, url, user_data=None, blocking=False):
|
||||
"""
|
||||
If called with blocking=True, this will block until the image is
|
||||
fetched.
|
||||
@@ -76,49 +82,52 @@ class ImageFetcher:
|
||||
background, and emit a signal when done
|
||||
"""
|
||||
|
||||
self.user_data = user_data
|
||||
self.fetched_url = url
|
||||
|
||||
# first look in the DB
|
||||
image_data = self.get_image_from_cache(url)
|
||||
if blocking or not qt_available:
|
||||
|
||||
if blocking:
|
||||
if image_data is None:
|
||||
try:
|
||||
image_data = requests.get(url, headers={"user-agent": "comictagger/" + ctversion.version}).content
|
||||
image_data = urllib.urlopen(url, context=self.ssl).read()
|
||||
except Exception as e:
|
||||
logger.exception("Fetching url failed: %s")
|
||||
raise ImageFetcherException("Network Error!") from e
|
||||
print(e)
|
||||
raise ImageFetcherException("Network Error!")
|
||||
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
return image_data
|
||||
|
||||
if qt_available:
|
||||
else:
|
||||
|
||||
# if we found it, just emit the signal asap
|
||||
if image_data is not None:
|
||||
self.image_fetch_complete(QtCore.QByteArray(image_data))
|
||||
return bytes()
|
||||
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
|
||||
return
|
||||
|
||||
# didn't find it. look online
|
||||
self.nam.finished.connect(self.finish_request)
|
||||
self.nam.get(QtNetwork.QNetworkRequest(QtCore.QUrl(url)))
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.finishRequest)
|
||||
self.nam.get(QNetworkRequest(QUrl(url)))
|
||||
|
||||
# we'll get called back when done...
|
||||
return bytes()
|
||||
|
||||
def finish_request(self, reply):
|
||||
def finishRequest(self, reply):
|
||||
|
||||
# read in the image data
|
||||
logger.debug("request finished")
|
||||
image_data = reply.readAll()
|
||||
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
|
||||
self.image_fetch_complete(image_data)
|
||||
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
|
||||
|
||||
def create_image_db(self):
|
||||
|
||||
# this will wipe out any existing version
|
||||
open(self.db_file, "wb").close()
|
||||
open(self.db_file, 'w').close()
|
||||
|
||||
# wipe any existing image cache folder too
|
||||
if os.path.isdir(self.cache_folder):
|
||||
@@ -129,24 +138,37 @@ class ImageFetcher:
|
||||
|
||||
# create tables
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
cur.execute("CREATE TABLE Images(url TEXT,filename TEXT,timestamp TEXT,PRIMARY KEY (url))")
|
||||
cur.execute("CREATE TABLE Images(" +
|
||||
"url TEXT," +
|
||||
"filename TEXT," +
|
||||
"timestamp TEXT," +
|
||||
"PRIMARY KEY (url))"
|
||||
)
|
||||
|
||||
def add_image_to_cache(self, url, image_data):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
tmp_fd, filename = tempfile.mkstemp(dir=self.cache_folder, prefix="img")
|
||||
with os.fdopen(tmp_fd, "w+b") as f:
|
||||
f.write(image_data)
|
||||
tmp_fd, filename = tempfile.mkstemp(
|
||||
dir=self.cache_folder, prefix="img")
|
||||
f = os.fdopen(tmp_fd, 'w+b')
|
||||
f.write(image_data)
|
||||
f.close()
|
||||
|
||||
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)", (url, filename, timestamp))
|
||||
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)",
|
||||
(url,
|
||||
filename,
|
||||
timestamp)
|
||||
)
|
||||
|
||||
def get_image_from_cache(self, url):
|
||||
|
||||
@@ -159,15 +181,15 @@ class ImageFetcher:
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
else:
|
||||
filename = row[0]
|
||||
image_data = None
|
||||
|
||||
filename = row[0]
|
||||
image_data = None
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
image_data = f.read()
|
||||
f.close()
|
||||
except IOError as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
with open(filename, "rb") as f:
|
||||
image_data = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
return image_data
|
||||
return image_data
|
||||
|
||||
@@ -14,56 +14,59 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import logging
|
||||
import StringIO
|
||||
import sys
|
||||
from functools import reduce
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageHasher:
|
||||
class ImageHasher(object):
|
||||
|
||||
def __init__(self, path=None, data=None, width=8, height=8):
|
||||
#self.hash_size = size
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
if path is None and data is None:
|
||||
raise IOError
|
||||
|
||||
try:
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(io.BytesIO(data))
|
||||
except Exception:
|
||||
logger.exception("Image data seems corrupted!")
|
||||
# just generate a bogus image
|
||||
self.image = Image.new("L", (1, 1))
|
||||
else:
|
||||
try:
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
except:
|
||||
print("Image data seems corrupted!")
|
||||
# just generate a bogus image
|
||||
self.image = Image.new("L", (1, 1))
|
||||
|
||||
def average_hash(self):
|
||||
try:
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception:
|
||||
logger.exception("average_hash error")
|
||||
return int(0)
|
||||
image = self.image.resize(
|
||||
(self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "average_hash error:", e
|
||||
return long(0)
|
||||
|
||||
pixels = list(image.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
|
||||
def compare_value_to_avg(i):
|
||||
return 1 if i > avg else 0
|
||||
return (1 if i > avg else 0)
|
||||
|
||||
bitlist = list(map(compare_value_to_avg, pixels))
|
||||
bitlist = map(compare_value_to_avg, pixels)
|
||||
|
||||
# build up an int value from the bit list, one bit at a time
|
||||
def set_bit(x, idx_val):
|
||||
(idx, val) = idx_val
|
||||
return x | (val << idx)
|
||||
return (x | (val << idx))
|
||||
|
||||
result = reduce(set_bit, enumerate(bitlist), 0)
|
||||
|
||||
@@ -71,8 +74,10 @@ class ImageHasher:
|
||||
return result
|
||||
|
||||
def average_hash2(self):
|
||||
pass
|
||||
"""
|
||||
# Got this one from somewhere on the net. Not a clue how the 'convolve2d' works!
|
||||
# Got this one from somewhere on the net. Not a clue how the 'convolve2d'
|
||||
# works!
|
||||
|
||||
from numpy import array
|
||||
from scipy.signal import convolve2d
|
||||
@@ -91,6 +96,7 @@ class ImageHasher:
|
||||
"""
|
||||
|
||||
def dct_average_hash(self):
|
||||
pass
|
||||
"""
|
||||
# Algorithm source: http://syntaxcandy.blogspot.com/2012/08/perceptual-hash.html
|
||||
|
||||
@@ -128,8 +134,8 @@ class ImageHasher:
|
||||
|
||||
7. Construct the hash. Set the 64 bits into a 64-bit integer. The order does not
|
||||
matter, just as long as you are consistent.
|
||||
|
||||
|
||||
"""
|
||||
"""
|
||||
import numpy
|
||||
import scipy.fftpack
|
||||
numpy.set_printoptions(threshold=10000, linewidth=200, precision=2, suppress=True)
|
||||
@@ -172,16 +178,16 @@ class ImageHasher:
|
||||
|
||||
@staticmethod
|
||||
def hamming_distance(h1, h2):
|
||||
if isinstance(h1, int) or isinstance(h2, int):
|
||||
if isinstance(h1, long) or isinstance(h1, int):
|
||||
n1 = h1
|
||||
n2 = h2
|
||||
else:
|
||||
# convert hex strings to ints
|
||||
n1 = int(h1, 16)
|
||||
n2 = int(h2, 16)
|
||||
n1 = long(h1, 16)
|
||||
n2 = long(h2, 16)
|
||||
|
||||
# xor the two numbers
|
||||
n = n1 ^ n2
|
||||
|
||||
# count up the 1's in the binary string
|
||||
return sum(b == "1" for b in bin(n)[2:])
|
||||
return sum(b == '1' for b in bin(n)[2:])
|
||||
|
||||
@@ -14,62 +14,69 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class ImagePopup(QtWidgets.QDialog):
|
||||
class ImagePopup(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("imagepopup.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui'), self)
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
self.setWindowFlags(QtCore.Qt.WindowType.Popup)
|
||||
self.setWindowState(QtCore.Qt.WindowState.WindowFullScreen)
|
||||
# self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
self.setWindowState(QtCore.Qt.WindowFullScreen)
|
||||
|
||||
self.imagePixmap = image_pixmap
|
||||
|
||||
screen_size = QtGui.QGuiApplication.primaryScreen().geometry()
|
||||
QtWidgets.QApplication.primaryScreen()
|
||||
screen_size = QtGui.QDesktopWidget().screenGeometry()
|
||||
self.resize(screen_size.width(), screen_size.height())
|
||||
self.move(0, 0)
|
||||
|
||||
# This is a total hack. Uses a snapshot of the desktop, and overlays a
|
||||
# translucent screen over it. Probably can do it better by setting opacity of a widget
|
||||
# TODO: macOS denies this
|
||||
screen = QtWidgets.QApplication.primaryScreen()
|
||||
self.desktopBg = screen.grabWindow(0, 0, 0, screen_size.width(), screen_size.height())
|
||||
bg = QtGui.QPixmap(ComicTaggerSettings.get_graphic("popup_bg.png"))
|
||||
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height())
|
||||
# translucent screen over it. Probably can do it better by setting opacity of a
|
||||
# widget
|
||||
self.desktopBg = QtGui.QPixmap.grabWindow(
|
||||
QtGui.QApplication.desktop().winId(),
|
||||
0,
|
||||
0,
|
||||
screen_size.width(),
|
||||
screen_size.height())
|
||||
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
|
||||
self.clientBgPixmap = bg.scaled(
|
||||
screen_size.width(), screen_size.height())
|
||||
self.setMask(self.clientBgPixmap.mask())
|
||||
|
||||
self.apply_image_pixmap()
|
||||
self.applyImagePixmap()
|
||||
self.showFullScreen()
|
||||
self.raise_()
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
||||
painter.drawPixmap(0, 0, self.desktopBg)
|
||||
painter.drawPixmap(0, 0, self.clientBgPixmap)
|
||||
painter.end()
|
||||
self.painter = QtGui.QPainter(self)
|
||||
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.painter.drawPixmap(0, 0, self.desktopBg)
|
||||
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
|
||||
self.painter.end()
|
||||
|
||||
def apply_image_pixmap(self):
|
||||
def applyImagePixmap(self):
|
||||
win_h = self.height()
|
||||
win_w = self.width()
|
||||
|
||||
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
|
||||
if self.imagePixmap.width(
|
||||
) > win_w or self.imagePixmap.height() > win_h:
|
||||
# scale the pixmap to fit in the frame
|
||||
display_pixmap = self.imagePixmap.scaled(win_w, win_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
|
||||
display_pixmap = self.imagePixmap.scaled(
|
||||
win_w, win_h, QtCore.Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap(display_pixmap)
|
||||
else:
|
||||
display_pixmap = self.imagePixmap
|
||||
@@ -79,7 +86,7 @@ class ImagePopup(QtWidgets.QDialog):
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move(int((win_w - img_w) / 2), int((win_h - img_h) / 2))
|
||||
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.close()
|
||||
|
||||
@@ -14,36 +14,27 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import logging
|
||||
import sys
|
||||
from typing import List, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException
|
||||
from comictaggerlib.imagehasher import ImageHasher
|
||||
from comictaggerlib.resulttypes import IssueResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import StringIO
|
||||
#import math
|
||||
#import urllib2
|
||||
#import urllib
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
|
||||
class SearchKeys(TypedDict):
|
||||
series: str
|
||||
issue_number: str
|
||||
month: int
|
||||
year: int
|
||||
issue_count: int
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagehasher import ImageHasher
|
||||
from imagefetcher import ImageFetcher, ImageFetcherException
|
||||
from issuestring import IssueString
|
||||
import utils
|
||||
#from settings import ComicTaggerSettings
|
||||
#from comicvinecacher import ComicVineCacher
|
||||
|
||||
|
||||
class IssueIdentifierNetworkError(Exception):
|
||||
@@ -55,22 +46,22 @@ class IssueIdentifierCancelled(Exception):
|
||||
|
||||
|
||||
class IssueIdentifier:
|
||||
result_no_matches = 0
|
||||
result_found_match_but_bad_cover_score = 1
|
||||
result_found_match_but_not_first_page = 2
|
||||
result_multiple_matches_with_bad_image_scores = 3
|
||||
result_one_good_match = 4
|
||||
result_multiple_good_matches = 5
|
||||
|
||||
def __init__(self, comic_archive: ComicArchive, settings):
|
||||
self.settings = settings
|
||||
self.comic_archive: ComicArchive = comic_archive
|
||||
ResultNoMatches = 0
|
||||
ResultFoundMatchButBadCoverScore = 1
|
||||
ResultFoundMatchButNotFirstPage = 2
|
||||
ResultMultipleMatchesWithBadImageScores = 3
|
||||
ResultOneGoodMatch = 4
|
||||
ResultMultipleGoodMatches = 5
|
||||
|
||||
def __init__(self, comic_archive, settings):
|
||||
self.comic_archive = comic_archive
|
||||
self.image_hasher = 1
|
||||
|
||||
self.only_use_additional_meta_data = False
|
||||
self.onlyUseAdditionalMetaData = False
|
||||
|
||||
# a decent hamming score, good enough to call it a match
|
||||
self.min_score_thresh: int = 16
|
||||
self.min_score_thresh = 16
|
||||
|
||||
# for alternate covers, be more stringent, since we're a bit more
|
||||
# scattershot in comparisons
|
||||
@@ -88,117 +79,113 @@ class IssueIdentifier:
|
||||
self.length_delta_thresh = settings.id_length_delta_thresh
|
||||
|
||||
# used to eliminate unlikely publishers
|
||||
self.publisher_filter = [s.strip().lower() for s in settings.id_publisher_filter.split(",")]
|
||||
self.publisher_blacklist = [
|
||||
s.strip().lower() for s in settings.id_publisher_blacklist.split(',')]
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function = IssueIdentifier.default_write_output
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
self.callback = None
|
||||
self.cover_url_callback = None
|
||||
self.search_result = self.result_no_matches
|
||||
self.coverUrlCallback = None
|
||||
self.search_result = self.ResultNoMatches
|
||||
self.cover_page_index = 0
|
||||
self.cancel = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.waitAndRetryOnRateLimit = False
|
||||
|
||||
self.match_list = []
|
||||
|
||||
def set_score_min_threshold(self, thresh: int):
|
||||
def setScoreMinThreshold(self, thresh):
|
||||
self.min_score_thresh = thresh
|
||||
|
||||
def set_score_min_distance(self, distance):
|
||||
def setScoreMinDistance(self, distance):
|
||||
self.min_score_distance = distance
|
||||
|
||||
def set_additional_metadata(self, md):
|
||||
def setAdditionalMetadata(self, md):
|
||||
self.additional_metadata = md
|
||||
|
||||
def set_name_length_delta_threshold(self, delta):
|
||||
def setNameLengthDeltaThreshold(self, delta):
|
||||
self.length_delta_thresh = delta
|
||||
|
||||
def set_publisher_filter(self, flt):
|
||||
self.publisher_filter = flt
|
||||
def setPublisherBlackList(self, blacklist):
|
||||
self.publisher_blacklist = blacklist
|
||||
|
||||
def set_hasher_algorithm(self, algo):
|
||||
def setHasherAlgorithm(self, algo):
|
||||
self.image_hasher = algo
|
||||
pass
|
||||
|
||||
def set_output_function(self, func):
|
||||
def setOutputFunction(self, func):
|
||||
self.output_function = func
|
||||
pass
|
||||
|
||||
def calculate_hash(self, image_data):
|
||||
if self.image_hasher == "3":
|
||||
def calculateHash(self, image_data):
|
||||
if self.image_hasher == '3':
|
||||
return ImageHasher(data=image_data).dct_average_hash()
|
||||
if self.image_hasher == "2":
|
||||
elif self.image_hasher == '2':
|
||||
return ImageHasher(data=image_data).average_hash2()
|
||||
else:
|
||||
return ImageHasher(data=image_data).average_hash()
|
||||
|
||||
return ImageHasher(data=image_data).average_hash()
|
||||
|
||||
def get_aspect_ratio(self, image_data):
|
||||
def getAspectRatio(self, image_data):
|
||||
try:
|
||||
im = Image.open(io.BytesIO(image_data))
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w, h = im.size
|
||||
return float(h) / float(w)
|
||||
except:
|
||||
return 1.5
|
||||
|
||||
def crop_cover(self, image_data):
|
||||
def cropCover(self, image_data):
|
||||
|
||||
im = Image.open(io.BytesIO(image_data))
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w, h = im.size
|
||||
|
||||
try:
|
||||
cropped_im = im.crop((int(w / 2), 0, w, h))
|
||||
except:
|
||||
logger.exception("cropCover() error")
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "cropCover() error:", e
|
||||
return None
|
||||
|
||||
output = io.BytesIO()
|
||||
output = StringIO.StringIO()
|
||||
cropped_im.save(output, format="PNG")
|
||||
cropped_image_data = output.getvalue()
|
||||
output.close()
|
||||
|
||||
return cropped_image_data
|
||||
|
||||
def set_progress_callback(self, cb_func):
|
||||
def setProgressCallback(self, cb_func):
|
||||
self.callback = cb_func
|
||||
|
||||
def set_cover_url_callback(self, cb_func):
|
||||
self.cover_url_callback = cb_func
|
||||
def setCoverURLCallback(self, cb_func):
|
||||
self.coverUrlCallback = cb_func
|
||||
|
||||
def get_search_keys(self):
|
||||
def getSearchKeys(self):
|
||||
|
||||
ca = self.comic_archive
|
||||
search_keys: SearchKeys = {
|
||||
"series": None,
|
||||
"issue_number": None,
|
||||
"month": None,
|
||||
"year": None,
|
||||
"issue_count": None,
|
||||
}
|
||||
search_keys = dict()
|
||||
search_keys['series'] = None
|
||||
search_keys['issue_number'] = None
|
||||
search_keys['month'] = None
|
||||
search_keys['year'] = None
|
||||
search_keys['issue_count'] = None
|
||||
|
||||
if ca is None:
|
||||
return None
|
||||
return
|
||||
|
||||
if self.only_use_additional_meta_data:
|
||||
search_keys["series"] = self.additional_metadata.series
|
||||
search_keys["issue_number"] = self.additional_metadata.issue
|
||||
search_keys["year"] = self.additional_metadata.year
|
||||
search_keys["month"] = self.additional_metadata.month
|
||||
search_keys["issue_count"] = self.additional_metadata.issue_count
|
||||
if self.onlyUseAdditionalMetaData:
|
||||
search_keys['series'] = self.additional_metadata.series
|
||||
search_keys['issue_number'] = self.additional_metadata.issue
|
||||
search_keys['year'] = self.additional_metadata.year
|
||||
search_keys['month'] = self.additional_metadata.month
|
||||
search_keys['issue_count'] = self.additional_metadata.issueCount
|
||||
return search_keys
|
||||
|
||||
# see if the archive has any useful meta data for searching with
|
||||
if ca.has_cix():
|
||||
internal_metadata = ca.read_cix()
|
||||
elif ca.has_cbi():
|
||||
internal_metadata = ca.read_cbi()
|
||||
if ca.hasCIX():
|
||||
internal_metadata = ca.readCIX()
|
||||
elif ca.hasCBI():
|
||||
internal_metadata = ca.readCBI()
|
||||
else:
|
||||
internal_metadata = ca.read_cbi()
|
||||
internal_metadata = ca.readCBI()
|
||||
|
||||
# try to get some metadata from filename
|
||||
md_from_filename = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
)
|
||||
md_from_filename = ca.metadataFromFilename()
|
||||
|
||||
# preference order:
|
||||
# 1. Additional metadata
|
||||
@@ -206,124 +193,136 @@ class IssueIdentifier:
|
||||
# 1. Filename metadata
|
||||
|
||||
if self.additional_metadata.series is not None:
|
||||
search_keys["series"] = self.additional_metadata.series
|
||||
search_keys['series'] = self.additional_metadata.series
|
||||
elif internal_metadata.series is not None:
|
||||
search_keys["series"] = internal_metadata.series
|
||||
search_keys['series'] = internal_metadata.series
|
||||
else:
|
||||
search_keys["series"] = md_from_filename.series
|
||||
search_keys['series'] = md_from_filename.series
|
||||
|
||||
if self.additional_metadata.issue is not None:
|
||||
search_keys["issue_number"] = self.additional_metadata.issue
|
||||
search_keys['issue_number'] = self.additional_metadata.issue
|
||||
elif internal_metadata.issue is not None:
|
||||
search_keys["issue_number"] = internal_metadata.issue
|
||||
search_keys['issue_number'] = internal_metadata.issue
|
||||
else:
|
||||
search_keys["issue_number"] = md_from_filename.issue
|
||||
search_keys['issue_number'] = md_from_filename.issue
|
||||
|
||||
if self.additional_metadata.year is not None:
|
||||
search_keys["year"] = self.additional_metadata.year
|
||||
search_keys['year'] = self.additional_metadata.year
|
||||
elif internal_metadata.year is not None:
|
||||
search_keys["year"] = internal_metadata.year
|
||||
search_keys['year'] = internal_metadata.year
|
||||
else:
|
||||
search_keys["year"] = md_from_filename.year
|
||||
search_keys['year'] = md_from_filename.year
|
||||
|
||||
if self.additional_metadata.month is not None:
|
||||
search_keys["month"] = self.additional_metadata.month
|
||||
search_keys['month'] = self.additional_metadata.month
|
||||
elif internal_metadata.month is not None:
|
||||
search_keys["month"] = internal_metadata.month
|
||||
search_keys['month'] = internal_metadata.month
|
||||
else:
|
||||
search_keys["month"] = md_from_filename.month
|
||||
search_keys['month'] = md_from_filename.month
|
||||
|
||||
if self.additional_metadata.issue_count is not None:
|
||||
search_keys["issue_count"] = self.additional_metadata.issue_count
|
||||
elif internal_metadata.issue_count is not None:
|
||||
search_keys["issue_count"] = internal_metadata.issue_count
|
||||
if self.additional_metadata.issueCount is not None:
|
||||
search_keys['issue_count'] = self.additional_metadata.issueCount
|
||||
elif internal_metadata.issueCount is not None:
|
||||
search_keys['issue_count'] = internal_metadata.issueCount
|
||||
else:
|
||||
search_keys["issue_count"] = md_from_filename.issue_count
|
||||
search_keys['issue_count'] = md_from_filename.issueCount
|
||||
|
||||
return search_keys
|
||||
|
||||
@staticmethod
|
||||
def default_write_output(text):
|
||||
def defaultWriteOutput(text):
|
||||
sys.stdout.write(text)
|
||||
sys.stdout.flush()
|
||||
|
||||
def log_msg(self, msg: str, newline=True):
|
||||
msg = str(msg)
|
||||
if newline:
|
||||
msg += "\n"
|
||||
def log_msg(self, msg, newline=True):
|
||||
self.output_function(msg)
|
||||
if newline:
|
||||
self.output_function("\n")
|
||||
|
||||
def get_issue_cover_match_score(
|
||||
self,
|
||||
comic_vine,
|
||||
issue_id,
|
||||
primary_img_url,
|
||||
primary_thumb_url,
|
||||
page_url,
|
||||
local_cover_hash_list,
|
||||
use_remote_alternates=False,
|
||||
use_log=True,
|
||||
):
|
||||
# local_cover_hash_list is a list of pre-calculated hashs.
|
||||
# use_remote_alternates - indicates to use alternate covers from CV
|
||||
def getIssueCoverMatchScore(
|
||||
self,
|
||||
comicVine,
|
||||
issue_id,
|
||||
primary_img_url,
|
||||
primary_thumb_url,
|
||||
page_url,
|
||||
localCoverHashList,
|
||||
useRemoteAlternates=False,
|
||||
useLog=True):
|
||||
# localHashes is a list of pre-calculated hashs.
|
||||
# useRemoteAlternates - indicates to use alternate covers from CV
|
||||
|
||||
try:
|
||||
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
|
||||
except ImageFetcherException as e:
|
||||
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
|
||||
raise IssueIdentifierNetworkError from e
|
||||
url_image_data = ImageFetcher().fetch(
|
||||
primary_thumb_url, blocking=True)
|
||||
except ImageFetcherException:
|
||||
self.log_msg(
|
||||
"Network issue while fetching cover image from Comic Vine. Aborting...")
|
||||
raise IssueIdentifierNetworkError
|
||||
|
||||
if self.cancel:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
# alert the GUI, if needed
|
||||
if self.cover_url_callback is not None:
|
||||
self.cover_url_callback(url_image_data)
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback(url_image_data)
|
||||
|
||||
remote_cover_list = []
|
||||
item = {"url": primary_img_url, "hash": self.calculate_hash(url_image_data)}
|
||||
item = dict()
|
||||
item['url'] = primary_img_url
|
||||
|
||||
item['hash'] = self.calculateHash(url_image_data)
|
||||
remote_cover_list.append(item)
|
||||
|
||||
if self.cancel:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
if use_remote_alternates:
|
||||
alt_img_url_list = comic_vine.fetch_alternate_cover_urls(issue_id, page_url)
|
||||
if useRemoteAlternates:
|
||||
alt_img_url_list = comicVine.fetchAlternateCoverURLs(
|
||||
issue_id, page_url)
|
||||
for alt_url in alt_img_url_list:
|
||||
try:
|
||||
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
|
||||
except ImageFetcherException as e:
|
||||
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
|
||||
raise IssueIdentifierNetworkError from e
|
||||
alt_url_image_data = ImageFetcher().fetch(
|
||||
alt_url, blocking=True)
|
||||
except ImageFetcherException:
|
||||
self.log_msg(
|
||||
"Network issue while fetching alt. cover image from Comic Vine. Aborting...")
|
||||
raise IssueIdentifierNetworkError
|
||||
|
||||
if self.cancel:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
# alert the GUI, if needed
|
||||
if self.cover_url_callback is not None:
|
||||
self.cover_url_callback(alt_url_image_data)
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback(alt_url_image_data)
|
||||
|
||||
item = {"url": alt_url, "hash": self.calculate_hash(alt_url_image_data)}
|
||||
item = dict()
|
||||
item['url'] = alt_url
|
||||
item['hash'] = self.calculateHash(alt_url_image_data)
|
||||
remote_cover_list.append(item)
|
||||
|
||||
if self.cancel:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
if use_log and use_remote_alternates:
|
||||
self.log_msg(f"[{len(remote_cover_list) - 1} alt. covers]", False)
|
||||
if use_log:
|
||||
if useLog and useRemoteAlternates:
|
||||
self.log_msg(
|
||||
"[{0} alt. covers]".format(len(remote_cover_list) - 1), False)
|
||||
if useLog:
|
||||
self.log_msg("[ ", False)
|
||||
|
||||
score_list = []
|
||||
done = False
|
||||
for local_cover_hash in local_cover_hash_list:
|
||||
for local_cover_hash in localCoverHashList:
|
||||
for remote_cover_item in remote_cover_list:
|
||||
score = ImageHasher.hamming_distance(local_cover_hash, remote_cover_item["hash"])
|
||||
score_item = {"score": score, "url": remote_cover_item["url"], "hash": remote_cover_item["hash"]}
|
||||
score = ImageHasher.hamming_distance(
|
||||
local_cover_hash, remote_cover_item['hash'])
|
||||
score_item = dict()
|
||||
score_item['score'] = score
|
||||
score_item['url'] = remote_cover_item['url']
|
||||
score_item['hash'] = remote_cover_item['hash']
|
||||
score_list.append(score_item)
|
||||
if use_log:
|
||||
self.log_msg(score, False)
|
||||
if useLog:
|
||||
self.log_msg("{0}".format(score), False)
|
||||
|
||||
if score <= self.strong_score_thresh:
|
||||
# such a good score, we can quit now, since for sure we
|
||||
@@ -333,71 +332,89 @@ class IssueIdentifier:
|
||||
if done:
|
||||
break
|
||||
|
||||
if use_log:
|
||||
if useLog:
|
||||
self.log_msg(" ]", False)
|
||||
|
||||
best_score_item = min(score_list, key=lambda x: x["score"])
|
||||
best_score_item = min(score_list, key=lambda x: x['score'])
|
||||
|
||||
return best_score_item
|
||||
|
||||
def search(self) -> List[IssueResult]:
|
||||
# def validate(self, issue_id):
|
||||
# create hash list
|
||||
# score = self.getIssueMatchScore(issue_id, hash_list, useRemoteAlternates = True)
|
||||
# if score < 20:
|
||||
# return True
|
||||
# else:
|
||||
# return False
|
||||
|
||||
def search(self):
|
||||
|
||||
ca = self.comic_archive
|
||||
self.match_list: List[IssueResult] = []
|
||||
self.match_list = []
|
||||
self.cancel = False
|
||||
self.search_result = self.result_no_matches
|
||||
self.search_result = self.ResultNoMatches
|
||||
|
||||
if not pil_available:
|
||||
self.log_msg("Python Imaging Library (PIL) is not available and is needed for issue identification.")
|
||||
self.log_msg(
|
||||
"Python Imaging Library (PIL) is not available and is needed for issue identification.")
|
||||
return self.match_list
|
||||
|
||||
if not ca.seems_to_be_a_comic_archive():
|
||||
self.log_msg("Sorry, but " + ca.path + " is not a comic archive!")
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
self.log_msg(
|
||||
"Sorry, but " + opts.filename + " is not a comic archive!")
|
||||
return self.match_list
|
||||
|
||||
cover_image_data = ca.get_page(self.cover_page_index)
|
||||
cover_hash = self.calculate_hash(cover_image_data)
|
||||
cover_image_data = ca.getPage(self.cover_page_index)
|
||||
cover_hash = self.calculateHash(cover_image_data)
|
||||
|
||||
# check the aspect ratio
|
||||
# if it's wider than it is high, it's probably a two page spread
|
||||
# if so, crop it and calculate a second hash
|
||||
narrow_cover_hash = None
|
||||
aspect_ratio = self.get_aspect_ratio(cover_image_data)
|
||||
aspect_ratio = self.getAspectRatio(cover_image_data)
|
||||
if aspect_ratio < 1.0:
|
||||
right_side_image_data = self.crop_cover(cover_image_data)
|
||||
right_side_image_data = self.cropCover(cover_image_data)
|
||||
if right_side_image_data is not None:
|
||||
narrow_cover_hash = self.calculate_hash(right_side_image_data)
|
||||
narrow_cover_hash = self.calculateHash(right_side_image_data)
|
||||
|
||||
keys = self.get_search_keys()
|
||||
#self.log_msg("Cover hash = {0:016x}".format(cover_hash))
|
||||
|
||||
keys = self.getSearchKeys()
|
||||
# normalize the issue number
|
||||
keys["issue_number"] = IssueString(keys["issue_number"]).as_string()
|
||||
keys['issue_number'] = IssueString(keys['issue_number']).asString()
|
||||
|
||||
# we need, at minimum, a series and issue number
|
||||
if keys["series"] is None or keys["issue_number"] is None:
|
||||
if keys['series'] is None or keys['issue_number'] is None:
|
||||
self.log_msg("Not enough info for a search!")
|
||||
return []
|
||||
|
||||
self.log_msg("Going to search for:")
|
||||
self.log_msg("\tSeries: " + keys["series"])
|
||||
self.log_msg("\tIssue: " + keys["issue_number"])
|
||||
if keys["issue_count"] is not None:
|
||||
self.log_msg("\tCount: " + str(keys["issue_count"]))
|
||||
if keys["year"] is not None:
|
||||
self.log_msg("\tYear: " + str(keys["year"]))
|
||||
if keys["month"] is not None:
|
||||
self.log_msg("\tMonth: " + str(keys["month"]))
|
||||
self.log_msg("\tSeries: " + keys['series'])
|
||||
self.log_msg("\tIssue: " + keys['issue_number'])
|
||||
if keys['issue_count'] is not None:
|
||||
self.log_msg("\tCount: " + str(keys['issue_count']))
|
||||
if keys['year'] is not None:
|
||||
self.log_msg("\tYear: " + str(keys['year']))
|
||||
if keys['month'] is not None:
|
||||
self.log_msg("\tMonth: " + str(keys['month']))
|
||||
|
||||
comic_vine = ComicVineTalker()
|
||||
comic_vine.wait_for_rate_limit = self.wait_and_retry_on_rate_limit
|
||||
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
comicVine = ComicVineTalker()
|
||||
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
|
||||
|
||||
comic_vine.set_log_func(self.output_function)
|
||||
comicVine.setLogFunc(self.output_function)
|
||||
|
||||
self.log_msg(f"Searching for {keys['series']} #{keys['issue_number']} ...")
|
||||
# self.log_msg(("Searching for " + keys['series'] + "...")
|
||||
self.log_msg(u"Searching for {0} #{1} ...".format(
|
||||
keys['series'], keys['issue_number']))
|
||||
try:
|
||||
cv_search_results = comic_vine.search_for_series(keys["series"])
|
||||
cv_search_results = comicVine.searchForSeries(keys['series'])
|
||||
except ComicVineTalkerException:
|
||||
self.log_msg("Network issue while searching for series. Aborting...")
|
||||
self.log_msg(
|
||||
"Network issue while searching for series. Aborting...")
|
||||
return []
|
||||
|
||||
#self.log_msg("Found " + str(len(cv_search_results)) + " initial results")
|
||||
if self.cancel:
|
||||
return []
|
||||
|
||||
@@ -406,80 +423,83 @@ class IssueIdentifier:
|
||||
|
||||
series_second_round_list = []
|
||||
|
||||
#self.log_msg("Removing results with too long names, banned publishers, or future start dates")
|
||||
for item in cv_search_results:
|
||||
length_approved = False
|
||||
publisher_approved = True
|
||||
date_approved = True
|
||||
|
||||
# remove any series that starts after the issue year
|
||||
if (
|
||||
keys["year"] is not None
|
||||
and str(keys["year"]).isdigit()
|
||||
and item["start_year"] is not None
|
||||
and str(item["start_year"]).isdigit()
|
||||
):
|
||||
if int(keys["year"]) < int(item["start_year"]):
|
||||
if keys['year'] is not None and str(
|
||||
keys['year']).isdigit() and item['start_year'] is not None and str(
|
||||
item['start_year']).isdigit():
|
||||
if int(keys['year']) < int(item['start_year']):
|
||||
date_approved = False
|
||||
|
||||
# assume that our search name is close to the actual name, say
|
||||
# within ,e.g. 5 chars
|
||||
# sanitize both the search string and the result so that
|
||||
# we are comparing the same type of data
|
||||
shortened_key = utils.sanitize_title(keys["series"])
|
||||
shortened_item_name = utils.sanitize_title(item["name"])
|
||||
if len(shortened_item_name) < (len(shortened_key) + self.length_delta_thresh):
|
||||
shortened_key = utils.removearticles(keys['series'])
|
||||
shortened_item_name = utils.removearticles(item['name'])
|
||||
if len(shortened_item_name) < (
|
||||
len(shortened_key) + self.length_delta_thresh):
|
||||
length_approved = True
|
||||
|
||||
# remove any series from publishers on the filter
|
||||
if item["publisher"] is not None:
|
||||
publisher = item["publisher"]["name"]
|
||||
if publisher is not None and publisher.lower() in self.publisher_filter:
|
||||
# remove any series from publishers on the blacklist
|
||||
if item['publisher'] is not None:
|
||||
publisher = item['publisher']['name']
|
||||
if publisher is not None and publisher.lower(
|
||||
) in self.publisher_blacklist:
|
||||
publisher_approved = False
|
||||
|
||||
if length_approved and publisher_approved and date_approved:
|
||||
series_second_round_list.append(item)
|
||||
|
||||
self.log_msg("Searching in " + str(len(series_second_round_list)) + " series")
|
||||
self.log_msg(
|
||||
"Searching in " + str(len(series_second_round_list)) + " series")
|
||||
|
||||
if self.callback is not None:
|
||||
self.callback(0, len(series_second_round_list))
|
||||
|
||||
# now sort the list by name length
|
||||
series_second_round_list.sort(key=lambda x: len(x["name"]), reverse=False)
|
||||
series_second_round_list.sort(
|
||||
key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
# build a list of volume IDs
|
||||
volume_id_list = []
|
||||
volume_id_list = list()
|
||||
for series in series_second_round_list:
|
||||
volume_id_list.append(series["id"])
|
||||
volume_id_list.append(series['id'])
|
||||
|
||||
issue_list = None
|
||||
try:
|
||||
if len(volume_id_list) > 0:
|
||||
issue_list = comic_vine.fetch_issues_by_volume_issue_num_and_year(
|
||||
volume_id_list, keys["issue_number"], keys["year"]
|
||||
)
|
||||
issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear(
|
||||
volume_id_list,
|
||||
keys['issue_number'],
|
||||
keys['year'])
|
||||
|
||||
except ComicVineTalkerException:
|
||||
self.log_msg("Network issue while searching for series details. Aborting...")
|
||||
self.log_msg(
|
||||
"Network issue while searching for series details. Aborting...")
|
||||
return []
|
||||
|
||||
if issue_list is None:
|
||||
return []
|
||||
|
||||
shortlist = []
|
||||
shortlist = list()
|
||||
# now re-associate the issues and volumes
|
||||
for issue in issue_list:
|
||||
for series in series_second_round_list:
|
||||
if series["id"] == issue["volume"]["id"]:
|
||||
if series['id'] == issue['volume']['id']:
|
||||
shortlist.append((series, issue))
|
||||
break
|
||||
|
||||
if keys["year"] is None:
|
||||
self.log_msg(f"Found {len(shortlist)} series that have an issue #{keys['issue_number']}")
|
||||
if keys['year'] is None:
|
||||
self.log_msg(u"Found {0} series that have an issue #{1}".format(
|
||||
len(shortlist), keys['issue_number']))
|
||||
else:
|
||||
self.log_msg(
|
||||
f"Found {len(shortlist)} series that have an issue #{keys['issue_number']} from {keys['year']}"
|
||||
)
|
||||
u"Found {0} series that have an issue #{1} from {2}".format(
|
||||
len(shortlist),
|
||||
keys['issue_number'],
|
||||
keys['year']))
|
||||
|
||||
# now we have a shortlist of volumes with the desired issue number
|
||||
# Do first round of cover matching
|
||||
@@ -489,13 +509,13 @@ class IssueIdentifier:
|
||||
self.callback(counter, len(shortlist) * 3)
|
||||
counter += 1
|
||||
|
||||
self.log_msg(
|
||||
f"Examining covers for ID: {series['id']} {series['name']} ({series['start_year']}) ...",
|
||||
newline=False,
|
||||
)
|
||||
self.log_msg(u"Examining covers for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=False)
|
||||
|
||||
# parse out the cover date
|
||||
_, month, year = comic_vine.parse_date_str(issue["cover_date"])
|
||||
day, month, year = comicVine.parseDateStr(issue['cover_date'])
|
||||
|
||||
# Now check the cover match against the primary image
|
||||
hash_list = [cover_hash]
|
||||
@@ -503,88 +523,87 @@ class IssueIdentifier:
|
||||
hash_list.append(narrow_cover_hash)
|
||||
|
||||
try:
|
||||
image_url = issue["image"]["super_url"]
|
||||
thumb_url = issue["image"]["thumb_url"]
|
||||
page_url = issue["site_detail_url"]
|
||||
image_url = issue['image']['super_url']
|
||||
thumb_url = issue['image']['thumb_url']
|
||||
page_url = issue['site_detail_url']
|
||||
|
||||
score_item = self.get_issue_cover_match_score(
|
||||
comic_vine,
|
||||
issue["id"],
|
||||
score_item = self.getIssueCoverMatchScore(
|
||||
comicVine,
|
||||
issue['id'],
|
||||
image_url,
|
||||
thumb_url,
|
||||
page_url,
|
||||
hash_list,
|
||||
use_remote_alternates=False,
|
||||
)
|
||||
useRemoteAlternates=False)
|
||||
except:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
match: IssueResult = {
|
||||
"series": f"{series['name']} ({series['start_year']})",
|
||||
"distance": score_item["score"],
|
||||
"issue_number": keys["issue_number"],
|
||||
"cv_issue_count": series["count_of_issues"],
|
||||
"url_image_hash": score_item["hash"],
|
||||
"issue_title": issue["name"],
|
||||
"issue_id": issue["id"],
|
||||
"volume_id": series["id"],
|
||||
"month": month,
|
||||
"year": year,
|
||||
"publisher": None,
|
||||
"image_url": image_url,
|
||||
"thumb_url": thumb_url,
|
||||
"page_url": page_url,
|
||||
"description": issue["description"],
|
||||
}
|
||||
if series["publisher"] is not None:
|
||||
match["publisher"] = series["publisher"]["name"]
|
||||
match = dict()
|
||||
match['series'] = u"{0} ({1})".format(
|
||||
series['name'], series['start_year'])
|
||||
match['distance'] = score_item['score']
|
||||
match['issue_number'] = keys['issue_number']
|
||||
match['cv_issue_count'] = series['count_of_issues']
|
||||
match['url_image_hash'] = score_item['hash']
|
||||
match['issue_title'] = issue['name']
|
||||
match['issue_id'] = issue['id']
|
||||
match['volume_id'] = series['id']
|
||||
match['month'] = month
|
||||
match['year'] = year
|
||||
match['publisher'] = None
|
||||
if series['publisher'] is not None:
|
||||
match['publisher'] = series['publisher']['name']
|
||||
match['image_url'] = image_url
|
||||
match['thumb_url'] = thumb_url
|
||||
match['page_url'] = page_url
|
||||
match['description'] = issue['description']
|
||||
|
||||
self.match_list.append(match)
|
||||
|
||||
self.log_msg(f" --> {match['distance']}", newline=False)
|
||||
self.log_msg(" --> {0}".format(match['distance']), newline=False)
|
||||
|
||||
self.log_msg("")
|
||||
|
||||
if len(self.match_list) == 0:
|
||||
self.log_msg(":-(no matches!")
|
||||
self.search_result = self.result_no_matches
|
||||
self.search_result = self.ResultNoMatches
|
||||
return self.match_list
|
||||
|
||||
# sort list by image match scores
|
||||
self.match_list.sort(key=lambda k: k["distance"])
|
||||
self.match_list.sort(key=lambda k: k['distance'])
|
||||
|
||||
lst = []
|
||||
l = []
|
||||
for i in self.match_list:
|
||||
lst.append(i["distance"])
|
||||
l.append(i['distance'])
|
||||
|
||||
self.log_msg(f"Compared to covers in {len(self.match_list)} issue(s):", newline=False)
|
||||
self.log_msg(str(lst))
|
||||
self.log_msg("Compared to covers in {0} issue(s):".format(
|
||||
len(self.match_list)), newline=False)
|
||||
self.log_msg(str(l))
|
||||
|
||||
def print_match(item):
|
||||
self.log_msg(
|
||||
"-----> {} #{} {} ({}/{}) -- score: {}".format(
|
||||
item["series"],
|
||||
item["issue_number"],
|
||||
item["issue_title"],
|
||||
item["month"],
|
||||
item["year"],
|
||||
item["distance"],
|
||||
)
|
||||
)
|
||||
self.log_msg(u"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
|
||||
item['series'],
|
||||
item['issue_number'],
|
||||
item['issue_title'],
|
||||
item['month'],
|
||||
item['year'],
|
||||
item['distance']))
|
||||
|
||||
best_score: int = self.match_list[0]["distance"]
|
||||
best_score = self.match_list[0]['distance']
|
||||
|
||||
if best_score >= self.min_score_thresh:
|
||||
# we have 1 or more low-confidence matches (all bad cover scores)
|
||||
# look at a few more pages in the archive, and also alternate covers online
|
||||
self.log_msg("Very weak scores for the cover. Analyzing alternate pages and covers...")
|
||||
# look at a few more pages in the archive, and also alternate
|
||||
# covers online
|
||||
self.log_msg(
|
||||
"Very weak scores for the cover. Analyzing alternate pages and covers...")
|
||||
hash_list = [cover_hash]
|
||||
if narrow_cover_hash is not None:
|
||||
hash_list.append(narrow_cover_hash)
|
||||
for i in range(1, min(3, ca.get_number_of_pages())):
|
||||
image_data = ca.get_page(i)
|
||||
page_hash = self.calculate_hash(image_data)
|
||||
for i in range(1, min(3, ca.getNumberOfPages())):
|
||||
image_data = ca.getPage(i)
|
||||
page_hash = self.calculateHash(image_data)
|
||||
hash_list.append(page_hash)
|
||||
|
||||
second_match_list = []
|
||||
@@ -593,92 +612,111 @@ class IssueIdentifier:
|
||||
if self.callback is not None:
|
||||
self.callback(counter, len(self.match_list) * 3)
|
||||
counter += 1
|
||||
self.log_msg(f"Examining alternate covers for ID: {m['volume_id']} {m['series']} ...", newline=False)
|
||||
self.log_msg(
|
||||
u"Examining alternate covers for ID: {0} {1} ...".format(
|
||||
m['volume_id'],
|
||||
m['series']),
|
||||
newline=False)
|
||||
try:
|
||||
score_item = self.get_issue_cover_match_score(
|
||||
comic_vine,
|
||||
m["issue_id"],
|
||||
m["image_url"],
|
||||
m["thumb_url"],
|
||||
m["page_url"],
|
||||
score_item = self.getIssueCoverMatchScore(
|
||||
comicVine,
|
||||
m['issue_id'],
|
||||
m['image_url'],
|
||||
m['thumb_url'],
|
||||
m['page_url'],
|
||||
hash_list,
|
||||
use_remote_alternates=True,
|
||||
)
|
||||
useRemoteAlternates=True)
|
||||
except:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
self.log_msg(f"--->{score_item['score']}")
|
||||
self.log_msg("--->{0}".format(score_item['score']))
|
||||
self.log_msg("")
|
||||
|
||||
if score_item["score"] < self.min_alternate_score_thresh:
|
||||
if score_item['score'] < self.min_alternate_score_thresh:
|
||||
second_match_list.append(m)
|
||||
m["distance"] = score_item["score"]
|
||||
m['distance'] = score_item['score']
|
||||
|
||||
if len(second_match_list) == 0:
|
||||
if len(self.match_list) == 1:
|
||||
self.log_msg("No matching pages in the issue.")
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.search_result = self.result_found_match_but_bad_cover_score
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultFoundMatchButBadCoverScore
|
||||
else:
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg("Multiple bad cover matches! Need to use other info...")
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.search_result = self.result_multiple_matches_with_bad_image_scores
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"Multiple bad cover matches! Need to use other info...")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultMultipleMatchesWithBadImageScores
|
||||
return self.match_list
|
||||
else:
|
||||
# We did good, found something!
|
||||
self.log_msg("Success in secondary/alternate cover matching!")
|
||||
|
||||
# We did good, found something!
|
||||
self.log_msg("Success in secondary/alternate cover matching!")
|
||||
|
||||
self.match_list = second_match_list
|
||||
# sort new list by image match scores
|
||||
self.match_list.sort(key=lambda k: k["distance"])
|
||||
best_score = self.match_list[0]["distance"]
|
||||
self.log_msg("[Second round cover matching: best score = {best_score}]")
|
||||
# now drop down into the rest of the processing
|
||||
self.match_list = second_match_list
|
||||
# sort new list by image match scores
|
||||
self.match_list.sort(key=lambda k: k['distance'])
|
||||
best_score = self.match_list[0]['distance']
|
||||
self.log_msg(
|
||||
"[Second round cover matching: best score = {0}]".format(best_score))
|
||||
# now drop down into the rest of the processing
|
||||
|
||||
if self.callback is not None:
|
||||
self.callback(99, 100)
|
||||
|
||||
# now pare down list, remove any item more than specified distant from the top scores
|
||||
# now pare down list, remove any item more than specified distant from
|
||||
# the top scores
|
||||
for item in reversed(self.match_list):
|
||||
if item["distance"] > best_score + self.min_score_distance:
|
||||
if item['distance'] > best_score + self.min_score_distance:
|
||||
self.match_list.remove(item)
|
||||
|
||||
# One more test for the case choosing limited series first issue vs a trade with the same cover:
|
||||
# if we have a given issue count > 1 and the volume from CV has count==1, remove it from match list
|
||||
if len(self.match_list) >= 2 and keys["issue_count"] is not None and keys["issue_count"] != 1:
|
||||
new_list = []
|
||||
# if we have a given issue count > 1 and the volume from CV has
|
||||
# count==1, remove it from match list
|
||||
if len(self.match_list) >= 2 and keys[
|
||||
'issue_count'] is not None and keys['issue_count'] != 1:
|
||||
new_list = list()
|
||||
for match in self.match_list:
|
||||
if match["cv_issue_count"] != 1:
|
||||
if match['cv_issue_count'] != 1:
|
||||
new_list.append(match)
|
||||
else:
|
||||
self.log_msg(
|
||||
f"Removing volume {match['series']} [{match['volume_id']}] from consideration (only 1 issue)"
|
||||
)
|
||||
"Removing volume {0} [{1}] from consideration (only 1 issue)".format(
|
||||
match['series'],
|
||||
match['volume_id']))
|
||||
|
||||
if len(new_list) > 0:
|
||||
self.match_list = new_list
|
||||
|
||||
if len(self.match_list) == 1:
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.search_result = self.result_one_good_match
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
|
||||
elif len(self.match_list) == 0:
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.log_msg("No matches found :(")
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.search_result = self.result_no_matches
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultNoMatches
|
||||
else:
|
||||
# we've got multiple good matches:
|
||||
self.log_msg("More than one likely candidate.")
|
||||
self.search_result = self.result_multiple_good_matches
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultMultipleGoodMatches
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
for item in self.match_list:
|
||||
print_match(item)
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
|
||||
return self.match_list
|
||||
|
||||
@@ -14,56 +14,58 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import sys
|
||||
#import os
|
||||
#import re
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
|
||||
class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem):
|
||||
class IssueNumberTableWidgetItem(QtGui.QTableWidgetItem):
|
||||
|
||||
def __lt__(self, other):
|
||||
self_str = self.data(QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
other_str = other.data(QtCore.Qt.ItemDataRole.DisplayRole)
|
||||
return IssueString(self_str).as_float() < IssueString(other_str).as_float()
|
||||
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
|
||||
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
|
||||
return (IssueString(selfStr).asFloat() <
|
||||
IssueString(otherStr).asFloat())
|
||||
|
||||
|
||||
class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, settings, series_id, issue_number):
|
||||
super().__init__(parent)
|
||||
super(IssueSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("issueselectionwindow.ui"), self)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('issueselectionwindow.ui'), self)
|
||||
|
||||
self.coverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
|
||||
self.coverWidget = CoverImageWidget(
|
||||
self.coverImageContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.coverImageContainer)
|
||||
gridlayout.addWidget(self.coverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
reduce_widget_font_size(self.twList)
|
||||
reduce_widget_font_size(self.teDescription, 1)
|
||||
reduceWidgetFontSize(self.twList)
|
||||
reduceWidgetFontSize(self.teDescription, 1)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.series_id = series_id
|
||||
self.issue_id = None
|
||||
self.settings = settings
|
||||
self.url_fetch_thread = None
|
||||
self.issue_list = []
|
||||
|
||||
if issue_number is None or issue_number == "":
|
||||
self.issue_number = 1
|
||||
@@ -71,11 +73,11 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.issue_number = issue_number
|
||||
|
||||
self.initial_id = None
|
||||
self.perform_query()
|
||||
self.performQuery()
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed)
|
||||
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
# now that the list has been sorted, find the initial record, and
|
||||
# select it
|
||||
@@ -83,25 +85,33 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
issue_id = self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
if issue_id == self.initial_id:
|
||||
issue_id, b = self.twList.item(
|
||||
r, 0).data(QtCore.Qt.UserRole).toInt()
|
||||
if (issue_id == self.initial_id):
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def perform_query(self):
|
||||
def performQuery(self):
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
try:
|
||||
comic_vine = ComicVineTalker()
|
||||
comic_vine.fetch_volume_data(self.series_id)
|
||||
self.issue_list = comic_vine.fetch_issues_by_volume(self.series_id)
|
||||
comicVine = ComicVineTalker()
|
||||
volume_data = comicVine.fetchVolumeData(self.series_id)
|
||||
self.issue_list = comicVine.fetchIssuesByVolume(self.series_id)
|
||||
except ComicVineTalkerException as e:
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
if e.code == ComicVineTalkerException.RateLimit:
|
||||
QtWidgets.QMessageBox.critical(self, "Comic Vine Error", ComicVineTalker.get_rate_limit_message())
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Comic Vine Error"),
|
||||
ComicVineTalker.getRateLimitMessage())
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Network Issue", "Could not connect to Comic Vine to list issues!")
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Network Issue"),
|
||||
self.tr("Could not connect to Comic Vine to list issues!"))
|
||||
return
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
@@ -113,15 +123,15 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
for record in self.issue_list:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record["issue_number"]
|
||||
item_text = record['issue_number']
|
||||
item = IssueNumberTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, record["id"])
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, record['id'])
|
||||
item.setData(QtCore.Qt.DisplayRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = record["cover_date"]
|
||||
item_text = record['cover_date']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
# remove the day of "YYYY-MM-DD"
|
||||
@@ -129,52 +139,52 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
if len(parts) > 1:
|
||||
item_text = parts[0] + "-" + parts[1]
|
||||
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record["name"]
|
||||
item_text = record['name']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if (
|
||||
IssueString(record["issue_number"]).as_string().lower()
|
||||
== IssueString(self.issue_number).as_string().lower()
|
||||
):
|
||||
self.initial_id = record["id"]
|
||||
if IssueString(
|
||||
record['issue_number']).asString().lower() == IssueString(
|
||||
self.issue_number).asString().lower():
|
||||
self.initial_id = record['id']
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.twList.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def cell_double_clicked(self, r, c):
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
|
||||
def current_item_changed(self, curr, prev):
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
self.issue_id, b = self.twList.item(
|
||||
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record["id"] == self.issue_id:
|
||||
self.issue_number = record["issue_number"]
|
||||
self.coverWidget.set_issue_id(int(self.issue_id))
|
||||
if record["description"] is None:
|
||||
if record['id'] == self.issue_id:
|
||||
self.issue_number = record['issue_number']
|
||||
self.coverWidget.setIssueID(int(self.issue_id))
|
||||
if record['description'] is None:
|
||||
self.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(record["description"])
|
||||
self.teDescription.setText(record['description'])
|
||||
|
||||
break
|
||||
|
||||
1
comictaggerlib/issuestring.py
Normal file
1
comictaggerlib/issuestring.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.issuestring import *
|
||||
@@ -14,37 +14,24 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui import qtutils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class LogWindow(QtWidgets.QDialog):
|
||||
class LogWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
super(LogWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("logwindow.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
def set_text(self, text):
|
||||
try:
|
||||
text = text.decode()
|
||||
self.textEdit.setPlainText(text)
|
||||
except AttributeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception("Displaying raw tags failed")
|
||||
qtutils.qt_error("Displaying raw tags failed:", e)
|
||||
def setText(self, text):
|
||||
self.textEdit.setPlainText(text)
|
||||
|
||||
@@ -14,182 +14,74 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import signal
|
||||
import sys
|
||||
import signal
|
||||
import traceback
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from comictaggerlib import cli
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker
|
||||
from comictaggerlib.ctversion import version
|
||||
from comictaggerlib.options import Options
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger("comictagger")
|
||||
logging.getLogger("comicapi").setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
import platform
|
||||
#import os
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
def show_exception_box(log_msg):
|
||||
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
|
||||
If unavailable (non-console application), log an additional notice.
|
||||
"""
|
||||
if QtWidgets.QApplication.instance() is not None:
|
||||
errorbox = QtWidgets.QMessageBox()
|
||||
errorbox.setText(f"Oops. An unexpected error occured:\n{log_msg}")
|
||||
errorbox.exec()
|
||||
QtWidgets.QApplication.exit(1)
|
||||
else:
|
||||
logger.debug("No QApplication instance available.")
|
||||
|
||||
class UncaughtHook(QtCore.QObject):
|
||||
_exception_caught = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# this registers the exception_hook() function as hook with the Python interpreter
|
||||
sys.excepthook = self.exception_hook
|
||||
|
||||
# connect signal to execute the message box function always on main thread
|
||||
self._exception_caught.connect(show_exception_box)
|
||||
|
||||
def exception_hook(self, exc_type, exc_value, exc_traceback):
|
||||
"""Function handling uncaught exceptions.
|
||||
It is triggered each time an uncaught exception occurs.
|
||||
"""
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
# ignore keyboard interrupt to support console applications
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
else:
|
||||
exc_info = (exc_type, exc_value, exc_traceback)
|
||||
log_msg = "\n".join(["".join(traceback.format_tb(exc_traceback)), f"{exc_type.__name__}: {exc_value}"])
|
||||
logger.critical("Uncaught exception: %s: %s", exc_type.__name__, exc_value, exc_info=exc_info)
|
||||
|
||||
# trigger message box show
|
||||
self._exception_caught.emit(log_msg)
|
||||
|
||||
qt_exception_hook = UncaughtHook()
|
||||
from comictaggerlib.taggerwindow import TaggerWindow
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from taggerwindow import TaggerWindow
|
||||
except ImportError as e:
|
||||
logger.error(str(e))
|
||||
qt_available = False
|
||||
|
||||
|
||||
def rotate(handler: logging.handlers.RotatingFileHandler, filename: pathlib.Path):
|
||||
if filename.is_file() and filename.stat().st_size > 0:
|
||||
handler.doRollover()
|
||||
import utils
|
||||
import cli
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options
|
||||
from comicvinetalker import ComicVineTalker
|
||||
|
||||
|
||||
def ctmain():
|
||||
opts = Options()
|
||||
opts.parse_cmd_line_args()
|
||||
SETTINGS = ComicTaggerSettings(opts.config_path)
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
os.makedirs(ComicTaggerSettings.get_settings_folder() / "logs", exist_ok=True)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.WARNING)
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log", encoding="utf-8", backupCount=10
|
||||
)
|
||||
rotate(file_handler, ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log")
|
||||
logging.basicConfig(
|
||||
handlers=[
|
||||
stream_handler,
|
||||
file_handler,
|
||||
],
|
||||
level=logging.WARNING,
|
||||
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
)
|
||||
# Need to load setting before anything else
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
|
||||
# manage the CV API key
|
||||
if opts.cv_api_key:
|
||||
if opts.cv_api_key != SETTINGS.cv_api_key:
|
||||
SETTINGS.cv_api_key = opts.cv_api_key
|
||||
SETTINGS.save()
|
||||
if opts.cv_api_key != settings.cv_api_key:
|
||||
settings.cv_api_key = opts.cv_api_key
|
||||
settings.save()
|
||||
if opts.only_set_key:
|
||||
print("Key set")
|
||||
return
|
||||
|
||||
ComicVineTalker.api_key = SETTINGS.cv_api_key
|
||||
ComicVineTalker.api_key = settings.cv_api_key
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
logger.info(
|
||||
"ComicTagger Version: %s running on: %s PyInstaller: %s",
|
||||
version,
|
||||
platform.system(),
|
||||
"Yes" if getattr(sys, "frozen", None) else "No",
|
||||
)
|
||||
|
||||
logger.debug("Installed Packages")
|
||||
for pkg in sorted(pkg_resources.working_set, key=lambda x: x.project_name):
|
||||
logger.debug("%s\t%s", pkg.project_name, pkg.version)
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print("PyQt5 is not available. ComicTagger is limited to command-line mode.")
|
||||
logger.info("PyQt5 is not available. ComicTagger is limited to command-line mode.")
|
||||
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
|
||||
|
||||
if opts.no_gui:
|
||||
try:
|
||||
cli.cli_mode(opts, SETTINGS)
|
||||
except:
|
||||
logger.exception("CLI mode failed")
|
||||
cli.cli_mode(opts, settings)
|
||||
else:
|
||||
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
|
||||
args = []
|
||||
if opts.darkmode:
|
||||
args.extend(["-platform", "windows:darkmode=2"])
|
||||
args.extend(sys.argv)
|
||||
app = QtWidgets.QApplication(args)
|
||||
if platform.system() == "Darwin":
|
||||
# Set the MacOS dock icon
|
||||
app.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("app.png")))
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# For pure python, tell windows that we're not python,
|
||||
# so we can have our own taskbar icon
|
||||
import ctypes
|
||||
|
||||
myappid = "comictagger" # arbitrary string
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
||||
# force close of console window
|
||||
swp_hidewindow = 0x0080
|
||||
console_wnd = ctypes.windll.kernel32.GetConsoleWindow()
|
||||
if console_wnd != 0:
|
||||
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow)
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.get_graphic("tags.png"))
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
|
||||
|
||||
splash = QtWidgets.QSplashScreen(img)
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(opts.file_list, SETTINGS, opts=opts)
|
||||
tagger_window.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("app.png")))
|
||||
tagger_window = TaggerWindow(opts.file_list, settings, opts=opts)
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish(tagger_window)
|
||||
|
||||
sys.exit(app.exec())
|
||||
except Exception:
|
||||
logger.exception("GUI mode failed")
|
||||
QtWidgets.QMessageBox.critical(
|
||||
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
|
||||
)
|
||||
sys.exit(app.exec_())
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.critical(
|
||||
QtGui.QMainWindow(),
|
||||
"Error",
|
||||
"Unhandled exception in app:\n" +
|
||||
traceback.format_exc())
|
||||
|
||||
@@ -14,66 +14,70 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
#import sys
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#from comicarchive import MetaDataStyle
|
||||
#from comicvinetalker import ComicVineTalker
|
||||
#import utils
|
||||
|
||||
|
||||
class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
class MatchSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, matches, comic_archive):
|
||||
super().__init__(parent)
|
||||
super(MatchSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("matchselectionwindow.ui"), self)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
reduce_widget_font_size(self.twList)
|
||||
reduce_widget_font_size(self.teDescription, 1)
|
||||
reduceWidgetFontSize(self.twList)
|
||||
reduceWidgetFontSize(self.teDescription, 1)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.matches = matches
|
||||
self.comic_archive = comic_archive
|
||||
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed)
|
||||
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
self.update_data()
|
||||
self.updateData()
|
||||
|
||||
def update_data(self):
|
||||
def updateData(self):
|
||||
|
||||
self.set_cover_image()
|
||||
self.populate_table()
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
path = self.comic_archive.path
|
||||
self.setWindowTitle(f"Select correct match: {os.path.split(path)[1]}")
|
||||
self.setWindowTitle(u"Select correct match: {0}".format(
|
||||
os.path.split(path)[1]))
|
||||
|
||||
def populate_table(self):
|
||||
def populateTable(self):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
@@ -84,72 +88,73 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
for match in self.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match["series"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match["publisher"] is not None:
|
||||
item_text = str(match["publisher"])
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = "Unknown"
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
month_str = ""
|
||||
year_str = "????"
|
||||
if match["month"] is not None:
|
||||
month_str = f"-{int(match['month']):02d}"
|
||||
if match["year"] is not None:
|
||||
year_str = str(match["year"])
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match["issue_title"]
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
def cell_double_clicked(self, r, c):
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
|
||||
def current_item_changed(self, curr, prev):
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.altCoverWidget.set_issue_id(self.current_match()["issue_id"])
|
||||
if self.current_match()["description"] is None:
|
||||
self.altCoverWidget.setIssueID(self.currentMatch()['issue_id'])
|
||||
if self.currentMatch()['description'] is None:
|
||||
self.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(self.current_match()["description"])
|
||||
self.teDescription.setText(self.currentMatch()['description'])
|
||||
|
||||
def set_cover_image(self):
|
||||
self.archiveCoverWidget.set_archive(self.comic_archive)
|
||||
def setCoverImage(self):
|
||||
self.archiveCoverWidget.setArchive(self.comic_archive)
|
||||
|
||||
def current_match(self):
|
||||
def currentMatch(self):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
match = self.twList.item(row, 0).data(
|
||||
QtCore.Qt.UserRole).toPyObject()[0]
|
||||
return match
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 dialog to show a message and let the user check a box
|
||||
"""A PyQt4 dialog to show a message and let the user check a box
|
||||
|
||||
Example usage:
|
||||
|
||||
@@ -6,7 +6,7 @@ checked = OptionalMessageDialog.msg(self, "Disclaimer",
|
||||
"This is beta software, and you are using it at your own risk!",
|
||||
)
|
||||
|
||||
said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
|
||||
said_yes, checked = OptionalMessageDialog.question(self, "Question",
|
||||
"Are you sure you wish to do this?",
|
||||
)
|
||||
"""
|
||||
@@ -25,36 +25,34 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
StyleMessage = 0
|
||||
StyleQuestion = 1
|
||||
|
||||
|
||||
class OptionalMessageDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent, style, title, msg, check_state=QtCore.Qt.CheckState.Unchecked, check_text=None):
|
||||
super().__init__(parent)
|
||||
class OptionalMessageDialog(QDialog):
|
||||
|
||||
def __init__(self, parent, style, title, msg,
|
||||
check_state=Qt.Unchecked, check_text=None):
|
||||
QDialog.__init__(self, parent)
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.was_accepted = False
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
self.theLabel = QtWidgets.QLabel(msg)
|
||||
l = QVBoxLayout(self)
|
||||
|
||||
self.theLabel = QLabel(msg)
|
||||
self.theLabel.setWordWrap(True)
|
||||
self.theLabel.setTextFormat(QtCore.Qt.TextFormat.RichText)
|
||||
self.theLabel.setTextFormat(Qt.RichText)
|
||||
self.theLabel.setOpenExternalLinks(True)
|
||||
self.theLabel.setTextInteractionFlags(
|
||||
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
| QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse
|
||||
| QtCore.Qt.TextInteractionFlag.LinksAccessibleByKeyboard
|
||||
)
|
||||
Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
|
||||
layout.addWidget(self.theLabel)
|
||||
layout.insertSpacing(-1, 10)
|
||||
l.addWidget(self.theLabel)
|
||||
l.insertSpacing(-1, 10)
|
||||
|
||||
if check_text is None:
|
||||
if style == StyleQuestion:
|
||||
@@ -62,46 +60,58 @@ class OptionalMessageDialog(QtWidgets.QDialog):
|
||||
else:
|
||||
check_text = "Don't show this message again"
|
||||
|
||||
self.theCheckBox = QtWidgets.QCheckBox(check_text)
|
||||
self.theCheckBox = QCheckBox(check_text)
|
||||
|
||||
self.theCheckBox.setCheckState(check_state)
|
||||
|
||||
layout.addWidget(self.theCheckBox)
|
||||
l.addWidget(self.theCheckBox)
|
||||
|
||||
btnbox_style = QtWidgets.QDialogButtonBox.StandardButton.Ok
|
||||
btnbox_style = QDialogButtonBox.Ok
|
||||
if style == StyleQuestion:
|
||||
btnbox_style = QtWidgets.QDialogButtonBox.StandardButton.Yes | QtWidgets.QDialogButtonBox.StandardButton.No
|
||||
btnbox_style = QDialogButtonBox.Yes | QDialogButtonBox.No
|
||||
|
||||
self.theButtonBox = QtWidgets.QDialogButtonBox(
|
||||
self.theButtonBox = QDialogButtonBox(
|
||||
btnbox_style,
|
||||
parent=self,
|
||||
accepted=self.accept,
|
||||
rejected=self.reject,
|
||||
)
|
||||
rejected=self.reject)
|
||||
|
||||
layout.addWidget(self.theButtonBox)
|
||||
l.addWidget(self.theButtonBox)
|
||||
|
||||
def accept(self):
|
||||
self.was_accepted = True
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
self.was_accepted = False
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QDialog.reject(self)
|
||||
|
||||
@staticmethod
|
||||
def msg(parent, title, msg, check_state=QtCore.Qt.CheckState.Unchecked, check_text=None):
|
||||
def msg(parent, title, msg, check_state=Qt.Unchecked, check_text=None):
|
||||
|
||||
d = OptionalMessageDialog(parent, StyleMessage, title, msg, check_state=check_state, check_text=check_text)
|
||||
d = OptionalMessageDialog(
|
||||
parent,
|
||||
StyleMessage,
|
||||
title,
|
||||
msg,
|
||||
check_state=check_state,
|
||||
check_text=check_text)
|
||||
|
||||
d.exec()
|
||||
d.exec_()
|
||||
return d.theCheckBox.isChecked()
|
||||
|
||||
@staticmethod
|
||||
def question(parent, title, msg, check_state=QtCore.Qt.CheckState.Unchecked, check_text=None):
|
||||
def question(
|
||||
parent, title, msg, check_state=Qt.Unchecked, check_text=None):
|
||||
|
||||
d = OptionalMessageDialog(parent, StyleQuestion, title, msg, check_state=check_state, check_text=check_text)
|
||||
d = OptionalMessageDialog(
|
||||
parent,
|
||||
StyleQuestion,
|
||||
title,
|
||||
msg,
|
||||
check_state=check_state,
|
||||
check_text=check_text)
|
||||
|
||||
d.exec()
|
||||
d.exec_()
|
||||
|
||||
return d.was_accepted, d.theCheckBox.isChecked()
|
||||
|
||||
@@ -14,18 +14,22 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import getopt
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import getopt
|
||||
import platform
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import ctversion
|
||||
try:
|
||||
import argparse
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicarchive import MetaDataStyle
|
||||
from versionchecker import VersionChecker
|
||||
import ctversion
|
||||
import utils
|
||||
|
||||
|
||||
class Options:
|
||||
@@ -96,13 +100,11 @@ If no options are given, {0} will run in windowed mode.
|
||||
error, wait and retry query.
|
||||
-v, --verbose Be noisy when doing what it does.
|
||||
--terse Don't say much (for print mode).
|
||||
--darkmode Windows only. Force a dark pallet
|
||||
--config=CONFIG_DIR Config directory defaults to ~/.ComicTagger
|
||||
--version Display version.
|
||||
-h, --help Display this message.
|
||||
|
||||
For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
"""
|
||||
For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.data_style = None
|
||||
@@ -136,21 +138,18 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.assume_issue_is_one_if_not_set = False
|
||||
self.file_list = []
|
||||
self.darkmode = False
|
||||
self.copy_source = None
|
||||
self.config_path = ""
|
||||
|
||||
def display_msg_and_quit(self, msg, code, show_help=False):
|
||||
appname = os.path.basename(sys.argv[0])
|
||||
if msg is not None:
|
||||
print(msg)
|
||||
if show_help:
|
||||
print((self.help_text.format(appname)))
|
||||
print(self.help_text.format(appname))
|
||||
else:
|
||||
print("For more help, run with '--help'")
|
||||
sys.exit(code)
|
||||
|
||||
def parse_metadata_from_string(self, mdstr):
|
||||
def parseMetadataFromString(self, mdstr):
|
||||
"""The metadata string is a comma separated list of name-value pairs
|
||||
The names match the attributes of the internal metadata struct (for now)
|
||||
The caret is the special "escape character", since it's not common in
|
||||
@@ -165,7 +164,8 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
|
||||
md = GenericMetadata()
|
||||
|
||||
# First, replace escaped commas with with a unique token (to be changed back later)
|
||||
# First, replace escaped commas with with a unique token (to be changed
|
||||
# back later)
|
||||
mdstr = mdstr.replace(escaped_comma, replacement_token)
|
||||
tmp_list = mdstr.split(",")
|
||||
md_list = []
|
||||
@@ -174,7 +174,7 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
md_list.append(item)
|
||||
|
||||
# Now build a nice dict from the list
|
||||
md_dict = {}
|
||||
md_dict = dict()
|
||||
for item in md_list:
|
||||
# Make sure to fix any escaped equal signs
|
||||
i = item.replace(escaped_equals, replacement_token)
|
||||
@@ -184,38 +184,44 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
if key.lower() == "credit":
|
||||
cred_attribs = value.split(":")
|
||||
role = cred_attribs[0]
|
||||
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
|
||||
primary = len(cred_attribs) > 2
|
||||
md.add_credit(person.strip(), role.strip(), primary)
|
||||
person = (cred_attribs[1] if len(cred_attribs) > 1 else "")
|
||||
primary = (cred_attribs[2] if len(cred_attribs) > 2 else None)
|
||||
md.addCredit(
|
||||
person.strip(),
|
||||
role.strip(),
|
||||
True if primary is not None else False)
|
||||
else:
|
||||
md_dict[key] = value
|
||||
|
||||
# Map the dict to the metadata object
|
||||
for key, value in md_dict.items():
|
||||
for key in md_dict:
|
||||
if not hasattr(md, key):
|
||||
logger.warning("'%s' is not a valid tag name", key)
|
||||
print("Warning: '{0}' is not a valid tag name".format(key))
|
||||
else:
|
||||
md.is_empty = False
|
||||
setattr(md, key, value)
|
||||
md.isEmpty = False
|
||||
setattr(md, key, md_dict[key])
|
||||
# print(md)
|
||||
return md
|
||||
|
||||
def launch_script(self, scriptfile):
|
||||
# we were given a script. special case for the args:
|
||||
# 1. ignore everything before the -S,
|
||||
# 2. pass all the ones that follow (including script name) to the script
|
||||
script_args = []
|
||||
# 2. pass all the ones that follow (including script name) to the
|
||||
# script
|
||||
script_args = list()
|
||||
for idx, arg in enumerate(sys.argv):
|
||||
if arg in ["-S", "--script"]:
|
||||
if arg in ['-S', '--script']:
|
||||
# found script!
|
||||
script_args = sys.argv[idx + 1 :]
|
||||
script_args = sys.argv[idx + 1:]
|
||||
break
|
||||
sys.argv = script_args
|
||||
if not os.path.exists(scriptfile):
|
||||
logger.error("Can't find %s", scriptfile)
|
||||
print("Can't find {0}".format(scriptfile))
|
||||
else:
|
||||
# I *think* this makes sense:
|
||||
# assume the base name of the file is the module name
|
||||
# add the folder of the given file to the python path import module
|
||||
# assume the base name of the file is the module name
|
||||
# add the folder of the given file to the python path
|
||||
# import module
|
||||
dirname = os.path.dirname(scriptfile)
|
||||
module_name = os.path.splitext(os.path.basename(scriptfile))[0]
|
||||
sys.path = [dirname] + sys.path
|
||||
@@ -226,23 +232,27 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
if "main" in dir(script):
|
||||
script.main()
|
||||
else:
|
||||
logger.error("Can't find entry point 'main()' in module '%s'", module_name)
|
||||
except Exception:
|
||||
logger.exception("Script: %s raised an unhandled exception: ", module_name)
|
||||
print(
|
||||
"Can't find entry point \"main()\" in module \"{0}\"".format(module_name))
|
||||
except Exception as e:
|
||||
print "Script raised an unhandled exception: ", e
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
def parse_cmd_line_args(self):
|
||||
def parseCmdLineArgs(self):
|
||||
|
||||
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
|
||||
# remove the PSN (process serial number) argument from OS/X
|
||||
if platform.system() == "Darwin" and hasattr(
|
||||
sys, "frozen") and sys.frozen == 1:
|
||||
# remove the PSN ("process serial number") argument from OS/X
|
||||
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a]
|
||||
else:
|
||||
input_args = sys.argv[1:]
|
||||
|
||||
# first check if we're launching a script:
|
||||
for n, _ in enumerate(input_args):
|
||||
if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args):
|
||||
for n in range(len(input_args)):
|
||||
if (input_args[n] in ["-S", "--script"] and
|
||||
n + 1 < len(input_args)):
|
||||
# insert a "--" which will cause getopt to ignore the remaining args
|
||||
# so they will be passed to the script
|
||||
input_args.insert(n + 2, "--")
|
||||
@@ -250,43 +260,15 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt(
|
||||
input_args,
|
||||
"hpdt:fm:vownsrc:ieRS:1",
|
||||
[
|
||||
"help",
|
||||
"print",
|
||||
"delete",
|
||||
"type=",
|
||||
"copy=",
|
||||
"parsefilename",
|
||||
"metadata=",
|
||||
"verbose",
|
||||
"online",
|
||||
"dryrun",
|
||||
"save",
|
||||
"rename",
|
||||
"raw",
|
||||
"noabort",
|
||||
"terse",
|
||||
"nooverwrite",
|
||||
"interactive",
|
||||
"nosummary",
|
||||
"version",
|
||||
"id=",
|
||||
"recursive",
|
||||
"script=",
|
||||
"export-to-zip",
|
||||
"delete-rar",
|
||||
"abort-on-conflict",
|
||||
"assume-issue-one",
|
||||
"cv-api-key=",
|
||||
"only-set-cv-key",
|
||||
"wait-on-cv-rate-limit",
|
||||
"darkmode",
|
||||
"config=",
|
||||
],
|
||||
)
|
||||
opts, args = getopt.getopt(input_args,
|
||||
"hpdt:fm:vownsrc:ieRS:1",
|
||||
["help", "print", "delete", "type=", "copy=", "parsefilename",
|
||||
"metadata=", "verbose", "online", "dryrun", "save", "rename",
|
||||
"raw", "noabort", "terse", "nooverwrite", "interactive",
|
||||
"nosummary", "version", "id=", "recursive", "script=",
|
||||
"export-to-zip", "delete-rar", "abort-on-conflict",
|
||||
"assume-issue-one", "cv-api-key=", "only-set-cv-key",
|
||||
"wait-on-cv-rate-limit"])
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_msg_and_quit(str(err), 2)
|
||||
@@ -310,7 +292,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
self.interactive = True
|
||||
if o in ("-c", "--copy"):
|
||||
self.copy_tags = True
|
||||
|
||||
if a.lower() == "cr":
|
||||
self.copy_source = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
@@ -318,13 +299,14 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
elif a.lower() == "comet":
|
||||
self.copy_source = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit("Invalid copy tag source type", 1)
|
||||
self.display_msg_and_quit(
|
||||
"Invalid copy tag source type", 1)
|
||||
if o in ("-o", "--online"):
|
||||
self.search_online = True
|
||||
if o in ("-n", "--dryrun"):
|
||||
self.dryrun = True
|
||||
if o in ("-m", "--metadata"):
|
||||
self.metadata = self.parse_metadata_from_string(a)
|
||||
self.metadata = self.parseMetadataFromString(a)
|
||||
if o in ("-s", "--save"):
|
||||
self.save_tags = True
|
||||
if o in ("-r", "--rename"):
|
||||
@@ -339,8 +321,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
self.parse_filename = True
|
||||
if o in ("-w", "--wait-on-cv-rate-limit"):
|
||||
self.wait_and_retry_on_rate_limit = True
|
||||
if o == "--config":
|
||||
self.config_path = os.path.abspath(a)
|
||||
if o == "--id":
|
||||
self.issue_id = a
|
||||
if o == "--raw":
|
||||
@@ -360,8 +340,10 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
if o == "--only-set-cv-key":
|
||||
self.only_set_key = True
|
||||
if o == "--version":
|
||||
print(f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team")
|
||||
print("Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
|
||||
print(
|
||||
"ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(ctversion.version))
|
||||
print(
|
||||
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
@@ -372,20 +354,8 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
self.data_style = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit("Invalid tag type", 1)
|
||||
if o == "--darkmode":
|
||||
self.darkmode = True
|
||||
|
||||
if any(
|
||||
[
|
||||
self.print_tags,
|
||||
self.delete_tags,
|
||||
self.save_tags,
|
||||
self.copy_tags,
|
||||
self.rename_file,
|
||||
self.export_to_zip,
|
||||
self.only_set_key,
|
||||
]
|
||||
):
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip or self.only_set_key:
|
||||
self.no_gui = True
|
||||
|
||||
count = 0
|
||||
@@ -408,8 +378,8 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
|
||||
if count > 1:
|
||||
self.display_msg_and_quit(
|
||||
"Must choose only one action of print, delete, save, copy, rename, export, set key, or run script", 1
|
||||
)
|
||||
"Must choose only one action of print, delete, save, copy, rename, export, set key, or run script",
|
||||
1)
|
||||
|
||||
if self.script is not None:
|
||||
self.launch_script(self.script)
|
||||
@@ -418,7 +388,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
if platform.system() == "Windows":
|
||||
# no globbing on windows shell, so do it for them
|
||||
import glob
|
||||
|
||||
self.file_list = []
|
||||
for item in args:
|
||||
self.file_list.extend(glob.glob(item))
|
||||
@@ -431,17 +400,25 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
|
||||
if self.only_set_key and self.cv_api_key is None:
|
||||
self.display_msg_and_quit("Key not given!", 1)
|
||||
|
||||
if not self.only_set_key and self.no_gui and self.filename is None:
|
||||
self.display_msg_and_quit("Command requires at least one filename!", 1)
|
||||
if (self.only_set_key == False) and self.no_gui and (
|
||||
self.filename is None):
|
||||
self.display_msg_and_quit(
|
||||
"Command requires at least one filename!", 1)
|
||||
|
||||
if self.delete_tags and self.data_style is None:
|
||||
self.display_msg_and_quit("Please specify the type to delete with -t", 1)
|
||||
self.display_msg_and_quit(
|
||||
"Please specify the type to delete with -t", 1)
|
||||
|
||||
if self.save_tags and self.data_style is None:
|
||||
self.display_msg_and_quit("Please specify the type to save with -t", 1)
|
||||
self.display_msg_and_quit(
|
||||
"Please specify the type to save with -t", 1)
|
||||
|
||||
if self.copy_tags and self.data_style is None:
|
||||
self.display_msg_and_quit("Please specify the type to copy to with -t", 1)
|
||||
self.display_msg_and_quit(
|
||||
"Please specify the type to copy to with -t", 1)
|
||||
|
||||
# if self.rename_file and self.data_style is None:
|
||||
# self.display_msg_and_quit("Please specify the type to use for renaming with -t", 1)
|
||||
|
||||
if self.recursive:
|
||||
self.file_list = utils.get_recursive_filelist(self.file_list)
|
||||
|
||||
@@ -14,53 +14,51 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import platform
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
|
||||
class PageBrowserWindow(QtWidgets.QDialog):
|
||||
class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, metadata):
|
||||
super().__init__(parent)
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("pagebrowser.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui'), self)
|
||||
|
||||
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
|
||||
self.pageWidget = CoverImageWidget(
|
||||
self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.pageContainer)
|
||||
gridlayout.addWidget(self.pageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Close).setDefault(True)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Close).setDefault(True)
|
||||
if platform.system() == "Darwin":
|
||||
self.btnPrev.setText("<<")
|
||||
self.btnNext.setText(">>")
|
||||
else:
|
||||
self.btnPrev.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("left.png")))
|
||||
self.btnNext.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("right.png")))
|
||||
self.btnPrev.setIcon(
|
||||
QtGui.QIcon(ComicTaggerSettings.getGraphic('left.png')))
|
||||
self.btnNext.setIcon(
|
||||
QtGui.QIcon(ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
self.btnNext.clicked.connect(self.next_page)
|
||||
self.btnPrev.clicked.connect(self.prev_page)
|
||||
self.btnNext.clicked.connect(self.nextPage)
|
||||
self.btnPrev.clicked.connect(self.prevPage)
|
||||
self.show()
|
||||
|
||||
self.btnNext.setEnabled(False)
|
||||
@@ -76,39 +74,41 @@ class PageBrowserWindow(QtWidgets.QDialog):
|
||||
self.btnPrev.setEnabled(False)
|
||||
self.pageWidget.clear()
|
||||
|
||||
def set_comic_archive(self, ca: ComicArchive):
|
||||
def setComicArchive(self, ca):
|
||||
|
||||
self.comic_archive = ca
|
||||
self.page_count = ca.get_number_of_pages()
|
||||
self.page_count = ca.getNumberOfPages()
|
||||
self.current_page_num = 0
|
||||
self.pageWidget.set_archive(self.comic_archive)
|
||||
self.set_page()
|
||||
self.pageWidget.setArchive(self.comic_archive)
|
||||
self.setPage()
|
||||
|
||||
if self.page_count > 1:
|
||||
self.btnNext.setEnabled(True)
|
||||
self.btnPrev.setEnabled(True)
|
||||
|
||||
def next_page(self):
|
||||
def nextPage(self):
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.current_page_num += 1
|
||||
else:
|
||||
self.current_page_num = 0
|
||||
self.set_page()
|
||||
self.setPage()
|
||||
|
||||
def prev_page(self):
|
||||
def prevPage(self):
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.current_page_num -= 1
|
||||
else:
|
||||
self.current_page_num = self.page_count - 1
|
||||
self.set_page()
|
||||
self.setPage()
|
||||
|
||||
def set_page(self):
|
||||
def setPage(self):
|
||||
if self.metadata is not None:
|
||||
archive_page_index = self.metadata.get_archive_page_index(self.current_page_num)
|
||||
archive_page_index = self.metadata.getArchivePageIndex(
|
||||
self.current_page_num)
|
||||
else:
|
||||
archive_page_index = self.current_page_num
|
||||
|
||||
self.pageWidget.set_page(archive_page_index)
|
||||
self.setWindowTitle(f"Page Browser - Page {self.current_page_num + 1} (of {self.page_count}) ")
|
||||
self.pageWidget.setPage(archive_page_index)
|
||||
self.setWindowTitle(
|
||||
"Page Browser - Page {0} (of {1}) ".format(self.current_page_num + 1, self.page_count))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 widget for editing the page list info"""
|
||||
"""A PyQt4 widget for editing the page list info"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
@@ -14,44 +14,49 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
#import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.genericmetadata import PageType
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
#from pageloader import PageLoader
|
||||
|
||||
|
||||
def item_move_events(widget):
|
||||
class Filter(QtCore.QObject):
|
||||
def itemMoveEvents(widget):
|
||||
|
||||
mysignal = QtCore.pyqtSignal(str)
|
||||
class Filter(QObject):
|
||||
|
||||
mysignal = pyqtSignal(str)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QtCore.QEvent.Type.ChildRemoved:
|
||||
# print(event.type())
|
||||
if event.type() == QEvent.ChildRemoved:
|
||||
# print("ChildRemoved")
|
||||
self.mysignal.emit("finish")
|
||||
if event.type() == QtCore.QEvent.Type.ChildAdded:
|
||||
if event.type() == QEvent.ChildAdded:
|
||||
# print("ChildAdded")
|
||||
self.mysignal.emit("start")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filt = Filter(widget)
|
||||
widget.installEventFilter(filt)
|
||||
return filt.mysignal
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.mysignal
|
||||
|
||||
|
||||
class PageListEditor(QtWidgets.QWidget):
|
||||
firstFrontCoverChanged = QtCore.pyqtSignal(int)
|
||||
listOrderChanged = QtCore.pyqtSignal()
|
||||
modified = QtCore.pyqtSignal()
|
||||
class PageListEditor(QWidget):
|
||||
|
||||
firstFrontCoverChanged = pyqtSignal(int)
|
||||
listOrderChanged = pyqtSignal()
|
||||
modified = pyqtSignal()
|
||||
|
||||
pageTypeNames = {
|
||||
PageType.FrontCover: "Front Cover",
|
||||
@@ -68,328 +73,209 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
}
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("pagelisteditor.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui'), self)
|
||||
|
||||
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
|
||||
self.pageWidget = CoverImageWidget(
|
||||
self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QGridLayout(self.pageContainer)
|
||||
gridlayout.addWidget(self.pageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.reset_page()
|
||||
self.resetPage()
|
||||
|
||||
# Add the entries to the page type combobox
|
||||
self.add_page_type_item("", "", "Alt+0", False)
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.FrontCover], PageType.FrontCover, "Alt+F")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.InnerCover], PageType.InnerCover, "Alt+I")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Advertisement], PageType.Advertisement, "Alt+A")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Roundup], PageType.Roundup, "Alt+R")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Story], PageType.Story, "Alt+S")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Editorial], PageType.Editorial, "Alt+E")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Letters], PageType.Letters, "Alt+L")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Preview], PageType.Preview, "Alt+P")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.BackCover], PageType.BackCover, "Alt+B")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Other], PageType.Other, "Alt+O")
|
||||
self.add_page_type_item(self.pageTypeNames[PageType.Deleted], PageType.Deleted, "Alt+X")
|
||||
# Add the entries to the manga combobox
|
||||
self.comboBox.addItem("", "")
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Roundup], PageType.Roundup)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Story], PageType.Story)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Editorial], PageType.Editorial)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Letters], PageType.Letters)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Preview], PageType.Preview)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.BackCover], PageType.BackCover)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Other], PageType.Other)
|
||||
self.comboBox.addItem(
|
||||
self.pageTypeNames[PageType.Deleted], PageType.Deleted)
|
||||
|
||||
self.listWidget.itemSelectionChanged.connect(self.change_page)
|
||||
item_move_events(self.listWidget).connect(self.item_move_event)
|
||||
self.cbPageType.activated.connect(self.change_page_type)
|
||||
self.chkDoublePage.toggled.connect(self.toggle_double_page)
|
||||
self.leBookmark.editingFinished.connect(self.save_bookmark)
|
||||
self.btnUp.clicked.connect(self.move_current_up)
|
||||
self.btnDown.clicked.connect(self.move_current_down)
|
||||
self.listWidget.itemSelectionChanged.connect(self.changePage)
|
||||
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
|
||||
self.comboBox.activated.connect(self.changePageType)
|
||||
self.btnUp.clicked.connect(self.moveCurrentUp)
|
||||
self.btnDown.clicked.connect(self.moveCurrentDown)
|
||||
self.pre_move_row = -1
|
||||
self.first_front_page = None
|
||||
|
||||
self.comic_archive: Optional[ComicArchive] = None
|
||||
self.pages_list = []
|
||||
|
||||
def reset_page(self):
|
||||
def resetPage(self):
|
||||
self.pageWidget.clear()
|
||||
self.cbPageType.setDisabled(True)
|
||||
self.chkDoublePage.setDisabled(True)
|
||||
self.leBookmark.setDisabled(True)
|
||||
self.comboBox.setDisabled(True)
|
||||
self.comic_archive = None
|
||||
self.pages_list = []
|
||||
self.pages_list = None
|
||||
|
||||
def add_page_type_item(self, text, user_data, shortcut, show_shortcut=True):
|
||||
if show_shortcut:
|
||||
text = text + " (" + shortcut + ")"
|
||||
self.cbPageType.addItem(text, user_data)
|
||||
actionItem = QtWidgets.QAction(
|
||||
shortcut, self, triggered=lambda: self.select_page_type_item(self.cbPageType.findData(user_data))
|
||||
)
|
||||
actionItem.setShortcut(shortcut)
|
||||
self.addAction(actionItem)
|
||||
|
||||
def select_page_type_item(self, idx):
|
||||
if self.cbPageType.isEnabled():
|
||||
self.cbPageType.setCurrentIndex(idx)
|
||||
self.change_page_type(idx)
|
||||
|
||||
def get_new_indexes(self, movement):
|
||||
selection = self.listWidget.selectionModel().selectedRows()
|
||||
selection.sort(reverse=movement > 0)
|
||||
newindexes = []
|
||||
oldindexes = []
|
||||
for x in selection:
|
||||
current = x.row()
|
||||
oldindexes.append(current)
|
||||
if 0 <= current + movement <= self.listWidget.count() - 1:
|
||||
if len(newindexes) < 1 or current + movement != newindexes[-1]:
|
||||
current += movement
|
||||
|
||||
newindexes.append(current)
|
||||
oldindexes.sort()
|
||||
newindexes.sort()
|
||||
return list(zip(newindexes, oldindexes))
|
||||
|
||||
def set_selection(self, indexes):
|
||||
selection_ranges = []
|
||||
first = 0
|
||||
for i, selection in enumerate(indexes):
|
||||
if i == 0:
|
||||
first = selection[0]
|
||||
continue
|
||||
|
||||
if selection != indexes[i - 1][0] + 1:
|
||||
selection_ranges.append((first, indexes[i - 1][0]))
|
||||
first = selection[0]
|
||||
|
||||
selection_ranges.append((first, indexes[-1][0]))
|
||||
selection = QtCore.QItemSelection()
|
||||
for x in selection_ranges:
|
||||
selection.merge(
|
||||
QtCore.QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)),
|
||||
QtCore.QItemSelectionModel.SelectionFlag.Select,
|
||||
)
|
||||
|
||||
self.listWidget.selectionModel().select(selection, QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect)
|
||||
return selection_ranges
|
||||
|
||||
def move_current_up(self):
|
||||
def moveCurrentUp(self):
|
||||
row = self.listWidget.currentRow()
|
||||
selection = self.get_new_indexes(-1)
|
||||
for sel in selection:
|
||||
item = self.listWidget.takeItem(sel[1])
|
||||
self.listWidget.insertItem(sel[0], item)
|
||||
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row - 1, item)
|
||||
self.listWidget.setCurrentRow(row - 1)
|
||||
self.set_selection(selection)
|
||||
self.listOrderChanged.emit()
|
||||
self.emit_front_cover_change()
|
||||
self.modified.emit()
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def move_current_down(self):
|
||||
def moveCurrentDown(self):
|
||||
row = self.listWidget.currentRow()
|
||||
selection = self.get_new_indexes(1)
|
||||
selection.sort(reverse=True)
|
||||
for sel in selection:
|
||||
item = self.listWidget.takeItem(sel[1])
|
||||
self.listWidget.insertItem(sel[0], item)
|
||||
|
||||
if row < self.listWidget.count() - 1:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row + 1, item)
|
||||
self.listWidget.setCurrentRow(row + 1)
|
||||
self.listOrderChanged.emit()
|
||||
self.emit_front_cover_change()
|
||||
self.set_selection(selection)
|
||||
self.modified.emit()
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def item_move_event(self, s):
|
||||
def itemMoveEvent(self, s):
|
||||
# print "move event: ", s, self.listWidget.currentRow()
|
||||
if s == "start":
|
||||
self.pre_move_row = self.listWidget.currentRow()
|
||||
if s == "finish":
|
||||
if self.pre_move_row != self.listWidget.currentRow():
|
||||
self.listOrderChanged.emit()
|
||||
self.emit_front_cover_change()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def change_page_type(self, i):
|
||||
new_type = self.cbPageType.itemData(i)
|
||||
if self.get_current_page_type() != new_type:
|
||||
self.set_current_page_type(new_type)
|
||||
self.emit_front_cover_change()
|
||||
def changePageType(self, i):
|
||||
new_type = self.comboBox.itemData(i).toString()
|
||||
if self.getCurrentPageType() != new_type:
|
||||
self.setCurrentPageType(new_type)
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def change_page(self):
|
||||
def changePage(self):
|
||||
row = self.listWidget.currentRow()
|
||||
pagetype = self.get_current_page_type()
|
||||
pagetype = self.getCurrentPageType()
|
||||
|
||||
i = self.cbPageType.findData(pagetype)
|
||||
self.cbPageType.setCurrentIndex(i)
|
||||
i = self.comboBox.findData(pagetype)
|
||||
self.comboBox.setCurrentIndex(i)
|
||||
|
||||
self.chkDoublePage.setChecked("DoublePage" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0])
|
||||
|
||||
if "Bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]:
|
||||
self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["Bookmark"])
|
||||
else:
|
||||
self.leBookmark.setText("")
|
||||
|
||||
idx = int(self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]["Image"])
|
||||
#idx = int(str (self.listWidget.item(row).text()))
|
||||
idx = int(self.listWidget.item(row).data(
|
||||
Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.pageWidget.set_archive(self.comic_archive, idx)
|
||||
self.pageWidget.setArchive(self.comic_archive, idx)
|
||||
|
||||
def get_first_front_cover(self):
|
||||
front_cover = 0
|
||||
def getFirstFrontCover(self):
|
||||
frontCover = 0
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
page_dict = item.data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
if "Type" in page_dict and page_dict["Type"] == PageType.FrontCover:
|
||||
front_cover = int(page_dict["Image"])
|
||||
page_dict = item.data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict and page_dict[
|
||||
'Type'] == PageType.FrontCover:
|
||||
frontCover = int(page_dict['Image'])
|
||||
break
|
||||
return front_cover
|
||||
return frontCover
|
||||
|
||||
def get_current_page_type(self):
|
||||
def getCurrentPageType(self):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
if "Type" in page_dict:
|
||||
return page_dict["Type"]
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict:
|
||||
return page_dict['Type']
|
||||
else:
|
||||
return ""
|
||||
|
||||
return ""
|
||||
|
||||
def set_current_page_type(self, t):
|
||||
def setCurrentPageType(self, t):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
|
||||
|
||||
if t == "":
|
||||
if "Type" in page_dict:
|
||||
del page_dict["Type"]
|
||||
if 'Type' in page_dict:
|
||||
del(page_dict['Type'])
|
||||
else:
|
||||
page_dict["Type"] = t
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
# wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, (page_dict,))
|
||||
item.setText(self.list_entry_text(page_dict))
|
||||
|
||||
def toggle_double_page(self):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
|
||||
|
||||
if self.sender().isChecked():
|
||||
page_dict["DoublePage"] = str("true")
|
||||
elif "DoublePage" in page_dict:
|
||||
del page_dict["DoublePage"]
|
||||
self.modified.emit()
|
||||
page_dict['Type'] = str(t)
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(QtCore.Qt.UserRole, (page_dict,))
|
||||
item.setText(self.list_entry_text(page_dict))
|
||||
item.setData(Qt.UserRole, (page_dict,))
|
||||
item.setText(self.listEntryText(page_dict))
|
||||
|
||||
self.listWidget.setFocus()
|
||||
|
||||
def save_bookmark(self):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
|
||||
|
||||
current_bookmark = ""
|
||||
if "Bookmark" in page_dict:
|
||||
current_bookmark = page_dict["Bookmark"]
|
||||
|
||||
if self.leBookmark.text().strip():
|
||||
new_bookmark = str(self.leBookmark.text().strip())
|
||||
if current_bookmark != new_bookmark:
|
||||
page_dict["Bookmark"] = new_bookmark
|
||||
self.modified.emit()
|
||||
elif current_bookmark != "":
|
||||
del page_dict["Bookmark"]
|
||||
self.modified.emit()
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(QtCore.Qt.UserRole, (page_dict,))
|
||||
item.setText(self.list_entry_text(page_dict))
|
||||
|
||||
self.listWidget.setFocus()
|
||||
|
||||
def set_data(self, comic_archive: ComicArchive, pages_list: list):
|
||||
def setData(self, comic_archive, pages_list):
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
if pages_list is not None and len(pages_list) > 0:
|
||||
self.cbPageType.setDisabled(False)
|
||||
self.chkDoublePage.setDisabled(False)
|
||||
self.leBookmark.setDisabled(False)
|
||||
self.comboBox.setDisabled(False)
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect(self.change_page)
|
||||
self.listWidget.itemSelectionChanged.disconnect(self.changePage)
|
||||
|
||||
self.listWidget.clear()
|
||||
for p in pages_list:
|
||||
item = QtWidgets.QListWidgetItem(self.list_entry_text(p))
|
||||
# wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, (p,))
|
||||
item = QListWidgetItem(self.listEntryText(p))
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (p,))
|
||||
|
||||
self.listWidget.addItem(item)
|
||||
self.first_front_page = self.get_first_front_cover()
|
||||
self.listWidget.itemSelectionChanged.connect(self.change_page)
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.listWidget.itemSelectionChanged.connect(self.changePage)
|
||||
self.listWidget.setCurrentRow(0)
|
||||
|
||||
def list_entry_text(self, page_dict):
|
||||
text = str(int(page_dict["Image"]) + 1)
|
||||
if "Type" in page_dict:
|
||||
if page_dict["Type"] in self.pageTypeNames:
|
||||
text += " (" + self.pageTypeNames[page_dict["Type"]] + ")"
|
||||
else:
|
||||
text += " (Error: " + page_dict["Type"] + ")"
|
||||
if "DoublePage" in page_dict:
|
||||
text += " " + "\U00002461"
|
||||
if "Bookmark" in page_dict:
|
||||
text += " " + "\U0001F516"
|
||||
def listEntryText(self, page_dict):
|
||||
text = str(int(page_dict['Image']) + 1)
|
||||
if 'Type' in page_dict:
|
||||
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
|
||||
return text
|
||||
|
||||
def get_page_list(self):
|
||||
def getPageList(self):
|
||||
page_list = []
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
page_list.append(item.data(QtCore.Qt.ItemDataRole.UserRole)[0])
|
||||
page_list.append(item.data(Qt.UserRole).toPyObject()[0])
|
||||
return page_list
|
||||
|
||||
def emit_front_cover_change(self):
|
||||
if self.first_front_page != self.get_first_front_cover():
|
||||
self.first_front_page = self.get_first_front_cover()
|
||||
def emitFrontCoverChange(self):
|
||||
if self.first_front_page != self.getFirstFrontCover():
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.firstFrontCoverChanged.emit(self.first_front_page)
|
||||
|
||||
def set_metadata_style(self, data_style):
|
||||
def setMetadataStyle(self, data_style):
|
||||
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
active_palette = self.cbPageType.palette()
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.comboBox.palette()
|
||||
|
||||
inactive_palette3 = self.cbPageType.palette()
|
||||
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
inactive_palette3 = self.comboBox.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
|
||||
if data_style == MetaDataStyle.CIX:
|
||||
self.btnUp.setEnabled(True)
|
||||
self.btnDown.setEnabled(True)
|
||||
self.cbPageType.setEnabled(True)
|
||||
self.chkDoublePage.setEnabled(True)
|
||||
self.leBookmark.setEnabled(True)
|
||||
self.comboBox.setEnabled(True)
|
||||
self.listWidget.setEnabled(True)
|
||||
|
||||
self.leBookmark.setPalette(active_palette)
|
||||
self.listWidget.setPalette(active_palette)
|
||||
|
||||
elif data_style == MetaDataStyle.CBI:
|
||||
self.btnUp.setEnabled(False)
|
||||
self.btnDown.setEnabled(False)
|
||||
self.cbPageType.setEnabled(False)
|
||||
self.chkDoublePage.setEnabled(False)
|
||||
self.leBookmark.setEnabled(False)
|
||||
self.comboBox.setEnabled(False)
|
||||
self.listWidget.setEnabled(False)
|
||||
|
||||
self.leBookmark.setPalette(inactive_palette3)
|
||||
self.listWidget.setPalette(inactive_palette3)
|
||||
|
||||
elif data_style == MetaDataStyle.COMET:
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
# make sure combo is disabled when no list
|
||||
if self.comic_archive is None:
|
||||
self.cbPageType.setEnabled(False)
|
||||
self.chkDoublePage.setEnabled(False)
|
||||
self.leBookmark.setEnabled(False)
|
||||
self.comboBox.setEnabled(False)
|
||||
|
||||
@@ -14,16 +14,16 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from comictaggerlib.ui.qtutils import getQImageFromData
|
||||
#from comicarchive import ComicArchive
|
||||
#import utils
|
||||
|
||||
|
||||
class PageLoader(QtCore.QThread):
|
||||
|
||||
"""
|
||||
This class holds onto a reference of each instance in a list since
|
||||
problems occur if the ref count goes to zero and the GC tries to reap
|
||||
@@ -32,36 +32,39 @@ class PageLoader(QtCore.QThread):
|
||||
"abandoned", and no signals will be issued.
|
||||
"""
|
||||
|
||||
loadComplete = QtCore.pyqtSignal(bytes)
|
||||
loadComplete = pyqtSignal(QtGui.QImage)
|
||||
|
||||
instanceList = []
|
||||
mutex = QtCore.QMutex()
|
||||
|
||||
# Remove all finished threads from the list
|
||||
@staticmethod
|
||||
def reap_instances():
|
||||
def reapInstances():
|
||||
for obj in reversed(PageLoader.instanceList):
|
||||
if obj.isFinished():
|
||||
PageLoader.instanceList.remove(obj)
|
||||
|
||||
def __init__(self, ca: ComicArchive, page_num):
|
||||
def __init__(self, ca, page_num):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.ca: ComicArchive = ca
|
||||
self.page_num: int = page_num
|
||||
self.ca = ca
|
||||
self.page_num = page_num
|
||||
self.abandoned = False
|
||||
|
||||
# remove any old instances, and then add ourself
|
||||
PageLoader.mutex.lock()
|
||||
PageLoader.reap_instances()
|
||||
PageLoader.reapInstances()
|
||||
PageLoader.instanceList.append(self)
|
||||
PageLoader.mutex.unlock()
|
||||
|
||||
def run(self):
|
||||
image_data = self.ca.get_page(self.page_num)
|
||||
image_data = self.ca.getPage(self.page_num)
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
if image_data is not None:
|
||||
img = getQImageFromData(image_data)
|
||||
|
||||
if self.abandoned:
|
||||
return
|
||||
self.loadComplete.emit(image_data)
|
||||
|
||||
self.loadComplete.emit(img)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 dialog to show ID log and progress"""
|
||||
"""A PyQT4 dialog to show ID log and progress"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
@@ -14,29 +14,25 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
import logging
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
from settings import ComicTaggerSettings
|
||||
#import utils
|
||||
|
||||
|
||||
class IDProgressWindow(QtWidgets.QDialog):
|
||||
class IDProgressWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
super(IDProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("progresswindow.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
reduce_widget_font_size(self.textEdit)
|
||||
reduceWidgetFontSize(self.textEdit)
|
||||
|
||||
@@ -14,55 +14,49 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
import comicapi.comicarchive
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comictaggerlib.filerenamer import FileRenamer
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.settingswindow import SettingsWindow
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from comicarchive import MetaDataStyle
|
||||
import utils
|
||||
|
||||
|
||||
class RenameWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent, comic_archive_list: List[comicapi.comicarchive.ComicArchive], data_style, settings):
|
||||
super().__init__(parent)
|
||||
class RenameWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("renamewindow.ui"), self)
|
||||
self.label.setText(f"Preview (based on {MetaDataStyle.name[data_style]} tags):")
|
||||
def __init__(self, parent, comic_archive_list, data_style, settings):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui'), self)
|
||||
self.label.setText(
|
||||
"Preview (based on {0} tags):".format(
|
||||
MetaDataStyle.name[data_style]))
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
|
||||
self.btnSettings.clicked.connect(self.modifySettings)
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def configRenamer(self):
|
||||
self.renamer = FileRenamer(None)
|
||||
self.renamer.setTemplate(self.settings.rename_template)
|
||||
self.renamer.setIssueZeroPadding(
|
||||
self.settings.rename_issue_number_padding)
|
||||
self.renamer.setSmartCleanup(
|
||||
self.settings.rename_use_smart_string_cleanup)
|
||||
|
||||
def doPreview(self):
|
||||
self.rename_list = []
|
||||
|
||||
self.btnSettings.clicked.connect(self.modify_settings)
|
||||
self.renamer = FileRenamer(None, platform="universal" if self.settings.rename_strict else "auto")
|
||||
|
||||
self.config_renamer()
|
||||
self.do_preview()
|
||||
|
||||
def config_renamer(self):
|
||||
self.renamer.set_template(self.settings.rename_template)
|
||||
self.renamer.set_issue_zero_padding(self.settings.rename_issue_number_padding)
|
||||
self.renamer.set_smart_cleanup(self.settings.rename_use_smart_string_cleanup)
|
||||
|
||||
def do_preview(self):
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
@@ -72,65 +66,46 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
|
||||
new_ext = None # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if ca.is_sevenzip():
|
||||
new_ext = ".cb7"
|
||||
elif ca.is_zip():
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.is_rar():
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
md = ca.read_metadata(self.data_style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
)
|
||||
self.renamer.set_metadata(md)
|
||||
self.renamer.move = self.settings.rename_move_dir
|
||||
|
||||
try:
|
||||
new_name = self.renamer.determine_name(new_ext)
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Invalid format string!",
|
||||
"Your rename template is invalid!"
|
||||
f"<br/><br/>{e}<br/><br/>"
|
||||
"Please consult the template help in the "
|
||||
"settings and the documentation on the format at "
|
||||
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
|
||||
"https://docs.python.org/3/library/string.html#format-string-syntax</a>",
|
||||
)
|
||||
return
|
||||
md = ca.readMetadata(self.data_style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
||||
self.renamer.setMetadata(md)
|
||||
new_name = self.renamer.determineName(ca.path, ext=new_ext)
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow(row)
|
||||
folder_item = QtWidgets.QTableWidgetItem()
|
||||
old_name_item = QtWidgets.QTableWidgetItem()
|
||||
new_name_item = QtWidgets.QTableWidgetItem()
|
||||
folder_item = QtGui.QTableWidgetItem()
|
||||
old_name_item = QtGui.QTableWidgetItem()
|
||||
new_name_item = QtGui.QTableWidgetItem()
|
||||
|
||||
item_text = os.path.split(ca.path)[0]
|
||||
folder_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
folder_item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, folder_item)
|
||||
folder_item.setText(item_text)
|
||||
folder_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
folder_item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
|
||||
item_text = os.path.split(ca.path)[1]
|
||||
old_name_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
old_name_item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, old_name_item)
|
||||
old_name_item.setText(item_text)
|
||||
old_name_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
old_name_item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
|
||||
new_name_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
new_name_item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, new_name_item)
|
||||
new_name_item.setText(new_name)
|
||||
new_name_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, new_name)
|
||||
new_name_item.setData(QtCore.Qt.ToolTipRole, new_name)
|
||||
|
||||
dict_item = {}
|
||||
dict_item["archive"] = ca
|
||||
dict_item["new_name"] = new_name
|
||||
dict_item = dict()
|
||||
dict_item['archive'] = ca
|
||||
dict_item['new_name'] = new_name
|
||||
self.rename_list.append(dict_item)
|
||||
|
||||
# Adjust column sizes
|
||||
@@ -142,54 +117,47 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modify_settings(self):
|
||||
def modifySettings(self):
|
||||
settingswin = SettingsWindow(self, self.settings)
|
||||
settingswin.setModal(True)
|
||||
settingswin.show_rename_tab()
|
||||
settingswin.exec()
|
||||
settingswin.showRenameTab()
|
||||
settingswin.exec_()
|
||||
if settingswin.result():
|
||||
self.config_renamer()
|
||||
self.do_preview()
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def accept(self):
|
||||
|
||||
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
|
||||
prog_dialog.setWindowTitle("Renaming Archives")
|
||||
prog_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
prog_dialog.setMinimumDuration(100)
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
progdialog = QtGui.QProgressDialog(
|
||||
"", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog.setWindowTitle("Renaming Archives")
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
for idx, item in enumerate(self.rename_list):
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
idx += 1
|
||||
prog_dialog.setValue(idx)
|
||||
prog_dialog.setLabelText(item["new_name"])
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
progdialog.setLabelText(item['new_name'])
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(item["archive"].path))
|
||||
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
|
||||
folder = self.settings.rename_dir.strip()
|
||||
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
|
||||
|
||||
if os.path.join(folder, item["new_name"]) == item["archive"].path:
|
||||
logger.info(item["new_name"], "Filename is already good!")
|
||||
if item['new_name'] == os.path.basename(item['archive'].path):
|
||||
print item['new_name'], "Filename is already good!"
|
||||
continue
|
||||
|
||||
if not item["archive"].is_writable(check_rar_status=False):
|
||||
if not item['archive'].isWritable(check_rar_status=False):
|
||||
continue
|
||||
|
||||
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
|
||||
os.rename(item["archive"].path, new_abs_path)
|
||||
folder = os.path.dirname(os.path.abspath(item['archive'].path))
|
||||
new_abs_path = utils.unique_file(
|
||||
os.path.join(folder, item['new_name']))
|
||||
|
||||
item["archive"].rename(new_abs_path)
|
||||
os.rename(item['archive'].path, new_abs_path)
|
||||
|
||||
prog_dialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
item['archive'].rename(new_abs_path)
|
||||
|
||||
QtWidgets.QDialog.accept(self)
|
||||
progdialog.close()
|
||||
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from typing import List, TypedDict
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
|
||||
|
||||
class IssueResult(TypedDict):
|
||||
series: str
|
||||
distance: int
|
||||
issue_number: str
|
||||
cv_issue_count: int
|
||||
url_image_hash: str
|
||||
issue_title: str
|
||||
issue_id: str # int?
|
||||
volume_id: str # int?
|
||||
month: int
|
||||
year: int
|
||||
publisher: str
|
||||
image_url: str
|
||||
thumb_url: str
|
||||
page_url: str
|
||||
description: str
|
||||
|
||||
|
||||
class OnlineMatchResults:
|
||||
def __init__(self):
|
||||
self.good_matches: List[str] = []
|
||||
self.no_matches: List[str] = []
|
||||
self.multiple_matches: List[MultipleMatch] = []
|
||||
self.low_confidence_matches: List[MultipleMatch] = []
|
||||
self.write_failures: List[str] = []
|
||||
self.fetch_data_failures: List[str] = []
|
||||
|
||||
|
||||
class MultipleMatch:
|
||||
def __init__(self, ca: ComicArchive, match_list: List[IssueResult]):
|
||||
self.ca: ComicArchive = ca
|
||||
self.matches = match_list
|
||||
@@ -14,53 +14,64 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import sys
|
||||
import configparser
|
||||
import platform
|
||||
import codecs
|
||||
import uuid
|
||||
|
||||
from comicapi import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import utils
|
||||
|
||||
|
||||
class ComicTaggerSettings:
|
||||
folder = ""
|
||||
|
||||
@staticmethod
|
||||
def get_settings_folder():
|
||||
if not ComicTaggerSettings.folder:
|
||||
if platform.system() == "Windows":
|
||||
ComicTaggerSettings.folder = os.path.join(os.environ["APPDATA"], "ComicTagger")
|
||||
else:
|
||||
ComicTaggerSettings.folder = os.path.join(os.path.expanduser("~"), ".ComicTagger")
|
||||
return pathlib.Path(ComicTaggerSettings.folder)
|
||||
def getSettingsFolder():
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
if platform.system() == "Windows":
|
||||
folder = os.path.join(os.environ['APPDATA'], 'ComicTagger')
|
||||
else:
|
||||
folder = os.path.join(os.path.expanduser('~'), '.ComicTagger')
|
||||
if folder is not None:
|
||||
folder = folder.decode(filename_encoding)
|
||||
return folder
|
||||
|
||||
frozen_win_exe_path = None
|
||||
|
||||
@staticmethod
|
||||
def base_dir():
|
||||
if getattr(sys, "frozen", None):
|
||||
return sys._MEIPASS
|
||||
|
||||
return pathlib.Path(__file__).parent
|
||||
def baseDir():
|
||||
if getattr(sys, 'frozen', None):
|
||||
if platform.system() == "Darwin":
|
||||
return sys._MEIPASS
|
||||
else: # Windows
|
||||
# Preserve this value, in case sys.argv gets changed importing
|
||||
# a plugin script
|
||||
if ComicTaggerSettings.frozen_win_exe_path is None:
|
||||
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname(
|
||||
os.path.abspath(sys.argv[0]))
|
||||
return ComicTaggerSettings.frozen_win_exe_path
|
||||
else:
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@staticmethod
|
||||
def get_graphic(filename):
|
||||
graphic_folder = pathlib.Path(os.path.join(ComicTaggerSettings.base_dir(), "graphics"))
|
||||
def getGraphic(filename):
|
||||
graphic_folder = os.path.join(
|
||||
ComicTaggerSettings.baseDir(), 'graphics')
|
||||
return os.path.join(graphic_folder, filename)
|
||||
|
||||
@staticmethod
|
||||
def get_ui_file(filename):
|
||||
ui_folder = os.path.join(ComicTaggerSettings.base_dir(), "ui")
|
||||
def getUIFile(filename):
|
||||
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), 'ui')
|
||||
return os.path.join(ui_folder, filename)
|
||||
|
||||
def set_default_values(self):
|
||||
def setDefaultValues(self):
|
||||
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.unrar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
self.check_for_new_version = False
|
||||
self.check_for_new_version = True
|
||||
self.send_usage_stats = False
|
||||
|
||||
# automatic settings
|
||||
@@ -79,19 +90,17 @@ class ComicTaggerSettings:
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_filter = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
self.dont_notify_about_this_version = ""
|
||||
self.ask_about_usage_stats = True
|
||||
self.show_no_unrar_warning = True
|
||||
|
||||
# filename parsing settings
|
||||
self.complicated_parser = False
|
||||
self.remove_c2c = False
|
||||
self.remove_fcbd = False
|
||||
self.remove_publisher = False
|
||||
self.parse_scan_info = True
|
||||
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
@@ -99,10 +108,6 @@ class ComicTaggerSettings:
|
||||
self.remove_html_tables = False
|
||||
self.cv_api_key = ""
|
||||
|
||||
self.sort_series_by_year = True
|
||||
self.exact_series_matches_first = True
|
||||
self.always_use_publisher_filter = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
@@ -120,9 +125,6 @@ class ComicTaggerSettings:
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
self.rename_dir = ""
|
||||
self.rename_move_dir = False
|
||||
self.rename_strict = False
|
||||
|
||||
# Auto-tag stickies
|
||||
self.save_on_low_confidence = False
|
||||
@@ -132,92 +134,19 @@ class ComicTaggerSettings:
|
||||
self.remove_archive_after_successful_match = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
|
||||
def __init__(self, folder):
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
self.check_for_new_version = False
|
||||
self.send_usage_stats = False
|
||||
def __init__(self):
|
||||
|
||||
# automatic settings
|
||||
self.install_id = uuid.uuid4().hex
|
||||
self.last_selected_save_data_style = 0
|
||||
self.last_selected_load_data_style = 0
|
||||
self.last_opened_folder = ""
|
||||
self.last_main_window_width = 0
|
||||
self.last_main_window_height = 0
|
||||
self.last_main_window_x = 0
|
||||
self.last_main_window_y = 0
|
||||
self.last_form_side_width = -1
|
||||
self.last_list_side_width = -1
|
||||
self.last_filelist_sorted_column = -1
|
||||
self.last_filelist_sorted_order = 0
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_filter = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
self.dont_notify_about_this_version = ""
|
||||
self.ask_about_usage_stats = True
|
||||
|
||||
# filename parsing settings
|
||||
self.complicated_parser = False
|
||||
self.remove_c2c = False
|
||||
self.remove_fcbd = False
|
||||
self.remove_publisher = False
|
||||
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
self.clear_form_before_populating_from_cv = False
|
||||
self.remove_html_tables = False
|
||||
self.cv_api_key = ""
|
||||
|
||||
self.sort_series_by_year = True
|
||||
self.exact_series_matches_first = True
|
||||
self.always_use_publisher_filter = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
self.copy_characters_to_tags = False
|
||||
self.copy_teams_to_tags = False
|
||||
self.copy_locations_to_tags = False
|
||||
self.copy_storyarcs_to_tags = False
|
||||
self.copy_notes_to_comments = False
|
||||
self.copy_weblink_to_comments = False
|
||||
self.apply_cbl_transform_on_cv_import = False
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
|
||||
# Rename settings
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
self.rename_dir = ""
|
||||
self.rename_move_dir = False
|
||||
self.rename_strict = False
|
||||
|
||||
# Auto-tag stickies
|
||||
self.save_on_low_confidence = False
|
||||
self.dont_use_year_when_identifying = False
|
||||
self.assume_1_if_no_issue_num = False
|
||||
self.ignore_leading_numbers_in_filename = False
|
||||
self.remove_archive_after_successful_match = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.settings_file = ""
|
||||
self.folder = ""
|
||||
self.setDefaultValues()
|
||||
|
||||
self.config = configparser.RawConfigParser()
|
||||
if folder:
|
||||
ComicTaggerSettings.folder = pathlib.Path(folder)
|
||||
else:
|
||||
ComicTaggerSettings.folder = ComicTaggerSettings.get_settings_folder()
|
||||
self.folder = ComicTaggerSettings.getSettingsFolder()
|
||||
|
||||
if not os.path.exists(ComicTaggerSettings.folder):
|
||||
os.makedirs(ComicTaggerSettings.folder)
|
||||
if not os.path.exists(self.folder):
|
||||
os.makedirs(self.folder)
|
||||
|
||||
self.settings_file = os.path.join(ComicTaggerSettings.folder, "settings")
|
||||
self.settings_file = os.path.join(self.folder, "settings")
|
||||
|
||||
# if config file doesn't exist, write one out
|
||||
if not os.path.exists(self.settings_file):
|
||||
@@ -225,264 +154,369 @@ class ComicTaggerSettings:
|
||||
else:
|
||||
self.load()
|
||||
|
||||
# take a crack at finding rar exe, if not set already
|
||||
# take a crack at finding rar exes, if not set already
|
||||
if self.rar_exe_path == "":
|
||||
if platform.system() == "Windows":
|
||||
# look in some likely places for Windows machines
|
||||
if os.path.exists(r"C:\Program Files\WinRAR\Rar.exe"):
|
||||
self.rar_exe_path = r"C:\Program Files\WinRAR\Rar.exe"
|
||||
elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"):
|
||||
self.rar_exe_path = r"C:\Program Files (x86)\WinRAR\Rar.exe"
|
||||
if os.path.exists("C:\Program Files\WinRAR\Rar.exe"):
|
||||
self.rar_exe_path = "C:\Program Files\WinRAR\Rar.exe"
|
||||
elif os.path.exists("C:\Program Files (x86)\WinRAR\Rar.exe"):
|
||||
self.rar_exe_path = "C:\Program Files (x86)\WinRAR\Rar.exe"
|
||||
else:
|
||||
# see if it's in the path of unix user
|
||||
if utils.which("rar") is not None:
|
||||
self.rar_exe_path = utils.which("rar")
|
||||
if self.rar_exe_path != "":
|
||||
self.save()
|
||||
if self.rar_exe_path != "":
|
||||
# make sure rar program is now in the path for the rar class
|
||||
utils.add_to_path(os.path.dirname(self.rar_exe_path))
|
||||
|
||||
if self.unrar_exe_path == "":
|
||||
if platform.system() != "Windows":
|
||||
# see if it's in the path of unix user
|
||||
if utils.which("unrar") is not None:
|
||||
self.unrar_exe_path = utils.which("unrar")
|
||||
if self.unrar_exe_path != "":
|
||||
self.save()
|
||||
|
||||
# make sure unrar/rar programs are now in the path for the UnRAR class to
|
||||
# use
|
||||
utils.addtopath(os.path.dirname(self.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.rar_exe_path))
|
||||
|
||||
def reset(self):
|
||||
os.unlink(self.settings_file)
|
||||
self.__init__(ComicTaggerSettings.folder)
|
||||
self.__init__()
|
||||
|
||||
def load(self):
|
||||
|
||||
def readline_generator(f):
|
||||
line = f.readline()
|
||||
while line:
|
||||
yield line
|
||||
line = f.readline()
|
||||
|
||||
with open(self.settings_file, "r", encoding="utf-8") as f:
|
||||
self.config.read_file(readline_generator(f))
|
||||
#self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
|
||||
self.config.read_file(
|
||||
readline_generator(codecs.open(self.settings_file, "r", "utf8")))
|
||||
|
||||
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
|
||||
if self.config.has_option("settings", "check_for_new_version"):
|
||||
self.check_for_new_version = self.config.getboolean("settings", "check_for_new_version")
|
||||
if self.config.has_option("settings", "send_usage_stats"):
|
||||
self.send_usage_stats = self.config.getboolean("settings", "send_usage_stats")
|
||||
self.rar_exe_path = self.config.get('settings', 'rar_exe_path')
|
||||
self.unrar_exe_path = self.config.get('settings', 'unrar_exe_path')
|
||||
if self.config.has_option('settings', 'check_for_new_version'):
|
||||
self.check_for_new_version = self.config.getboolean(
|
||||
'settings', 'check_for_new_version')
|
||||
if self.config.has_option('settings', 'send_usage_stats'):
|
||||
self.send_usage_stats = self.config.getboolean(
|
||||
'settings', 'send_usage_stats')
|
||||
|
||||
if self.config.has_option("auto", "install_id"):
|
||||
self.install_id = self.config.get("auto", "install_id")
|
||||
if self.config.has_option("auto", "last_selected_load_data_style"):
|
||||
self.last_selected_load_data_style = self.config.getint("auto", "last_selected_load_data_style")
|
||||
if self.config.has_option("auto", "last_selected_save_data_style"):
|
||||
self.last_selected_save_data_style = self.config.getint("auto", "last_selected_save_data_style")
|
||||
if self.config.has_option("auto", "last_opened_folder"):
|
||||
self.last_opened_folder = self.config.get("auto", "last_opened_folder")
|
||||
if self.config.has_option("auto", "last_main_window_width"):
|
||||
self.last_main_window_width = self.config.getint("auto", "last_main_window_width")
|
||||
if self.config.has_option("auto", "last_main_window_height"):
|
||||
self.last_main_window_height = self.config.getint("auto", "last_main_window_height")
|
||||
if self.config.has_option("auto", "last_main_window_x"):
|
||||
self.last_main_window_x = self.config.getint("auto", "last_main_window_x")
|
||||
if self.config.has_option("auto", "last_main_window_y"):
|
||||
self.last_main_window_y = self.config.getint("auto", "last_main_window_y")
|
||||
if self.config.has_option("auto", "last_form_side_width"):
|
||||
self.last_form_side_width = self.config.getint("auto", "last_form_side_width")
|
||||
if self.config.has_option("auto", "last_list_side_width"):
|
||||
self.last_list_side_width = self.config.getint("auto", "last_list_side_width")
|
||||
if self.config.has_option("auto", "last_filelist_sorted_column"):
|
||||
self.last_filelist_sorted_column = self.config.getint("auto", "last_filelist_sorted_column")
|
||||
if self.config.has_option("auto", "last_filelist_sorted_order"):
|
||||
self.last_filelist_sorted_order = self.config.getint("auto", "last_filelist_sorted_order")
|
||||
if self.config.has_option('auto', 'install_id'):
|
||||
self.install_id = self.config.get('auto', 'install_id')
|
||||
if self.config.has_option('auto', 'last_selected_load_data_style'):
|
||||
self.last_selected_load_data_style = self.config.getint(
|
||||
'auto', 'last_selected_load_data_style')
|
||||
if self.config.has_option('auto', 'last_selected_save_data_style'):
|
||||
self.last_selected_save_data_style = self.config.getint(
|
||||
'auto', 'last_selected_save_data_style')
|
||||
if self.config.has_option('auto', 'last_opened_folder'):
|
||||
self.last_opened_folder = self.config.get(
|
||||
'auto', 'last_opened_folder')
|
||||
if self.config.has_option('auto', 'last_main_window_width'):
|
||||
self.last_main_window_width = self.config.getint(
|
||||
'auto', 'last_main_window_width')
|
||||
if self.config.has_option('auto', 'last_main_window_height'):
|
||||
self.last_main_window_height = self.config.getint(
|
||||
'auto', 'last_main_window_height')
|
||||
if self.config.has_option('auto', 'last_main_window_x'):
|
||||
self.last_main_window_x = self.config.getint(
|
||||
'auto', 'last_main_window_x')
|
||||
if self.config.has_option('auto', 'last_main_window_y'):
|
||||
self.last_main_window_y = self.config.getint(
|
||||
'auto', 'last_main_window_y')
|
||||
if self.config.has_option('auto', 'last_form_side_width'):
|
||||
self.last_form_side_width = self.config.getint(
|
||||
'auto', 'last_form_side_width')
|
||||
if self.config.has_option('auto', 'last_list_side_width'):
|
||||
self.last_list_side_width = self.config.getint(
|
||||
'auto', 'last_list_side_width')
|
||||
if self.config.has_option('auto', 'last_filelist_sorted_column'):
|
||||
self.last_filelist_sorted_column = self.config.getint(
|
||||
'auto', 'last_filelist_sorted_column')
|
||||
if self.config.has_option('auto', 'last_filelist_sorted_order'):
|
||||
self.last_filelist_sorted_order = self.config.getint(
|
||||
'auto', 'last_filelist_sorted_order')
|
||||
|
||||
if self.config.has_option("identifier", "id_length_delta_thresh"):
|
||||
self.id_length_delta_thresh = self.config.getint("identifier", "id_length_delta_thresh")
|
||||
if self.config.has_option("identifier", "id_publisher_filter"):
|
||||
self.id_publisher_filter = self.config.get("identifier", "id_publisher_filter")
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint(
|
||||
'identifier', 'id_length_delta_thresh')
|
||||
if self.config.has_option('identifier', 'id_publisher_blacklist'):
|
||||
self.id_publisher_blacklist = self.config.get(
|
||||
'identifier', 'id_publisher_blacklist')
|
||||
|
||||
if self.config.has_option("filenameparser", "complicated_parser"):
|
||||
self.complicated_parser = self.config.getboolean("filenameparser", "complicated_parser")
|
||||
if self.config.has_option("filenameparser", "remove_c2c"):
|
||||
self.remove_c2c = self.config.getboolean("filenameparser", "remove_c2c")
|
||||
if self.config.has_option("filenameparser", "remove_fcbd"):
|
||||
self.remove_fcbd = self.config.getboolean("filenameparser", "remove_fcbd")
|
||||
if self.config.has_option("filenameparser", "remove_publisher"):
|
||||
self.remove_publisher = self.config.getboolean("filenameparser", "remove_publisher")
|
||||
if self.config.has_option('filenameparser', 'parse_scan_info'):
|
||||
self.parse_scan_info = self.config.getboolean(
|
||||
'filenameparser', 'parse_scan_info')
|
||||
|
||||
if self.config.has_option("dialogflags", "ask_about_cbi_in_rar"):
|
||||
self.ask_about_cbi_in_rar = self.config.getboolean("dialogflags", "ask_about_cbi_in_rar")
|
||||
if self.config.has_option("dialogflags", "show_disclaimer"):
|
||||
self.show_disclaimer = self.config.getboolean("dialogflags", "show_disclaimer")
|
||||
if self.config.has_option("dialogflags", "dont_notify_about_this_version"):
|
||||
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
|
||||
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
|
||||
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
|
||||
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
|
||||
self.ask_about_cbi_in_rar = self.config.getboolean(
|
||||
'dialogflags', 'ask_about_cbi_in_rar')
|
||||
if self.config.has_option('dialogflags', 'show_disclaimer'):
|
||||
self.show_disclaimer = self.config.getboolean(
|
||||
'dialogflags', 'show_disclaimer')
|
||||
if self.config.has_option(
|
||||
'dialogflags', 'dont_notify_about_this_version'):
|
||||
self.dont_notify_about_this_version = self.config.get(
|
||||
'dialogflags', 'dont_notify_about_this_version')
|
||||
if self.config.has_option('dialogflags', 'ask_about_usage_stats'):
|
||||
self.ask_about_usage_stats = self.config.getboolean(
|
||||
'dialogflags', 'ask_about_usage_stats')
|
||||
if self.config.has_option('dialogflags', 'show_no_unrar_warning'):
|
||||
self.show_no_unrar_warning = self.config.getboolean(
|
||||
'dialogflags', 'show_no_unrar_warning')
|
||||
|
||||
if self.config.has_option("comicvine", "use_series_start_as_volume"):
|
||||
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
|
||||
if self.config.has_option("comicvine", "clear_form_before_populating_from_cv"):
|
||||
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
|
||||
self.use_series_start_as_volume = self.config.getboolean(
|
||||
'comicvine', 'use_series_start_as_volume')
|
||||
if self.config.has_option(
|
||||
'comicvine', 'clear_form_before_populating_from_cv'):
|
||||
self.clear_form_before_populating_from_cv = self.config.getboolean(
|
||||
"comicvine", "clear_form_before_populating_from_cv"
|
||||
)
|
||||
if self.config.has_option("comicvine", "remove_html_tables"):
|
||||
self.remove_html_tables = self.config.getboolean("comicvine", "remove_html_tables")
|
||||
'comicvine', 'clear_form_before_populating_from_cv')
|
||||
if self.config.has_option('comicvine', 'remove_html_tables'):
|
||||
self.remove_html_tables = self.config.getboolean(
|
||||
'comicvine', 'remove_html_tables')
|
||||
if self.config.has_option('comicvine', 'cv_api_key'):
|
||||
self.cv_api_key = self.config.get('comicvine', 'cv_api_key')
|
||||
|
||||
if self.config.has_option("comicvine", "sort_series_by_year"):
|
||||
self.sort_series_by_year = self.config.getboolean("comicvine", "sort_series_by_year")
|
||||
if self.config.has_option("comicvine", "exact_series_matches_first"):
|
||||
self.exact_series_matches_first = self.config.getboolean("comicvine", "exact_series_matches_first")
|
||||
if self.config.has_option("comicvine", "always_use_publisher_filter"):
|
||||
self.always_use_publisher_filter = self.config.getboolean("comicvine", "always_use_publisher_filter")
|
||||
|
||||
if self.config.has_option("comicvine", "cv_api_key"):
|
||||
self.cv_api_key = self.config.get("comicvine", "cv_api_key")
|
||||
|
||||
if self.config.has_option("cbl_transform", "assume_lone_credit_is_primary"):
|
||||
if self.config.has_option(
|
||||
'cbl_transform', 'assume_lone_credit_is_primary'):
|
||||
self.assume_lone_credit_is_primary = self.config.getboolean(
|
||||
"cbl_transform", "assume_lone_credit_is_primary"
|
||||
)
|
||||
if self.config.has_option("cbl_transform", "copy_characters_to_tags"):
|
||||
self.copy_characters_to_tags = self.config.getboolean("cbl_transform", "copy_characters_to_tags")
|
||||
if self.config.has_option("cbl_transform", "copy_teams_to_tags"):
|
||||
self.copy_teams_to_tags = self.config.getboolean("cbl_transform", "copy_teams_to_tags")
|
||||
if self.config.has_option("cbl_transform", "copy_locations_to_tags"):
|
||||
self.copy_locations_to_tags = self.config.getboolean("cbl_transform", "copy_locations_to_tags")
|
||||
if self.config.has_option("cbl_transform", "copy_notes_to_comments"):
|
||||
self.copy_notes_to_comments = self.config.getboolean("cbl_transform", "copy_notes_to_comments")
|
||||
if self.config.has_option("cbl_transform", "copy_storyarcs_to_tags"):
|
||||
self.copy_storyarcs_to_tags = self.config.getboolean("cbl_transform", "copy_storyarcs_to_tags")
|
||||
if self.config.has_option("cbl_transform", "copy_weblink_to_comments"):
|
||||
self.copy_weblink_to_comments = self.config.getboolean("cbl_transform", "copy_weblink_to_comments")
|
||||
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_cv_import"):
|
||||
'cbl_transform', 'assume_lone_credit_is_primary')
|
||||
if self.config.has_option('cbl_transform', 'copy_characters_to_tags'):
|
||||
self.copy_characters_to_tags = self.config.getboolean(
|
||||
'cbl_transform', 'copy_characters_to_tags')
|
||||
if self.config.has_option('cbl_transform', 'copy_teams_to_tags'):
|
||||
self.copy_teams_to_tags = self.config.getboolean(
|
||||
'cbl_transform', 'copy_teams_to_tags')
|
||||
if self.config.has_option('cbl_transform', 'copy_locations_to_tags'):
|
||||
self.copy_locations_to_tags = self.config.getboolean(
|
||||
'cbl_transform', 'copy_locations_to_tags')
|
||||
if self.config.has_option('cbl_transform', 'copy_notes_to_comments'):
|
||||
self.copy_notes_to_comments = self.config.getboolean(
|
||||
'cbl_transform', 'copy_notes_to_comments')
|
||||
if self.config.has_option('cbl_transform', 'copy_storyarcs_to_tags'):
|
||||
self.copy_storyarcs_to_tags = self.config.getboolean(
|
||||
'cbl_transform', 'copy_storyarcs_to_tags')
|
||||
if self.config.has_option('cbl_transform', 'copy_weblink_to_comments'):
|
||||
self.copy_weblink_to_comments = self.config.getboolean(
|
||||
'cbl_transform', 'copy_weblink_to_comments')
|
||||
if self.config.has_option(
|
||||
'cbl_transform', 'apply_cbl_transform_on_cv_import'):
|
||||
self.apply_cbl_transform_on_cv_import = self.config.getboolean(
|
||||
"cbl_transform", "apply_cbl_transform_on_cv_import"
|
||||
)
|
||||
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_bulk_operation"):
|
||||
'cbl_transform', 'apply_cbl_transform_on_cv_import')
|
||||
if self.config.has_option(
|
||||
'cbl_transform', 'apply_cbl_transform_on_bulk_operation'):
|
||||
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean(
|
||||
"cbl_transform", "apply_cbl_transform_on_bulk_operation"
|
||||
)
|
||||
'cbl_transform',
|
||||
'apply_cbl_transform_on_bulk_operation')
|
||||
|
||||
if self.config.has_option("rename", "rename_template"):
|
||||
self.rename_template = self.config.get("rename", "rename_template")
|
||||
if self.config.has_option("rename", "rename_issue_number_padding"):
|
||||
self.rename_issue_number_padding = self.config.getint("rename", "rename_issue_number_padding")
|
||||
if self.config.has_option("rename", "rename_use_smart_string_cleanup"):
|
||||
self.rename_use_smart_string_cleanup = self.config.getboolean("rename", "rename_use_smart_string_cleanup")
|
||||
if self.config.has_option("rename", "rename_extension_based_on_archive"):
|
||||
if self.config.has_option('rename', 'rename_template'):
|
||||
self.rename_template = self.config.get('rename', 'rename_template')
|
||||
if self.config.has_option('rename', 'rename_issue_number_padding'):
|
||||
self.rename_issue_number_padding = self.config.getint(
|
||||
'rename', 'rename_issue_number_padding')
|
||||
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
|
||||
self.rename_use_smart_string_cleanup = self.config.getboolean(
|
||||
'rename', 'rename_use_smart_string_cleanup')
|
||||
if self.config.has_option(
|
||||
'rename', 'rename_extension_based_on_archive'):
|
||||
self.rename_extension_based_on_archive = self.config.getboolean(
|
||||
"rename", "rename_extension_based_on_archive"
|
||||
)
|
||||
if self.config.has_option("rename", "rename_dir"):
|
||||
self.rename_dir = self.config.get("rename", "rename_dir")
|
||||
if self.config.has_option("rename", "rename_move_dir"):
|
||||
self.rename_move_dir = self.config.getboolean("rename", "rename_move_dir")
|
||||
if self.config.has_option("rename", "rename_strict"):
|
||||
self.rename_strict = self.config.getboolean("rename", "rename_strict")
|
||||
'rename', 'rename_extension_based_on_archive')
|
||||
|
||||
if self.config.has_option("autotag", "save_on_low_confidence"):
|
||||
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
|
||||
if self.config.has_option("autotag", "dont_use_year_when_identifying"):
|
||||
self.dont_use_year_when_identifying = self.config.getboolean("autotag", "dont_use_year_when_identifying")
|
||||
if self.config.has_option("autotag", "assume_1_if_no_issue_num"):
|
||||
self.assume_1_if_no_issue_num = self.config.getboolean("autotag", "assume_1_if_no_issue_num")
|
||||
if self.config.has_option("autotag", "ignore_leading_numbers_in_filename"):
|
||||
if self.config.has_option('autotag', 'save_on_low_confidence'):
|
||||
self.save_on_low_confidence = self.config.getboolean(
|
||||
'autotag', 'save_on_low_confidence')
|
||||
if self.config.has_option('autotag', 'dont_use_year_when_identifying'):
|
||||
self.dont_use_year_when_identifying = self.config.getboolean(
|
||||
'autotag', 'dont_use_year_when_identifying')
|
||||
if self.config.has_option('autotag', 'assume_1_if_no_issue_num'):
|
||||
self.assume_1_if_no_issue_num = self.config.getboolean(
|
||||
'autotag', 'assume_1_if_no_issue_num')
|
||||
if self.config.has_option(
|
||||
'autotag', 'ignore_leading_numbers_in_filename'):
|
||||
self.ignore_leading_numbers_in_filename = self.config.getboolean(
|
||||
"autotag", "ignore_leading_numbers_in_filename"
|
||||
)
|
||||
if self.config.has_option("autotag", "remove_archive_after_successful_match"):
|
||||
'autotag', 'ignore_leading_numbers_in_filename')
|
||||
if self.config.has_option(
|
||||
'autotag', 'remove_archive_after_successful_match'):
|
||||
self.remove_archive_after_successful_match = self.config.getboolean(
|
||||
"autotag", "remove_archive_after_successful_match"
|
||||
)
|
||||
if self.config.has_option("autotag", "wait_and_retry_on_rate_limit"):
|
||||
self.wait_and_retry_on_rate_limit = self.config.getboolean("autotag", "wait_and_retry_on_rate_limit")
|
||||
'autotag',
|
||||
'remove_archive_after_successful_match')
|
||||
if self.config.has_option('autotag', 'wait_and_retry_on_rate_limit'):
|
||||
self.wait_and_retry_on_rate_limit = self.config.getboolean(
|
||||
'autotag', 'wait_and_retry_on_rate_limit')
|
||||
|
||||
def save(self):
|
||||
|
||||
if not self.config.has_section("settings"):
|
||||
self.config.add_section("settings")
|
||||
if not self.config.has_section('settings'):
|
||||
self.config.add_section('settings')
|
||||
|
||||
self.config.set("settings", "check_for_new_version", self.check_for_new_version)
|
||||
self.config.set("settings", "rar_exe_path", self.rar_exe_path)
|
||||
self.config.set("settings", "send_usage_stats", self.send_usage_stats)
|
||||
|
||||
if not self.config.has_section("auto"):
|
||||
self.config.add_section("auto")
|
||||
|
||||
self.config.set("auto", "install_id", self.install_id)
|
||||
self.config.set("auto", "last_selected_load_data_style", self.last_selected_load_data_style)
|
||||
self.config.set("auto", "last_selected_save_data_style", self.last_selected_save_data_style)
|
||||
self.config.set("auto", "last_opened_folder", self.last_opened_folder)
|
||||
self.config.set("auto", "last_main_window_width", self.last_main_window_width)
|
||||
self.config.set("auto", "last_main_window_height", self.last_main_window_height)
|
||||
self.config.set("auto", "last_main_window_x", self.last_main_window_x)
|
||||
self.config.set("auto", "last_main_window_y", self.last_main_window_y)
|
||||
self.config.set("auto", "last_form_side_width", self.last_form_side_width)
|
||||
self.config.set("auto", "last_list_side_width", self.last_list_side_width)
|
||||
self.config.set("auto", "last_filelist_sorted_column", self.last_filelist_sorted_column)
|
||||
self.config.set("auto", "last_filelist_sorted_order", self.last_filelist_sorted_order)
|
||||
|
||||
if not self.config.has_section("identifier"):
|
||||
self.config.add_section("identifier")
|
||||
|
||||
self.config.set("identifier", "id_length_delta_thresh", self.id_length_delta_thresh)
|
||||
self.config.set("identifier", "id_publisher_filter", self.id_publisher_filter)
|
||||
|
||||
if not self.config.has_section("dialogflags"):
|
||||
self.config.add_section("dialogflags")
|
||||
|
||||
self.config.set("dialogflags", "ask_about_cbi_in_rar", self.ask_about_cbi_in_rar)
|
||||
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
|
||||
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
|
||||
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
|
||||
|
||||
if not self.config.has_section("filenameparser"):
|
||||
self.config.add_section("filenameparser")
|
||||
|
||||
self.config.set("filenameparser", "complicated_parser", self.complicated_parser)
|
||||
self.config.set("filenameparser", "remove_c2c", self.remove_c2c)
|
||||
self.config.set("filenameparser", "remove_fcbd", self.remove_fcbd)
|
||||
self.config.set("filenameparser", "remove_publisher", self.remove_publisher)
|
||||
|
||||
if not self.config.has_section("comicvine"):
|
||||
self.config.add_section("comicvine")
|
||||
|
||||
self.config.set("comicvine", "use_series_start_as_volume", self.use_series_start_as_volume)
|
||||
self.config.set("comicvine", "clear_form_before_populating_from_cv", self.clear_form_before_populating_from_cv)
|
||||
self.config.set("comicvine", "remove_html_tables", self.remove_html_tables)
|
||||
|
||||
self.config.set("comicvine", "sort_series_by_year", self.sort_series_by_year)
|
||||
self.config.set("comicvine", "exact_series_matches_first", self.exact_series_matches_first)
|
||||
self.config.set("comicvine", "always_use_publisher_filter", self.always_use_publisher_filter)
|
||||
|
||||
self.config.set("comicvine", "cv_api_key", self.cv_api_key)
|
||||
|
||||
if not self.config.has_section("cbl_transform"):
|
||||
self.config.add_section("cbl_transform")
|
||||
|
||||
self.config.set("cbl_transform", "assume_lone_credit_is_primary", self.assume_lone_credit_is_primary)
|
||||
self.config.set("cbl_transform", "copy_characters_to_tags", self.copy_characters_to_tags)
|
||||
self.config.set("cbl_transform", "copy_teams_to_tags", self.copy_teams_to_tags)
|
||||
self.config.set("cbl_transform", "copy_locations_to_tags", self.copy_locations_to_tags)
|
||||
self.config.set("cbl_transform", "copy_storyarcs_to_tags", self.copy_storyarcs_to_tags)
|
||||
self.config.set("cbl_transform", "copy_notes_to_comments", self.copy_notes_to_comments)
|
||||
self.config.set("cbl_transform", "copy_weblink_to_comments", self.copy_weblink_to_comments)
|
||||
self.config.set("cbl_transform", "apply_cbl_transform_on_cv_import", self.apply_cbl_transform_on_cv_import)
|
||||
self.config.set(
|
||||
"cbl_transform", "apply_cbl_transform_on_bulk_operation", self.apply_cbl_transform_on_bulk_operation
|
||||
)
|
||||
'settings', 'check_for_new_version', self.check_for_new_version)
|
||||
self.config.set('settings', 'rar_exe_path', self.rar_exe_path)
|
||||
self.config.set('settings', 'unrar_exe_path', self.unrar_exe_path)
|
||||
self.config.set('settings', 'send_usage_stats', self.send_usage_stats)
|
||||
|
||||
if not self.config.has_section("rename"):
|
||||
self.config.add_section("rename")
|
||||
if not self.config.has_section('auto'):
|
||||
self.config.add_section('auto')
|
||||
|
||||
self.config.set("rename", "rename_template", self.rename_template)
|
||||
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
|
||||
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
|
||||
self.config.set("rename", "rename_extension_based_on_archive", self.rename_extension_based_on_archive)
|
||||
self.config.set("rename", "rename_dir", self.rename_dir)
|
||||
self.config.set("rename", "rename_move_dir", self.rename_move_dir)
|
||||
self.config.set("rename", "rename_strict", self.rename_strict)
|
||||
self.config.set('auto', 'install_id', self.install_id)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_selected_load_data_style',
|
||||
self.last_selected_load_data_style)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_selected_save_data_style',
|
||||
self.last_selected_save_data_style)
|
||||
self.config.set('auto', 'last_opened_folder', self.last_opened_folder)
|
||||
self.config.set(
|
||||
'auto', 'last_main_window_width', self.last_main_window_width)
|
||||
self.config.set(
|
||||
'auto', 'last_main_window_height', self.last_main_window_height)
|
||||
self.config.set('auto', 'last_main_window_x', self.last_main_window_x)
|
||||
self.config.set('auto', 'last_main_window_y', self.last_main_window_y)
|
||||
self.config.set(
|
||||
'auto', 'last_form_side_width', self.last_form_side_width)
|
||||
self.config.set(
|
||||
'auto', 'last_list_side_width', self.last_list_side_width)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_filelist_sorted_column',
|
||||
self.last_filelist_sorted_column)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_filelist_sorted_order',
|
||||
self.last_filelist_sorted_order)
|
||||
|
||||
if not self.config.has_section("autotag"):
|
||||
self.config.add_section("autotag")
|
||||
self.config.set("autotag", "save_on_low_confidence", self.save_on_low_confidence)
|
||||
self.config.set("autotag", "dont_use_year_when_identifying", self.dont_use_year_when_identifying)
|
||||
self.config.set("autotag", "assume_1_if_no_issue_num", self.assume_1_if_no_issue_num)
|
||||
self.config.set("autotag", "ignore_leading_numbers_in_filename", self.ignore_leading_numbers_in_filename)
|
||||
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
|
||||
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
|
||||
if not self.config.has_section('identifier'):
|
||||
self.config.add_section('identifier')
|
||||
|
||||
with open(self.settings_file, "w", encoding="utf-8") as configfile:
|
||||
self.config.set(
|
||||
'identifier',
|
||||
'id_length_delta_thresh',
|
||||
self.id_length_delta_thresh)
|
||||
self.config.set(
|
||||
'identifier',
|
||||
'id_publisher_blacklist',
|
||||
self.id_publisher_blacklist)
|
||||
|
||||
if not self.config.has_section('dialogflags'):
|
||||
self.config.add_section('dialogflags')
|
||||
|
||||
self.config.set(
|
||||
'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar)
|
||||
self.config.set('dialogflags', 'show_disclaimer', self.show_disclaimer)
|
||||
self.config.set(
|
||||
'dialogflags',
|
||||
'dont_notify_about_this_version',
|
||||
self.dont_notify_about_this_version)
|
||||
self.config.set(
|
||||
'dialogflags', 'ask_about_usage_stats', self.ask_about_usage_stats)
|
||||
self.config.set(
|
||||
'dialogflags', 'show_no_unrar_warning', self.show_no_unrar_warning)
|
||||
|
||||
if not self.config.has_section('filenameparser'):
|
||||
self.config.add_section('filenameparser')
|
||||
|
||||
self.config.set(
|
||||
'filenameparser', 'parse_scan_info', self.parse_scan_info)
|
||||
|
||||
if not self.config.has_section('comicvine'):
|
||||
self.config.add_section('comicvine')
|
||||
|
||||
self.config.set(
|
||||
'comicvine',
|
||||
'use_series_start_as_volume',
|
||||
self.use_series_start_as_volume)
|
||||
self.config.set('comicvine', 'clear_form_before_populating_from_cv',
|
||||
self.clear_form_before_populating_from_cv)
|
||||
self.config.set(
|
||||
'comicvine', 'remove_html_tables', self.remove_html_tables)
|
||||
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
|
||||
|
||||
if not self.config.has_section('cbl_transform'):
|
||||
self.config.add_section('cbl_transform')
|
||||
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'assume_lone_credit_is_primary',
|
||||
self.assume_lone_credit_is_primary)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_characters_to_tags',
|
||||
self.copy_characters_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_locations_to_tags',
|
||||
self.copy_locations_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_storyarcs_to_tags',
|
||||
self.copy_storyarcs_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_notes_to_comments',
|
||||
self.copy_notes_to_comments)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_weblink_to_comments',
|
||||
self.copy_weblink_to_comments)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'apply_cbl_transform_on_cv_import',
|
||||
self.apply_cbl_transform_on_cv_import)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'apply_cbl_transform_on_bulk_operation',
|
||||
self.apply_cbl_transform_on_bulk_operation)
|
||||
|
||||
if not self.config.has_section('rename'):
|
||||
self.config.add_section('rename')
|
||||
|
||||
self.config.set('rename', 'rename_template', self.rename_template)
|
||||
self.config.set(
|
||||
'rename',
|
||||
'rename_issue_number_padding',
|
||||
self.rename_issue_number_padding)
|
||||
self.config.set(
|
||||
'rename',
|
||||
'rename_use_smart_string_cleanup',
|
||||
self.rename_use_smart_string_cleanup)
|
||||
self.config.set('rename', 'rename_extension_based_on_archive',
|
||||
self.rename_extension_based_on_archive)
|
||||
|
||||
if not self.config.has_section('autotag'):
|
||||
self.config.add_section('autotag')
|
||||
self.config.set(
|
||||
'autotag', 'save_on_low_confidence', self.save_on_low_confidence)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'dont_use_year_when_identifying',
|
||||
self.dont_use_year_when_identifying)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'assume_1_if_no_issue_num',
|
||||
self.assume_1_if_no_issue_num)
|
||||
self.config.set('autotag', 'ignore_leading_numbers_in_filename',
|
||||
self.ignore_leading_numbers_in_filename)
|
||||
self.config.set('autotag', 'remove_archive_after_successful_match',
|
||||
self.remove_archive_after_successful_match)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'wait_and_retry_on_rate_limit',
|
||||
self.wait_and_retry_on_rate_limit)
|
||||
|
||||
with codecs.open(self.settings_file, 'wb', 'utf8') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
# make sure the basedir is cached, in case we're on Windows running a
|
||||
# script from frozen binary
|
||||
ComicTaggerSettings.baseDir()
|
||||
|
||||
@@ -14,153 +14,91 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import md_test
|
||||
from comictaggerlib.comicvinecacher import ComicVineCacher
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker
|
||||
from comictaggerlib.filerenamer import FileRenamer
|
||||
from comictaggerlib.imagefetcher import ImageFetcher
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from comicvinetalker import ComicVineTalker
|
||||
from imagefetcher import ImageFetcher
|
||||
import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
windowsRarHelp = """
|
||||
<html><head/><body><p>To write to CBR/RAR archives,
|
||||
<html><head/><body><p>In order to write to CBR/RAR archives,
|
||||
you will need to have the tools from
|
||||
<span style=" text-decoration: underline; color:#0000ff;">
|
||||
<a href="http://www.win-rar.com/download.html">WINRar</a></span>
|
||||
installed. (ComicTagger only uses the command-line rar tool,
|
||||
which is free to use.)</p></body></html>
|
||||
<a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span>
|
||||
</a> installed. </p></body></html>
|
||||
"""
|
||||
|
||||
linuxRarHelp = """
|
||||
<html><head/><body><p>To write to CBR/RAR archives,
|
||||
you will need to have the shareware rar tool from RARLab installed.
|
||||
Your package manager should have rar (e.g. "apt-get install rar"). If not, download it
|
||||
<span style=" text-decoration: underline; color:#0000ff;">
|
||||
<a href="https://www.rarlab.com/download.htm">here</a></span>,
|
||||
and install in your path. </p></body></html>
|
||||
<html><head/><body><p>In order to read/write to CBR/RAR archives,
|
||||
you will need to have the shareware tools from WinRar installed.
|
||||
Your package manager should have unrar, and probably rar.
|
||||
If not, download them <a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">here</span>
|
||||
</a>, and install in your path. </p></body></html>
|
||||
"""
|
||||
|
||||
macRarHelp = """
|
||||
<html><head/><body><p>To write to CBR/RAR archives,
|
||||
you will need the rar tool. The easiest way to get this is
|
||||
to install <span style=" text-decoration: underline; color:#0000ff;">
|
||||
<a href="https://brew.sh/">homebrew</a></span>.
|
||||
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
|
||||
<html><head/><body><p>In order to read/write to CBR/RAR archives,
|
||||
you will need the shareware tools from
|
||||
<a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span>
|
||||
</a>. </p></body></html>
|
||||
"""
|
||||
|
||||
|
||||
template_tooltip = """
|
||||
<pre>The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
|
||||
Accepts the following variables:
|
||||
{is_empty} (boolean)
|
||||
{tag_origin} (string)
|
||||
{series} (string)
|
||||
{issue} (string)
|
||||
{title} (string)
|
||||
{publisher} (string)
|
||||
{month} (integer)
|
||||
{year} (integer)
|
||||
{day} (integer)
|
||||
{issue_count} (integer)
|
||||
{volume} (integer)
|
||||
{genre} (string)
|
||||
{language} (string)
|
||||
{comments} (string)
|
||||
{volume_count} (integer)
|
||||
{critical_rating} (string)
|
||||
{country} (string)
|
||||
{alternate_series} (string)
|
||||
{alternate_number} (string)
|
||||
{alternate_count} (integer)
|
||||
{imprint} (string)
|
||||
{notes} (string)
|
||||
{web_link} (string)
|
||||
{format} (string)
|
||||
{manga} (string)
|
||||
{black_and_white} (boolean)
|
||||
{page_count} (integer)
|
||||
{maturity_rating} (string)
|
||||
{community_rating} (string)
|
||||
{story_arc} (string)
|
||||
{series_group} (string)
|
||||
{scan_info} (string)
|
||||
{characters} (string)
|
||||
{teams} (string)
|
||||
{locations} (string)
|
||||
{credits} (list of dict({'role': string, 'person': string, 'primary': boolean}))
|
||||
{tags} (list of str)
|
||||
{pages} (list of dict({'Image': string(int), 'Type': string}))
|
||||
class SettingsWindow(QtGui.QDialog):
|
||||
|
||||
CoMet-only items:
|
||||
{price} (float)
|
||||
{is_version_of} (string)
|
||||
{rights} (string)
|
||||
{identifier} (string)
|
||||
{last_mark} (string)
|
||||
{cover_image} (string)
|
||||
|
||||
Examples:
|
||||
|
||||
{series} {issue} ({year})
|
||||
Spider-Geddon 1 (2018)
|
||||
|
||||
{series} #{issue} - {title}
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
</pre>
|
||||
"""
|
||||
|
||||
|
||||
class SettingsWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent, settings):
|
||||
super().__init__(parent)
|
||||
super(SettingsWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("settingswindow.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.name = "Settings"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
self.lblUnrar.hide()
|
||||
self.leUnrarExePath.hide()
|
||||
self.btnBrowseUnrar.hide()
|
||||
self.lblRarHelp.setText(windowsRarHelp)
|
||||
|
||||
elif platform.system() == "Linux":
|
||||
self.lblRarHelp.setText(linuxRarHelp)
|
||||
|
||||
elif platform.system() == "Darwin":
|
||||
self.leRarExePath.setReadOnly(False)
|
||||
|
||||
self.lblRarHelp.setText(macRarHelp)
|
||||
self.name = "Preferences"
|
||||
|
||||
self.setWindowTitle("ComicTagger " + self.name)
|
||||
self.lblDefaultSettings.setText("Revert to default " + self.name.lower())
|
||||
self.lblDefaultSettings.setText(
|
||||
"Revert to default " + self.name.lower())
|
||||
self.btnResetSettings.setText("Default " + self.name)
|
||||
|
||||
nldt_tip = """<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
|
||||
nldtTip = (
|
||||
"""<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The higher
|
||||
it is, the more likely to have a good match, but each search will take longer and
|
||||
use more bandwidth. Too low, and only the very closest lexical matches will be
|
||||
explored.</html>"""
|
||||
explored.</html>""")
|
||||
|
||||
self.leNameLengthDeltaThresh.setToolTip(nldt_tip)
|
||||
self.leNameLengthDeltaThresh.setToolTip(nldtTip)
|
||||
|
||||
pbl_tip = """<html>
|
||||
The <b>Publisher Filter</b> is for eliminating automatic matches to certain publishers
|
||||
pblTip = (
|
||||
"""<html>
|
||||
The <b>Publisher Blacklist</b> is for eliminating automatic matches to certain publishers
|
||||
that you know are incorrect. Useful for avoiding international re-prints with same
|
||||
covers or series names. Enter publisher names separated by commas.
|
||||
</html>"""
|
||||
self.tePublisherFilter.setToolTip(pbl_tip)
|
||||
)
|
||||
self.tePublisherBlacklist.setToolTip(pblTip)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
@@ -168,128 +106,76 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthDeltaThresh.setValidator(validator)
|
||||
|
||||
self.leRenameTemplate.setToolTip(template_tooltip)
|
||||
self.settings_to_form()
|
||||
self.rename_error = None
|
||||
self.rename_test()
|
||||
self.settingsToForm()
|
||||
|
||||
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.leRenameTemplate.textEdited.connect(self.rename__test)
|
||||
self.cbxMoveFiles.clicked.connect(self.rename_test)
|
||||
self.cbxRenameStrict.clicked.connect(self.rename_test)
|
||||
self.leDirectory.textEdited.connect(self.rename_test)
|
||||
self.cbxComplicatedParser.clicked.connect(self.switch_parser)
|
||||
self.btnBrowseRar.clicked.connect(self.selectRar)
|
||||
self.btnBrowseUnrar.clicked.connect(self.selectUnrar)
|
||||
self.btnClearCache.clicked.connect(self.clearCache)
|
||||
self.btnResetSettings.clicked.connect(self.resetSettings)
|
||||
self.btnTestKey.clicked.connect(self.testAPIKey)
|
||||
|
||||
def rename_test(self):
|
||||
self.rename__test(self.leRenameTemplate.text())
|
||||
def settingsToForm(self):
|
||||
|
||||
def rename__test(self, template):
|
||||
fr = FileRenamer(md_test, platform="universal" if self.cbxRenameStrict.isChecked() else "auto")
|
||||
fr.move = self.cbxMoveFiles.isChecked()
|
||||
fr.set_template(template)
|
||||
fr.set_issue_zero_padding(int(self.leIssueNumPadding.text()))
|
||||
fr.set_smart_cleanup(self.cbxSmartCleanup.isChecked())
|
||||
try:
|
||||
self.lblRenameTest.setText(fr.determine_name(".cbz"))
|
||||
self.rename_error = None
|
||||
except Exception as e:
|
||||
self.rename_error = e
|
||||
self.lblRenameTest.setText(str(e))
|
||||
|
||||
def switch_parser(self):
|
||||
complicated = self.cbxComplicatedParser.isChecked()
|
||||
|
||||
self.cbxRemoveC2C.setEnabled(complicated)
|
||||
self.cbxRemoveFCBD.setEnabled(complicated)
|
||||
self.cbxRemovePublisher.setEnabled(complicated)
|
||||
|
||||
def settings_to_form(self):
|
||||
# Copy values from settings to form
|
||||
self.leRarExePath.setText(self.settings.rar_exe_path)
|
||||
self.leNameLengthDeltaThresh.setText(str(self.settings.id_length_delta_thresh))
|
||||
self.tePublisherFilter.setPlainText(self.settings.id_publisher_filter)
|
||||
self.leUnrarExePath.setText(self.settings.unrar_exe_path)
|
||||
self.leNameLengthDeltaThresh.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.tePublisherBlacklist.setPlainText(
|
||||
self.settings.id_publisher_blacklist)
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
self.cbxComplicatedParser.setChecked(self.settings.complicated_parser)
|
||||
self.cbxRemoveC2C.setChecked(self.settings.remove_c2c)
|
||||
self.cbxRemoveFCBD.setChecked(self.settings.remove_fcbd)
|
||||
self.cbxRemovePublisher.setChecked(self.settings.remove_publisher)
|
||||
self.switch_parser()
|
||||
if self.settings.parse_scan_info:
|
||||
self.cbxParseScanInfo.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.use_series_start_as_volume:
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.clear_form_before_populating_from_cv:
|
||||
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.remove_html_tables:
|
||||
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
|
||||
if self.settings.always_use_publisher_filter:
|
||||
self.cbxUseFilter.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
if self.settings.sort_series_by_year:
|
||||
self.cbxSortByYear.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
if self.settings.exact_series_matches_first:
|
||||
self.cbxExactMatches.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
|
||||
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
|
||||
self.leKey.setText(str(self.settings.cv_api_key))
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_characters_to_tags:
|
||||
self.cbxCopyCharactersToTags.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCopyCharactersToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_teams_to_tags:
|
||||
self.cbxCopyTeamsToTags.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCopyTeamsToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_locations_to_tags:
|
||||
self.cbxCopyLocationsToTags.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCopyLocationsToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_storyarcs_to_tags:
|
||||
self.cbxCopyStoryArcsToTags.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCopyStoryArcsToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_notes_to_comments:
|
||||
self.cbxCopyNotesToComments.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCopyNotesToComments.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
self.cbxCopyWebLinkToComments.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxCopyWebLinkToComments.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
self.cbxApplyCBLTransformOnCVIMport.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxApplyCBLTransformOnCVIMport.setCheckState(
|
||||
QtCore.Qt.Checked)
|
||||
if self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(
|
||||
QtCore.Qt.Checked)
|
||||
|
||||
self.leRenameTemplate.setText(self.settings.rename_template)
|
||||
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
|
||||
self.leIssueNumPadding.setText(
|
||||
str(self.settings.rename_issue_number_padding))
|
||||
if self.settings.rename_use_smart_string_cleanup:
|
||||
self.cbxSmartCleanup.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxSmartCleanup.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
self.cbxChangeExtension.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
if self.settings.rename_move_dir:
|
||||
self.cbxMoveFiles.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.leDirectory.setText(self.settings.rename_dir)
|
||||
if self.settings.rename_strict:
|
||||
self.cbxRenameStrict.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.cbxChangeExtension.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
def accept(self):
|
||||
self.rename_test()
|
||||
if self.rename_error is not None:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Invalid format string!",
|
||||
"Your rename template is invalid!"
|
||||
f"<br/><br/>{self.rename_error}<br/><br/>"
|
||||
"Please consult the template help in the "
|
||||
"settings and the documentation on the format at "
|
||||
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
|
||||
"https://docs.python.org/3/library/string.html#format-string-syntax</a>",
|
||||
)
|
||||
return
|
||||
|
||||
# Copy values from form to settings and save
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
|
||||
|
||||
# make sure rar program is now in the path for the rar class
|
||||
if self.settings.rar_exe_path:
|
||||
utils.add_to_path(os.path.dirname(self.settings.rar_exe_path))
|
||||
# make sure unrar/rar program is now in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
@@ -299,24 +185,18 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
|
||||
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_filter = str(self.tePublisherFilter.toPlainText())
|
||||
self.settings.id_length_delta_thresh = int(
|
||||
self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_blacklist = str(
|
||||
self.tePublisherBlacklist.toPlainText())
|
||||
|
||||
self.settings.complicated_parser = self.cbxComplicatedParser.isChecked()
|
||||
self.settings.remove_c2c = self.cbxRemoveC2C.isChecked()
|
||||
self.settings.remove_fcbd = self.cbxRemoveFCBD.isChecked()
|
||||
self.settings.remove_publisher = self.cbxRemovePublisher.isChecked()
|
||||
self.settings.parse_scan_info = self.cbxParseScanInfo.isChecked()
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
|
||||
|
||||
self.settings.always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.settings.sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.settings.exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
|
||||
self.settings.cv_api_key = str(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key.strip()
|
||||
self.settings.cv_api_key = unicode(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
@@ -328,71 +208,65 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
|
||||
self.settings.rename_template = str(self.leRenameTemplate.text())
|
||||
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.settings.rename_issue_number_padding = int(
|
||||
self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
|
||||
self.settings.rename_dir = self.leDirectory.text()
|
||||
|
||||
self.settings.rename_strict = self.cbxRenameStrict.isChecked()
|
||||
|
||||
self.settings.save()
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
def select_rar(self):
|
||||
self.select_file(self.leRarExePath, "RAR")
|
||||
def selectRar(self):
|
||||
self.selectFile(self.leRarExePath, "RAR")
|
||||
|
||||
def clear_cache(self):
|
||||
ImageFetcher().clear_cache()
|
||||
ComicVineCacher().clear_cache()
|
||||
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
|
||||
def selectUnrar(self):
|
||||
self.selectFile(self.leUnrarExePath, "UnRAR")
|
||||
|
||||
def test_api_key(self):
|
||||
if ComicVineTalker().test_key(str(self.leKey.text()).strip()):
|
||||
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
|
||||
def clearCache(self):
|
||||
ImageFetcher().clearCache()
|
||||
ComicVineCacher().clearCache()
|
||||
QtGui.QMessageBox.information(
|
||||
self, self.name, "Cache has been cleared.")
|
||||
|
||||
def testAPIKey(self):
|
||||
if ComicVineTalker().testKey(unicode(self.leKey.text())):
|
||||
QtGui.QMessageBox.information(
|
||||
self, "API Key Test", "Key is valid!")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
|
||||
QtGui.QMessageBox.warning(
|
||||
self, "API Key Test", "Key is NOT valid.")
|
||||
|
||||
def reset_settings(self):
|
||||
def resetSettings(self):
|
||||
self.settings.reset()
|
||||
self.settings_to_form()
|
||||
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
|
||||
self.settingsToForm()
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
self.name,
|
||||
self.name +
|
||||
" have been returned to default values.")
|
||||
|
||||
def select_file(self, control: QtWidgets.QLineEdit, name):
|
||||
def selectFile(self, control, name):
|
||||
|
||||
dialog = QtWidgets.QFileDialog(self)
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFile)
|
||||
dialog = QtGui.QFileDialog(self)
|
||||
dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
if name == "RAR":
|
||||
flt = "Rar Program (Rar.exe)"
|
||||
filter = self.tr("Rar Program (Rar.exe)")
|
||||
else:
|
||||
flt = "Libraries (*.dll)"
|
||||
dialog.setNameFilter(flt)
|
||||
filter = self.tr("Programs (*.exe)")
|
||||
dialog.setNameFilter(filter)
|
||||
else:
|
||||
dialog.setFilter(QtCore.QDir.Filter.Files)
|
||||
# QtCore.QDir.Executable | QtCore.QDir.Files)
|
||||
dialog.setFilter(QtCore.QDir.Files)
|
||||
pass
|
||||
|
||||
dialog.setDirectory(os.path.dirname(str(control.text())))
|
||||
if name == "RAR":
|
||||
dialog.setWindowTitle("Find " + name + " program")
|
||||
else:
|
||||
dialog.setWindowTitle("Find " + name + " library")
|
||||
dialog.setWindowTitle("Find " + name + " program")
|
||||
|
||||
if dialog.exec():
|
||||
file_list = dialog.selectedFiles()
|
||||
control.setText(str(file_list[0]))
|
||||
if (dialog.exec_()):
|
||||
fileList = dialog.selectedFiles()
|
||||
control.setText(str(fileList[0]))
|
||||
|
||||
def show_rename_tab(self):
|
||||
def showRenameTab(self):
|
||||
self.tabWidget.setCurrentIndex(5)
|
||||
|
||||
def show_template_help(self):
|
||||
template_help_win = TemplateHelpWindow(self)
|
||||
template_help_win.setModal(False)
|
||||
template_help_win.show()
|
||||
|
||||
|
||||
class TemplateHelpWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(TemplateHelpWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("TemplateHelp.ui"), self)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,135 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>702</width>
|
||||
<height>452</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Template Help</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="textEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="html">
|
||||
<string><html>
|
||||
<head>
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="text-align: center">Template help</h1>
|
||||
<p>The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
|
||||
|
||||
<a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p>
|
||||
Accepts the following variables:
|
||||
<table>
|
||||
<tr>
|
||||
<th>Tag name</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
<tr><td>{is_empty}</td><td>boolean</td></tr>
|
||||
<tr><td>{tag_origin}</td><td>string</td></tr>
|
||||
<tr><td>{series}</td><td>string</td></tr>
|
||||
<tr><td>{issue}</td><td>string</td></tr>
|
||||
<tr><td>{title}</td><td>string</td></tr>
|
||||
<tr><td>{publisher}</td><td>string</td></tr>
|
||||
<tr><td>{month}</td><td>integer</td></tr>
|
||||
<tr><td>{year}</td><td>integer</td></tr>
|
||||
<tr><td>{day}</td><td>integer</td></tr>
|
||||
<tr><td>{issue_count}</td><td>integer</td></tr>
|
||||
<tr><td>{volume}</td><td>integer</td></tr>
|
||||
<tr><td>{genre}</td><td>string</td></tr>
|
||||
<tr><td>{language}</td><td>string</td></tr>
|
||||
<tr><td>{comments}</td><td>string</td></tr>
|
||||
<tr><td>{volume_count}</td><td>integer</td></tr>
|
||||
<tr><td>{critical_rating}</td><td>string</td></tr>
|
||||
<tr><td>{country}</td><td>string</td></tr>
|
||||
<tr><td>{alternate_series}</td><td>string</td></tr>
|
||||
<tr><td>{alternate_number}</td><td>string</td></tr>
|
||||
<tr><td>{alternate_count}</td><td>integer</td></tr>
|
||||
<tr><td>{imprint}</td><td>string</td></tr>
|
||||
<tr><td>{notes}</td><td>string</td></tr>
|
||||
<tr><td>{web_link}</td><td>string</td></tr>
|
||||
<tr><td>{format}</td><td>string</td></tr>
|
||||
<tr><td>{manga}</td><td>string</td></tr>
|
||||
<tr><td>{black_and_white}</td><td>boolean</td></tr>
|
||||
<tr><td>{page_count}</td><td>integer</td></tr>
|
||||
<tr><td>{maturity_rating}</td><td>string</td></tr>
|
||||
<tr><td>{community_rating}</td><td>string</td></tr>
|
||||
<tr><td>{story_arc}</td><td>string</td></tr>
|
||||
<tr><td>{series_group}</td><td>string</td></tr>
|
||||
<tr><td>{scan_info}</td><td>string</td></tr>
|
||||
<tr><td>{characters}</td><td>string</td></tr>
|
||||
<tr><td>{teams}</td><td>string</td></tr>
|
||||
<tr><td>{locations}</td><td>string</td></tr>
|
||||
<tr><td>{credits}</td><td>list of dict({'role': string, 'person': string, 'primary': boolean})</td></tr>
|
||||
<tr><td>{tags}</td><td>list of str</td></tr>
|
||||
<tr><td>{pages}</td><td>list of dict({'Image': string(int), 'Type': string})</td></tr>
|
||||
<tr><td>{price}</td><td>float</td></tr>
|
||||
<tr><td>{is_version_of}</td><td>string</td></tr>
|
||||
<tr><td>{rights}</td><td>string</td></tr>
|
||||
<tr><td>{identifier}</td><td>string</td></tr>
|
||||
<tr><td>{last_mark}</td><td>string</td></tr>
|
||||
<tr><td>{cover_image}</td><td>string</td></tr>
|
||||
</table>
|
||||
<pre>
|
||||
Examples:
|
||||
|
||||
{series} {issue} ({year})
|
||||
Spider-Geddon 1 (2018)
|
||||
|
||||
{series} #{issue} - {title}
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
</html></string></property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextBrowserInteraction</set>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -21,7 +21,7 @@
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<widget class="QWidget" name="archiveCoverContainer">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@@ -43,7 +43,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QWidget" name="testCoverContainer" native="true">
|
||||
<widget class="QWidget" name="testCoverContainer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
|
||||
@@ -14,24 +14,15 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>533</width>
|
||||
<height>231</height>
|
||||
<height>202</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideMiddle</enum>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>36</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>61</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>36</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
@@ -45,7 +45,7 @@
|
||||
<string>File Name</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -56,7 +56,7 @@
|
||||
<string>Has ComicRack Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -67,7 +67,7 @@
|
||||
<string>Has ComicBookLover Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -78,7 +78,7 @@
|
||||
<string>Archive Type</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -89,7 +89,7 @@
|
||||
<string>Read-Only</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -100,7 +100,7 @@
|
||||
<string>File Location</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<string>Title</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
|
||||
@@ -37,9 +37,6 @@
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@@ -87,47 +84,16 @@
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_1">
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblPageType">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Page Type:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="cbPageType"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QCheckBox" name="chkDoublePage">
|
||||
<property name="text">
|
||||
<string>&Double Page?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblBookmark">
|
||||
<property name="text">
|
||||
<string>Book&mark:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>leBookmark</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="leBookmark">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QComboBox" name="comboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
|
||||
@@ -28,12 +28,11 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="font">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
</property> <property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
"""Some utilities for the GUI"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import traceback
|
||||
#import StringIO
|
||||
|
||||
#from PIL import Image
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
from PyQt4 import QtGui
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
|
||||
if qt_available:
|
||||
try:
|
||||
from PIL import Image, ImageQt
|
||||
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
def reduce_widget_font_size(widget, delta=2):
|
||||
def reduceWidgetFontSize(widget, delta=2):
|
||||
f = widget.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize(f.pointSize() - delta)
|
||||
widget.setFont(f)
|
||||
|
||||
def center_window_on_screen(window):
|
||||
def centerWindowOnScreen(window):
|
||||
"""Center the window on screen.
|
||||
|
||||
This implementation will handle the window
|
||||
being resized or the screen resolution changing.
|
||||
"""
|
||||
# Get the current screens' dimensions...
|
||||
screen = QtGui.QGuiApplication.primaryScreen().geometry()
|
||||
# The horizontal position is calculated as (screen width - window width) / 2
|
||||
hpos = int((screen.width() - window.width()) / 2)
|
||||
screen = QtGui.QDesktopWidget().screenGeometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calculated as screen width - window width
|
||||
# / 2
|
||||
hpos = (screen.width() - window.width()) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = int((screen.height() - window.height()) / 2)
|
||||
vpos = (screen.height() - window.height()) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos, vpos)
|
||||
|
||||
def center_window_on_parent(window):
|
||||
def centerWindowOnParent(window):
|
||||
|
||||
top_level = window
|
||||
while top_level.parent() is not None:
|
||||
@@ -53,32 +48,44 @@ if qt_available:
|
||||
# Get the current screens' dimensions...
|
||||
main_window_size = top_level.geometry()
|
||||
# ... and get this windows' dimensions
|
||||
# The horizontal position is calculated as (screen width - window width) / 2
|
||||
hpos = int((main_window_size.width() - window.width()) / 2)
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calculated as screen width - window width
|
||||
# /2
|
||||
hpos = (main_window_size.width() - window.width()) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = int((main_window_size.height() - window.height()) / 2)
|
||||
vpos = (main_window_size.height() - window.height()) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos + main_window_size.left(), vpos + main_window_size.top())
|
||||
window.move(
|
||||
hpos +
|
||||
main_window_size.left(),
|
||||
vpos +
|
||||
main_window_size.top())
|
||||
|
||||
def get_qimage_from_data(image_data):
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
import StringIO
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
def getQImageFromData(image_data):
|
||||
img = QtGui.QImage()
|
||||
success = img.loadFromData(image_data)
|
||||
if not success:
|
||||
try:
|
||||
if pil_available:
|
||||
# Qt doesn't understand the format, but maybe PIL does
|
||||
img = ImageQt.ImageQt(Image.open(io.BytesIO(image_data)))
|
||||
# Qt doesn't understand the format, but maybe PIL does
|
||||
# so try to convert the image data to uncompressed tiff
|
||||
# format
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
output = StringIO.StringIO()
|
||||
im.save(output, format="TIFF")
|
||||
img.loadFromData(output.getvalue())
|
||||
success = True
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
pass
|
||||
# if still nothing, go with default image
|
||||
if not success:
|
||||
img.load(ComicTaggerSettings.get_graphic("nocover.png"))
|
||||
img.load(ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
return img
|
||||
|
||||
def qt_error(msg: str, e: Exception = None):
|
||||
trace = ""
|
||||
if e:
|
||||
trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||
|
||||
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", msg + trace)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>702</width>
|
||||
<height>478</height>
|
||||
<height>432</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -116,6 +116,118 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="grpBoxRar">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="lblRarHelp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>In order to read/write to CBR/RAR archives, you will need to have the shareware tools from <a href="www.win-rar.com/download.html"><span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a> installed. </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="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<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>RAR program</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRarExePath">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btnBrowseRar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="lblUnrar">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>UnRAR program</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="leUnrarExePath">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QPushButton" name="btnBrowseUnrar">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
@@ -133,7 +245,7 @@
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Searching</string>
|
||||
<string>Identifier</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
@@ -187,15 +299,15 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Publisher Filter:</string>
|
||||
<string>Publisher Blacklist:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPlainTextEdit" name="tePublisherFilter">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPlainTextEdit" name="tePublisherBlacklist">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@@ -204,23 +316,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="cbxUseFilter">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Applies the <span style=" font-weight:600;">Publisher Filter</span> on all searches.<br/>The search window has a dynamic toggle to show the unfiltered results.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Always use Publisher Filter on "manual" searches:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -229,55 +324,19 @@
|
||||
<attribute name="title">
|
||||
<string>Filename Parser</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxComplicatedParser">
|
||||
<property name="text">
|
||||
<string>Use "Complicated" Parser</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxRemoveC2C">
|
||||
<property name="text">
|
||||
<string>Remove 'C2C' from Scan Info</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxRemoveFCBD">
|
||||
<property name="text">
|
||||
<string>Remove 'FCBD' from Scan Info</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxRemovePublisher">
|
||||
<property name="text">
|
||||
<string>Remove Publisher from filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<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>
|
||||
<widget class="QCheckBox" name="cbxParseScanInfo">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>421</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Parse Scan Info From Filename (Experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
@@ -316,33 +375,6 @@
|
||||
</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>Initally 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>Initally show Series Name exact matches first</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -439,7 +471,7 @@
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btnTestKey">
|
||||
<property name="text">
|
||||
<string>Test Key</string>
|
||||
<string>Tesk Key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -497,7 +529,7 @@
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>251</width>
|
||||
<height>199</height>
|
||||
<height>192</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
@@ -583,43 +615,40 @@
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="lblTemplate">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Template:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRenameTemplate"/>
|
||||
<widget class="QLineEdit" name="leRenameTemplate">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
|
||||
%genre%<br/>
|
||||
%language_code%<br/>
|
||||
%criticalrating%<br/>
|
||||
%alternateseries%<br/>
|
||||
%alternatenumber%<br/>
|
||||
%alternatecount%<br/>
|
||||
%imprint%<br/>
|
||||
%format%<br/>
|
||||
%maturityrating%<br/>
|
||||
%storyarc%<br/>
|
||||
%seriesgroup%<br/>
|
||||
%scaninfo%
|
||||
</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="btnTemplateHelp">
|
||||
<property name="text">
|
||||
<string>Template Help</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="lblRenameTest">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="lblPadding">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Issue # Zero Padding</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="leIssueNumPadding">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@@ -638,7 +667,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSmartCleanup">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html></string>
|
||||
@@ -648,153 +677,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxChangeExtension">
|
||||
<property name="text">
|
||||
<string>Change Extension Based On Archive Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QCheckBox" name="cbxMoveFiles">
|
||||
<property name="toolTip">
|
||||
<string>If checked moves files to specified folder</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Move files when renaming</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="lblDirectory">
|
||||
<property name="text">
|
||||
<string>Destination Directory:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QLineEdit" name="leDirectory"/>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QCheckBox" name="cbxRenameStrict">
|
||||
<property name="toolTip">
|
||||
<string>If checked will ensure reserved characters and filenames are removed for all Operating Systems.
|
||||
By default only removes restricted characters and filenames for the current Operating System.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Strict renaming</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_7">
|
||||
<attribute name="title">
|
||||
<string>RAR Tools</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="grpBoxRar">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<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>RAR program</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRarExePath">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btnBrowseRar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="lblRarHelp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>In order to write to CBR/RAR archives, you will need to have the shareware tools from <a href="www.win-rar.com/download.html"><span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a> installed. </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>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<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>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -108,16 +108,7 @@
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<property name="margin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -816,7 +807,7 @@
|
||||
<string>Primary</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -938,31 +929,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLineEdit" name="leWebLink">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="btnOpenWebLink">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QLineEdit" name="leWebLink">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="userRatingLabel">
|
||||
@@ -990,35 +961,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="lblCommunityRating">
|
||||
<property name="text">
|
||||
<string>Community Rating</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QDoubleSpinBox" name="dsbCommunityRating">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -1182,7 +1124,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>21</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
||||
@@ -1236,7 +1178,6 @@
|
||||
<addaction name="actionAutoIdentify"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionApplyCBLTransform"/>
|
||||
<addaction name="actionReCalcPageDims"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuWindow">
|
||||
<property name="title">
|
||||
@@ -1411,11 +1352,6 @@
|
||||
<string>Apply CBL Transform</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionReCalcPageDims">
|
||||
<property name="text">
|
||||
<string>Re-Calculate Page Dimensions</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoadFolder">
|
||||
<property name="text">
|
||||
<string>Open Folder</string>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>893</width>
|
||||
<width>849</width>
|
||||
<height>476</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -86,7 +86,7 @@
|
||||
<string>Year</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -94,7 +94,7 @@
|
||||
<string>Issues</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignCenter</set>
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
@@ -148,16 +148,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxFilter">
|
||||
<property name="toolTip">
|
||||
<string>Filter the publishers based on the publisher filter.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Filter Publishers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
|
||||
1
comictaggerlib/utils.py
Normal file
1
comictaggerlib/utils.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.utils import *
|
||||
@@ -14,45 +14,86 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
import platform
|
||||
import urllib2
|
||||
#import os
|
||||
#import urllib
|
||||
|
||||
import requests
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
|
||||
from comictaggerlib import ctversion
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
class pyqtSignal():
|
||||
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
def emit(a, b, c):
|
||||
pass
|
||||
|
||||
import ctversion
|
||||
|
||||
|
||||
class VersionChecker:
|
||||
def get_request_url(self, uuid, use_stats):
|
||||
class VersionChecker(QObject):
|
||||
|
||||
def getRequestUrl(self, uuid, use_stats):
|
||||
|
||||
base_url = "http://comictagger1.appspot.com/latest"
|
||||
params = {}
|
||||
args = ""
|
||||
|
||||
if use_stats:
|
||||
params = {"uuid": uuid, "version": ctversion.version}
|
||||
if platform.system() == "Windows":
|
||||
params["platform"] = "win"
|
||||
plat = "win"
|
||||
elif platform.system() == "Linux":
|
||||
params["platform"] = "lin"
|
||||
plat = "lin"
|
||||
elif platform.system() == "Darwin":
|
||||
params["platform"] = "mac"
|
||||
plat = "mac"
|
||||
else:
|
||||
params["platform"] = "other"
|
||||
plat = "other"
|
||||
args = "?uuid={0}&platform={1}&version={2}".format(
|
||||
uuid, plat, ctversion.version)
|
||||
if not getattr(sys, 'frozen', None):
|
||||
args += "&src=T"
|
||||
|
||||
if not getattr(sys, "frozen", None):
|
||||
params["src"] = "T"
|
||||
return base_url + args
|
||||
|
||||
return base_url, params
|
||||
def getLatestVersion(self, uuid, use_stats=True):
|
||||
|
||||
def get_latest_version(self, uuid, use_stats=True):
|
||||
try:
|
||||
url, params = self.get_request_url(uuid, use_stats)
|
||||
new_version = requests.get(url, params=params).text
|
||||
except Exception:
|
||||
resp = urllib2.urlopen(self.getRequestUrl(uuid, use_stats))
|
||||
new_version = resp.read()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return None
|
||||
return new_version.strip()
|
||||
|
||||
versionRequestComplete = pyqtSignal(str)
|
||||
|
||||
def asyncGetLatestVersion(self, uuid, use_stats):
|
||||
|
||||
url = self.getRequestUrl(uuid, use_stats)
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.asyncGetLatestVersionComplete)
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url))))
|
||||
|
||||
def asyncGetLatestVersionComplete(self, reply):
|
||||
if (reply.error() != QNetworkReply.NoError):
|
||||
return
|
||||
|
||||
# read in the response
|
||||
new_version = str(reply.readAll())
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return
|
||||
|
||||
self.versionRequestComplete.emit(new_version.strip())
|
||||
|
||||
@@ -14,26 +14,30 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
#import sys
|
||||
#import time
|
||||
#import os
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal
|
||||
#from PyQt4.QtCore import QObject
|
||||
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
from comictaggerlib.issueselectionwindow import IssueSelectionWindow
|
||||
from comictaggerlib.matchselectionwindow import MatchSelectionWindow
|
||||
from comictaggerlib.progresswindow import IDProgressWindow
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from issueselectionwindow import IssueSelectionWindow
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from progresswindow import IDProgressWindow
|
||||
from settings import ComicTaggerSettings
|
||||
from matchselectionwindow import MatchSelectionWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
|
||||
class SearchThread(QtCore.QThread):
|
||||
|
||||
searchComplete = pyqtSignal()
|
||||
progressUpdate = pyqtSignal(int, int)
|
||||
|
||||
@@ -42,16 +46,15 @@ class SearchThread(QtCore.QThread):
|
||||
self.series_name = series_name
|
||||
self.refresh = refresh
|
||||
self.error_code = None
|
||||
self.cv_error = False
|
||||
self.cv_search_results = []
|
||||
|
||||
def run(self):
|
||||
comic_vine = ComicVineTalker()
|
||||
comicVine = ComicVineTalker()
|
||||
try:
|
||||
self.cv_error = False
|
||||
self.cv_search_results = comic_vine.search_for_series(
|
||||
self.series_name, callback=self.prog_callback, refresh_cache=self.refresh
|
||||
)
|
||||
self.cv_search_results = comicVine.searchForSeries(
|
||||
self.series_name,
|
||||
callback=self.prog_callback,
|
||||
refresh_cache=self.refresh)
|
||||
except ComicVineTalkerException as e:
|
||||
self.cv_search_results = []
|
||||
self.cv_error = True
|
||||
@@ -65,62 +68,51 @@ class SearchThread(QtCore.QThread):
|
||||
|
||||
|
||||
class IdentifyThread(QtCore.QThread):
|
||||
|
||||
identifyComplete = pyqtSignal()
|
||||
identifyLogMsg = pyqtSignal(str)
|
||||
identifyProgress = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, identifier: IssueIdentifier):
|
||||
def __init__(self, identifier):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.identifier = identifier
|
||||
self.identifier.set_output_function(self.log_output)
|
||||
self.identifier.set_progress_callback(self.progress_callback)
|
||||
self.identifier.setOutputFunction(self.logOutput)
|
||||
self.identifier.setProgressCallback(self.progressCallback)
|
||||
|
||||
def log_output(self, text: str):
|
||||
self.identifyLogMsg.emit(str(text))
|
||||
def logOutput(self, text):
|
||||
self.identifyLogMsg.emit(text)
|
||||
|
||||
def progress_callback(self, cur, total):
|
||||
def progressCallback(self, cur, total):
|
||||
self.identifyProgress.emit(cur, total)
|
||||
|
||||
def run(self):
|
||||
self.identifier.search()
|
||||
matches = self.identifier.search()
|
||||
self.identifyComplete.emit()
|
||||
|
||||
|
||||
class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
series_name,
|
||||
issue_number,
|
||||
year,
|
||||
issue_count,
|
||||
cover_index_list,
|
||||
comic_archive,
|
||||
settings,
|
||||
autoselect=False,
|
||||
):
|
||||
super().__init__(parent)
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("volumeselectionwindow.ui"), self)
|
||||
def __init__(self, parent, series_name, issue_number, year, issue_count,
|
||||
cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
self.imageWidget = CoverImageWidget(self.imageContainer, CoverImageWidget.URLMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('volumeselectionwindow.ui'), self)
|
||||
|
||||
self.imageWidget = CoverImageWidget(
|
||||
self.imageContainer, CoverImageWidget.URLMode)
|
||||
gridlayout = QtGui.QGridLayout(self.imageContainer)
|
||||
gridlayout.addWidget(self.imageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
reduce_widget_font_size(self.teDetails, 1)
|
||||
reduce_widget_font_size(self.twList)
|
||||
reduceWidgetFontSize(self.teDetails, 1)
|
||||
reduceWidgetFontSize(self.twList)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
self.windowFlags()
|
||||
| QtCore.Qt.WindowType.WindowSystemMenuHint
|
||||
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
|
||||
)
|
||||
)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.parent = parent
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
self.year = year
|
||||
@@ -130,57 +122,51 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.immediate_autoselect = autoselect
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
self.ii = None
|
||||
self.iddialog = None
|
||||
self.id_thread = None
|
||||
self.progdialog = None
|
||||
self.search_thread = None
|
||||
|
||||
self.use_filter = self.settings.always_use_publisher_filter
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed)
|
||||
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.btnRequery.clicked.connect(self.requery)
|
||||
self.btnIssues.clicked.connect(self.show_issues)
|
||||
self.btnAutoSelect.clicked.connect(self.auto_select)
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.cbxFilter.setChecked(self.use_filter)
|
||||
self.cbxFilter.toggled.connect(self.filter_toggled)
|
||||
|
||||
self.update_buttons()
|
||||
self.perform_query()
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def update_buttons(self):
|
||||
enabled = self.cv_search_results is not None and len(self.cv_search_results) > 0
|
||||
def updateButtons(self):
|
||||
if self.cv_search_results is not None and len(
|
||||
self.cv_search_results) > 0:
|
||||
enabled = True
|
||||
else:
|
||||
enabled = False
|
||||
|
||||
self.btnRequery.setEnabled(enabled)
|
||||
self.btnIssues.setEnabled(enabled)
|
||||
self.btnAutoSelect.setEnabled(enabled)
|
||||
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(enabled)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(enabled)
|
||||
|
||||
def requery(self):
|
||||
self.perform_query(refresh=True)
|
||||
def requery(self,):
|
||||
self.performQuery(refresh=True)
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def filter_toggled(self):
|
||||
self.use_filter = not self.use_filter
|
||||
self.perform_query(refresh=False)
|
||||
|
||||
def auto_select(self):
|
||||
def autoSelect(self):
|
||||
|
||||
if self.comic_archive is None:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select", "You need to load a comic first!")
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Auto-Select", "You need to load a comic first!")
|
||||
return
|
||||
|
||||
if self.issue_number is None or self.issue_number == "":
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select", "Can't auto-select without an issue number (yet!)")
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select",
|
||||
"Can't auto-select without an issue number (yet!)")
|
||||
return
|
||||
|
||||
self.iddialog = IDProgressWindow(self)
|
||||
self.iddialog.setModal(True)
|
||||
self.iddialog.rejected.connect(self.identify_cancel)
|
||||
self.iddialog.rejected.connect(self.identifyCancel)
|
||||
self.iddialog.show()
|
||||
|
||||
self.ii = IssueIdentifier(self.comic_archive, self.settings)
|
||||
@@ -189,199 +175,165 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
md.series = self.series_name
|
||||
md.issue = self.issue_number
|
||||
md.year = self.year
|
||||
md.issue_count = self.issue_count
|
||||
md.issueCount = self.issue_count
|
||||
|
||||
self.ii.set_additional_metadata(md)
|
||||
self.ii.only_use_additional_meta_data = True
|
||||
self.ii.setAdditionalMetadata(md)
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
self.id_thread = IdentifyThread(self.ii)
|
||||
self.id_thread.identifyComplete.connect(self.identify_complete)
|
||||
self.id_thread.identifyLogMsg.connect(self.log_id_output)
|
||||
self.id_thread.identifyProgress.connect(self.identify_progress)
|
||||
self.id_thread.identifyComplete.connect(self.identifyComplete)
|
||||
self.id_thread.identifyLogMsg.connect(self.logIDOutput)
|
||||
self.id_thread.identifyProgress.connect(self.identifyProgress)
|
||||
|
||||
self.id_thread.start()
|
||||
|
||||
self.iddialog.exec()
|
||||
self.iddialog.exec_()
|
||||
|
||||
def log_id_output(self, text):
|
||||
print(str(text), end=" ")
|
||||
def logIDOutput(self, text):
|
||||
print unicode(text),
|
||||
self.iddialog.textEdit.ensureCursorVisible()
|
||||
self.iddialog.textEdit.insertPlainText(text)
|
||||
|
||||
def identify_progress(self, cur, total):
|
||||
def identifyProgress(self, cur, total):
|
||||
self.iddialog.progressBar.setMaximum(total)
|
||||
self.iddialog.progressBar.setValue(cur)
|
||||
|
||||
def identify_cancel(self):
|
||||
def identifyCancel(self):
|
||||
self.ii.cancel = True
|
||||
|
||||
def identify_complete(self):
|
||||
def identifyComplete(self):
|
||||
|
||||
matches = self.ii.match_list
|
||||
result = self.ii.search_result
|
||||
match_index = 0
|
||||
|
||||
found_match = None
|
||||
choices = False
|
||||
if result == self.ii.result_no_matches:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.result_found_match_but_bad_cover_score:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!"
|
||||
)
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.result_found_match_but_not_first_page:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Auto-Select Result", " Found a match, but not with the first page of the archive."
|
||||
)
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found a match, but not with the first page of the archive.")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.result_multiple_matches_with_bad_image_scores:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually."
|
||||
)
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found some possibilities, but no confidence. Proceed manually.")
|
||||
choices = True
|
||||
elif result == self.ii.result_one_good_match:
|
||||
elif result == self.ii.ResultOneGoodMatch:
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.result_multiple_good_matches:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Auto-Select Result", " Found multiple likely matches. Please select."
|
||||
)
|
||||
elif result == self.ii.ResultMultipleGoodMatches:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
selector = MatchSelectionWindow(self, matches, self.comic_archive)
|
||||
selector.setModal(True)
|
||||
selector.exec()
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
# we should now have a list index
|
||||
found_match = selector.current_match()
|
||||
found_match = selector.currentMatch()
|
||||
|
||||
if found_match is not None:
|
||||
self.iddialog.accept()
|
||||
|
||||
self.volume_id = found_match["volume_id"]
|
||||
self.issue_number = found_match["issue_number"]
|
||||
self.select_by_id()
|
||||
self.show_issues()
|
||||
self.volume_id = found_match['volume_id']
|
||||
self.issue_number = found_match['issue_number']
|
||||
self.selectByID()
|
||||
self.showIssues()
|
||||
|
||||
def show_issues(self):
|
||||
selector = IssueSelectionWindow(self, self.settings, self.volume_id, self.issue_number)
|
||||
def showIssues(self):
|
||||
selector = IssueSelectionWindow(
|
||||
self, self.settings, self.volume_id, self.issue_number)
|
||||
title = ""
|
||||
for record in self.cv_search_results:
|
||||
if record["id"] == self.volume_id:
|
||||
title = record["name"]
|
||||
title += " (" + str(record["start_year"]) + ")"
|
||||
if record['id'] == self.volume_id:
|
||||
title = record['name']
|
||||
title += " (" + unicode(record['start_year']) + ")"
|
||||
title += " - "
|
||||
break
|
||||
|
||||
selector.setWindowTitle(title + "Select Issue")
|
||||
selector.setModal(True)
|
||||
selector.exec()
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
# we should now have a volume ID
|
||||
self.issue_number = selector.issue_number
|
||||
self.accept()
|
||||
return
|
||||
|
||||
def select_by_id(self):
|
||||
def selectByID(self):
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
volume_id = self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
if volume_id == self.volume_id:
|
||||
volume_id, b = self.twList.item(
|
||||
r, 0).data(QtCore.Qt.UserRole).toInt()
|
||||
if (volume_id == self.volume_id):
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def perform_query(self, refresh=False):
|
||||
def performQuery(self, refresh=False):
|
||||
|
||||
self.progdialog = QtWidgets.QProgressDialog("Searching Online", "Cancel", 0, 100, self)
|
||||
self.progdialog = QtGui.QProgressDialog(
|
||||
"Searching Online", "Cancel", 0, 100, self)
|
||||
self.progdialog.setWindowTitle("Online Search")
|
||||
self.progdialog.canceled.connect(self.search_canceled)
|
||||
self.progdialog.canceled.connect(self.searchCanceled)
|
||||
self.progdialog.setModal(True)
|
||||
self.progdialog.setMinimumDuration(300)
|
||||
|
||||
self.search_thread = SearchThread(self.series_name, refresh)
|
||||
self.search_thread.searchComplete.connect(self.search_complete)
|
||||
self.search_thread.progressUpdate.connect(self.search_progress_update)
|
||||
self.search_thread.searchComplete.connect(self.searchComplete)
|
||||
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
|
||||
self.search_thread.start()
|
||||
self.progdialog.exec()
|
||||
|
||||
def search_canceled(self):
|
||||
logger.info("query cancelled")
|
||||
self.search_thread.searchComplete.disconnect(self.search_complete)
|
||||
self.search_thread.progressUpdate.disconnect(self.search_progress_update)
|
||||
self.progdialog.canceled.disconnect(self.search_canceled)
|
||||
# QtCore.QCoreApplication.processEvents()
|
||||
self.progdialog.exec_()
|
||||
|
||||
def searchCanceled(self):
|
||||
print("query cancelled")
|
||||
self.search_thread.searchComplete.disconnect(self.searchComplete)
|
||||
self.search_thread.progressUpdate.disconnect(self.searchProgressUpdate)
|
||||
self.progdialog.canceled.disconnect(self.searchCanceled)
|
||||
self.progdialog.reject()
|
||||
QtCore.QTimer.singleShot(200, self.close_me)
|
||||
QtCore.QTimer.singleShot(200, self.closeMe)
|
||||
|
||||
def close_me(self):
|
||||
def closeMe(self):
|
||||
print("closeme")
|
||||
self.reject()
|
||||
|
||||
def search_progress_update(self, current, total):
|
||||
def searchProgressUpdate(self, current, total):
|
||||
self.progdialog.setMaximum(total)
|
||||
self.progdialog.setValue(current + 1)
|
||||
self.progdialog.setValue(current)
|
||||
|
||||
def search_complete(self):
|
||||
def searchComplete(self):
|
||||
self.progdialog.accept()
|
||||
del self.progdialog
|
||||
if self.search_thread.cv_error:
|
||||
if self.search_thread.error_code == ComicVineTalkerException.RateLimit:
|
||||
QtWidgets.QMessageBox.critical(self, "Comic Vine Error", ComicVineTalker.get_rate_limit_message())
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Comic Vine Error"),
|
||||
ComicVineTalker.getRateLimitMessage())
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Network Issue", "Could not connect to Comic Vine to search for series!"
|
||||
)
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Network Issue"),
|
||||
self.tr("Could not connect to Comic Vine to search for series!"))
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
# filter the publishers if enabled set
|
||||
if self.use_filter:
|
||||
try:
|
||||
publisher_filter = {s.strip().lower() for s in self.settings.id_publisher_filter.split(",")}
|
||||
# use '' as publisher name if None
|
||||
self.cv_search_results = list(
|
||||
filter(
|
||||
lambda d: ("" if d["publisher"] is None else str(d["publisher"]["name"]).lower())
|
||||
not in publisher_filter,
|
||||
self.cv_search_results,
|
||||
)
|
||||
)
|
||||
except:
|
||||
logger.exception("bad data error filtering filter publishers")
|
||||
|
||||
# pre sort the data - so that we can put exact matches first afterwards
|
||||
# compare as str incase 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.settings.sort_series_by_year:
|
||||
try:
|
||||
self.cv_search_results = sorted(
|
||||
self.cv_search_results,
|
||||
key=lambda i: (str(i["start_year"]), str(i["count_of_issues"])),
|
||||
reverse=True,
|
||||
)
|
||||
except:
|
||||
logger.exception("bad data error sorting results by start_year,count_of_issues")
|
||||
else:
|
||||
try:
|
||||
self.cv_search_results = sorted(
|
||||
self.cv_search_results, key=lambda i: str(i["count_of_issues"]), reverse=True
|
||||
)
|
||||
except:
|
||||
logger.exception("bad data error sorting results by count_of_issues")
|
||||
|
||||
# move sanitized matches to the front
|
||||
if self.settings.exact_series_matches_first:
|
||||
try:
|
||||
sanitized = utils.sanitize_title(self.series_name)
|
||||
exact_matches = list(
|
||||
filter(lambda d: utils.sanitize_title(str(d["name"])) in sanitized, self.cv_search_results)
|
||||
)
|
||||
non_matches = list(
|
||||
filter(lambda d: utils.sanitize_title(str(d["name"])) not in sanitized, self.cv_search_results)
|
||||
)
|
||||
self.cv_search_results = exact_matches + non_matches
|
||||
except:
|
||||
logger.exception("bad data error filtering exact/near matches")
|
||||
|
||||
self.update_buttons()
|
||||
self.updateButtons()
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
@@ -392,71 +344,75 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
for record in self.cv_search_results:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record["name"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, record["id"])
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, record['id'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = str(record["start_year"])
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = str(record['start_year'])
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record["count_of_issues"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, record["count_of_issues"])
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
item_text = record['count_of_issues']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if record["publisher"] is not None:
|
||||
item_text = record["publisher"]["name"]
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
if record['publisher'] is not None:
|
||||
item_text = record['publisher']['name']
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.DescendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
if len(self.cv_search_results) == 0:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtWidgets.QMessageBox.information(self, "Search Result", "No matches found!")
|
||||
QtCore.QTimer.singleShot(200, self.close_me)
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Search Result", "No matches found!")
|
||||
|
||||
if self.immediate_autoselect and len(self.cv_search_results) > 0:
|
||||
# defer the immediate autoselect so this dialog has time to pop up
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QTimer.singleShot(10, self.do_immediate_autoselect)
|
||||
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
|
||||
|
||||
def do_immediate_autoselect(self):
|
||||
def doImmediateAutoselect(self):
|
||||
self.immediate_autoselect = False
|
||||
self.auto_select()
|
||||
self.autoSelect()
|
||||
|
||||
def cell_double_clicked(self, r, c):
|
||||
self.show_issues()
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.showIssues()
|
||||
|
||||
def current_item_changed(self, curr, prev):
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
self.volume_id, b = self.twList.item(
|
||||
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
|
||||
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record["id"] == self.volume_id:
|
||||
if record["description"] is None:
|
||||
if record['id'] == self.volume_id:
|
||||
if record['description'] is None:
|
||||
self.teDetails.setText("")
|
||||
else:
|
||||
self.teDetails.setText(record["description"])
|
||||
self.imageWidget.set_url(record["image"]["super_url"])
|
||||
self.teDetails.setText(record['description'])
|
||||
self.imageWidget.setURL(record['image']['super_url'])
|
||||
break
|
||||
|
||||
1
current_version.txt
Normal file
1
current_version.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.1.16-beta-rc
|
||||
@@ -1,11 +0,0 @@
|
||||
[Desktop Entry]
|
||||
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
|
||||
Icon=/usr/local/share/comictagger/app.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=text/plain;
|
||||
Categories=Application;
|
||||
@@ -1,4 +0,0 @@
|
||||
This file is a placeholder that will automaticlly 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.
|
||||
11
google/gadgets/social.xml
Normal file
11
google/gadgets/social.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&width=292&colorscheme=light&show_faces =false&border_color&stream=false&header=false&height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|
||||
10
google/gadgets/twitter.xml
Normal file
10
google/gadgets/twitter.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|
||||
260
google/googlecode_upload.py
Executable file
260
google/googlecode_upload.py
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
|
||||
# Author: danderson@google.com (David Anderson)
|
||||
#
|
||||
# Script for uploading files to a Google Code project.
|
||||
#
|
||||
# This is intended to be both a useful script for people who want to
|
||||
# streamline project uploads and a reference implementation for
|
||||
# uploading files to Google Code projects.
|
||||
#
|
||||
# To upload a file to Google Code, you need to provide a path to the
|
||||
# file on your local machine, a small summary of what the file is, a
|
||||
# project name, and a valid account that is a member or owner of that
|
||||
# project. You can optionally provide a list of labels that apply to
|
||||
# the file. The file will be uploaded under the same name that it has
|
||||
# in your local filesystem (that is, the "basename" or last path
|
||||
# component). Run the script with '--help' to get the exact syntax
|
||||
# and available options.
|
||||
#
|
||||
# Note that the upload script requests that you enter your
|
||||
# googlecode.com password. This is NOT your Gmail account password!
|
||||
# This is the password you use on googlecode.com for committing to
|
||||
# Subversion and uploading files. You can find your password by going
|
||||
# to http://code.google.com/hosting/settings when logged in with your
|
||||
# Gmail account. If you have already committed to your project's
|
||||
# Subversion repository, the script will automatically retrieve your
|
||||
# credentials from there (unless disabled, see the output of '--help'
|
||||
# for details).
|
||||
#
|
||||
# If you are looking at this script as a reference for implementing
|
||||
# your own Google Code file uploader, then you should take a look at
|
||||
# the upload() function, which is the meat of the uploader. You
|
||||
# basically need to build a multipart/form-data POST request with the
|
||||
# right fields and send it to https://PROJECT.googlecode.com/files .
|
||||
# Authenticate the request using HTTP Basic authentication, as is
|
||||
# shown below.
|
||||
#
|
||||
# Licensed under the terms of the Apache Software License 2.0:
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Questions, comments, feature requests and patches are most welcome.
|
||||
# Please direct all of these to the Google Code users group:
|
||||
# http://groups.google.com/group/google-code-hosting
|
||||
|
||||
"""Google Code file uploader script.
|
||||
"""
|
||||
|
||||
__author__ = 'danderson@google.com (David Anderson)'
|
||||
|
||||
import httplib
|
||||
import os.path
|
||||
import optparse
|
||||
import getpass
|
||||
import base64
|
||||
import sys
|
||||
|
||||
|
||||
def upload(file, project_name, user_name, password, summary, labels=None):
|
||||
"""Upload a file to a Google Code project's file server.
|
||||
|
||||
Args:
|
||||
file: The local path to the file.
|
||||
project_name: The name of your project on Google Code.
|
||||
user_name: Your Google account name.
|
||||
password: The googlecode.com password for your account.
|
||||
Note that this is NOT your global Google Account password!
|
||||
summary: A small description for the file.
|
||||
labels: an optional list of label strings with which to tag the file.
|
||||
|
||||
Returns: a tuple:
|
||||
http_status: 201 if the upload succeeded, something else if an
|
||||
error occured.
|
||||
http_reason: The human-readable string associated with http_status
|
||||
file_url: If the upload succeeded, the URL of the file on Google
|
||||
Code, None otherwise.
|
||||
"""
|
||||
# The login is the user part of user@gmail.com. If the login provided
|
||||
# is in the full user@domain form, strip it down.
|
||||
if user_name.endswith('@gmail.com'):
|
||||
user_name = user_name[:user_name.index('@gmail.com')]
|
||||
|
||||
form_fields = [('summary', summary)]
|
||||
if labels is not None:
|
||||
form_fields.extend([('label', l.strip()) for l in labels])
|
||||
|
||||
content_type, body = encode_upload_request(form_fields, file)
|
||||
|
||||
upload_host = '%s.googlecode.com' % project_name
|
||||
upload_uri = '/files'
|
||||
auth_token = base64.b64encode('%s:%s' % (user_name, password))
|
||||
headers = {
|
||||
'Authorization': 'Basic %s' % auth_token,
|
||||
'User-Agent': 'Googlecode.com uploader v0.9.4',
|
||||
'Content-Type': content_type,
|
||||
}
|
||||
|
||||
server = httplib.HTTPSConnection(upload_host)
|
||||
server.request('POST', upload_uri, body, headers)
|
||||
resp = server.getresponse()
|
||||
server.close()
|
||||
|
||||
if resp.status == 201:
|
||||
location = resp.getheader('Location', None)
|
||||
else:
|
||||
location = None
|
||||
return resp.status, resp.reason, location
|
||||
|
||||
|
||||
def encode_upload_request(fields, file_path):
|
||||
"""Encode the given fields and file into a multipart form body.
|
||||
|
||||
fields is a sequence of (name, value) pairs. file is the path of
|
||||
the file to upload. The file will be uploaded to Google Code with
|
||||
the same file name.
|
||||
|
||||
Returns: (content_type, body) ready for httplib.HTTP instance
|
||||
"""
|
||||
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
|
||||
CRLF = '\r\n'
|
||||
|
||||
body = []
|
||||
|
||||
# Add the metadata about the upload first
|
||||
for key, value in fields:
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
'',
|
||||
value,
|
||||
])
|
||||
|
||||
# Now add the file itself
|
||||
file_name = os.path.basename(file_path)
|
||||
f = open(file_path, 'rb')
|
||||
file_content = f.read()
|
||||
f.close()
|
||||
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="filename"; filename="%s"'
|
||||
% file_name,
|
||||
# The upload server determines the mime-type, no need to set it.
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
file_content,
|
||||
])
|
||||
|
||||
# Finalize the form body
|
||||
body.extend(['--' + BOUNDARY + '--', ''])
|
||||
|
||||
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
|
||||
|
||||
|
||||
def upload_find_auth(file_path, project_name, summary, labels=None,
|
||||
user_name=None, password=None, tries=3):
|
||||
"""Find credentials and upload a file to a Google Code project's file server.
|
||||
|
||||
file_path, project_name, summary, and labels are passed as-is to upload.
|
||||
|
||||
Args:
|
||||
file_path: The local path to the file.
|
||||
project_name: The name of your project on Google Code.
|
||||
summary: A small description for the file.
|
||||
labels: an optional list of label strings with which to tag the file.
|
||||
config_dir: Path to Subversion configuration directory, 'none', or None.
|
||||
user_name: Your Google account name.
|
||||
tries: How many attempts to make.
|
||||
"""
|
||||
if user_name is None or password is None:
|
||||
from netrc import netrc
|
||||
authenticators = netrc().authenticators("code.google.com")
|
||||
if authenticators:
|
||||
if user_name is None:
|
||||
user_name = authenticators[0]
|
||||
if password is None:
|
||||
password = authenticators[2]
|
||||
|
||||
while tries > 0:
|
||||
if user_name is None:
|
||||
# Read username if not specified or loaded from svn config, or on
|
||||
# subsequent tries.
|
||||
sys.stdout.write('Please enter your googlecode.com username: ')
|
||||
sys.stdout.flush()
|
||||
user_name = sys.stdin.readline().rstrip()
|
||||
if password is None:
|
||||
# Read password if not loaded from svn config, or on subsequent
|
||||
# tries.
|
||||
print 'Please enter your googlecode.com password.'
|
||||
print '** Note that this is NOT your Gmail account password! **'
|
||||
print 'It is the password you use to access Subversion repositories,'
|
||||
print 'and can be found here: http://code.google.com/hosting/settings'
|
||||
password = getpass.getpass()
|
||||
|
||||
status, reason, url = upload(
|
||||
file_path, project_name, user_name, password, summary, labels)
|
||||
# Returns 403 Forbidden instead of 401 Unauthorized for bad
|
||||
# credentials as of 2007-07-17.
|
||||
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
|
||||
# Rest for another try.
|
||||
user_name = password = None
|
||||
tries = tries - 1
|
||||
else:
|
||||
# We're done.
|
||||
break
|
||||
|
||||
return status, reason, url
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
|
||||
'-p PROJECT [options] FILE')
|
||||
parser.add_option('-s', '--summary', dest='summary',
|
||||
help='Short description of the file')
|
||||
parser.add_option('-p', '--project', dest='project',
|
||||
help='Google Code project name')
|
||||
parser.add_option('-u', '--user', dest='user',
|
||||
help='Your Google Code username')
|
||||
parser.add_option('-w', '--password', dest='password',
|
||||
help='Your Google Code password')
|
||||
parser.add_option(
|
||||
'-l',
|
||||
'--labels',
|
||||
dest='labels',
|
||||
help='An optional list of comma-separated labels to attach '
|
||||
'to the file')
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if not options.summary:
|
||||
parser.error('File summary is missing.')
|
||||
elif not options.project:
|
||||
parser.error('Project name is missing.')
|
||||
elif len(args) < 1:
|
||||
parser.error('File to upload not provided.')
|
||||
elif len(args) > 1:
|
||||
parser.error('Only one file may be specified.')
|
||||
|
||||
file_path = args[0]
|
||||
|
||||
if options.labels:
|
||||
labels = options.labels.split(',')
|
||||
else:
|
||||
labels = None
|
||||
|
||||
status, reason, url = upload_find_auth(file_path, options.project,
|
||||
options.summary, labels,
|
||||
options.user, options.password)
|
||||
if url:
|
||||
print 'The file was uploaded successfully.'
|
||||
print 'URL: %s' % url
|
||||
return 0
|
||||
else:
|
||||
print 'An error occurred. Your file was not uploaded.'
|
||||
print 'Google Code upload server said: %s (%s)' % (reason, status)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
48
localefix.py
48
localefix.py
@@ -1,48 +0,0 @@
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _lang_code_mac():
|
||||
"""
|
||||
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():
|
||||
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())
|
||||
sys.stderr.reconfigure(encoding=sys.getdefaultencoding())
|
||||
sys.stdin.reconfigure(encoding=sys.getdefaultencoding())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user