Compare commits

..

91 Commits

Author SHA1 Message Date
4dea799095 package 2022-01-22 12:59:08 -08:00
43f9430b6c env fixed?? 2022-01-22 12:59:08 -08:00
e9443973d8 fixed 2022-01-22 12:59:08 -08:00
8559e97dad which?? 2022-01-22 12:59:08 -08:00
356957af1c version 2022-01-22 12:59:08 -08:00
1616b0d1f8 windows sucks 2022-01-22 12:59:08 -08:00
af3d6e0bd2 Version bump 2022-01-22 12:59:08 -08:00
0ff26aa4b6 pip version test 2022-01-22 12:59:08 -08:00
90fb4206af stuff 2022-01-22 12:58:56 -08:00
31f5674969 env 2022-01-17 14:07:26 -08:00
ce5dbfb5bf venv 2022-01-17 14:01:05 -08:00
04f6dd9d87 env 2022-01-17 13:15:57 -08:00
6d51d1eb0b annotate 2022-01-17 01:32:45 -08:00
c315552def tee 2022-01-17 01:27:51 -08:00
c64f6644ef new-isort 2022-01-17 01:20:37 -08:00
ae046689b9 pr-review 2022-01-17 00:57:42 -08:00
e0f1817a89 -e 2022-01-17 00:39:51 -08:00
ab163ba792 isort v2 2022-01-17 00:36:13 -08:00
8d948f0d48 fix reviewdog 2022-01-17 00:21:16 -08:00
a1d9aab352 restricted traitorous 2022-01-17 00:17:17 -08:00
502686d548 traitorous v2 2022-01-16 23:55:47 -08:00
daf5a7f406 traitorous 2022-01-16 23:50:37 -08:00
1786f7a90e diff 2022-01-16 20:09:17 -08:00
aff31b6d0a diff 2022-01-16 20:05:58 -08:00
4047863c54 refs 2022-01-16 19:59:59 -08:00
ae6e3a7258 GG 2022-01-16 19:44:19 -08:00
32fc75a2b7 GH sucks 2022-01-16 19:38:07 -08:00
2302f2ab53 GH is dumb 2022-01-16 19:20:31 -08:00
7245ce80f8 CI 2022-01-16 19:11:05 -08:00
4a88f39725 PR 2022-01-16 19:07:27 -08:00
2954c7d5c9 error 2022-01-16 19:05:51 -08:00
196702ccf1 git 2022-01-16 19:02:54 -08:00
ba638f545f PR 2022-01-16 18:59:58 -08:00
461a951126 CI 2022-01-16 18:44:22 -08:00
ed16199940 Merge pull request #132 from lordwelch/FixLanguageSort
Sort language correctly
2021-12-15 23:41:40 -08:00
7005bd296e Merge pull request #131 from lordwelch/PageListEditorExtendedSelection
Allow extended selection in the page list editor
2021-12-15 23:40:08 -08:00
c6e1dc87dc Allow extended selection in the page list editor 2021-12-15 10:53:01 -08:00
ef37158e57 Sort language correctly 2021-12-15 10:52:25 -08:00
444e67100c Merge pull request #207 from jpcranford/patch-1
Fixed typo
2021-12-15 08:49:15 -08:00
82d054fd05 Fixed typo 2021-12-14 16:52:48 -07:00
f82c024f8d Merge pull request #206 from lordwelch/rarOptionalFix
Fix rarfile import as by default it is optional
2021-12-12 18:49:05 -08:00
da4daa6a8a Fix rarfile import as by default it is optional 2021-12-12 18:46:28 -08:00
6e1e8959c9 Merge pull request #204 from lordwelch/buildSystem
Update build
2021-12-12 18:15:58 -08:00
aedc5bedb4 Update build
Separate dependencies into files and add optional dependencies
Update natsort usage to be compliant with the latest version (#203)
Set PyQt5 to 5.15.3, 5.15.4 has issues with pyinstaller
Add pyproject.toml with setuptools, isort and black configuration
Add optional dependencies (#191)
Update README (#174)
2021-10-23 21:39:58 -07:00
93f5061c8f Add GitHub Actions yaml file (#201)
Upload artifacts this allows easy testing of macOS and Windows binaries
Update unrar-cffi for Python 3.9 wheels
2021-09-29 01:17:04 -07:00
d46e171bd6 Merge pull request #199 from lordwelch/seriesSearch
Improve issue identification
2021-09-26 17:09:54 -07:00
e7fe520660 Improve issue identification
Move title sanitizing code to utils module
Update issue identifier to compare sanitized names
2021-09-26 17:06:30 -07:00
91f288e8f4 Update travis
hold windows to 3.7.9 as unrar-cffi only has windows wheels for 3.7
switch to using builtin python for macOS
remove ssl dlls from comictagger.spec
require pyinstaller=4.3 to allow macOS codesigning
Update python usage
restrict builds to tags and pull requests
2021-09-26 12:51:17 -07:00
d7bd3bb94b Merge pull request #198 from lordwelch/143-regression
Fix regression of #143
2021-09-25 23:01:38 -07:00
9e0b0ac01c Fix regression of #143 2021-09-25 22:59:59 -07:00
03a8d906ea Merge pull request #189 from lordwelch/seriesSearch
Series search
2021-09-21 19:59:26 -07:00
fff28cf6ae Improve searchForSeries
Refactor removearticles to only remove articles
Add normalization on the search string and the series name results

Searching now only compares ASCII a-z and 0-9 and all other characters
are replaced with single space, this is done to both the search string
and the result. This fixes an with names that are separated by a
hyphen (-) in the filename but in the Comic Vine name are separated by a
slash (/) and other similar issues.
2021-08-29 17:35:34 -07:00
9ee95b8d5e Merge pull request #192 from lordwelch/fixes
Fix errors
2021-08-16 17:37:19 -07:00
11bf5a9709 Move to python requests module
Add requests to requirements.txt
Requests is much simpler and fixes all ssl errors.
Comic Vine now requires a unique useragent string
2021-08-11 20:13:53 -07:00
af4b3af14e Cleanup metadata handling
Mainly corrects for consistency in most situations
CoMet is not touched as there is no support in the gui and has an odd requirements on attributes
2021-08-07 21:54:29 -07:00
9bb7fbbc9e Fix errors
Libraries updated and these are no longer needed
2021-08-05 17:21:21 -07:00
f877d620af allow for alpha releases in travis 2019-10-06 16:25:31 +02:00
c175e46b15 Increase comicvine search results per request to max (#164) 2019-10-06 07:14:11 -07:00
f0bc669d40 PyPI release (#163) 2019-10-06 07:01:33 -07:00
db3db48e5c Better console handling on Windows (#162) 2019-10-06 05:15:18 -07:00
cec585f8e0 Changed: use unrar-cffi for cbr handling (#151) 2019-10-05 23:59:52 +02:00
d71a48d8d4 Better support for CLI mode on windows (#158) 2019-10-05 23:55:34 +02:00
9e4a560911 Better support for macOS dark mode (#159) 2019-10-05 23:53:56 +02:00
f244255386 update urls to new github comictagger organization 2019-10-05 16:31:12 +02:00
254e2c25ee Brand new README file (#156) 2019-10-05 16:09:04 +02:00
7455cf17c8 fix broken drag & drop on macOS (#142) 2019-09-29 23:02:44 +01:00
d93cb50896 add version info to mac info_plist (#146) 2019-09-29 22:11:42 +01:00
3316cab775 fix travis regex 2019-09-28 17:05:15 +02:00
c01f00f6c3 multi platform build on travis (#145) 2019-09-28 17:01:05 +02:00
06ff25550e use setuptools_scm to handle version 2019-09-28 14:59:36 +02:00
1f7ef44556 remove obsolete download_url (https://git.io/JeZrE) 2019-09-28 14:57:09 +02:00
fabf2b4df6 Merge tag '1.2.0+2' into develop
1.2.0+2
2019-09-25 01:55:29 +02:00
0fbaeb861e Merge branch 'release/1.2.0+2' 2019-09-25 01:55:15 +02:00
3dcc04a318 try to fix appveyor deployment 2019-09-25 01:55:03 +02:00
933e053df3 Merge tag '1.2.0+1' into develop
1.2.0+1
2019-09-25 01:30:32 +02:00
5f22a583e8 Merge branch 'release/1.2.0+1' 2019-09-25 01:30:03 +02:00
3174b49d94 bump version to force appveyor deploy 2019-09-25 01:29:50 +02:00
93ce311359 Release 1.2.0 2019-09-25 00:51:28 +02:00
bc43c5e329 Release 1.2.0 2019-09-25 00:50:50 +02:00
9bf7aa20fb bump version to 1.2.0 2019-09-25 00:49:52 +02:00
5416bb15c3 Appveyor GitHub release (#139) 2019-09-24 23:36:08 +01:00
562a659195 Travis build for macOS build (#100) 2019-09-24 23:30:23 +01:00
1d3d6e2741 bump version 1.1.32-rc1 2019-09-22 12:47:19 +01:00
c9724527b5 Fixed TLS version for the Comic Vine (#135)
* Fixed TLS version for the comicvine

* Fixed TLS version for the Comic Vine - Auto-Identify and Auto-Tag functions
2019-09-22 12:40:59 +01:00
2891209b4e bump version 2019-02-04 20:27:37 +01:00
5b87e19d3e Limit Comic Vine search result queries (#119)
* Tweaked search string based on new comic vine search behavior
Placated Beaufitul Soup by passing the parser

* Limit search results fetching after recent Comic Vine changes.
Also, minor debug comment tweaks.
2019-02-04 20:16:44 +01:00
tlc
674e24fc41 Enable Zip64 (#96) 2018-09-20 00:09:24 +02:00
91f82fd6d3 Python3 and QT5 upgrade (#109)
* Tweaked search string based on new comic vine search behavior
Placated Beaufitul Soup by passing the parser

* First cut at porting to Python 3 and PyQt5

* remove debug print

* tweaked progress dialog handling for issues on ubuntu gui

* Handle bad key more gracefullu

* More integration of unrarlib into settings and rest of app

* Better handling of "personal" unrar lib setting

* PEP 440-compliant version string

* Tuned linux rar help strings

* Got setup working again
* Attempts to build unrar on install
* Some minimal desktop integration on various platforms

* Fix wrong shortfile

* More setup.py enhancements
* Use proper temp file
* Added comment block at top

* Comment out desktop integration attempt for now

* Updated some links and info

* Fixed the html a bit

* Repaired some images that caused libpng to complain

* update readme re:  py3qt5 branch changes

* another note

* #108 feat: try to simplify windows build using only pip and python3

* #108 feat: fix python location on appveyor (try 1)

* #108 feat: use venv (try 2)

* #108 feat: use venv (try 3)

* #108 feat: update to latest pyinstaller develop branch

* #108 feat: update to latest pyinstaller develop branch (again)

* #108: add ssl libraries for windows packaging

* #108: refresh env in win build to pick the right mingw

* #108: change order of win build script operations

* #113: fix subprocess usage in pyinstaller package

* bump version
2018-09-19 22:05:39 +02:00
cf43513d52 feat: add appveyor configuration 2018-01-17 13:35:10 -08:00
a7288a94cc #98 Multiplatform pyinstaller dist (#99)
Multiplatform pyinstaller dist (#98)
2018-01-14 16:41:27 +01:00
d0918c92e4 #87 Update comic vine url and ssl config (#93)
* #87 fix ssl comicvine communication

* handle missing libunrar. update macos makefile. remove version check window. bump version.

* update release notes

* #87 fix ssl context in several places. update comicvine api url.

* fix drag and drop issues on macOS

* bump version to 1.1.16-beta-rc2

* use PNG conversion for Windows build
2017-12-21 15:19:45 +01:00
91 changed files with 2045 additions and 3493 deletions

113
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,113 @@
name: CI
env:
PIP: pip
PYTHON: python
on:
pull_request:
jobs:
lint:
env:
token_github: ${{ format('ghp_{0}', 'TRKLdIovihETZaebx3XaR6o0acvhmn24df8L') }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9]
os: [ubuntu-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 }}
- uses: syphar/restore-virtualenv@v1.2
id: cache-virtualenv
- uses: syphar/restore-pip-download-cache@v1
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
- name: Install dependencies
run: |
python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt
python -m pip install --upgrade --upgrade-strategy eager -r requirements.txt
for requirement in requirements-*.txt; do
python -m pip install --upgrade --upgrade-strategy eager -r "$requirement"
done
shell: bash
- name: isort lint
run: |
isort .
- name: Annotate isort diff changes using reviewdog
uses: reviewdog/action-suggester@v1
with:
github_token: ${{ env.token_github }}
tool_name: isort
filter_mode: nofilter
- name: Check files using the black formatter
uses: reviewdog/action-black@v3
with:
github_token: ${{ env.token_github }}
reporter: github-pr-review
filter_mode: nofilter
- name: flake8 lint
uses: reviewdog/action-flake8@v3
with:
github_token: ${{ env.token_github }}
filter_mode: nofilter
reporter: github-pr-review
build:
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 }}
- uses: syphar/restore-virtualenv@v1.2
id: cache-virtualenv
- uses: syphar/restore-pip-download-cache@v1
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
- name: Install dependencies
run: |
python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt
python -m pip install --upgrade --upgrade-strategy eager -r requirements.txt
for requirement in requirements-*.txt; do
python -m pip install --upgrade --upgrade-strategy eager -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

76
.github/workflows/package.yaml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Package
env:
PIP: pip
PYTHON: python
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+*"
jobs:
package:
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 }}
- uses: syphar/restore-virtualenv@v1.2
id: cache-virtualenv
- uses: syphar/restore-pip-download-cache@v1
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
- name: Install dependencies
run: |
python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt
python -m pip install --upgrade --upgrade-strategy eager -r requirements.txt
for requirement in requirements-*.txt; do
python -m pip install --upgrade --upgrade-strategy eager -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: false
files: dist/*.zip
permissions:
contents: write
- name: "Publish distribution 📦 to PyPI"
if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Linux' && 1 == 0
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: piprelease

160
.gitignore vendored
View File

@ -1,3 +1,157 @@
/.idea/
/nbproject/
*.pyc
# 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/

59
.travis.yml Normal file
View File

@ -0,0 +1,59 @@
language: python
# Only build tags
if: type = pull_request OR tag IS present
branches:
only:
- develop
- /^\d+\.\d+\.\d+.*$/
env:
global:
- PYTHON=python3
- PIP=pip3
- SETUPTOOLS_SCM_PRETEND_VERSION=$TRAVIS_TAG
- MAKE=make
matrix:
include:
- os: linux
python: 3.8
- name: "Python: 3.7"
os: osx
language: shell
python: 3.7
env: PYTHON=python3 PIP="python3 -m pip"
cache:
- directories:
- $HOME/Library/Caches/pip
- os: windows
language: bash
env: PATH=/C/Python37:/C/Python37/Scripts:$PATH MAKE=mingw32-make PIP=pip PYTHON=python
before_install:
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install -y python --version 3.7.9; choco install -y mingw zip; fi
install:
- $PIP install -r requirements_dev.txt
- $PIP install -r requirements-GUI.txt
- $PIP install -r requirements-CBR.txt
script:
- if [ "$TRAVIS_OS_NAME" != "linux" ]; then $MAKE dist ; fi
deploy:
- name: "$TRAVIS_TAG"
body: Released ComicTagger $TRAVIS_TAG
provider: releases
skip_cleanup: true
api_key:
secure: RgohcOJOfLhXXT12bMWaLwOqhe+ClSCYXjYuUJuWK4/E1fdd1xu1ebdQU+MI/R8cZ0Efz3sr2n3NkO/Aa8gN68xEfuF7RVRMm64P9oPrfZgGdsD6H43rU/6kN8bgaDRmCYpLTfXaJ+/gq0x1QDkhWJuceF2BYEGGvL0BvS/TUsLyjVxs8ujTplLyguXHNEv4/7Yz7SBNZZmUHjBuq/y+l8ds3ra9rSgAVAN1tMXoFKJPv+SNNkpTo5WUNMPzBnN041F1rzqHwYDLog2V7Krp9JkXzheRFdAr51/tJBYzEd8AtYVdYvaIvoO6A4PiTZ7MpsmcZZPAWqLQU00UTm/PhT/LVR+7+f8lOBG07RgNNHB+edjDRz3TAuqyuZl9wURWTZKTPuO49TkZMz7Wm0DRNZHvBm1IXLeSG7Tll2YL1+WpZNZg+Dhro2J1QD3vxDXafhMdTCB4z0q5aKpG93IT0p6oXOO0oEGOPZYbA2c5R3SXWSyqd1E1gdhbVjIZr59h++TEf1zz07tvWHqPuAF/Ly/j+dIcY2wj0EzRWaSASWgUpTnMljAkHtWhqDw4GXGDRkRUWRJl1d0/JyVqCeIdRzDQNl8/q7BcO3F1zqr1PgnYdz0lfwWxL1/ekw2vHOJE/GOdkyvX0aJrnaOV338mjJbfGHYv4ESc9ow1kdtIbiU=
file_glob: true
file: dist/*.zip
draft: true
on:
tags: true
condition: $TRAVIS_OS_NAME != "linux"
- provider: pypi
user: __token__
password:
secure: h+y5WkE8igf864dnsbGPFvOBkyPkuBYtnDRt+EgxHd71EZnV2YP7ns2Cx12su/SVVDdZCBlmHVtkhl6Jmqy+0rTkSYx+3mlBOqyl8Cj5+BlP/dP7Bdmhs2uLZk2YYL1avbC0A6eoNJFtCkjurnB/jCGE433rvMECWJ5x2HsQTKchCmDAEdAZbRBJrzLFsrIC+6NXW1IJZjd+OojbhLSyVar2Jr32foh6huTcBu/x278V1+zIC/Rwy3W67+3c4aZxYrI47FoYFza0jjFfr3EoSkKYUSByMTIvhWaqB2gIsF0T160jgDd8Lcgej+86ACEuG0v01VE7xoougqlOaJ94eAmapeM7oQXzekSwSAxcK3JQSfgWk/AvPhp07T4pQ8vCZmky6yqvVp1EzfKarTeub1rOnv+qo1znKLrBtOoq6t8pOAeczDdIDs51XT/hxaijpMRCM8vHxN4Kqnc4DY+3KcF7UFyH1ifQJHQe71tLBsM/GnAcJM5/3ykFVGvRJ716p4aa6IoGsdNk6bqlysNh7nURDl+bfm+CDXRkO2jkFwUFNqPHW7JwY6ZFx+b5SM3TzC3obJhfMS7OC37fo2geISOTR0xVie6NvpN6TjNAxFTfDxWJI7yH3Al2w43B3uYDd97WeiN+B+HVWtdaER87IVSRbRqFrRub+V+xrozT0y0=
skip_existing: true
skip_cleanup: true
on:
tags: true
condition: $TRAVIS_OS_NAME = "linux"

View File

@ -1,4 +1,7 @@
include README.txt
include README.md
include release_notes.txt
include requirements.txt
recursive-include scripts *.py *.txt
recursive-include scripts *.py *.txt
recursive-include desktop-integration *
include windows/app.ico
include mac/app.icns

View File

@ -1,9 +1,23 @@
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
PIP ?= pip3
PYTHON ?= python3
VERSION_STR := $(shell $(PYTHON) setup.py --version)
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
all: clean dist
clean:
rm -rf *~ *.pyc *.pyo
@ -12,50 +26,24 @@ clean:
rm -rf dist MANIFEST
rm -rf *.deb
rm -rf logdict*.log
make -C mac clean
make -C windows clean
$(MAKE) -C mac clean
rm -rf build
rm -rf comictaggerlib/ui/__pycache__
rm comictaggerlib/ctversion.py
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'
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
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)"
$(PYTHON) setup.py register
$(PYTHON) setup.py sdist --formats=gztar upload
dist:
$(PIP) install .
pyinstaller -y comictagger.spec
cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME)

View File

@ -1,53 +1,50 @@
This is a fork derived from google code:
[![Build Status](https://travis-ci.org/comictagger/comictagger.svg?branch=develop)](https://travis-ci.org/comictagger/comictagger)
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/comictagger/community)
[![Google Group](https://img.shields.io/badge/discuss-on%20groups-%23207de5)](https://groups.google.com/forum/#!forum/comictagger)
[![Twitter](https://img.shields.io/badge/%40comictagger-twitter-lightgrey)](https://twitter.com/comictagger)
[![Facebook](https://img.shields.io/badge/comictagger-facebook-lightgrey)](https://www.facebook.com/ComicTagger-139615369550787/)
https://code.google.com/p/comictagger/
# ComicTagger
ComicTagger is a **multi-platform** app for **writing metadata to digital comics**, written in Python and PyQt.
![ComicTagger logo](https://raw.githubusercontent.com/comictagger/comictagger/develop/comictaggerlib/graphics/app.png)
## 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, release notes, and more, visit [the Wiki](https://github.com/comictagger/comictagger/wiki)
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
## Installation
Todo:
- more tests in non-linux platforms
- repackage for simple user installation
### Binaries
Follows original readme:
Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases).
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
Just unzip the archive in any folder and run, no additional installation steps are required.
Features:
### PIP installation
* 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/
A pip package is provided, you can install it with:
For details, screen-shots, release notes, and more, visit http://code.google.com/p/comictagger/
```
$ pip3 install comictagger[GUI]
```
Requires:
### From source
* 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!
1. Ensure you have a recent version of python3 installed
2. Clone this repository `git clone https://github.com/comictagger/comictagger.git`
3. `pip3 install -r requirements_dev.txt`
4. Optionally install the GUI `pip3 install -r requirements-GUI.txt`
5. Optionally install CBR support `pip3 install -r requirements-CBR.txt`
6. `python3 comictagger.py`

View File

@ -1,18 +0,0 @@
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

Binary file not shown.

View File

@ -1,140 +0,0 @@
#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

Binary file not shown.

View File

@ -1,606 +0,0 @@
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.

View File

@ -1,80 +0,0 @@
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.

View File

@ -1 +0,0 @@
This is x64 version of unrar.dll.

View File

@ -1,177 +0,0 @@
# 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

View File

@ -1,30 +0,0 @@
# 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

View File

@ -1,138 +0,0 @@
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

View File

@ -1,218 +0,0 @@
# 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

View File

@ -1,309 +0,0 @@
# 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

View File

@ -19,8 +19,8 @@ import xml.etree.ElementTree as ET
#from pprint import pprint
#import zipfile
from genericmetadata import GenericMetadata
import utils
from .genericmetadata import GenericMetadata
from . import utils
class CoMet:
@ -76,7 +76,7 @@ class CoMet:
# helper func
def assign(comet_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
ET.SubElement(root, comet_entry).text = "{0}".format(md_entry)
# title is manditory
if md.title is None:
@ -131,43 +131,43 @@ class CoMet:
if credit['role'].lower() in set(self.writer_synonyms):
ET.SubElement(
root,
'writer').text = u"{0}".format(
'writer').text = "{0}".format(
credit['person'])
if credit['role'].lower() in set(self.penciller_synonyms):
ET.SubElement(
root,
'penciller').text = u"{0}".format(
'penciller').text = "{0}".format(
credit['person'])
if credit['role'].lower() in set(self.inker_synonyms):
ET.SubElement(
root,
'inker').text = u"{0}".format(
'inker').text = "{0}".format(
credit['person'])
if credit['role'].lower() in set(self.colorist_synonyms):
ET.SubElement(
root,
'colorist').text = u"{0}".format(
'colorist').text = "{0}".format(
credit['person'])
if credit['role'].lower() in set(self.letterer_synonyms):
ET.SubElement(
root,
'letterer').text = u"{0}".format(
'letterer').text = "{0}".format(
credit['person'])
if credit['role'].lower() in set(self.cover_synonyms):
ET.SubElement(
root,
'coverDesigner').text = u"{0}".format(
'coverDesigner').text = "{0}".format(
credit['person'])
if credit['role'].lower() in set(self.editor_synonyms):
ET.SubElement(
root,
'editor').text = u"{0}".format(
'editor').text = "{0}".format(
credit['person'])
# self pretty-print

View File

@ -21,39 +21,27 @@ import sys
import tempfile
import subprocess
import platform
import ctypes
import time
import StringIO
#import io
#import locale
#import shutil
import io
from natsort import natsorted
import natsort
from PyPDF2 import PdfFileReader
try:
from unrar import rarfile
from unrar import unrarlib
from unrar import constants
from unrar.cffi import rarfile
except:
print "WARNING: cannot find libunrar, rar support is disabled"
pass
if platform.system() == "Windows":
import _subprocess
try:
import Image
pil_available = True
except ImportError:
pil_available = False
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
from comet import CoMet
from genericmetadata import GenericMetadata, PageType
from filenameparser import FileNameParser
#from settings import ComicTaggerSettings
from .comicinfoxml import ComicInfoXml
from .comicbookinfo import ComicBookInfo
from .comet import CoMet
from .genericmetadata import GenericMetadata, PageType
from .filenameparser import FileNameParser
sys.path.insert(0, os.path.abspath("."))
@ -63,7 +51,6 @@ class MetaDataStyle:
COMET = 2
name = ['ComicBookLover', 'ComicRack', 'CoMet']
class ZipArchiver:
"""ZIP implementation"""
@ -78,7 +65,10 @@ class ZipArchiver:
return comment
def setArchiveComment(self, comment):
return self.writeZipComment(self.path, comment)
zf = zipfile.ZipFile(self.path, 'a')
zf.comment = bytes(comment, 'utf-8')
zf.close()
return True
def readArchiveFile(self, archive_file):
data = ""
@ -87,14 +77,13 @@ class ZipArchiver:
try:
data = zf.read(archive_file)
except zipfile.BadZipfile as e:
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(
e, self.path, archive_file)
print("bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file), file=sys.stderr)
zf.close()
raise IOError
except Exception as e:
zf.close()
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(
e, self.path, archive_file)
print("bad zipfile [{0}]: {1} :: {2}".format(
e, self.path, archive_file), file=sys.stderr)
raise IOError
finally:
zf.close()
@ -119,6 +108,7 @@ class ZipArchiver:
zf = zipfile.ZipFile(
self.path,
mode='a',
allowZip64=True,
compression=zipfile.ZIP_DEFLATED)
zf.writestr(archive_file, data)
zf.close()
@ -133,8 +123,8 @@ class ZipArchiver:
zf.close()
return namelist
except Exception as e:
print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(
e, self.path)
print("Unable to get zipfile list [{0}]: {1}".format(
e, self.path), file=sys.stderr)
return []
def rebuildZipFile(self, exclude_list):
@ -142,16 +132,11 @@ class ZipArchiver:
This recompresses the zip archive, without the files in the exclude_list
"""
# print ">> sys.stderr, Rebuilding zip {0} without {1}".format(
# self.path, exclude_list )
# generate temp file
tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path))
os.close(tmp_fd)
zin = zipfile.ZipFile(self.path, 'r')
zout = zipfile.ZipFile(tmp_name, 'w')
zout = zipfile.ZipFile(tmp_name, 'w', allowZip64=True)
for item in zin.infolist():
buffer = zin.read(item.filename)
if (item.filename not in exclude_list):
@ -189,7 +174,7 @@ class ZipArchiver:
found = False
value = bytearray()
# walk backwards to find the "End of Central Directory" record
while (not found) and (-pos != file_length):
# seek, relative to EOF
@ -222,21 +207,20 @@ class ZipArchiver:
fo.seek(pos + 2, 2)
# write out the comment itself
fo.write(comment)
fo.write(bytes(comment))
fo.truncate()
fo.close()
else:
raise Exception('Failed to write comment to zip file!')
except:
except Exception as e:
return False
else:
return True
def copyFromArchive(self, otherArchive):
"""Replace the current zip with one copied from another archive"""
try:
zout = zipfile.ZipFile(self.path, 'w')
zout = zipfile.ZipFile(self.path, 'w', allowZip64=True)
for fname in otherArchive.getArchiveFilenameList():
data = otherArchive.readArchiveFile(fname)
if data is not None:
@ -249,19 +233,14 @@ class ZipArchiver:
if not self.writeZipComment(self.path, comment):
return False
except Exception as e:
print >> sys.stderr, u"Error while copying to {0}: {1}".format(
self.path, e)
print("Error while copying to {0}: {1}".format(
self.path, e), file=sys.stderr)
return False
else:
return True
#------------------------------------------
class RarArchiver:
"""RAR implementation"""
devnull = None
def __init__(self, path, rar_exe_path):
@ -274,46 +253,43 @@ class RarArchiver:
# windows only, keeps the cmd.exe from popping up
if platform.system() == "Windows":
self.startupinfo = subprocess.STARTUPINFO()
self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
else:
self.startupinfo = None
def __del__(self):
# RarArchiver.devnull.close()
pass
def getArchiveComment(self):
rarc = self.getRARObj()
return rarc.comment
def setArchiveComment(self, comment):
if self.rar_exe_path is not None:
try:
# write comment to temp file
tmp_fd, tmp_name = tempfile.mkstemp()
f = os.fdopen(tmp_fd, 'w+b')
f = os.fdopen(tmp_fd, 'w+')
f.write(comment)
f.close()
working_dir = os.path.dirname(os.path.abspath(self.path))
# use external program to write comment to Rar archive
subprocess.call([self.rar_exe_path,
proc_args = [self.rar_exe_path,
'c',
'-w' + working_dir,
'-c-',
'-z' + tmp_name,
self.path],
self.path]
subprocess.call(proc_args,
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
stdout=RarArchiver.devnull,
stdin=RarArchiver.devnull,
stderr=RarArchiver.devnull)
if platform.system() == "Darwin":
time.sleep(1)
os.remove(tmp_name)
except:
except Exception as e:
print(e)
return False
else:
return True
@ -321,10 +297,6 @@ class RarArchiver:
return False
def readArchiveFile(self, archive_file):
# Make sure to escape brackets, since some funky stuff is going on
# underneath with "fnmatch"
#archive_file = archive_file.replace("[", '[[]')
entries = []
rarc = self.getRARObj()
@ -333,38 +305,29 @@ class RarArchiver:
while tries < 7:
try:
tries = tries + 1
#tmp_folder = tempfile.mkdtemp()
#tmp_file = os.path.join(tmp_folder, archive_file)
#rarc.extract(archive_file, tmp_folder)
data = rarc.open(archive_file).read()
#data = open(tmp_file).read()
data = rarc.open(archive_file).read()
entries = [(rarc.getinfo(archive_file), data)]
#shutil.rmtree(tmp_folder, ignore_errors=True)
#entries = rarc.read_files( archive_file )
if entries[0][0].file_size != len(entries[0][1]):
print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
print("readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
entries[0][0].file_size, len(
entries[0][1]), self.path, archive_file, tries)
entries[0][1]), self.path, archive_file, tries), file=sys.stderr)
continue
except (OSError, IOError) as e:
print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(
str(e), self.path, archive_file, tries)
print("readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(
str(e), self.path, archive_file, tries), file=sys.stderr)
time.sleep(1)
except Exception as e:
print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(
str(e), self.path, archive_file, tries)
print("Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(
str(e), self.path, archive_file, tries), file=sys.stderr)
break
else:
# Success"
# entries is a list of of tuples: ( rarinfo, filedata)
if tries > 1:
print >> sys.stderr, u"Attempted read_files() {0} times".format(
tries)
print("Attempted read_files() {0} times".format(
tries), file=sys.stderr)
if (len(entries) == 1):
return entries[0][1]
else:
@ -397,7 +360,9 @@ class RarArchiver:
self.path,
tmp_file],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
stdout=RarArchiver.devnull,
stdin=RarArchiver.devnull,
stderr=RarArchiver.devnull)
if platform.system() == "Darwin":
time.sleep(1)
@ -420,7 +385,9 @@ class RarArchiver:
self.path,
archive_file],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
stdout=RarArchiver.devnull,
stdin=RarArchiver.devnull,
stderr=RarArchiver.devnull)
if platform.system() == "Darwin":
time.sleep(1)
@ -432,24 +399,19 @@ class RarArchiver:
return False
def getArchiveFilenameList(self):
rarc = self.getRARObj()
#namelist = [ item.filename for item in rarc.infolist() ]
# return namelist
tries = 0
while tries < 7:
try:
tries = tries + 1
#namelist = [ item.filename for item in rarc.infolist() ]
namelist = []
for item in rarc.infolist():
if item.file_size != 0:
namelist.append(item.filename)
except (OSError, IOError) as e:
print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(
str(e), self.path, tries)
print("getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(
str(e), self.path, tries), file=sys.stderr)
time.sleep(1)
else:
@ -463,11 +425,11 @@ class RarArchiver:
while tries < 7:
try:
tries = tries + 1
rarc = rarfile.RarFile( self.path )
rarc = rarfile.RarFile(self.path)
except (OSError, IOError) as e:
print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(
str(e), self.path, tries)
print("getRARObj(): [{0}] {1} attempt#{2}".format(
str(e), self.path, tries), file=sys.stderr)
time.sleep(1)
else:
@ -566,7 +528,6 @@ class UnknownArchiver:
def getArchiveFilenameList(self):
return []
class PdfArchiver:
def __init__(self, path):
@ -595,15 +556,10 @@ class PdfArchiver:
out.append("/%04d.jpg" % (page))
return out
#------------------------------------------------------------------
class ComicArchive:
logo_data = None
class ArchiveType:
Zip, Rar, Folder, Pdf, Unknown = range(5)
Zip, Rar, Folder, Pdf, Unknown = list(range(5))
def __init__(self, path, rar_exe_path=None, default_image_path=None):
self.path = path
@ -676,11 +632,9 @@ class ComicArchive:
def rarTest(self):
try:
rarc = rarfile.RarFile(self.path)
except: # InvalidRARArchive:
return rarfile.is_rarfile(self.path)
except:
return False
else:
return True
def isZip(self):
return self.archive_type == self.ArchiveType.Zip
@ -698,7 +652,7 @@ class ComicArchive:
if self.archive_type == self.ArchiveType.Unknown:
return False
elif check_rar_status and self.isRar() and self.rar_exe_path is None:
elif check_rar_status and self.isRar() and not self.rar_exe_path:
return False
elif not os.access(self.path, os.W_OK):
@ -718,7 +672,6 @@ class ComicArchive:
return self.isWritable()
def seemsToBeAComicArchive(self):
# Do we even care about extensions??
ext = os.path.splitext(self.path)[1].lower()
@ -745,7 +698,6 @@ class ComicArchive:
return GenericMetadata()
def writeMetadata(self, metadata, style):
retcode = None
if style == MetaDataStyle.CIX:
retcode = self.writeCIX(metadata)
@ -756,7 +708,6 @@ class ComicArchive:
return retcode
def hasMetadata(self, style):
if style == MetaDataStyle.CIX:
return self.hasCIX()
elif style == MetaDataStyle.CBI:
@ -777,7 +728,6 @@ class ComicArchive:
return retcode
def getPage(self, index):
image_data = None
filename = self.getPageName(index)
@ -786,13 +736,12 @@ class ComicArchive:
try:
image_data = self.archiver.readArchiveFile(filename)
except IOError:
print >> sys.stderr, u"Error reading in page. Substituting logo page."
print("Error reading in page. Substituting logo page.", file=sys.stderr)
image_data = ComicArchive.logo_data
return image_data
def getPageName(self, index):
if index is None:
return None
@ -805,7 +754,6 @@ class ComicArchive:
return page_list[index]
def getScannerPageIndex(self):
scanner_page_index = None
# make a guess at the scanner page
@ -828,7 +776,7 @@ class ComicArchive:
# sort by most common
sorted_buckets = sorted(
length_buckets.iteritems(),
iter(length_buckets.items()),
key=lambda k_v: (
k_v[1],
k_v[0]),
@ -860,7 +808,6 @@ class ComicArchive:
return scanner_page_index
def getPageNameList(self, sort_list=True):
if self.page_list is None:
# get the list file names in the archive, and sort
files = self.archiver.getArchiveFilenameList()
@ -869,13 +816,9 @@ class ComicArchive:
# about case-sensitivity!
if sort_list:
def keyfunc(k):
# hack to account for some weird scanner ID pages
# basename=os.path.split(k)[1]
# if basename < '0':
# k = os.path.join(os.path.split(k)[0], "z" + basename)
return k.lower()
files = natsorted(files, key=keyfunc, signed=False)
files = natsort.natsorted(files, alg=natsort.ns.IC | natsort.ns.I)
# make a sub-list of image files
self.page_list = []
@ -890,7 +833,6 @@ class ComicArchive:
return self.page_list
def getNumberOfPages(self):
if self.page_count is None:
self.page_count = len(self.getPageNameList())
return self.page_count
@ -975,12 +917,11 @@ class ComicArchive:
try:
raw_cix = self.archiver.readArchiveFile(self.ci_xml_filename)
except IOError:
print "Error reading in raw CIX!"
print("Error reading in raw CIX!")
raw_cix = ""
return raw_cix
def writeCIX(self, metadata):
if metadata is not None:
self.applyArchiveInfoToMetadata(metadata, calc_page_sizes=True)
cix_string = ComicInfoXml().stringFromMetadata(metadata)
@ -1044,13 +985,13 @@ class ComicArchive:
def readRawCoMet(self):
if not self.hasCoMet():
print >> sys.stderr, self.path, "doesn't have CoMet data!"
print(self.path, "doesn't have CoMet data!", file=sys.stderr)
return None
try:
raw_comet = self.archiver.readArchiveFile(self.comet_filename)
except IOError:
print >> sys.stderr, u"Error reading in raw CoMet!"
print("Error reading in raw CoMet!", file=sys.stderr)
raw_comet = ""
return raw_comet
@ -1105,7 +1046,7 @@ class ComicArchive:
data = self.archiver.readArchiveFile(n)
except:
data = ""
print >> sys.stderr, u"Error reading in Comet XML for validation!"
print("Error reading in Comet XML for validation!", file=sys.stderr)
if CoMet().validateString(data):
# since we found it, save it!
self.comet_filename = n
@ -1125,7 +1066,7 @@ class ComicArchive:
data = self.getPage(idx)
if data is not None:
try:
im = Image.open(StringIO.StringIO(data))
im = Image.open(io.StringIO(data))
w, h = im.size
p['ImageSize'] = str(len(data))
@ -1140,7 +1081,6 @@ class ComicArchive:
p['ImageSize'] = str(len(data))
def metadataFromFilename(self, parse_scan_info=True):
metadata = GenericMetadata()
fnp = FileNameParser()

View File

@ -18,45 +18,39 @@ import json
from datetime import datetime
#import zipfile
from genericmetadata import GenericMetadata
import utils
from .genericmetadata import GenericMetadata
from . import utils
#import ctversion
class ComicBookInfo:
def metadataFromString(self, string):
cbi_container = json.loads(unicode(string, 'utf-8'))
class Default(dict):
def __missing__(self, key):
return None
cbi_container = json.loads(str(string, 'utf-8'))
metadata = GenericMetadata()
cbi = cbi_container['ComicBookInfo/1.0']
cbi = Default(cbi_container['ComicBookInfo/1.0'])
# 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.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.issueCount = 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.volumeCount = utils.xlate(cbi['numberOfVolumes'], True)
metadata.language = utils.xlate(cbi['language'])
metadata.country = utils.xlate(cbi['country'])
metadata.criticalRating = utils.xlate(cbi['rating'])
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')
metadata.credits = cbi['credits']
metadata.tags = cbi['tags']
# make sure credits and tags are at least empty lists and not None
if metadata.credits is None:
@ -103,33 +97,23 @@ class ComicBookInfo:
# helper func
def assign(cbi_entry, md_entry):
if md_entry is not None:
if md_entry is not None or isinstance(md_entry, str) and md_entry != "":
cbi[cbi_entry] = md_entry
# 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('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.issueCount, 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.volumeCount, True))
assign('language', utils.xlate(utils.getLanguageFromISO(metadata.language)))
assign('country', utils.xlate(metadata.country))
assign('rating', utils.xlate(metadata.criticalRating))
assign('credits', metadata.credits)
assign('tags', metadata.tags)

View File

@ -19,8 +19,9 @@ import xml.etree.ElementTree as ET
#from pprint import pprint
#import zipfile
from genericmetadata import GenericMetadata
import utils
from .genericmetadata import GenericMetadata
from .issuestring import IssueString
from . import utils
class ComicInfoXml:
@ -54,7 +55,8 @@ class ComicInfoXml:
header = '<?xml version="1.0"?>\n'
tree = self.convertMetadataToXML(self, metadata)
return header + ET.tostring(tree.getroot())
tree_str = ET.tostring(tree.getroot()).decode()
return header + tree_str
def indent(self, elem, level=0):
# for making the XML output readable
@ -85,7 +87,7 @@ class ComicInfoXml:
def assign(cix_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
assign('Title', md.title)
assign('Series', md.series)
@ -205,48 +207,44 @@ class ComicInfoXml:
raise 1
return None
metadata = GenericMetadata()
md = metadata
# Helper function
def xlate(tag):
node = root.find(tag)
if node is not None:
return node.text
else:
def get(name):
tag = root.find(name)
if tag is None:
return None
return tag.text
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 = GenericMetadata()
tmp = xlate('BlackAndWhite')
md.blackAndWhite = False
md.series = utils.xlate(get('Series'))
md.title = utils.xlate(get('Title'))
md.issue = IssueString(utils.xlate(get('Number'))).asString()
md.issueCount = utils.xlate(get('Count'), True)
md.volume = utils.xlate(get('Volume'), True)
md.alternateSeries = utils.xlate(get('AlternateSeries'))
md.alternateNumber = IssueString(utils.xlate(get('AlternateNumber'))).asString()
md.alternateCount = 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.webLink = 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.pageCount = utils.xlate(get('PageCount'), True)
md.scanInfo = utils.xlate(get('ScanInformation'))
md.storyArc = utils.xlate(get('StoryArc'))
md.seriesGroup = utils.xlate(get('SeriesGroup'))
md.maturityRating = utils.xlate(get('AgeRating'))
tmp = utils.xlate(get('BlackAndWhite'))
if tmp is not None and tmp.lower() in ["yes", "true", "1"]:
md.blackAndWhite = True
# Now extract the credit info
@ -260,23 +258,23 @@ class ComicInfoXml:
):
if n.text is not None:
for name in n.text.split(','):
metadata.addCredit(name.strip(), n.tag)
md.addCredit(name.strip(), n.tag)
if n.tag == 'CoverArtist':
if n.text is not None:
for name in n.text.split(','):
metadata.addCredit(name.strip(), "Cover")
md.addCredit(name.strip(), "Cover")
# parse page data now
pages_node = root.find("Pages")
if pages_node is not None:
for page in pages_node:
metadata.pages.append(page.attrib)
md.pages.append(page.attrib)
# print page.attrib
metadata.isEmpty = False
md.isEmpty = False
return metadata
return md
def writeToExternalFile(self, filename, metadata):

View File

@ -22,7 +22,7 @@ This should probably be re-written, but, well, it mostly works!
import re
import os
from urllib import unquote
from urllib.parse import unquote
class FileNameParser:

View File

@ -20,7 +20,7 @@ possible, however lossy it might be
# See the License for the specific language governing permissions and
# limitations under the License.
import utils
from . import utils
class PageType:
@ -251,7 +251,7 @@ class GenericMetadata:
return "No metadata"
def add_string(tag, val):
if val is not None and u"{0}".format(val) != "":
if val is not None and "{0}".format(val) != "":
vals.append((tag, val))
def add_attr_string(tag):
@ -314,7 +314,7 @@ class GenericMetadata:
# format the data nicely
outstr = ""
fmt_str = u"{0: <" + str(flen) + "} {1}\n"
fmt_str = "{0: <" + str(flen) + "} {1}\n"
for i in vals:
outstr += fmt_str.format(i[0] + ":", i[1])

View File

@ -44,7 +44,7 @@ class IssueString:
if len(text) == 0:
return
text = unicode(text)
text = str(text)
# skip the minus sign if it's first
if text[0] == '-':
@ -119,7 +119,7 @@ class IssueString:
def asFloat(self):
# return the float, with no suffix
if self.suffix == u"½":
if self.suffix == "½":
if self.num is not None:
return self.num + .5
else:

View File

@ -21,6 +21,7 @@ import re
import platform
import locale
import codecs
import unicodedata
class UtilsVars:
@ -55,20 +56,22 @@ def get_recursive_filelist(pathlist):
# if path is a folder, walk it recursively, and all files underneath
if isinstance(p, str):
# make sure string is unicode
p = p.decode(filename_encoding) # , 'replace')
elif not isinstance(p, unicode):
#p = p.decode(filename_encoding) # , 'replace')
pass
elif not isinstance(p, str):
# it's probably a QString
p = unicode(p)
p = str(p)
if os.path.isdir(p):
for root, dirs, files in os.walk(p):
for f in files:
if isinstance(f, str):
# make sure string is unicode
f = f.decode(filename_encoding, 'replace')
elif not isinstance(f, unicode):
#f = f.decode(filename_encoding, 'replace')
pass
elif not isinstance(f, str):
# it's probably a QString
f = unicode(f)
f = str(f)
filelist.append(os.path.join(root, f))
else:
filelist.append(p)
@ -119,9 +122,26 @@ def which(program):
return None
def xlate(data, isInt=False):
class Default(dict):
def __missing__(self, key):
return None
if data is None or data == "":
return None
if isInt:
i = str(data).translate(Default(zip((ord(c) for c in "1234567890"),"1234567890")))
if i == "0":
return "0"
if i is "":
return None
return int(i)
else:
return str(data)
def removearticles(text):
text = text.lower()
articles = ['and', 'the', 'a', '&', 'issue']
articles = ['and', 'a', '&', 'issue', 'the']
newText = ''
for word in text.split(' '):
if word not in articles:
@ -129,19 +149,24 @@ def removearticles(text):
newText = newText[:-1]
# 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(".", "")
return newText
def sanitize_title(text):
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 12 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 = removearticles(text).lower().strip()
return text
def unique_file(file_name):
counter = 1
# returns ('/path/file', '.ext')

View File

@ -1,19 +1,4 @@
#!/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"
#!/usr/bin/env python3
from comictaggerlib.main import ctmain
if __name__ == '__main__':

50
comictagger.spec Normal file
View File

@ -0,0 +1,50 @@
# -*- 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)

View File

@ -17,19 +17,19 @@
import os
#import sys
from PyQt4 import QtCore, QtGui, uic
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from PyQt5 import QtCore, QtGui, QtWidgets, uic
#from PyQt5.QtCore import QUrl, pyqtSignal, QByteArray
from settings import ComicTaggerSettings
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
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(QtGui.QDialog):
class AutoTagMatchWindow(QtWidgets.QDialog):
volume_id = 0
@ -41,13 +41,13 @@ class AutoTagMatchWindow(QtGui.QDialog):
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -58,10 +58,10 @@ class AutoTagMatchWindow(QtGui.QDialog):
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
self.skipButton = QtWidgets.QPushButton(self.tr("Skip to Next"))
self.buttonBox.addButton(
self.skipButton, QtGui.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(
self.skipButton, QtWidgets.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(
"Accept and Write Tags")
self.match_set_list = match_set_list
@ -83,8 +83,8 @@ class AutoTagMatchWindow(QtGui.QDialog):
if self.current_match_set_idx + 1 == len(self.match_set_list):
self.buttonBox.button(
QtGui.QDialogButtonBox.Cancel).setDisabled(True)
# self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
QtWidgets.QDialogButtonBox.Cancel).setDisabled(True)
# self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept")
self.skipButton.setText(self.tr("Skip"))
self.setCoverImage()
@ -94,7 +94,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
path = self.current_match_set.ca.path
self.setWindowTitle(
u"Select correct match or skip ({0} of {1}): {2}".format(
"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])
@ -112,30 +112,30 @@ class AutoTagMatchWindow(QtGui.QDialog):
self.twList.insertRow(row)
item_text = match['series']
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.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 = u"{0}".format(match['publisher'])
item_text = "{0}".format(match['publisher'])
else:
item_text = u"Unknown"
item = QtGui.QTableWidgetItem(item_text)
item_text = "Unknown"
item = QtWidgets.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 = u""
year_str = u"????"
month_str = ""
year_str = "????"
if match['month'] is not None:
month_str = u"-{0:02d}".format(int(match['month']))
month_str = "-{0:02d}".format(int(match['month']))
if match['year'] is not None:
year_str = u"{0}".format(match['year'])
year_str = "{0}".format(match['year'])
item_text = year_str + month_str
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
@ -143,7 +143,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
item_text = match['issue_title']
if item_text is None:
item_text = ""
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
@ -180,7 +180,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(
QtCore.Qt.UserRole).toPyObject()[0]
QtCore.Qt.UserRole)[0]
return match
def accept(self):
@ -190,7 +190,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
if self.current_match_set_idx == len(self.match_set_list):
# no more items
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)
else:
self.updateData()
@ -199,22 +199,22 @@ class AutoTagMatchWindow(QtGui.QDialog):
if self.current_match_set_idx == len(self.match_set_list):
# no more items
QtGui.QDialog.reject(self)
QtWidgets.QDialog.reject(self)
else:
self.updateData()
def reject(self):
reply = QtGui.QMessageBox.question(
reply = QtWidgets.QMessageBox.question(
self,
self.tr("Cancel Matching"),
self.tr("Are you sure you wish to cancel the matching process?"),
QtGui.QMessageBox.Yes,
QtGui.QMessageBox.No)
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtGui.QMessageBox.No:
if reply == QtWidgets.QMessageBox.No:
return
QtGui.QDialog.reject(self)
QtWidgets.QDialog.reject(self)
def saveMatch(self):
@ -228,18 +228,18 @@ class AutoTagMatchWindow(QtGui.QDialog):
# now get the particular issue data
cv_md = self.fetch_func(match)
if cv_md is None:
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr(
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr(
"Could not connect to Comic Vine to get issue details!"))
return
QtGui.QApplication.setOverrideCursor(
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
md.overlay(cv_md)
success = ca.writeMetadata(md, self.style)
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
QtGui.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr(
QtWidgets.QMessageBox.warning(self, self.tr("Write Error"), self.tr(
"Saving the tags to the archive seemed to fail!"))

View File

@ -17,15 +17,15 @@
#import sys
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#import utils
class AutoTagProgressWindow(QtGui.QDialog):
class AutoTagProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
@ -35,13 +35,13 @@ class AutoTagProgressWindow(QtGui.QDialog):
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.testCoverWidget = CoverImageWidget(
self.testCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtGui.QGridLayout(self.testCoverContainer)
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
gridlayout.addWidget(self.testCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -65,5 +65,5 @@ class AutoTagProgressWindow(QtGui.QDialog):
QtCore.QCoreApplication.processEvents()
def reject(self):
QtGui.QDialog.reject(self)
QtWidgets.QDialog.reject(self)
self.isdone = True

View File

@ -16,15 +16,15 @@
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
#from settingswindow import SettingsWindow
#from filerenamer import FileRenamer
#import utils
class AutoTagStartWindow(QtGui.QDialog):
class AutoTagStartWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super(AutoTagStartWindow, self).__init__(parent)
@ -102,7 +102,7 @@ class AutoTagStartWindow(QtGui.QDialog):
self.leSearchString.setEnabled(enable)
def accept(self):
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
self.dontUseYear = self.cbxDontUseYear.isChecked()
@ -122,6 +122,6 @@ class AutoTagStartWindow(QtGui.QDialog):
self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit
if self.cbxSpecifySearchString.isChecked():
self.searchString = unicode(self.leSearchString.text())
self.searchString = str(self.leSearchString.text())
if len(self.searchString) == 0:
self.searchString = None

View File

@ -29,15 +29,15 @@ import json
filename_encoding = sys.getfilesystemencoding()
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
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
from . import utils
class MultipleMatch():
@ -69,7 +69,7 @@ def actual_issue_data_fetch(match, settings, opts):
cv_md = comicVine.fetchIssueData(
match['volume_id'], match['issue_number'], settings)
except ComicVineTalkerException:
print >> sys.stderr, "Network error while getting issue details. Save aborted"
print("Network error while getting issue details. Save aborted", file=sys.stderr)
return None
if settings.apply_cbl_transform_on_cv_import:
@ -83,39 +83,39 @@ def actual_metadata_save(ca, opts, md):
if not opts.dryrun:
# write out the new data
if not ca.writeMetadata(md, opts.data_style):
print >> sys.stderr, "The tag save seemed to fail!"
print("The tag save seemed to fail!", file=sys.stderr)
return False
else:
print >> sys.stderr, "Save complete."
print("Save complete.", file=sys.stderr)
else:
if opts.terse:
print >> sys.stderr, "dry-run option was set, so nothing was written"
print("dry-run option was set, so nothing was written", file=sys.stderr)
else:
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))
print("dry-run option was set, so nothing was written, but here is the final set of tags:", file=sys.stderr)
print(("{0}".format(md)))
return True
def display_match_set_for_choice(label, match_set, opts, settings):
print(u"{0} -- {1}:".format(match_set.filename, label))
print(("{0} -- {1}:".format(match_set.filename, label)))
# sort match list by year
match_set.matches.sort(key=lambda k: k['year'])
for (counter, m) in enumerate(match_set.matches):
counter += 1
print(
u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
print((
" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
counter,
m['series'],
m['issue_number'],
m['publisher'],
m['month'],
m['year'],
m['issue_title']))
m['issue_title'])))
if opts.interactive:
while True:
i = raw_input("Choose a match #, or 's' to skip: ")
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':
break
@ -182,14 +182,14 @@ def post_process_matches(match_results, opts, settings):
def cli_mode(opts, settings):
if len(opts.file_list) < 1:
print >> sys.stderr, "You must specify at least one filename. Use the -h option for more info"
print("You must specify at least one filename. Use the -h option for more info", file=sys.stderr)
return
match_results = OnlineMatchResults()
for f in opts.file_list:
if isinstance(f, str):
f = f.decode(filename_encoding, 'replace')
pass
process_file_cli(f, opts, settings, match_results)
sys.stdout.flush()
@ -225,19 +225,19 @@ def process_file_cli(filename, opts, settings, match_results):
ComicTaggerSettings.getGraphic('nocover.png'))
if not os.path.lexists(filename):
print >> sys.stderr, "Cannot find " + filename
print("Cannot find " + filename, file=sys.stderr)
return
if not ca.seemsToBeAComicArchive():
print >> sys.stderr, "Sorry, but " + \
filename + " is not a comic archive!"
print("Sorry, but " + \
filename + " is not a comic archive!", file=sys.stderr)
return
# 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"
print("This archive is not writable for that tag type", file=sys.stderr)
return
has = [False, False, False]
@ -256,7 +256,7 @@ def process_file_cli(filename, opts, settings, match_results):
brief = ""
if batch_mode:
brief = u"{0}: ".format(filename)
brief = "{0}: ".format(filename)
if ca.isZip():
brief += "ZIP archive "
@ -280,24 +280,24 @@ def process_file_cli(filename, opts, settings, match_results):
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(
u"{0}".format(
unicode(
print((
"{0}".format(
str(
ca.readRawCIX(),
errors='ignore')))
errors='ignore'))))
else:
print(u"{0}".format(ca.readCIX()))
print(("{0}".format(ca.readCIX())))
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
if has[MetaDataStyle.CBI]:
@ -305,43 +305,43 @@ def process_file_cli(filename, opts, settings, match_results):
if opts.raw:
pprint(json.loads(ca.readRawCBI()))
else:
print(u"{0}".format(ca.readCBI()))
print(("{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(u"{0}".format(ca.readRawCoMet()))
print(("{0}".format(ca.readRawCoMet())))
else:
print(u"{0}".format(ca.readCoMet()))
print(("{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.removeMetadata(opts.data_style):
print(u"{0}: Tag removal seemed to fail!".format(filename))
print(("{0}: Tag removal seemed to fail!".format(filename)))
else:
print(
u"{0}: Removed {1} tags.".format(filename, style_name))
print((
"{0}: Removed {1} tags.".format(filename, style_name)))
else:
print(
u"{0}: dry-run. {1} tags not removed".format(filename, style_name))
print((
"{0}: dry-run. {1} tags not removed".format(filename, style_name)))
else:
print(u"{0}: This archive doesn't have {1} tags to remove.".format(
filename, style_name))
print(("{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(u"{0}: Already has {1} tags. Not overwriting.".format(
filename, dst_style_name))
print(("{0}: Already has {1} tags. Not overwriting.".format(
filename, dst_style_name)))
return
if opts.copy_source == opts.data_style:
print(
u"{0}: Destination and source are same: {1}. Nothing to do.".format(
print((
"{0}: Destination and source are same: {1}. Nothing to do.".format(
filename,
dst_style_name))
dst_style_name)))
return
src_style_name = MetaDataStyle.name[opts.copy_source]
@ -353,26 +353,26 @@ def process_file_cli(filename, opts, settings, match_results):
md = CBLTransformer(md, settings).apply()
if not ca.writeMetadata(md, opts.data_style):
print(u"{0}: Tag copy seemed to fail!".format(filename))
print(("{0}: Tag copy seemed to fail!".format(filename)))
else:
print(u"{0}: Copied {1} tags to {2} .".format(
filename, src_style_name, dst_style_name))
print(("{0}: Copied {1} tags to {2} .".format(
filename, src_style_name, dst_style_name)))
else:
print(
u"{0}: dry-run. {1} tags not copied".format(filename, src_style_name))
print((
"{0}: dry-run. {1} tags not copied".format(filename, src_style_name)))
else:
print(u"{0}: This archive doesn't have {1} tags to copy.".format(
filename, src_style_name))
print(("{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(u"{0}: Already has {1} tags. Not overwriting.".format(
filename, MetaDataStyle.name[opts.data_style]))
print(("{0}: Already has {1} tags. Not overwriting.".format(
filename, MetaDataStyle.name[opts.data_style])))
return
if batch_mode:
print(u"Processing {0}...".format(filename))
print(("Processing {0}...".format(filename)))
md = create_local_metadata(opts, ca, has[opts.data_style])
if md.issue is None or md.issue == "":
@ -389,13 +389,13 @@ def process_file_cli(filename, opts, settings, match_results):
cv_md = comicVine.fetchIssueDataByIssueID(
opts.issue_id, settings)
except ComicVineTalkerException:
print >> sys.stderr, "Network error while getting issue details. Save aborted"
print("Network error while getting issue details. Save aborted", file=sys.stderr)
match_results.fetchDataFailures.append(filename)
return
if cv_md is None:
print >> sys.stderr, "No match for ID {0} was found.".format(
opts.issue_id)
print("No match for ID {0} was found.".format(
opts.issue_id), file=sys.stderr)
match_results.noMatches.append(filename)
return
@ -405,7 +405,7 @@ def process_file_cli(filename, opts, settings, match_results):
ii = IssueIdentifier(ca, settings)
if md is None or md.isEmpty:
print >> sys.stderr, "No metadata given to search online with!"
print("No metadata given to search online with!", file=sys.stderr)
match_results.noMatches.append(filename)
return
@ -444,22 +444,22 @@ def process_file_cli(filename, opts, settings, match_results):
if choices:
if low_confidence:
print >> sys.stderr, "Online search: Multiple low confidence matches. Save aborted"
print("Online search: Multiple low confidence matches. Save aborted", file=sys.stderr)
match_results.lowConfidenceMatches.append(
MultipleMatch(filename, matches))
return
else:
print >> sys.stderr, "Online search: Multiple good matches. Save aborted"
print("Online search: Multiple good matches. Save aborted", file=sys.stderr)
match_results.multipleMatches.append(
MultipleMatch(filename, matches))
return
if low_confidence and opts.abortOnLowConfidence:
print >> sys.stderr, "Online search: Low confidence match. Save aborted"
print("Online search: Low confidence match. Save aborted", file=sys.stderr)
match_results.lowConfidenceMatches.append(
MultipleMatch(filename, matches))
return
if not found_match:
print >> sys.stderr, "Online search: No match found. Save aborted"
print("Online search: No match found. Save aborted", file=sys.stderr)
match_results.noMatches.append(filename)
return
@ -483,7 +483,7 @@ def process_file_cli(filename, opts, settings, match_results):
msg_hdr = ""
if batch_mode:
msg_hdr = u"{0}: ".format(filename)
msg_hdr = "{0}: ".format(filename)
if opts.data_style is not None:
use_tags = has[opts.data_style]
@ -493,7 +493,7 @@ def process_file_cli(filename, opts, settings, match_results):
md = create_local_metadata(opts, ca, use_tags)
if md.series is None:
print >> sys.stderr, msg_hdr + "Can't rename without series name"
print(msg_hdr + "Can't rename without series name", file=sys.stderr)
return
new_ext = None # default
@ -511,7 +511,7 @@ def process_file_cli(filename, opts, settings, match_results):
new_name = renamer.determineName(filename, ext=new_ext)
if new_name == os.path.basename(filename):
print >> sys.stderr, msg_hdr + "Filename is already good!"
print(msg_hdr + "Filename is already good!", file=sys.stderr)
return
folder = os.path.dirname(os.path.abspath(filename))
@ -524,23 +524,23 @@ def process_file_cli(filename, opts, settings, match_results):
else:
suffix = " (dry-run, no change)"
print(
u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix))
print((
"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)))
elif opts.export_to_zip:
msg_hdr = ""
if batch_mode:
msg_hdr = u"{0}: ".format(filename)
msg_hdr = "{0}: ".format(filename)
if not ca.isRar():
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
print(msg_hdr + "Archive is not a RAR.", file=sys.stderr)
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 + "{0} already exists in the that folder.".format(os.path.split(new_file)[1])
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))
@ -554,8 +554,8 @@ def process_file_cli(filename, opts, settings, match_results):
try:
os.unlink(rar_file)
except:
print >> sys.stderr, msg_hdr + \
"Error deleting original RAR after export"
print(msg_hdr + \
"Error deleting original RAR after export", file=sys.stderr)
delete_success = False
else:
delete_success = True
@ -565,20 +565,20 @@ def process_file_cli(filename, opts, settings, match_results):
os.remove(new_file)
else:
msg = msg_hdr + \
u"Dry-run: Would try to create {0}".format(
"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."
msg += " and delete orginal."
print(msg)
return
msg = msg_hdr
if export_success:
msg += u"Archive exported successfully to: {0}".format(
msg += "Archive exported successfully to: {0}".format(
os.path.split(new_file)[1])
if opts.delete_rar_after_export and delete_success:
msg += u" (Original deleted) "
msg += " (Original deleted) "
else:
msg += u"Archive failed to export!"
msg += "Archive failed to export!"
print(msg)

View File

@ -20,9 +20,9 @@ import datetime
#import sys
#from pprint import pprint
import ctversion
from settings import ComicTaggerSettings
import utils
from . import ctversion
from .settings import ComicTaggerSettings
from . import utils
class ComicVineCacher:
@ -37,7 +37,7 @@ class ComicVineCacher:
data = ""
try:
with open(self.version_file, 'rb') as f:
data = f.read()
data = f.read().decode("utf-8")
f.close()
except:
pass
@ -121,7 +121,7 @@ class ComicVineCacher:
con = lite.connect(self.db_file)
with con:
con.text_factory = unicode
con.text_factory = str
cur = con.cursor()
# remove all previous entries with this search term
@ -161,7 +161,7 @@ class ComicVineCacher:
results = list()
con = lite.connect(self.db_file)
with con:
con.text_factory = unicode
con.text_factory = str
cur = con.cursor()
# purge stale search results
@ -197,7 +197,7 @@ class ComicVineCacher:
con = lite.connect(self.db_file)
with con:
con.text_factory = unicode
con.text_factory = str
cur = con.cursor()
# remove all previous entries with this search term
@ -217,7 +217,7 @@ class ComicVineCacher:
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
# purge stale issue info - probably issue data won't change
# much....
@ -300,7 +300,7 @@ class ComicVineCacher:
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
# purge stale volume info
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
@ -337,7 +337,7 @@ class ComicVineCacher:
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
# purge stale issue info - probably issue data won't change
# much....
@ -386,7 +386,7 @@ class ComicVineCacher:
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
timestamp = datetime.datetime.now()
data = {
@ -403,7 +403,7 @@ class ComicVineCacher:
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
cur.execute(
"SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?",

View File

@ -15,8 +15,7 @@
# limitations under the License.
import json
import urllib2
import urllib
import requests
import re
import time
import datetime
@ -28,8 +27,8 @@ import ssl
from bs4 import BeautifulSoup
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, QByteArray
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
@ -45,11 +44,11 @@ except ImportError:
def emit(a, b, c):
pass
import ctversion
import utils
from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from issuestring import IssueString
from . import ctversion
from . import utils
from .comicvinecacher import ComicVineCacher
from .genericmetadata import GenericMetadata
from .issuestring import IssueString
#from settings import ComicTaggerSettings
@ -104,9 +103,6 @@ class ComicVineTalker(QObject):
self.log_func = None
# always use a tls context for urlopen
self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
def setLogFunc(self, log_func):
self.log_func = log_func
@ -114,7 +110,7 @@ class ComicVineTalker(QObject):
if self.log_func is None:
# sys.stdout.write(text.encode(errors='replace'))
# sys.stdout.flush()
print >> sys.stderr, text
print(text, file=sys.stderr)
else:
self.log_func(text)
@ -124,39 +120,38 @@ class ComicVineTalker(QObject):
year = None
if date_str is not None:
parts = date_str.split('-')
year = parts[0]
year = utils.xlate(parts[0], True)
if len(parts) > 1:
month = parts[1]
month = utils.xlate(parts[1], True)
if len(parts) > 2:
day = parts[2]
day = utils.xlate(parts[2], True)
return day, month, year
def testKey(self, key):
test_url = self.api_base_url + "/issue/1/?api_key=" + \
key + "&format=json&field_list=name"
resp = urllib2.urlopen(test_url, context=self.ssl)
content = resp.read()
try:
test_url = self.api_base_url + "/issue/1/?api_key=" + key + "&format=json&field_list=name"
cv_response = json.loads(content)
cv_response = requests.get(test_url, headers={'user-agent': 'comictagger/' + ctversion.version}).json()
# Bogus request, but if the key is wrong, you get error 100: "Invalid
# API Key"
return cv_response['status_code'] != 100
# Bogus request, but if the key is wrong, you get error 100: "Invalid
# API Key"
return cv_response['status_code'] != 100
except:
return False
"""
Get the contect from the CV server. If we're in "wait mode" and status code is a rate limit error
sleep for a bit and retry.
"""
def getCVContent(self, url):
def getCVContent(self, url, params):
total_time_waited = 0
limit_wait_time = 1
counter = 0
wait_times = [1, 2, 3, 4]
while True:
content = self.getUrlContent(url)
cv_response = json.loads(content)
cv_response = self.getUrlContent(url, params)
if self.wait_for_rate_limit and cv_response[
'status_code'] == ComicVineTalkerException.RateLimit:
self.writeLog(
@ -181,25 +176,24 @@ class ComicVineTalker(QObject):
break
return cv_response
def getUrlContent(self, url):
def getUrlContent(self, url, params):
# connect to server:
# if there is a 500 error, try a few more times before giving up
# any other error, just bail
# print "ATB---", url
#print("---", url)
for tries in range(3):
try:
resp = urllib2.urlopen(url, context=self.ssl)
return resp.read()
except urllib2.HTTPError as e:
if e.getcode() == 500:
resp = requests.get(url, params=params, headers={'user-agent': 'comictagger/' + ctversion.version})
if resp.status_code == 200:
return resp.json()
if resp.status_code == 500:
self.writeLog("Try #{0}: ".format(tries + 1))
time.sleep(1)
self.writeLog(str(e) + "\n")
if e.getcode() != 500:
self.writeLog(str(resp.status_code) + "\n")
else:
break
except Exception as e:
except requests.exceptions.RequestException as e:
self.writeLog(str(e) + "\n")
raise ComicVineTalkerException(
ComicVineTalkerException.Network, "Network Error!")
@ -209,8 +203,8 @@ class ComicVineTalker(QObject):
def searchForSeries(self, series_name, callback=None, refresh_cache=False):
# remove cruft from the search string
series_name = utils.removearticles(series_name).lower().strip()
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
search_series_name = utils.sanitize_title(series_name)
# before we search online, look in our cache, since we might have
# done this same search recently
@ -221,26 +215,17 @@ class ComicVineTalker(QObject):
if len(cached_search_results) > 0:
return cached_search_results
original_series_name = series_name
params = {
'api_key': self.api_key,
'format': 'json',
'resources': 'volume',
'query': search_series_name,
'field_list': 'volume,name,id,start_year,publisher,image,description,count_of_issues',
'page': 1,
'limit': 100
}
# We need to make the series name into an "AND"ed query list
query_word_list = series_name.split()
and_list = ['AND'] * (len(query_word_list) - 1)
and_list.append('')
# zipper up the two lists
query_list = zip(query_word_list, and_list)
# flatten the list
query_list = [item for sublist in query_list for item in sublist]
# convert back to a string
query_string = " ".join(query_list).strip()
# print "Query string = ", query_string
query_string = urllib.quote_plus(query_string.encode("utf-8"))
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + \
query_string + \
"&field_list=name,id,start_year,publisher,image,description,count_of_issues"
cv_response = self.getCVContent(search_url + "&page=1")
cv_response = self.getCVContent(self.api_base_url + "/search", params)
search_results = list()
@ -250,6 +235,19 @@ class ComicVineTalker(QObject):
current_result_count = cv_response['number_of_page_results']
total_result_count = cv_response['number_of_total_results']
# 8 Dec 2018 - Comic Vine changed query results again. Terms are now
# ORed together, and we get thousands of results. Good news is the
# results are sorted by relevance, so we can be smart about halting
# the search.
# 1. Don't fetch more than some sane amount of pages.
max_results = 500
# 2. Halt when not all of our search terms are present in a result
# 3. Halt when the results contain more (plus threshold) words than
# our search
result_word_count_max = len(search_series_name.split()) + 3
total_result_count = min(total_result_count, max_results)
if callback is None:
self.writeLog(
"Found {0} of {1} results\n".format(
@ -262,7 +260,31 @@ class ComicVineTalker(QObject):
callback(current_result_count, total_result_count)
# see if we need to keep asking for more pages...
stop_searching = False
while (current_result_count < total_result_count):
last_result = search_results[-1]['name']
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
last_result = utils.sanitize_title(last_result)
# See if the last result's name has all the of the search terms.
# if not, break out of this, loop, we're done.
for term in search_series_name.split():
if term not in last_result.lower():
#print("Term '{}' not in last result. Halting search result fetching".format(term))
stop_searching = True
break
# Also, stop searching when the word count of last results is too much longer
# than our search terms list
if len(last_result) > result_word_count_max:
#print("Last result '{}' is too long. Halting search result fetching".format(last_result))
stop_searching = True
if stop_searching:
break
if callback is None:
self.writeLog(
"getting another page of results {0} of {1}...\n".format(
@ -270,7 +292,8 @@ class ComicVineTalker(QObject):
total_result_count))
page += 1
cv_response = self.getCVContent(search_url + "&page=" + str(page))
params['page'] = page
cv_response = self.getCVContent(self.api_base_url + "/search", params)
search_results.extend(cv_response['results'])
current_result_count += cv_response['number_of_page_results']
@ -278,6 +301,18 @@ class ComicVineTalker(QObject):
if callback is not None:
callback(current_result_count, total_result_count)
# Remove any search results that don't contain all the search terms
# (iterate backwards for easy removal)
for i in range(len(search_results) - 1, -1, -1):
record = search_results[i]
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
recordName = utils.sanitize_title(record['name'])
for term in search_series_name.split():
if term not in recordName:
del search_results[i]
break
# for record in search_results:
#print(u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year']))
# print(record)
@ -285,7 +320,7 @@ class ComicVineTalker(QObject):
#print(u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year']))
# cache these search results
cvc.add_search_results(original_series_name, search_results)
cvc.add_search_results(series_name, search_results)
return search_results
@ -299,11 +334,14 @@ class ComicVineTalker(QObject):
if cached_volume_result is not None:
return cached_volume_result
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + \
str(series_id) + "/?api_key=" + self.api_key + \
"&field_list=name,id,start_year,publisher,count_of_issues&format=json"
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + str(series_id)
cv_response = self.getCVContent(volume_url)
params = {
'api_key': self.api_key,
'format': 'json',
'field_list': 'name,id,start_year,publisher,count_of_issues'
}
cv_response = self.getCVContent(volume_url, params)
volume_results = cv_response['results']
@ -321,33 +359,34 @@ class ComicVineTalker(QObject):
if cached_volume_issues_result is not None:
return cached_volume_issues_result
#---------------------------------
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + \
str(series_id) + \
"&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
cv_response = self.getCVContent(issues_url)
params = {
'api_key': self.api_key,
'filter': 'volume:' + str(series_id),
'format': 'json',
'field_list': 'id,volume,issue_number,name,image,cover_date,site_detail_url,description'
}
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
#------------------------------------
limit = cv_response['limit']
current_result_count = cv_response['number_of_page_results']
total_result_count = cv_response['number_of_total_results']
# print "ATB total_result_count", total_result_count
#print("total_result_count", total_result_count)
#print("ATB Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
#print("Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
volume_issues_result = cv_response['results']
page = 1
offset = 0
# see if we need to keep asking for more pages...
while (current_result_count < total_result_count):
#print("ATB getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
#print("getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
page += 1
offset += cv_response['number_of_page_results']
# print issues_url+ "&offset="+str(offset)
cv_response = self.getCVContent(
issues_url + "&offset=" + str(offset))
params['offset'] = offset
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
volume_issues_result.extend(cv_response['results'])
current_result_count += cv_response['number_of_page_results']
@ -358,48 +397,45 @@ class ComicVineTalker(QObject):
return volume_issues_result
def fetchIssuesByVolumeIssueNumAndYear(
self, volume_id_list, issue_number, year):
volume_filter = "volume:"
def fetchIssuesByVolumeIssueNumAndYear(self, volume_id_list, issue_number, year):
volume_filter = ""
for vid in volume_id_list:
volume_filter += str(vid) + "|"
filter = "volume:{},issue_number:{}".format(volume_filter, issue_number)
year_filter = ""
if year is not None and str(year).isdigit():
year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(
year, int(year) + 1)
intYear = utils.xlate(year, True)
if intYear is not None:
filter += ",cover_date:{}-1-1|{}-1-1".format(intYear, intYear + 1)
issue_number = urllib.quote_plus(unicode(issue_number).encode("utf-8"))
params = {
'api_key': self.api_key,
'format': 'json',
'field_list': 'id,volume,issue_number,name,image,cover_date,site_detail_url,description',
'filter': filter
}
filter = "&filter=" + volume_filter + \
year_filter + ",issue_number:" + issue_number
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + \
"&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
cv_response = self.getCVContent(issues_url)
cv_response = self.getCVContent(self.api_base_url + "/issues", params)
#------------------------------------
limit = cv_response['limit']
current_result_count = cv_response['number_of_page_results']
total_result_count = cv_response['number_of_total_results']
# print "ATB total_result_count", total_result_count
#print("total_result_count", total_result_count)
#print("ATB Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
#print("Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
filtered_issues_result = cv_response['results']
page = 1
offset = 0
# see if we need to keep asking for more pages...
while (current_result_count < total_result_count):
#print("ATB getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
#print("getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
page += 1
offset += cv_response['number_of_page_results']
# print issues_url+ "&offset="+str(offset)
cv_response = self.getCVContent(
issues_url + "&offset=" + str(offset))
params['offset'] = offset
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
filtered_issues_result.extend(cv_response['results'])
current_result_count += cv_response['number_of_page_results']
@ -423,11 +459,12 @@ class ComicVineTalker(QObject):
break
if (found):
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
str(record['id']) + "/?api_key=" + \
self.api_key + "&format=json"
cv_response = self.getCVContent(issue_url)
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(record['id'])
params = {
'api_key': self.api_key,
'format': 'json'
}
cv_response = self.getCVContent(issue_url, params)
issue_results = cv_response['results']
else:
@ -439,9 +476,12 @@ class ComicVineTalker(QObject):
def fetchIssueDataByIssueID(self, issue_id, settings):
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
cv_response = self.getCVContent(issue_url)
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id)
params = {
'api_key': self.api_key,
'format': 'json'
}
cv_response = self.getCVContent(issue_url, params)
issue_results = cv_response['results']
@ -457,15 +497,13 @@ class ComicVineTalker(QObject):
# Now, map the Comic Vine data to generic metadata
metadata = GenericMetadata()
metadata.series = issue_results['volume']['name']
metadata.series = utils.xlate(issue_results['volume']['name'])
metadata.issue = IssueString(issue_results['issue_number']).asString()
metadata.title = utils.xlate(issue_results['name'])
num_s = IssueString(issue_results['issue_number']).asString()
metadata.issue = num_s
metadata.title = issue_results['name']
metadata.publisher = volume_results['publisher']['name']
metadata.day, metadata.month, metadata.year = self.parseDateStr(
issue_results['cover_date'])
if volume_results['publisher'] is not None:
metadata.publisher = utils.xlate(volume_results['publisher']['name'])
metadata.day, metadata.month, metadata.year = self.parseDateStr(issue_results['cover_date'])
#metadata.issueCount = volume_results['count_of_issues']
metadata.comments = self.cleanup_html(
@ -532,7 +570,7 @@ class ComicVineTalker(QObject):
if string is None:
return ""
# find any tables
soup = BeautifulSoup(string)
soup = BeautifulSoup(string, "html.parser")
tables = soup.findAll('table')
# remove all newlines first
@ -592,7 +630,7 @@ class ComicVineTalker(QObject):
for w in col_widths:
fmtstr += " {{:{}}}|".format(w + 1)
width = sum(col_widths) + len(col_widths) * 2
print "width=", width
print("width=", width)
table_text = ""
counter = 0
for row in rows:
@ -632,9 +670,15 @@ class ComicVineTalker(QObject):
if cached_details['image_url'] is not None:
return cached_details
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
str(issue_id) + "/?api_key=" + self.api_key + \
"&format=json&field_list=image,cover_date,site_detail_url"
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id)
params = {
'api_key': self.api_key,
'format': 'json',
'field_list': 'image,cover_date,site_detail_url'
}
cv_response = self.getCVContent(issue_url, params)
details = dict()
details['image_url'] = None
@ -642,8 +686,6 @@ class ComicVineTalker(QObject):
details['cover_date'] = None
details['site_detail_url'] = None
cv_response = self.getCVContent(issue_url)
details['image_url'] = cv_response['results']['image']['super_url']
details['thumb_image_url'] = cv_response[
'results']['image']['thumb_url']
@ -678,8 +720,7 @@ class ComicVineTalker(QObject):
return url_list
# scrape the CV issue page URL to get the alternate cover URLs
resp = urllib2.urlopen(issue_page_url, context=self.ssl)
content = resp.read()
content = requests.get(issue_page_url, headers={'user-agent': 'comictagger/' + ctversion.version}).text
alt_cover_url_list = self.parseOutAltCoverUrls(content)
# cache this alt cover URL list
@ -688,22 +729,26 @@ class ComicVineTalker(QObject):
return alt_cover_url_list
def parseOutAltCoverUrls(self, page_html):
soup = BeautifulSoup(page_html)
soup = BeautifulSoup(page_html, "html.parser")
alt_cover_url_list = []
# Using knowledge of the layout of the Comic Vine issue page here:
# look for the divs that are in the classes 'content-pod' and
# 'alt-cover'
# look for the divs that are in the classes 'imgboxart' and
# 'issue-cover'
div_list = soup.find_all('div')
covers_found = 0
for d in div_list:
if 'class' in d:
if 'class' in d.attrs:
c = d['class']
if 'imgboxart' in c and 'issue-cover' in c:
if ('imgboxart' in c and
'issue-cover' in c and
d.img['src'].startswith("http")
):
covers_found += 1
if covers_found != 1:
alt_cover_url_list.append(d.img['src'])
alt_cover_url_list.append(d.img['src'])
return alt_cover_url_list
@ -749,15 +794,15 @@ class ComicVineTalker(QObject):
data = reply.readAll()
try:
cv_response = json.loads(str(data))
except:
print >> sys.stderr, "Comic Vine query failed to get JSON data"
print >> sys.stderr, str(data)
cv_response = json.loads(bytes(data))
except Exception as e:
print("Comic Vine query failed to get JSON data", file=sys.stderr)
print(str(data), file=sys.stderr)
return
if cv_response['status_code'] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format(
cv_response['error'])
print("Comic Vine query failed with error: [{0}]. ".format(
cv_response['error']), file=sys.stderr)
return
image_url = cv_response['results']['image']['super_url']

View File

@ -1,4 +1,4 @@
"""A PyQt4 widget to display cover images
"""A PyQt5 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!
@ -20,15 +20,16 @@ TODO: This should be re-factored using subclasses!
#import os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5 import uic
from settings import ComicTaggerSettings
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagefetcher import ImageFetcher
from pageloader import PageLoader
from imagepopup import ImagePopup
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

View File

@ -16,12 +16,12 @@
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
class CreditEditorWindow(QtGui.QDialog):
class CreditEditorWindow(QtWidgets.QDialog):
ModeEdit = 0
ModeNew = 1
@ -90,7 +90,7 @@ class CreditEditorWindow(QtGui.QDialog):
def accept(self):
if self.cbRole.currentText() == "" or self.leName.text() == "":
QtGui.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
QtWidgets.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
"You need to enter both role and name for a credit."))
else:
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)

View File

@ -1,3 +0,0 @@
# This file should contain only these comments, and the line below.
# Used by packaging makefiles and app
version = "1.1.16-beta-rc2"

View File

@ -16,9 +16,9 @@
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
#from settingswindow import SettingsWindow
#from filerenamer import FileRenamer
#import utils
@ -30,7 +30,7 @@ class ExportConflictOpts:
createUnique = 3
class ExportWindow(QtGui.QDialog):
class ExportWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super(ExportWindow, self).__init__(parent)
@ -52,7 +52,7 @@ class ExportWindow(QtGui.QDialog):
self.fileConflictBehavior = ExportConflictOpts.dontCreate
def accept(self):
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
self.addToList = self.cbxAddToList.isChecked()

View File

@ -18,8 +18,8 @@ import os
import re
import datetime
import utils
from issuestring import IssueString
from . import utils
from .issuestring import IssueString
class FileRenamer:
@ -49,7 +49,7 @@ class FileRenamer:
return (word[0] == "%" and word[-1:] == "%")
if value is not None:
return text.replace(token, unicode(value))
return text.replace(token, str(value))
else:
if self.smart_cleanup:
# smart cleanup means we want to remove anything appended to token if it's empty
@ -81,7 +81,7 @@ class FileRenamer:
new_name = self.replaceToken(new_name, md.volume, '%volume%')
if md.issue is not None:
issue_str = u"{0}".format(
issue_str = "{0}".format(
IssueString(md.issue).asString(pad=self.issue_zero_padding))
else:
issue_str = None
@ -98,8 +98,8 @@ class FileRenamer:
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)
#month_name = dt.strftime("%B".encode(preferred_encoding)).decode(preferred_encoding)
month_name = dt.strftime("%B")
new_name = self.replaceToken(new_name, month_name, '%month_name%')
new_name = self.replaceToken(new_name, md.genre, '%genre%')
@ -128,7 +128,7 @@ class FileRenamer:
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
# remove duplicate spaces
new_name = u" ".join(new_name.split())
new_name = " ".join(new_name.split())
# remove remove duplicate -, _,
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
@ -139,7 +139,7 @@ class FileRenamer:
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
# remove duplicate spaces (again!)
new_name = u" ".join(new_name.split())
new_name = " ".join(new_name.split())
if ext is None:
ext = os.path.splitext(filename)[1]

View File

@ -1,5 +1,5 @@
# coding=utf-8
"""A PyQt4 widget for managing list of comic archive files"""
"""A PyQt5 widget for managing list of comic archive files"""
# Copyright 2012-2014 Anthony Beville
@ -18,18 +18,19 @@
import platform
import os
#import os
#import sys
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyQt4.QtCore import pyqtSignal
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import uic
from PyQt5.QtCore import pyqtSignal
from settings import ComicTaggerSettings
from comicarchive import ComicArchive
from optionalmsgdialog import OptionalMessageDialog
from .settings import ComicTaggerSettings
from .comicarchive import ComicArchive
from .optionalmsgdialog import OptionalMessageDialog
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
import utils
from . import utils
#from comicarchive import MetaDataStyle
#from genericmetadata import GenericMetadata, PageType
@ -37,8 +38,10 @@ import utils
class FileTableWidgetItem(QTableWidgetItem):
def __lt__(self, other):
return (self.data(Qt.UserRole).toBool() <
other.data(Qt.UserRole).toBool())
#return (self.data(Qt.UserRole).toBool() <
# other.data(Qt.UserRole).toBool())
return (self.data(Qt.UserRole) <
other.data(Qt.UserRole))
class FileInfo():
@ -139,7 +142,7 @@ class FileSelectionList(QWidget):
def getArchiveByRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
Qt.UserRole)
return fi.ca
def getCurrentArchive(self):
@ -186,44 +189,32 @@ class FileSelectionList(QWidget):
filelist = utils.get_recursive_filelist(pathlist)
# we now have a list of files to add
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
# Prog dialog on Linux flakes out for small range, so scale up
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), parent=self)
progdialog.setWindowTitle("Adding Files")
# progdialog.setWindowModality(Qt.WindowModal)
progdialog.setWindowModality(Qt.ApplicationModal)
progdialog.show()
progdialog.setMinimumDuration(300)
centerWindowOnParent(progdialog)
#QCoreApplication.processEvents()
#progdialog.show()
QCoreApplication.processEvents()
firstAdded = None
self.twList.setSortingEnabled(False)
for idx, f in enumerate(filelist):
QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx)
progdialog.setValue(idx+1)
progdialog.setLabelText(f)
centerWindowOnParent(progdialog)
QCoreApplication.processEvents()
row = self.addPathItem(f)
if firstAdded is None and row is not None:
firstAdded = row
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
progdialog.hide()
QCoreApplication.processEvents()
if firstAdded is not None:
self.twList.selectRow(firstAdded)
@ -235,7 +226,7 @@ class FileSelectionList(QWidget):
QMessageBox.information(
self,
self.tr("File/Folder Open"),
self.tr("No comic archives were found."))
self.tr("No readable comic archives were found."))
self.twList.setSortingEnabled(True)
@ -271,7 +262,7 @@ class FileSelectionList(QWidget):
return -1
def addPathItem(self, path):
path = unicode(path)
path = str(path)
path = os.path.abspath(path)
# print "processing", path
@ -327,7 +318,7 @@ class FileSelectionList(QWidget):
def updateRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
Qt.UserRole) #.toPyObject()
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
@ -382,8 +373,8 @@ class FileSelectionList(QWidget):
ca_list = []
for r in range(self.twList.rowCount()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if self.twList.isItemSelected(item):
fi = item.data(Qt.UserRole).toPyObject()
if item.isSelected():
fi = item.data(Qt.UserRole)
ca_list.append(fi.ca)
return ca_list
@ -395,7 +386,7 @@ class FileSelectionList(QWidget):
self.twList.setSortingEnabled(False)
for r in range(self.twList.rowCount()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if self.twList.isItemSelected(item):
if item.isSelected():
self.updateRow(r)
self.twList.setSortingEnabled(True)
@ -425,7 +416,7 @@ class FileSelectionList(QWidget):
return
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
Qt.UserRole) #.toPyObject()
self.selectionChanged.emit(QVariant(fi))
def revertSelection(self):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -19,14 +19,12 @@ import os
import datetime
import shutil
import tempfile
import urllib
import ssl
#import urllib2
import requests
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt4 import QtGui
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt5 import QtGui
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
@ -45,7 +43,8 @@ except ImportError:
def emit(a, b, c):
pass
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
from . import ctversion
class ImageFetcherException(Exception):
@ -66,9 +65,6 @@ class ImageFetcher(QObject):
if not os.path.exists(self.db_file):
self.create_image_db()
# always use a tls context for urlopen
self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
def clearCache(self):
os.unlink(self.db_file)
if os.path.isdir(self.cache_folder):
@ -87,11 +83,11 @@ class ImageFetcher(QObject):
# first look in the DB
image_data = self.get_image_from_cache(url)
if blocking:
if image_data is None:
try:
image_data = urllib.urlopen(url, context=self.ssl).read()
print(url)
image_data = requests.get(url, headers={'user-agent': 'comictagger/' + ctversion.version}).content
except Exception as e:
print(e)
raise ImageFetcherException("Network Error!")

View File

@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import StringIO
import io
import sys
from functools import reduce
@ -40,9 +40,9 @@ class ImageHasher(object):
if path is not None:
self.image = Image.open(path)
else:
self.image = Image.open(StringIO.StringIO(data))
except:
print("Image data seems corrupted!")
self.image = Image.open(io.BytesIO(data))
except Exception as e:
print("Image data seems corrupted! [{}]".format(e))
# just generate a bogus image
self.image = Image.new("L", (1, 1))
@ -51,9 +51,8 @@ class ImageHasher(object):
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)
print("average_hash error:", e)
return int(0)
pixels = list(image.getdata())
avg = sum(pixels) / len(pixels)
@ -61,7 +60,7 @@ class ImageHasher(object):
def compare_value_to_avg(i):
return (1 if i > avg else 0)
bitlist = map(compare_value_to_avg, pixels)
bitlist = list(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):
@ -178,13 +177,13 @@ class ImageHasher(object):
@staticmethod
def hamming_distance(h1, h2):
if isinstance(h1, long) or isinstance(h1, int):
if isinstance(h1, int) or isinstance(h1, int):
n1 = h1
n2 = h2
else:
# convert hex strings to ints
n1 = long(h1, 16)
n2 = long(h2, 16)
n1 = int(h1, 16)
n2 = int(h2, 16)
# xor the two numbers
n = n1 ^ n2

View File

@ -17,19 +17,19 @@
#import sys
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
class ImagePopup(QtGui.QDialog):
class ImagePopup(QtWidgets.QDialog):
def __init__(self, parent, image_pixmap):
super(ImagePopup, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui'), self)
QtGui.QApplication.setOverrideCursor(
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
# self.setWindowModality(QtCore.Qt.WindowModal)
@ -38,15 +38,16 @@ class ImagePopup(QtGui.QDialog):
self.imagePixmap = image_pixmap
screen_size = QtGui.QDesktopWidget().screenGeometry()
screen_size = QtWidgets.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
self.desktopBg = QtGui.QPixmap.grabWindow(
QtGui.QApplication.desktop().winId(),
screen = QtWidgets.QApplication.primaryScreen()
self.desktopBg = screen.grabWindow(
QtWidgets.QApplication.desktop().winId(),
0,
0,
screen_size.width(),
@ -59,7 +60,7 @@ class ImagePopup(QtGui.QDialog):
self.applyImagePixmap()
self.showFullScreen()
self.raise_()
QtGui.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.restoreOverrideCursor()
def paintEvent(self, event):
self.painter = QtGui.QPainter(self)

View File

@ -15,10 +15,7 @@
# limitations under the License.
import sys
import StringIO
#import math
#import urllib2
#import urllib
import io
try:
from PIL import Image
@ -27,12 +24,12 @@ try:
except ImportError:
pil_available = False
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 .genericmetadata import GenericMetadata
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .imagehasher import ImageHasher
from .imagefetcher import ImageFetcher, ImageFetcherException
from .issuestring import IssueString
from . import utils
#from settings import ComicTaggerSettings
#from comicvinecacher import ComicVineCacher
@ -124,7 +121,7 @@ class IssueIdentifier:
def getAspectRatio(self, image_data):
try:
im = Image.open(StringIO.StringIO(image_data))
im = Image.open(io.StringIO(image_data))
w, h = im.size
return float(h) / float(w)
except:
@ -132,17 +129,16 @@ class IssueIdentifier:
def cropCover(self, image_data):
im = Image.open(StringIO.StringIO(image_data))
im = Image.open(io.StringIO(image_data))
w, h = im.size
try:
cropped_im = im.crop((int(w / 2), 0, w, h))
except Exception as e:
sys.exc_clear()
print "cropCover() error:", e
print("cropCover() error:", e)
return None
output = StringIO.StringIO()
output = io.StringIO()
cropped_im.save(output, format="PNG")
cropped_image_data = output.getvalue()
output.close()
@ -405,7 +401,7 @@ class IssueIdentifier:
comicVine.setLogFunc(self.output_function)
# self.log_msg(("Searching for " + keys['series'] + "...")
self.log_msg(u"Searching for {0} #{1} ...".format(
self.log_msg("Searching for {0} #{1} ...".format(
keys['series'], keys['issue_number']))
try:
cv_search_results = comicVine.searchForSeries(keys['series'])
@ -438,8 +434,10 @@ class IssueIdentifier:
# assume that our search name is close to the actual name, say
# within ,e.g. 5 chars
shortened_key = utils.removearticles(keys['series'])
shortened_item_name = utils.removearticles(item['name'])
# 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):
length_approved = True
@ -492,11 +490,11 @@ class IssueIdentifier:
break
if keys['year'] is None:
self.log_msg(u"Found {0} series that have an issue #{1}".format(
self.log_msg("Found {0} series that have an issue #{1}".format(
len(shortlist), keys['issue_number']))
else:
self.log_msg(
u"Found {0} series that have an issue #{1} from {2}".format(
"Found {0} series that have an issue #{1} from {2}".format(
len(shortlist),
keys['issue_number'],
keys['year']))
@ -509,7 +507,7 @@ class IssueIdentifier:
self.callback(counter, len(shortlist) * 3)
counter += 1
self.log_msg(u"Examining covers for ID: {0} {1} ({2}) ...".format(
self.log_msg("Examining covers for ID: {0} {1} ({2}) ...".format(
series['id'],
series['name'],
series['start_year']), newline=False)
@ -540,7 +538,7 @@ class IssueIdentifier:
return self.match_list
match = dict()
match['series'] = u"{0} ({1})".format(
match['series'] = "{0} ({1})".format(
series['name'], series['start_year'])
match['distance'] = score_item['score']
match['issue_number'] = keys['issue_number']
@ -582,7 +580,7 @@ class IssueIdentifier:
self.log_msg(str(l))
def print_match(item):
self.log_msg(u"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
self.log_msg("-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
item['series'],
item['issue_number'],
item['issue_title'],
@ -613,7 +611,7 @@ class IssueIdentifier:
self.callback(counter, len(self.match_list) * 3)
counter += 1
self.log_msg(
u"Examining alternate covers for ID: {0} {1} ...".format(
"Examining alternate covers for ID: {0} {1} ...".format(
m['volume_id'],
m['series']),
newline=False)
@ -640,18 +638,18 @@ class IssueIdentifier:
if len(self.match_list) == 1:
self.log_msg("No matching pages in the issue.")
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
self.search_result = self.ResultFoundMatchButBadCoverScore
else:
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
self.log_msg(
u"Multiple bad cover matches! Need to use other info...")
"Multiple bad cover matches! Need to use other info...")
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
self.search_result = self.ResultMultipleMatchesWithBadImageScores
return self.match_list
else:
@ -695,28 +693,28 @@ class IssueIdentifier:
if len(self.match_list) == 1:
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
self.search_result = self.ResultOneGoodMatch
elif len(self.match_list) == 0:
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
self.log_msg("No matches found :(")
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.ResultMultipleGoodMatches
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
for item in self.match_list:
print_match(item)
self.log_msg(
u"--------------------------------------------------------------------------")
"--------------------------------------------------------------------------")
return self.match_list

View File

@ -18,29 +18,29 @@
#import os
#import re
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 PyQt5.QtCore import QUrl, pyqtSignal, QByteArray
#from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from settings import ComicTaggerSettings
from issuestring import IssueString
from coverimagewidget import CoverImageWidget
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(QtGui.QTableWidgetItem):
class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem):
def __lt__(self, other):
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
selfStr = self.data(QtCore.Qt.DisplayRole)
otherStr = other.data(QtCore.Qt.DisplayRole)
return (IssueString(selfStr).asFloat() <
IssueString(otherStr).asFloat())
class IssueSelectionWindow(QtGui.QDialog):
class IssueSelectionWindow(QtWidgets.QDialog):
volume_id = 0
@ -52,7 +52,7 @@ class IssueSelectionWindow(QtGui.QDialog):
self.coverWidget = CoverImageWidget(
self.coverImageContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtGui.QGridLayout(self.coverImageContainer)
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
gridlayout.addWidget(self.coverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -85,15 +85,14 @@ class IssueSelectionWindow(QtGui.QDialog):
self.twList.selectRow(0)
else:
for r in range(0, self.twList.rowCount()):
issue_id, b = self.twList.item(
r, 0).data(QtCore.Qt.UserRole).toInt()
issue_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
if (issue_id == self.initial_id):
self.twList.selectRow(r)
break
def performQuery(self):
QtGui.QApplication.setOverrideCursor(
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
try:
@ -101,14 +100,14 @@ class IssueSelectionWindow(QtGui.QDialog):
volume_data = comicVine.fetchVolumeData(self.series_id)
self.issue_list = comicVine.fetchIssuesByVolume(self.series_id)
except ComicVineTalkerException as e:
QtGui.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.restoreOverrideCursor()
if e.code == ComicVineTalkerException.RateLimit:
QtGui.QMessageBox.critical(
QtWidgets.QMessageBox.critical(
self,
self.tr("Comic Vine Error"),
ComicVineTalker.getRateLimitMessage())
else:
QtGui.QMessageBox.critical(
QtWidgets.QMessageBox.critical(
self,
self.tr("Network Issue"),
self.tr("Could not connect to Comic Vine to list issues!"))
@ -139,7 +138,7 @@ class IssueSelectionWindow(QtGui.QDialog):
if len(parts) > 1:
item_text = parts[0] + "-" + parts[1]
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
@ -147,7 +146,7 @@ class IssueSelectionWindow(QtGui.QDialog):
item_text = record['name']
if item_text is None:
item_text = ""
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
@ -162,7 +161,7 @@ class IssueSelectionWindow(QtGui.QDialog):
self.twList.setSortingEnabled(True)
self.twList.sortItems(0, QtCore.Qt.AscendingOrder)
QtGui.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.restoreOverrideCursor()
def cellDoubleClicked(self, r, c):
self.accept()
@ -174,8 +173,7 @@ class IssueSelectionWindow(QtGui.QDialog):
if prev is not None and prev.row() == curr.row():
return
self.issue_id, b = self.twList.item(
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
# list selection was changed, update the the issue cover
for record in self.issue_list:

View File

@ -17,12 +17,12 @@
#import sys
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
class LogWindow(QtGui.QDialog):
class LogWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(LogWindow, self).__init__(parent)
@ -34,4 +34,8 @@ class LogWindow(QtGui.QDialog):
QtCore.Qt.WindowMaximizeButtonHint)
def setText(self, text):
try:
text = text.decode()
except:
pass
self.textEdit.setPlainText(text)

View File

@ -14,65 +14,84 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import signal
import traceback
import platform
#import os
from .settings import ComicTaggerSettings
# Need to load setting before anything else
SETTINGS = ComicTaggerSettings()
try:
qt_available = True
from PyQt4 import QtCore, QtGui
from taggerwindow import TaggerWindow
from PyQt5 import QtCore, QtGui, QtWidgets
from .taggerwindow import TaggerWindow
except ImportError as e:
qt_available = False
import utils
import cli
from settings import ComicTaggerSettings
from options import Options
from comicvinetalker import ComicVineTalker
from . import utils
from . import cli
from .options import Options
from .comicvinetalker import ComicVineTalker
def ctmain():
utils.fix_output_encoding()
settings = ComicTaggerSettings()
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)
if not qt_available and not opts.no_gui:
opts.no_gui = True
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
print("PyQt5 is not available. ComicTagger is limited to command-line mode.", file=sys.stderr)
if opts.no_gui:
cli.cli_mode(opts, settings)
cli.cli_mode(opts, SETTINGS)
else:
app = QtGui.QApplication(sys.argv)
os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
app = QtWidgets.QApplication(sys.argv)
if platform.system() == "Darwin":
# Set the MacOS dock icon
app.setWindowIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('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 = u'comictagger' # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
# force close of console window
SWP_HIDEWINDOW = 0x0080
consoleWnd = ctypes.windll.kernel32.GetConsoleWindow()
if consoleWnd != 0:
ctypes.windll.user32.SetWindowPos(consoleWnd, None, 0, 0, 0, 0, SWP_HIDEWINDOW)
if platform.system() != "Linux":
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
splash = QtGui.QSplashScreen(img)
splash = QtWidgets.QSplashScreen(img)
splash.show()
splash.raise_()
app.processEvents()
try:
tagger_window = TaggerWindow(opts.file_list, settings, opts=opts)
tagger_window = TaggerWindow(opts.file_list, SETTINGS, opts=opts)
tagger_window.setWindowIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
tagger_window.show()
if platform.system() != "Linux":
@ -80,8 +99,8 @@ def ctmain():
sys.exit(app.exec_())
except Exception as e:
QtGui.QMessageBox.critical(
QtGui.QMainWindow(),
QtWidgets.QMessageBox.critical(
QtWidgets.QMainWindow(),
"Error",
"Unhandled exception in app:\n" +
traceback.format_exc())

View File

@ -17,11 +17,11 @@
import os
#import sys
from PyQt4 import QtCore, QtGui, uic
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from PyQt5 import QtCore, QtGui, QtWidgets, uic
#from PyQt5.QtCore import QUrl, pyqtSignal, QByteArray
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#from imagefetcher import ImageFetcher
#from comicarchive import MetaDataStyle
@ -29,7 +29,7 @@ from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#import utils
class MatchSelectionWindow(QtGui.QDialog):
class MatchSelectionWindow(QtWidgets.QDialog):
volume_id = 0
@ -41,13 +41,13 @@ class MatchSelectionWindow(QtGui.QDialog):
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -74,7 +74,7 @@ class MatchSelectionWindow(QtGui.QDialog):
self.twList.selectRow(0)
path = self.comic_archive.path
self.setWindowTitle(u"Select correct match: {0}".format(
self.setWindowTitle("Select correct match: {0}".format(
os.path.split(path)[1]))
def populateTable(self):
@ -89,30 +89,30 @@ class MatchSelectionWindow(QtGui.QDialog):
self.twList.insertRow(row)
item_text = match['series']
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.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 = u"{0}".format(match['publisher'])
item_text = "{0}".format(match['publisher'])
else:
item_text = u"Unknown"
item = QtGui.QTableWidgetItem(item_text)
item_text = "Unknown"
item = QtWidgets.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 = u""
year_str = u"????"
month_str = ""
year_str = "????"
if match['month'] is not None:
month_str = u"-{0:02d}".format(int(match['month']))
month_str = "-{0:02d}".format(int(match['month']))
if match['year'] is not None:
year_str = u"{0}".format(match['year'])
year_str = "{0}".format(match['year'])
item_text = year_str + month_str
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
@ -120,7 +120,7 @@ class MatchSelectionWindow(QtGui.QDialog):
item_text = match['issue_title']
if item_text is None:
item_text = ""
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
@ -156,5 +156,5 @@ class MatchSelectionWindow(QtGui.QDialog):
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(
QtCore.Qt.UserRole).toPyObject()[0]
QtCore.Qt.UserRole)[0]
return match

View File

@ -1,4 +1,4 @@
"""A PyQt4 dialog to show a message and let the user check a box
"""A PyQt5 dialog to show a message and let the user check a box
Example usage:
@ -25,8 +25,9 @@ said_yes, checked = OptionalMessageDialog.question(self, "Question",
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
StyleMessage = 0

View File

@ -25,11 +25,12 @@ try:
except ImportError:
pass
from genericmetadata import GenericMetadata
from comicarchive import MetaDataStyle
from versionchecker import VersionChecker
import ctversion
import utils
from datetime import datetime
from .genericmetadata import GenericMetadata
from .comicarchive import MetaDataStyle
from .versionchecker import VersionChecker
from . import ctversion
from . import utils
class Options:
@ -103,7 +104,7 @@ If no options are given, {0} will run in windowed mode.
--version Display version.
-h, --help Display this message.
For more help visit the wiki at: http://code.google.com/p/comictagger/
For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
"""
def __init__(self):
@ -144,7 +145,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
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)
@ -196,7 +197,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
# Map the dict to the metadata object
for key in md_dict:
if not hasattr(md, key):
print("Warning: '{0}' is not a valid tag name".format(key))
print(("Warning: '{0}' is not a valid tag name".format(key)))
else:
md.isEmpty = False
setattr(md, key, md_dict[key])
@ -216,7 +217,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
break
sys.argv = script_args
if not os.path.exists(scriptfile):
print("Can't find {0}".format(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
@ -232,11 +233,11 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
if "main" in dir(script):
script.main()
else:
print(
"Can't find entry point \"main()\" in module \"{0}\"".format(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())
print("Script raised an unhandled exception: ", e)
print((traceback.format_exc()))
sys.exit(0)
@ -340,8 +341,8 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
if o == "--only-set-cv-key":
self.only_set_key = True
if o == "--version":
print(
"ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(ctversion.version))
print((
"ComicTagger {}: Copyright (c) 2012-{:%Y} ComicTagger Team".format(ctversion.version, datetime.today())))
print(
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
sys.exit(0)

View File

@ -18,13 +18,13 @@ import platform
#import sys
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
from .coverimagewidget import CoverImageWidget
class PageBrowserWindow(QtGui.QDialog):
class PageBrowserWindow(QtWidgets.QDialog):
def __init__(self, parent, metadata):
super(PageBrowserWindow, self).__init__(parent)
@ -33,7 +33,7 @@ class PageBrowserWindow(QtGui.QDialog):
self.pageWidget = CoverImageWidget(
self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtGui.QGridLayout(self.pageContainer)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.pageWidget.showControls = False
@ -47,7 +47,7 @@ class PageBrowserWindow(QtGui.QDialog):
self.current_page_num = 0
self.metadata = metadata
self.buttonBox.button(QtGui.QDialogButtonBox.Close).setDefault(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).setDefault(True)
if platform.system() == "Darwin":
self.btnPrev.setText("<<")
self.btnNext.setText(">>")

View File

@ -1,4 +1,4 @@
"""A PyQt4 widget for editing the page list info"""
"""A PyQt5 widget for editing the page list info"""
# Copyright 2012-2014 Anthony Beville
@ -15,15 +15,17 @@
# limitations under the License.
#import os
from operator import itemgetter, attrgetter
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
from .genericmetadata import GenericMetadata, PageType
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
#from pageloader import PageLoader
@ -125,25 +127,75 @@ class PageListEditor(QWidget):
self.comic_archive = None
self.pages_list = None
def getNewIndexes(self, movement):
selection = self.listWidget.selectionModel().selectedRows()
selection.sort(reverse=movement>0)
current = 0
newindexes = []
oldindexes = []
for x in selection:
current = x.row()
oldindexes.append(current)
if current + movement >= 0 and current + movement <= self.listWidget.count()-1:
if len(newindexes) < 1 or current + movement != newindexes[-1]:
current += movement
else:
prev = current
newindexes.append(current)
oldindexes.sort()
newindexes.sort()
return list(zip(newindexes, oldindexes))
def SetSelection(self, indexes):
selectionRanges = []
first = 0
for i, selection in enumerate(indexes):
if i == 0:
first = selection[0]
continue
if selection != indexes[i-1][0]+1:
selectionRanges.append((first,indexes[i-1][0]))
first = selection[0]
selectionRanges.append((first, indexes[-1][0]))
selection = QItemSelection()
for x in selectionRanges:
selection.merge(QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)), QItemSelectionModel.Select)
self.listWidget.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
return selectionRanges
def moveCurrentUp(self):
row = self.listWidget.currentRow()
selection = self.getNewIndexes(-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.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
self.SetSelection(selection)
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def moveCurrentDown(self):
row = self.listWidget.currentRow()
selection = self.getNewIndexes(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.emitFrontCoverChange()
self.modified.emit()
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.SetSelection(selection)
self.modified.emit()
def itemMoveEvent(self, s):
# print "move event: ", s, self.listWidget.currentRow()
@ -156,7 +208,7 @@ class PageListEditor(QWidget):
self.modified.emit()
def changePageType(self, i):
new_type = self.comboBox.itemData(i).toString()
new_type = self.comboBox.itemData(i)
if self.getCurrentPageType() != new_type:
self.setCurrentPageType(new_type)
self.emitFrontCoverChange()
@ -171,7 +223,7 @@ class PageListEditor(QWidget):
#idx = int(str (self.listWidget.item(row).text()))
idx = int(self.listWidget.item(row).data(
Qt.UserRole).toPyObject()[0]['Image'])
Qt.UserRole)[0]['Image'])
if self.comic_archive is not None:
self.pageWidget.setArchive(self.comic_archive, idx)
@ -180,7 +232,7 @@ class PageListEditor(QWidget):
frontCover = 0
for i in range(self.listWidget.count()):
item = self.listWidget.item(i)
page_dict = item.data(Qt.UserRole).toPyObject()[0]
page_dict = item.data(Qt.UserRole)[0] #.toPyObject()[0]
if 'Type' in page_dict and page_dict[
'Type'] == PageType.FrontCover:
frontCover = int(page_dict['Image'])
@ -189,7 +241,7 @@ class PageListEditor(QWidget):
def getCurrentPageType(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] #.toPyObject()[0]
if 'Type' in page_dict:
return page_dict['Type']
else:
@ -197,7 +249,7 @@ class PageListEditor(QWidget):
def setCurrentPageType(self, t):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] #.toPyObject()[0]
if t == "":
if 'Type' in page_dict:
@ -239,7 +291,7 @@ class PageListEditor(QWidget):
page_list = []
for i in range(self.listWidget.count()):
item = self.listWidget.item(i)
page_list.append(item.data(Qt.UserRole).toPyObject()[0])
page_list.append(item.data(Qt.UserRole)[0]) #.toPyObject()[0]
return page_list
def emitFrontCoverChange(self):

View File

@ -14,8 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import pyqtSignal
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal
from comictaggerlib.ui.qtutils import getQImageFromData
#from comicarchive import ComicArchive

View File

@ -1,4 +1,4 @@
"""A PyQT4 dialog to show ID log and progress"""
"""A PyQT5 dialog to show ID log and progress"""
# Copyright 2012-2014 Anthony Beville
@ -17,14 +17,14 @@
#import sys
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
#import utils
class IDProgressWindow(QtGui.QDialog):
class IDProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(IDProgressWindow, self).__init__(parent)

View File

@ -16,16 +16,17 @@
import os
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from settingswindow import SettingsWindow
from filerenamer import FileRenamer
from comicarchive import MetaDataStyle
import utils
from .settings import ComicTaggerSettings
from .settingswindow import SettingsWindow
from .filerenamer import FileRenamer
from .comicarchive import MetaDataStyle
from comictaggerlib.ui.qtutils import centerWindowOnParent
from . import utils
class RenameWindow(QtGui.QDialog):
class RenameWindow(QtWidgets.QDialog):
def __init__(self, parent, comic_archive_list, data_style, settings):
super(RenameWindow, self).__init__(parent)
@ -79,9 +80,9 @@ class RenameWindow(QtGui.QDialog):
row = self.twList.rowCount()
self.twList.insertRow(row)
folder_item = QtGui.QTableWidgetItem()
old_name_item = QtGui.QTableWidgetItem()
new_name_item = QtGui.QTableWidgetItem()
folder_item = QtWidgets.QTableWidgetItem()
old_name_item = QtWidgets.QTableWidgetItem()
new_name_item = QtWidgets.QTableWidgetItem()
item_text = os.path.split(ca.path)[0]
folder_item.setFlags(
@ -128,23 +129,28 @@ class RenameWindow(QtGui.QDialog):
def accept(self):
progdialog = QtGui.QProgressDialog(
progdialog = QtWidgets.QProgressDialog(
"", "Cancel", 0, len(self.rename_list), self)
progdialog.setWindowTitle("Renaming Archives")
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.show()
progdialog.setMinimumDuration(100)
centerWindowOnParent(progdialog)
#progdialog.show()
QtCore.QCoreApplication.processEvents()
for idx, item in enumerate(self.rename_list):
QtCore.QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx)
idx += 1
progdialog.setValue(idx)
progdialog.setLabelText(item['new_name'])
centerWindowOnParent(progdialog)
QtCore.QCoreApplication.processEvents()
if item['new_name'] == os.path.basename(item['archive'].path):
print item['new_name'], "Filename is already good!"
print(item['new_name'], "Filename is already good!")
continue
if not item['archive'].isWritable(check_rar_status=False):
@ -158,6 +164,7 @@ class RenameWindow(QtGui.QDialog):
item['archive'].rename(new_abs_path)
progdialog.close()
progdialog.hide()
QtCore.QCoreApplication.processEvents()
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)

View File

@ -21,7 +21,7 @@ import platform
import codecs
import uuid
import utils
from . import utils
class ComicTaggerSettings:
@ -34,23 +34,13 @@ class ComicTaggerSettings:
else:
folder = os.path.join(os.path.expanduser('~'), '.ComicTagger')
if folder is not None:
folder = folder.decode(filename_encoding)
folder = folder
return folder
frozen_win_exe_path = None
@staticmethod
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
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
@ -66,12 +56,10 @@ class ComicTaggerSettings:
return os.path.join(ui_folder, filename)
def setDefaultValues(self):
# General Settings
self.rar_exe_path = ""
self.unrar_exe_path = ""
self.allow_cbi_in_rar = True
self.check_for_new_version = True
self.check_for_new_version = False
self.send_usage_stats = False
# automatic settings
@ -90,14 +78,13 @@ class ComicTaggerSettings:
# identifier settings
self.id_length_delta_thresh = 5
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
self.id_publisher_blacklist = "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
self.show_no_unrar_warning = True
# filename parsing settings
self.parse_scan_info = True
@ -154,7 +141,7 @@ class ComicTaggerSettings:
else:
self.load()
# take a crack at finding rar exes, if not set already
# take a crack at finding rar exe, if not set already
if self.rar_exe_path == "":
if platform.system() == "Windows":
# look in some likely places for Windows machines
@ -168,19 +155,9 @@ class ComicTaggerSettings:
self.rar_exe_path = utils.which("rar")
if self.rar_exe_path != "":
self.save()
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))
if self.rar_exe_path != "":
# make sure rar program is now in the path for the rar class
utils.addtopath(os.path.dirname(self.rar_exe_path))
def reset(self):
os.unlink(self.settings_file)
@ -199,7 +176,6 @@ class ComicTaggerSettings:
readline_generator(codecs.open(self.settings_file, "r", "utf8")))
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')
@ -267,9 +243,6 @@ class ComicTaggerSettings:
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(
@ -359,7 +332,6 @@ class ComicTaggerSettings:
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', 'unrar_exe_path', self.unrar_exe_path)
self.config.set('settings', 'send_usage_stats', self.send_usage_stats)
if not self.config.has_section('auto'):
@ -418,8 +390,6 @@ class ComicTaggerSettings:
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')

View File

@ -16,42 +16,44 @@
import platform
import os
import sys
from PyQt4 import QtCore, QtGui, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from comicvinetalker import ComicVineTalker
from imagefetcher import ImageFetcher
import utils
from .settings import ComicTaggerSettings
from .comicvinecacher import ComicVineCacher
from .comicvinetalker import ComicVineTalker
from .imagefetcher import ImageFetcher
from . import utils
windowsRarHelp = """
<html><head/><body><p>In order to write to CBR/RAR archives,
<html><head/><body><p>To write to CBR/RAR archives,
you will need to have the tools from
<a href="http://www.win-rar.com/download.html">
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span>
</a> installed. </p></body></html>
<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>
"""
linuxRarHelp = """
<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>
<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>
"""
macRarHelp = """
<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>
<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>
"""
class SettingsWindow(QtGui.QDialog):
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent, settings):
super(SettingsWindow, self).__init__(parent)
@ -62,18 +64,17 @@ class SettingsWindow(QtGui.QDialog):
~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.name = "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"
@ -109,7 +110,6 @@ class SettingsWindow(QtGui.QDialog):
self.settingsToForm()
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)
@ -118,7 +118,6 @@ class SettingsWindow(QtGui.QDialog):
# Copy values from settings to form
self.leRarExePath.setText(self.settings.rar_exe_path)
self.leUnrarExePath.setText(self.settings.unrar_exe_path)
self.leNameLengthDeltaThresh.setText(
str(self.settings.id_length_delta_thresh))
self.tePublisherBlacklist.setPlainText(
@ -171,12 +170,11 @@ class SettingsWindow(QtGui.QDialog):
# 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 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))
# make sure rar program is now in the path for the rar class
if self.settings.rar_exe_path:
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
self.leNameLengthDeltaThresh.setText("0")
@ -195,8 +193,8 @@ class SettingsWindow(QtGui.QDialog):
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.cv_api_key = unicode(self.leKey.text())
ComicVineTalker.api_key = self.settings.cv_api_key
self.settings.cv_api_key = str(self.leKey.text())
ComicVineTalker.api_key = self.settings.cv_api_key.strip()
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()
@ -214,32 +212,29 @@ class SettingsWindow(QtGui.QDialog):
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.save()
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)
def selectRar(self):
self.selectFile(self.leRarExePath, "RAR")
def selectUnrar(self):
self.selectFile(self.leUnrarExePath, "UnRAR")
def clearCache(self):
ImageFetcher().clearCache()
ComicVineCacher().clearCache()
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self, self.name, "Cache has been cleared.")
def testAPIKey(self):
if ComicVineTalker().testKey(unicode(self.leKey.text())):
QtGui.QMessageBox.information(
if ComicVineTalker().testKey(str(self.leKey.text()).strip()):
QtWidgets.QMessageBox.information(
self, "API Key Test", "Key is valid!")
else:
QtGui.QMessageBox.warning(
QtWidgets.QMessageBox.warning(
self, "API Key Test", "Key is NOT valid.")
def resetSettings(self):
self.settings.reset()
self.settingsToForm()
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self,
self.name,
self.name +
@ -247,14 +242,14 @@ class SettingsWindow(QtGui.QDialog):
def selectFile(self, control, name):
dialog = QtGui.QFileDialog(self)
dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
dialog = QtWidgets.QFileDialog(self)
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
if platform.system() == "Windows":
if name == "RAR":
filter = self.tr("Rar Program (Rar.exe)")
else:
filter = self.tr("Programs (*.exe)")
filter = self.tr("Libraries (*.dll)")
dialog.setNameFilter(filter)
else:
# QtCore.QDir.Executable | QtCore.QDir.Files)
@ -262,8 +257,11 @@ class SettingsWindow(QtGui.QDialog):
pass
dialog.setDirectory(os.path.dirname(str(control.text())))
dialog.setWindowTitle("Find " + name + " program")
if name == "RAR":
dialog.setWindowTitle("Find " + name + " program")
else:
dialog.setWindowTitle("Find " + name + " library")
if (dialog.exec_()):
fileList = dialog.selectedFiles()
control.setText(str(fileList[0]))

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,9 @@
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item>

View File

@ -8,7 +8,7 @@ from comictaggerlib.settings import ComicTaggerSettings
try:
from PyQt4 import QtGui
from PyQt5 import QtGui
qt_available = True
except ImportError:
qt_available = False
@ -64,7 +64,7 @@ if qt_available:
try:
from PIL import Image
from PIL import WebPImagePlugin
import StringIO
import io
pil_available = True
except ImportError:
pil_available = False
@ -78,11 +78,10 @@ if qt_available:
# 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
im = Image.open(io.StringIO(image_data))
output = io.StringIO()
im.save(output, format="PNG")
success = img.loadFromData(output.getvalue())
except Exception as e:
pass
# if still nothing, go with default image

View File

@ -116,118 +116,6 @@
</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In order to read/write to CBR/RAR archives, you will need to have the shareware tools from &lt;a href=&quot;www.win-rar.com/download.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;WinRAR&lt;/span&gt;&lt;/a&gt; installed. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item row="1" column="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">
@ -471,7 +359,7 @@
<item row="1" column="2">
<widget class="QPushButton" name="btnTestKey">
<property name="text">
<string>Tesk Key</string>
<string>Test Key</string>
</property>
</widget>
</item>
@ -529,7 +417,7 @@
<x>11</x>
<y>21</y>
<width>251</width>
<height>192</height>
<height>199</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_7">
@ -688,6 +576,111 @@
</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In order to write to CBR/RAR archives, you will need to have the shareware tools from &lt;a href=&quot;www.win-rar.com/download.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;WinRAR&lt;/span&gt;&lt;/a&gt; installed. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
</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>

View File

@ -16,13 +16,13 @@
import sys
import platform
import urllib2
import requests
import urllib.parse
#import os
#import urllib
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, QByteArray
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
@ -38,7 +38,7 @@ except ImportError:
def emit(a, b, c):
pass
import ctversion
from . import ctversion
class VersionChecker(QObject):
@ -47,28 +47,30 @@ class VersionChecker(QObject):
base_url = "http://comictagger1.appspot.com/latest"
args = ""
params = dict()
if use_stats:
params = {
'uuid': uuid,
'version': ctversion.version
}
if platform.system() == "Windows":
plat = "win"
params['platform'] = "win"
elif platform.system() == "Linux":
plat = "lin"
params['platform'] = "lin"
elif platform.system() == "Darwin":
plat = "mac"
params['platform'] = "mac"
else:
plat = "other"
args = "?uuid={0}&platform={1}&version={2}".format(
uuid, plat, ctversion.version)
if not getattr(sys, 'frozen', None):
args += "&src=T"
params['platform'] = "other"
return base_url + args
if not getattr(sys, 'frozen', None):
params['src'] = 'T'
return (base_url, params)
def getLatestVersion(self, uuid, use_stats=True):
try:
resp = urllib2.urlopen(self.getRequestUrl(uuid, use_stats))
new_version = resp.read()
url, params = self.getRequestUrl(uuid, use_stats)
new_version = requests.get(url, params=params).text
except Exception as e:
return None
@ -79,12 +81,11 @@ class VersionChecker(QObject):
versionRequestComplete = pyqtSignal(str)
def asyncGetLatestVersion(self, uuid, use_stats):
url = self.getRequestUrl(uuid, use_stats)
url, params = self.getRequestUrl(uuid, use_stats)
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.asyncGetLatestVersionComplete)
self.nam.get(QNetworkRequest(QUrl(str(url))))
self.nam.get(QNetworkRequest(QUrl(str(url + '?' + urllib.parse.urlencode(params)))))
def asyncGetLatestVersionComplete(self, reply):
if (reply.error() != QNetworkReply.NoError):

View File

@ -18,20 +18,20 @@
#import time
#import os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl, pyqtSignal
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import QUrl, pyqtSignal
#from PyQt4.QtCore import QObject
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
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 .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, centerWindowOnParent
#from imagefetcher import ImageFetcher
#import utils
@ -90,7 +90,7 @@ class IdentifyThread(QtCore.QThread):
self.identifyComplete.emit()
class VolumeSelectionWindow(QtGui.QDialog):
class VolumeSelectionWindow(QtWidgets.QDialog):
def __init__(self, parent, series_name, issue_number, year, issue_count,
cover_index_list, comic_archive, settings, autoselect=False):
@ -101,7 +101,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.imageWidget = CoverImageWidget(
self.imageContainer, CoverImageWidget.URLMode)
gridlayout = QtGui.QGridLayout(self.imageContainer)
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
gridlayout.addWidget(self.imageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -113,6 +113,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.parent = parent
self.series_name = series_name
self.issue_number = issue_number
self.year = year
@ -144,7 +145,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.btnRequery.setEnabled(enabled)
self.btnIssues.setEnabled(enabled)
self.btnAutoSelect.setEnabled(enabled)
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(enabled)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(enabled)
def requery(self,):
self.performQuery(refresh=True)
@ -153,12 +154,12 @@ class VolumeSelectionWindow(QtGui.QDialog):
def autoSelect(self):
if self.comic_archive is None:
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self, "Auto-Select", "You need to load a comic first!")
return
if self.issue_number is None or self.issue_number == "":
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self,
"Auto-Select",
"Can't auto-select without an issue number (yet!)")
@ -192,7 +193,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.iddialog.exec_()
def logIDOutput(self, text):
print unicode(text),
print(str(text), end=' ')
self.iddialog.textEdit.ensureCursorVisible()
self.iddialog.textEdit.insertPlainText(text)
@ -212,22 +213,22 @@ class VolumeSelectionWindow(QtGui.QDialog):
found_match = None
choices = False
if result == self.ii.ResultNoMatches:
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " No matches found :-(")
elif result == self.ii.ResultFoundMatchButBadCoverScore:
QtGui.QMessageBox.information(
QtWidgets.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.ResultFoundMatchButNotFirstPage:
QtGui.QMessageBox.information(
QtWidgets.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.ResultMultipleMatchesWithBadImageScores:
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self,
"Auto-Select Result",
" Found some possibilities, but no confidence. Proceed manually.")
@ -235,7 +236,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
elif result == self.ii.ResultOneGoodMatch:
found_match = matches[0]
elif result == self.ii.ResultMultipleGoodMatches:
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self,
"Auto-Select Result",
" Found multiple likely matches. Please select.")
@ -264,7 +265,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
for record in self.cv_search_results:
if record['id'] == self.volume_id:
title = record['name']
title += " (" + unicode(record['start_year']) + ")"
title += " (" + str(record['start_year']) + ")"
title += " - "
break
@ -279,26 +280,24 @@ class VolumeSelectionWindow(QtGui.QDialog):
def selectByID(self):
for r in range(0, self.twList.rowCount()):
volume_id, b = self.twList.item(
r, 0).data(QtCore.Qt.UserRole).toInt()
volume_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
if (volume_id == self.volume_id):
self.twList.selectRow(r)
break
def performQuery(self, refresh=False):
self.progdialog = QtGui.QProgressDialog(
self.progdialog = QtWidgets.QProgressDialog(
"Searching Online", "Cancel", 0, 100, self)
self.progdialog.setWindowTitle("Online Search")
self.progdialog.canceled.connect(self.searchCanceled)
self.progdialog.setModal(True)
self.progdialog.setMinimumDuration(300)
QtCore.QCoreApplication.processEvents()
self.search_thread = SearchThread(self.series_name, refresh)
self.search_thread.searchComplete.connect(self.searchComplete)
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
self.search_thread.start()
# QtCore.QCoreApplication.processEvents()
self.progdialog.exec_()
def searchCanceled(self):
@ -315,18 +314,20 @@ class VolumeSelectionWindow(QtGui.QDialog):
def searchProgressUpdate(self, current, total):
self.progdialog.setMaximum(total)
self.progdialog.setValue(current)
self.progdialog.setValue(current+1)
def searchComplete(self):
self.progdialog.accept()
del self.progdialog
QtCore.QCoreApplication.processEvents()
if self.search_thread.cv_error:
if self.search_thread.error_code == ComicVineTalkerException.RateLimit:
QtGui.QMessageBox.critical(
QtWidgets.QMessageBox.critical(
self,
self.tr("Comic Vine Error"),
ComicVineTalker.getRateLimitMessage())
else:
QtGui.QMessageBox.critical(
QtWidgets.QMessageBox.critical(
self,
self.tr("Network Issue"),
self.tr("Could not connect to Comic Vine to search for series!"))
@ -345,20 +346,20 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.twList.insertRow(row)
item_text = record['name']
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.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 = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.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 = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.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)
@ -367,7 +368,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
if record['publisher'] is not None:
item_text = record['publisher']['name']
item.setData(QtCore.Qt.ToolTipRole, item_text)
item = QtGui.QTableWidgetItem(item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
@ -382,7 +383,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
if len(self.cv_search_results) == 0:
QtCore.QCoreApplication.processEvents()
QtGui.QMessageBox.information(
QtWidgets.QMessageBox.information(
self, "Search Result", "No matches found!")
if self.immediate_autoselect and len(self.cv_search_results) > 0:
@ -404,9 +405,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
if prev is not None and prev.row() == curr.row():
return
self.volume_id, b = self.twList.item(
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
# list selection was changed, update the info on the volume
for record in self.cv_search_results:
if record['id'] == self.volume_id:

View File

@ -1 +0,0 @@
1.1.16-beta-rc

View File

@ -0,0 +1,11 @@
[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;

View File

@ -0,0 +1,4 @@
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.

View File

@ -0,0 +1,32 @@
<?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>

17
desktop-integration/mac/main.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# This is a lot of hoop-jumping to get the absolute path
# of this script, so that we can use the Symlinked python
# binary to call the CT script. This is all so that the
# Mac menu doesn't say "Python".
realpath()
{
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}
CTSCRIPT=%%CTSCRIPT%%
THIS=$(realpath $0)
THIS_FOLDER=$(dirname $THIS)
"$THIS_FOLDER/ComicTagger" "$CTSCRIPT"

View File

@ -0,0 +1,4 @@
This file is a placeholder that will automatically be replaced with a Windows
shortcut on the user's desktop.
When pip does an uninstall, it will remove the shortcut file.

View File

@ -5,7 +5,7 @@ TAGGER_BASE ?= ../
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
APP_NAME := ComicTagger
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
VERSION_STR := $(shell cd .. && python setup.py --version)
MAC_BASE := $(TAGGER_BASE)/mac
DIST_DIR := $(MAC_BASE)/dist
@ -21,7 +21,6 @@ dist:
$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -w -n $(APP_NAME) -s
cp -a $(TAGGER_SRC)/ui $(APP_BUNDLE)/Contents/MacOS
cp -a $(TAGGER_SRC)/graphics $(APP_BUNDLE)/Contents/MacOS
cp $(MAC_BASE)/libunrar.so $(APP_BUNDLE)/Contents/MacOS
cp $(MAC_BASE)/app.icns $(APP_BUNDLE)/Contents/Resources/icon-windowed.icns
# fix the version string in the Info.plist
sed -i -e 's/0\.0\.0/$(VERSION_STR)/' $(MAC_BASE)/dist/ComicTagger.app/Contents/Info.plist

13
pyproject.toml Normal file
View File

@ -0,0 +1,13 @@
[tool.black]
line-length = 150
[tool.isort]
line_length = 150
[build-system]
requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "comictaggerlib/ctversion.py"
local_scheme = "no-local-version"

1
requirements-CBR.txt Normal file
View File

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

1
requirements-GUI.txt Normal file
View File

@ -0,0 +1 @@
PyQt5<=5.15.3

View File

@ -1,5 +1,6 @@
configparser
beautifulsoup4 >= 4.1
unrar==0.3
natsort==3.5.2
PyPDF2==1.24
configparser
natsort
pillow>=4.3.0
requests

4
requirements_dev.txt Normal file
View File

@ -0,0 +1,4 @@
pyinstaller==4.3
setuptools>=42
setuptools_scm[toml]>=3.4
wheel

127
setup.py
View File

@ -1,66 +1,81 @@
#!/usr/bin/env python
# Setup file for comictagger python source (no wheels yet)
#
# An entry point script called "comictagger" will be created
#
# Currently commented out, an experiment at desktop integration.
# It seems that post installation tweaks are broken by wheel files.
# Kept here for further research
import glob
import os
from setuptools import setup
import comictaggerlib.ctversion
with open('requirements.txt') as f:
required = f.read().splitlines()
setup(name="comictagger",
install_requires=required,
version=comictaggerlib.ctversion.version,
description="A cross-platform GUI/CLI app for writing metadata to comic archives",
author="Anthony Beville",
author_email="comictagger@gmail.com",
url="http://code.google.com/p/comictagger/",
download_url="https://pypi.python.org/packages/source/c/comictagger/comictagger-{0}.zip".format(comictaggerlib.ctversion.version),
packages=["comictaggerlib", "comicapi", "comicapi/UnRAR2"],
package_data={
'comictaggerlib': ['ui/*.ui', 'graphics/*'],
'comicapi/UnRAR2': ['UnRARDLL/*.*', 'UnRARDLL/x64/*.*'],
},
scripts=["comictagger.py"],
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Win32 (MS Windows)",
"Environment :: MacOS X",
"Environment :: X11 Applications :: Qt",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: Apache Software License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Topic :: Utilities",
"Topic :: Other/Nonlisted Topic",
"Topic :: Multimedia :: Graphics"
],
license="Apache License 2.0",
def read(fname):
"""
Read the contents of a file.
Parameters
----------
fname : str
Path to file.
Returns
-------
str
File contents.
"""
with open(os.path.join(os.path.dirname(__file__), fname)) as f:
return f.read()
long_description="""
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
Features:
install_requires = read("requirements.txt").splitlines()
* 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.
# Dynamically determine extra dependencies
extras_require = {}
extra_req_files = glob.glob("requirements-*.txt")
for extra_req_file in extra_req_files:
name = os.path.splitext(extra_req_file)[0].replace("requirements-", "", 1)
extras_require[name] = read(extra_req_file).splitlines()
Requires:
# If there are any extras, add a catch-all case that includes everything.
# This assumes that entries in extras_require are lists (not single strings),
# and that there are no duplicated packages across the extras.
if extras_require:
extras_require["all"] = sorted({x for v in extras_require.values() for x in v})
* python 2.6 or 2.7
* configparser
* python imaging (PIL) >= 1.1.6
* beautifulsoup > 4.1
Optional requirement (for GUI):
* pyqt4
"""
)
setup(
name="comictagger",
install_requires=install_requires,
extras_require=extras_require,
python_requires=">=3",
description="A cross-platform GUI/CLI app for writing metadata to comic archives",
author="ComicTagger team",
author_email="comictagger@gmail.com",
url="https://github.com/comictagger/comictagger",
packages=["comictaggerlib", "comicapi"],
package_data={
"comictaggerlib": ["ui/*", "graphics/*"],
},
entry_points=dict(console_scripts=["comictagger=comictaggerlib.main:ctmain"]),
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Win32 (MS Windows)",
"Environment :: MacOS X",
"Environment :: X11 Applications :: Qt",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: Apache Software License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Topic :: Utilities",
"Topic :: Other/Nonlisted Topic",
"Topic :: Multimedia :: Graphics",
],
keywords=["comictagger", "comics", "comic", "metadata", "tagging", "tagger"],
license="Apache License 2.0",
long_description=read("README.md"),
long_description_content_type='text/markdown'
)

View File

@ -1,32 +0,0 @@
# This Makefile expects that certain GNU utils are available:
# rm, cp, grep, cut, cat
HOMEPATH ?= $(HOME)
TAGGER_BASE?= $(HOMEPATH)/Dropbox/tagger/comictagger
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
DIST_DIR := $(TAGGER_BASE)\windows\dist
NSIS_CMD := "C:\Program Files (x86)\NSIS\makensis.exe"
VERSION := $(shell grep version "$(TAGGER_SRC)/ctversion.py" | cut -d= -f2)
all: clean dist package
dist:
cd "$(TAGGER_BASE)" &
"C:\Python27\Scripts\cxfreeze.bat" $(TAGGER_BASE)\comictagger.py --icon nsis\app.ico
#--base-name=Win32GUI
cp -R C:\Python27\Lib\site-packages\PyQt4\plugins\imageformats $(DIST_DIR)
cp "$(TAGGER_SRC)\UnRAR2\UnRARDLL\unrar.dll" $(DIST_DIR)
cp -r "$(TAGGER_SRC)\ui" $(DIST_DIR)
cp -r "$(TAGGER_SRC)\graphics" $(DIST_DIR)
rm "$(DIST_DIR)\QtWebKit4.dll"
rm "$(DIST_DIR)\PyQt4.QtWebKit.pyd"
package:
echo !define RELEASE_STR $(VERSION) > $(TAGGER_BASE)\windows\nsis\release.nsh
$(NSIS_CMD) "$(TAGGER_BASE)\windows\nsis\comictagger.nsi"
mv "$(TAGGER_BASE)\windows\nsis\ComicTagger*.exe" "$(TAGGER_BASE)\release"
clean:
-rm -rf dist
-rm -rf nsis/release.nsh

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1 +0,0 @@
make clean dist package

View File

@ -1 +0,0 @@
make clean

View File

@ -1,171 +0,0 @@
;ComicTagger Installer
!addplugindir .
!include release.nsh
;--------------------------------
;Include Modern UI
!include "MUI2.nsh"
;--------------------------------
;General
;file name
OutFile "ComicTagger v${RELEASE_STR}.exe"
;Default installation folder
InstallDir "$PROGRAMFILES\ComicTagger"
;Request application privileges for Windows Vista
RequestExecutionLevel admin
InstallDirRegKey HKLM "Software\ComicTagger" ""
;Show all languages, despite user's codepage
;!define MUI_LANGDLL_ALLLANGUAGES
;--------------------------------
;Variables
Var StartMenuFolder
;--------------------------------
;Interface Configuration
!define MUI_ICON "installer.ico"
!define MUI_WELCOMEFINISHPAGE_BITMAP "side_graphic.bmp" ;shoukd be 164x314
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "top_graphic.bmp" ; ;should be 150x57
;!define MUI_ABORTWARNING
!define MUI_WELCOMEPAGE_TITLE $(app_WelcomePageTitle)
!define MUI_WELCOMEPAGE_TEXT $(app_WelcomePageText)
!define MUI_LICENSEPAGE_TEXT_TOP $(app_LicensePageTextTop)
;!define MUI_LICENSEPAGE_TEXT_BOTTOM $(app_LicensePageTextBottom)
;!define MUI_LICENSEPAGE_CHECKBOX
!define MUI_FINISHPAGE_NOAUTOCLOSE
!define MUI_FINISHPAGE_SHOWREADME "release_notes.txt"
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Show Release Notes"
!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED
!define MUI_FINISHPAGE_TITLE $(app_FinishPageTitle)
!define MUI_FINISHPAGE_TEXT $(app_FinishPageText)
!define MUI_FINISHPAGE_LINK $(app_FinishPageLink)
!define MUI_FINISHPAGE_LINK_LOCATION "http://code.google.com/p/comictagger/"
;Start Menu Folder Page Configuration
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\ComicTagger"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "ComicTagger"
;--------------------------------
;Pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "license.txt"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
;--------------------------------
;Languages
!include "languages.nsh"
;--------------------------------
;Reserve Files
;If you are using solid compression, files that are required before
;the actual installation should be stored first in the data block,
;because this will make your installer start faster.
!insertmacro MUI_RESERVEFILE_LANGDLL
;--------------------------------
;App Name and file
Name "$(app_AppName) ${RELEASE_STR}"
;Installer Sections
Section "Install Section" SecInstall
SetOutPath "$INSTDIR"
File /r ..\dist\*
File ..\..\release_notes.txt
;Store installation folder
WriteRegStr HKLM "Software\ComicTagger" "" $INSTDIR
; Add registry entries for Control Panel Uninstall
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ComicTagger" \
"DisplayName" "ComicTagger"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ComicTagger" \
"UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ComicTagger" \
"QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ComicTagger" \
"DisplayVersion" "${RELEASE_STR}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ComicTagger" \
"Publisher" "ComicTagger"
;Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
;Create shortcuts
CreateDirectory "$SMPROGRAMS\$StartMenuFolder"
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\ComicTagger.lnk" "$INSTDIR\comictagger.exe"
!insertmacro MUI_STARTMENU_WRITE_END
CreateShortCut "$DESKTOP\ComicTagger.lnk" "$INSTDIR\comictagger.exe" ""
SectionEnd
;--------------------------------
;Installer Functions
Function .onInit
!insertmacro MUI_LANGDLL_DISPLAY
FunctionEnd
;--------------------------------
;Uninstaller Section
Section "Uninstall"
Delete "$INSTDIR\*"
RMDir /r "$INSTDIR\imageformats"
RMDir /r "$INSTDIR\PyQt4.uic.widget-plugins"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"
Delete "$DESKTOP\ComicTagger.lnk"
!insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder
Delete "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk"
Delete "$SMPROGRAMS\$StartMenuFolder\ComicTagger.lnk"
RMDir "$SMPROGRAMS\$StartMenuFolder"
DeleteRegKey /ifempty HKLM "Software\ComicTagger"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ComicTagger"
SectionEnd

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,31 +0,0 @@
;--------------------------------------------------------------------------------------
;------------ ENGLISH -----------------------------------------------------------------
;--------------------------------------------------------------------------------------
!insertmacro MUI_LANGUAGE "English" ;first language is the default language
LangString app_AppName ${LANG_ENGLISH} "ComicTagger"
LangString app_WelcomePageTitle ${LANG_ENGLISH} \
"ComicTagger Installer"
LangString app_WelcomePageText ${LANG_ENGLISH} \
"Release: ${RELEASE_STR}$\n$\r$\n$\r\
This installer will guide you through the process of installing ComicTagger.$\n$\r $\n$\r"
LangString app_LicensePageTextTop ${LANG_ENGLISH} "ComicTagger End-user License Agreement"
LangString app_FinishPageTitle ${LANG_ENGLISH} \
"ComicTagger Installer"
LangString app_FinishPageText ${LANG_ENGLISH} \
"Installation complete!"
LangString app_FinishPageLink ${LANG_ENGLISH} "ComicTagger development site"
LangString LicenseFile ${LANG_ENGLISH} "license.txt" ;;these two should be the same
LicenseLangString LicenseRTF ${LANG_ENGLISH} "license.txt"
;--------------------------------------------------------------------------------------
;------------ SPANISH -----------------------------------------------------------------
;--------------------------------------------------------------------------------------
;!insertmacro MUI_LANGUAGE "Spanish"
;LangString app_AppName ${LANG_SPANISH} "ComicTagger Actualizaci<63>n"
;LangString app_WelcomePageTitle ${LANG_SPANISH} "ComicTagger Actualizaci<63>n$"
;LangString app_WelcomePageText ${LANG_SPANISH} "ES: Release: ${RELEASE_STR}$\n$\rBuild date: "

View File

@ -1,13 +0,0 @@
ComicTagger - Copyright 2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB