Compare commits

..

54 Commits

Author SHA1 Message Date
4608b97e23 v1.2.0
Separate comicapi into it's own package
Add support for tar files
Insert standard gitignore
Use suggested _version from setuptools-scm
Cleanup setup.py

Fix formatting in the rename template help
2021-08-05 22:42:13 -07:00
d18e94cd4e Update gitignore 2021-08-02 11:33:59 -07:00
feb206c165 Remove pyinstaller from requirements
pyinstaller is only needed for a specific deploy scenario
2021-07-11 05:54:05 +00:00
31e567fe1f Update current_version.txt 2021-07-02 03:42:52 +00:00
cb2031f2e7 Merge remote-tracking branch 'origin/un-cffi' 2021-07-01 19:20:38 -07:00
4ae1632aba Update usage of filetype
Fix an index out of range error when there are no duplicates to display
Exit immediately if there are no duplicates found
2021-03-07 19:59:39 -08:00
737ddd2486 Fix remove_ads.py 2021-03-07 19:41:54 -08:00
f31abe2df9 Add a shortcut for marking a page as an ad 2021-03-07 19:40:25 -08:00
fbc1137e0f Revert "Changed: use unrar-cffi for cbr handling (#151)"
This reverts commit 83d2557af3.
2020-08-14 22:15:52 -07:00
be698a17d6 Remove setuptools_scm 2020-07-26 19:28:45 -07:00
29be759c9c Implement isort and black 2020-07-06 16:11:15 -07:00
39052b58c6 Remove PyPDF2 2020-06-29 19:07:14 -07:00
83d2557af3 Changed: use unrar-cffi for cbr handling (#151) 2020-06-02 21:26:11 -07:00
a3d0b3372b fix broken drag & drop on macOS (#142) 2020-06-02 21:10:38 -07:00
9b097c80eb Increase comicvine search results per request to max (#164) 2020-06-02 21:09:32 -07:00
bbf9e6e38f Fix an issue with reducing the returned search results 2020-06-02 20:21:05 -07:00
6ab03cc3d3 Update natsort usage 2020-06-02 20:09:32 -07:00
8b227afbb2 Add a literal search option
In order to bypass processing of search results and search terms
2020-06-02 20:09:02 -07:00
3a3d67cbd0 update requirements.txt 2020-06-02 20:05:31 -07:00
676ecaf547 Update comic publisher imprints 2020-06-02 20:03:48 -07:00
931df0109d duplicate script and fixes 2020-05-30 15:30:27 -07:00
475540560a Merge branch 'seriesSearch' 2020-02-13 00:30:21 -08:00
7aa4e1c4ed 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.
2020-02-13 00:27:08 -08:00
f5e88d07bb Fix errors
Libraries updated and these are no longer needed
2020-02-13 00:04:44 -08:00
f634783d26 Merge branch 'requests' 2020-02-12 23:44:14 -08:00
ebfbbacc16 Add requests to requirements.txt 2020-02-12 23:30:04 -08:00
76b524c767 Merge branch 'Renaming' 2019-09-23 17:48:19 -07:00
fbb234a527 Add template error checking and other small issues
Error checking has been added in CLI mode, the settings window and when
running a rename operation from the GUI.
The Template Help window is now non-modal
Discrepancies between the tool tip and help window have been resolved
2019-09-23 17:47:59 -07:00
ce021d82cf Change filename parsing to default to the issue number
e.g. 123.cbr parses with series: 123, issue number: 123
2019-09-11 14:45:55 -07:00
cd4097f0c0 Fix seriesYear handling 2019-09-11 14:45:50 -07:00
cb9db19073 Fix requirements
pip doesn't recognize my PyQt5 install (Void Linux) and pip complains
on the git+https for pyinstaller
2019-09-11 14:45:45 -07:00
abd8019bf9 Add seriesYear attribute
Attribute is only serialized in ComicRack style metadata
2019-09-11 14:45:40 -07:00
c596062f55 Merge branch 'requests' into temp 2019-09-11 14:43:52 -07:00
c26f33e3d5 Merge branch 'duplicateFinder' into temp 2019-09-11 14:43:28 -07:00
37e6e81894 Merge branch 'Renaming' into temp 2019-09-11 14:43:21 -07:00
f73324f003 Merge branch 'AutoImprint' into temp 2019-09-11 14:43:05 -07:00
f0f8a061b5 Add publisher and imprint handling
Imprint handling has been added to utils and uses a subclassed dict to
return tuples for imprint matching may not be the best idea but it works
for now.

Add settings option auto_imprint
Add cli flag -a, --auto-import
2019-09-11 14:42:29 -07:00
944c0b9b2e Move to python requests module
requests is much simpler and fixes all ssl errors.
Comic Vine now requires a unique useragent string
2019-09-10 14:52:59 -07:00
8752e26476 Merge branch 'PageListEditorExtendedSelection' 2019-09-05 15:10:43 -07:00
146e7b21c6 Merge branch 'IssueString' 2019-09-05 15:10:39 -07:00
60e962b903 Merge branch 'FixLanguageSort' 2019-09-05 15:10:33 -07:00
0747a6b0ef Improve file renaming
Moves to Python format strings for renaming, handles directory
structures, moving of files to a destination directory, sanitizes
file paths with pathvalidate and takes a different approach to
smart filename cleanup using the Python string.Formatter class

Moving to Python format strings means we can point to python
documentation for syntax and all we have to do is document the
properties and types that are attached to the GenericMetadata class.

Switching to pathvalidate allows comictagger to more simply handle both
directories and symbols in filenames.

The only changes to the string.Formatter class is:
1. format_field returns
an empty string if the value is none or an empty string regardless of
the format specifier.
2. _vformat drops the previous literal text if the field value
is an empty string and lstrips the following literal text of closing
special characters.
2019-09-05 14:40:17 -07:00
51132c061b 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
2019-09-05 14:40:14 -07:00
3b01a9b58b convert duplicate finder script to Python 3, provide JSON output and call a script to actually handle the duplicate comics 2019-09-05 13:58:46 -07:00
7f79c2b024 Allow extended selection in the page list editor 2019-08-16 13:45:00 -07:00
0069d7fdc5 issue string parsing now strips off (# of #) (e.g. 1 of 45) 2019-08-16 13:43:43 -07:00
3606cbd0f2 Sort language correctly 2019-08-16 13:22:15 -07: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
113 changed files with 5652 additions and 9032 deletions

157
.gitignore vendored
View File

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

View File

@ -1,4 +0,0 @@
include README.txt
include release_notes.txt
include requirements.txt
recursive-include scripts *.py *.txt

View File

@ -1,9 +1,22 @@
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
VERSION_STR := $(shell python -c 'import comictaggerlib._version; print( comictaggerlib._version.version)')
ifeq ($(OS),Windows_NT)
APP_NAME=comictagger.exe
FINAL_NAME=ComicTagger-$(VERSION_STR).exe
ICON_PATH="windows/app.ico"
else ifeq ($(shell uname -s),Darwin)
APP_NAME=ComicTagger.app
FINAL_NAME=ComicTagger-$(VERSION_STR).app
ICON_PATH="mac/app.icns"
else
APP_NAME=comictagger
FINAL_NAME=ComicTagger-$(VERSION_STR)
ICON_PATH="windows/app.ico"
endif
.PHONY: all clean pydist upload dist
all: clean dist
clean:
rm -rf *~ *.pyc *.pyo
@ -12,50 +25,22 @@ 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__
pydist:
mkdir -p release
rm -f release/*.zip
make clean
mkdir -p piprelease
rm -f comictagger-$(VERSION_STR).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'
mv dist/comictagger-$(VERSION_STR).zip 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)"
dist:
pyinstaller.exe --name="comictagger" --windowed --add-data 'comictaggerlib/ui/*.ui;ui' --add-data 'comictaggerlib/graphics;graphics' -i windows/app.ico --version-file file_version_info.py comictagger.py
mv dist/$(APP_NAME) dist/$(FINAL_NAME)

View File

@ -1,53 +1,16 @@
This is a fork derived from google code:
A fork from https://github.com/comictagger/comictagger
https://code.google.com/p/comictagger/
Changes:
- switched to rarfile, makes dependencies simpler and I had issues using unrar-cffi with python<6.7
- Move to Python requests module, requests is much simpler and fixes all ssl errors.
- Moved to using Python format strings and use pathvalidate to handle filenames, supports directory structures
- Issue string parsing now strips off (# of #) (e.g. 1 of 45)
- Add publisher and imprint handling, currently hardcoded
Notes:
- I did some testing with the pyinstaller build, and it worked on both platforms. I did encounter two problems:
- Mac build showed the wrong widget set. I found a solution here that seemed to work: https://stackoverflow.com/questions/48626999/packaging-with-pyinstaller-pyqt5-setstyle-ignored
- Windows build had problems grabbing images from ComicVine using SSL. It think that some libraries are missing from the monolithic exe, but I couldn't figure out how to fix the problem.
- In setup.py you can also find the remains of an attempt to do some desktop integration from a pip install. It does work, but can cause problems with wheel installs, and I don't know if it's worth the bother. I kept the commented-out code in place, just in case.
Changes in this fork:
- using different unrar library https://pypi.python.org/pypi/unrar/. The previous one used unrar.dll on windows and
hackish wrapping of unrar command on linux, while this new one should use unrarlib on both platforms. From my tests
it is more stable and faster. *Requires unrarlib availability, check unrar module documentation for more
information*.
- extracted core libraries in its own package comicapi, shared in a new repository using git subtree for better
alignment with comicstreamer
- support for *day of month* field in the GUI
- merge of changes from fcanc fork
Todo:
- more tests in non-linux platforms
- repackage for simple user installation
Follows original readme:
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
Features:
* Runs on Mac OSX, Microsoft Windows, and Linux systems
* Communicates with an online database (Comic Vine) for acquiring metadata
* Uses image processing to automatically match a given archive with the correct issue data
* Batch processing in the GUI for tagging hundreds or more comics at a time
* Reads and writes multiple tagging schemes ( ComicBookLover and ComicRack, with more planned).
* Reads and writes RAR and Zip archives (external tools needed for writing RAR)
* Command line interface (CLI) on all platforms (including Windows), which supports batch operations, and which can be
used in native scripts for complex operations. For example, to recursively scrape and tag all archives in a folder
comictagger.py -R -s -o -f -t cr -v -i --nooverwrite /path/to/comics/
For details, screen-shots, release notes, and more, visit http://code.google.com/p/comictagger/
Requires:
* python 2.6 or 2.7
* configparser
* python imaging (PIL) >= 1.1.6
* beautifulsoup > 4.1
Optional requirement (for GUI):
* pyqt4
Install and run:
* ComicTagger can be run directly from this directory, using the launcher script "comictagger.py"
* To install on your system use: "python setup.py install". Take note in the output where comictagger.py goes!
With Python 3, it's much easier to get the app working from scratch on a new distro, as all of the dependencies are available as wheels, including PyQt5, so just a simple "pip install comictagger.zip" is all that's needed.

6
appveyor.yml Normal file
View File

@ -0,0 +1,6 @@
version: 1.0.{build}
build_script:
- cmd: powershell -exec bypass -File windows\fullbuild.ps1
artifacts:
- path: dist\*.exe
name: ComicTagger

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

@ -1 +0,0 @@
__author__ = 'dromanin'

View File

@ -1,276 +0,0 @@
"""A class to encapsulate CoMet data"""
# Copyright 2012-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.
import xml.etree.ElementTree as ET
#from datetime import datetime
#from pprint import pprint
#import zipfile
from genericmetadata import GenericMetadata
import utils
class CoMet:
writer_synonyms = ['writer', 'plotter', 'scripter']
penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns']
inker_synonyms = ['inker', 'artist', 'finishes']
colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer']
letterer_synonyms = ['letterer']
cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist']
editor_synonyms = ['editor']
def metadataFromString(self, string):
tree = ET.ElementTree(ET.fromstring(string))
return self.convertXMLToMetadata(tree)
def stringFromMetadata(self, metadata):
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
tree = self.convertMetadataToXML(self, metadata)
return header + ET.tostring(tree.getroot())
def indent(self, elem, level=0):
# for making the XML output readable
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML(self, filename, metadata):
# shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("comet")
root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/"
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib[
'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
# 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)
# title is manditory
if md.title is None:
md.title = ""
assign('title', md.title)
assign('series', md.series)
assign('issue', md.issue) # must be int??
assign('volume', md.volume)
assign('description', md.comments)
assign('publisher', md.publisher)
assign('pages', md.pageCount)
assign('format', md.format)
assign('language', md.language)
assign('rating', md.maturityRating)
assign('price', md.price)
assign('isVersionOf', md.isVersionOf)
assign('rights', md.rights)
assign('identifier', md.identifier)
assign('lastMark', md.lastMark)
assign('genre', md.genre) # TODO repeatable
if md.characters is not None:
char_list = [c.strip() for c in md.characters.split(',')]
for c in char_list:
assign('character', c)
if md.manga is not None and md.manga == "YesAndRightToLeft":
assign('readingDirection', "rtl")
date_str = ""
if md.year is not None:
date_str = str(md.year).zfill(4)
if md.month is not None:
date_str += "-" + str(md.month).zfill(2)
assign('date', date_str)
assign('coverImage', md.coverImage)
# need to specially process the credits, since they are structured
# differently than CIX
credit_writer_list = list()
credit_penciller_list = list()
credit_inker_list = list()
credit_colorist_list = list()
credit_letterer_list = list()
credit_cover_list = list()
credit_editor_list = list()
# loop thru credits, and build a list for each role that CoMet supports
for credit in metadata.credits:
if credit['role'].lower() in set(self.writer_synonyms):
ET.SubElement(
root,
'writer').text = u"{0}".format(
credit['person'])
if credit['role'].lower() in set(self.penciller_synonyms):
ET.SubElement(
root,
'penciller').text = u"{0}".format(
credit['person'])
if credit['role'].lower() in set(self.inker_synonyms):
ET.SubElement(
root,
'inker').text = u"{0}".format(
credit['person'])
if credit['role'].lower() in set(self.colorist_synonyms):
ET.SubElement(
root,
'colorist').text = u"{0}".format(
credit['person'])
if credit['role'].lower() in set(self.letterer_synonyms):
ET.SubElement(
root,
'letterer').text = u"{0}".format(
credit['person'])
if credit['role'].lower() in set(self.cover_synonyms):
ET.SubElement(
root,
'coverDesigner').text = u"{0}".format(
credit['person'])
if credit['role'].lower() in set(self.editor_synonyms):
ET.SubElement(
root,
'editor').text = u"{0}".format(
credit['person'])
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convertXMLToMetadata(self, tree):
root = tree.getroot()
if root.tag != 'comet':
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:
return None
md.series = xlate('series')
md.title = xlate('title')
md.issue = xlate('issue')
md.volume = xlate('volume')
md.comments = xlate('description')
md.publisher = xlate('publisher')
md.language = xlate('language')
md.format = xlate('format')
md.pageCount = xlate('pages')
md.maturityRating = xlate('rating')
md.price = xlate('price')
md.isVersionOf = xlate('isVersionOf')
md.rights = xlate('rights')
md.identifier = xlate('identifier')
md.lastMark = xlate('lastMark')
md.genre = xlate('genre') # TODO - repeatable field
date = xlate('date')
if date is not None:
parts = date.split('-')
if len(parts) > 0:
md.year = parts[0]
if len(parts) > 1:
md.month = parts[1]
md.coverImage = xlate('coverImage')
readingDirection = xlate('readingDirection')
if readingDirection is not None and readingDirection == "rtl":
md.manga = "YesAndRightToLeft"
# loop for character tags
char_list = []
for n in root:
if n.tag == 'character':
char_list.append(n.text.strip())
md.characters = utils.listToString(char_list)
# Now extract the credit info
for n in root:
if (n.tag == 'writer' or
n.tag == 'penciller' or
n.tag == 'inker' or
n.tag == 'colorist' or
n.tag == 'letterer' or
n.tag == 'editor'
):
metadata.addCredit(n.text.strip(), n.tag.title())
if n.tag == 'coverDesigner':
metadata.addCredit(n.text.strip(), "Cover")
metadata.isEmpty = False
return metadata
# verify that the string actually contains CoMet data in XML format
def validateString(self, string):
try:
tree = ET.ElementTree(ET.fromstring(string))
root = tree.getroot()
if root.tag != 'comet':
raise Exception
except:
return False
return True
def writeToExternalFile(self, filename, metadata):
tree = self.convertMetadataToXML(self, metadata)
# ET.dump(tree)
tree.write(filename, encoding='utf-8')
def readFromExternalFile(self, filename):
tree = ET.parse(filename)
return self.convertXMLToMetadata(tree)

File diff suppressed because it is too large Load Diff

View File

@ -1,144 +0,0 @@
"""A class to encapsulate the ComicBookInfo data"""
# Copyright 2012-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.
import json
from datetime import datetime
#import zipfile
from genericmetadata import GenericMetadata
import utils
#import ctversion
class ComicBookInfo:
def metadataFromString(self, string):
cbi_container = json.loads(unicode(string, 'utf-8'))
metadata = GenericMetadata()
cbi = 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 = xlate('series')
metadata.title = xlate('title')
metadata.issue = xlate('issue')
metadata.publisher = xlate('publisher')
metadata.month = xlate('publicationMonth')
metadata.year = xlate('publicationYear')
metadata.issueCount = xlate('numberOfIssues')
metadata.comments = xlate('comments')
metadata.credits = xlate('credits')
metadata.genre = xlate('genre')
metadata.volume = xlate('volume')
metadata.volumeCount = xlate('numberOfVolumes')
metadata.language = xlate('language')
metadata.country = xlate('country')
metadata.criticalRating = xlate('rating')
metadata.tags = xlate('tags')
# make sure credits and tags are at least empty lists and not None
if metadata.credits is None:
metadata.credits = []
if metadata.tags is None:
metadata.tags = []
# need to massage the language string to be ISO
if metadata.language is not None:
# reverse look-up
pattern = metadata.language
metadata.language = None
for key in utils.getLanguageDict():
if utils.getLanguageDict()[key] == pattern.encode('utf-8'):
metadata.language = key
break
metadata.isEmpty = False
return metadata
def stringFromMetadata(self, metadata):
cbi_container = self.createJSONDictionary(metadata)
return json.dumps(cbi_container)
def validateString(self, string):
"""Verify that the string actually contains CBI data in JSON format"""
try:
cbi_container = json.loads(string)
except:
return False
return ('ComicBookInfo/1.0' in cbi_container)
def createJSONDictionary(self, metadata):
"""Create the dictionary that we will convert to JSON text"""
cbi = dict()
cbi_container = {'appID': 'ComicTagger/' + '1.0.0', # ctversion.version,
'lastModified': str(datetime.now()),
'ComicBookInfo/1.0': cbi}
# helper func
def assign(cbi_entry, md_entry):
if md_entry is not None:
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('credits', metadata.credits)
assign('tags', metadata.tags)
return cbi_container
def writeToExternalFile(self, filename, metadata):
cbi_container = self.createJSONDictionary(metadata)
f = open(filename, 'w')
f.write(json.dumps(cbi_container, indent=4))
f.close

View File

@ -1,290 +0,0 @@
"""A class to encapsulate ComicRack's ComicInfo.xml data"""
# Copyright 2012-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.
import xml.etree.ElementTree as ET
#from datetime import datetime
#from pprint import pprint
#import zipfile
from genericmetadata import GenericMetadata
import utils
class ComicInfoXml:
writer_synonyms = ['writer', 'plotter', 'scripter']
penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns']
inker_synonyms = ['inker', 'artist', 'finishes']
colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer']
letterer_synonyms = ['letterer']
cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist']
editor_synonyms = ['editor']
def getParseableCredits(self):
parsable_credits = []
parsable_credits.extend(self.writer_synonyms)
parsable_credits.extend(self.penciller_synonyms)
parsable_credits.extend(self.inker_synonyms)
parsable_credits.extend(self.colorist_synonyms)
parsable_credits.extend(self.letterer_synonyms)
parsable_credits.extend(self.cover_synonyms)
parsable_credits.extend(self.editor_synonyms)
return parsable_credits
def metadataFromString(self, string):
tree = ET.ElementTree(ET.fromstring(string))
return self.convertXMLToMetadata(tree)
def stringFromMetadata(self, metadata):
header = '<?xml version="1.0"?>\n'
tree = self.convertMetadataToXML(self, metadata)
return header + ET.tostring(tree.getroot())
def indent(self, elem, level=0):
# for making the XML output readable
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML(self, filename, metadata):
# shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("ComicInfo")
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
# helper func
def assign(cix_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
assign('Title', md.title)
assign('Series', md.series)
assign('Number', md.issue)
assign('Count', md.issueCount)
assign('Volume', md.volume)
assign('AlternateSeries', md.alternateSeries)
assign('AlternateNumber', md.alternateNumber)
assign('StoryArc', md.storyArc)
assign('SeriesGroup', md.seriesGroup)
assign('AlternateCount', md.alternateCount)
assign('Summary', md.comments)
assign('Notes', md.notes)
assign('Year', md.year)
assign('Month', md.month)
assign('Day', md.day)
# need to specially process the credits, since they are structured
# differently than CIX
credit_writer_list = list()
credit_penciller_list = list()
credit_inker_list = list()
credit_colorist_list = list()
credit_letterer_list = list()
credit_cover_list = list()
credit_editor_list = list()
# first, loop thru credits, and build a list for each role that CIX
# supports
for credit in metadata.credits:
if credit['role'].lower() in set(self.writer_synonyms):
credit_writer_list.append(credit['person'].replace(",", ""))
if credit['role'].lower() in set(self.penciller_synonyms):
credit_penciller_list.append(credit['person'].replace(",", ""))
if credit['role'].lower() in set(self.inker_synonyms):
credit_inker_list.append(credit['person'].replace(",", ""))
if credit['role'].lower() in set(self.colorist_synonyms):
credit_colorist_list.append(credit['person'].replace(",", ""))
if credit['role'].lower() in set(self.letterer_synonyms):
credit_letterer_list.append(credit['person'].replace(",", ""))
if credit['role'].lower() in set(self.cover_synonyms):
credit_cover_list.append(credit['person'].replace(",", ""))
if credit['role'].lower() in set(self.editor_synonyms):
credit_editor_list.append(credit['person'].replace(",", ""))
# second, convert each list to string, and add to XML struct
if len(credit_writer_list) > 0:
node = ET.SubElement(root, 'Writer')
node.text = utils.listToString(credit_writer_list)
if len(credit_penciller_list) > 0:
node = ET.SubElement(root, 'Penciller')
node.text = utils.listToString(credit_penciller_list)
if len(credit_inker_list) > 0:
node = ET.SubElement(root, 'Inker')
node.text = utils.listToString(credit_inker_list)
if len(credit_colorist_list) > 0:
node = ET.SubElement(root, 'Colorist')
node.text = utils.listToString(credit_colorist_list)
if len(credit_letterer_list) > 0:
node = ET.SubElement(root, 'Letterer')
node.text = utils.listToString(credit_letterer_list)
if len(credit_cover_list) > 0:
node = ET.SubElement(root, 'CoverArtist')
node.text = utils.listToString(credit_cover_list)
if len(credit_editor_list) > 0:
node = ET.SubElement(root, 'Editor')
node.text = utils.listToString(credit_editor_list)
assign('Publisher', md.publisher)
assign('Imprint', md.imprint)
assign('Genre', md.genre)
assign('Web', md.webLink)
assign('PageCount', md.pageCount)
assign('LanguageISO', md.language)
assign('Format', md.format)
assign('AgeRating', md.maturityRating)
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
assign('Manga', md.manga)
assign('Characters', md.characters)
assign('Teams', md.teams)
assign('Locations', md.locations)
assign('ScanInformation', md.scanInfo)
# loop and add the page entries under pages node
if len(md.pages) > 0:
pages_node = ET.SubElement(root, 'Pages')
for page_dict in md.pages:
page_node = ET.SubElement(pages_node, 'Page')
page_node.attrib = page_dict
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convertXMLToMetadata(self, tree):
root = tree.getroot()
if root.tag != 'ComicInfo':
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:
return None
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')
tmp = xlate('BlackAndWhite')
md.blackAndWhite = False
if tmp is not None and tmp.lower() in ["yes", "true", "1"]:
md.blackAndWhite = True
# Now extract the credit info
for n in root:
if (n.tag == 'Writer' or
n.tag == 'Penciller' or
n.tag == 'Inker' or
n.tag == 'Colorist' or
n.tag == 'Letterer' or
n.tag == 'Editor'
):
if n.text is not None:
for name in n.text.split(','):
metadata.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")
# 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)
# print page.attrib
metadata.isEmpty = False
return metadata
def writeToExternalFile(self, filename, metadata):
tree = self.convertMetadataToXML(self, metadata)
# ET.dump(tree)
tree.write(filename, encoding='utf-8')
def readFromExternalFile(self, filename):
tree = ET.parse(filename)
return self.convertXMLToMetadata(tree)

View File

@ -1,292 +0,0 @@
"""Functions for parsing comic info from filename
This should probably be re-written, but, well, it mostly works!
"""
# Copyright 2012-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.
# Some portions of this code were modified from pyComicMetaThis project
# http://code.google.com/p/pycomicmetathis/
import re
import os
from urllib import unquote
class FileNameParser:
def repl(self, m):
return ' ' * len(m.group())
def fixSpaces(self, string, remove_dashes=True):
if remove_dashes:
placeholders = ['[-_]', ' +']
else:
placeholders = ['[_]', ' +']
for ph in placeholders:
string = re.sub(ph, self.repl, string)
return string # .strip()
def getIssueCount(self, filename, issue_end):
count = ""
filename = filename[issue_end:]
# replace any name separators with spaces
tmpstr = self.fixSpaces(filename)
found = False
match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
if not found:
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
count = count.lstrip("0")
return count
def getIssueNumber(self, filename):
"""Returns a tuple of issue number string, and start and end indexes in the filename
(The indexes will be used to split the string up for further parsing)
"""
found = False
issue = ''
start = 0
end = 0
# first, look for multiple "--", this means it's formatted differently
# from most:
if "--" in filename:
# the pattern seems to be that anything to left of the first "--"
# is the series name followed by issue
filename = re.sub("--.*", self.repl, filename)
elif "__" in filename:
# the pattern seems to be that anything to left of the first "__"
# is the series name followed by issue
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
# replace parenthetical phrases with spaces
filename = re.sub("\(.*?\)", self.repl, filename)
filename = re.sub("\[.*?\]", self.repl, filename)
# replace any name separators with spaces
filename = self.fixSpaces(filename)
# remove any "of NN" phrase with spaces (problem: this could break on
# some titles)
filename = re.sub("of [\d]+", self.repl, filename)
# print u"[{0}]".format(filename)
# we should now have a cleaned up filename version with all the words in
# the same positions as original filename
# make a list of each word and its position
word_list = list()
for m in re.finditer("\S+", filename):
word_list.append((m.group(0), m.start(), m.end()))
# remove the first word, since it can't be the issue number
if len(word_list) > 1:
word_list = word_list[1:]
else:
# only one word?? just bail.
return issue, start, end
# Now try to search for the likely issue number word in the list
# first look for a word with "#" followed by digits with optional suffix
# this is almost certainly the issue number
for w in reversed(word_list):
if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
found = True
break
# same as above but w/o a '#', and only look at the last word in the
# list
if not found:
w = word_list[-1]
if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
found = True
# now try to look for a # followed by any characters
if not found:
for w in reversed(word_list):
if re.match("#\S+", w[0]):
found = True
break
if found:
issue = w[0]
start = w[1]
end = w[2]
if issue[0] == '#':
issue = issue[1:]
return issue, start, end
def getSeriesName(self, filename, issue_start):
"""Use the issue number string index to split the filename string"""
if issue_start != 0:
filename = filename[:issue_start]
# in case there is no issue number, remove some obvious stuff
if "--" in filename:
# the pattern seems to be that anything to left of the first "--"
# is the series name followed by issue
filename = re.sub("--.*", self.repl, filename)
elif "__" in filename:
# the pattern seems to be that anything to left of the first "__"
# is the series name followed by issue
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
tmpstr = self.fixSpaces(filename, remove_dashes=False)
series = tmpstr
volume = ""
# save the last word
try:
last_word = series.split()[-1]
except:
last_word = ""
# remove any parenthetical phrases
series = re.sub("\(.*?\)", "", series)
# search for volume number
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
if match:
series = match.group(1)
volume = match.group(3)
# if a volume wasn't found, see if the last word is a year in parentheses
# since that's a common way to designate the volume
if volume == "":
# match either (YEAR), (YEAR-), or (YEAR-YEAR2)
match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
if match:
volume = match.group(2)
series = series.strip()
# if we don't have an issue number (issue_start==0), look
# for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might
# be removed to help search online
if issue_start == 0:
one_shot_words = ["tpb", "os", "one-shot", "ogn", "gn"]
try:
last_word = series.split()[-1]
if last_word.lower() in one_shot_words:
series = series.rsplit(' ', 1)[0]
except:
pass
return series, volume.strip()
def getYear(self, filename, issue_end):
filename = filename[issue_end:]
year = ""
# look for four digit number with "(" ")" or "--" around it
match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
if match:
year = match.group()
# remove non-digits
year = re.sub("[^0-9]", "", year)
return year
def getRemainder(self, filename, year, count, volume, issue_end):
"""Make a guess at where the the non-interesting stuff begins"""
remainder = ""
if "--" in filename:
remainder = filename.split("--", 1)[1]
elif "__" in filename:
remainder = filename.split("__", 1)[1]
elif issue_end != 0:
remainder = filename[issue_end:]
remainder = self.fixSpaces(remainder, remove_dashes=False)
if volume != "":
remainder = remainder.replace("Vol." + volume, "", 1)
if year != "":
remainder = remainder.replace(year, "", 1)
if count != "":
remainder = remainder.replace("of " + count, "", 1)
remainder = remainder.replace("()", "")
remainder = remainder.replace(
" ",
" ") # cleans some whitespace mess
return remainder.strip()
def parseFilename(self, filename):
# remove the path
filename = os.path.basename(filename)
# remove the extension
filename = os.path.splitext(filename)[0]
# url decode, just in case
filename = unquote(filename)
# sometimes archives get messed up names from too many decodes
# often url encodings will break and leave "_28" and "_29" in place
# of "(" and ")" see if there are a number of these, and replace them
if filename.count("_28") > 1 and filename.count("_29") > 1:
filename = filename.replace("_28", "(")
filename = filename.replace("_29", ")")
self.issue, issue_start, issue_end = self.getIssueNumber(filename)
self.series, self.volume = self.getSeriesName(filename, issue_start)
# provides proper value when the filename doesn't have a issue number
if issue_end == 0:
issue_end = len(self.series)
self.year = self.getYear(filename, issue_end)
self.issue_count = self.getIssueCount(filename, issue_end)
self.remainder = self.getRemainder(
filename,
self.year,
self.issue_count,
self.volume,
issue_end)
if self.issue != "":
# strip off leading zeros
self.issue = self.issue.lstrip("0")
if self.issue == "":
self.issue = "0"
if self.issue[0] == ".":
self.issue = "0" + self.issue

View File

@ -1,321 +0,0 @@
"""A class for internal metadata storage
The goal of this class is to handle ALL the data that might come from various
tagging schemes and databases, such as ComicVine or GCD. This makes conversion
possible, however lossy it might be
"""
# Copyright 2012-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.
import utils
class PageType:
"""
These page info classes are exactly the same as the CIX scheme, since
it's unique
"""
FrontCover = "FrontCover"
InnerCover = "InnerCover"
Roundup = "Roundup"
Story = "Story"
Advertisement = "Advertisement"
Editorial = "Editorial"
Letters = "Letters"
Preview = "Preview"
BackCover = "BackCover"
Other = "Other"
Deleted = "Deleted"
"""
class PageInfo:
Image = 0
Type = PageType.Story
DoublePage = False
ImageSize = 0
Key = ""
ImageWidth = 0
ImageHeight = 0
"""
class GenericMetadata:
def __init__(self):
self.isEmpty = True
self.tagOrigin = None
self.series = None
self.issue = None
self.title = None
self.publisher = None
self.month = None
self.year = None
self.day = None
self.issueCount = None
self.volume = None
self.genre = None
self.language = None # 2 letter iso code
self.comments = None # use same way as Summary in CIX
self.volumeCount = None
self.criticalRating = None
self.country = None
self.alternateSeries = None
self.alternateNumber = None
self.alternateCount = None
self.imprint = None
self.notes = None
self.webLink = None
self.format = None
self.manga = None
self.blackAndWhite = None
self.pageCount = None
self.maturityRating = None
self.storyArc = None
self.seriesGroup = None
self.scanInfo = None
self.characters = None
self.teams = None
self.locations = None
self.credits = list()
self.tags = list()
self.pages = list()
# Some CoMet-only items
self.price = None
self.isVersionOf = None
self.rights = None
self.identifier = None
self.lastMark = None
self.coverImage = None
def overlay(self, new_md):
"""Overlay a metadata object on this one
That is, when the new object has non-None values, over-write them
to this one.
"""
def assign(cur, new):
if new is not None:
if isinstance(new, str) and len(new) == 0:
setattr(self, cur, None)
else:
setattr(self, cur, new)
if not new_md.isEmpty:
self.isEmpty = False
assign('series', new_md.series)
assign("issue", new_md.issue)
assign("issueCount", new_md.issueCount)
assign("title", new_md.title)
assign("publisher", new_md.publisher)
assign("day", new_md.day)
assign("month", new_md.month)
assign("year", new_md.year)
assign("volume", new_md.volume)
assign("volumeCount", new_md.volumeCount)
assign("genre", new_md.genre)
assign("language", new_md.language)
assign("country", new_md.country)
assign("criticalRating", new_md.criticalRating)
assign("alternateSeries", new_md.alternateSeries)
assign("alternateNumber", new_md.alternateNumber)
assign("alternateCount", new_md.alternateCount)
assign("imprint", new_md.imprint)
assign("webLink", new_md.webLink)
assign("format", new_md.format)
assign("manga", new_md.manga)
assign("blackAndWhite", new_md.blackAndWhite)
assign("maturityRating", new_md.maturityRating)
assign("storyArc", new_md.storyArc)
assign("seriesGroup", new_md.seriesGroup)
assign("scanInfo", new_md.scanInfo)
assign("characters", new_md.characters)
assign("teams", new_md.teams)
assign("locations", new_md.locations)
assign("comments", new_md.comments)
assign("notes", new_md.notes)
assign("price", new_md.price)
assign("isVersionOf", new_md.isVersionOf)
assign("rights", new_md.rights)
assign("identifier", new_md.identifier)
assign("lastMark", new_md.lastMark)
self.overlayCredits(new_md.credits)
# TODO
# not sure if the tags and pages should broken down, or treated
# as whole lists....
# For now, go the easy route, where any overlay
# value wipes out the whole list
if len(new_md.tags) > 0:
assign("tags", new_md.tags)
if len(new_md.pages) > 0:
assign("pages", new_md.pages)
def overlayCredits(self, new_credits):
for c in new_credits:
if 'primary' in c and c['primary']:
primary = True
else:
primary = False
# Remove credit role if person is blank
if c['person'] == "":
for r in reversed(self.credits):
if r['role'].lower() == c['role'].lower():
self.credits.remove(r)
# otherwise, add it!
else:
self.addCredit(c['person'], c['role'], primary)
def setDefaultPageList(self, count):
# generate a default page list, with the first page marked as the cover
for i in range(count):
page_dict = dict()
page_dict['Image'] = str(i)
if i == 0:
page_dict['Type'] = PageType.FrontCover
self.pages.append(page_dict)
def getArchivePageIndex(self, pagenum):
# convert the displayed page number to the page index of the file in
# the archive
if pagenum < len(self.pages):
return int(self.pages[pagenum]['Image'])
else:
return 0
def getCoverPageIndexList(self):
# return a list of archive page indices of cover pages
coverlist = []
for p in self.pages:
if 'Type' in p and p['Type'] == PageType.FrontCover:
coverlist.append(int(p['Image']))
if len(coverlist) == 0:
coverlist.append(0)
return coverlist
def addCredit(self, person, role, primary=False):
credit = dict()
credit['person'] = person
credit['role'] = role
if primary:
credit['primary'] = primary
# look to see if it's not already there...
found = False
for c in self.credits:
if (c['person'].lower() == person.lower() and
c['role'].lower() == role.lower()):
# no need to add it. just adjust the "primary" flag as needed
c['primary'] = primary
found = True
break
if not found:
self.credits.append(credit)
def __str__(self):
vals = []
if self.isEmpty:
return "No metadata"
def add_string(tag, val):
if val is not None and u"{0}".format(val) != "":
vals.append((tag, val))
def add_attr_string(tag):
val = getattr(self, tag)
add_string(tag, getattr(self, tag))
add_attr_string("series")
add_attr_string("issue")
add_attr_string("issueCount")
add_attr_string("title")
add_attr_string("publisher")
add_attr_string("year")
add_attr_string("month")
add_attr_string("day")
add_attr_string("volume")
add_attr_string("volumeCount")
add_attr_string("genre")
add_attr_string("language")
add_attr_string("country")
add_attr_string("criticalRating")
add_attr_string("alternateSeries")
add_attr_string("alternateNumber")
add_attr_string("alternateCount")
add_attr_string("imprint")
add_attr_string("webLink")
add_attr_string("format")
add_attr_string("manga")
add_attr_string("price")
add_attr_string("isVersionOf")
add_attr_string("rights")
add_attr_string("identifier")
add_attr_string("lastMark")
if self.blackAndWhite:
add_attr_string("blackAndWhite")
add_attr_string("maturityRating")
add_attr_string("storyArc")
add_attr_string("seriesGroup")
add_attr_string("scanInfo")
add_attr_string("characters")
add_attr_string("teams")
add_attr_string("locations")
add_attr_string("comments")
add_attr_string("notes")
add_string("tags", utils.listToString(self.tags))
for c in self.credits:
primary = ""
if 'primary' in c and c['primary']:
primary = " [P]"
add_string("credit", c['role'] + ": " + c['person'] + primary)
# find the longest field name
flen = 0
for i in vals:
flen = max(flen, len(i[0]))
flen += 1
# format the data nicely
outstr = ""
fmt_str = u"{0: <" + str(flen) + "} {1}\n"
for i in vals:
outstr += fmt_str.format(i[0] + ":", i[1])
return outstr

View File

@ -1,133 +0,0 @@
# coding=utf-8
"""Support for mixed digit/string type Issue field
Class for handling the odd permutations of an 'issue number' that the
comics industry throws at us.
e.g.: "12", "12.1", "0", "-1", "5AU", "100-2"
"""
# Copyright 2012-2014 Anthony Beville
# 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.
#import utils
#import math
#import re
class IssueString:
def __init__(self, text):
# break up the issue number string into 2 parts: the numeric and suffix string.
# (assumes that the numeric portion is always first)
self.num = None
self.suffix = ""
if text is None:
return
if isinstance(text, int):
text = str(text)
if len(text) == 0:
return
text = unicode(text)
# skip the minus sign if it's first
if text[0] == '-':
start = 1
else:
start = 0
# if it's still not numeric at start skip it
if text[start].isdigit() or text[start] == ".":
# walk through the string, look for split point (the first
# non-numeric)
decimal_count = 0
for idx in range(start, len(text)):
if text[idx] not in "0123456789.":
break
# special case: also split on second "."
if text[idx] == ".":
decimal_count += 1
if decimal_count > 1:
break
else:
idx = len(text)
# move trailing numeric decimal to suffix
# (only if there is other junk after )
if text[idx - 1] == "." and len(text) != idx:
idx = idx - 1
# if there is no numeric after the minus, make the minus part of
# the suffix
if idx == 1 and start == 1:
idx = 0
part1 = text[0:idx]
part2 = text[idx:len(text)]
if part1 != "":
self.num = float(part1)
self.suffix = part2
else:
self.suffix = text
# print "num: {0} suf: {1}".format(self.num, self.suffix)
def asString(self, pad=0):
# return the float, left side zero-padded, with suffix attached
if self.num is None:
return self.suffix
negative = self.num < 0
num_f = abs(self.num)
num_int = int(num_f)
num_s = str(num_int)
if float(num_int) != num_f:
num_s = str(num_f)
num_s += self.suffix
# create padding
padding = ""
l = len(str(num_int))
if l < pad:
padding = "0" * (pad - l)
num_s = padding + num_s
if negative:
num_s = "-" + num_s
return num_s
def asFloat(self):
# return the float, with no suffix
if self.suffix == u"½":
if self.num is not None:
return self.num + .5
else:
return .5
return self.num
def asInt(self):
# return the int version of the float
if self.num is None:
return None
return int(self.num)

View File

@ -1,592 +0,0 @@
# coding=utf-8
"""Some generic utilities"""
# Copyright 2012-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.
import sys
import os
import re
import platform
import locale
import codecs
class UtilsVars:
already_fixed_encoding = False
def get_actual_preferred_encoding():
preferred_encoding = locale.getpreferredencoding()
if platform.system() == "Darwin":
preferred_encoding = "utf-8"
return preferred_encoding
def fix_output_encoding():
if not UtilsVars.already_fixed_encoding:
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
# try to make stdout/stderr encodings happy for unicode printing
preferred_encoding = get_actual_preferred_encoding()
sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout)
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
UtilsVars.already_fixed_encoding = True
def get_recursive_filelist(pathlist):
"""Get a recursive list of of all files under all path items in the list"""
filename_encoding = sys.getfilesystemencoding()
filelist = []
for p in pathlist:
# if path is a folder, walk it recursively, and all files underneath
if isinstance(p, str):
# make sure string is unicode
p = p.decode(filename_encoding) # , 'replace')
elif not isinstance(p, unicode):
# it's probably a QString
p = unicode(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):
# it's probably a QString
f = unicode(f)
filelist.append(os.path.join(root, f))
else:
filelist.append(p)
return filelist
def listToString(l):
string = ""
if l is not None:
for item in l:
if len(string) > 0:
string += ", "
string += item
return string
def addtopath(dirname):
if dirname is not None and dirname != "":
# verify that path doesn't already contain the given dirname
tmpdirname = re.escape(dirname)
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format(
dir=tmpdirname,
sep=os.pathsep)
match = re.search(pattern, os.environ['PATH'])
if not match:
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
def which(program):
"""Returns path of the executable, if it exists"""
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
def removearticles(text):
text = text.lower()
articles = ['and', 'the', 'a', '&', 'issue']
newText = ''
for word in text.split(' '):
if word not in articles:
newText += word + ' '
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 unique_file(file_name):
counter = 1
# returns ('/path/file', '.ext')
file_name_parts = os.path.splitext(file_name)
while True:
if not os.path.lexists(file_name):
return file_name
file_name = file_name_parts[
0] + ' (' + str(counter) + ')' + file_name_parts[1]
counter += 1
# -o- coding: utf-8 -o-
# ISO639 python dict
# official list in http://www.loc.gov/standards/iso639-2/php/code_list.php
lang_dict = {
'ab': 'Abkhaz',
'aa': 'Afar',
'af': 'Afrikaans',
'ak': 'Akan',
'sq': 'Albanian',
'am': 'Amharic',
'ar': 'Arabic',
'an': 'Aragonese',
'hy': 'Armenian',
'as': 'Assamese',
'av': 'Avaric',
'ae': 'Avestan',
'ay': 'Aymara',
'az': 'Azerbaijani',
'bm': 'Bambara',
'ba': 'Bashkir',
'eu': 'Basque',
'be': 'Belarusian',
'bn': 'Bengali',
'bh': 'Bihari',
'bi': 'Bislama',
'bs': 'Bosnian',
'br': 'Breton',
'bg': 'Bulgarian',
'my': 'Burmese',
'ca': 'Catalan; Valencian',
'ch': 'Chamorro',
'ce': 'Chechen',
'ny': 'Chichewa; Chewa; Nyanja',
'zh': 'Chinese',
'cv': 'Chuvash',
'kw': 'Cornish',
'co': 'Corsican',
'cr': 'Cree',
'hr': 'Croatian',
'cs': 'Czech',
'da': 'Danish',
'dv': 'Divehi; Maldivian;',
'nl': 'Dutch',
'dz': 'Dzongkha',
'en': 'English',
'eo': 'Esperanto',
'et': 'Estonian',
'ee': 'Ewe',
'fo': 'Faroese',
'fj': 'Fijian',
'fi': 'Finnish',
'fr': 'French',
'ff': 'Fula',
'gl': 'Galician',
'ka': 'Georgian',
'de': 'German',
'el': 'Greek, Modern',
'gn': 'Guaraní',
'gu': 'Gujarati',
'ht': 'Haitian',
'ha': 'Hausa',
'he': 'Hebrew (modern)',
'hz': 'Herero',
'hi': 'Hindi',
'ho': 'Hiri Motu',
'hu': 'Hungarian',
'ia': 'Interlingua',
'id': 'Indonesian',
'ie': 'Interlingue',
'ga': 'Irish',
'ig': 'Igbo',
'ik': 'Inupiaq',
'io': 'Ido',
'is': 'Icelandic',
'it': 'Italian',
'iu': 'Inuktitut',
'ja': 'Japanese',
'jv': 'Javanese',
'kl': 'Kalaallisut',
'kn': 'Kannada',
'kr': 'Kanuri',
'ks': 'Kashmiri',
'kk': 'Kazakh',
'km': 'Khmer',
'ki': 'Kikuyu, Gikuyu',
'rw': 'Kinyarwanda',
'ky': 'Kirghiz, Kyrgyz',
'kv': 'Komi',
'kg': 'Kongo',
'ko': 'Korean',
'ku': 'Kurdish',
'kj': 'Kwanyama, Kuanyama',
'la': 'Latin',
'lb': 'Luxembourgish',
'lg': 'Luganda',
'li': 'Limburgish',
'ln': 'Lingala',
'lo': 'Lao',
'lt': 'Lithuanian',
'lu': 'Luba-Katanga',
'lv': 'Latvian',
'gv': 'Manx',
'mk': 'Macedonian',
'mg': 'Malagasy',
'ms': 'Malay',
'ml': 'Malayalam',
'mt': 'Maltese',
'mi': 'Māori',
'mr': 'Marathi (Marāṭhī)',
'mh': 'Marshallese',
'mn': 'Mongolian',
'na': 'Nauru',
'nv': 'Navajo, Navaho',
'nb': 'Norwegian Bokmål',
'nd': 'North Ndebele',
'ne': 'Nepali',
'ng': 'Ndonga',
'nn': 'Norwegian Nynorsk',
'no': 'Norwegian',
'ii': 'Nuosu',
'nr': 'South Ndebele',
'oc': 'Occitan',
'oj': 'Ojibwe, Ojibwa',
'cu': 'Old Church Slavonic',
'om': 'Oromo',
'or': 'Oriya',
'os': 'Ossetian, Ossetic',
'pa': 'Panjabi, Punjabi',
'pi': 'Pāli',
'fa': 'Persian',
'pl': 'Polish',
'ps': 'Pashto, Pushto',
'pt': 'Portuguese',
'qu': 'Quechua',
'rm': 'Romansh',
'rn': 'Kirundi',
'ro': 'Romanian, Moldavan',
'ru': 'Russian',
'sa': 'Sanskrit (Saṁskṛta)',
'sc': 'Sardinian',
'sd': 'Sindhi',
'se': 'Northern Sami',
'sm': 'Samoan',
'sg': 'Sango',
'sr': 'Serbian',
'gd': 'Scottish Gaelic',
'sn': 'Shona',
'si': 'Sinhala, Sinhalese',
'sk': 'Slovak',
'sl': 'Slovene',
'so': 'Somali',
'st': 'Southern Sotho',
'es': 'Spanish; Castilian',
'su': 'Sundanese',
'sw': 'Swahili',
'ss': 'Swati',
'sv': 'Swedish',
'ta': 'Tamil',
'te': 'Telugu',
'tg': 'Tajik',
'th': 'Thai',
'ti': 'Tigrinya',
'bo': 'Tibetan',
'tk': 'Turkmen',
'tl': 'Tagalog',
'tn': 'Tswana',
'to': 'Tonga',
'tr': 'Turkish',
'ts': 'Tsonga',
'tt': 'Tatar',
'tw': 'Twi',
'ty': 'Tahitian',
'ug': 'Uighur, Uyghur',
'uk': 'Ukrainian',
'ur': 'Urdu',
'uz': 'Uzbek',
've': 'Venda',
'vi': 'Vietnamese',
'vo': 'Volapük',
'wa': 'Walloon',
'cy': 'Welsh',
'wo': 'Wolof',
'fy': 'Western Frisian',
'xh': 'Xhosa',
'yi': 'Yiddish',
'yo': 'Yoruba',
'za': 'Zhuang, Chuang',
'zu': 'Zulu',
}
countries = [
('AF', 'Afghanistan'),
('AL', 'Albania'),
('DZ', 'Algeria'),
('AS', 'American Samoa'),
('AD', 'Andorra'),
('AO', 'Angola'),
('AI', 'Anguilla'),
('AQ', 'Antarctica'),
('AG', 'Antigua And Barbuda'),
('AR', 'Argentina'),
('AM', 'Armenia'),
('AW', 'Aruba'),
('AU', 'Australia'),
('AT', 'Austria'),
('AZ', 'Azerbaijan'),
('BS', 'Bahamas'),
('BH', 'Bahrain'),
('BD', 'Bangladesh'),
('BB', 'Barbados'),
('BY', 'Belarus'),
('BE', 'Belgium'),
('BZ', 'Belize'),
('BJ', 'Benin'),
('BM', 'Bermuda'),
('BT', 'Bhutan'),
('BO', 'Bolivia'),
('BA', 'Bosnia And Herzegowina'),
('BW', 'Botswana'),
('BV', 'Bouvet Island'),
('BR', 'Brazil'),
('BN', 'Brunei Darussalam'),
('BG', 'Bulgaria'),
('BF', 'Burkina Faso'),
('BI', 'Burundi'),
('KH', 'Cambodia'),
('CM', 'Cameroon'),
('CA', 'Canada'),
('CV', 'Cape Verde'),
('KY', 'Cayman Islands'),
('CF', 'Central African Rep'),
('TD', 'Chad'),
('CL', 'Chile'),
('CN', 'China'),
('CX', 'Christmas Island'),
('CC', 'Cocos Islands'),
('CO', 'Colombia'),
('KM', 'Comoros'),
('CG', 'Congo'),
('CK', 'Cook Islands'),
('CR', 'Costa Rica'),
('CI', 'Cote D`ivoire'),
('HR', 'Croatia'),
('CU', 'Cuba'),
('CY', 'Cyprus'),
('CZ', 'Czech Republic'),
('DK', 'Denmark'),
('DJ', 'Djibouti'),
('DM', 'Dominica'),
('DO', 'Dominican Republic'),
('TP', 'East Timor'),
('EC', 'Ecuador'),
('EG', 'Egypt'),
('SV', 'El Salvador'),
('GQ', 'Equatorial Guinea'),
('ER', 'Eritrea'),
('EE', 'Estonia'),
('ET', 'Ethiopia'),
('FK', 'Falkland Islands (Malvinas)'),
('FO', 'Faroe Islands'),
('FJ', 'Fiji'),
('FI', 'Finland'),
('FR', 'France'),
('GF', 'French Guiana'),
('PF', 'French Polynesia'),
('TF', 'French S. Territories'),
('GA', 'Gabon'),
('GM', 'Gambia'),
('GE', 'Georgia'),
('DE', 'Germany'),
('GH', 'Ghana'),
('GI', 'Gibraltar'),
('GR', 'Greece'),
('GL', 'Greenland'),
('GD', 'Grenada'),
('GP', 'Guadeloupe'),
('GU', 'Guam'),
('GT', 'Guatemala'),
('GN', 'Guinea'),
('GW', 'Guinea-bissau'),
('GY', 'Guyana'),
('HT', 'Haiti'),
('HN', 'Honduras'),
('HK', 'Hong Kong'),
('HU', 'Hungary'),
('IS', 'Iceland'),
('IN', 'India'),
('ID', 'Indonesia'),
('IR', 'Iran'),
('IQ', 'Iraq'),
('IE', 'Ireland'),
('IL', 'Israel'),
('IT', 'Italy'),
('JM', 'Jamaica'),
('JP', 'Japan'),
('JO', 'Jordan'),
('KZ', 'Kazakhstan'),
('KE', 'Kenya'),
('KI', 'Kiribati'),
('KP', 'Korea (North)'),
('KR', 'Korea (South)'),
('KW', 'Kuwait'),
('KG', 'Kyrgyzstan'),
('LA', 'Laos'),
('LV', 'Latvia'),
('LB', 'Lebanon'),
('LS', 'Lesotho'),
('LR', 'Liberia'),
('LY', 'Libya'),
('LI', 'Liechtenstein'),
('LT', 'Lithuania'),
('LU', 'Luxembourg'),
('MO', 'Macau'),
('MK', 'Macedonia'),
('MG', 'Madagascar'),
('MW', 'Malawi'),
('MY', 'Malaysia'),
('MV', 'Maldives'),
('ML', 'Mali'),
('MT', 'Malta'),
('MH', 'Marshall Islands'),
('MQ', 'Martinique'),
('MR', 'Mauritania'),
('MU', 'Mauritius'),
('YT', 'Mayotte'),
('MX', 'Mexico'),
('FM', 'Micronesia'),
('MD', 'Moldova'),
('MC', 'Monaco'),
('MN', 'Mongolia'),
('MS', 'Montserrat'),
('MA', 'Morocco'),
('MZ', 'Mozambique'),
('MM', 'Myanmar'),
('NA', 'Namibia'),
('NR', 'Nauru'),
('NP', 'Nepal'),
('NL', 'Netherlands'),
('AN', 'Netherlands Antilles'),
('NC', 'New Caledonia'),
('NZ', 'New Zealand'),
('NI', 'Nicaragua'),
('NE', 'Niger'),
('NG', 'Nigeria'),
('NU', 'Niue'),
('NF', 'Norfolk Island'),
('MP', 'Northern Mariana Islands'),
('NO', 'Norway'),
('OM', 'Oman'),
('PK', 'Pakistan'),
('PW', 'Palau'),
('PA', 'Panama'),
('PG', 'Papua New Guinea'),
('PY', 'Paraguay'),
('PE', 'Peru'),
('PH', 'Philippines'),
('PN', 'Pitcairn'),
('PL', 'Poland'),
('PT', 'Portugal'),
('PR', 'Puerto Rico'),
('QA', 'Qatar'),
('RE', 'Reunion'),
('RO', 'Romania'),
('RU', 'Russian Federation'),
('RW', 'Rwanda'),
('KN', 'Saint Kitts And Nevis'),
('LC', 'Saint Lucia'),
('VC', 'St Vincent/Grenadines'),
('WS', 'Samoa'),
('SM', 'San Marino'),
('ST', 'Sao Tome'),
('SA', 'Saudi Arabia'),
('SN', 'Senegal'),
('SC', 'Seychelles'),
('SL', 'Sierra Leone'),
('SG', 'Singapore'),
('SK', 'Slovakia'),
('SI', 'Slovenia'),
('SB', 'Solomon Islands'),
('SO', 'Somalia'),
('ZA', 'South Africa'),
('ES', 'Spain'),
('LK', 'Sri Lanka'),
('SH', 'St. Helena'),
('PM', 'St.Pierre'),
('SD', 'Sudan'),
('SR', 'Suriname'),
('SZ', 'Swaziland'),
('SE', 'Sweden'),
('CH', 'Switzerland'),
('SY', 'Syrian Arab Republic'),
('TW', 'Taiwan'),
('TJ', 'Tajikistan'),
('TZ', 'Tanzania'),
('TH', 'Thailand'),
('TG', 'Togo'),
('TK', 'Tokelau'),
('TO', 'Tonga'),
('TT', 'Trinidad And Tobago'),
('TN', 'Tunisia'),
('TR', 'Turkey'),
('TM', 'Turkmenistan'),
('TV', 'Tuvalu'),
('UG', 'Uganda'),
('UA', 'Ukraine'),
('AE', 'United Arab Emirates'),
('UK', 'United Kingdom'),
('US', 'United States'),
('UY', 'Uruguay'),
('UZ', 'Uzbekistan'),
('VU', 'Vanuatu'),
('VA', 'Vatican City State'),
('VE', 'Venezuela'),
('VN', 'Viet Nam'),
('VG', 'Virgin Islands (British)'),
('VI', 'Virgin Islands (U.S.)'),
('EH', 'Western Sahara'),
('YE', 'Yemen'),
('YU', 'Yugoslavia'),
('ZR', 'Zaire'),
('ZM', 'Zambia'),
('ZW', 'Zimbabwe')
]
def getLanguageDict():
return lang_dict
def getLanguageFromISO(iso):
if iso is None:
return None
else:
return lang_dict[iso]

View File

@ -1,20 +1,5 @@
#!/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__':
if __name__ == "__main__":
ctmain()

View File

@ -0,0 +1,17 @@
{
"build_systems":
[
{
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"name": "Anaconda Python Builder",
"selector": "source.python",
"shell_cmd": "\"python3\" -u \"$file\""
}
],
"folders":
[
{
"path": "."
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
from ._version import version as __version__

View File

@ -15,54 +15,43 @@
# limitations under the License.
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 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
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class AutoTagMatchWindow(QtGui.QDialog):
class AutoTagMatchWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, match_set_list, style, fetch_func):
super(AutoTagMatchWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("matchselectionwindow.ui"), self)
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
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)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
self.buttonBox.addButton(
self.skipButton, QtGui.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(
"Accept and Write Tags")
self.skipButton = QtWidgets.QPushButton(self.tr("Skip to Next"))
self.buttonBox.addButton(self.skipButton, QtWidgets.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept and Write Tags")
self.match_set_list = match_set_list
self.style = style
@ -78,13 +67,11 @@ class AutoTagMatchWindow(QtGui.QDialog):
def updateData(self):
self.current_match_set = self.match_set_list[
self.current_match_set_idx]
self.current_match_set = self.match_set_list[self.current_match_set_idx]
if self.current_match_set_idx + 1 == len(self.match_set_list):
self.buttonBox.button(
QtGui.QDialogButtonBox.Cancel).setDisabled(True)
# self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setDisabled(True)
# self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept")
self.skipButton.setText(self.tr("Skip"))
self.setCoverImage()
@ -94,10 +81,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(
self.current_match_set_idx + 1,
len(self.match_set_list),
os.path.split(path)[1])
"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])
)
def populateTable(self):
@ -111,39 +95,39 @@ class AutoTagMatchWindow(QtGui.QDialog):
for match in self.current_match_set.matches:
self.twList.insertRow(row)
item_text = match['series']
item = QtGui.QTableWidgetItem(item_text)
item_text = match["series"]
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'])
if match["publisher"] is not None:
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"????"
if match['month'] is not None:
month_str = u"-{0:02d}".format(int(match['month']))
if match['year'] is not None:
year_str = u"{0}".format(match['year'])
month_str = ""
year_str = "????"
if match["month"] is not None:
month_str = "-{0:02d}".format(int(match["month"]))
if match["year"] is not None:
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)
item_text = match['issue_title']
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)
@ -167,11 +151,11 @@ class AutoTagMatchWindow(QtGui.QDialog):
if prev is not None and prev.row() == curr.row():
return
self.altCoverWidget.setIssueID(self.currentMatch()['issue_id'])
if self.currentMatch()['description'] is None:
self.altCoverWidget.setIssueID(self.currentMatch()["issue_id"])
if self.currentMatch()["description"] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(self.currentMatch()['description'])
self.teDescription.setText(self.currentMatch()["description"])
def setCoverImage(self):
ca = self.current_match_set.ca
@ -179,8 +163,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(
QtCore.Qt.UserRole).toPyObject()[0]
match = self.twList.item(row, 0).data(QtCore.Qt.UserRole)[0]
return match
def accept(self):
@ -190,7 +173,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 +182,23 @@ 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 +212,15 @@ 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(
"Could not connect to Comic Vine to get issue details!"))
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to get issue details!"))
return
QtGui.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
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(
"Saving the tags to the archive seemed to fail!"))
QtWidgets.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))

View File

@ -14,42 +14,33 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#import utils
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class AutoTagProgressWindow(QtGui.QDialog):
class AutoTagProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('autotagprogresswindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("autotagprogresswindow.ui"), self)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.DataMode, False)
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)
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
gridlayout.addWidget(self.testCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.isdone = False
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
reduceWidgetFontSize(self.textEdit)
@ -65,5 +56,5 @@ class AutoTagProgressWindow(QtGui.QDialog):
QtCore.QCoreApplication.processEvents()
def reject(self):
QtGui.QDialog.reject(self)
QtWidgets.QDialog.reject(self)
self.isdone = True

View File

@ -14,39 +14,30 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
#from settingswindow import SettingsWindow
#from filerenamer import FileRenamer
#import utils
from .settings import ComicTaggerSettings
class AutoTagStartWindow(QtGui.QDialog):
class AutoTagStartWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super(AutoTagStartWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('autotagstartwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("autotagstartwindow.ui"), self)
self.label.setText(msg)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Unchecked)
self.cbxDontUseYear.setCheckState(QtCore.Qt.Unchecked)
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Unchecked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
QtCore.Qt.Unchecked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.Unchecked)
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Unchecked)
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.Unchecked)
self.leNameLengthMatchTolerance.setText(
str(self.settings.id_length_delta_thresh))
self.cbxAutoImprint.setCheckState(QtCore.Qt.Unchecked)
self.leNameLengthMatchTolerance.setText(str(self.settings.id_length_delta_thresh))
self.leSearchString.setEnabled(False)
if self.settings.save_on_low_confidence:
@ -56,37 +47,34 @@ class AutoTagStartWindow(QtGui.QDialog):
if self.settings.assume_1_if_no_issue_num:
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Checked)
if self.settings.ignore_leading_numbers_in_filename:
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
QtCore.Qt.Checked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.Checked)
if self.settings.remove_archive_after_successful_match:
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Checked)
if self.settings.wait_and_retry_on_rate_limit:
self.cbxWaitForRateLimit.setCheckState(QtCore.Qt.Checked)
if self.settings.auto_imprint:
self.cbxAutoImprint.setCheckState(QtCore.Qt.Checked)
nlmtTip = (
""" <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
nlmtTip = """ <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
search matches that are too long compared to your series name search. The higher
it is, the more likely to have a good match, but each search will take longer and
use more bandwidth. Too low, and only the very closest lexical matches will be
explored.</html>""")
explored.</html>"""
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
ssTip = (
"""<html>
ssTip = """<html>
The <b>series search string</b> specifies the search string to be used for all selected archives.
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
should be from the same series.
</html>"""
)
self.leSearchString.setToolTip(ssTip)
self.cbxSpecifySearchString.setToolTip(ssTip)
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthMatchTolerance.setValidator(validator)
self.cbxSpecifySearchString.stateChanged.connect(
self.searchStringToggle)
self.cbxSpecifySearchString.stateChanged.connect(self.searchStringToggle)
self.autoSaveOnLow = False
self.dontUseYear = False
@ -102,15 +90,14 @@ 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()
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
self.nameLengthMatchTolerance = int(
self.leNameLengthMatchTolerance.text())
self.nameLengthMatchTolerance = int(self.leNameLengthMatchTolerance.text())
self.waitAndRetryOnRateLimit = self.cbxWaitForRateLimit.isChecked()
# persist some settings
@ -122,6 +109,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

@ -14,13 +14,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
#import utils
class CBLTransformer:
def __init__(self, metadata, settings):
self.metadata = metadata
self.settings = settings
@ -33,7 +29,7 @@ class CBLTransformer:
def add_string_list_to_tags(str_list):
if str_list is not None and str_list != "":
items = [s.strip() for s in str_list.split(',')]
items = [s.strip() for s in str_list.split(",")]
for item in items:
append_to_tags_if_unique(item)
@ -44,25 +40,25 @@ class CBLTransformer:
lone_credit = None
count = 0
for c in self.metadata.credits:
if c['role'].lower() in role_list:
if c["role"].lower() in role_list:
count += 1
lone_credit = c
if count > 1:
lone_credit = None
break
if lone_credit is not None:
lone_credit['primary'] = True
lone_credit["primary"] = True
return lone_credit, count
# need to loop three times, once for 'writer', 'artist', and then
# 'penciler' if no artist
setLonePrimary(['writer'])
c, count = setLonePrimary(['artist'])
setLonePrimary(["writer"])
c, count = setLonePrimary(["artist"])
if c is None and count == 0:
c, count = setLonePrimary(['penciler', 'penciller'])
c, count = setLonePrimary(["penciler", "penciller"])
if c is not None:
c['primary'] = False
self.metadata.addCredit(c['person'], 'Artist', True)
c["primary"] = False
self.metadata.addCredit(c["person"], "Artist", True)
if self.settings.copy_characters_to_tags:
add_string_list_to_tags(self.metadata.characters)

View File

@ -16,39 +16,31 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import os
from pprint import pprint
import json
#import signal
#import traceback
#import time
#import platform
#import locale
#import codecs
import os
import sys
from pprint import pprint
from . import utils
from .cbltransformer import CBLTransformer
from .comicarchive import ComicArchive, MetaDataStyle
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .filerenamer import FileRenamer
from .genericmetadata import GenericMetadata
from .issueidentifier import IssueIdentifier
from .options import Options
from .settings import ComicTaggerSettings
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
class MultipleMatch():
class MultipleMatch:
def __init__(self, filename, match_list):
self.filename = filename
self.matches = match_list
class OnlineMatchResults():
class OnlineMatchResults:
def __init__(self):
self.goodMatches = []
self.noMatches = []
@ -57,8 +49,6 @@ class OnlineMatchResults():
self.writeFailures = []
self.fetchDataFailures = []
#-----------------------------
def actual_issue_data_fetch(match, settings, opts):
@ -66,10 +56,9 @@ def actual_issue_data_fetch(match, settings, opts):
try:
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comicVine.fetchIssueData(
match['volume_id'], match['issue_number'], settings)
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,55 +72,49 @@ 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'])
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(
counter,
m['series'],
m['issue_number'],
m['publisher'],
m['month'],
m['year'],
m['issue_title']))
" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
counter, m["series"], m["issue_number"], m["publisher"], m["month"], m["year"], m["issue_title"]
)
)
if opts.interactive:
while True:
i = raw_input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(
1, len(match_set.matches) + 1)) or i == 's':
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
if i != 's':
if i != "s":
i = int(i) - 1
# save the data!
# we know at this point, that the file is all good to go
ca = ComicArchive(
match_set.filename,
settings.rar_exe_path,
ComicTaggerSettings.getGraphic('nocover.png'))
md = create_local_metadata(
opts, ca, ca.hasMetadata(opts.data_style))
cv_md = actual_issue_data_fetch(
match_set.matches[int(i)], settings, opts)
ca = ComicArchive(match_set.filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
md = create_local_metadata(opts, ca, ca.hasMetadata(opts.data_style))
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings, opts)
md.overlay(cv_md)
if settings.auto_imprint:
md.fixPublisher()
actual_metadata_save(ca, opts, md)
@ -163,11 +146,9 @@ def post_process_matches(match_results, opts, settings):
return
if len(match_results.multipleMatches) > 0:
print(
"\nArchives with multiple high-confidence matches:\n------------------")
print("\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multipleMatches:
display_match_set_for_choice(
"Multiple high-confidence matches", match_set, opts, settings)
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings)
if len(match_results.lowConfidenceMatches) > 0:
print("\nArchives with low-confidence matches:\n------------------")
@ -182,14 +163,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()
@ -219,25 +200,22 @@ def process_file_cli(filename, opts, settings, match_results):
batch_mode = len(opts.file_list) > 1
ca = ComicArchive(
filename,
settings.rar_exe_path,
ComicTaggerSettings.getGraphic('nocover.png'))
settings.auto_imprint = opts.auto_imprint
ca = ComicArchive(filename, settings.rar_exe_path, 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"
if not ca.isWritable() and (opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
print("This archive is not writable for that tag type", file=sys.stderr)
return
has = [False, False, False]
@ -256,7 +234,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 "
@ -268,8 +246,7 @@ def process_file_cli(filename, opts, settings, match_results):
brief += "({0: >3} pages)".format(page_count)
brief += " tags:[ "
if not (has[MetaDataStyle.CBI] or has[
MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
brief += "none "
else:
if has[MetaDataStyle.CBI]:
@ -280,24 +257,20 @@ 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(
ca.readRawCIX(),
errors='ignore')))
print("{0}".format(str(ca.readRawCIX(), 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 +278,36 @@ 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(
filename,
dst_style_name))
print("{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name))
return
src_style_name = MetaDataStyle.name[opts.copy_source]
@ -353,26 +319,22 @@ 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 == "":
@ -386,16 +348,14 @@ def process_file_cli(filename, opts, settings, match_results):
try:
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comicVine.fetchIssueDataByIssueID(
opts.issue_id, settings)
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 +365,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 +404,19 @@ 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"
match_results.lowConfidenceMatches.append(
MultipleMatch(filename, matches))
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"
match_results.multipleMatches.append(
MultipleMatch(filename, matches))
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"
match_results.lowConfidenceMatches.append(
MultipleMatch(filename, matches))
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
@ -473,6 +430,9 @@ def process_file_cli(filename, opts, settings, match_results):
md.overlay(cv_md)
if settings.auto_imprint:
md.fixPublisher()
# ok, done building our metadata. time to save
if not actual_metadata_save(ca, opts, md):
match_results.writeFailures.append(filename)
@ -483,7 +443,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 +453,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
@ -507,40 +467,54 @@ def process_file_cli(filename, opts, settings, match_results):
renamer.setTemplate(settings.rename_template)
renamer.setIssueZeroPadding(settings.rename_issue_number_padding)
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
new_name = renamer.determineName(filename, ext=new_ext)
if new_name == os.path.basename(filename):
print >> sys.stderr, msg_hdr + "Filename is already good!"
try:
new_name = renamer.determineName(filename, ext=new_ext)
except Exception as e:
print(
msg_hdr + "Invalid format string!\nYour rename template is invalid!\n\n"
"{}\n\nPlease consult the template help in the settings "
"and the documentation on the format at "
"https://docs.python.org/3/library/string.html#format-string-syntax",
file=sys.stderr,
)
return
folder = os.path.dirname(os.path.abspath(filename))
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
folder = settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
if os.path.join(folder, new_name) == os.path.abspath(filename):
print(msg_hdr + "Filename is already good!", file=sys.stderr)
return
suffix = ""
if not opts.dryrun:
# rename the file
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(filename, new_abs_path)
else:
suffix = " (dry-run, no change)"
print(
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 +528,7 @@ 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
@ -564,21 +537,18 @@ def process_file_cli(filename, opts, settings, match_results):
if os.path.lexists(new_file):
os.remove(new_file)
else:
msg = msg_hdr + \
u"Dry-run: Would try to create {0}".format(
os.path.split(new_file)[1])
msg = msg_hdr + "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(
os.path.split(new_file)[1])
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

@ -14,34 +14,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sqlite3 as lite
import os
import datetime
#import sys
#from pprint import pprint
import os
import sqlite3 as lite
import ctversion
from settings import ComicTaggerSettings
import utils
from . import _version, utils
from .settings import ComicTaggerSettings
class ComicVineCacher:
def __init__(self):
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join(self.settings_folder, "cv_cache.db")
self.version_file = os.path.join(
self.settings_folder, "cache_version.txt")
self.version_file = os.path.join(self.settings_folder, "cache_version.txt")
# verify that cache is from same version as this one
data = ""
try:
with open(self.version_file, 'rb') as f:
data = f.read()
with open(self.version_file, "rb") as f:
data = f.read().decode("utf-8")
f.close()
except:
pass
if data != ctversion.version:
if data != _version.version:
self.clearCache()
if not os.path.exists(self.db_file):
@ -60,11 +55,11 @@ class ComicVineCacher:
def create_cache_db(self):
# create the version file
with open(self.version_file, 'w') as f:
f.write(ctversion.version)
with open(self.version_file, "w") as f:
f.write(_version.version)
# this will wipe out any existing version
open(self.db_file, 'w').close()
open(self.db_file, "w").close()
con = lite.connect(self.db_file)
@ -74,119 +69,121 @@ class ComicVineCacher:
cur = con.cursor()
# name,id,start_year,publisher,image,description,count_of_issues
cur.execute(
"CREATE TABLE VolumeSearchCache(" +
"search_term TEXT," +
"id INT," +
"name TEXT," +
"start_year INT," +
"publisher TEXT," +
"count_of_issues INT," +
"image_url TEXT," +
"description TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime'))) ")
"CREATE TABLE VolumeSearchCache("
+ "search_term TEXT,"
+ "id INT,"
+ "name TEXT,"
+ "start_year INT,"
+ "publisher TEXT,"
+ "count_of_issues INT,"
+ "image_url TEXT,"
+ "description TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime'))) "
)
cur.execute(
"CREATE TABLE Volumes(" +
"id INT," +
"name TEXT," +
"publisher TEXT," +
"count_of_issues INT," +
"start_year INT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id))")
"CREATE TABLE Volumes("
+ "id INT,"
+ "name TEXT,"
+ "publisher TEXT,"
+ "count_of_issues INT,"
+ "start_year INT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (id))"
)
cur.execute(
"CREATE TABLE AltCovers(" +
"issue_id INT," +
"url_list TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (issue_id))")
"CREATE TABLE AltCovers("
+ "issue_id INT,"
+ "url_list TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (issue_id))"
)
cur.execute(
"CREATE TABLE Issues(" +
"id INT," +
"volume_id INT," +
"name TEXT," +
"issue_number TEXT," +
"super_url TEXT," +
"thumb_url TEXT," +
"cover_date TEXT," +
"site_detail_url TEXT," +
"description TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id))")
"CREATE TABLE Issues("
+ "id INT,"
+ "volume_id INT,"
+ "name TEXT,"
+ "issue_number TEXT,"
+ "super_url TEXT,"
+ "thumb_url TEXT,"
+ "cover_date TEXT,"
+ "site_detail_url TEXT,"
+ "description TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (id))"
)
def add_search_results(self, search_term, cv_search_results):
con = lite.connect(self.db_file)
with con:
con.text_factory = unicode
con.text_factory = str
cur = con.cursor()
# remove all previous entries with this search term
cur.execute(
"DELETE FROM VolumeSearchCache WHERE search_term = ?", [
search_term.lower()])
cur.execute("DELETE FROM VolumeSearchCache WHERE search_term = ?", [search_term.lower()])
# now add in new results
for record in cv_search_results:
timestamp = datetime.datetime.now()
if record['publisher'] is None:
if record["publisher"] is None:
pub_name = ""
else:
pub_name = record['publisher']['name']
pub_name = record["publisher"]["name"]
if record['image'] is None:
if record["image"] is None:
url = ""
else:
url = record['image']['super_url']
url = record["image"]["super_url"]
cur.execute(
"INSERT INTO VolumeSearchCache " +
"(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) " +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
(search_term.lower(),
record['id'],
record['name'],
record['start_year'],
"INSERT INTO VolumeSearchCache "
+ "(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) "
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
(
search_term.lower(),
record["id"],
record["name"],
record["start_year"],
pub_name,
record['count_of_issues'],
record["count_of_issues"],
url,
record['description']))
record["description"],
),
)
def get_search_results(self, search_term):
results = list()
con = lite.connect(self.db_file)
with con:
con.text_factory = unicode
con.text_factory = str
cur = con.cursor()
# purge stale search results
a_day_ago = datetime.datetime.today() - datetime.timedelta(days=1)
cur.execute(
"DELETE FROM VolumeSearchCache WHERE timestamp < ?", [
str(a_day_ago)])
cur.execute("DELETE FROM VolumeSearchCache WHERE timestamp < ?", [str(a_day_ago)])
# fetch
cur.execute(
"SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
cur.execute("SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
rows = cur.fetchall()
# now process the results
for record in rows:
result = dict()
result['id'] = record[1]
result['name'] = record[2]
result['start_year'] = record[3]
result['publisher'] = dict()
result['publisher']['name'] = record[4]
result['count_of_issues'] = record[5]
result['image'] = dict()
result['image']['super_url'] = record[6]
result['description'] = record[7]
result["id"] = record[1]
result["name"] = record[2]
result["start_year"] = record[3]
result["publisher"] = dict()
result["publisher"]["name"] = record[4]
result["count_of_issues"] = record[5]
result["image"] = dict()
result["image"]["super_url"] = record[6]
result["description"] = record[7]
results.append(result)
@ -197,7 +194,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
@ -205,30 +202,21 @@ class ComicVineCacher:
url_list_str = utils.listToString(url_list)
# now add in new record
cur.execute("INSERT INTO AltCovers " +
"(issue_id, url_list) " +
"VALUES(?, ?)",
(issue_id,
url_list_str)
)
cur.execute("INSERT INTO AltCovers " + "(issue_id, url_list) " + "VALUES(?, ?)", (issue_id, url_list_str))
def get_alt_covers(self, issue_id):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
# purge stale issue info - probably issue data won't change
# much....
a_month_ago = datetime.datetime.today() - \
datetime.timedelta(days=30)
cur.execute(
"DELETE FROM AltCovers WHERE timestamp < ?", [
str(a_month_ago)])
a_month_ago = datetime.datetime.today() - datetime.timedelta(days=30)
cur.execute("DELETE FROM AltCovers WHERE timestamp < ?", [str(a_month_ago)])
cur.execute(
"SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
row = cur.fetchone()
if row is None:
return None
@ -252,19 +240,19 @@ class ComicVineCacher:
timestamp = datetime.datetime.now()
if cv_volume_record['publisher'] is None:
if cv_volume_record["publisher"] is None:
pub_name = ""
else:
pub_name = cv_volume_record['publisher']['name']
pub_name = cv_volume_record["publisher"]["name"]
data = {
"name": cv_volume_record['name'],
"name": cv_volume_record["name"],
"publisher": pub_name,
"count_of_issues": cv_volume_record['count_of_issues'],
"start_year": cv_volume_record['start_year'],
"timestamp": timestamp
"count_of_issues": cv_volume_record["count_of_issues"],
"start_year": cv_volume_record["start_year"],
"timestamp": timestamp,
}
self.upsert(cur, "volumes", "id", cv_volume_record['id'], data)
self.upsert(cur, "volumes", "id", cv_volume_record["id"], data)
def add_volume_issues_info(self, volume_id, cv_volume_issues):
@ -282,16 +270,16 @@ class ComicVineCacher:
data = {
"volume_id": volume_id,
"name": issue['name'],
"issue_number": issue['issue_number'],
"site_detail_url": issue['site_detail_url'],
"cover_date": issue['cover_date'],
"super_url": issue['image']['super_url'],
"thumb_url": issue['image']['thumb_url'],
"description": issue['description'],
"timestamp": timestamp
"name": issue["name"],
"issue_number": issue["issue_number"],
"site_detail_url": issue["site_detail_url"],
"cover_date": issue["cover_date"],
"super_url": issue["image"]["super_url"],
"thumb_url": issue["image"]["thumb_url"],
"description": issue["description"],
"timestamp": timestamp,
}
self.upsert(cur, "issues", "id", issue['id'], data)
self.upsert(cur, "issues", "id", issue["id"], data)
def get_volume_info(self, volume_id):
@ -300,17 +288,14 @@ 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)
cur.execute(
"DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
cur.execute("DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
# fetch
cur.execute(
"SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?",
[volume_id])
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [volume_id])
row = cur.fetchone()
@ -320,13 +305,13 @@ class ComicVineCacher:
result = dict()
# since ID is primary key, there is only one row
result['id'] = row[0]
result['name'] = row[1]
result['publisher'] = dict()
result['publisher']['name'] = row[2]
result['count_of_issues'] = row[3]
result['start_year'] = row[4]
result['issues'] = list()
result["id"] = row[0]
result["name"] = row[1]
result["publisher"] = dict()
result["publisher"]["name"] = row[2]
result["count_of_issues"] = row[3]
result["start_year"] = row[4]
result["issues"] = list()
return result
@ -337,35 +322,34 @@ 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....
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
cur.execute(
"DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
# fetch
results = list()
cur.execute(
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?",
[volume_id])
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?", [volume_id]
)
rows = cur.fetchall()
# now process the results
for row in rows:
record = dict()
record['id'] = row[0]
record['name'] = row[1]
record['issue_number'] = row[2]
record['site_detail_url'] = row[3]
record['cover_date'] = row[4]
record['image'] = dict()
record['image']['super_url'] = row[5]
record['image']['thumb_url'] = row[6]
record['description'] = row[7]
record["id"] = row[0]
record["name"] = row[1]
record["issue_number"] = row[2]
record["site_detail_url"] = row[3]
record["cover_date"] = row[4]
record["image"] = dict()
record["image"]["super_url"] = row[5]
record["image"]["thumb_url"] = row[6]
record["description"] = row[7]
results.append(record)
@ -374,19 +358,13 @@ class ComicVineCacher:
return results
def add_issue_select_details(
self,
issue_id,
image_url,
thumb_image_url,
cover_date,
site_detail_url):
def add_issue_select_details(self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = unicode
con.text_factory = str
timestamp = datetime.datetime.now()
data = {
@ -394,7 +372,7 @@ class ComicVineCacher:
"thumb_url": thumb_image_url,
"cover_date": cover_date,
"site_detail_url": site_detail_url,
"timestamp": timestamp
"timestamp": timestamp,
}
self.upsert(cur, "issues", "id", issue_id, data)
@ -403,25 +381,23 @@ 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=?",
[issue_id])
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [issue_id])
row = cur.fetchone()
details = dict()
if row is None or row[0] is None:
details['image_url'] = None
details['thumb_image_url'] = None
details['cover_date'] = None
details['site_detail_url'] = None
details["image_url"] = None
details["thumb_image_url"] = None
details["cover_date"] = None
details["site_detail_url"] = None
else:
details['image_url'] = row[0]
details['thumb_image_url'] = row[1]
details['cover_date'] = row[2]
details['site_detail_url'] = row[3]
details["image_url"] = row[0]
details["thumb_image_url"] = row[1]
details["cover_date"] = row[2]
details["site_detail_url"] = row[3]
return details
@ -459,11 +435,8 @@ class ComicVineCacher:
ins_slots += ", ?"
condition = pkname + " = ?"
sql_ins = ("INSERT OR IGNORE INTO " + tablename +
" (" + keys + ") " +
" VALUES (" + ins_slots + ")")
sql_ins = "INSERT OR IGNORE INTO " + tablename + " (" + keys + ") " + " VALUES (" + ins_slots + ")"
cur.execute(sql_ins, vals)
sql_upd = ("UPDATE " + tablename +
" SET " + set_slots + " WHERE " + condition)
sql_upd = "UPDATE " + tablename + " SET " + set_slots + " WHERE " + condition
cur.execute(sql_upd, vals)

File diff suppressed because it is too large Load Diff

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!
@ -18,21 +18,18 @@ TODO: This should be re-factored using subclasses!
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from comictaggerlib.ui.qtutils import getQImageFromData, reduceWidgetFontSize
from settings import ComicTaggerSettings
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagefetcher import ImageFetcher
from pageloader import PageLoader
from imagepopup import ImagePopup
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, getQImageFromData
#from genericmetadata import GenericMetadata, PageType
#from comicarchive import MetaDataStyle
#import utils
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .imagefetcher import ImageFetcher
from .imagepopup import ImagePopup
from .pageloader import PageLoader
from .settings import ComicTaggerSettings
def clickable(widget):
@ -66,7 +63,7 @@ class CoverImageWidget(QWidget):
def __init__(self, parent, mode, expand_on_click=True):
super(CoverImageWidget, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("coverimagewidget.ui"), self)
reduceWidgetFontSize(self.label)
@ -75,9 +72,8 @@ class CoverImageWidget(QWidget):
self.page_loader = None
self.showControls = True
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
self.btnRight.setIcon(
QIcon(ComicTaggerSettings.getGraphic('right.png')))
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic("left.png")))
self.btnRight.setIcon(QIcon(ComicTaggerSettings.getGraphic("right.png")))
self.btnLeft.clicked.connect(self.decrementImage)
self.btnRight.clicked.connect(self.incrementImage)
@ -144,8 +140,7 @@ class CoverImageWidget(QWidget):
self.issue_id = issue_id
self.comicVine = ComicVineTalker()
self.comicVine.urlFetchComplete.connect(
self.primaryUrlFetchComplete)
self.comicVine.urlFetchComplete.connect(self.primaryUrlFetchComplete)
self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id))
def setImageData(self, image_data):
@ -177,10 +172,8 @@ class CoverImageWidget(QWidget):
# page URL should already be cached, so no need to defer
self.comicVine = ComicVineTalker()
issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id)
self.comicVine.altUrlListFetchComplete.connect(
self.altCoverUrlListFetchComplete)
self.comicVine.asyncFetchAlternateCoverURLs(
int(self.issue_id), issue_page_url)
self.comicVine.altUrlListFetchComplete.connect(self.altCoverUrlListFetchComplete)
self.comicVine.asyncFetchAlternateCoverURLs(int(self.issue_id), issue_page_url)
def altCoverUrlListFetchComplete(self, url_list, issue_id):
if len(url_list) > 0:
@ -228,29 +221,23 @@ class CoverImageWidget(QWidget):
if self.imageIndex == -1 or self.imageCount == 1:
self.label.setText("")
elif self.mode == CoverImageWidget.AltCoverMode:
self.label.setText(
"Cover {0} (of {1})".format(
self.imageIndex + 1,
self.imageCount))
self.label.setText("Cover {0} (of {1})".format(self.imageIndex + 1, self.imageCount))
else:
self.label.setText(
"Page {0} (of {1})".format(
self.imageIndex + 1,
self.imageCount))
self.label.setText("Page {0} (of {1})".format(self.imageIndex + 1, self.imageCount))
def loadURL(self):
self.loadDefault()
self.cover_fetcher = ImageFetcher()
self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete)
self.cover_fetcher.fetch(self.url_list[self.imageIndex])
#print("ATB cover fetch started...")
# print("ATB cover fetch started...")
# called when the image is done loading from internet
def coverRemoteFetchComplete(self, image_data, issue_id):
img = getQImageFromData(image_data)
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap(0, 0)
#print("ATB cover fetch complete!")
# print("ATB cover fetch complete!")
def loadPage(self):
if self.comic_archive is not None:
@ -266,17 +253,14 @@ class CoverImageWidget(QWidget):
self.page_loader = None
def loadDefault(self):
self.current_pixmap = QPixmap(
ComicTaggerSettings.getGraphic('nocover.png'))
#print("loadDefault called")
self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic("nocover.png"))
# print("loadDefault called")
self.setDisplayPixmap(0, 0)
def resizeEvent(self, resize_event):
if self.current_pixmap is not None:
delta_w = resize_event.size().width() - \
resize_event.oldSize().width()
delta_h = resize_event.size().height() - \
resize_event.oldSize().height()
delta_w = resize_event.size().width() - resize_event.oldSize().width()
delta_h = resize_event.size().height() - resize_event.oldSize().height()
# print "ATB resizeEvent deltas", resize_event.size().width(),
# resize_event.size().height()
self.setDisplayPixmap(delta_w, delta_h)
@ -284,14 +268,14 @@ class CoverImageWidget(QWidget):
def setDisplayPixmap(self, delta_w, delta_h):
"""The deltas let us know what the new width and height of the label will be"""
#new_h = self.frame.height() + delta_h
#new_w = self.frame.width() + delta_w
# new_h = self.frame.height() + delta_h
# new_w = self.frame.width() + delta_w
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
# print "ATB self.frame", self.frame.width(), self.frame.height()
# print "ATB self.", self.width(), self.height()
#frame_w = new_w
#frame_h = new_h
# frame_w = new_w
# frame_h = new_h
new_h = self.frame.height()
new_w = self.frame.width()
@ -311,8 +295,7 @@ class CoverImageWidget(QWidget):
# print "ATB new size", new_w, new_h
# scale the pixmap to fit in the frame
scaled_pixmap = self.current_pixmap.scaled(
new_w, new_h, Qt.KeepAspectRatio)
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.lblImage.setPixmap(scaled_pixmap)
# move and resize the label to be centered in the fame

View File

@ -14,14 +14,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
class CreditEditorWindow(QtGui.QDialog):
class CreditEditorWindow(QtWidgets.QDialog):
ModeEdit = 0
ModeNew = 1
@ -29,8 +27,7 @@ class CreditEditorWindow(QtGui.QDialog):
def __init__(self, parent, mode, role, name, primary):
super(CreditEditorWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('crediteditorwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("crediteditorwindow.ui"), self)
self.mode = mode
@ -90,7 +87,6 @@ class CreditEditorWindow(QtGui.QDialog):
def accept(self):
if self.cbRole.currentText() == "" or self.leName.text() == "":
QtGui.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
"You need to enter both role and name for a credit."))
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

@ -14,14 +14,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
#from settingswindow import SettingsWindow
#from filerenamer import FileRenamer
#import utils
from .settings import ComicTaggerSettings
class ExportConflictOpts:
@ -30,16 +25,14 @@ class ExportConflictOpts:
createUnique = 3
class ExportWindow(QtGui.QDialog):
class ExportWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super(ExportWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("exportwindow.ui"), self)
self.label.setText(msg)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
@ -52,7 +45,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

@ -16,20 +16,85 @@
import os
import re
import datetime
import string
import sys
import utils
from issuestring import IssueString
from pathvalidate import sanitize_filepath
from . import utils
from .issuestring import IssueString
class MetadataFormatter(string.Formatter):
def __init__(self, smart_cleanup=False):
super().__init__()
self.smart_cleanup = smart_cleanup
def format_field(self, value, format_spec):
if value is None or value == "":
return ""
return super().format_field(value, format_spec)
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0):
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
result = []
lstrip = False
for literal_text, field_name, format_spec, conversion in self.parse(format_string):
# output the literal text
if literal_text:
if lstrip:
result.append(literal_text.lstrip("-_)}]#"))
else:
result.append(literal_text)
lstrip = False
# if there's a field, output it
if field_name is not None:
# this is some markup, find the object and do
# the formatting
# handle arg indexing when empty field_names are given.
if field_name == "":
if auto_arg_index is False:
raise ValueError("cannot switch from manual field specification to automatic field numbering")
field_name = str(auto_arg_index)
auto_arg_index += 1
elif field_name.isdigit():
if auto_arg_index:
raise ValueError("cannot switch from manual field specification to automatic field numbering")
# disable auto arg incrementing, if it gets
# used later on, then an exception will be raised
auto_arg_index = False
# given the field_name, find the object it references
# and the argument it came from
obj, arg_used = self.get_field(field_name, args, kwargs)
used_args.add(arg_used)
# do any conversion on the resulting object
obj = self.convert_field(obj, conversion)
# expand the format spec, if needed
format_spec, auto_arg_index = self._vformat(format_spec, args, kwargs, used_args, recursion_depth - 1, auto_arg_index=auto_arg_index)
# format the object and append to the result
fmtObj = self.format_field(obj, format_spec)
if fmtObj == "" and len(result) > 0 and self.smart_cleanup:
lstrip = True
result.pop()
result.append(fmtObj)
return "".join(result), auto_arg_index
class FileRenamer:
def __init__(self, metadata):
self.setMetadata(metadata)
self.setTemplate(
"%series% v%volume% #%issue% (of %issuecount%) (%year%)")
self.setTemplate("{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})")
self.smart_cleanup = True
self.issue_zero_padding = 3
self.move = False
def setMetadata(self, metadata):
self.metdata = metadata
@ -43,114 +108,37 @@ class FileRenamer:
def setTemplate(self, template):
self.template = template
def replaceToken(self, text, value, token):
# helper func
def isToken(word):
return (word[0] == "%" and word[-1:] == "%")
if value is not None:
return text.replace(token, unicode(value))
else:
if self.smart_cleanup:
# smart cleanup means we want to remove anything appended to token if it's empty
# (e.g "#%issue%" or "v%volume%")
# (TODO: This could fail if there is more than one token appended together, I guess)
text_list = text.split()
# special case for issuecount, remove preceding non-token word,
# as in "...(of %issuecount%)..."
if token == '%issuecount%':
for idx, word in enumerate(text_list):
if token in word and not isToken(text_list[idx - 1]):
text_list[idx - 1] = ""
text_list = [x for x in text_list if token not in x]
return " ".join(text_list)
else:
return text.replace(token, "")
def determineName(self, filename, ext=None):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
md = self.metdata
new_name = self.template
preferred_encoding = utils.get_actual_preferred_encoding()
# print(u"{0}".format(md))
# padding for issue
md.issue = IssueString(md.issue).asString(pad=self.issue_zero_padding)
new_name = self.replaceToken(new_name, md.series, '%series%')
new_name = self.replaceToken(new_name, md.volume, '%volume%')
template = self.template
if md.issue is not None:
issue_str = u"{0}".format(
IssueString(md.issue).asString(pad=self.issue_zero_padding))
else:
issue_str = None
new_name = self.replaceToken(new_name, issue_str, '%issue%')
pathComponents = template.split(os.sep)
new_name = ""
new_name = self.replaceToken(new_name, md.issueCount, '%issuecount%')
new_name = self.replaceToken(new_name, md.year, '%year%')
new_name = self.replaceToken(new_name, md.publisher, '%publisher%')
new_name = self.replaceToken(new_name, md.title, '%title%')
new_name = self.replaceToken(new_name, md.month, '%month%')
month_name = None
if md.month is not None:
if (isinstance(md.month, str) and md.month.isdigit()) or isinstance(
md.month, int):
if int(md.month) in range(1, 13):
dt = datetime.datetime(1970, int(md.month), 1, 0, 0)
month_name = dt.strftime(
u"%B".encode(preferred_encoding)).decode(preferred_encoding)
new_name = self.replaceToken(new_name, month_name, '%month_name%')
fmt = MetadataFormatter(self.smart_cleanup)
for Component in pathComponents:
new_name = os.path.join(new_name, fmt.vformat(Component, args=[], kwargs=Default(vars(md))).replace("/", "-"))
new_name = self.replaceToken(new_name, md.genre, '%genre%')
new_name = self.replaceToken(new_name, md.language, '%language_code%')
new_name = self.replaceToken(
new_name, md.criticalRating, '%criticalrating%')
new_name = self.replaceToken(
new_name, md.alternateSeries, '%alternateseries%')
new_name = self.replaceToken(
new_name, md.alternateNumber, '%alternatenumber%')
new_name = self.replaceToken(
new_name, md.alternateCount, '%alternatecount%')
new_name = self.replaceToken(new_name, md.imprint, '%imprint%')
new_name = self.replaceToken(new_name, md.format, '%format%')
new_name = self.replaceToken(
new_name, md.maturityRating, '%maturityrating%')
new_name = self.replaceToken(new_name, md.storyArc, '%storyarc%')
new_name = self.replaceToken(new_name, md.seriesGroup, '%seriesgroup%')
new_name = self.replaceToken(new_name, md.scanInfo, '%scaninfo%')
if self.smart_cleanup:
# remove empty braces,brackets, parentheses
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name)
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name)
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
# remove duplicate spaces
new_name = u" ".join(new_name.split())
# remove remove duplicate -, _,
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
new_name = re.sub("(\s--)+", " --", new_name)
new_name = re.sub("(\s-)+", " -", new_name)
# remove dash or double dash at end of line
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
# remove duplicate spaces (again!)
new_name = u" ".join(new_name.split())
if ext is None:
if ext is None or ext == "":
ext = os.path.splitext(filename)[1]
new_name += ext
# some tweaks to keep various filesystems happy
new_name = new_name.replace("/", "-")
new_name = new_name.replace(" :", " -")
new_name = new_name.replace(": ", " - ")
new_name = new_name.replace(":", "-")
new_name = new_name.replace("?", "")
return new_name
# remove padding
md.issue = IssueString(md.issue).asString()
if self.move:
return sanitize_filepath(new_name.strip())
else:
return os.path.basename(sanitize_filepath(new_name.strip()))

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
@ -15,34 +15,32 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import platform
import os
#import os
#import sys
import platform
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyQt4.QtCore import pyqtSignal
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from settings import ComicTaggerSettings
from comicarchive import ComicArchive
from optionalmsgdialog import OptionalMessageDialog
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
import utils
#from comicarchive import MetaDataStyle
#from genericmetadata import GenericMetadata, PageType
from comictaggerlib.ui.qtutils import centerWindowOnParent, reduceWidgetFontSize
from . import utils
from .comicarchive import ComicArchive
from .optionalmsgdialog import OptionalMessageDialog
from .settings import ComicTaggerSettings
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():
class FileInfo:
def __init__(self, ca):
self.ca = ca
@ -63,14 +61,14 @@ class FileSelectionList(QWidget):
def __init__(self, parent, settings):
super(FileSelectionList, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("fileselectionlist.ui"), self)
self.settings = settings
reduceWidgetFontSize(self.twList)
self.twList.setColumnCount(6)
#self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
# self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
# self.twList.horizontalHeader().setStretchLastSection(True)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
@ -83,8 +81,8 @@ class FileSelectionList(QWidget):
self.separator = QAction("", self)
self.separator.setSeparator(True)
selectAllAction.setShortcut('Ctrl+A')
removeAction.setShortcut('Ctrl+X')
selectAllAction.setShortcut("Ctrl+A")
removeAction.setShortcut("Ctrl+X")
selectAllAction.triggered.connect(self.selectAll)
removeAction.triggered.connect(self.removeSelection)
@ -108,24 +106,10 @@ class FileSelectionList(QWidget):
self.modifiedFlag = modified
def selectAll(self):
self.twList.setRangeSelected(
QTableWidgetSelectionRange(
0,
0,
self.twList.rowCount() -
1,
5),
True)
self.twList.setRangeSelected(QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), True)
def deselectAll(self):
self.twList.setRangeSelected(
QTableWidgetSelectionRange(
0,
0,
self.twList.rowCount() -
1,
5),
False)
self.twList.setRangeSelected(QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), False)
def removeArchiveList(self, ca_list):
self.twList.setSortingEnabled(False)
@ -138,8 +122,7 @@ class FileSelectionList(QWidget):
self.twList.setSortingEnabled(True)
def getArchiveByRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
fi = self.twList.item(row, FileSelectionList.dataColNum).data(Qt.UserRole)
return fi.ca
def getCurrentArchive(self):
@ -155,9 +138,7 @@ class FileSelectionList(QWidget):
return
if self.twList.currentRow() in row_list:
if not self.modifiedFlagVerification(
"Remove Archive",
"If you close this archive, data in the form will be lost. Are you sure?"):
if not self.modifiedFlagVerification("Remove Archive", "If you close this archive, data in the form will be lost. Are you sure?"):
return
row_list.sort()
@ -186,19 +167,23 @@ 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()
@ -206,22 +191,24 @@ class FileSelectionList(QWidget):
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"):
progdialog.hide()
QCoreApplication.processEvents()
if self.settings.show_no_unrar_warning and self.settings.unrar_lib_path == "" and not ComicTaggerSettings.haveOwnUnrarLib():
for f in filelist:
ext = os.path.splitext(f)[1].lower()
if ext == ".rar" or ext == ".cbr":
checked = OptionalMessageDialog.msg(self, "No unrar tool",
"""
checked = OptionalMessageDialog.msg(
self,
"No UnRAR Ability",
"""
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.
"""
)
the location of the unrar library in the settings. Until then, ComicTagger
will not be able read these kind of files. See the "RAR Tools" tab in the
settings/preferences for more info.
""",
)
self.settings.show_no_unrar_warning = not checked
break
@ -229,13 +216,17 @@ class FileSelectionList(QWidget):
self.twList.selectRow(firstAdded)
else:
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
QMessageBox.information(self, self.tr("File Open"), self.tr(
"Selected file doesn't seem to be a comic archive."))
ext = os.path.splitext(pathlist[0])[1].lower()
if ext == ".rar" or ext == ".cbr" and self.settings.unrar_lib_path == "":
QMessageBox.information(
self,
self.tr("File Open"),
self.tr("Selected file seems to be a rar file, " "and can't be read until the unrar library is configured."),
)
else:
QMessageBox.information(self, self.tr("File Open"), self.tr("Selected file doesn't seem to be a comic archive."))
else:
QMessageBox.information(
self,
self.tr("File/Folder Open"),
self.tr("No comic archives were found."))
QMessageBox.information(self, self.tr("File/Folder Open"), self.tr("No readable comic archives were found."))
self.twList.setSortingEnabled(True)
@ -271,17 +262,14 @@ class FileSelectionList(QWidget):
return -1
def addPathItem(self, path):
path = unicode(path)
path = str(path)
path = os.path.abspath(path)
# print "processing", path
if self.isListDupe(path):
return self.getCurrentListRow(path)
ca = ComicArchive(
path,
self.settings.rar_exe_path,
ComicTaggerSettings.getGraphic('nocover.png'))
ca = ComicArchive(path, self.settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
if ca.seemsToBeAComicArchive():
row = self.twList.rowCount()
@ -298,12 +286,10 @@ class FileSelectionList(QWidget):
filename_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
filename_item.setData(Qt.UserRole, fi)
self.twList.setItem(
row, FileSelectionList.fileColNum, filename_item)
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
folder_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.twList.setItem(
row, FileSelectionList.folderColNum, folder_item)
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
type_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
@ -318,16 +304,14 @@ class FileSelectionList(QWidget):
readonly_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
readonly_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(
row, FileSelectionList.readonlyColNum, readonly_item)
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
self.updateRow(row)
return row
def updateRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
fi = self.twList.item(row, FileSelectionList.dataColNum).data(Qt.UserRole) # .toPyObject()
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
@ -348,6 +332,8 @@ class FileSelectionList(QWidget):
item_text = "ZIP"
elif fi.ca.isRar():
item_text = "RAR"
elif fi.ca.isTar():
item_text = "TAR"
else:
item_text = ""
type_item.setText(item_text)
@ -382,8 +368,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 +381,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)
@ -405,27 +391,22 @@ class FileSelectionList(QWidget):
old_idx = -1
if prev is not None:
old_idx = prev.row()
#print("old {0} new {1}".format(old_idx, new_idx))
# print("old {0} new {1}".format(old_idx, new_idx))
if old_idx == new_idx:
return
# don't allow change if modified
if prev is not None and new_idx != old_idx:
if not self.modifiedFlagVerification(
"Change Archive",
"If you change archives now, data in the form will be lost. Are you sure?"):
self.twList.currentItemChanged.disconnect(
self.currentItemChangedCB)
if not self.modifiedFlagVerification("Change Archive", "If you change archives now, data in the form will be lost. Are you sure?"):
self.twList.currentItemChanged.disconnect(self.currentItemChangedCB)
self.twList.setCurrentItem(prev)
self.twList.currentItemChanged.connect(
self.currentItemChangedCB)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
# Need to defer this revert selection, for some reason
QTimer.singleShot(1, self.revertSelection)
return
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(Qt.UserRole) # .toPyObject()
self.selectionChanged.emit(QVariant(fi))
def revertSelection(self):
@ -433,10 +414,7 @@ class FileSelectionList(QWidget):
def modifiedFlagVerification(self, title, desc):
if self.modifiedFlag:
reply = QMessageBox.question(self,
self.tr(title),
self.tr(desc),
QMessageBox.Yes, QMessageBox.No)
reply = QMessageBox.question(self, self.tr(title), self.tr(desc), QMessageBox.Yes, QMessageBox.No)
if reply != QMessageBox.Yes:
return False
@ -445,12 +423,12 @@ class FileSelectionList(QWidget):
# Attempt to use a special checkbox widget in the cell.
# Couldn't figure out how to disable it with "enabled" colors
#w = QWidget()
#cb = QCheckBox(w)
# w = QWidget()
# cb = QCheckBox(w)
# cb.setCheckState(Qt.Checked)
#layout = QHBoxLayout()
# layout = QHBoxLayout()
# layout.addWidget(cb)
# layout.setAlignment(Qt.AlignHCenter)
# layout.setMargin(2)
# w.setLayout(layout)
#self.twList.setCellWidget(row, 2, w)
# self.twList.setCellWidget(row, 2, w)

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

@ -14,39 +14,37 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sqlite3 as lite
import os
import datetime
import os
import shutil
import sqlite3 as lite
import tempfile
import urllib
import ssl
#import urllib2
import requests
from . import _version
from .settings import ComicTaggerSettings
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt4 import QtGui
from PyQt5 import QtGui
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
class QObject:
def __init__(self, *args):
pass
class QByteArray():
class QByteArray:
pass
class pyqtSignal():
class pyqtSignal:
def __init__(self, *args):
pass
def emit(a, b, c):
pass
from settings import ComicTaggerSettings
class ImageFetcherException(Exception):
pass
@ -66,9 +64,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 +82,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/" + _version.version}).content
except Exception as e:
print(e)
raise ImageFetcherException("Network Error!")
@ -127,7 +122,7 @@ class ImageFetcher(QObject):
def create_image_db(self):
# this will wipe out any existing version
open(self.db_file, 'w').close()
open(self.db_file, "w").close()
# wipe any existing image cache folder too
if os.path.isdir(self.cache_folder):
@ -141,12 +136,7 @@ class ImageFetcher(QObject):
cur = con.cursor()
cur.execute("CREATE TABLE Images(" +
"url TEXT," +
"filename TEXT," +
"timestamp TEXT," +
"PRIMARY KEY (url))"
)
cur.execute("CREATE TABLE Images(" + "url TEXT," + "filename TEXT," + "timestamp TEXT," + "PRIMARY KEY (url))")
def add_image_to_cache(self, url, image_data):
@ -158,17 +148,12 @@ class ImageFetcher(QObject):
timestamp = datetime.datetime.now()
tmp_fd, filename = tempfile.mkstemp(
dir=self.cache_folder, prefix="img")
f = os.fdopen(tmp_fd, 'w+b')
tmp_fd, filename = tempfile.mkstemp(dir=self.cache_folder, prefix="img")
f = os.fdopen(tmp_fd, "w+b")
f.write(image_data)
f.close()
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)",
(url,
filename,
timestamp)
)
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)", (url, filename, timestamp))
def get_image_from_cache(self, url):
@ -186,7 +171,7 @@ class ImageFetcher(QObject):
image_data = None
try:
with open(filename, 'rb') as f:
with open(filename, "rb") as f:
image_data = f.read()
f.close()
except IOError as e:

View File

@ -14,22 +14,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import StringIO
import io
import sys
from functools import reduce
try:
from PIL import Image
from PIL import WebPImagePlugin
from PIL import Image, WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
class ImageHasher(object):
def __init__(self, path=None, data=None, width=8, height=8):
#self.hash_size = size
# self.hash_size = size
self.width = width
self.height = height
@ -40,33 +39,31 @@ 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))
def average_hash(self):
try:
image = self.image.resize(
(self.width, self.height), Image.ANTIALIAS).convert("L")
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)
def compare_value_to_avg(i):
return (1 if i > avg else 0)
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):
(idx, val) = idx_val
return (x | (val << idx))
return x | (val << idx)
result = reduce(set_bit, enumerate(bitlist), 0)
@ -178,16 +175,16 @@ 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
# count up the 1's in the binary string
return sum(b == '1' for b in bin(n)[2:])
return sum(b == "1" for b in bin(n)[2:])

View File

@ -14,23 +14,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt4 import QtCore, QtGui, 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)
uic.loadUi(ComicTaggerSettings.getUIFile("imagepopup.ui"), self)
QtGui.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
# self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowFlags(QtCore.Qt.Popup)
@ -38,28 +33,23 @@ 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(),
0,
0,
screen_size.width(),
screen_size.height())
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
self.clientBgPixmap = bg.scaled(
screen_size.width(), screen_size.height())
screen = QtWidgets.QApplication.primaryScreen()
self.desktopBg = screen.grabWindow(QtWidgets.QApplication.desktop().winId(), 0, 0, screen_size.width(), screen_size.height())
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic("popup_bg.png"))
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height(), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
self.setMask(self.clientBgPixmap.mask())
self.applyImagePixmap()
self.showFullScreen()
self.raise_()
QtGui.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.restoreOverrideCursor()
def paintEvent(self, event):
self.painter = QtGui.QPainter(self)
@ -72,11 +62,9 @@ class ImagePopup(QtGui.QDialog):
win_h = self.height()
win_w = self.width()
if self.imagePixmap.width(
) > win_w or self.imagePixmap.height() > win_h:
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
# scale the pixmap to fit in the frame
display_pixmap = self.imagePixmap.scaled(
win_w, win_h, QtCore.Qt.KeepAspectRatio)
display_pixmap = self.imagePixmap.scaled(win_w, win_h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
self.lblImage.setPixmap(display_pixmap)
else:
display_pixmap = self.imagePixmap

View File

@ -14,27 +14,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import sys
import StringIO
#import math
#import urllib2
#import urllib
from . import utils
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .genericmetadata import GenericMetadata
from .imagefetcher import ImageFetcher, ImageFetcherException
from .imagehasher import ImageHasher
from .issuestring import IssueString
try:
from PIL import Image
from PIL import WebPImagePlugin
from PIL import Image, WebPImagePlugin
pil_available = True
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 settings import ComicTaggerSettings
#from comicvinecacher import ComicVineCacher
class IssueIdentifierNetworkError(Exception):
@ -79,8 +75,7 @@ class IssueIdentifier:
self.length_delta_thresh = settings.id_length_delta_thresh
# used to eliminate unlikely publishers
self.publisher_blacklist = [
s.strip().lower() for s in settings.id_publisher_blacklist.split(',')]
self.publisher_blacklist = [s.strip().lower() for s in settings.id_publisher_blacklist.split(",")]
self.additional_metadata = GenericMetadata()
self.output_function = IssueIdentifier.defaultWriteOutput
@ -115,16 +110,16 @@ class IssueIdentifier:
pass
def calculateHash(self, image_data):
if self.image_hasher == '3':
if self.image_hasher == "3":
return ImageHasher(data=image_data).dct_average_hash()
elif self.image_hasher == '2':
elif self.image_hasher == "2":
return ImageHasher(data=image_data).average_hash2()
else:
return ImageHasher(data=image_data).average_hash()
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 +127,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()
@ -159,21 +153,21 @@ class IssueIdentifier:
ca = self.comic_archive
search_keys = dict()
search_keys['series'] = None
search_keys['issue_number'] = None
search_keys['month'] = None
search_keys['year'] = None
search_keys['issue_count'] = None
search_keys["series"] = None
search_keys["issue_number"] = None
search_keys["month"] = None
search_keys["year"] = None
search_keys["issue_count"] = None
if ca is None:
return
if self.onlyUseAdditionalMetaData:
search_keys['series'] = self.additional_metadata.series
search_keys['issue_number'] = self.additional_metadata.issue
search_keys['year'] = self.additional_metadata.year
search_keys['month'] = self.additional_metadata.month
search_keys['issue_count'] = self.additional_metadata.issueCount
search_keys["series"] = self.additional_metadata.series
search_keys["issue_number"] = self.additional_metadata.issue
search_keys["year"] = self.additional_metadata.year
search_keys["month"] = self.additional_metadata.month
search_keys["issue_count"] = self.additional_metadata.issueCount
return search_keys
# see if the archive has any useful meta data for searching with
@ -193,39 +187,39 @@ class IssueIdentifier:
# 1. Filename metadata
if self.additional_metadata.series is not None:
search_keys['series'] = self.additional_metadata.series
search_keys["series"] = self.additional_metadata.series
elif internal_metadata.series is not None:
search_keys['series'] = internal_metadata.series
search_keys["series"] = internal_metadata.series
else:
search_keys['series'] = md_from_filename.series
search_keys["series"] = md_from_filename.series
if self.additional_metadata.issue is not None:
search_keys['issue_number'] = self.additional_metadata.issue
search_keys["issue_number"] = self.additional_metadata.issue
elif internal_metadata.issue is not None:
search_keys['issue_number'] = internal_metadata.issue
search_keys["issue_number"] = internal_metadata.issue
else:
search_keys['issue_number'] = md_from_filename.issue
search_keys["issue_number"] = md_from_filename.issue
if self.additional_metadata.year is not None:
search_keys['year'] = self.additional_metadata.year
search_keys["year"] = self.additional_metadata.year
elif internal_metadata.year is not None:
search_keys['year'] = internal_metadata.year
search_keys["year"] = internal_metadata.year
else:
search_keys['year'] = md_from_filename.year
search_keys["year"] = md_from_filename.year
if self.additional_metadata.month is not None:
search_keys['month'] = self.additional_metadata.month
search_keys["month"] = self.additional_metadata.month
elif internal_metadata.month is not None:
search_keys['month'] = internal_metadata.month
search_keys["month"] = internal_metadata.month
else:
search_keys['month'] = md_from_filename.month
search_keys["month"] = md_from_filename.month
if self.additional_metadata.issueCount is not None:
search_keys['issue_count'] = self.additional_metadata.issueCount
search_keys["issue_count"] = self.additional_metadata.issueCount
elif internal_metadata.issueCount is not None:
search_keys['issue_count'] = internal_metadata.issueCount
search_keys["issue_count"] = internal_metadata.issueCount
else:
search_keys['issue_count'] = md_from_filename.issueCount
search_keys["issue_count"] = md_from_filename.issueCount
return search_keys
@ -240,24 +234,15 @@ class IssueIdentifier:
self.output_function("\n")
def getIssueCoverMatchScore(
self,
comicVine,
issue_id,
primary_img_url,
primary_thumb_url,
page_url,
localCoverHashList,
useRemoteAlternates=False,
useLog=True):
self, comicVine, issue_id, primary_img_url, primary_thumb_url, page_url, localCoverHashList, useRemoteAlternates=False, useLog=True
):
# localHashes is a list of pre-calculated hashs.
# useRemoteAlternates - indicates to use alternate covers from CV
try:
url_image_data = ImageFetcher().fetch(
primary_thumb_url, blocking=True)
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
except ImageFetcherException:
self.log_msg(
"Network issue while fetching cover image from Comic Vine. Aborting...")
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError
if self.cancel:
@ -269,24 +254,21 @@ class IssueIdentifier:
remote_cover_list = []
item = dict()
item['url'] = primary_img_url
item["url"] = primary_img_url
item['hash'] = self.calculateHash(url_image_data)
item["hash"] = self.calculateHash(url_image_data)
remote_cover_list.append(item)
if self.cancel:
raise IssueIdentifierCancelled
if useRemoteAlternates:
alt_img_url_list = comicVine.fetchAlternateCoverURLs(
issue_id, page_url)
alt_img_url_list = comicVine.fetchAlternateCoverURLs(issue_id, page_url)
for alt_url in alt_img_url_list:
try:
alt_url_image_data = ImageFetcher().fetch(
alt_url, blocking=True)
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
except ImageFetcherException:
self.log_msg(
"Network issue while fetching alt. cover image from Comic Vine. Aborting...")
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError
if self.cancel:
@ -297,16 +279,15 @@ class IssueIdentifier:
self.coverUrlCallback(alt_url_image_data)
item = dict()
item['url'] = alt_url
item['hash'] = self.calculateHash(alt_url_image_data)
item["url"] = alt_url
item["hash"] = self.calculateHash(alt_url_image_data)
remote_cover_list.append(item)
if self.cancel:
raise IssueIdentifierCancelled
if useLog and useRemoteAlternates:
self.log_msg(
"[{0} alt. covers]".format(len(remote_cover_list) - 1), False)
self.log_msg("[{0} alt. covers]".format(len(remote_cover_list) - 1), False)
if useLog:
self.log_msg("[ ", False)
@ -314,12 +295,11 @@ class IssueIdentifier:
done = False
for local_cover_hash in localCoverHashList:
for remote_cover_item in remote_cover_list:
score = ImageHasher.hamming_distance(
local_cover_hash, remote_cover_item['hash'])
score = ImageHasher.hamming_distance(local_cover_hash, remote_cover_item["hash"])
score_item = dict()
score_item['score'] = score
score_item['url'] = remote_cover_item['url']
score_item['hash'] = remote_cover_item['hash']
score_item["score"] = score
score_item["url"] = remote_cover_item["url"]
score_item["hash"] = remote_cover_item["hash"]
score_list.append(score_item)
if useLog:
self.log_msg("{0}".format(score), False)
@ -335,12 +315,12 @@ class IssueIdentifier:
if useLog:
self.log_msg(" ]", False)
best_score_item = min(score_list, key=lambda x: x['score'])
best_score_item = min(score_list, key=lambda x: x["score"])
return best_score_item
# def validate(self, issue_id):
# create hash list
# create hash list
# score = self.getIssueMatchScore(issue_id, hash_list, useRemoteAlternates = True)
# if score < 20:
# return True
@ -355,13 +335,11 @@ class IssueIdentifier:
self.search_result = self.ResultNoMatches
if not pil_available:
self.log_msg(
"Python Imaging Library (PIL) is not available and is needed for issue identification.")
self.log_msg("Python Imaging Library (PIL) is not available and is needed for issue identification.")
return self.match_list
if not ca.seemsToBeAComicArchive():
self.log_msg(
"Sorry, but " + opts.filename + " is not a comic archive!")
self.log_msg("Sorry, but " + opts.filename + " is not a comic archive!")
return self.match_list
cover_image_data = ca.getPage(self.cover_page_index)
@ -377,44 +355,42 @@ class IssueIdentifier:
if right_side_image_data is not None:
narrow_cover_hash = self.calculateHash(right_side_image_data)
#self.log_msg("Cover hash = {0:016x}".format(cover_hash))
# self.log_msg("Cover hash = {0:016x}".format(cover_hash))
keys = self.getSearchKeys()
# normalize the issue number
keys['issue_number'] = IssueString(keys['issue_number']).asString()
keys["issue_number"] = IssueString(keys["issue_number"]).asString()
# we need, at minimum, a series and issue number
if keys['series'] is None or keys['issue_number'] is None:
if keys["series"] is None or keys["issue_number"] is None:
self.log_msg("Not enough info for a search!")
return []
self.log_msg("Going to search for:")
self.log_msg("\tSeries: " + keys['series'])
self.log_msg("\tIssue: " + keys['issue_number'])
if keys['issue_count'] is not None:
self.log_msg("\tCount: " + str(keys['issue_count']))
if keys['year'] is not None:
self.log_msg("\tYear: " + str(keys['year']))
if keys['month'] is not None:
self.log_msg("\tMonth: " + str(keys['month']))
self.log_msg("\tSeries: " + keys["series"])
self.log_msg("\tIssue: " + keys["issue_number"])
if keys["issue_count"] is not None:
self.log_msg("\tCount: " + str(keys["issue_count"]))
if keys["year"] is not None:
self.log_msg("\tYear: " + str(keys["year"]))
if keys["month"] is not None:
self.log_msg("\tMonth: " + str(keys["month"]))
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
# self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
comicVine.setLogFunc(self.output_function)
# self.log_msg(("Searching for " + keys['series'] + "...")
self.log_msg(u"Searching for {0} #{1} ...".format(
keys['series'], keys['issue_number']))
self.log_msg("Searching for {0} #{1} ...".format(keys["series"], keys["issue_number"]))
try:
cv_search_results = comicVine.searchForSeries(keys['series'])
cv_search_results = comicVine.searchForSeries(keys["series"])
except ComicVineTalkerException:
self.log_msg(
"Network issue while searching for series. Aborting...")
self.log_msg("Network issue while searching for series. Aborting...")
return []
#self.log_msg("Found " + str(len(cv_search_results)) + " initial results")
# self.log_msg("Found " + str(len(cv_search_results)) + " initial results")
if self.cancel:
return []
@ -423,61 +399,51 @@ class IssueIdentifier:
series_second_round_list = []
#self.log_msg("Removing results with too long names, banned publishers, or future start dates")
# self.log_msg("Removing results with too long names, banned publishers, or future start dates")
for item in cv_search_results:
length_approved = False
publisher_approved = True
date_approved = True
# remove any series that starts after the issue year
if keys['year'] is not None and str(
keys['year']).isdigit() and item['start_year'] is not None and str(
item['start_year']).isdigit():
if int(keys['year']) < int(item['start_year']):
if keys["year"] is not None and str(keys["year"]).isdigit() and item["start_year"] is not None and str(item["start_year"]).isdigit():
if int(keys["year"]) < int(item["start_year"]):
date_approved = False
# assume that our search name is close to the actual name, say
# within ,e.g. 5 chars
shortened_key = utils.removearticles(keys['series'])
shortened_item_name = utils.removearticles(item['name'])
if len(shortened_item_name) < (
len(shortened_key) + self.length_delta_thresh):
shortened_key = utils.removearticles(keys["series"])
shortened_item_name = utils.removearticles(item["name"])
if len(shortened_item_name) < (len(shortened_key) + self.length_delta_thresh):
length_approved = True
# remove any series from publishers on the blacklist
if item['publisher'] is not None:
publisher = item['publisher']['name']
if publisher is not None and publisher.lower(
) in self.publisher_blacklist:
if item["publisher"] is not None:
publisher = item["publisher"]["name"]
if publisher is not None and publisher.lower() in self.publisher_blacklist:
publisher_approved = False
if length_approved and publisher_approved and date_approved:
series_second_round_list.append(item)
self.log_msg(
"Searching in " + str(len(series_second_round_list)) + " series")
self.log_msg("Searching in " + str(len(series_second_round_list)) + " series")
if self.callback is not None:
self.callback(0, len(series_second_round_list))
# now sort the list by name length
series_second_round_list.sort(
key=lambda x: len(x['name']), reverse=False)
series_second_round_list.sort(key=lambda x: len(x["name"]), reverse=False)
# build a list of volume IDs
volume_id_list = list()
for series in series_second_round_list:
volume_id_list.append(series['id'])
volume_id_list.append(series["id"])
try:
issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear(
volume_id_list,
keys['issue_number'],
keys['year'])
issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear(volume_id_list, keys["issue_number"], keys["year"])
except ComicVineTalkerException:
self.log_msg(
"Network issue while searching for series details. Aborting...")
self.log_msg("Network issue while searching for series details. Aborting...")
return []
if issue_list is None:
@ -487,19 +453,14 @@ class IssueIdentifier:
# now re-associate the issues and volumes
for issue in issue_list:
for series in series_second_round_list:
if series['id'] == issue['volume']['id']:
if series["id"] == issue["volume"]["id"]:
shortlist.append((series, issue))
break
if keys['year'] is None:
self.log_msg(u"Found {0} series that have an issue #{1}".format(
len(shortlist), keys['issue_number']))
if keys["year"] is None:
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(
len(shortlist),
keys['issue_number'],
keys['year']))
self.log_msg("Found {0} series that have an issue #{1} from {2}".format(len(shortlist), keys["issue_number"], keys["year"]))
# now we have a shortlist of volumes with the desired issue number
# Do first round of cover matching
@ -509,13 +470,10 @@ class IssueIdentifier:
self.callback(counter, len(shortlist) * 3)
counter += 1
self.log_msg(u"Examining covers for ID: {0} {1} ({2}) ...".format(
series['id'],
series['name'],
series['start_year']), newline=False)
self.log_msg("Examining covers for ID: {0} {1} ({2}) ...".format(series["id"], series["name"], series["start_year"]), newline=False)
# parse out the cover date
day, month, year = comicVine.parseDateStr(issue['cover_date'])
day, month, year = comicVine.parseDateStr(issue["cover_date"])
# Now check the cover match against the primary image
hash_list = [cover_hash]
@ -523,45 +481,39 @@ class IssueIdentifier:
hash_list.append(narrow_cover_hash)
try:
image_url = issue['image']['super_url']
thumb_url = issue['image']['thumb_url']
page_url = issue['site_detail_url']
image_url = issue["image"]["super_url"]
thumb_url = issue["image"]["thumb_url"]
page_url = issue["site_detail_url"]
score_item = self.getIssueCoverMatchScore(
comicVine,
issue['id'],
image_url,
thumb_url,
page_url,
hash_list,
useRemoteAlternates=False)
comicVine, issue["id"], image_url, thumb_url, page_url, hash_list, useRemoteAlternates=False
)
except:
self.match_list = []
return self.match_list
match = dict()
match['series'] = u"{0} ({1})".format(
series['name'], series['start_year'])
match['distance'] = score_item['score']
match['issue_number'] = keys['issue_number']
match['cv_issue_count'] = series['count_of_issues']
match['url_image_hash'] = score_item['hash']
match['issue_title'] = issue['name']
match['issue_id'] = issue['id']
match['volume_id'] = series['id']
match['month'] = month
match['year'] = year
match['publisher'] = None
if series['publisher'] is not None:
match['publisher'] = series['publisher']['name']
match['image_url'] = image_url
match['thumb_url'] = thumb_url
match['page_url'] = page_url
match['description'] = issue['description']
match["series"] = "{0} ({1})".format(series["name"], series["start_year"])
match["distance"] = score_item["score"]
match["issue_number"] = keys["issue_number"]
match["cv_issue_count"] = series["count_of_issues"]
match["url_image_hash"] = score_item["hash"]
match["issue_title"] = issue["name"]
match["issue_id"] = issue["id"]
match["volume_id"] = series["id"]
match["month"] = month
match["year"] = year
match["publisher"] = None
if series["publisher"] is not None:
match["publisher"] = series["publisher"]["name"]
match["image_url"] = image_url
match["thumb_url"] = thumb_url
match["page_url"] = page_url
match["description"] = issue["description"]
self.match_list.append(match)
self.log_msg(" --> {0}".format(match['distance']), newline=False)
self.log_msg(" --> {0}".format(match["distance"]), newline=False)
self.log_msg("")
@ -571,33 +523,29 @@ class IssueIdentifier:
return self.match_list
# sort list by image match scores
self.match_list.sort(key=lambda k: k['distance'])
self.match_list.sort(key=lambda k: k["distance"])
l = []
for i in self.match_list:
l.append(i['distance'])
l.append(i["distance"])
self.log_msg("Compared to covers in {0} issue(s):".format(
len(self.match_list)), newline=False)
self.log_msg("Compared to covers in {0} issue(s):".format(len(self.match_list)), newline=False)
self.log_msg(str(l))
def print_match(item):
self.log_msg(u"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
item['series'],
item['issue_number'],
item['issue_title'],
item['month'],
item['year'],
item['distance']))
self.log_msg(
"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
item["series"], item["issue_number"], item["issue_title"], item["month"], item["year"], item["distance"]
)
)
best_score = self.match_list[0]['distance']
best_score = self.match_list[0]["distance"]
if best_score >= self.min_score_thresh:
# we have 1 or more low-confidence matches (all bad cover scores)
# look at a few more pages in the archive, and also alternate
# covers online
self.log_msg(
"Very weak scores for the cover. Analyzing alternate pages and covers...")
self.log_msg("Very weak scores for the cover. Analyzing alternate pages and covers...")
hash_list = [cover_hash]
if narrow_cover_hash is not None:
hash_list.append(narrow_cover_hash)
@ -612,46 +560,32 @@ class IssueIdentifier:
if self.callback is not None:
self.callback(counter, len(self.match_list) * 3)
counter += 1
self.log_msg(
u"Examining alternate covers for ID: {0} {1} ...".format(
m['volume_id'],
m['series']),
newline=False)
self.log_msg("Examining alternate covers for ID: {0} {1} ...".format(m["volume_id"], m["series"]), newline=False)
try:
score_item = self.getIssueCoverMatchScore(
comicVine,
m['issue_id'],
m['image_url'],
m['thumb_url'],
m['page_url'],
hash_list,
useRemoteAlternates=True)
comicVine, m["issue_id"], m["image_url"], m["thumb_url"], m["page_url"], hash_list, useRemoteAlternates=True
)
except:
self.match_list = []
return self.match_list
self.log_msg("--->{0}".format(score_item['score']))
self.log_msg("--->{0}".format(score_item["score"]))
self.log_msg("")
if score_item['score'] < self.min_alternate_score_thresh:
if score_item["score"] < self.min_alternate_score_thresh:
second_match_list.append(m)
m['distance'] = score_item['score']
m["distance"] = score_item["score"]
if len(second_match_list) == 0:
if len(self.match_list) == 1:
self.log_msg("No matching pages in the issue.")
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultFoundMatchButBadCoverScore
else:
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg(
u"Multiple bad cover matches! Need to use other info...")
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
self.log_msg("Multiple bad cover matches! Need to use other info...")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultMultipleMatchesWithBadImageScores
return self.match_list
else:
@ -660,10 +594,9 @@ class IssueIdentifier:
self.match_list = second_match_list
# sort new list by image match scores
self.match_list.sort(key=lambda k: k['distance'])
best_score = self.match_list[0]['distance']
self.log_msg(
"[Second round cover matching: best score = {0}]".format(best_score))
self.match_list.sort(key=lambda k: k["distance"])
best_score = self.match_list[0]["distance"]
self.log_msg("[Second round cover matching: best score = {0}]".format(best_score))
# now drop down into the rest of the processing
if self.callback is not None:
@ -672,51 +605,41 @@ class IssueIdentifier:
# now pare down list, remove any item more than specified distant from
# the top scores
for item in reversed(self.match_list):
if item['distance'] > best_score + self.min_score_distance:
if item["distance"] > best_score + self.min_score_distance:
self.match_list.remove(item)
# One more test for the case choosing limited series first issue vs a trade with the same cover:
# if we have a given issue count > 1 and the volume from CV has
# count==1, remove it from match list
if len(self.match_list) >= 2 and keys[
'issue_count'] is not None and keys['issue_count'] != 1:
if len(self.match_list) >= 2 and keys["issue_count"] is not None and keys["issue_count"] != 1:
new_list = list()
for match in self.match_list:
if match['cv_issue_count'] != 1:
if match["cv_issue_count"] != 1:
new_list.append(match)
else:
self.log_msg(
"Removing volume {0} [{1}] from consideration (only 1 issue)".format(
match['series'],
match['volume_id']))
self.log_msg("Removing volume {0} [{1}] from consideration (only 1 issue)".format(match["series"], match["volume_id"]))
if len(new_list) > 0:
self.match_list = new_list
if len(self.match_list) == 1:
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultOneGoodMatch
elif len(self.match_list) == 0:
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
self.log_msg("No matches found :(")
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
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"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
for item in self.match_list:
print_match(item)
self.log_msg(
u"--------------------------------------------------------------------------")
self.log_msg("--------------------------------------------------------------------------")
return self.match_list

View File

@ -14,54 +14,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#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 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
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .coverimagewidget import CoverImageWidget
from .issuestring import IssueString
from .settings import ComicTaggerSettings
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()
return (IssueString(selfStr).asFloat() <
IssueString(otherStr).asFloat())
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
def __init__(self, parent, settings, series_id, issue_number):
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('issueselectionwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("issueselectionwindow.ui"), self)
self.coverWidget = CoverImageWidget(
self.coverImageContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtGui.QGridLayout(self.coverImageContainer)
self.coverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
gridlayout.addWidget(self.coverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.series_id = series_id
self.settings = settings
@ -85,33 +73,25 @@ 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()
if (issue_id == self.initial_id):
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(
QtGui.QCursor(QtCore.Qt.WaitCursor))
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
try:
comicVine = ComicVineTalker()
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(
self,
self.tr("Comic Vine Error"),
ComicVineTalker.getRateLimitMessage())
QtWidgets.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage())
else:
QtGui.QMessageBox.critical(
self,
self.tr("Network Issue"),
self.tr("Could not connect to Comic Vine to list issues!"))
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to list issues!"))
return
while self.twList.rowCount() > 0:
@ -123,15 +103,15 @@ class IssueSelectionWindow(QtGui.QDialog):
for record in self.issue_list:
self.twList.insertRow(row)
item_text = record['issue_number']
item_text = record["issue_number"]
item = IssueNumberTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, record['id'])
item.setData(QtCore.Qt.UserRole, record["id"])
item.setData(QtCore.Qt.DisplayRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = record['cover_date']
item_text = record["cover_date"]
if item_text is None:
item_text = ""
# remove the day of "YYYY-MM-DD"
@ -139,30 +119,28 @@ 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)
item_text = record['name']
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)
if IssueString(
record['issue_number']).asString().lower() == IssueString(
self.issue_number).asString().lower():
self.initial_id = record['id']
if IssueString(record["issue_number"]).asString().lower() == IssueString(self.issue_number).asString().lower():
self.initial_id = record["id"]
row += 1
self.twList.setSortingEnabled(True)
self.twList.sortItems(0, QtCore.Qt.AscendingOrder)
QtGui.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.restoreOverrideCursor()
def cellDoubleClicked(self, r, c):
self.accept()
@ -174,17 +152,16 @@ 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:
if record['id'] == self.issue_id:
self.issue_number = record['issue_number']
if record["id"] == self.issue_id:
self.issue_number = record["issue_number"]
self.coverWidget.setIssueID(int(self.issue_id))
if record['description'] is None:
if record["description"] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(record['description'])
self.teDescription.setText(record["description"])
break

View File

@ -14,24 +14,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt4 import QtCore, QtGui, 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)
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("logwindow.ui"), self)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
def setText(self, text):
try:
text = text.decode()
except:
pass
self.textEdit.setPlainText(text)

View File

@ -14,65 +14,85 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import signal
import traceback
import os
import platform
#import os
import signal
import sys
import traceback
from . import cli, utils
from .comicvinetalker import ComicVineTalker
from .options import Options
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
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"
# if platform.system() == "Darwin":
# QtWidgets.QApplication.setStyle("macintosh")
# else:
# QtWidgets.QApplication.setStyle("Fusion")
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)
if platform.system() != "Linux":
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
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 +100,4 @@ def ctmain():
sys.exit(app.exec_())
except Exception as e:
QtGui.QMessageBox.critical(
QtGui.QMainWindow(),
"Error",
"Unhandled exception in app:\n" +
traceback.format_exc())
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc())

View File

@ -15,48 +15,38 @@
# limitations under the License.
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 settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#from imagefetcher import ImageFetcher
#from comicarchive import MetaDataStyle
#from comicvinetalker import ComicVineTalker
#import utils
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class MatchSelectionWindow(QtGui.QDialog):
class MatchSelectionWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, matches, comic_archive):
super(MatchSelectionWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("matchselectionwindow.ui"), self)
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
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)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.matches = matches
self.comic_archive = comic_archive
@ -74,8 +64,7 @@ class MatchSelectionWindow(QtGui.QDialog):
self.twList.selectRow(0)
path = self.comic_archive.path
self.setWindowTitle(u"Select correct match: {0}".format(
os.path.split(path)[1]))
self.setWindowTitle("Select correct match: {0}".format(os.path.split(path)[1]))
def populateTable(self):
@ -88,39 +77,39 @@ class MatchSelectionWindow(QtGui.QDialog):
for match in self.matches:
self.twList.insertRow(row)
item_text = match['series']
item = QtGui.QTableWidgetItem(item_text)
item_text = match["series"]
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'])
if match["publisher"] is not None:
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"????"
if match['month'] is not None:
month_str = u"-{0:02d}".format(int(match['month']))
if match['year'] is not None:
year_str = u"{0}".format(match['year'])
month_str = ""
year_str = "????"
if match["month"] is not None:
month_str = "-{0:02d}".format(int(match["month"]))
if match["year"] is not None:
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)
item_text = match['issue_title']
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)
@ -144,17 +133,16 @@ class MatchSelectionWindow(QtGui.QDialog):
if prev is not None and prev.row() == curr.row():
return
self.altCoverWidget.setIssueID(self.currentMatch()['issue_id'])
if self.currentMatch()['description'] is None:
self.altCoverWidget.setIssueID(self.currentMatch()["issue_id"])
if self.currentMatch()["description"] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(self.currentMatch()['description'])
self.teDescription.setText(self.currentMatch()["description"])
def setCoverImage(self):
self.archiveCoverWidget.setArchive(self.comic_archive)
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(
QtCore.Qt.UserRole).toPyObject()[0]
match = self.twList.item(row, 0).data(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,18 +25,16 @@ 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
StyleQuestion = 1
class OptionalMessageDialog(QDialog):
def __init__(self, parent, style, title, msg,
check_state=Qt.Unchecked, check_text=None):
def __init__(self, parent, style, title, msg, check_state=Qt.Unchecked, check_text=None):
QDialog.__init__(self, parent)
self.setWindowTitle(title)
@ -48,8 +46,7 @@ class OptionalMessageDialog(QDialog):
self.theLabel.setWordWrap(True)
self.theLabel.setTextFormat(Qt.RichText)
self.theLabel.setOpenExternalLinks(True)
self.theLabel.setTextInteractionFlags(
Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
self.theLabel.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
l.addWidget(self.theLabel)
l.insertSpacing(-1, 10)
@ -70,11 +67,7 @@ class OptionalMessageDialog(QDialog):
if style == StyleQuestion:
btnbox_style = QDialogButtonBox.Yes | QDialogButtonBox.No
self.theButtonBox = QDialogButtonBox(
btnbox_style,
parent=self,
accepted=self.accept,
rejected=self.reject)
self.theButtonBox = QDialogButtonBox(btnbox_style, parent=self, accepted=self.accept, rejected=self.reject)
l.addWidget(self.theButtonBox)
@ -89,28 +82,15 @@ class OptionalMessageDialog(QDialog):
@staticmethod
def msg(parent, title, msg, check_state=Qt.Unchecked, check_text=None):
d = OptionalMessageDialog(
parent,
StyleMessage,
title,
msg,
check_state=check_state,
check_text=check_text)
d = OptionalMessageDialog(parent, StyleMessage, title, msg, check_state=check_state, check_text=check_text)
d.exec_()
return d.theCheckBox.isChecked()
@staticmethod
def question(
parent, title, msg, check_state=Qt.Unchecked, check_text=None):
def question(parent, title, msg, check_state=Qt.Unchecked, check_text=None):
d = OptionalMessageDialog(
parent,
StyleQuestion,
title,
msg,
check_state=check_state,
check_text=check_text)
d = OptionalMessageDialog(parent, StyleQuestion, title, msg, check_state=check_state, check_text=check_text)
d.exec_()

View File

@ -14,23 +14,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import getopt
import platform
import os
import platform
import sys
import traceback
from . import _version, utils
from .comicarchive import MetaDataStyle
from .genericmetadata import GenericMetadata
from .versionchecker import VersionChecker
try:
import argparse
except ImportError:
pass
from genericmetadata import GenericMetadata
from comicarchive import MetaDataStyle
from versionchecker import VersionChecker
import ctversion
import utils
class Options:
help_text = """Usage: {0} [option] ... [file [files ...]]
@ -112,6 +111,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
self.filename = None
self.verbose = False
self.terse = False
self.auto_imprint = False
self.metadata = None
self.print_tags = False
self.copy_tags = False
@ -144,7 +144,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)
@ -184,12 +184,9 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
if key.lower() == "credit":
cred_attribs = value.split(":")
role = cred_attribs[0]
person = (cred_attribs[1] if len(cred_attribs) > 1 else "")
primary = (cred_attribs[2] if len(cred_attribs) > 2 else None)
md.addCredit(
person.strip(),
role.strip(),
True if primary is not None else False)
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
primary = cred_attribs[2] if len(cred_attribs) > 2 else None
md.addCredit(person.strip(), role.strip(), True if primary is not None else False)
else:
md_dict[key] = value
@ -210,9 +207,9 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
# script
script_args = list()
for idx, arg in enumerate(sys.argv):
if arg in ['-S', '--script']:
if arg in ["-S", "--script"]:
# found script!
script_args = sys.argv[idx + 1:]
script_args = sys.argv[idx + 1 :]
break
sys.argv = script_args
if not os.path.exists(scriptfile):
@ -232,18 +229,16 @@ 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)
def parseCmdLineArgs(self):
if platform.system() == "Darwin" and hasattr(
sys, "frozen") and sys.frozen == 1:
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
# remove the PSN ("process serial number") argument from OS/X
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a]
else:
@ -251,8 +246,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
# first check if we're launching a script:
for n in range(len(input_args)):
if (input_args[n] in ["-S", "--script"] and
n + 1 < len(input_args)):
if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args):
# insert a "--" which will cause getopt to ignore the remaining args
# so they will be passed to the script
input_args.insert(n + 2, "--")
@ -260,15 +254,41 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
# parse command line options
try:
opts, args = getopt.getopt(input_args,
"hpdt:fm:vownsrc:ieRS:1",
["help", "print", "delete", "type=", "copy=", "parsefilename",
"metadata=", "verbose", "online", "dryrun", "save", "rename",
"raw", "noabort", "terse", "nooverwrite", "interactive",
"nosummary", "version", "id=", "recursive", "script=",
"export-to-zip", "delete-rar", "abort-on-conflict",
"assume-issue-one", "cv-api-key=", "only-set-cv-key",
"wait-on-cv-rate-limit"])
opts, args = getopt.getopt(
input_args,
"hpdt:fm:vownsrc:ieRS:1",
[
"help",
"print",
"delete",
"type=",
"copy=",
"parsefilename",
"metadata=",
"verbose",
"online",
"dryrun",
"save",
"rename",
"raw",
"noabort",
"terse",
"nooverwrite",
"interactive",
"nosummary",
"version",
"id=",
"recursive",
"script=",
"export-to-zip",
"delete-rar",
"abort-on-conflict",
"assume-issue-one",
"cv-api-key=",
"only-set-cv-key",
"wait-on-cv-rate-limit",
],
)
except getopt.GetoptError as err:
self.display_msg_and_quit(str(err), 2)
@ -290,6 +310,8 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
self.delete_tags = True
if o in ("-i", "--interactive"):
self.interactive = True
if o in ("-a", "--auto-imprint"):
self.auto_imprint = True
if o in ("-c", "--copy"):
self.copy_tags = True
if a.lower() == "cr":
@ -299,8 +321,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
elif a.lower() == "comet":
self.copy_source = MetaDataStyle.COMET
else:
self.display_msg_and_quit(
"Invalid copy tag source type", 1)
self.display_msg_and_quit("Invalid copy tag source type", 1)
if o in ("-o", "--online"):
self.search_online = True
if o in ("-n", "--dryrun"):
@ -340,10 +361,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(
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
print("ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(_version.version))
print("Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
sys.exit(0)
if o in ("-t", "--type"):
if a.lower() == "cr":
@ -377,9 +396,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
count += 1
if count > 1:
self.display_msg_and_quit(
"Must choose only one action of print, delete, save, copy, rename, export, set key, or run script",
1)
self.display_msg_and_quit("Must choose only one action of print, delete, save, copy, rename, export, set key, or run script", 1)
if self.script is not None:
self.launch_script(self.script)
@ -388,6 +405,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
if platform.system() == "Windows":
# no globbing on windows shell, so do it for them
import glob
self.file_list = []
for item in args:
self.file_list.extend(glob.glob(item))
@ -400,22 +418,17 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
if self.only_set_key and self.cv_api_key is None:
self.display_msg_and_quit("Key not given!", 1)
if (self.only_set_key == False) and self.no_gui and (
self.filename is None):
self.display_msg_and_quit(
"Command requires at least one filename!", 1)
if (self.only_set_key == False) and self.no_gui and (self.filename is None):
self.display_msg_and_quit("Command requires at least one filename!", 1)
if self.delete_tags and self.data_style is None:
self.display_msg_and_quit(
"Please specify the type to delete with -t", 1)
self.display_msg_and_quit("Please specify the type to delete with -t", 1)
if self.save_tags and self.data_style is None:
self.display_msg_and_quit(
"Please specify the type to save with -t", 1)
self.display_msg_and_quit("Please specify the type to save with -t", 1)
if self.copy_tags and self.data_style is None:
self.display_msg_and_quit(
"Please specify the type to copy to with -t", 1)
self.display_msg_and_quit("Please specify the type to copy to with -t", 1)
# if self.rename_file and self.data_style is None:
# self.display_msg_and_quit("Please specify the type to use for renaming with -t", 1)

View File

@ -15,47 +15,39 @@
# limitations under the License.
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 .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class PageBrowserWindow(QtGui.QDialog):
class PageBrowserWindow(QtWidgets.QDialog):
def __init__(self, parent, metadata):
super(PageBrowserWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("pagebrowser.ui"), self)
self.pageWidget = CoverImageWidget(
self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtGui.QGridLayout(self.pageContainer)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.pageWidget.showControls = False
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.comic_archive = None
self.page_count = 0
self.current_page_num = 0
self.metadata = metadata
self.buttonBox.button(QtGui.QDialogButtonBox.Close).setDefault(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).setDefault(True)
if platform.system() == "Darwin":
self.btnPrev.setText("<<")
self.btnNext.setText(">>")
else:
self.btnPrev.setIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('left.png')))
self.btnNext.setIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('right.png')))
self.btnPrev.setIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("left.png")))
self.btnNext.setIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("right.png")))
self.btnNext.clicked.connect(self.nextPage)
self.btnPrev.clicked.connect(self.prevPage)
@ -104,11 +96,9 @@ class PageBrowserWindow(QtGui.QDialog):
def setPage(self):
if self.metadata is not None:
archive_page_index = self.metadata.getArchivePageIndex(
self.current_page_num)
archive_page_index = self.metadata.getArchivePageIndex(self.current_page_num)
else:
archive_page_index = self.current_page_num
self.pageWidget.setPage(archive_page_index)
self.setWindowTitle(
"Page Browser - Page {0} (of {1}) ".format(self.current_page_num + 1, self.page_count))
self.setWindowTitle("Page Browser - Page {0} (of {1}) ".format(self.current_page_num + 1, self.page_count))

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
@ -14,21 +14,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
import sys
from operator import attrgetter, itemgetter
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
#from pageloader import PageLoader
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
from .genericmetadata import GenericMetadata, PageType
from .settings import ComicTaggerSettings
def itemMoveEvents(widget):
class Filter(QObject):
mysignal = pyqtSignal(str)
@ -75,10 +75,10 @@ class PageListEditor(QWidget):
def __init__(self, parent):
super(PageListEditor, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("pagelisteditor.ui"), self)
self.pageWidget = CoverImageWidget(
self.pageContainer, CoverImageWidget.ArchiveMode)
self.setEnabled(True)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -88,28 +88,17 @@ class PageListEditor(QWidget):
# Add the entries to the manga combobox
self.comboBox.addItem("", "")
self.comboBox.addItem(
self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
self.comboBox.addItem(
self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
self.comboBox.addItem(
self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
self.comboBox.addItem(
self.pageTypeNames[PageType.Roundup], PageType.Roundup)
self.comboBox.addItem(
self.pageTypeNames[PageType.Story], PageType.Story)
self.comboBox.addItem(
self.pageTypeNames[PageType.Editorial], PageType.Editorial)
self.comboBox.addItem(
self.pageTypeNames[PageType.Letters], PageType.Letters)
self.comboBox.addItem(
self.pageTypeNames[PageType.Preview], PageType.Preview)
self.comboBox.addItem(
self.pageTypeNames[PageType.BackCover], PageType.BackCover)
self.comboBox.addItem(
self.pageTypeNames[PageType.Other], PageType.Other)
self.comboBox.addItem(
self.pageTypeNames[PageType.Deleted], PageType.Deleted)
self.comboBox.addItem(self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
self.comboBox.addItem(self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
self.comboBox.addItem(self.pageTypeNames[PageType.Roundup], PageType.Roundup)
self.comboBox.addItem(self.pageTypeNames[PageType.Story], PageType.Story)
self.comboBox.addItem(self.pageTypeNames[PageType.Editorial], PageType.Editorial)
self.comboBox.addItem(self.pageTypeNames[PageType.Letters], PageType.Letters)
self.comboBox.addItem(self.pageTypeNames[PageType.Preview], PageType.Preview)
self.comboBox.addItem(self.pageTypeNames[PageType.BackCover], PageType.BackCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Other], PageType.Other)
self.comboBox.addItem(self.pageTypeNames[PageType.Deleted], PageType.Deleted)
self.listWidget.itemSelectionChanged.connect(self.changePage)
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
@ -119,31 +108,90 @@ class PageListEditor(QWidget):
self.pre_move_row = -1
self.first_front_page = None
def toggleAd(self):
ad = self.comboBox.findData(PageType.Advertisement)
if self.comboBox.currentIndex() == ad:
self.comboBox.setCurrentIndex(0)
self.changePageType(0)
else:
self.comboBox.setCurrentIndex(ad)
self.changePageType(ad)
def resetPage(self):
self.pageWidget.clear()
self.comboBox.setDisabled(True)
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 +204,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()
@ -169,9 +217,8 @@ class PageListEditor(QWidget):
i = self.comboBox.findData(pagetype)
self.comboBox.setCurrentIndex(i)
#idx = int(str (self.listWidget.item(row).text()))
idx = int(self.listWidget.item(row).data(
Qt.UserRole).toPyObject()[0]['Image'])
# idx = int(str (self.listWidget.item(row).text()))
idx = int(self.listWidget.item(row).data(Qt.UserRole)[0]["Image"])
if self.comic_archive is not None:
self.pageWidget.setArchive(self.comic_archive, idx)
@ -180,30 +227,29 @@ 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]
if 'Type' in page_dict and page_dict[
'Type'] == PageType.FrontCover:
frontCover = int(page_dict['Image'])
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"])
break
return frontCover
def getCurrentPageType(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
if 'Type' in page_dict:
return page_dict['Type']
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] # .toPyObject()[0]
if "Type" in page_dict:
return page_dict["Type"]
else:
return ""
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:
del(page_dict['Type'])
if "Type" in page_dict:
del page_dict["Type"]
else:
page_dict['Type'] = str(t)
page_dict["Type"] = str(t)
item = self.listWidget.item(row)
# wrap the dict in a tuple to keep from being converted to QStrings
@ -230,16 +276,16 @@ class PageListEditor(QWidget):
self.listWidget.setCurrentRow(0)
def listEntryText(self, page_dict):
text = str(int(page_dict['Image']) + 1)
if 'Type' in page_dict:
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
text = str(int(page_dict["Image"]) + 1)
if "Type" in page_dict:
text += " (" + self.pageTypeNames[page_dict["Type"]] + ")"
return text
def getPageList(self):
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,12 +14,10 @@
# 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
#import utils
class PageLoader(QtCore.QThread):

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
@ -14,25 +14,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#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
#import utils
from .settings import ComicTaggerSettings
class IDProgressWindow(QtGui.QDialog):
class IDProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(IDProgressWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("progresswindow.ui"), self)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
reduceWidgetFontSize(self.textEdit)

View File

@ -16,28 +16,25 @@
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 comictaggerlib.ui.qtutils import centerWindowOnParent
from . import utils
from .comicarchive import MetaDataStyle
from .filerenamer import FileRenamer
from .settings import ComicTaggerSettings
from .settingswindow import SettingsWindow
class RenameWindow(QtGui.QDialog):
class RenameWindow(QtWidgets.QDialog):
def __init__(self, parent, comic_archive_list, data_style, settings):
super(RenameWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui'), self)
self.label.setText(
"Preview (based on {0} tags):".format(
MetaDataStyle.name[data_style]))
uic.loadUi(ComicTaggerSettings.getUIFile("renamewindow.ui"), self)
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.comic_archive_list = comic_archive_list
@ -50,10 +47,8 @@ class RenameWindow(QtGui.QDialog):
def configRenamer(self):
self.renamer = FileRenamer(None)
self.renamer.setTemplate(self.settings.rename_template)
self.renamer.setIssueZeroPadding(
self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(
self.settings.rename_use_smart_string_cleanup)
self.renamer.setIssueZeroPadding(self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(self.settings.rename_use_smart_string_cleanup)
def doPreview(self):
self.rename_list = []
@ -75,37 +70,49 @@ class RenameWindow(QtGui.QDialog):
if md.isEmpty:
md = ca.metadataFromFilename(self.settings.parse_scan_info)
self.renamer.setMetadata(md)
new_name = self.renamer.determineName(ca.path, ext=new_ext)
self.renamer.move = self.settings.rename_move_dir
try:
new_name = self.renamer.determineName(ca.path, ext=new_ext)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
"<br/><br/>{}<br/><br/>"
"Please consult the template help in the "
"settings and the documentation on the format at "
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
"https://docs.python.org/3/library/string.html#format-string-syntax</a>".format(e),
)
return
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(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
folder_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, folder_item)
folder_item.setText(item_text)
folder_item.setData(QtCore.Qt.ToolTipRole, item_text)
item_text = os.path.split(ca.path)[1]
old_name_item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
old_name_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, old_name_item)
old_name_item.setText(item_text)
old_name_item.setData(QtCore.Qt.ToolTipRole, item_text)
new_name_item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
new_name_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, new_name_item)
new_name_item.setText(new_name)
new_name_item.setData(QtCore.Qt.ToolTipRole, new_name)
dict_item = dict()
dict_item['archive'] = ca
dict_item['new_name'] = new_name
dict_item["archive"] = ca
dict_item["new_name"] = new_name
self.rename_list.append(dict_item)
# Adjust column sizes
@ -128,36 +135,44 @@ class RenameWindow(QtGui.QDialog):
def accept(self):
progdialog = QtGui.QProgressDialog(
"", "Cancel", 0, len(self.rename_list), self)
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.setLabelText(item['new_name'])
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!"
folder = os.path.dirname(os.path.abspath(item["archive"].path))
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
folder = self.settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
if os.path.join(folder, item["new_name"]) == item["archive"].path:
print(item["new_name"], "Filename is already good!")
continue
if not item['archive'].isWritable(check_rar_status=False):
if not item["archive"].isWritable(check_rar_status=False):
continue
folder = os.path.dirname(os.path.abspath(item['archive'].path))
new_abs_path = utils.unique_file(
os.path.join(folder, item['new_name']))
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(item["archive"].path, new_abs_path)
os.rename(item['archive'].path, new_abs_path)
item["archive"].rename(new_abs_path)
item['archive'].rename(new_abs_path)
progdialog.hide()
QtCore.QCoreApplication.processEvents()
progdialog.close()
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)

View File

@ -14,64 +14,59 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import configparser
import platform
import codecs
import configparser
import os
import platform
import sys
import uuid
import utils
from . import utils
class ComicTaggerSettings:
@staticmethod
def getSettingsFolder():
filename_encoding = sys.getfilesystemencoding()
if platform.system() == "Windows":
folder = os.path.join(os.environ['APPDATA'], 'ComicTagger')
folder = os.path.join(os.environ["APPDATA"], "ComicTagger")
else:
folder = os.path.join(os.path.expanduser('~'), '.ComicTagger')
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 defaultLibunrarPath():
return ComicTaggerSettings.baseDir() + "/libunrar.so"
@staticmethod
def haveOwnUnrarLib():
return os.path.exists(ComicTaggerSettings.defaultLibunrarPath())
@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
if getattr(sys, "frozen", None):
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
@staticmethod
def getGraphic(filename):
graphic_folder = os.path.join(
ComicTaggerSettings.baseDir(), 'graphics')
graphic_folder = os.path.join(ComicTaggerSettings.baseDir(), "graphics")
return os.path.join(graphic_folder, filename)
@staticmethod
def getUIFile(filename):
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), 'ui')
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), "ui")
return os.path.join(ui_folder, filename)
def setDefaultValues(self):
# General Settings
self.rar_exe_path = ""
self.unrar_exe_path = ""
self.unrar_lib_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,7 +85,7 @@ 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
@ -107,6 +102,7 @@ class ComicTaggerSettings:
self.clear_form_before_populating_from_cv = False
self.remove_html_tables = False
self.cv_api_key = ""
self.auto_imprint = False
# CBL Tranform settings
@ -121,10 +117,12 @@ class ComicTaggerSettings:
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
self.rename_template = "%series% #%issue% (%year%)"
self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
self.rename_dir = ""
self.rename_move_dir = False
# Auto-tag stickies
self.save_on_low_confidence = False
@ -154,7 +152,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,355 +166,261 @@ class ComicTaggerSettings:
self.rar_exe_path = utils.which("rar")
if self.rar_exe_path != "":
self.save()
if self.rar_exe_path != "":
# make sure rar program is now in the path for the rar class
utils.addtopath(os.path.dirname(self.rar_exe_path))
if self.unrar_exe_path == "":
if platform.system() != "Windows":
# see if it's in the path of unix user
if utils.which("unrar") is not None:
self.unrar_exe_path = utils.which("unrar")
if self.unrar_exe_path != "":
if self.haveOwnUnrarLib():
# We have a 'personal' copy of the unrar lib in the basedir, so
# don't search and change the setting
# NOTE: a manual edit of the settings file overrides this below
os.environ["UNRAR_LIB_PATH"] = self.defaultLibunrarPath()
elif self.unrar_lib_path == "":
# Priority is for unrar lib search is:
# 1. explicit setting in settings file
# 2. UNRAR_LIB_PATH in environment
# 3. check some likely platform specific places
if "UNRAR_LIB_PATH" in os.environ:
self.unrar_lib_path = os.environ["UNRAR_LIB_PATH"]
else:
# look in some platform specific places:
if platform.system() == "Windows":
# Default location for the RARLab DLL installer
if platform.architecture()[0] == "64bit" and os.path.exists("C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll"):
self.unrar_lib_path = "C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll"
elif platform.architecture()[0] == "32bit" and os.path.exists("C:\\Program Files\\UnrarDLL\\UnRAR.dll"):
self.unrar_lib_path = "C:\\Program Files\\UnrarDLL\\UnRAR.dll"
elif platform.system() == "Darwin":
# Look for the brew unrar library
if os.path.exists("/usr/local/lib/libunrar.dylib"):
self.unrar_lib_path = "/usr/local/lib/libunrar.dylib"
elif platform.system() == "Linux":
if os.path.exists("/usr/local/lib/libunrar.so"):
self.unrar_lib_path = "/usr/local/lib/libunrar.so"
elif os.path.exists("/usr/lib/libunrar.so"):
self.unrar_lib_path = "/usr/lib/libunrar.so"
if self.unrar_lib_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.unrar_lib_path != "":
# This needs to occur before the unrar module is loaded for the first time
os.environ["UNRAR_LIB_PATH"] = self.unrar_lib_path
def reset(self):
os.unlink(self.settings_file)
self.__init__()
def load(self):
def readline_generator(f):
line = f.readline()
while line:
yield line
line = f.readline()
#self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
self.config.read_file(
readline_generator(codecs.open(self.settings_file, "r", "utf8")))
# self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
self.config.read_file(readline_generator(codecs.open(self.settings_file, "r", "utf8")))
self.rar_exe_path = self.config.get('settings', 'rar_exe_path')
self.unrar_exe_path = self.config.get('settings', 'unrar_exe_path')
if self.config.has_option('settings', 'check_for_new_version'):
self.check_for_new_version = self.config.getboolean(
'settings', 'check_for_new_version')
if self.config.has_option('settings', 'send_usage_stats'):
self.send_usage_stats = self.config.getboolean(
'settings', 'send_usage_stats')
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
if self.config.has_option("settings", "unrar_lib_path"):
self.unrar_lib_path = self.config.get("settings", "unrar_lib_path")
if self.config.has_option("settings", "check_for_new_version"):
self.check_for_new_version = self.config.getboolean("settings", "check_for_new_version")
if self.config.has_option("settings", "send_usage_stats"):
self.send_usage_stats = self.config.getboolean("settings", "send_usage_stats")
if self.config.has_option('auto', 'install_id'):
self.install_id = self.config.get('auto', 'install_id')
if self.config.has_option('auto', 'last_selected_load_data_style'):
self.last_selected_load_data_style = self.config.getint(
'auto', 'last_selected_load_data_style')
if self.config.has_option('auto', 'last_selected_save_data_style'):
self.last_selected_save_data_style = self.config.getint(
'auto', 'last_selected_save_data_style')
if self.config.has_option('auto', 'last_opened_folder'):
self.last_opened_folder = self.config.get(
'auto', 'last_opened_folder')
if self.config.has_option('auto', 'last_main_window_width'):
self.last_main_window_width = self.config.getint(
'auto', 'last_main_window_width')
if self.config.has_option('auto', 'last_main_window_height'):
self.last_main_window_height = self.config.getint(
'auto', 'last_main_window_height')
if self.config.has_option('auto', 'last_main_window_x'):
self.last_main_window_x = self.config.getint(
'auto', 'last_main_window_x')
if self.config.has_option('auto', 'last_main_window_y'):
self.last_main_window_y = self.config.getint(
'auto', 'last_main_window_y')
if self.config.has_option('auto', 'last_form_side_width'):
self.last_form_side_width = self.config.getint(
'auto', 'last_form_side_width')
if self.config.has_option('auto', 'last_list_side_width'):
self.last_list_side_width = self.config.getint(
'auto', 'last_list_side_width')
if self.config.has_option('auto', 'last_filelist_sorted_column'):
self.last_filelist_sorted_column = self.config.getint(
'auto', 'last_filelist_sorted_column')
if self.config.has_option('auto', 'last_filelist_sorted_order'):
self.last_filelist_sorted_order = self.config.getint(
'auto', 'last_filelist_sorted_order')
if self.config.has_option("auto", "install_id"):
self.install_id = self.config.get("auto", "install_id")
if self.config.has_option("auto", "last_selected_load_data_style"):
self.last_selected_load_data_style = self.config.getint("auto", "last_selected_load_data_style")
if self.config.has_option("auto", "last_selected_save_data_style"):
self.last_selected_save_data_style = self.config.getint("auto", "last_selected_save_data_style")
if self.config.has_option("auto", "last_opened_folder"):
self.last_opened_folder = self.config.get("auto", "last_opened_folder")
if self.config.has_option("auto", "last_main_window_width"):
self.last_main_window_width = self.config.getint("auto", "last_main_window_width")
if self.config.has_option("auto", "last_main_window_height"):
self.last_main_window_height = self.config.getint("auto", "last_main_window_height")
if self.config.has_option("auto", "last_main_window_x"):
self.last_main_window_x = self.config.getint("auto", "last_main_window_x")
if self.config.has_option("auto", "last_main_window_y"):
self.last_main_window_y = self.config.getint("auto", "last_main_window_y")
if self.config.has_option("auto", "last_form_side_width"):
self.last_form_side_width = self.config.getint("auto", "last_form_side_width")
if self.config.has_option("auto", "last_list_side_width"):
self.last_list_side_width = self.config.getint("auto", "last_list_side_width")
if self.config.has_option("auto", "last_filelist_sorted_column"):
self.last_filelist_sorted_column = self.config.getint("auto", "last_filelist_sorted_column")
if self.config.has_option("auto", "last_filelist_sorted_order"):
self.last_filelist_sorted_order = self.config.getint("auto", "last_filelist_sorted_order")
if self.config.has_option('identifier', 'id_length_delta_thresh'):
self.id_length_delta_thresh = self.config.getint(
'identifier', 'id_length_delta_thresh')
if self.config.has_option('identifier', 'id_publisher_blacklist'):
self.id_publisher_blacklist = self.config.get(
'identifier', 'id_publisher_blacklist')
if self.config.has_option("identifier", "id_length_delta_thresh"):
self.id_length_delta_thresh = self.config.getint("identifier", "id_length_delta_thresh")
if self.config.has_option("identifier", "id_publisher_blacklist"):
self.id_publisher_blacklist = self.config.get("identifier", "id_publisher_blacklist")
if self.config.has_option('filenameparser', 'parse_scan_info'):
self.parse_scan_info = self.config.getboolean(
'filenameparser', 'parse_scan_info')
if self.config.has_option("filenameparser", "parse_scan_info"):
self.parse_scan_info = self.config.getboolean("filenameparser", "parse_scan_info")
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
self.ask_about_cbi_in_rar = self.config.getboolean(
'dialogflags', 'ask_about_cbi_in_rar')
if self.config.has_option('dialogflags', 'show_disclaimer'):
self.show_disclaimer = self.config.getboolean(
'dialogflags', 'show_disclaimer')
if self.config.has_option(
'dialogflags', 'dont_notify_about_this_version'):
self.dont_notify_about_this_version = self.config.get(
'dialogflags', 'dont_notify_about_this_version')
if self.config.has_option('dialogflags', 'ask_about_usage_stats'):
self.ask_about_usage_stats = self.config.getboolean(
'dialogflags', 'ask_about_usage_stats')
if self.config.has_option('dialogflags', 'show_no_unrar_warning'):
self.show_no_unrar_warning = self.config.getboolean(
'dialogflags', 'show_no_unrar_warning')
if self.config.has_option("dialogflags", "ask_about_cbi_in_rar"):
self.ask_about_cbi_in_rar = self.config.getboolean("dialogflags", "ask_about_cbi_in_rar")
if self.config.has_option("dialogflags", "show_disclaimer"):
self.show_disclaimer = self.config.getboolean("dialogflags", "show_disclaimer")
if self.config.has_option("dialogflags", "dont_notify_about_this_version"):
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
if self.config.has_option("dialogflags", "show_no_unrar_warning"):
self.show_no_unrar_warning = self.config.getboolean("dialogflags", "show_no_unrar_warning")
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
self.use_series_start_as_volume = self.config.getboolean(
'comicvine', 'use_series_start_as_volume')
if self.config.has_option(
'comicvine', 'clear_form_before_populating_from_cv'):
self.clear_form_before_populating_from_cv = self.config.getboolean(
'comicvine', 'clear_form_before_populating_from_cv')
if self.config.has_option('comicvine', 'remove_html_tables'):
self.remove_html_tables = self.config.getboolean(
'comicvine', 'remove_html_tables')
if self.config.has_option('comicvine', 'cv_api_key'):
self.cv_api_key = self.config.get('comicvine', 'cv_api_key')
if self.config.has_option("comicvine", "use_series_start_as_volume"):
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
if self.config.has_option("comicvine", "clear_form_before_populating_from_cv"):
self.clear_form_before_populating_from_cv = self.config.getboolean("comicvine", "clear_form_before_populating_from_cv")
if self.config.has_option("comicvine", "remove_html_tables"):
self.remove_html_tables = self.config.getboolean("comicvine", "remove_html_tables")
if self.config.has_option("comicvine", "cv_api_key"):
self.cv_api_key = self.config.get("comicvine", "cv_api_key")
if self.config.has_option(
'cbl_transform', 'assume_lone_credit_is_primary'):
self.assume_lone_credit_is_primary = self.config.getboolean(
'cbl_transform', 'assume_lone_credit_is_primary')
if self.config.has_option('cbl_transform', 'copy_characters_to_tags'):
self.copy_characters_to_tags = self.config.getboolean(
'cbl_transform', 'copy_characters_to_tags')
if self.config.has_option('cbl_transform', 'copy_teams_to_tags'):
self.copy_teams_to_tags = self.config.getboolean(
'cbl_transform', 'copy_teams_to_tags')
if self.config.has_option('cbl_transform', 'copy_locations_to_tags'):
self.copy_locations_to_tags = self.config.getboolean(
'cbl_transform', 'copy_locations_to_tags')
if self.config.has_option('cbl_transform', 'copy_notes_to_comments'):
self.copy_notes_to_comments = self.config.getboolean(
'cbl_transform', 'copy_notes_to_comments')
if self.config.has_option('cbl_transform', 'copy_storyarcs_to_tags'):
self.copy_storyarcs_to_tags = self.config.getboolean(
'cbl_transform', 'copy_storyarcs_to_tags')
if self.config.has_option('cbl_transform', 'copy_weblink_to_comments'):
self.copy_weblink_to_comments = self.config.getboolean(
'cbl_transform', 'copy_weblink_to_comments')
if self.config.has_option(
'cbl_transform', 'apply_cbl_transform_on_cv_import'):
self.apply_cbl_transform_on_cv_import = self.config.getboolean(
'cbl_transform', 'apply_cbl_transform_on_cv_import')
if self.config.has_option(
'cbl_transform', 'apply_cbl_transform_on_bulk_operation'):
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean(
'cbl_transform',
'apply_cbl_transform_on_bulk_operation')
if self.config.has_option("cbl_transform", "assume_lone_credit_is_primary"):
self.assume_lone_credit_is_primary = self.config.getboolean("cbl_transform", "assume_lone_credit_is_primary")
if self.config.has_option("cbl_transform", "copy_characters_to_tags"):
self.copy_characters_to_tags = self.config.getboolean("cbl_transform", "copy_characters_to_tags")
if self.config.has_option("cbl_transform", "copy_teams_to_tags"):
self.copy_teams_to_tags = self.config.getboolean("cbl_transform", "copy_teams_to_tags")
if self.config.has_option("cbl_transform", "copy_locations_to_tags"):
self.copy_locations_to_tags = self.config.getboolean("cbl_transform", "copy_locations_to_tags")
if self.config.has_option("cbl_transform", "copy_notes_to_comments"):
self.copy_notes_to_comments = self.config.getboolean("cbl_transform", "copy_notes_to_comments")
if self.config.has_option("cbl_transform", "copy_storyarcs_to_tags"):
self.copy_storyarcs_to_tags = self.config.getboolean("cbl_transform", "copy_storyarcs_to_tags")
if self.config.has_option("cbl_transform", "copy_weblink_to_comments"):
self.copy_weblink_to_comments = self.config.getboolean("cbl_transform", "copy_weblink_to_comments")
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_cv_import"):
self.apply_cbl_transform_on_cv_import = self.config.getboolean("cbl_transform", "apply_cbl_transform_on_cv_import")
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_bulk_operation"):
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean("cbl_transform", "apply_cbl_transform_on_bulk_operation")
if self.config.has_option('rename', 'rename_template'):
self.rename_template = self.config.get('rename', 'rename_template')
if self.config.has_option('rename', 'rename_issue_number_padding'):
self.rename_issue_number_padding = self.config.getint(
'rename', 'rename_issue_number_padding')
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
self.rename_use_smart_string_cleanup = self.config.getboolean(
'rename', 'rename_use_smart_string_cleanup')
if self.config.has_option(
'rename', 'rename_extension_based_on_archive'):
self.rename_extension_based_on_archive = self.config.getboolean(
'rename', 'rename_extension_based_on_archive')
if self.config.has_option("rename", "rename_template"):
self.rename_template = self.config.get("rename", "rename_template")
if self.config.has_option("rename", "rename_issue_number_padding"):
self.rename_issue_number_padding = self.config.getint("rename", "rename_issue_number_padding")
if self.config.has_option("rename", "rename_use_smart_string_cleanup"):
self.rename_use_smart_string_cleanup = self.config.getboolean("rename", "rename_use_smart_string_cleanup")
if self.config.has_option("rename", "rename_extension_based_on_archive"):
self.rename_extension_based_on_archive = self.config.getboolean("rename", "rename_extension_based_on_archive")
if self.config.has_option("rename", "rename_dir"):
self.rename_dir = self.config.get("rename", "rename_dir")
if self.config.has_option("rename", "rename_move_dir"):
self.rename_move_dir = self.config.getboolean("rename", "rename_move_dir")
if self.config.has_option('autotag', 'save_on_low_confidence'):
self.save_on_low_confidence = self.config.getboolean(
'autotag', 'save_on_low_confidence')
if self.config.has_option('autotag', 'dont_use_year_when_identifying'):
self.dont_use_year_when_identifying = self.config.getboolean(
'autotag', 'dont_use_year_when_identifying')
if self.config.has_option('autotag', 'assume_1_if_no_issue_num'):
self.assume_1_if_no_issue_num = self.config.getboolean(
'autotag', 'assume_1_if_no_issue_num')
if self.config.has_option(
'autotag', 'ignore_leading_numbers_in_filename'):
self.ignore_leading_numbers_in_filename = self.config.getboolean(
'autotag', 'ignore_leading_numbers_in_filename')
if self.config.has_option(
'autotag', 'remove_archive_after_successful_match'):
self.remove_archive_after_successful_match = self.config.getboolean(
'autotag',
'remove_archive_after_successful_match')
if self.config.has_option('autotag', 'wait_and_retry_on_rate_limit'):
self.wait_and_retry_on_rate_limit = self.config.getboolean(
'autotag', 'wait_and_retry_on_rate_limit')
if self.config.has_option("autotag", "save_on_low_confidence"):
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
if self.config.has_option("autotag", "dont_use_year_when_identifying"):
self.dont_use_year_when_identifying = self.config.getboolean("autotag", "dont_use_year_when_identifying")
if self.config.has_option("autotag", "assume_1_if_no_issue_num"):
self.assume_1_if_no_issue_num = self.config.getboolean("autotag", "assume_1_if_no_issue_num")
if self.config.has_option("autotag", "ignore_leading_numbers_in_filename"):
self.ignore_leading_numbers_in_filename = self.config.getboolean("autotag", "ignore_leading_numbers_in_filename")
if self.config.has_option("autotag", "remove_archive_after_successful_match"):
self.remove_archive_after_successful_match = self.config.getboolean("autotag", "remove_archive_after_successful_match")
if self.config.has_option("autotag", "wait_and_retry_on_rate_limit"):
self.wait_and_retry_on_rate_limit = self.config.getboolean("autotag", "wait_and_retry_on_rate_limit")
if self.config.has_option("autotag", "auto_imprint"):
self.auto_imprint = self.config.getboolean("autotag", "auto_imprint")
def save(self):
if not self.config.has_section('settings'):
self.config.add_section('settings')
if not self.config.has_section("settings"):
self.config.add_section("settings")
self.config.set(
'settings', 'check_for_new_version', self.check_for_new_version)
self.config.set('settings', 'rar_exe_path', self.rar_exe_path)
self.config.set('settings', 'unrar_exe_path', self.unrar_exe_path)
self.config.set('settings', 'send_usage_stats', self.send_usage_stats)
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_lib_path", self.unrar_lib_path)
self.config.set("settings", "send_usage_stats", self.send_usage_stats)
if not self.config.has_section('auto'):
self.config.add_section('auto')
if not self.config.has_section("auto"):
self.config.add_section("auto")
self.config.set('auto', 'install_id', self.install_id)
self.config.set(
'auto',
'last_selected_load_data_style',
self.last_selected_load_data_style)
self.config.set(
'auto',
'last_selected_save_data_style',
self.last_selected_save_data_style)
self.config.set('auto', 'last_opened_folder', self.last_opened_folder)
self.config.set(
'auto', 'last_main_window_width', self.last_main_window_width)
self.config.set(
'auto', 'last_main_window_height', self.last_main_window_height)
self.config.set('auto', 'last_main_window_x', self.last_main_window_x)
self.config.set('auto', 'last_main_window_y', self.last_main_window_y)
self.config.set(
'auto', 'last_form_side_width', self.last_form_side_width)
self.config.set(
'auto', 'last_list_side_width', self.last_list_side_width)
self.config.set(
'auto',
'last_filelist_sorted_column',
self.last_filelist_sorted_column)
self.config.set(
'auto',
'last_filelist_sorted_order',
self.last_filelist_sorted_order)
self.config.set("auto", "install_id", self.install_id)
self.config.set("auto", "last_selected_load_data_style", self.last_selected_load_data_style)
self.config.set("auto", "last_selected_save_data_style", self.last_selected_save_data_style)
self.config.set("auto", "last_opened_folder", self.last_opened_folder)
self.config.set("auto", "last_main_window_width", self.last_main_window_width)
self.config.set("auto", "last_main_window_height", self.last_main_window_height)
self.config.set("auto", "last_main_window_x", self.last_main_window_x)
self.config.set("auto", "last_main_window_y", self.last_main_window_y)
self.config.set("auto", "last_form_side_width", self.last_form_side_width)
self.config.set("auto", "last_list_side_width", self.last_list_side_width)
self.config.set("auto", "last_filelist_sorted_column", self.last_filelist_sorted_column)
self.config.set("auto", "last_filelist_sorted_order", self.last_filelist_sorted_order)
if not self.config.has_section('identifier'):
self.config.add_section('identifier')
if not self.config.has_section("identifier"):
self.config.add_section("identifier")
self.config.set(
'identifier',
'id_length_delta_thresh',
self.id_length_delta_thresh)
self.config.set(
'identifier',
'id_publisher_blacklist',
self.id_publisher_blacklist)
self.config.set("identifier", "id_length_delta_thresh", self.id_length_delta_thresh)
self.config.set("identifier", "id_publisher_blacklist", self.id_publisher_blacklist)
if not self.config.has_section('dialogflags'):
self.config.add_section('dialogflags')
if not self.config.has_section("dialogflags"):
self.config.add_section("dialogflags")
self.config.set(
'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar)
self.config.set('dialogflags', 'show_disclaimer', self.show_disclaimer)
self.config.set(
'dialogflags',
'dont_notify_about_this_version',
self.dont_notify_about_this_version)
self.config.set(
'dialogflags', 'ask_about_usage_stats', self.ask_about_usage_stats)
self.config.set(
'dialogflags', 'show_no_unrar_warning', self.show_no_unrar_warning)
self.config.set("dialogflags", "ask_about_cbi_in_rar", self.ask_about_cbi_in_rar)
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
self.config.set("dialogflags", "show_no_unrar_warning", self.show_no_unrar_warning)
if not self.config.has_section('filenameparser'):
self.config.add_section('filenameparser')
if not self.config.has_section("filenameparser"):
self.config.add_section("filenameparser")
self.config.set(
'filenameparser', 'parse_scan_info', self.parse_scan_info)
self.config.set("filenameparser", "parse_scan_info", self.parse_scan_info)
if not self.config.has_section('comicvine'):
self.config.add_section('comicvine')
if not self.config.has_section("comicvine"):
self.config.add_section("comicvine")
self.config.set(
'comicvine',
'use_series_start_as_volume',
self.use_series_start_as_volume)
self.config.set('comicvine', 'clear_form_before_populating_from_cv',
self.clear_form_before_populating_from_cv)
self.config.set(
'comicvine', 'remove_html_tables', self.remove_html_tables)
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
self.config.set("comicvine", "use_series_start_as_volume", self.use_series_start_as_volume)
self.config.set("comicvine", "clear_form_before_populating_from_cv", self.clear_form_before_populating_from_cv)
self.config.set("comicvine", "remove_html_tables", self.remove_html_tables)
self.config.set("comicvine", "cv_api_key", self.cv_api_key)
if not self.config.has_section('cbl_transform'):
self.config.add_section('cbl_transform')
if not self.config.has_section("cbl_transform"):
self.config.add_section("cbl_transform")
self.config.set(
'cbl_transform',
'assume_lone_credit_is_primary',
self.assume_lone_credit_is_primary)
self.config.set(
'cbl_transform',
'copy_characters_to_tags',
self.copy_characters_to_tags)
self.config.set(
'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags)
self.config.set(
'cbl_transform',
'copy_locations_to_tags',
self.copy_locations_to_tags)
self.config.set(
'cbl_transform',
'copy_storyarcs_to_tags',
self.copy_storyarcs_to_tags)
self.config.set(
'cbl_transform',
'copy_notes_to_comments',
self.copy_notes_to_comments)
self.config.set(
'cbl_transform',
'copy_weblink_to_comments',
self.copy_weblink_to_comments)
self.config.set(
'cbl_transform',
'apply_cbl_transform_on_cv_import',
self.apply_cbl_transform_on_cv_import)
self.config.set(
'cbl_transform',
'apply_cbl_transform_on_bulk_operation',
self.apply_cbl_transform_on_bulk_operation)
self.config.set("cbl_transform", "assume_lone_credit_is_primary", self.assume_lone_credit_is_primary)
self.config.set("cbl_transform", "copy_characters_to_tags", self.copy_characters_to_tags)
self.config.set("cbl_transform", "copy_teams_to_tags", self.copy_teams_to_tags)
self.config.set("cbl_transform", "copy_locations_to_tags", self.copy_locations_to_tags)
self.config.set("cbl_transform", "copy_storyarcs_to_tags", self.copy_storyarcs_to_tags)
self.config.set("cbl_transform", "copy_notes_to_comments", self.copy_notes_to_comments)
self.config.set("cbl_transform", "copy_weblink_to_comments", self.copy_weblink_to_comments)
self.config.set("cbl_transform", "apply_cbl_transform_on_cv_import", self.apply_cbl_transform_on_cv_import)
self.config.set("cbl_transform", "apply_cbl_transform_on_bulk_operation", self.apply_cbl_transform_on_bulk_operation)
if not self.config.has_section('rename'):
self.config.add_section('rename')
if not self.config.has_section("rename"):
self.config.add_section("rename")
self.config.set('rename', 'rename_template', self.rename_template)
self.config.set(
'rename',
'rename_issue_number_padding',
self.rename_issue_number_padding)
self.config.set(
'rename',
'rename_use_smart_string_cleanup',
self.rename_use_smart_string_cleanup)
self.config.set('rename', 'rename_extension_based_on_archive',
self.rename_extension_based_on_archive)
self.config.set("rename", "rename_template", self.rename_template)
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
self.config.set("rename", "rename_extension_based_on_archive", self.rename_extension_based_on_archive)
self.config.set("rename", "rename_dir", self.rename_dir)
self.config.set("rename", "rename_move_dir", self.rename_move_dir)
if not self.config.has_section('autotag'):
self.config.add_section('autotag')
self.config.set(
'autotag', 'save_on_low_confidence', self.save_on_low_confidence)
self.config.set(
'autotag',
'dont_use_year_when_identifying',
self.dont_use_year_when_identifying)
self.config.set(
'autotag',
'assume_1_if_no_issue_num',
self.assume_1_if_no_issue_num)
self.config.set('autotag', 'ignore_leading_numbers_in_filename',
self.ignore_leading_numbers_in_filename)
self.config.set('autotag', 'remove_archive_after_successful_match',
self.remove_archive_after_successful_match)
self.config.set(
'autotag',
'wait_and_retry_on_rate_limit',
self.wait_and_retry_on_rate_limit)
if not self.config.has_section("autotag"):
self.config.add_section("autotag")
self.config.set("autotag", "save_on_low_confidence", self.save_on_low_confidence)
self.config.set("autotag", "dont_use_year_when_identifying", self.dont_use_year_when_identifying)
self.config.set("autotag", "assume_1_if_no_issue_num", self.assume_1_if_no_issue_num)
self.config.set("autotag", "ignore_leading_numbers_in_filename", self.ignore_leading_numbers_in_filename)
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
self.config.set("autotag", "auto_imprint", self.auto_imprint)
with codecs.open(self.settings_file, 'wb', 'utf8') as configfile:
with codecs.open(self.settings_file, "wb", "utf8") as configfile:
self.config.write(configfile)
# make sure the basedir is cached, in case we're on Windows running a
# script from frozen binary
ComicTaggerSettings.baseDir()

View File

@ -14,90 +14,124 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import platform
import os
import platform
import sys
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from comicvinetalker import ComicVineTalker
from imagefetcher import ImageFetcher
import utils
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from . import utils
from .comicvinecacher import ComicVineCacher
from .comicvinetalker import ComicVineTalker
from .filerenamer import FileRenamer
from .genericmetadata import GenericMetadata
from .imagefetcher import ImageFetcher
from .settings import ComicTaggerSettings
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>
"""
windowsUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need to have the unrar DLL from
<span style=" text-decoration: underline; color:#0000ff;">
<a href="https://www.rarlab.com/rar_add.htm">
RARLab</a></span> installed. </p></body></html>
"""
linuxUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need to have the unrar library from RARLab installed.
Look <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://github.com/beville/libunrar-binaries/releases">here</a></span>
for pre-compiled binaries, or <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://www.rarlab.com/rar_add.htm">here</a></span>
for the UnRAR source (which is easy to compile on Linux). </p></body></html>
"""
macUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need the unrar library. The easiest way to get this is
to install <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://brew.sh/homebrew">homebrew</a></span>.
</p>Once homebrew is installed, run: <b>brew install unrar</b></body></html>
"""
class SettingsWindow(QtGui.QDialog):
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent, settings):
super(SettingsWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("settingswindow.ui"), self)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.name = "Settings"
self.priorUnrarLibPath = self.settings.unrar_lib_path
if self.settings.haveOwnUnrarLib():
# We have our own unrarlib, so no need for this GUI
self.grpBoxUnrar.hide()
if platform.system() == "Windows":
self.lblUnrar.hide()
self.leUnrarExePath.hide()
self.btnBrowseUnrar.hide()
self.lblRarHelp.setText(windowsRarHelp)
self.lblUnrarHelp.setText(windowsUnrarHelp)
elif platform.system() == "Linux":
self.lblRarHelp.setText(linuxRarHelp)
self.lblUnrarHelp.setText(linuxUnrarHelp)
elif platform.system() == "Darwin":
# Mac file dialog hides "/usr" and others, so allow user to type
self.leUnrarLibPath.setReadOnly(False)
self.leRarExePath.setReadOnly(False)
self.lblRarHelp.setText(macRarHelp)
self.lblUnrarHelp.setText(macUnrarHelp)
self.name = "Preferences"
self.setWindowTitle("ComicTagger " + self.name)
self.lblDefaultSettings.setText(
"Revert to default " + self.name.lower())
self.lblDefaultSettings.setText("Revert to default " + self.name.lower())
self.btnResetSettings.setText("Default " + self.name)
nldtTip = (
"""<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
nldtTip = """<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
search matches that are too long compared to your series name search. The higher
it is, the more likely to have a good match, but each search will take longer and
use more bandwidth. Too low, and only the very closest lexical matches will be
explored.</html>""")
explored.</html>"""
self.leNameLengthDeltaThresh.setToolTip(nldtTip)
pblTip = (
"""<html>
pblTip = """<html>
The <b>Publisher Blacklist</b> is for eliminating automatic matches to certain publishers
that you know are incorrect. Useful for avoiding international re-prints with same
covers or series names. Enter publisher names separated by commas.
</html>"""
)
self.tePublisherBlacklist.setToolTip(pblTip)
validator = QtGui.QIntValidator(1, 4, self)
@ -113,16 +147,74 @@ class SettingsWindow(QtGui.QDialog):
self.btnClearCache.clicked.connect(self.clearCache)
self.btnResetSettings.clicked.connect(self.resetSettings)
self.btnTestKey.clicked.connect(self.testAPIKey)
self.btnTemplateHelp.clicked.connect(self.showTemplateHelp)
def configRenamer(self):
md = GenericMetadata()
md.isEmpty = False
md.tagOrigin = "testing"
md.series = "series name"
md.issue = "1"
md.title = "issue title"
md.publisher = "lordwelch"
md.month = 4
md.year = 1998
md.day = 4
md.issueCount = 1
md.volume = 256
md.genre = "test"
md.language = "en" # 2 letter iso code
md.comments = "This is definitly not something you want to read." # use same way as Summary in CIX
md.volumeCount = 4096
md.criticalRating = "Worst Comic Ever"
md.country = "US"
md.alternateSeries = "None"
md.alternateNumber = 4.4
md.alternateCount = 4444
md.imprint = "Welch Publishing"
md.notes = "This doesn't actually exist"
md.webLink = "https://example.com/series name/1"
md.format = "Box Set"
md.manga = "Yes"
md.blackAndWhite = False
md.pageCount = 4
md.maturityRating = "Everyone"
md.storyArc = "None of your buisness"
md.seriesGroup = "Advertures of buisness"
md.scanInfo = "(lordwelch)"
md.characters = "lordwelch, Welch"
md.teams = "None"
md.locations = "Earth, 444 B.C."
md.credits = [dict({"role": "Everything", "person": "lordwelch", "primary": True})]
md.tags = ["testing", "not testing", "fake"]
md.pages = [dict({"Image": "0", "Type": "Front Cover"}), dict({"Image": "1", "Type": "Story"})]
# Some CoMet-only items
md.price = 0.00
md.isVersionOf = "SERIES #1"
md.rights = "None"
md.identifier = "LW4444-Comic"
md.lastMark = "0"
md.coverImage = "https://example.com/series name/1/cover"
self.renamer = FileRenamer(md)
self.renamer.setTemplate(str(self.leRenameTemplate.text()))
self.renamer.setIssueZeroPadding(self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(self.settings.rename_use_smart_string_cleanup)
def settingsToForm(self):
# 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(
self.settings.id_publisher_blacklist)
self.leUnrarLibPath.setText(self.settings.unrar_lib_path)
self.leNameLengthDeltaThresh.setText(str(self.settings.id_length_delta_thresh))
self.tePublisherBlacklist.setPlainText(self.settings.id_publisher_blacklist)
if self.settings.check_for_new_version:
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
@ -153,29 +245,53 @@ class SettingsWindow(QtGui.QDialog):
if self.settings.copy_weblink_to_comments:
self.cbxCopyWebLinkToComments.setCheckState(QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_cv_import:
self.cbxApplyCBLTransformOnCVIMport.setCheckState(
QtCore.Qt.Checked)
self.cbxApplyCBLTransformOnCVIMport.setCheckState(QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_bulk_operation:
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(
QtCore.Qt.Checked)
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(QtCore.Qt.Checked)
self.leRenameTemplate.setText(self.settings.rename_template)
self.leIssueNumPadding.setText(
str(self.settings.rename_issue_number_padding))
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
if self.settings.rename_use_smart_string_cleanup:
self.cbxSmartCleanup.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_extension_based_on_archive:
self.cbxChangeExtension.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_move_dir:
self.cbxMoveFiles.setCheckState(QtCore.Qt.Checked)
self.leDirectory.setText(self.settings.rename_dir)
def accept(self):
self.configRenamer()
try:
new_name = self.renamer.determineName("test.cbz")
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
"<br/><br/>{}<br/><br/>"
"Please consult the template help in the "
"settings and the documentation on the format at "
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
"https://docs.python.org/3/library/string.html#format-string-syntax</a>".format(e),
)
return
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
# make sure 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))
# Don't accept the form info if we have our own unrar lib
if not self.settings.haveOwnUnrarLib():
self.settings.unrar_lib_path = str(self.leUnrarLibPath.text())
# 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 self.settings.unrar_lib_path:
os.environ["UNRAR_LIB_PATH"] = self.settings.unrar_lib_path
# This doesn't do anything... we need to restart!
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
self.leNameLengthDeltaThresh.setText("0")
@ -185,18 +301,16 @@ class SettingsWindow(QtGui.QDialog):
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
self.settings.id_length_delta_thresh = int(
self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_blacklist = str(
self.tePublisherBlacklist.toPlainText())
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
self.settings.parse_scan_info = self.cbxParseScanInfo.isChecked()
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
self.settings.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()
@ -208,53 +322,50 @@ class SettingsWindow(QtGui.QDialog):
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.settings.rename_template = str(self.leRenameTemplate.text())
self.settings.rename_issue_number_padding = int(
self.leIssueNumPadding.text())
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
self.settings.rename_dir = self.leDirectory.text()
self.settings.save()
QtGui.QDialog.accept(self)
QtWidgets.QDialog.accept(self)
if self.priorUnrarLibPath != self.settings.unrar_lib_path:
QtWidgets.QMessageBox.information(self, "UnRar Library Change", "ComicTagger will need to be restarted for changes to take effect.")
def selectRar(self):
self.selectFile(self.leRarExePath, "RAR")
def selectUnrar(self):
self.selectFile(self.leUnrarExePath, "UnRAR")
self.selectFile(self.leUnrarLibPath, "UnRAR")
def clearCache(self):
ImageFetcher().clearCache()
ComicVineCacher().clearCache()
QtGui.QMessageBox.information(
self, self.name, "Cache has been cleared.")
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
def testAPIKey(self):
if ComicVineTalker().testKey(unicode(self.leKey.text())):
QtGui.QMessageBox.information(
self, "API Key Test", "Key is valid!")
if ComicVineTalker().testKey(str(self.leKey.text()).strip()):
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
else:
QtGui.QMessageBox.warning(
self, "API Key Test", "Key is NOT valid.")
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
def resetSettings(self):
self.settings.reset()
self.settingsToForm()
QtGui.QMessageBox.information(
self,
self.name,
self.name +
" have been returned to default values.")
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
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,11 +373,26 @@ 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_()):
if dialog.exec_():
fileList = dialog.selectedFiles()
control.setText(str(fileList[0]))
def showRenameTab(self):
self.tabWidget.setCurrentIndex(5)
def showTemplateHelp(self):
TemplateHelpWin = TemplateHelpWindow(self)
TemplateHelpWin.setModal(False)
TemplateHelpWin.show()
class TemplateHelpWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(TemplateHelpWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("TemplateHelp.ui"), self)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>702</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>Template Help</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item>
<widget class="QTextBrowser" name="textBrowser">
<property name="html">
<string>&lt;html&gt;
&lt;head/&gt;
&lt;body&gt;
&lt;h1 style=&quot;text-align: center&quot;&gt;Template help&lt;/h1&gt;
&lt;p&gt;The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
&lt;a href=&quot;https://docs.python.org/3/library/string.html#format-string-syntax&quot;&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;Accepts the following variables:
{isEmpty}&#009;&#009;(boolean)
{tagOrigin}&#009;&#009;(string)
{series}&#009;&#009;(string)
{issue}&#009;&#009;(string)
{title}&#009;&#009;(string)
{publisher}&#009;&#009;(string)
{month}&#009;&#009;(integer)
{year}&#009;&#009;(integer)
{day}&#009;&#009;(integer)
{issueCount}&#009;(integer)
{volume}&#009;&#009;(integer)
{genre}&#009;&#009;(string)
{language}&#009;&#009;(string)
{comments}&#009;&#009;(string)
{volumeCount}&#009;(integer)
{criticalRating}&#009;(string)
{country}&#009;&#009;(string)
{alternateSeries}&#009;(string)
{alternateNumber}&#009;(string)
{alternateCount}&#009;(integer)
{imprint}&#009;&#009;(string)
{notes}&#009;&#009;(string)
{webLink}&#009;&#009;(string)
{format}&#009;&#009;(string)
{manga}&#009;&#009;(string)
{blackAndWhite}&#009;(boolean)
{pageCount}&#009;&#009;(integer)
{maturityRating}&#009;(string)
{storyArc}&#009;&#009;(string)
{seriesGroup}&#009;(string)
{scanInfo}&#009;&#009;(string)
{characters}&#009;(string)
{teams}&#009;&#009;(string)
{locations}&#009;&#009;(string)
{credits}&#009;&#009;(list of dict({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean}))
{tags}&#009;&#009;(list of str)
{pages}&#009;&#009;(list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;}))
CoMet-only items:
{price}&#009;&#009;(float)
{isVersionOf}&#009;(string)
{rights}&#009;&#009;(string)
{identifier}&#009;(string)
{lastMark}&#009;&#009;(string)
{coverImage}&#009;(string)
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -44,7 +44,7 @@
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="6" column="0">
<item row="7" column="0">
<widget class="QCheckBox" name="cbxSpecifySearchString">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@ -129,6 +129,16 @@
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxAutoImprint">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Checks the publisher against a list of imprints.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Auto Imprint</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLineEdit" name="leNameLengthMatchTolerance">
<property name="sizePolicy">
@ -145,7 +155,7 @@
</property>
</widget>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QLineEdit" name="leSearchString">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -155,7 +165,7 @@
</property>
</widget>
</item>
<item row="8" column="0">
<item row="9" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">

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

@ -1,14 +1,14 @@
"""Some utilities for the GUI"""
#import StringIO
# import StringIO
#from PIL import Image
# from PIL import Image
from comictaggerlib.settings import ComicTaggerSettings
try:
from PyQt4 import QtGui
from PyQt5 import QtGui
qt_available = True
except ImportError:
qt_available = False
@ -55,16 +55,13 @@ if qt_available:
# And vertical position the same, but with the height dimensions
vpos = (main_window_size.height() - window.height()) / 2
# And the move call repositions the window
window.move(
hpos +
main_window_size.left(),
vpos +
main_window_size.top())
window.move(hpos + main_window_size.left(), vpos + main_window_size.top())
try:
from PIL import Image
from PIL import WebPImagePlugin
import StringIO
import io
from PIL import Image, WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
@ -78,14 +75,13 @@ 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
if not success:
img.load(ComicTaggerSettings.getGraphic('nocover.png'))
img.load(ComicTaggerSettings.getGraphic("nocover.png"))
return img

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">
@ -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">
@ -615,7 +503,7 @@
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label">
<widget class="QLabel" name="lblTemplate">
<property name="text">
<string>Template:</string>
</property>
@ -624,31 +512,78 @@
<item row="1" column="1">
<widget class="QLineEdit" name="leRenameTemplate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%month%&lt;br/&gt;%month_name%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;br/&gt;
%genre%&lt;br/&gt;
%language_code%&lt;br/&gt;
%criticalrating%&lt;br/&gt;
%alternateseries%&lt;br/&gt;
%alternatenumber%&lt;br/&gt;
%alternatecount%&lt;br/&gt;
%imprint%&lt;br/&gt;
%format%&lt;br/&gt;
%maturityrating%&lt;br/&gt;
%storyarc%&lt;br/&gt;
%seriesgroup%&lt;br/&gt;
%scaninfo%
&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;pre&gt;The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
Accepts the following variables:
{isEmpty} (boolean)
{tagOrigin} (string)
{series} (string)
{issue} (string)
{title} (string)
{publisher} (string)
{month} (integer)
{year} (integer)
{day} (integer)
{issueCount} (integer)
{volume} (integer)
{genre} (string)
{language} (string)
{comments} (string)
{volumeCount} (integer)
{criticalRating} (string)
{country} (string)
{alternateSeries} (string)
{alternateNumber} (string)
{alternateCount} (integer)
{imprint} (string)
{notes} (string)
{webLink} (string)
{format} (string)
{manga} (string)
{blackAndWhite} (boolean)
{pageCount} (integer)
{maturityRating} (string)
{storyArc} (string)
{seriesGroup} (string)
{scanInfo} (string)
{characters} (string)
{teams} (string)
{locations} (string)
{credits} (list of dict({'role': 'str', 'person': 'str', 'primary': boolean}))
{tags} (list of str)
{pages} (list of dict({'Image': 'str(int)', 'Type': 'str'}))
CoMet-only items:
{price} (float)
{isVersionOf} (string)
{rights} (string)
{identifier} (string)
{lastMark} (string)
{coverImage} (string)
Examples:
{series} {issue} ({year})
{series} #{issue} - {title}
&lt;/pre&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<widget class="QPushButton" name="btnTemplateHelp">
<property name="text">
<string>Template Help</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblPadding">
<property name="text">
<string>Issue # Zero Padding</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="QLineEdit" name="leIssueNumPadding">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -667,7 +602,7 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="cbxSmartCleanup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -677,17 +612,203 @@
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="cbxChangeExtension">
<property name="text">
<string>Change Extension Based On Archive Type</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxMoveFiles">
<property name="toolTip">
<string>If checked moves files to specified folder</string>
</property>
<property name="text">
<string>Move files when renaming</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="lblDirectory">
<property name="text">
<string>Destination Directory:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="leDirectory"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_7">
<attribute name="title">
<string>RAR Tools</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="grpBoxUnrar">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="lblUnrar">
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>UnRAR library</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leUnrarLibPath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnBrowseUnrar">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="lblUnrarHelp">
<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 CBR/RAR archives, you will need to have the unrar library 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>
<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="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 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

@ -512,6 +512,26 @@
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="leSeriesPubYear"/>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Series Year</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QPushButton" name="btnAutoImprint">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Checks the publisher against a list of imprints.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Auto Imprint</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -1176,8 +1196,10 @@
<addaction name="actionParse_Filename"/>
<addaction name="actionSearchOnline"/>
<addaction name="actionAutoIdentify"/>
<addaction name="actionLiteralSearch"/>
<addaction name="separator"/>
<addaction name="actionApplyCBLTransform"/>
<addaction name="actionMarkAd"/>
</widget>
<widget class="QMenu" name="menuWindow">
<property name="title">
@ -1373,6 +1395,22 @@
<string>Search online for tags,auto-identify best match, and save to archive</string>
</property>
</action>
<action name="actionLiteralSearch">
<property name="text">
<string>Literal Search</string>
</property>
<property name="toolTip">
<string>perform a literal search on the series and return the first 50 results</string>
</property>
</action>
<action name="actionMarkAd">
<property name="text">
<string>Mark Advertisement</string>
</property>
<property name="toolTip">
<string>mark the current page as an advertisement</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>

View File

@ -14,61 +14,57 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import platform
import urllib2
#import os
#import urllib
import sys
import urllib.parse
import requests
from . import _version
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
class QObject:
def __init__(self, *args):
pass
class pyqtSignal():
class pyqtSignal:
def __init__(self, *args):
pass
def emit(a, b, c):
pass
import ctversion
class VersionChecker(QObject):
def getRequestUrl(self, uuid, use_stats):
base_url = "http://comictagger1.appspot.com/latest"
args = ""
params = dict()
if use_stats:
params = {"uuid": uuid, "version": _version.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,15 +75,14 @@ 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):
if reply.error() != QNetworkReply.NoError:
return
# read in the response

View File

@ -14,26 +14,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import time
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import QUrl, pyqtSignal
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl, pyqtSignal
#from PyQt4.QtCore import QObject
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comictaggerlib.ui.qtutils import centerWindowOnParent, 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
#from imagefetcher import ImageFetcher
#import utils
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .coverimagewidget import CoverImageWidget
from .genericmetadata import GenericMetadata
from .issueidentifier import IssueIdentifier
from .issueselectionwindow import IssueSelectionWindow
from .matchselectionwindow import MatchSelectionWindow
from .progresswindow import IDProgressWindow
from .settings import ComicTaggerSettings
class SearchThread(QtCore.QThread):
@ -41,20 +34,21 @@ class SearchThread(QtCore.QThread):
searchComplete = pyqtSignal()
progressUpdate = pyqtSignal(int, int)
def __init__(self, series_name, refresh):
def __init__(self, series_name, refresh, literal=False):
QtCore.QThread.__init__(self)
self.series_name = series_name
self.refresh = refresh
self.error_code = None
self.literal = literal
def run(self):
comicVine = ComicVineTalker()
try:
self.cv_error = False
self.cv_search_results = comicVine.searchForSeries(
self.series_name,
callback=self.prog_callback,
refresh_cache=self.refresh)
if self.literal:
self.cv_search_results = comicVine.literalSearchForSeries(self.series_name, callback=self.prog_callback)
else:
self.cv_search_results = comicVine.searchForSeries(self.series_name, callback=self.prog_callback, refresh_cache=self.refresh)
except ComicVineTalkerException as e:
self.cv_search_results = []
self.cv_error = True
@ -90,29 +84,26 @@ class IdentifyThread(QtCore.QThread):
self.identifyComplete.emit()
class VolumeSelectionWindow(QtGui.QDialog):
def __init__(self, parent, series_name, issue_number, year, issue_count,
cover_index_list, comic_archive, settings, autoselect=False):
class VolumeSelectionWindow(QtWidgets.QDialog):
def __init__(
self, parent, series_name, issue_number, year, issue_count, cover_index_list, comic_archive, settings, autoselect=False, literal=False
):
super(VolumeSelectionWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('volumeselectionwindow.ui'), self)
uic.loadUi(ComicTaggerSettings.getUIFile("volumeselectionwindow.ui"), self)
self.imageWidget = CoverImageWidget(
self.imageContainer, CoverImageWidget.URLMode)
gridlayout = QtGui.QGridLayout(self.imageContainer)
self.imageWidget = CoverImageWidget(self.imageContainer, CoverImageWidget.URLMode)
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
gridlayout.addWidget(self.imageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.teDetails, 1)
reduceWidgetFontSize(self.twList)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.parent = parent
self.series_name = series_name
self.issue_number = issue_number
self.year = year
@ -122,6 +113,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.immediate_autoselect = autoselect
self.cover_index_list = cover_index_list
self.cv_search_results = None
self.literal = literal
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
@ -135,8 +127,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.twList.selectRow(0)
def updateButtons(self):
if self.cv_search_results is not None and len(
self.cv_search_results) > 0:
if self.cv_search_results is not None and len(self.cv_search_results) > 0:
enabled = True
else:
enabled = False
@ -144,24 +135,20 @@ 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,):
def requery(self):
self.performQuery(refresh=True)
self.twList.selectRow(0)
def autoSelect(self):
if self.comic_archive is None:
QtGui.QMessageBox.information(
self, "Auto-Select", "You need to load a comic first!")
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(
self,
"Auto-Select",
"Can't auto-select without an issue number (yet!)")
QtWidgets.QMessageBox.information(self, "Auto-Select", "Can't auto-select without an issue number (yet!)")
return
self.iddialog = IDProgressWindow(self)
@ -192,7 +179,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,33 +199,22 @@ class VolumeSelectionWindow(QtGui.QDialog):
found_match = None
choices = False
if result == self.ii.ResultNoMatches:
QtGui.QMessageBox.information(
self, "Auto-Select Result", " No matches found :-(")
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " No matches found :-(")
elif result == self.ii.ResultFoundMatchButBadCoverScore:
QtGui.QMessageBox.information(
self,
"Auto-Select Result",
" Found a match, but cover doesn't seem the same. Verify before commiting!")
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(
self,
"Auto-Select Result",
" Found a match, but not with the first page of the archive.")
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(
self,
"Auto-Select Result",
" Found some possibilities, but no confidence. Proceed manually.")
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually.")
choices = True
elif result == self.ii.ResultOneGoodMatch:
found_match = matches[0]
elif result == self.ii.ResultMultipleGoodMatches:
QtGui.QMessageBox.information(
self,
"Auto-Select Result",
" Found multiple likely matches. Please select.")
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " Found multiple likely matches. Please select.")
choices = True
if choices:
@ -252,19 +228,18 @@ class VolumeSelectionWindow(QtGui.QDialog):
if found_match is not None:
self.iddialog.accept()
self.volume_id = found_match['volume_id']
self.issue_number = found_match['issue_number']
self.volume_id = found_match["volume_id"]
self.issue_number = found_match["issue_number"]
self.selectByID()
self.showIssues()
def showIssues(self):
selector = IssueSelectionWindow(
self, self.settings, self.volume_id, self.issue_number)
selector = IssueSelectionWindow(self, self.settings, self.volume_id, self.issue_number)
title = ""
for record in self.cv_search_results:
if record['id'] == self.volume_id:
title = record['name']
title += " (" + unicode(record['start_year']) + ")"
if record["id"] == self.volume_id:
title = record["name"]
title += " (" + str(record["start_year"]) + ")"
title += " - "
break
@ -279,26 +254,23 @@ 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()
if (volume_id == self.volume_id):
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(
"Searching Online", "Cancel", 0, 100, self)
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.search_thread = SearchThread(self.series_name, refresh)
self.progdialog.setMinimumDuration(300)
QtCore.QCoreApplication.processEvents()
self.search_thread = SearchThread(self.series_name, refresh, self.literal)
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,21 +287,17 @@ 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(
self,
self.tr("Comic Vine Error"),
ComicVineTalker.getRateLimitMessage())
QtWidgets.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage())
else:
QtGui.QMessageBox.critical(
self,
self.tr("Network Issue"),
self.tr("Could not connect to Comic Vine to search for series!"))
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to search for series!"))
return
self.cv_search_results = self.search_thread.cv_search_results
@ -344,32 +312,31 @@ class VolumeSelectionWindow(QtGui.QDialog):
for record in self.cv_search_results:
self.twList.insertRow(row)
item_text = record['name']
item = QtGui.QTableWidgetItem(item_text)
item_text = record["name"]
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, record['id'])
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_text = str(record["start_year"])
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_text = record["count_of_issues"]
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
item.setData(QtCore.Qt.DisplayRole, record["count_of_issues"])
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
if record['publisher'] is not None:
item_text = record['publisher']['name']
if record["publisher"] is not None:
item_text = record["publisher"]["name"]
item.setData(QtCore.Qt.ToolTipRole, item_text)
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
item = QtWidgets.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
row += 1
@ -382,8 +349,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
if len(self.cv_search_results) == 0:
QtCore.QCoreApplication.processEvents()
QtGui.QMessageBox.information(
self, "Search Result", "No matches found!")
QtWidgets.QMessageBox.information(self, "Search Result", "No matches found!")
if self.immediate_autoselect and len(self.cv_search_results) > 0:
# defer the immediate autoselect so this dialog has time to pop up
@ -404,15 +370,14 @@ 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:
if record['description'] is None:
if record["id"] == self.volume_id:
if record["description"] is None:
self.teDetails.setText("")
else:
self.teDetails.setText(record['description'])
self.imageWidget.setURL(record['image']['super_url'])
self.teDetails.setText(record["description"])
self.imageWidget.setURL(record["image"]["super_url"])
break

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.

52
file_version_info.py Normal file
View File

@ -0,0 +1,52 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
exec(
'''
with open("comictaggerlib/_version.py") as file:
exec(file.read())
version_tuple = version_tuple + (0,) if len(version_tuple) < 4 else version_tuple
version_tuple = version_tuple if str(version_tuple[3]).isnumeric() else version_tuple[:3] + (int(version_tuple[3].replace("dev","")),)
'''
) or VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=version_tuple,
prodvers=version_tuple,
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3F,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x40004,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0),
),
kids=[
StringFileInfo(
[
StringTable(
"040904B0",
[
StringStruct("CompanyName", "ComicTagger team"),
StringStruct("FileDescription", "A cross-platform GUI/CLI app for writing metadata to comic archives"),
StringStruct("FileVersion", version),
StringStruct("OriginalFilename", "comictagger.exe"),
StringStruct("ProductName", "ComicTagger"),
StringStruct("ProductVersion", version),
],
)
]
),
VarFileInfo([VarStruct("Translation", [1033, 1200])]),
],
)

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="mygaget" />
<Content type="html">
<![CDATA[
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&amp;width=292&amp;colorscheme=light&amp;show_faces =false&amp;border_color&amp;stream=false&amp;header=false&amp;height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
]]>
</Content>
</Module>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="mygaget" />
<Content type="html">
<![CDATA[
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
]]>
</Content>
</Module>

View File

@ -1,260 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
# Author: danderson@google.com (David Anderson)
#
# Script for uploading files to a Google Code project.
#
# This is intended to be both a useful script for people who want to
# streamline project uploads and a reference implementation for
# uploading files to Google Code projects.
#
# To upload a file to Google Code, you need to provide a path to the
# file on your local machine, a small summary of what the file is, a
# project name, and a valid account that is a member or owner of that
# project. You can optionally provide a list of labels that apply to
# the file. The file will be uploaded under the same name that it has
# in your local filesystem (that is, the "basename" or last path
# component). Run the script with '--help' to get the exact syntax
# and available options.
#
# Note that the upload script requests that you enter your
# googlecode.com password. This is NOT your Gmail account password!
# This is the password you use on googlecode.com for committing to
# Subversion and uploading files. You can find your password by going
# to http://code.google.com/hosting/settings when logged in with your
# Gmail account. If you have already committed to your project's
# Subversion repository, the script will automatically retrieve your
# credentials from there (unless disabled, see the output of '--help'
# for details).
#
# If you are looking at this script as a reference for implementing
# your own Google Code file uploader, then you should take a look at
# the upload() function, which is the meat of the uploader. You
# basically need to build a multipart/form-data POST request with the
# right fields and send it to https://PROJECT.googlecode.com/files .
# Authenticate the request using HTTP Basic authentication, as is
# shown below.
#
# Licensed under the terms of the Apache Software License 2.0:
# http://www.apache.org/licenses/LICENSE-2.0
#
# Questions, comments, feature requests and patches are most welcome.
# Please direct all of these to the Google Code users group:
# http://groups.google.com/group/google-code-hosting
"""Google Code file uploader script.
"""
__author__ = 'danderson@google.com (David Anderson)'
import httplib
import os.path
import optparse
import getpass
import base64
import sys
def upload(file, project_name, user_name, password, summary, labels=None):
"""Upload a file to a Google Code project's file server.
Args:
file: The local path to the file.
project_name: The name of your project on Google Code.
user_name: Your Google account name.
password: The googlecode.com password for your account.
Note that this is NOT your global Google Account password!
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
Returns: a tuple:
http_status: 201 if the upload succeeded, something else if an
error occured.
http_reason: The human-readable string associated with http_status
file_url: If the upload succeeded, the URL of the file on Google
Code, None otherwise.
"""
# The login is the user part of user@gmail.com. If the login provided
# is in the full user@domain form, strip it down.
if user_name.endswith('@gmail.com'):
user_name = user_name[:user_name.index('@gmail.com')]
form_fields = [('summary', summary)]
if labels is not None:
form_fields.extend([('label', l.strip()) for l in labels])
content_type, body = encode_upload_request(form_fields, file)
upload_host = '%s.googlecode.com' % project_name
upload_uri = '/files'
auth_token = base64.b64encode('%s:%s' % (user_name, password))
headers = {
'Authorization': 'Basic %s' % auth_token,
'User-Agent': 'Googlecode.com uploader v0.9.4',
'Content-Type': content_type,
}
server = httplib.HTTPSConnection(upload_host)
server.request('POST', upload_uri, body, headers)
resp = server.getresponse()
server.close()
if resp.status == 201:
location = resp.getheader('Location', None)
else:
location = None
return resp.status, resp.reason, location
def encode_upload_request(fields, file_path):
"""Encode the given fields and file into a multipart form body.
fields is a sequence of (name, value) pairs. file is the path of
the file to upload. The file will be uploaded to Google Code with
the same file name.
Returns: (content_type, body) ready for httplib.HTTP instance
"""
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
CRLF = '\r\n'
body = []
# Add the metadata about the upload first
for key, value in fields:
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="%s"' % key,
'',
value,
])
# Now add the file itself
file_name = os.path.basename(file_path)
f = open(file_path, 'rb')
file_content = f.read()
f.close()
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="filename"; filename="%s"'
% file_name,
# The upload server determines the mime-type, no need to set it.
'Content-Type: application/octet-stream',
'',
file_content,
])
# Finalize the form body
body.extend(['--' + BOUNDARY + '--', ''])
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
def upload_find_auth(file_path, project_name, summary, labels=None,
user_name=None, password=None, tries=3):
"""Find credentials and upload a file to a Google Code project's file server.
file_path, project_name, summary, and labels are passed as-is to upload.
Args:
file_path: The local path to the file.
project_name: The name of your project on Google Code.
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
config_dir: Path to Subversion configuration directory, 'none', or None.
user_name: Your Google account name.
tries: How many attempts to make.
"""
if user_name is None or password is None:
from netrc import netrc
authenticators = netrc().authenticators("code.google.com")
if authenticators:
if user_name is None:
user_name = authenticators[0]
if password is None:
password = authenticators[2]
while tries > 0:
if user_name is None:
# Read username if not specified or loaded from svn config, or on
# subsequent tries.
sys.stdout.write('Please enter your googlecode.com username: ')
sys.stdout.flush()
user_name = sys.stdin.readline().rstrip()
if password is None:
# Read password if not loaded from svn config, or on subsequent
# tries.
print 'Please enter your googlecode.com password.'
print '** Note that this is NOT your Gmail account password! **'
print 'It is the password you use to access Subversion repositories,'
print 'and can be found here: http://code.google.com/hosting/settings'
password = getpass.getpass()
status, reason, url = upload(
file_path, project_name, user_name, password, summary, labels)
# Returns 403 Forbidden instead of 401 Unauthorized for bad
# credentials as of 2007-07-17.
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
# Rest for another try.
user_name = password = None
tries = tries - 1
else:
# We're done.
break
return status, reason, url
def main():
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
'-p PROJECT [options] FILE')
parser.add_option('-s', '--summary', dest='summary',
help='Short description of the file')
parser.add_option('-p', '--project', dest='project',
help='Google Code project name')
parser.add_option('-u', '--user', dest='user',
help='Your Google Code username')
parser.add_option('-w', '--password', dest='password',
help='Your Google Code password')
parser.add_option(
'-l',
'--labels',
dest='labels',
help='An optional list of comma-separated labels to attach '
'to the file')
options, args = parser.parse_args()
if not options.summary:
parser.error('File summary is missing.')
elif not options.project:
parser.error('Project name is missing.')
elif len(args) < 1:
parser.error('File to upload not provided.')
elif len(args) > 1:
parser.error('Only one file may be specified.')
file_path = args[0]
if options.labels:
labels = options.labels.split(',')
else:
labels = None
status, reason, url = upload_find_auth(file_path, options.project,
options.summary, labels,
options.user, options.password)
if url:
print 'The file was uploaded successfully.'
print 'URL: %s' % url
return 0
else:
print 'An error occurred. Your file was not uploaded.'
print 'Google Code upload server said: %s (%s)' % (reason, status)
return 1
if __name__ == '__main__':
sys.exit(main())

14
pyproject.toml Normal file
View File

@ -0,0 +1,14 @@
[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/_version.py"
local_scheme = "no-local-version"
version_scheme = "python-simplified-semver"

1
requirements-CBR.txt Normal file
View File

@ -0,0 +1 @@
comicapi[CBR] @ git+https://github.com/lordwelch/comicapi

1
requirements-GUI.txt Normal file
View File

@ -0,0 +1 @@
pyqt5

1
requirements-scripts.txt Normal file
View File

@ -0,0 +1 @@
filetype

View File

@ -1,5 +1,7 @@
configparser
beautifulsoup4 >= 4.1
unrar==0.3
natsort==3.5.2
PyPDF2==1.24
configparser
natsort
pathvalidate
pillow>=4.3.0
requests
comicapi @ git+https://github.com/lordwelch/comicapi

4
requirements_dev.txt Normal file
View File

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

158
scripts/dupe.ui Normal file
View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>729</width>
<height>406</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<widget class="QListWidget" name="dupeList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="enabled">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTableWidget" name="pageList">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="columnCount">
<number>3</number>
</property>
<column>
<property name="text">
<string>name</string>
</property>
</column>
<column>
<property name="text">
<string>score</string>
</property>
</column>
<column>
<property name="text">
<string>dupe name</string>
</property>
</column>
</widget>
<widget class="QFrame" name="comicData">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QFrame" name="comic1Data">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<item>
<widget class="QWidget" name="comic1Image" native="true">
<layout class="QVBoxLayout" name="verticalLayout_3"/>
</widget>
</item>
<item>
<widget class="QPushButton" name="comic1Delete">
<property name="toolTip">
<string>Delete Comic 1</string>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="comic2Data">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWidget" name="comic2Image" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4"/>
</widget>
</item>
<item>
<widget class="QPushButton" name="comic2Delete">
<property name="toolTip">
<string>Delete Comic 2</string>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,84 +1,687 @@
#!/usr/bin/python
#!/usr/bin/python3
"""Find all duplicate comics"""
#import sys
import argparse
import hashlib
import shutil
import signal
from pathlib import Path
from operator import itemgetter
from typing import Dict, List
import filetype
import typing
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import centerWindowOnParent
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
#from comictaggerlib.issuestring import *
#import comictaggerlib.utils
from comictaggerlib.imagehasher import ImageHasher
from comictaggerlib.filerenamer import FileRenamer
root = 1 << 31 - 1
something = 1 << 31 - 1
class ImageMeta:
def __init__(self, name, file_hash, image_hash, image_type, score=-1, score_file_hash=""):
self.name = name
self.file_hash = file_hash
self.image_hash = image_hash
self.type = image_type
self.score = score
self.score_file_hash = score_file_hash
class Duplicate:
"""docstring for Duplicate"""
imageHashes: Dict[str, ImageMeta]
def __init__(self, path, metadata: GenericMetadata, ca: ComicArchive, cover):
self.path = path
self.digest = ""
self.ca = ca
self.metadata = metadata
self.imageHashes = dict()
self.duplicateImages = set()
self.extras = set()
self.extractedPath = ""
self.deletable = False
self.keeping = False
self.fileCount = 0 # Excluding comicinfo.xml
self.imageCount = 0
self.cover = cover
blake2b = hashlib.blake2b(digest_size=16)
for f in open(self.path, "rb"):
blake2b.update(f)
self.digest = blake2b.hexdigest()
def extract(self, directory):
if self.ca.seemsToBeAComicArchive():
self.extractedPath = directory
for filepath in self.ca.archiver.getArchiveFilenameList():
filename = os.path.basename(filepath)
if filename.lower() in ["comicinfo.xml"]:
continue
self.fileCount += 1
archived_file = self.ca.archiver.readArchiveFile(filepath)
image_type = filetype.image_match(archived_file)
if image_type is not None:
self.imageCount += 1
file_hash = hashlib.blake2b(archived_file, digest_size=16).hexdigest().upper()
if file_hash in self.imageHashes.keys():
self.duplicateImages.add(filename)
else:
image_hash = ImageHasher(data=archived_file, width=12, height=12).average_hash()
self.imageHashes[file_hash] = ImageMeta(os.path.join(self.extractedPath, filename), file_hash,
image_hash, image_type.extension)
else:
self.extras.add(filename)
os.makedirs(self.extractedPath, 0o777, True)
unarchived_file = Path(os.path.join(self.extractedPath, filename))
unarchived_file.write_bytes(archived_file)
def clean(self):
shutil.rmtree(self.extractedPath, ignore_errors=True)
def delete(self):
if not self.keeping:
self.clean()
try:
os.remove(self.path)
except Exception:
pass
return not (os.path.exists(self.path) or os.path.exists(self.extractedPath))
class Tree(QtCore.QAbstractListModel):
def __init__(self, item: List[List[Duplicate]]):
super(Tree, self).__init__()
self.rootItem = item
def rowCount(self, index: QtCore.QModelIndex = ...) -> int:
if not index.isValid():
return len(self.rootItem)
return 0
def columnCount(self, index: QtCore.QModelIndex = ...) -> int:
if index.isValid():
return 1
return 0
def data(self, index: QtCore.QModelIndex, role: int = ...) -> typing.Any:
if not index.isValid():
return QtCore.QVariant()
f = FileRenamer(self.rootItem[index.row()][0].metadata)
f.setTemplate("{series} #{issue} - {title} ({year})")
if role == QtCore.Qt.DisplayRole:
return f.determineName('')
elif role == QtCore.Qt.UserRole:
return f.determineName('')
return QtCore.QVariant()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, file_list, style, work_path, parent=None):
super().__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("../../scripts/mainwindow.ui"), self)
self.dupes = []
self.firstRun = 0
self.dupe_set_list: List[List[Duplicate]] = list()
self.style = style
if work_path == "":
work_path = tempfile.mkdtemp()
self.work_path = work_path
self.initFiles = file_list
self.dupe_set_qlist.clicked.connect(self.dupe_set_clicked)
self.dupe_set_qlist.doubleClicked.connect(self.dupe_set_double_clicked)
self.actionCompare_Comic.triggered.connect(self.compare_action)
def comic_deleted(self, archive_path):
self.update_dupes()
def update_dupes(self):
# print("updating duplicates")
new_set_list = list()
for dupe in self.dupe_set_list:
dupe_list = list()
for d in dupe:
QtCore.QCoreApplication.processEvents()
if os.path.exists(d.path):
dupe_list.append(d)
else:
d.clean()
if len(dupe_list) > 1:
new_set_list.append(dupe_list)
else:
dupe_list[0].clean()
self.dupe_set_list: List[List[Duplicate]] = new_set_list
self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
def compare(self, i):
if len(self.dupe_set_list) > i:
dw = DupeWindow(self.dupe_set_list[i], self.work_path, self)
dw.closed.connect(self.update_dupes)
dw.show()
def compare_action(self, b):
selection = self.dupe_set_qlist.selectedIndexes()
if len(selection) > 0:
self.compare(selection[0].row())
def dupe_set_double_clicked(self, index: QtCore.QModelIndex):
self.compare(index.row())
def dupe_set_clicked(self, index: QtCore.QModelIndex):
for f in self.dupe_list.children():
f.deleteLater()
self.dupe_set_list[index.row()].sort(key=lambda k: k.digest)
for i, f in enumerate(self.dupe_set_list[index.row()]):
color = "black"
if i > 0:
if self.dupe_set_list[index.row()][i - 1].digest == f.digest:
color = "green"
elif i == 0:
if len(self.dupe_set_list[index.row()]) > 1:
if self.dupe_set_list[index.row()][i + 1].digest == f.digest:
color = "green"
ql = DupeImage(duplicate=f, style=f".path {{color: black;}}.hash {{color: {color};}}",
parent=self.dupe_list)
ql.deleted.connect(self.update_dupes)
ql.setMinimumWidth(300)
ql.setMinimumHeight(500)
self.dupe_list.layout().addWidget(ql)
def showEvent(self, event: QtGui.QShowEvent):
if self.firstRun == 0:
self.firstRun = 1
self.load_files(self.initFiles)
if len(self.dupe_set_list) < 1:
dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.NoIcon, "ComicTagger Duplicate finder",
"No duplicate comics found", QtWidgets.QMessageBox.Ok, parent=self)
dialog.setWindowModality(QtCore.Qt.ApplicationModal)
qw = QtWidgets.QWidget()
qw.setFixedWidth(90)
dialog.layout().addWidget(qw, 3, 2, 1, 3)
dialog.exec()
QtWidgets.QApplication.quit()
sys.exit(0)
self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
def load_files(self, file_list):
# Progress dialog on Linux flakes out for small range, so scale up
dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(file_list), parent=self)
dialog.setWindowTitle("Reading Comics")
dialog.setWindowModality(QtCore.Qt.ApplicationModal)
dialog.setMinimumDuration(300)
dialog.setMinimumWidth(400)
centerWindowOnParent(dialog)
comic_list = []
max_name_len = 2
for filename in file_list:
QtCore.QCoreApplication.processEvents()
if dialog.wasCanceled():
break
dialog.setValue(dialog.value() + 1)
dialog.setLabelText(filename)
ca = ComicArchive(path=filename, rar_exe_path=settings.rar_exe_path,
default_image_path=ComicTaggerSettings.getGraphic('nocover.png'))
if ca.seemsToBeAComicArchive() and ca.hasMetadata(self.style):
# fmt_str = "{{0:{0}}}".format(max_name_len)
# print(fmt_str.format(filename) + "\r", end='', file=sys.stderr)
# sys.stderr.flush()
md = ca.readMetadata(self.style)
cover = ca.getPage(0)
comic_list.append((make_key(md), filename, ca, md, cover))
# max_name_len = len(filename)
comic_list.sort(key=itemgetter(0), reverse=False)
# look for duplicate blocks
dupe_set = list()
prev_key = ""
dialog.setWindowTitle("Finding Duplicates")
dialog.setMaximum(len(comic_list))
dialog.setValue(dialog.minimum())
set_list = list()
for new_key, filename, ca, md, cover in comic_list:
dialog.setValue(dialog.value() + 1)
QtCore.QCoreApplication.processEvents()
if dialog.wasCanceled():
break
dialog.setLabelText(filename)
# if the new key same as the last, add to to dupe set
if new_key == prev_key:
dupe_set.append((filename, ca, md, cover))
# else we're on a new potential block
else:
# only add if the dupe list has 2 or more
if len(dupe_set) > 1:
set_list.append(dupe_set)
dupe_set = list()
dupe_set.append((filename, ca, md, cover))
prev_key = new_key
# Final dupe_set
if len(dupe_set) > 1:
set_list.append(dupe_set)
for d_set in set_list:
new_set = list()
for filename, ca, md, cover in d_set:
new_set.append(Duplicate(filename, md, ca, cover))
self.dupe_set_list.append(new_set)
self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
# print()
dialog.close()
# def delete_hashes(self):
# working_dir = os.path.join(self.tmp, "working")
# s = False
# # while working and len(dupe_set) > 1:
# remaining = list()
# for dupe_set in self.dupe_set_list:
# not_deleted = True
# if os.path.exists(working_dir):
# shutil.rmtree(working_dir, ignore_errors=True)
#
# os.mkdir(working_dir)
# extract(dupe_set, working_dir)
# if mark_hashes(dupe_set):
# if s: # Auto delete if s flag or if there are not any non image extras
# dupe_set.sort(key=attrgetter("fileCount"))
# dupe_set.sort(key=lambda x: len(x.duplicateImages))
# dupe_set[0].keeping = True
# else:
# dupe_set[select_archive("Select archive to keep: ", dupe_set)].keeping = True
# else:
# # app.exec_()
# compare_dupe(dupe_set[0], dupe_set[1])
# for i, dupe in enumerate(dupe_set):
# print("{0}. {1}: {2.series} #{2.issue:0>3} {2.year}; extras: {3}; deletable: {4}".format(
# i,
# dupe.path,
# dupe.metadata,
# ", ".join(sorted(dupe.extras)), dupe.deletable))
# dupe_set = delete(dupe_set)
# if not_deleted:
# remaining.append(dupe_set)
# self.dupe_set_list = remaining
class DupeWindow(QtWidgets.QWidget):
closed = QtCore.pyqtSignal()
def __init__(self, duplicates: List[Duplicate], tmp, parent=None):
super().__init__(parent, QtCore.Qt.Window)
uic.loadUi(ComicTaggerSettings.getUIFile("../../scripts/dupe.ui"), self)
for f in self.comic1Image.children():
f.deleteLater()
for f in self.comic2Image.children():
f.deleteLater()
self.deleting = -1
self.duplicates = duplicates
self.dupe1 = -1
self.dupe2 = -1
self.tmp = tmp
self.setWindowTitle("ComicTagger Duplicate compare")
self.pageList.currentItemChanged.connect(self.current_item_changed)
self.comic1Delete.clicked.connect(self.delete_1)
self.comic2Delete.clicked.connect(self.delete_2)
self.dupeList.itemSelectionChanged.connect(self.show_dupe_list)
# self.dupeList = QtWidgets.QListWidget()
self.dupeList.setIconSize(QtCore.QSize(100, 50))
while self.pageList.rowCount() > 0:
self.pageList.removeRow(0)
self.pageList.setSortingEnabled(False)
if len(duplicates) < 2:
return
extract(duplicates, tmp)
tmp1 = DupeImage(self.duplicates[0])
tmp2 = DupeImage(self.duplicates[1])
self.comic1Data.layout().replaceWidget(self.comic1Image, tmp1)
self.comic2Data.layout().replaceWidget(self.comic2Image, tmp2)
self.comic1Image = tmp1
self.comic2Image = tmp2
self.comic1Image.deleted.connect(self.update_dupes)
self.comic2Image.deleted.connect(self.update_dupes)
def showEvent(self, event: QtGui.QShowEvent) -> None:
self.update_dupes()
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
self.closed.emit()
event.accept()
def show_dupe_list(self):
dupes = self.dupeList.selectedItems()
if len(dupes) != 2:
return
self.dupe1 = int(dupes[0].data(QtCore.Qt.UserRole))
self.dupe2 = int(dupes[1].data(QtCore.Qt.UserRole))
if len(self.duplicates[self.dupe2].imageHashes) > len(self.duplicates[self.dupe1].imageHashes):
self.dupe1, self.dupe2 = self.dupe2, self.dupe1
compare_dupe(self.duplicates[self.dupe1].imageHashes, self.duplicates[self.dupe2].imageHashes)
self.display_dupe()
def update_dupes(self):
dupes: List[Duplicate] = list()
for f in self.duplicates:
if os.path.exists(f.path):
dupes.append(f)
else:
f.clean()
self.duplicates = dupes
if len(self.duplicates) < 2:
self.close()
for i, dupe in enumerate(self.duplicates):
item = QtWidgets.QListWidgetItem()
item.setText(dupe.path)
item.setToolTip(dupe.path)
pm = QtGui.QPixmap()
pm.loadFromData(dupe.cover)
item.setIcon(QtGui.QIcon(pm))
item.setData(QtCore.Qt.UserRole, i)
self.dupeList.addItem(item)
self.dupeList.setCurrentRow(0)
self.dupeList.setCurrentRow(1, QtCore.QItemSelectionModel.Select)
def delete_1(self):
self.duplicates[self.dupe1].delete()
self.update_dupes()
def delete_2(self):
self.duplicates[self.dupe2].delete()
self.update_dupes()
def display_dupe(self):
for f in range(self.pageList.rowCount()):
self.pageList.removeRow(0)
for h in self.duplicates[self.dupe1].imageHashes.values():
row = self.pageList.rowCount()
self.pageList.insertRow(row)
name = QtWidgets.QTableWidgetItem()
score = QtWidgets.QTableWidgetItem()
dupe_name = QtWidgets.QTableWidgetItem()
item_text = os.path.basename(h.name)
name.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
name.setText(item_text)
name.setData(QtCore.Qt.UserRole, h.file_hash)
name.setData(QtCore.Qt.ToolTipRole, item_text)
self.pageList.setItem(row, 0, name)
item_text = str(h.score)
score.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
score.setText(item_text)
score.setData(QtCore.Qt.UserRole, h.file_hash)
score.setData(QtCore.Qt.ToolTipRole, item_text)
self.pageList.setItem(row, 1, score)
item_text = os.path.basename(self.duplicates[self.dupe2].imageHashes[h.score_file_hash].name)
dupe_name.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
dupe_name.setText(item_text)
dupe_name.setData(QtCore.Qt.UserRole, h.file_hash)
dupe_name.setData(QtCore.Qt.ToolTipRole, item_text)
self.pageList.setItem(row, 2, dupe_name)
self.pageList.resizeColumnsToContents()
self.pageList.selectRow(0)
def current_item_changed(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
file_hash = str(self.pageList.item(curr.row(), 0).data(QtCore.Qt.UserRole))
image_hash = self.duplicates[self.dupe1].imageHashes[file_hash]
score_hash = self.duplicates[self.dupe2].imageHashes[image_hash.score_file_hash]
image1 = QtGui.QPixmap(image_hash.name)
image2 = QtGui.QPixmap(score_hash.name)
page_color = "red"
size_color = "red"
type_color = "red"
file_color = "black"
image_color = "black"
if image1.width() == image2.width() and image2.height() == image1.height():
size_color = "green"
if len(self.duplicates[self.dupe1].imageHashes) == len(self.duplicates[self.dupe2].imageHashes):
page_color = "green"
if image_hash.type == score_hash.type:
type_color = "green"
if image_hash.image_hash == score_hash.image_hash:
image_color = "green"
if image_hash.file_hash == score_hash.file_hash:
file_color = "green"
style = f"""
.page {{
color: {page_color};
}}
.size {{
color: {size_color};
}}
.type {{
color: {type_color};
}}
.file {{
color: {file_color};
}}
.image {{
color: {image_color};
}}
"""
text = "name: {{duplicate.path}}<br/>" \
"page count: <span class='page'>{len}</span><br/>" \
"size/type: <span class='size'>{{width}}x{{height}}</span>/<span class='type'>{meta.type}</span><br/>" \
"file_hash: <span class='file'>{meta.file_hash}</span><br/>" \
"image_hash: <span class='image'>{meta.image_hash}</span>" \
.format(meta=image_hash, style=style, len=len(self.duplicates[self.dupe1].imageHashes))
self.comic1Image.setDuplicate(self.duplicates[self.dupe1])
self.comic1Image.setImage(image_hash.name)
self.comic1Image.setText(text)
self.comic1Image.setLabelStyle(style)
text = "name: {{duplicate.path}}<br/>" \
"page count: <span class='page'>{len}</span><br/>" \
"size/type: <span class='size'>{{width}}x{{height}}</span>/<span class='type'>{score.type}</span><br/>" \
"file_hash: <span class='file'>{score.file_hash}</span><br/>" \
"image_hash: <span class='image'>{score.image_hash}</span>" \
.format(score=score_hash, style=style, len=len(self.duplicates[self.dupe2].imageHashes))
self.comic2Image.setDuplicate(self.duplicates[self.dupe2])
self.comic2Image.setImage(score_hash.name)
self.comic2Image.setText(text)
self.comic2Image.setLabelStyle(style)
class QQlabel(QtWidgets.QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.image = None
self.setMinimumSize(1, 1)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
def setPixmap(self, pixmap: QtGui.QPixmap) -> None:
self.image = pixmap
self.setMaximumWidth(pixmap.width())
self.setMaximumHeight(pixmap.height())
super().setPixmap(
self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
if self.image is not None:
super().setPixmap(self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
class DupeImage(QtWidgets.QWidget):
deleted = QtCore.pyqtSignal(str)
def __init__(self, duplicate: Duplicate, style=".path {color: black;}.hash {color: black;}",
text="path: <span class='path'>{duplicate.path}</span><br/>hash: <span class='hash'>{duplicate.digest}</span>",
image="cover", parent=None):
super().__init__(parent)
self.setLayout(QtWidgets.QVBoxLayout())
self.image = QQlabel()
self.label = QtWidgets.QLabel()
self.duplicate = duplicate
self.text = text
self.labelStyle = style
self.iHeight = 0
self.iWidth = 0
self.setStyleSheet("color: black;")
self.label.setWordWrap(True)
self.setImage(image)
self.setLabelStyle(self.labelStyle)
self.setText(self.text)
# label.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.layout().addWidget(self.image)
self.layout().addWidget(self.label)
def contextMenuEvent(self, event: QtGui.QContextMenuEvent):
menu = QtWidgets.QMenu()
delete_action = menu.addAction("delete")
action = menu.exec(self.mapToGlobal(event.pos()))
if action == delete_action:
if self.duplicate.delete():
self.hide()
self.deleteLater()
# print("signal emitted")
self.deleted.emit(self.duplicate.path)
def setDuplicate(self, duplicate: Duplicate):
self.duplicate = duplicate
self.setImage("cover")
self.label.setText(
f"<style>{self.labelStyle}</style>" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
height=self.iHeight))
def setText(self, text):
self.text = text
self.label.setText(
f"<style>{self.labelStyle}</style>" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
height=self.iHeight))
def setImage(self, image):
if self.duplicate is not None:
pm = QtGui.QPixmap()
if image == "cover":
pm.loadFromData(self.duplicate.cover)
else:
pm.load(image)
self.iHeight = pm.height()
self.iWidth = pm.width()
self.image.setPixmap(pm)
def setLabelStyle(self, style):
self.labelStyle = style
self.label.setText(
f"<style>{self.labelStyle}</style>" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
height=self.iHeight))
def extract(dupe_set, directory):
for dupe in dupe_set:
dupe.extract(unique_dir(os.path.join(directory, os.path.basename(dupe.path))))
def compare_dupe(dupe1: Dict[str, ImageMeta], dupe2: Dict[str, ImageMeta]):
for k, image1 in dupe1.items():
score = sys.maxsize
file_hash = ""
for k2, image2 in dupe2.items():
tmp = ImageHasher.hamming_distance(image1.image_hash, image2.image_hash)
if tmp < score:
score = tmp
file_hash = image2.file_hash
dupe1[k].score = score
dupe1[k].score_file_hash = file_hash
def make_key(x):
return "<" + str(x.series) + " #" + str(x.issue) + " - " + str(x.title) + " - " + str(x.year) + ">"
def unique_dir(file_name):
counter = 1
file_name_parts = os.path.splitext(file_name)
while True:
if not os.path.lexists(file_name):
return file_name
file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1]
counter += 1
app = None
settings = ComicTaggerSettings()
def main():
utils.fix_output_encoding()
settings = ComicTaggerSettings()
signal.signal(signal.SIGINT, sigint_handler)
parser = argparse.ArgumentParser(description='ComicTagger Duplicate comparison script')
parser.add_argument('-w', metavar='workdir', type=str, nargs=1, default=tempfile.mkdtemp(), help='work directory')
parser.add_argument('paths', metavar='PATH', type=str, nargs='+', help='Path(s) to search for duplicates')
args = parser.parse_args()
style = MetaDataStyle.CIX
global app
workdir = args.w
app = QtWidgets.QApplication(sys.argv)
file_list = utils.get_recursive_filelist(args.paths)
if len(sys.argv) < 2:
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
return
timer = QtCore.QTimer()
timer.start(50) # You may change this if you wish.
timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms.
filelist = utils.get_recursive_filelist(sys.argv[1:])
window = MainWindow(file_list, style, workdir)
window.show()
app.exec()
shutil.rmtree(workdir, True)
# first find all comics with metadata
print >> sys.stderr, "Reading in all comics..."
comic_list = []
fmt_str = ""
max_name_len = 2
for filename in filelist:
ca = ComicArchive(filename, settings.rar_exe_path)
if ca.seemsToBeAComicArchive() and ca.hasMetadata(style):
max_name_len = max(max_name_len, len(filename))
fmt_str = u"{{0:{0}}}".format(max_name_len)
print >> sys.stderr, fmt_str.format(filename) + "\r",
sys.stderr.flush()
comic_list.append((filename, ca.readMetadata(style)))
print >> sys.stderr, fmt_str.format("") + "\r",
print "--------------------------------------------------------------------------"
print "Found {0} comics with {1} tags".format(len(comic_list), MetaDataStyle.name[style])
print "--------------------------------------------------------------------------"
def sigint_handler(*args):
"""Handler for the SIGINT signal."""
sys.stderr.write('\r')
QtWidgets.QApplication.quit()
# sort the list by series+issue+year, to put all the dupes together
def makeKey(x):
return "<" + unicode(x[1].series) + u" #" + \
unicode(x[1].issue) + u" - " + unicode(x[1].year) + ">"
comic_list.sort(key=makeKey, reverse=False)
# look for duplicate blocks
dupe_set_list = list()
dupe_set = list()
prev_key = ""
for filename, md in comic_list:
print >> sys.stderr, fmt_str.format(filename) + "\r",
sys.stderr.flush()
new_key = makeKey((filename, md))
# if the new key same as the last, add to to dupe set
if new_key == prev_key:
dupe_set.append(filename)
# else we're on a new potential block
else:
# only add if the dupe list has 2 or more
if len(dupe_set) > 1:
dupe_set_list.append(dupe_set)
dupe_set = list()
dupe_set.append(filename)
prev_key = new_key
print >> sys.stderr, fmt_str.format("") + "\r",
print "Found {0} duplicate sets".format(len(dupe_set_list))
for dupe_set in dupe_set_list:
ca = ComicArchive(dupe_set[0], settings.rar_exe_path)
md = ca.readMetadata(style)
print "{0} #{1} ({2})".format(md.series, md.issue, md.year)
for filename in dupe_set:
print "------>{0}".format(filename)
if __name__ == '__main__':
main()

View File

@ -15,13 +15,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
# import sys
# import os
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
from comictaggerlib.issuestring import *
#import comictaggerlib.utils
from comictaggerlib.settings import *
# import comictaggerlib.utils
def main():
@ -69,15 +70,14 @@ def main():
fmt_str = u"{0:" + str(w0) + "} {1:" + str(w1) + "} #{2:6} ({3})"
# now sort the list by issue, and then series
metadata_list.sort(
key=lambda x: IssueString(x[1].issue).asString(3), reverse=False)
metadata_list.sort(
key=lambda x: unicode(x[1].series).lower() + str(x[1].year), reverse=False)
metadata_list.sort(key=lambda x: IssueString(x[1].issue).asString(3), reverse=False)
metadata_list.sort(key=lambda x: unicode(x[1].series).lower() + str(x[1].year), reverse=False)
# now print
for filename, md in metadata_list:
if not md.isEmpty:
print fmt_str.format(os.path.split(filename)[1] + ":", md.series, md.issue, md.year), md.title
if __name__ == '__main__':
if __name__ == "__main__":
main()

92
scripts/mainwindow.ui Normal file
View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>ComicTagger Duplicate finder</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<widget class="QSplitter" name="splitter">
<property name="enabled">
<bool>true</bool>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTreeView" name="dupe_set_qlist"/>
<widget class="QScrollArea" name="dupe_list_p">
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="dupe_list">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>396</width>
<height>520</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>30</height>
</rect>
</property>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionCompare_Comic"/>
</widget>
<action name="actionCompare_Comic">
<property name="text">
<string>Compare Comic</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -18,14 +18,15 @@ organizing by date and series, in different trees
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
#import platform
# import sys
# import os
# import platform
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
#from comictaggerlib.issuestring import *
#import comictaggerlib.utils
# from comictaggerlib.issuestring import *
# import comictaggerlib.utils
def make_folder(folder):
@ -52,8 +53,7 @@ def main():
print >> sys.stderr, "Sorry, this script works only on UNIX systems"
if len(sys.argv) < 3:
print >> sys.stderr, "Usage: {0} [comic_root][link_root]".format(
sys.argv[0])
print >> sys.stderr, "Usage: {0} [comic_root][link_root]".format(sys.argv[0])
return
comic_root = sys.argv[1]
@ -93,8 +93,7 @@ def main():
month_str = "00"
date_folder = os.path.join(link_root, "date", str(md.year), month_str)
make_folder(date_folder)
make_link(
filename, os.path.join(date_folder, os.path.basename(filename)))
make_link(filename, os.path.join(date_folder, os.path.basename(filename)))
# do publisher/series organizing:
fixed_series_name = md.series
@ -102,11 +101,10 @@ def main():
# some tweaks to keep various filesystems happy
fixed_series_name = fixed_series_name.replace("/", "-")
fixed_series_name = fixed_series_name.replace("?", "")
series_folder = os.path.join(
link_root, "series", str(md.publisher), unicode(fixed_series_name))
series_folder = os.path.join(link_root, "series", str(md.publisher), unicode(fixed_series_name))
make_folder(series_folder)
make_link(filename, os.path.join(
series_folder, os.path.basename(filename)))
make_link(filename, os.path.join(series_folder, os.path.basename(filename)))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -18,14 +18,18 @@
# limitations under the License.
import shutil
#import sys
#import os
#import platform
from comicapi.comicarchive import *
from comictaggerlib.settings import *
from comicapi.comicarchive import *
#from comicapi.issuestring import *
#import comicapi.utils
# import sys
# import os
# import platform
# from comicapi.issuestring import *
# import comicapi.utils
def make_folder(folder):
@ -52,8 +56,7 @@ def main():
print >> sys.stderr, "Sorry, this script works only on UNIX systems"
if len(sys.argv) < 3:
print >> sys.stderr, "Usage: {0} [comic_root][tree_root]".format(
sys.argv[0])
print >> sys.stderr, "Usage: {0} [comic_root][tree_root]".format(sys.argv[0])
return
comic_root = sys.argv[1]
@ -78,7 +81,7 @@ def main():
max_name_len = 2
fmt_str = ""
for filename in filelist:
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic('nocover.png'))
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
if ca.seemsToBeAComicArchive() and ca.hasMetadata(style):
comic_list.append((filename, ca.readMetadata(style)))
@ -106,13 +109,10 @@ def main():
series_name = series_name.replace(":", " -")
series_name = series_name.replace("/", "-")
series_name = series_name.replace("?", "")
series_folder = os.path.join(
tree_root,
unicode(publisher_name),
unicode(series_name) + " (" + unicode(start_year) + ")")
series_folder = os.path.join(tree_root, unicode(publisher_name), unicode(series_name) + " (" + unicode(start_year) + ")")
make_folder(series_folder)
move_file(filename, os.path.join(
series_folder, os.path.basename(filename)))
move_file(filename, os.path.join(series_folder, os.path.basename(filename)))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -17,34 +17,27 @@
import argparse
import json
#import sys
#import os
#import re
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
from comictaggerlib.filerenamer import *
#import comictaggerlib.utils
from comictaggerlib.settings import *
# import sys
# import os
# import re
# import comictaggerlib.utils
def parse_args():
input_args = sys.argv[1:]
parser = argparse.ArgumentParser(
description='A script to rename comic files')
parser.add_argument(
'-t',
'--transforms',
metavar='xformfile',
help="The file with transforms")
parser.add_argument(
'-n',
'--noconfirm',
action='store_true',
help="Don't confirm before rename")
parser.add_argument('paths', metavar='PATH', type=str,
nargs='+', help='path to look for comic files')
parser = argparse.ArgumentParser(description="A script to rename comic files")
parser.add_argument("-t", "--transforms", metavar="xformfile", help="The file with transforms")
parser.add_argument("-n", "--noconfirm", action="store_true", help="Don't confirm before rename")
parser.add_argument("paths", metavar="PATH", type=str, nargs="+", help="path to look for comic files")
parsed_args = parser.parse_args(input_args)
return parsed_args
@ -60,8 +53,7 @@ def calculate_rename(ca, md, settings):
new_ext = ".cbr"
renamer = FileRenamer(md)
renamer.setTemplate(
"%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%")
renamer.setTemplate("%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%")
renamer.setIssueZeroPadding(0)
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
@ -96,11 +88,11 @@ def main():
print "Reading in transforms from:", parsed_args.transforms
json_data = open(parsed_args.transforms).read()
data = json.loads(json_data)
xform_list = data['xforms']
xform_list = data["xforms"]
else:
xform_list = default_xform_list
#pprint( xform_list, indent=4)
# pprint( xform_list, indent=4)
filelist = utils.get_recursive_filelist(parsed_args.paths)
@ -153,11 +145,12 @@ def main():
print u"'{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
i = raw_input("Do you want to proceed with rename? [y/N] ")
if i.lower() not in ('y', 'yes'):
if i.lower() not in ("y", "yes"):
print "exiting without rename."
sys.exit(0)
perform_rename(modify_list)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -19,133 +19,67 @@ are kept in a sub-folder at the level of the original
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import os
import shutil
import sys
import tempfile
import zipfile
import shutil
import comictaggerlib.utils
from comictaggerlib.settings import *
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
subfolder_name = "PRE_AD_REMOVAL"
unwanted_types = ['Deleted', 'Advertisement']
unwanted_types = ["Deleted", "Advertisement"]
def main():
utils.fix_output_encoding()
# utils.fix_output_encoding()
settings = ComicTaggerSettings()
# this can only work with files with ComicRack tags
style = MetaDataStyle.CIX
if len(sys.argv) < 2:
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
print("Usage: {0} [comic_folder]".format(sys.argv[0]), file=sys.stderr)
return
filelist = utils.get_recursive_filelist(sys.argv[1:])
if sys.argv[1] == "-n":
filelist = utils.get_recursive_filelist(sys.argv[2:])
else:
filelist = utils.get_recursive_filelist(sys.argv[1:])
# first read in CIX metadata from all files, make a list of candidates
modify_list = []
for filename in filelist:
print(filename, end="\n")
ca = ComicArchive(filename, settings.rar_exe_path)
ca = ComicArchive(
filename,
settings.rar_exe_path,
default_image_path="/home/timmy/build/source/comictagger-test/comictaggerlib/graphics/nocover.png",
)
if (ca.isZip or ca.isRar()) and ca.hasMetadata(style):
md = ca.readMetadata(style)
if len(md.pages) != 0:
pgs = list()
mod = False
for p in md.pages:
if 'Type' in p and p['Type'] in unwanted_types:
# This one has pages to remove. add to list!
modify_list.append((filename, md))
break
if "Type" in p and p["Type"] in unwanted_types:
# This one has pages to remove. Remove it!
print("removing " + ca.getPageName(int(p["Image"])))
if sys.argv[1] != "-n":
mod = True
ca.archiver.removeArchiveFile(ca.getPageName(int(p["Image"])))
else:
pgs.append(p)
# now actually process those files
for filename, md in modify_list:
ca = ComicArchive(filename, settings.rar_exe_path)
curr_folder = os.path.dirname(filename)
curr_subfolder = os.path.join(curr_folder, subfolder_name)
# skip any of our generated subfolders...
if os.path.basename(curr_folder) == subfolder_name:
continue
sys.stdout.write("Removing unwanted pages from " + filename)
# verify that we can write to current folder
if not os.access(filename, os.W_OK):
print "Can't move: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder) and not os.access(
curr_folder, os.W_OK):
print "Can't create subfolder here: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder):
os.mkdir(curr_subfolder)
if not os.access(curr_subfolder, os.W_OK):
print "Can't write to the subfolder here: {0}: skipped!".format(filename)
continue
# generate a new file with temp name
tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename))
os.close(tmp_fd)
try:
zout = zipfile.ZipFile(tmp_name, 'w')
# now read in all the pages from the old one, except the ones we
# want to skip
new_num = 0
new_pages = list()
for p in md.pages:
if 'Type' in p and p['Type'] in unwanted_types:
continue
else:
pageNum = int(p['Image'])
name = ca.getPageName(pageNum)
buffer = ca.getPage(pageNum)
sys.stdout.write('.')
sys.stdout.flush()
# Generate a new name for the page file
ext = os.path.splitext(name)[1]
new_name = "page{0:04d}{1}".format(new_num, ext)
zout.writestr(new_name, buffer)
# create new page entry
new_p = dict()
new_p['Image'] = str(new_num)
if 'Type' in p:
new_p['Type'] = p['Type']
new_pages.append(new_p)
new_num += 1
# preserve the old comment
comment = ca.archiver.getArchiveComment()
if comment is not None:
zout.comment = ca.archiver.getArchiveComment()
except Exception as e:
print "Failure creating new archive: {0}!".format(filename)
print e, sys.exc_info()[0]
zout.close()
os.unlink(tmp_name)
else:
zout.close()
# Success! Now move the files
shutil.move(filename, curr_subfolder)
os.rename(tmp_name, filename)
# TODO: We might have converted a rar to a zip, and should probably change
# the extension, as needed.
print "Done!".format(filename)
# Create a new archive object for the new file, and write the old
# CIX data, with new page info
ca = ComicArchive(filename, settings.rar_exe_path)
md.pages = new_pages
ca.writeMetadata(style, md)
if mod:
for num, p in enumerate(pgs):
p["Image"] = str(num)
md.pages = pgs
ca.writeCIX(md)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -16,16 +16,13 @@
# limitations under the License.
import shutil
#import sys
#import os
#import tempfile
#import zipfile
import Image
from comictaggerlib.settings import *
from comictaggerlib.comicarchive import *
#import comictaggerlib.utils
from comictaggerlib.settings import *
# import comictaggerlib.utils
subfolder_name = "ORIGINALS"
@ -52,7 +49,7 @@ def main():
for filename in filelist:
ca = ComicArchive(filename, settings.rar_exe_path)
if (ca.seemsToBeAComicArchive()):
if ca.seemsToBeAComicArchive():
# Check the images in the file, see if we need to reduce any
for idx in range(ca.getNumberOfPages()):
@ -66,8 +63,7 @@ def main():
max_name_len = max(max_name_len, len(filename))
fmt_str = u"{{0:{0}}}".format(max_name_len)
print >> sys.stderr, fmt_str.format(
filename) + "\r",
print >> sys.stderr, fmt_str.format(filename) + "\r",
sys.stderr.flush()
break
@ -99,8 +95,7 @@ def main():
if not os.access(filename, os.W_OK):
print "Can't move: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder) and not os.access(
curr_folder, os.W_OK):
if not os.path.exists(curr_subfolder) and not os.access(curr_folder, os.W_OK):
print "Can't create subfolder here: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder):
@ -118,7 +113,7 @@ def main():
cix_md = ca.readCIX()
try:
zout = zipfile.ZipFile(tmp_name, 'w')
zout = zipfile.ZipFile(tmp_name, "w")
# Check the images in the file, see if we want to reduce them
page_count = ca.getNumberOfPages()
@ -133,7 +128,7 @@ def main():
w, h = im.size
if h > max_height:
# resize the image
hpercent = (max_height / float(h))
hpercent = max_height / float(h)
wsize = int((float(w) * float(hpercent)))
size = (wsize, max_height)
im = im.resize(size, Image.ANTIALIAS)
@ -151,7 +146,7 @@ def main():
# page is empty?? nothing to write
out_data = ""
sys.stdout.write('.')
sys.stdout.write(".")
sys.stdout.flush()
# write out the new resized image
@ -186,5 +181,5 @@ def main():
ca.writeCIX(cix_md)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -16,14 +16,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
# import sys
# import os
from comictaggerlib.settings import *
from comictaggerlib.comicarchive import *
from comictaggerlib.issueidentifier import *
from comictaggerlib.comicvinetalker import *
#import comictaggerlib.utils
from comictaggerlib.issueidentifier import *
from comictaggerlib.settings import *
# import comictaggerlib.utils
def main():
@ -32,8 +33,7 @@ def main():
settings = ComicTaggerSettings()
if len(sys.argv) < 3:
print >> sys.stderr, "Usage: {0} [comicfile][issueid]".format(
sys.argv[0])
print >> sys.stderr, "Usage: {0} [comicfile][issueid]".format(sys.argv[0])
return
filename = sys.argv[1]
@ -45,8 +45,7 @@ def main():
ca = ComicArchive(filename, settings.rar_exe_path)
if not ca.seemsToBeAComicArchive():
print >> sys.stderr, "Sorry, but " + \
filename + " is not a comic archive!"
print >> sys.stderr, "Sorry, but " + filename + " is not a comic archive!"
return
ii = IssueIdentifier(ca, settings)
@ -59,16 +58,15 @@ def main():
hash_list = [cover_hash0, cover_hash1]
comicVine = ComicVineTalker()
result = ii.getIssueCoverMatchScore(
comicVine, issue_id, hash_list, useRemoteAlternates=True, useLog=False)
result = ii.getIssueCoverMatchScore(comicVine, issue_id, hash_list, useRemoteAlternates=True, useLog=False)
print "Best cover match score is:", result['score']
if result['score'] < ii.min_alternate_score_thresh:
print "Best cover match score is:", result["score"]
if result["score"] < ii.min_alternate_score_thresh:
print "Looks like a match!"
else:
print "Bad score, maybe not a match?"
print result['url']
print result["url"]
if __name__ == '__main__':
if __name__ == "__main__":
main()

135
setup.py
View File

@ -1,45 +1,86 @@
#!/usr/bin/env python
# Setup file for comictagger python source (no wheels yet)
#
# The install process will attempt to compile the unrar lib from source.
# If it succeeds, the unrar lib binary will be installed with the python
# source. If it fails, install will just continue. On most Linux systems it
# should just work. (Tested on a Mac system with homebrew, as well)
#
# 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 os
import glob
from setuptools import setup
import comictaggerlib.ctversion
with open('requirements.txt') as f:
required = f.read().splitlines()
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()
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",
long_description="""
install_requires = read("requirements.txt").splitlines()
# Dynamically determine extra dependencies
extras_require = {}
extra_req_files = glob.glob("requirements-*.txt")
for extra_req_file in extra_req_files:
name = os.path.splitext(extra_req_file)[0].replace("requirements-", "", 1)
extras_require[name] = read(extra_req_file).splitlines()
# If there are any extras, add a catch-all case that includes everything.
# This assumes that entries in extras_require are lists (not single strings),
# and that there are no duplicated packages across the extras.
if extras_require:
extras_require["all"] = sorted({x for v in extras_require.values() for x in v})
setup(
name="comictagger",
install_requires=install_requires,
extras_require=extras_require,
python_requires=">=3",
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"],
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="""
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
Features:
@ -48,19 +89,9 @@ Features:
* 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)
* Reads and writes multiple tagging schemes ( ComicBookLover and ComicRack).
* Reads and writes rar, zip and tar 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.
Requires:
* python 2.6 or 2.7
* configparser
* python imaging (PIL) >= 1.1.6
* beautifulsoup > 4.1
Optional requirement (for GUI):
* pyqt4
"""
)
* Can run without PyQt5 installed
""",
)

Some files were not shown because too many files have changed in this diff Show More