Compare commits
6 Commits
master
...
1.1.16-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0484d05462 | ||
|
|
0766bf7064 | ||
|
|
7e8fc143fd | ||
|
|
43a913294e | ||
|
|
70e28c7863 | ||
|
|
14713d8ad0 |
157
.gitignore
vendored
157
.gitignore
vendored
@@ -1,154 +1,3 @@
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
|
||||
### Other editors
|
||||
.*.swp
|
||||
nbproject/
|
||||
.vscode
|
||||
|
||||
comictaggerlib/_version.py
|
||||
*.exe
|
||||
*.zip
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
/.idea/
|
||||
/nbproject/
|
||||
*.pyc
|
||||
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include README.txt
|
||||
include release_notes.txt
|
||||
include requirements.txt
|
||||
recursive-include scripts *.py *.txt
|
||||
73
Makefile
73
Makefile
@@ -1,22 +1,9 @@
|
||||
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
|
||||
TAGGER_BASE ?= $(HOME)/Dropbox/tagger/comictagger
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
|
||||
PASSWORD := $(shell cat $(TAGGER_BASE)/project_password.txt)
|
||||
UPLOAD_TOOL := $(TAGGER_BASE)/google/googlecode_upload.py
|
||||
all: clean
|
||||
|
||||
clean:
|
||||
rm -rf *~ *.pyc *.pyo
|
||||
@@ -25,22 +12,50 @@ clean:
|
||||
rm -rf dist MANIFEST
|
||||
rm -rf *.deb
|
||||
rm -rf logdict*.log
|
||||
$(MAKE) -C mac clean
|
||||
make -C mac clean
|
||||
make -C windows clean
|
||||
rm -rf build
|
||||
rm -rf comictaggerlib/ui/__pycache__
|
||||
|
||||
pydist:
|
||||
make clean
|
||||
mkdir -p piprelease
|
||||
rm -f comictagger-$(VERSION_STR).zip
|
||||
mkdir -p release
|
||||
rm -f release/*.zip
|
||||
python setup.py sdist --formats=zip #,gztar
|
||||
mv dist/comictagger-$(VERSION_STR).zip piprelease
|
||||
rm -rf comictagger.egg-info dist
|
||||
mv dist/comictagger-$(VERSION_STR).zip release
|
||||
@echo When satisfied with release, do this:
|
||||
@echo make svn_tag
|
||||
|
||||
remove_test_install:
|
||||
sudo rm -rf /usr/local/bin/comictagger.py
|
||||
sudo rm -rf /usr/local/lib/python2.7/dist-packages/comictagger*
|
||||
|
||||
#deb:
|
||||
# fpm -s python -t deb \
|
||||
# -n 'comictagger' \
|
||||
# --category 'utilities' \
|
||||
# --maintainer 'comictagger@gmail.com' \
|
||||
# --after-install debian_scripts/after_install.sh \
|
||||
# --before-remove debian_scripts/before_remove.sh \
|
||||
# -d 'python >= 2.6' \
|
||||
# -d 'python < 2.8' \
|
||||
# -d 'python-imaging' \
|
||||
# -d 'python-bs4' \
|
||||
# --deb-suggests 'rar' \
|
||||
# --deb-suggests 'unrar-free' \
|
||||
# --python-install-bin /usr/share/comictagger \
|
||||
# --python-install-lib /usr/share/comictagger \
|
||||
# setup.py
|
||||
#
|
||||
# # For now, don't require PyQt, since command-line is available without it
|
||||
# #-d 'python-qt4 >= 4.8'
|
||||
|
||||
upload:
|
||||
#$(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)
|
||||
|
||||
65
README.md
65
README.md
@@ -1,16 +1,53 @@
|
||||
A fork from https://github.com/comictagger/comictagger
|
||||
This is a fork derived from google code:
|
||||
|
||||
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.
|
||||
https://code.google.com/p/comictagger/
|
||||
|
||||
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.
|
||||
|
||||
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!
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
version: 1.0.{build}
|
||||
build_script:
|
||||
- cmd: powershell -exec bypass -File windows\fullbuild.ps1
|
||||
artifacts:
|
||||
- path: dist\*.exe
|
||||
name: ComicTagger
|
||||
18
comicapi/UnRAR2/UnRARDLL/license.txt
Normal file
18
comicapi/UnRAR2/UnRARDLL/license.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
The unrar.dll library is freeware. This means:
|
||||
|
||||
1. All copyrights to RAR and the unrar.dll are exclusively
|
||||
owned by the author - Alexander Roshal.
|
||||
|
||||
2. The unrar.dll library may be used in any software to handle RAR
|
||||
archives without limitations free of charge.
|
||||
|
||||
3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS".
|
||||
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT
|
||||
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS,
|
||||
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING
|
||||
OR MISUSING THIS SOFTWARE.
|
||||
|
||||
Thank you for your interest in RAR and unrar.dll.
|
||||
|
||||
|
||||
Alexander L. Roshal
|
||||
BIN
comicapi/UnRAR2/UnRARDLL/unrar.dll
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/unrar.dll
Normal file
Binary file not shown.
140
comicapi/UnRAR2/UnRARDLL/unrar.h
Normal file
140
comicapi/UnRAR2/UnRARDLL/unrar.h
Normal file
@@ -0,0 +1,140 @@
|
||||
#ifndef _UNRAR_DLL_
|
||||
#define _UNRAR_DLL_
|
||||
|
||||
#define ERAR_END_ARCHIVE 10
|
||||
#define ERAR_NO_MEMORY 11
|
||||
#define ERAR_BAD_DATA 12
|
||||
#define ERAR_BAD_ARCHIVE 13
|
||||
#define ERAR_UNKNOWN_FORMAT 14
|
||||
#define ERAR_EOPEN 15
|
||||
#define ERAR_ECREATE 16
|
||||
#define ERAR_ECLOSE 17
|
||||
#define ERAR_EREAD 18
|
||||
#define ERAR_EWRITE 19
|
||||
#define ERAR_SMALL_BUF 20
|
||||
#define ERAR_UNKNOWN 21
|
||||
#define ERAR_MISSING_PASSWORD 22
|
||||
|
||||
#define RAR_OM_LIST 0
|
||||
#define RAR_OM_EXTRACT 1
|
||||
#define RAR_OM_LIST_INCSPLIT 2
|
||||
|
||||
#define RAR_SKIP 0
|
||||
#define RAR_TEST 1
|
||||
#define RAR_EXTRACT 2
|
||||
|
||||
#define RAR_VOL_ASK 0
|
||||
#define RAR_VOL_NOTIFY 1
|
||||
|
||||
#define RAR_DLL_VERSION 4
|
||||
|
||||
#ifdef _UNIX
|
||||
#define CALLBACK
|
||||
#define PASCAL
|
||||
#define LONG long
|
||||
#define HANDLE void *
|
||||
#define LPARAM long
|
||||
#define UINT unsigned int
|
||||
#endif
|
||||
|
||||
struct RARHeaderData
|
||||
{
|
||||
char ArcName[260];
|
||||
char FileName[260];
|
||||
unsigned int Flags;
|
||||
unsigned int PackSize;
|
||||
unsigned int UnpSize;
|
||||
unsigned int HostOS;
|
||||
unsigned int FileCRC;
|
||||
unsigned int FileTime;
|
||||
unsigned int UnpVer;
|
||||
unsigned int Method;
|
||||
unsigned int FileAttr;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
};
|
||||
|
||||
|
||||
struct RARHeaderDataEx
|
||||
{
|
||||
char ArcName[1024];
|
||||
wchar_t ArcNameW[1024];
|
||||
char FileName[1024];
|
||||
wchar_t FileNameW[1024];
|
||||
unsigned int Flags;
|
||||
unsigned int PackSize;
|
||||
unsigned int PackSizeHigh;
|
||||
unsigned int UnpSize;
|
||||
unsigned int UnpSizeHigh;
|
||||
unsigned int HostOS;
|
||||
unsigned int FileCRC;
|
||||
unsigned int FileTime;
|
||||
unsigned int UnpVer;
|
||||
unsigned int Method;
|
||||
unsigned int FileAttr;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Reserved[1024];
|
||||
};
|
||||
|
||||
|
||||
struct RAROpenArchiveData
|
||||
{
|
||||
char *ArcName;
|
||||
unsigned int OpenMode;
|
||||
unsigned int OpenResult;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
};
|
||||
|
||||
struct RAROpenArchiveDataEx
|
||||
{
|
||||
char *ArcName;
|
||||
wchar_t *ArcNameW;
|
||||
unsigned int OpenMode;
|
||||
unsigned int OpenResult;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Flags;
|
||||
unsigned int Reserved[32];
|
||||
};
|
||||
|
||||
enum UNRARCALLBACK_MESSAGES {
|
||||
UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD
|
||||
};
|
||||
|
||||
typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2);
|
||||
|
||||
typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode);
|
||||
typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData);
|
||||
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData);
|
||||
int PASCAL RARCloseArchive(HANDLE hArcData);
|
||||
int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData);
|
||||
int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData);
|
||||
int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName);
|
||||
int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName);
|
||||
void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData);
|
||||
void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc);
|
||||
void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc);
|
||||
void PASCAL RARSetPassword(HANDLE hArcData,char *Password);
|
||||
int PASCAL RARGetDllVersion();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
BIN
comicapi/UnRAR2/UnRARDLL/unrar.lib
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/unrar.lib
Normal file
Binary file not shown.
606
comicapi/UnRAR2/UnRARDLL/unrardll.txt
Normal file
606
comicapi/UnRAR2/UnRARDLL/unrardll.txt
Normal file
@@ -0,0 +1,606 @@
|
||||
|
||||
UnRAR.dll Manual
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
UnRAR.dll is a 32-bit Windows dynamic-link library which provides
|
||||
file extraction from RAR archives.
|
||||
|
||||
|
||||
Exported functions
|
||||
|
||||
====================================================================
|
||||
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Open RAR archive and allocate memory structures
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
ArchiveData Points to RAROpenArchiveData structure
|
||||
|
||||
struct RAROpenArchiveData
|
||||
{
|
||||
char *ArcName;
|
||||
UINT OpenMode;
|
||||
UINT OpenResult;
|
||||
char *CmtBuf;
|
||||
UINT CmtBufSize;
|
||||
UINT CmtSize;
|
||||
UINT CmtState;
|
||||
};
|
||||
|
||||
Structure fields:
|
||||
|
||||
ArcName
|
||||
Input parameter which should point to zero terminated string
|
||||
containing the archive name.
|
||||
|
||||
OpenMode
|
||||
Input parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
RAR_OM_LIST
|
||||
Open archive for reading file headers only.
|
||||
|
||||
RAR_OM_EXTRACT
|
||||
Open archive for testing and extracting files.
|
||||
|
||||
RAR_OM_LIST_INCSPLIT
|
||||
Open archive for reading file headers only. If you open an archive
|
||||
in such mode, RARReadHeader[Ex] will return all file headers,
|
||||
including those with "file continued from previous volume" flag.
|
||||
In case of RAR_OM_LIST such headers are automatically skipped.
|
||||
So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will
|
||||
get several file header records for same file if file is split between
|
||||
volumes. For such files only the last file header record will contain
|
||||
the correct file CRC and if you wish to get the correct packed size,
|
||||
you need to sum up packed sizes of all parts.
|
||||
|
||||
OpenResult
|
||||
Output parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
0 Success
|
||||
ERAR_NO_MEMORY Not enough memory to initialize data structures
|
||||
ERAR_BAD_DATA Archive header broken
|
||||
ERAR_BAD_ARCHIVE File is not valid RAR archive
|
||||
ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers
|
||||
ERAR_EOPEN File open error
|
||||
|
||||
CmtBuf
|
||||
Input parameter which should point to the buffer for archive
|
||||
comments. Maximum comment size is limited to 64Kb. Comment text is
|
||||
zero terminated. If the comment text is larger than the buffer
|
||||
size, the comment text will be truncated. If CmtBuf is set to
|
||||
NULL, comments will not be read.
|
||||
|
||||
CmtBufSize
|
||||
Input parameter which should contain size of buffer for archive
|
||||
comments.
|
||||
|
||||
CmtSize
|
||||
Output parameter containing size of comments actually read into the
|
||||
buffer, cannot exceed CmtBufSize.
|
||||
|
||||
CmtState
|
||||
Output parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
0 comments not present
|
||||
1 Comments read completely
|
||||
ERAR_NO_MEMORY Not enough memory to extract comments
|
||||
ERAR_BAD_DATA Broken comment
|
||||
ERAR_UNKNOWN_FORMAT Unknown comment format
|
||||
ERAR_SMALL_BUF Buffer too small, comments not completely read
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
Archive handle or NULL in case of error
|
||||
|
||||
|
||||
========================================================================
|
||||
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData)
|
||||
========================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure
|
||||
allowing to specify Unicode archive name and returning information
|
||||
about archive flags.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
ArchiveData Points to RAROpenArchiveDataEx structure
|
||||
|
||||
struct RAROpenArchiveDataEx
|
||||
{
|
||||
char *ArcName;
|
||||
wchar_t *ArcNameW;
|
||||
unsigned int OpenMode;
|
||||
unsigned int OpenResult;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Flags;
|
||||
unsigned int Reserved[32];
|
||||
};
|
||||
|
||||
Structure fields:
|
||||
|
||||
ArcNameW
|
||||
Input parameter which should point to zero terminated Unicode string
|
||||
containing the archive name or NULL if Unicode name is not specified.
|
||||
|
||||
Flags
|
||||
Output parameter. Combination of bit flags.
|
||||
|
||||
Possible values
|
||||
|
||||
0x0001 - Volume attribute (archive volume)
|
||||
0x0002 - Archive comment present
|
||||
0x0004 - Archive lock attribute
|
||||
0x0008 - Solid attribute (solid archive)
|
||||
0x0010 - New volume naming scheme ('volname.partN.rar')
|
||||
0x0020 - Authenticity information present
|
||||
0x0040 - Recovery record present
|
||||
0x0080 - Block headers are encrypted
|
||||
0x0100 - First volume (set only by RAR 3.0 and later)
|
||||
|
||||
Reserved[32]
|
||||
Reserved for future use. Must be zero.
|
||||
|
||||
Information on other structure fields and function return values
|
||||
is available above, in RAROpenArchive function description.
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARCloseArchive(HANDLE hArcData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Close RAR archive and release allocated memory. It must be called when
|
||||
archive processing is finished, even if the archive processing was stopped
|
||||
due to an error.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
0 Success
|
||||
ERAR_ECLOSE Archive close error
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARReadHeader(HANDLE hArcData,
|
||||
struct RARHeaderData *HeaderData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Read header of file in archive.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
HeaderData
|
||||
It should point to RARHeaderData structure:
|
||||
|
||||
struct RARHeaderData
|
||||
{
|
||||
char ArcName[260];
|
||||
char FileName[260];
|
||||
UINT Flags;
|
||||
UINT PackSize;
|
||||
UINT UnpSize;
|
||||
UINT HostOS;
|
||||
UINT FileCRC;
|
||||
UINT FileTime;
|
||||
UINT UnpVer;
|
||||
UINT Method;
|
||||
UINT FileAttr;
|
||||
char *CmtBuf;
|
||||
UINT CmtBufSize;
|
||||
UINT CmtSize;
|
||||
UINT CmtState;
|
||||
};
|
||||
|
||||
Structure fields:
|
||||
|
||||
ArcName
|
||||
Output parameter which contains a zero terminated string of the
|
||||
current archive name. May be used to determine the current volume
|
||||
name.
|
||||
|
||||
FileName
|
||||
Output parameter which contains a zero terminated string of the
|
||||
file name in OEM (DOS) encoding.
|
||||
|
||||
Flags
|
||||
Output parameter which contains file flags:
|
||||
|
||||
0x01 - file continued from previous volume
|
||||
0x02 - file continued on next volume
|
||||
0x04 - file encrypted with password
|
||||
0x08 - file comment present
|
||||
0x10 - compression of previous files is used (solid flag)
|
||||
|
||||
bits 7 6 5
|
||||
|
||||
0 0 0 - dictionary size 64 Kb
|
||||
0 0 1 - dictionary size 128 Kb
|
||||
0 1 0 - dictionary size 256 Kb
|
||||
0 1 1 - dictionary size 512 Kb
|
||||
1 0 0 - dictionary size 1024 Kb
|
||||
1 0 1 - dictionary size 2048 KB
|
||||
1 1 0 - dictionary size 4096 KB
|
||||
1 1 1 - file is directory
|
||||
|
||||
Other bits are reserved.
|
||||
|
||||
PackSize
|
||||
Output parameter means packed file size or size of the
|
||||
file part if file was split between volumes.
|
||||
|
||||
UnpSize
|
||||
Output parameter - unpacked file size.
|
||||
|
||||
HostOS
|
||||
Output parameter - operating system used for archiving:
|
||||
|
||||
0 - MS DOS;
|
||||
1 - OS/2.
|
||||
2 - Win32
|
||||
3 - Unix
|
||||
|
||||
FileCRC
|
||||
Output parameter which contains unpacked file CRC. In case of file parts
|
||||
split between volumes only the last part contains the correct CRC
|
||||
and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode.
|
||||
|
||||
FileTime
|
||||
Output parameter - contains date and time in standard MS DOS format.
|
||||
|
||||
UnpVer
|
||||
Output parameter - RAR version needed to extract file.
|
||||
It is encoded as 10 * Major version + minor version.
|
||||
|
||||
Method
|
||||
Output parameter - packing method.
|
||||
|
||||
FileAttr
|
||||
Output parameter - file attributes.
|
||||
|
||||
CmtBuf
|
||||
File comments support is not implemented in the new DLL version yet.
|
||||
Now CmtState is always 0.
|
||||
|
||||
/*
|
||||
* Input parameter which should point to the buffer for file
|
||||
* comments. Maximum comment size is limited to 64Kb. Comment text is
|
||||
* a zero terminated string in OEM encoding. If the comment text is
|
||||
* larger than the buffer size, the comment text will be truncated.
|
||||
* If CmtBuf is set to NULL, comments will not be read.
|
||||
*/
|
||||
|
||||
CmtBufSize
|
||||
Input parameter which should contain size of buffer for archive
|
||||
comments.
|
||||
|
||||
CmtSize
|
||||
Output parameter containing size of comments actually read into the
|
||||
buffer, should not exceed CmtBufSize.
|
||||
|
||||
CmtState
|
||||
Output parameter.
|
||||
|
||||
Possible values
|
||||
|
||||
0 Absent comments
|
||||
1 Comments read completely
|
||||
ERAR_NO_MEMORY Not enough memory to extract comments
|
||||
ERAR_BAD_DATA Broken comment
|
||||
ERAR_UNKNOWN_FORMAT Unknown comment format
|
||||
ERAR_SMALL_BUF Buffer too small, comments not completely read
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
0 Success
|
||||
ERAR_END_ARCHIVE End of archive
|
||||
ERAR_BAD_DATA File header broken
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARReadHeaderEx(HANDLE hArcData,
|
||||
struct RARHeaderDataEx *HeaderData)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Similar to RARReadHeader, but uses RARHeaderDataEx structure,
|
||||
containing information about Unicode file names and 64 bit file sizes.
|
||||
|
||||
struct RARHeaderDataEx
|
||||
{
|
||||
char ArcName[1024];
|
||||
wchar_t ArcNameW[1024];
|
||||
char FileName[1024];
|
||||
wchar_t FileNameW[1024];
|
||||
unsigned int Flags;
|
||||
unsigned int PackSize;
|
||||
unsigned int PackSizeHigh;
|
||||
unsigned int UnpSize;
|
||||
unsigned int UnpSizeHigh;
|
||||
unsigned int HostOS;
|
||||
unsigned int FileCRC;
|
||||
unsigned int FileTime;
|
||||
unsigned int UnpVer;
|
||||
unsigned int Method;
|
||||
unsigned int FileAttr;
|
||||
char *CmtBuf;
|
||||
unsigned int CmtBufSize;
|
||||
unsigned int CmtSize;
|
||||
unsigned int CmtState;
|
||||
unsigned int Reserved[1024];
|
||||
};
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARProcessFile(HANDLE hArcData,
|
||||
int Operation,
|
||||
char *DestPath,
|
||||
char *DestName)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Performs action and moves the current position in the archive to
|
||||
the next file. Extract or test the current file from the archive
|
||||
opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set,
|
||||
then a call to this function will simply skip the archive position
|
||||
to the next file.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
Operation
|
||||
File operation.
|
||||
|
||||
Possible values
|
||||
|
||||
RAR_SKIP Move to the next file in the archive. If the
|
||||
archive is solid and RAR_OM_EXTRACT mode was set
|
||||
when the archive was opened, the current file will
|
||||
be processed - the operation will be performed
|
||||
slower than a simple seek.
|
||||
|
||||
RAR_TEST Test the current file and move to the next file in
|
||||
the archive. If the archive was opened with
|
||||
RAR_OM_LIST mode, the operation is equal to
|
||||
RAR_SKIP.
|
||||
|
||||
RAR_EXTRACT Extract the current file and move to the next file.
|
||||
If the archive was opened with RAR_OM_LIST mode,
|
||||
the operation is equal to RAR_SKIP.
|
||||
|
||||
|
||||
DestPath
|
||||
This parameter should point to a zero terminated string containing the
|
||||
destination directory to which to extract files to. If DestPath is equal
|
||||
to NULL, it means extract to the current directory. This parameter has
|
||||
meaning only if DestName is NULL.
|
||||
|
||||
DestName
|
||||
This parameter should point to a string containing the full path and name
|
||||
to assign to extracted file or it can be NULL to use the default name.
|
||||
If DestName is defined (not NULL), it overrides both the original file
|
||||
name saved in the archive and path specigied in DestPath setting.
|
||||
|
||||
Both DestPath and DestName must be in OEM encoding. If necessary,
|
||||
use CharToOem to convert text to OEM before passing to this function.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
0 Success
|
||||
ERAR_BAD_DATA File CRC error
|
||||
ERAR_BAD_ARCHIVE Volume is not valid RAR archive
|
||||
ERAR_UNKNOWN_FORMAT Unknown archive format
|
||||
ERAR_EOPEN Volume open error
|
||||
ERAR_ECREATE File create error
|
||||
ERAR_ECLOSE File close error
|
||||
ERAR_EREAD Read error
|
||||
ERAR_EWRITE Write error
|
||||
|
||||
|
||||
Note: if you wish to cancel extraction, return -1 when processing
|
||||
UCM_PROCESSDATA callback message.
|
||||
|
||||
|
||||
====================================================================
|
||||
int PASCAL RARProcessFileW(HANDLE hArcData,
|
||||
int Operation,
|
||||
wchar_t *DestPath,
|
||||
wchar_t *DestName)
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Unicode version of RARProcessFile. It uses Unicode DestPath
|
||||
and DestName parameters, other parameters and return values
|
||||
are the same as in RARProcessFile.
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetCallback(HANDLE hArcData,
|
||||
int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2),
|
||||
LPARAM UserData);
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Set a user-defined callback function to process Unrar events.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
CallbackProc
|
||||
It should point to a user-defined callback function.
|
||||
|
||||
The function will be passed four parameters:
|
||||
|
||||
|
||||
msg Type of event. Described below.
|
||||
|
||||
UserData User defined value passed to RARSetCallback.
|
||||
|
||||
P1 and P2 Event dependent parameters. Described below.
|
||||
|
||||
|
||||
Possible events
|
||||
|
||||
UCM_CHANGEVOLUME Process volume change.
|
||||
|
||||
P1 Points to the zero terminated name
|
||||
of the next volume.
|
||||
|
||||
P2 The function call mode:
|
||||
|
||||
RAR_VOL_ASK Required volume is absent. The function should
|
||||
prompt user and return a positive value
|
||||
to retry or return -1 value to terminate
|
||||
operation. The function may also specify a new
|
||||
volume name, placing it to the address specified
|
||||
by P1 parameter.
|
||||
|
||||
RAR_VOL_NOTIFY Required volume is successfully opened.
|
||||
This is a notification call and volume name
|
||||
modification is not allowed. The function should
|
||||
return a positive value to continue or -1
|
||||
to terminate operation.
|
||||
|
||||
UCM_PROCESSDATA Process unpacked data. It may be used to read
|
||||
a file while it is being extracted or tested
|
||||
without actual extracting file to disk.
|
||||
Return a positive value to continue process
|
||||
or -1 to cancel the archive operation
|
||||
|
||||
P1 Address pointing to the unpacked data.
|
||||
Function may refer to the data but must not
|
||||
change it.
|
||||
|
||||
P2 Size of the unpacked data. It is guaranteed
|
||||
only that the size will not exceed the maximum
|
||||
dictionary size (4 Mb in RAR 3.0).
|
||||
|
||||
UCM_NEEDPASSWORD DLL needs a password to process archive.
|
||||
This message must be processed if you wish
|
||||
to be able to handle archives with encrypted
|
||||
file names. It can be also used as replacement
|
||||
of RARSetPassword function even for usual
|
||||
encrypted files with non-encrypted names.
|
||||
|
||||
P1 Address pointing to the buffer for a password.
|
||||
You need to copy a password here.
|
||||
|
||||
P2 Size of the password buffer.
|
||||
|
||||
|
||||
UserData
|
||||
User data passed to callback function.
|
||||
|
||||
Other functions of UnRAR.dll should not be called from the callback
|
||||
function.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
None
|
||||
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetChangeVolProc(HANDLE hArcData,
|
||||
int PASCAL (*ChangeVolProc)(char *ArcName,int Mode));
|
||||
====================================================================
|
||||
|
||||
Obsoleted, use RARSetCallback instead.
|
||||
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetProcessDataProc(HANDLE hArcData,
|
||||
int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size))
|
||||
====================================================================
|
||||
|
||||
Obsoleted, use RARSetCallback instead.
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARSetPassword(HANDLE hArcData,
|
||||
char *Password);
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Set a password to decrypt files.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
hArcData
|
||||
This parameter should contain the archive handle obtained from the
|
||||
RAROpenArchive function call.
|
||||
|
||||
Password
|
||||
It should point to a string containing a zero terminated password.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
None
|
||||
|
||||
|
||||
====================================================================
|
||||
void PASCAL RARGetDllVersion();
|
||||
====================================================================
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
Returns API version.
|
||||
|
||||
Parameters
|
||||
~~~~~~~~~~
|
||||
None.
|
||||
|
||||
Return values
|
||||
~~~~~~~~~~~~~
|
||||
Returns an integer value denoting UnRAR.dll API version, which is also
|
||||
defined in unrar.h as RAR_DLL_VERSION. API version number is incremented
|
||||
only in case of noticeable changes in UnRAR.dll API. Do not confuse it
|
||||
with version of UnRAR.dll stored in DLL resources, which is incremented
|
||||
with every DLL rebuild.
|
||||
|
||||
If RARGetDllVersion() returns a value lower than UnRAR.dll which your
|
||||
application was designed for, it may indicate that DLL version is too old
|
||||
and it will fail to provide all necessary functions to your application.
|
||||
|
||||
This function is absent in old versions of UnRAR.dll, so it is safer
|
||||
to use LoadLibrary and GetProcAddress to access this function.
|
||||
|
||||
80
comicapi/UnRAR2/UnRARDLL/whatsnew.txt
Normal file
80
comicapi/UnRAR2/UnRARDLL/whatsnew.txt
Normal file
@@ -0,0 +1,80 @@
|
||||
List of unrar.dll API changes. We do not include performance and reliability
|
||||
improvements into this list, but this library and RAR/UnRAR tools share
|
||||
the same source code. So the latest version of unrar.dll usually contains
|
||||
same decompression algorithm changes as the latest UnRAR version.
|
||||
============================================================================
|
||||
|
||||
-- 18 January 2008
|
||||
|
||||
all LONG parameters of CallbackProc function were changed
|
||||
to LPARAM type for 64 bit mode compatibility.
|
||||
|
||||
|
||||
-- 12 December 2007
|
||||
|
||||
Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive.
|
||||
|
||||
|
||||
-- 14 August 2007
|
||||
|
||||
Added NoCrypt\unrar_nocrypt.dll without decryption code for those
|
||||
applications where presence of encryption or decryption code is not
|
||||
allowed because of legal restrictions.
|
||||
|
||||
|
||||
-- 14 December 2006
|
||||
|
||||
Added ERAR_MISSING_PASSWORD error type. This error is returned
|
||||
if empty password is specified for encrypted file.
|
||||
|
||||
|
||||
-- 12 June 2003
|
||||
|
||||
Added RARProcessFileW function, Unicode version of RARProcessFile
|
||||
|
||||
|
||||
-- 9 August 2002
|
||||
|
||||
Added RAROpenArchiveEx function allowing to specify Unicode archive
|
||||
name and get archive flags.
|
||||
|
||||
|
||||
-- 24 January 2002
|
||||
|
||||
Added RARReadHeaderEx function allowing to read Unicode file names
|
||||
and 64 bit file sizes.
|
||||
|
||||
|
||||
-- 23 January 2002
|
||||
|
||||
Added ERAR_UNKNOWN error type (it is used for all errors which
|
||||
do not have special ERAR code yet) and UCM_NEEDPASSWORD callback
|
||||
message.
|
||||
|
||||
Unrar.dll automatically opens all next volumes not only when extracting,
|
||||
but also in RAR_OM_LIST mode.
|
||||
|
||||
|
||||
-- 27 November 2001
|
||||
|
||||
RARSetChangeVolProc and RARSetProcessDataProc are replaced by
|
||||
the single callback function installed with RARSetCallback.
|
||||
Unlike old style callbacks, the new function accepts the user defined
|
||||
parameter. Unrar.dll still supports RARSetChangeVolProc and
|
||||
RARSetProcessDataProc for compatibility purposes, but if you write
|
||||
a new application, better use RARSetCallback.
|
||||
|
||||
File comments support is not implemented in the new DLL version yet.
|
||||
Now CmtState is always 0.
|
||||
|
||||
|
||||
-- 13 August 2001
|
||||
|
||||
Added RARGetDllVersion function, so you may distinguish old unrar.dll,
|
||||
which used C style callback functions and the new one with PASCAL callbacks.
|
||||
|
||||
|
||||
-- 10 May 2001
|
||||
|
||||
Callback functions in RARSetChangeVolProc and RARSetProcessDataProc
|
||||
use PASCAL style call convention now.
|
||||
1
comicapi/UnRAR2/UnRARDLL/x64/readme.txt
Normal file
1
comicapi/UnRAR2/UnRARDLL/x64/readme.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is x64 version of unrar.dll.
|
||||
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll
Normal file
Binary file not shown.
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib
Normal file
BIN
comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib
Normal file
Binary file not shown.
177
comicapi/UnRAR2/__init__.py
Normal file
177
comicapi/UnRAR2/__init__.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""
|
||||
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
|
||||
|
||||
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
|
||||
stable and foolproof.
|
||||
Notice that it has INCOMPATIBLE interface.
|
||||
|
||||
It enables reading and unpacking of archives created with the
|
||||
RAR/WinRAR archivers. There is a low-level interface which is very
|
||||
similar to the C interface provided by UnRAR. There is also a
|
||||
higher level interface which makes some common operations easier.
|
||||
"""
|
||||
|
||||
__version__ = '0.99.3'
|
||||
|
||||
try:
|
||||
WindowsError
|
||||
in_windows = True
|
||||
except NameError:
|
||||
in_windows = False
|
||||
|
||||
if in_windows:
|
||||
from windows import RarFileImplementation
|
||||
else:
|
||||
from unix import RarFileImplementation
|
||||
|
||||
|
||||
import fnmatch, time, weakref
|
||||
|
||||
class RarInfo(object):
|
||||
"""Represents a file header in an archive. Don't instantiate directly.
|
||||
Use only to obtain information about file.
|
||||
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.
|
||||
USE METHODS OF RarFile CLASS INSTEAD.
|
||||
|
||||
Properties:
|
||||
index - index of file within the archive
|
||||
filename - name of the file in the archive including path (if any)
|
||||
datetime - file date/time as a struct_time suitable for time.strftime
|
||||
isdir - True if the file is a directory
|
||||
size - size in bytes of the uncompressed file
|
||||
comment - comment associated with the file
|
||||
|
||||
Note - this is not currently intended to be a Python file-like object.
|
||||
"""
|
||||
|
||||
def __init__(self, rarfile, data):
|
||||
self.rarfile = weakref.proxy(rarfile)
|
||||
self.index = data['index']
|
||||
self.filename = data['filename']
|
||||
self.isdir = data['isdir']
|
||||
self.size = data['size']
|
||||
self.datetime = data['datetime']
|
||||
self.comment = data['comment']
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
try :
|
||||
arcName = self.rarfile.archiveName
|
||||
except ReferenceError:
|
||||
arcName = "[ARCHIVE_NO_LONGER_LOADED]"
|
||||
return '<RarInfo "%s" in "%s">' % (self.filename, arcName)
|
||||
|
||||
class RarFile(RarFileImplementation):
|
||||
|
||||
def __init__(self, archiveName, password=None):
|
||||
"""Instantiate the archive.
|
||||
|
||||
archiveName is the name of the RAR file.
|
||||
password is used to decrypt the files in the archive.
|
||||
|
||||
Properties:
|
||||
comment - comment associated with the archive
|
||||
|
||||
>>> print RarFile('test.rar').comment
|
||||
This is a test.
|
||||
"""
|
||||
self.archiveName = archiveName
|
||||
RarFileImplementation.init(self, password)
|
||||
|
||||
def __del__(self):
|
||||
self.destruct()
|
||||
|
||||
def infoiter(self):
|
||||
"""Iterate over all the files in the archive, generating RarInfos.
|
||||
|
||||
>>> import os
|
||||
>>> for fileInArchive in RarFile('test.rar').infoiter():
|
||||
... print os.path.split(fileInArchive.filename)[-1],
|
||||
... print fileInArchive.isdir,
|
||||
... print fileInArchive.size,
|
||||
... print fileInArchive.comment,
|
||||
... print tuple(fileInArchive.datetime)[0:5],
|
||||
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)
|
||||
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59
|
||||
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01
|
||||
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47
|
||||
"""
|
||||
for params in RarFileImplementation.infoiter(self):
|
||||
yield RarInfo(self, params)
|
||||
|
||||
def infolist(self):
|
||||
"""Return a list of RarInfos, descripting the contents of the archive."""
|
||||
return list(self.infoiter())
|
||||
|
||||
def read_files(self, condition='*'):
|
||||
"""Read specific files from archive into memory.
|
||||
If "condition" is a list of numbers, then return files which have those positions in infolist.
|
||||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||
and returns boolean True (extract) or False (skip).
|
||||
If "condition" is omitted, all files are returned.
|
||||
|
||||
Returns list of tuples (RarInfo info, str contents)
|
||||
"""
|
||||
checker = condition2checker(condition)
|
||||
return RarFileImplementation.read_files(self, checker)
|
||||
|
||||
|
||||
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
|
||||
"""Extract specific files from archive to disk.
|
||||
|
||||
If "condition" is a list of numbers, then extract files which have those positions in infolist.
|
||||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||
and returns either boolean True (extract) or boolean False (skip).
|
||||
DEPRECATED: If "condition" callback returns string (only supported for Windows) -
|
||||
that string will be used as a new name to save the file under.
|
||||
If "condition" is omitted, all files are extracted.
|
||||
|
||||
"path" is a directory to extract to
|
||||
"withSubpath" flag denotes whether files are extracted with their full path in the archive.
|
||||
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
|
||||
|
||||
Returns list of RarInfos for extracted files."""
|
||||
checker = condition2checker(condition)
|
||||
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
|
||||
|
||||
def condition2checker(condition):
|
||||
"""Converts different condition types to callback"""
|
||||
if type(condition) in [str, unicode]:
|
||||
def smatcher(info):
|
||||
return fnmatch.fnmatch(info.filename, condition)
|
||||
return smatcher
|
||||
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]:
|
||||
def imatcher(info):
|
||||
return info.index in condition
|
||||
return imatcher
|
||||
elif callable(condition):
|
||||
return condition
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
30
comicapi/UnRAR2/rar_exceptions.py
Normal file
30
comicapi/UnRAR2/rar_exceptions.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||
|
||||
|
||||
class ArchiveHeaderBroken(Exception): pass
|
||||
class InvalidRARArchive(Exception): pass
|
||||
class FileOpenError(Exception): pass
|
||||
class IncorrectRARPassword(Exception): pass
|
||||
class InvalidRARArchiveUsage(Exception): pass
|
||||
138
comicapi/UnRAR2/test_UnRAR2.py
Normal file
138
comicapi/UnRAR2/test_UnRAR2.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import os, sys
|
||||
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
|
||||
|
||||
def cleanup(dir='test'):
|
||||
for path, dirs, files in os.walk(dir):
|
||||
for fn in files:
|
||||
os.remove(os.path.join(path, fn))
|
||||
for dir in dirs:
|
||||
os.removedirs(os.path.join(path, dir))
|
||||
|
||||
|
||||
# basic test
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test.rar')
|
||||
rarc.infolist()
|
||||
assert rarc.comment == "This is a test."
|
||||
for info in rarc.infoiter():
|
||||
saveinfo = info
|
||||
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
|
||||
break
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert os.path.exists('test'+os.sep+'this.py')
|
||||
del rarc
|
||||
assert (str(saveinfo)=="""<RarInfo "test" in "[ARCHIVE_NO_LONGER_LOADED]">""")
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test.rar').extract()
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar matching the wildcard *.txt
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt')
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert not os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
|
||||
# check the name and size of each file, extracting small ones
|
||||
cleanup()
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
assert archive.comment == 'This is a test.'
|
||||
archive.extract(lambda rarinfo: rarinfo.size <= 1024)
|
||||
for rarinfo in archive.infoiter():
|
||||
if rarinfo.size <= 1024 and not rarinfo.isdir:
|
||||
assert rarinfo.size == os.stat(rarinfo.filename).st_size
|
||||
assert file('test'+os.sep+'test.txt', 'rt').read() == 'This is only a test.'
|
||||
assert not os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
|
||||
# extract this.py, overriding it's destination
|
||||
cleanup('test2')
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
archive.extract('*.py', 'test2', False)
|
||||
assert os.path.exists('test2'+os.sep+'this.py')
|
||||
cleanup('test2')
|
||||
|
||||
|
||||
# extract test.txt to memory
|
||||
cleanup()
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
entries = UnRAR2.RarFile('test.rar').read_files('*test.txt')
|
||||
assert len(entries)==1
|
||||
assert entries[0][0].filename.endswith('test.txt')
|
||||
assert entries[0][1]=='This is only a test.'
|
||||
|
||||
|
||||
# extract all the files in test.rar with overwriting
|
||||
cleanup()
|
||||
fo = open('test'+os.sep+'test.txt',"wt")
|
||||
fo.write("blah")
|
||||
fo.close()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt')
|
||||
assert open('test'+os.sep+'test.txt',"rt").read()!="blah"
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar without overwriting
|
||||
cleanup()
|
||||
fo = open('test'+os.sep+'test.txt',"wt")
|
||||
fo.write("blahblah")
|
||||
fo.close()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt', overwrite = False)
|
||||
assert open('test'+os.sep+'test.txt',"rt").read()=="blahblah"
|
||||
cleanup()
|
||||
|
||||
# list big file in an archive
|
||||
list(UnRAR2.RarFile('test_nulls.rar').infoiter())
|
||||
|
||||
# extract files from an archive with protected files
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected")
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
try:
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract()
|
||||
except IncorrectRARPassword:
|
||||
errored = True
|
||||
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
assert errored
|
||||
cleanup()
|
||||
|
||||
# extract files from an archive with protected headers
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
try:
|
||||
UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract()
|
||||
except IncorrectRARPassword:
|
||||
errored = True
|
||||
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
assert errored
|
||||
cleanup()
|
||||
|
||||
# make sure docstring examples are working
|
||||
import doctest
|
||||
doctest.testmod(UnRAR2)
|
||||
|
||||
# update documentation
|
||||
import pydoc
|
||||
pydoc.writedoc(UnRAR2)
|
||||
|
||||
# cleanup
|
||||
try:
|
||||
os.remove('__init__.pyc')
|
||||
except:
|
||||
pass
|
||||
218
comicapi/UnRAR2/unix.py
Normal file
218
comicapi/UnRAR2/unix.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Unix version uses unrar command line executable
|
||||
|
||||
import subprocess
|
||||
import gc
|
||||
|
||||
import os, os.path
|
||||
import time, re
|
||||
|
||||
from rar_exceptions import *
|
||||
|
||||
class UnpackerNotInstalled(Exception): pass
|
||||
|
||||
rar_executable_cached = None
|
||||
rar_executable_version = None
|
||||
|
||||
def call_unrar(params):
|
||||
"Calls rar/unrar command line executable, returns stdout pipe"
|
||||
global rar_executable_cached
|
||||
if rar_executable_cached is None:
|
||||
for command in ('unrar', 'rar'):
|
||||
try:
|
||||
subprocess.Popen([command], stdout=subprocess.PIPE)
|
||||
rar_executable_cached = command
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
if rar_executable_cached is None:
|
||||
raise UnpackerNotInstalled("No suitable RAR unpacker installed")
|
||||
|
||||
assert type(params) == list, "params must be list"
|
||||
args = [rar_executable_cached] + params
|
||||
try:
|
||||
gc.disable() # See http://bugs.python.org/issue1336
|
||||
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
global rar_executable_version
|
||||
self.password = password
|
||||
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', []).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
raise FileOpenError
|
||||
if line.find("CRC failed")>=0:
|
||||
raise IncorrectRARPassword
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not (line.startswith('UNRAR')):
|
||||
line = source.next()
|
||||
signature = line
|
||||
# The code below is mighty flaky
|
||||
# and will probably crash on localized versions of RAR
|
||||
# but I see no safe way to rewrite it using a CLI tool
|
||||
if signature.startswith("UNRAR 4"):
|
||||
rar_executable_version = 4
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:] # strip out "Comment:" part
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
else:
|
||||
self.comment = None
|
||||
elif signature.startswith("UNRAR 5"):
|
||||
rar_executable_version = 5
|
||||
line = source.next()
|
||||
while not line.startswith('Archive:'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
self.comment = '\n'.join(accum[:-1]).strip()
|
||||
else:
|
||||
self.comment = None
|
||||
else:
|
||||
raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: "
|
||||
+ signature.split(" ")[1])
|
||||
|
||||
|
||||
def escaped_password(self):
|
||||
return '-' if self.password == None else self.password
|
||||
|
||||
|
||||
def call(self, cmd, options=[], files=[]):
|
||||
options2 = options + ['p'+self.escaped_password()]
|
||||
soptions = ['-'+x for x in options2]
|
||||
return call_unrar([cmd]+soptions+['--',self.archiveName]+files)
|
||||
|
||||
def infoiter(self):
|
||||
|
||||
command = "v" if rar_executable_version == 4 else "l"
|
||||
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
raise FileOpenError
|
||||
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not line.startswith('-----------'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
if line.startswith("CRC failed") or line.startswith("Checksum error"):
|
||||
raise IncorrectRARPassword
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
i = 0
|
||||
re_spaces = re.compile(r"\s+")
|
||||
if rar_executable_version == 4:
|
||||
while not line.startswith('-----------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
# asterisks mark password-encrypted files
|
||||
data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files
|
||||
fields = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(fields[0])
|
||||
attr = fields[5]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
elif rar_executable_version == 5:
|
||||
while not line.startswith('-----------'):
|
||||
fields = line.strip().lstrip("*").split()
|
||||
data = {}
|
||||
data['index'] = i
|
||||
data['filename'] = " ".join(fields[4:])
|
||||
data['size'] = int(fields[1])
|
||||
attr = fields[0]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
i += 1
|
||||
line = source.next()
|
||||
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if checkres==True and not info.isdir:
|
||||
pipe = self.call('p', ['inul'], [info.filename]).stdout
|
||||
res.append((info, pipe.read()))
|
||||
return res
|
||||
|
||||
|
||||
def extract(self, checker, path, withSubpath, overwrite):
|
||||
res = []
|
||||
command = 'x'
|
||||
if not withSubpath:
|
||||
command = 'e'
|
||||
options = []
|
||||
if overwrite:
|
||||
options.append('o+')
|
||||
else:
|
||||
options.append('o-')
|
||||
if not path.endswith(os.sep):
|
||||
path += os.sep
|
||||
names = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if type(checkres) in [str, unicode]:
|
||||
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
|
||||
if checkres==True and not info.isdir:
|
||||
names.append(info.filename)
|
||||
res.append(info)
|
||||
names.append(path)
|
||||
proc = self.call(command, options, names)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
|
||||
raise IncorrectRARPassword
|
||||
return res
|
||||
|
||||
def destruct(self):
|
||||
pass
|
||||
|
||||
|
||||
309
comicapi/UnRAR2/windows.py
Normal file
309
comicapi/UnRAR2/windows.py
Normal file
@@ -0,0 +1,309 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||
|
||||
from __future__ import generators
|
||||
|
||||
import ctypes, ctypes.wintypes
|
||||
import os, os.path, sys
|
||||
import Queue
|
||||
import time
|
||||
|
||||
from rar_exceptions import *
|
||||
|
||||
ERAR_END_ARCHIVE = 10
|
||||
ERAR_NO_MEMORY = 11
|
||||
ERAR_BAD_DATA = 12
|
||||
ERAR_BAD_ARCHIVE = 13
|
||||
ERAR_UNKNOWN_FORMAT = 14
|
||||
ERAR_EOPEN = 15
|
||||
ERAR_ECREATE = 16
|
||||
ERAR_ECLOSE = 17
|
||||
ERAR_EREAD = 18
|
||||
ERAR_EWRITE = 19
|
||||
ERAR_SMALL_BUF = 20
|
||||
ERAR_UNKNOWN = 21
|
||||
|
||||
RAR_OM_LIST = 0
|
||||
RAR_OM_EXTRACT = 1
|
||||
|
||||
RAR_SKIP = 0
|
||||
RAR_TEST = 1
|
||||
RAR_EXTRACT = 2
|
||||
|
||||
RAR_VOL_ASK = 0
|
||||
RAR_VOL_NOTIFY = 1
|
||||
|
||||
RAR_DLL_VERSION = 3
|
||||
|
||||
# enum UNRARCALLBACK_MESSAGES
|
||||
UCM_CHANGEVOLUME = 0
|
||||
UCM_PROCESSDATA = 1
|
||||
UCM_NEEDPASSWORD = 2
|
||||
|
||||
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
|
||||
dll_name = "unrar.dll"
|
||||
if architecture_bits == 64:
|
||||
dll_name = "x64\\unrar64.dll"
|
||||
|
||||
|
||||
try:
|
||||
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
|
||||
except WindowsError:
|
||||
unrar = ctypes.WinDLL(dll_name)
|
||||
|
||||
|
||||
class RAROpenArchiveDataEx(ctypes.Structure):
|
||||
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
|
||||
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||
|
||||
_fields_ = [
|
||||
('ArcName', ctypes.c_char_p),
|
||||
('ArcNameW', ctypes.c_wchar_p),
|
||||
('OpenMode', ctypes.c_uint),
|
||||
('OpenResult', ctypes.c_uint),
|
||||
('_CmtBuf', ctypes.c_voidp),
|
||||
('CmtBufSize', ctypes.c_uint),
|
||||
('CmtSize', ctypes.c_uint),
|
||||
('CmtState', ctypes.c_uint),
|
||||
('Flags', ctypes.c_uint),
|
||||
('Reserved', ctypes.c_uint*32),
|
||||
]
|
||||
|
||||
class RARHeaderDataEx(ctypes.Structure):
|
||||
def __init__(self):
|
||||
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||
|
||||
_fields_ = [
|
||||
('ArcName', ctypes.c_char*1024),
|
||||
('ArcNameW', ctypes.c_wchar*1024),
|
||||
('FileName', ctypes.c_char*1024),
|
||||
('FileNameW', ctypes.c_wchar*1024),
|
||||
('Flags', ctypes.c_uint),
|
||||
('PackSize', ctypes.c_uint),
|
||||
('PackSizeHigh', ctypes.c_uint),
|
||||
('UnpSize', ctypes.c_uint),
|
||||
('UnpSizeHigh', ctypes.c_uint),
|
||||
('HostOS', ctypes.c_uint),
|
||||
('FileCRC', ctypes.c_uint),
|
||||
('FileTime', ctypes.c_uint),
|
||||
('UnpVer', ctypes.c_uint),
|
||||
('Method', ctypes.c_uint),
|
||||
('FileAttr', ctypes.c_uint),
|
||||
('_CmtBuf', ctypes.c_voidp),
|
||||
('CmtBufSize', ctypes.c_uint),
|
||||
('CmtSize', ctypes.c_uint),
|
||||
('CmtState', ctypes.c_uint),
|
||||
('Reserved', ctypes.c_uint*1024),
|
||||
]
|
||||
|
||||
def DosDateTimeToTimeTuple(dosDateTime):
|
||||
"""Convert an MS-DOS format date time to a Python time tuple.
|
||||
"""
|
||||
dosDate = dosDateTime >> 16
|
||||
dosTime = dosDateTime & 0xffff
|
||||
day = dosDate & 0x1f
|
||||
month = (dosDate >> 5) & 0xf
|
||||
year = 1980 + (dosDate >> 9)
|
||||
second = 2*(dosTime & 0x1f)
|
||||
minute = (dosTime >> 5) & 0x3f
|
||||
hour = dosTime >> 11
|
||||
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))
|
||||
|
||||
def _wrap(restype, function, argtypes):
|
||||
result = function
|
||||
result.argtypes = argtypes
|
||||
result.restype = restype
|
||||
return result
|
||||
|
||||
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])
|
||||
|
||||
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])
|
||||
|
||||
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])
|
||||
|
||||
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
|
||||
def RARSetPassword(*args, **kwargs):
|
||||
_RARSetPassword(*args, **kwargs)
|
||||
|
||||
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])
|
||||
|
||||
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])
|
||||
|
||||
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
|
||||
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])
|
||||
|
||||
|
||||
|
||||
RARExceptions = {
|
||||
ERAR_NO_MEMORY : MemoryError,
|
||||
ERAR_BAD_DATA : ArchiveHeaderBroken,
|
||||
ERAR_BAD_ARCHIVE : InvalidRARArchive,
|
||||
ERAR_EOPEN : FileOpenError,
|
||||
}
|
||||
|
||||
class PassiveReader:
|
||||
"""Used for reading files to memory"""
|
||||
def __init__(self, usercallback = None):
|
||||
self.buf = []
|
||||
self.ucb = usercallback
|
||||
|
||||
def _callback(self, msg, UserData, P1, P2):
|
||||
if msg == UCM_PROCESSDATA:
|
||||
data = (ctypes.c_char*P2).from_address(P1).raw
|
||||
if self.ucb!=None:
|
||||
self.ucb(data)
|
||||
else:
|
||||
self.buf.append(data)
|
||||
return 1
|
||||
|
||||
def get_result(self):
|
||||
return ''.join(self.buf)
|
||||
|
||||
class RarInfoIterator(object):
|
||||
def __init__(self, arc):
|
||||
self.arc = arc
|
||||
self.index = 0
|
||||
self.headerData = RARHeaderDataEx()
|
||||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||
if self.res==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
self.arc.lockStatus = "locked"
|
||||
self.arc.needskip = False
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if self.index>0:
|
||||
if self.arc.needskip:
|
||||
RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
|
||||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||
|
||||
if self.res:
|
||||
raise StopIteration
|
||||
self.arc.needskip = True
|
||||
|
||||
data = {}
|
||||
data['index'] = self.index
|
||||
data['filename'] = self.headerData.FileName
|
||||
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
|
||||
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
|
||||
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
|
||||
if self.headerData.CmtState == 1:
|
||||
data['comment'] = self.headerData.CmtBuf.value
|
||||
else:
|
||||
data['comment'] = None
|
||||
self.index += 1
|
||||
return data
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.arc.lockStatus = "finished"
|
||||
|
||||
def generate_password_provider(password):
|
||||
def password_provider_callback(msg, UserData, P1, P2):
|
||||
if msg == UCM_NEEDPASSWORD and password!=None:
|
||||
(ctypes.c_char*P2).from_address(P1).value = password
|
||||
return 1
|
||||
return password_provider_callback
|
||||
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
self.password = password
|
||||
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
|
||||
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
|
||||
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
|
||||
RARSetCallback(self._handle, self.c_callback, 1)
|
||||
|
||||
if archiveData.OpenResult != 0:
|
||||
raise RARExceptions[archiveData.OpenResult]
|
||||
|
||||
if archiveData.CmtState == 1:
|
||||
self.comment = archiveData.CmtBuf.value
|
||||
else:
|
||||
self.comment = None
|
||||
|
||||
if password:
|
||||
RARSetPassword(self._handle, password)
|
||||
|
||||
self.lockStatus = "ready"
|
||||
|
||||
|
||||
|
||||
def destruct(self):
|
||||
if self._handle and RARCloseArchive:
|
||||
RARCloseArchive(self._handle)
|
||||
|
||||
def make_sure_ready(self):
|
||||
if self.lockStatus == "locked":
|
||||
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
|
||||
if self.lockStatus == "finished":
|
||||
self.destruct()
|
||||
self.init(self.password)
|
||||
|
||||
def infoiter(self):
|
||||
self.make_sure_ready()
|
||||
return RarInfoIterator(self)
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
if checker(info) and not info.isdir:
|
||||
reader = PassiveReader()
|
||||
c_callback = UNRARCALLBACK(reader._callback)
|
||||
RARSetCallback(self._handle, c_callback, 1)
|
||||
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
|
||||
if tmpres==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
self.needskip = False
|
||||
res.append((info, reader.get_result()))
|
||||
return res
|
||||
|
||||
|
||||
def extract(self, checker, path, withSubpath, overwrite):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if checkres!=False and not info.isdir:
|
||||
if checkres==True:
|
||||
fn = info.filename
|
||||
if not withSubpath:
|
||||
fn = os.path.split(fn)[-1]
|
||||
target = os.path.join(path, fn)
|
||||
else:
|
||||
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
|
||||
target = checkres
|
||||
if overwrite or (not os.path.exists(target)):
|
||||
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
|
||||
if tmpres==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
|
||||
self.needskip = False
|
||||
res.append(info)
|
||||
return res
|
||||
|
||||
|
||||
1
comicapi/__init__.py
Normal file
1
comicapi/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__author__ = 'dromanin'
|
||||
276
comicapi/comet.py
Normal file
276
comicapi/comet.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""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)
|
||||
1173
comicapi/comicarchive.py
Normal file
1173
comicapi/comicarchive.py
Normal file
File diff suppressed because it is too large
Load Diff
144
comicapi/comicbookinfo.py
Normal file
144
comicapi/comicbookinfo.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""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
|
||||
290
comicapi/comicinfoxml.py
Normal file
290
comicapi/comicinfoxml.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""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)
|
||||
292
comicapi/filenameparser.py
Normal file
292
comicapi/filenameparser.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""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
|
||||
321
comicapi/genericmetadata.py
Normal file
321
comicapi/genericmetadata.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""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
|
||||
133
comicapi/issuestring.py
Normal file
133
comicapi/issuestring.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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)
|
||||
592
comicapi/utils.py
Normal file
592
comicapi/utils.py
Normal file
@@ -0,0 +1,592 @@
|
||||
# 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]
|
||||
@@ -1,5 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# we are running in a bundle
|
||||
frozen = 'ever so'
|
||||
bundle_dir = sys._MEIPASS
|
||||
else:
|
||||
# we are running in a normal Python environment
|
||||
bundle_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# setup libunrar
|
||||
if not os.environ.get("UNRAR_LIB_PATH", None):
|
||||
os.environ["UNRAR_LIB_PATH"] = bundle_dir + "/libunrar.so"
|
||||
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
ctmain()
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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
@@ -1 +0,0 @@
|
||||
from ._version import version as __version__
|
||||
|
||||
@@ -15,43 +15,54 @@
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
#import sys
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
|
||||
from .comicarchive import MetaDataStyle
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from .settings import ComicTaggerSettings
|
||||
#from imagefetcher import ImageFetcher
|
||||
#from comicvinetalker import ComicVineTalker
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, match_set_list, 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 = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
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 = 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.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
|
||||
self.buttonBox.addButton(
|
||||
self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(
|
||||
"Accept and Write Tags")
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
@@ -67,11 +78,13 @@ class AutoTagMatchWindow(QtWidgets.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(QtWidgets.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
# self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.buttonBox.button(
|
||||
QtGui.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
# self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.skipButton.setText(self.tr("Skip"))
|
||||
|
||||
self.setCoverImage()
|
||||
@@ -81,7 +94,10 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle(
|
||||
"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])
|
||||
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])
|
||||
)
|
||||
|
||||
def populateTable(self):
|
||||
@@ -95,39 +111,39 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
for match in self.current_match_set.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match["series"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match["publisher"] is not None:
|
||||
item_text = "{0}".format(match["publisher"])
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = "Unknown"
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
month_str = ""
|
||||
year_str = "????"
|
||||
if match["month"] is not None:
|
||||
month_str = "-{0:02d}".format(int(match["month"]))
|
||||
if match["year"] is not None:
|
||||
year_str = "{0}".format(match["year"])
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match["issue_title"]
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
@@ -151,11 +167,11 @@ class AutoTagMatchWindow(QtWidgets.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
|
||||
@@ -163,7 +179,8 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
def currentMatch(self):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data(QtCore.Qt.UserRole)[0]
|
||||
match = self.twList.item(row, 0).data(
|
||||
QtCore.Qt.UserRole).toPyObject()[0]
|
||||
return match
|
||||
|
||||
def accept(self):
|
||||
@@ -173,7 +190,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
if self.current_match_set_idx == len(self.match_set_list):
|
||||
# no more items
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
@@ -182,23 +199,22 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
if self.current_match_set_idx == len(self.match_set_list):
|
||||
# no more items
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QtGui.QDialog.reject(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def reject(self):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
reply = QtGui.QMessageBox.question(
|
||||
self,
|
||||
self.tr("Cancel Matching"),
|
||||
self.tr("Are you sure you wish to cancel the matching process?"),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No,
|
||||
)
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QtGui.QDialog.reject(self)
|
||||
|
||||
def saveMatch(self):
|
||||
|
||||
@@ -212,15 +228,18 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
# now get the particular issue data
|
||||
cv_md = self.fetch_func(match)
|
||||
if cv_md is None:
|
||||
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to get issue details!"))
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr(
|
||||
"Could not connect to Comic Vine to get issue details!"))
|
||||
return
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay(cv_md)
|
||||
success = ca.writeMetadata(md, self.style)
|
||||
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
QtWidgets.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))
|
||||
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr(
|
||||
"Saving the tags to the archive seemed to fail!"))
|
||||
|
||||
@@ -14,33 +14,42 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from .settings import ComicTaggerSettings
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagProgressWindow(QtWidgets.QDialog):
|
||||
class AutoTagProgressWindow(QtGui.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 = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
|
||||
self.testCoverWidget = CoverImageWidget(
|
||||
self.testCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtGui.QGridLayout(self.testCoverContainer)
|
||||
gridlayout.addWidget(self.testCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.isdone = False
|
||||
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
reduceWidgetFontSize(self.textEdit)
|
||||
|
||||
@@ -56,5 +65,5 @@ class AutoTagProgressWindow(QtWidgets.QDialog):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
||||
@@ -14,30 +14,39 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
#import os
|
||||
|
||||
from .settings import ComicTaggerSettings
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
#from settingswindow import SettingsWindow
|
||||
#from filerenamer import FileRenamer
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, settings, msg):
|
||||
super(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.cbxAutoImprint.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.leNameLengthMatchTolerance.setText(str(self.settings.id_length_delta_thresh))
|
||||
self.leNameLengthMatchTolerance.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.leSearchString.setEnabled(False)
|
||||
|
||||
if self.settings.save_on_low_confidence:
|
||||
@@ -47,34 +56,37 @@ class AutoTagStartWindow(QtWidgets.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
|
||||
@@ -90,14 +102,15 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.leSearchString.setEnabled(enable)
|
||||
|
||||
def accept(self):
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.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
|
||||
@@ -109,6 +122,6 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.searchString = str(self.leSearchString.text())
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
||||
self.searchString = None
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
# 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
|
||||
@@ -29,7 +33,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)
|
||||
|
||||
@@ -40,25 +44,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)
|
||||
|
||||
@@ -16,31 +16,39 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import os
|
||||
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
|
||||
import json
|
||||
#import signal
|
||||
#import traceback
|
||||
#import time
|
||||
#import platform
|
||||
#import locale
|
||||
#import codecs
|
||||
|
||||
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 = []
|
||||
@@ -49,6 +57,8 @@ class OnlineMatchResults:
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
|
||||
def actual_issue_data_fetch(match, settings, opts):
|
||||
|
||||
@@ -56,9 +66,10 @@ 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("Network error while getting issue details. Save aborted", file=sys.stderr)
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
@@ -72,49 +83,55 @@ 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("The tag save seemed to fail!", file=sys.stderr)
|
||||
print >> sys.stderr, "The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print("Save complete.", file=sys.stderr)
|
||||
print >> sys.stderr, "Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print("dry-run option was set, so nothing was written", file=sys.stderr)
|
||||
print >> sys.stderr, "dry-run option was set, so nothing was written"
|
||||
else:
|
||||
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))
|
||||
print >> sys.stderr, "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print(u"{0}".format(md))
|
||||
return True
|
||||
|
||||
|
||||
def display_match_set_for_choice(label, match_set, opts, settings):
|
||||
print("{0} -- {1}:".format(match_set.filename, label))
|
||||
print(u"{0} -- {1}:".format(match_set.filename, label))
|
||||
|
||||
# sort match list by year
|
||||
match_set.matches.sort(key=lambda k: k["year"])
|
||||
match_set.matches.sort(key=lambda k: k['year'])
|
||||
|
||||
for (counter, m) in enumerate(match_set.matches):
|
||||
counter += 1
|
||||
print(
|
||||
" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
|
||||
counter, m["series"], m["issue_number"], m["publisher"], m["month"], m["year"], m["issue_title"]
|
||||
)
|
||||
)
|
||||
u" {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 = input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(
|
||||
1, len(match_set.matches) + 1)) or i == 's':
|
||||
break
|
||||
if i != "s":
|
||||
if i != 's':
|
||||
i = int(i) - 1
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = 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)
|
||||
|
||||
|
||||
@@ -146,9 +163,11 @@ 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------------------")
|
||||
@@ -163,14 +182,14 @@ def post_process_matches(match_results, opts, settings):
|
||||
|
||||
def cli_mode(opts, settings):
|
||||
if len(opts.file_list) < 1:
|
||||
print("You must specify at least one filename. Use the -h option for more info", file=sys.stderr)
|
||||
print >> sys.stderr, "You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
if isinstance(f, str):
|
||||
pass
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli(f, opts, settings, match_results)
|
||||
sys.stdout.flush()
|
||||
|
||||
@@ -200,22 +219,25 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
|
||||
batch_mode = len(opts.file_list) > 1
|
||||
|
||||
settings.auto_imprint = opts.auto_imprint
|
||||
|
||||
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
|
||||
ca = ComicArchive(
|
||||
filename,
|
||||
settings.rar_exe_path,
|
||||
ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
|
||||
if not os.path.lexists(filename):
|
||||
print("Cannot find " + filename, file=sys.stderr)
|
||||
print >> sys.stderr, "Cannot find " + filename
|
||||
return
|
||||
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print("Sorry, but " + filename + " is not a comic archive!", file=sys.stderr)
|
||||
print >> sys.stderr, "Sorry, but " + \
|
||||
filename + " is not a comic archive!"
|
||||
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("This archive is not writable for that tag type", file=sys.stderr)
|
||||
if not ca.isWritable() and (
|
||||
opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
|
||||
print >> sys.stderr, "This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [False, False, False]
|
||||
@@ -234,7 +256,7 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
brief = ""
|
||||
|
||||
if batch_mode:
|
||||
brief = "{0}: ".format(filename)
|
||||
brief = u"{0}: ".format(filename)
|
||||
|
||||
if ca.isZip():
|
||||
brief += "ZIP archive "
|
||||
@@ -246,7 +268,8 @@ 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]:
|
||||
@@ -257,20 +280,24 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print(brief)
|
||||
print brief
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print()
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if has[MetaDataStyle.CIX]:
|
||||
print("--------- ComicRack tags ---------")
|
||||
if opts.raw:
|
||||
print("{0}".format(str(ca.readRawCIX(), errors="ignore")))
|
||||
print(
|
||||
u"{0}".format(
|
||||
unicode(
|
||||
ca.readRawCIX(),
|
||||
errors='ignore')))
|
||||
else:
|
||||
print("{0}".format(ca.readCIX()))
|
||||
print(u"{0}".format(ca.readCIX()))
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if has[MetaDataStyle.CBI]:
|
||||
@@ -278,36 +305,43 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print("{0}".format(ca.readCBI()))
|
||||
print(u"{0}".format(ca.readCBI()))
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[MetaDataStyle.COMET]:
|
||||
print("----------- CoMet tags -----------")
|
||||
if opts.raw:
|
||||
print("{0}".format(ca.readRawCoMet()))
|
||||
print(u"{0}".format(ca.readRawCoMet()))
|
||||
else:
|
||||
print("{0}".format(ca.readCoMet()))
|
||||
print(u"{0}".format(ca.readCoMet()))
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[opts.data_style]
|
||||
if has[opts.data_style]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata(opts.data_style):
|
||||
print("{0}: Tag removal seemed to fail!".format(filename))
|
||||
print(u"{0}: Tag removal seemed to fail!".format(filename))
|
||||
else:
|
||||
print("{0}: Removed {1} tags.".format(filename, style_name))
|
||||
print(
|
||||
u"{0}: Removed {1} tags.".format(filename, style_name))
|
||||
else:
|
||||
print("{0}: dry-run. {1} tags not removed".format(filename, style_name))
|
||||
print(
|
||||
u"{0}: dry-run. {1} tags not removed".format(filename, style_name))
|
||||
else:
|
||||
print("{0}: This archive doesn't have {1} tags to remove.".format(filename, style_name))
|
||||
print(u"{0}: This archive doesn't have {1} tags to remove.".format(
|
||||
filename, style_name))
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[opts.data_style]
|
||||
if opts.no_overwrite and has[opts.data_style]:
|
||||
print("{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name))
|
||||
print(u"{0}: Already has {1} tags. Not overwriting.".format(
|
||||
filename, dst_style_name))
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print("{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name))
|
||||
print(
|
||||
u"{0}: Destination and source are same: {1}. Nothing to do.".format(
|
||||
filename,
|
||||
dst_style_name))
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[opts.copy_source]
|
||||
@@ -319,22 +353,26 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
md = CBLTransformer(md, settings).apply()
|
||||
|
||||
if not ca.writeMetadata(md, opts.data_style):
|
||||
print("{0}: Tag copy seemed to fail!".format(filename))
|
||||
print(u"{0}: Tag copy seemed to fail!".format(filename))
|
||||
else:
|
||||
print("{0}: Copied {1} tags to {2} .".format(filename, src_style_name, dst_style_name))
|
||||
print(u"{0}: Copied {1} tags to {2} .".format(
|
||||
filename, src_style_name, dst_style_name))
|
||||
else:
|
||||
print("{0}: dry-run. {1} tags not copied".format(filename, src_style_name))
|
||||
print(
|
||||
u"{0}: dry-run. {1} tags not copied".format(filename, src_style_name))
|
||||
else:
|
||||
print("{0}: This archive doesn't have {1} tags to copy.".format(filename, src_style_name))
|
||||
print(u"{0}: This archive doesn't have {1} tags to copy.".format(
|
||||
filename, src_style_name))
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
if opts.no_overwrite and has[opts.data_style]:
|
||||
print("{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[opts.data_style]))
|
||||
print(u"{0}: Already has {1} tags. Not overwriting.".format(
|
||||
filename, MetaDataStyle.name[opts.data_style]))
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print("Processing {0}...".format(filename))
|
||||
print(u"Processing {0}...".format(filename))
|
||||
|
||||
md = create_local_metadata(opts, ca, has[opts.data_style])
|
||||
if md.issue is None or md.issue == "":
|
||||
@@ -348,14 +386,16 @@ 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("Network error while getting issue details. Save aborted", file=sys.stderr)
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
if cv_md is None:
|
||||
print("No match for ID {0} was found.".format(opts.issue_id), file=sys.stderr)
|
||||
print >> sys.stderr, "No match for ID {0} was found.".format(
|
||||
opts.issue_id)
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
@@ -365,7 +405,7 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
ii = IssueIdentifier(ca, settings)
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print("No metadata given to search online with!", file=sys.stderr)
|
||||
print >> sys.stderr, "No metadata given to search online with!"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
@@ -404,19 +444,22 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
|
||||
if choices:
|
||||
if low_confidence:
|
||||
print("Online search: Multiple low confidence matches. Save aborted", file=sys.stderr)
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename, matches))
|
||||
print >> sys.stderr, "Online search: Multiple low confidence matches. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(
|
||||
MultipleMatch(filename, matches))
|
||||
return
|
||||
else:
|
||||
print("Online search: Multiple good matches. Save aborted", file=sys.stderr)
|
||||
match_results.multipleMatches.append(MultipleMatch(filename, matches))
|
||||
print >> sys.stderr, "Online search: Multiple good matches. Save aborted"
|
||||
match_results.multipleMatches.append(
|
||||
MultipleMatch(filename, matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print("Online search: Low confidence match. Save aborted", file=sys.stderr)
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename, matches))
|
||||
print >> sys.stderr, "Online search: Low confidence match. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(
|
||||
MultipleMatch(filename, matches))
|
||||
return
|
||||
if not found_match:
|
||||
print("Online search: No match found. Save aborted", file=sys.stderr)
|
||||
print >> sys.stderr, "Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
@@ -430,9 +473,6 @@ 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)
|
||||
@@ -443,7 +483,7 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = "{0}: ".format(filename)
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[opts.data_style]
|
||||
@@ -453,7 +493,7 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
md = create_local_metadata(opts, ca, use_tags)
|
||||
|
||||
if md.series is None:
|
||||
print(msg_hdr + "Can't rename without series name", file=sys.stderr)
|
||||
print >> sys.stderr, msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
new_ext = None # default
|
||||
@@ -467,54 +507,40 @@ 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
|
||||
|
||||
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,
|
||||
)
|
||||
new_name = renamer.determineName(filename, ext=new_ext)
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print >> sys.stderr, msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(filename))
|
||||
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
|
||||
folder = settings.rename_dir.strip()
|
||||
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
|
||||
|
||||
if os.path.join(folder, new_name) == os.path.abspath(filename):
|
||||
print(msg_hdr + "Filename is already good!", file=sys.stderr)
|
||||
return
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
|
||||
os.rename(filename, new_abs_path)
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print("renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix))
|
||||
print(
|
||||
u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix))
|
||||
|
||||
elif opts.export_to_zip:
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = "{0}: ".format(filename)
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if not ca.isRar():
|
||||
print(msg_hdr + "Archive is not a RAR.", file=sys.stderr)
|
||||
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
|
||||
return
|
||||
|
||||
rar_file = os.path.abspath(os.path.abspath(filename))
|
||||
new_file = os.path.splitext(rar_file)[0] + ".cbz"
|
||||
|
||||
if opts.abort_export_on_conflict and os.path.lexists(new_file):
|
||||
print(msg_hdr + "{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))
|
||||
@@ -528,7 +554,8 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
try:
|
||||
os.unlink(rar_file)
|
||||
except:
|
||||
print(msg_hdr + "Error deleting original RAR after export", file=sys.stderr)
|
||||
print >> sys.stderr, msg_hdr + \
|
||||
"Error deleting original RAR after export"
|
||||
delete_success = False
|
||||
else:
|
||||
delete_success = True
|
||||
@@ -537,18 +564,21 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
if os.path.lexists(new_file):
|
||||
os.remove(new_file)
|
||||
else:
|
||||
msg = msg_hdr + "Dry-run: Would try to create {0}".format(os.path.split(new_file)[1])
|
||||
msg = msg_hdr + \
|
||||
u"Dry-run: Would try to create {0}".format(
|
||||
os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export:
|
||||
msg += " and delete orginal."
|
||||
msg += u" and delete orginal."
|
||||
print(msg)
|
||||
return
|
||||
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += "Archive exported successfully to: {0}".format(os.path.split(new_file)[1])
|
||||
msg += u"Archive exported successfully to: {0}".format(
|
||||
os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export and delete_success:
|
||||
msg += " (Original deleted) "
|
||||
msg += u" (Original deleted) "
|
||||
else:
|
||||
msg += "Archive failed to export!"
|
||||
msg += u"Archive failed to export!"
|
||||
|
||||
print(msg)
|
||||
|
||||
@@ -14,29 +14,34 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sqlite3 as lite
|
||||
import os
|
||||
import datetime
|
||||
#import sys
|
||||
#from pprint import pprint
|
||||
|
||||
from . import _version, utils
|
||||
from .settings import ComicTaggerSettings
|
||||
import ctversion
|
||||
from settings import ComicTaggerSettings
|
||||
import utils
|
||||
|
||||
|
||||
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().decode("utf-8")
|
||||
with open(self.version_file, 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
if data != _version.version:
|
||||
if data != ctversion.version:
|
||||
self.clearCache()
|
||||
|
||||
if not os.path.exists(self.db_file):
|
||||
@@ -55,11 +60,11 @@ class ComicVineCacher:
|
||||
def create_cache_db(self):
|
||||
|
||||
# create the version file
|
||||
with open(self.version_file, "w") as f:
|
||||
f.write(_version.version)
|
||||
with open(self.version_file, 'w') as f:
|
||||
f.write(ctversion.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)
|
||||
|
||||
@@ -69,121 +74,119 @@ 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 = str
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM VolumeSearchCache WHERE search_term = ?", [search_term.lower()])
|
||||
cur.execute(
|
||||
"DELETE FROM VolumeSearchCache WHERE search_term = ?", [
|
||||
search_term.lower()])
|
||||
|
||||
# now add in new results
|
||||
for record in cv_search_results:
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
if record["publisher"] is None:
|
||||
if record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = record["publisher"]["name"]
|
||||
pub_name = record['publisher']['name']
|
||||
|
||||
if record["image"] is None:
|
||||
if record['image'] is None:
|
||||
url = ""
|
||||
else:
|
||||
url = record["image"]["super_url"]
|
||||
url = record['image']['super_url']
|
||||
|
||||
cur.execute(
|
||||
"INSERT INTO VolumeSearchCache "
|
||||
+ "(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) "
|
||||
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
search_term.lower(),
|
||||
record["id"],
|
||||
record["name"],
|
||||
record["start_year"],
|
||||
"INSERT INTO VolumeSearchCache " +
|
||||
"(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) " +
|
||||
"VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(search_term.lower(),
|
||||
record['id'],
|
||||
record['name'],
|
||||
record['start_year'],
|
||||
pub_name,
|
||||
record["count_of_issues"],
|
||||
record['count_of_issues'],
|
||||
url,
|
||||
record["description"],
|
||||
),
|
||||
)
|
||||
record['description']))
|
||||
|
||||
def get_search_results(self, search_term):
|
||||
|
||||
results = list()
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# purge stale search results
|
||||
a_day_ago = datetime.datetime.today() - datetime.timedelta(days=1)
|
||||
cur.execute("DELETE FROM VolumeSearchCache WHERE timestamp < ?", [str(a_day_ago)])
|
||||
cur.execute(
|
||||
"DELETE FROM VolumeSearchCache WHERE timestamp < ?", [
|
||||
str(a_day_ago)])
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
|
||||
cur.execute(
|
||||
"SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
|
||||
rows = cur.fetchall()
|
||||
# now process the results
|
||||
for record in rows:
|
||||
|
||||
result = 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)
|
||||
|
||||
@@ -194,7 +197,7 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
@@ -202,21 +205,30 @@ 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 = str
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change
|
||||
# much....
|
||||
a_month_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||
cur.execute("DELETE FROM AltCovers WHERE timestamp < ?", [str(a_month_ago)])
|
||||
a_month_ago = datetime.datetime.today() - \
|
||||
datetime.timedelta(days=30)
|
||||
cur.execute(
|
||||
"DELETE FROM AltCovers WHERE timestamp < ?", [
|
||||
str(a_month_ago)])
|
||||
|
||||
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
|
||||
cur.execute(
|
||||
"SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
@@ -240,19 +252,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):
|
||||
|
||||
@@ -270,16 +282,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):
|
||||
|
||||
@@ -288,14 +300,17 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale volume info
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
|
||||
cur.execute(
|
||||
"DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [volume_id])
|
||||
cur.execute(
|
||||
"SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?",
|
||||
[volume_id])
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
@@ -305,13 +320,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
|
||||
|
||||
@@ -322,34 +337,35 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change
|
||||
# much....
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
cur.execute(
|
||||
"DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
results = 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)
|
||||
|
||||
@@ -358,13 +374,19 @@ class ComicVineCacher:
|
||||
|
||||
return results
|
||||
|
||||
def add_issue_select_details(self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url):
|
||||
def add_issue_select_details(
|
||||
self,
|
||||
issue_id,
|
||||
image_url,
|
||||
thumb_image_url,
|
||||
cover_date,
|
||||
site_detail_url):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
@@ -372,7 +394,7 @@ class ComicVineCacher:
|
||||
"thumb_url": thumb_image_url,
|
||||
"cover_date": cover_date,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert(cur, "issues", "id", issue_id, data)
|
||||
|
||||
@@ -381,23 +403,25 @@ class ComicVineCacher:
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [issue_id])
|
||||
cur.execute(
|
||||
"SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?",
|
||||
[issue_id])
|
||||
row = cur.fetchone()
|
||||
|
||||
details = 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
|
||||
|
||||
@@ -435,8 +459,11 @@ 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)
|
||||
|
||||
@@ -14,40 +14,43 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import urllib2
|
||||
import urllib
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
import datetime
|
||||
import sys
|
||||
import ssl
|
||||
#from pprint import pprint
|
||||
#import math
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from . import _version, utils
|
||||
from .comicvinecacher import ComicVineCacher
|
||||
from .genericmetadata import GenericMetadata
|
||||
from .issuestring import IssueString
|
||||
|
||||
try:
|
||||
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
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
|
||||
|
||||
|
||||
# from settings import ComicTaggerSettings
|
||||
import ctversion
|
||||
import utils
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from genericmetadata import GenericMetadata
|
||||
from issuestring import IssueString
|
||||
#from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class CVTypeID:
|
||||
@@ -66,7 +69,8 @@ class ComicVineTalkerException(Exception):
|
||||
self.code = code
|
||||
|
||||
def __str__(self):
|
||||
if self.code == ComicVineTalkerException.Unknown or self.code == ComicVineTalkerException.Network:
|
||||
if (self.code == ComicVineTalkerException.Unknown or
|
||||
self.code == ComicVineTalkerException.Network):
|
||||
return self.desc
|
||||
else:
|
||||
return "CV error #{0}: [{1}]. \n".format(self.code, self.desc)
|
||||
@@ -91,7 +95,7 @@ class ComicVineTalker(QObject):
|
||||
self.wait_for_rate_limit = False
|
||||
|
||||
# key that is registered to comictagger
|
||||
default_api_key = "27431e6787042105bd3e47e169a624521f89f3a4"
|
||||
default_api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
|
||||
|
||||
if ComicVineTalker.api_key == "":
|
||||
self.api_key = default_api_key
|
||||
@@ -100,6 +104,9 @@ class ComicVineTalker(QObject):
|
||||
|
||||
self.log_func = None
|
||||
|
||||
# always use a tls context for urlopen
|
||||
self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
||||
|
||||
def setLogFunc(self, log_func):
|
||||
self.log_func = log_func
|
||||
|
||||
@@ -107,7 +114,7 @@ class ComicVineTalker(QObject):
|
||||
if self.log_func is None:
|
||||
# sys.stdout.write(text.encode(errors='replace'))
|
||||
# sys.stdout.flush()
|
||||
print(text, file=sys.stderr)
|
||||
print >> sys.stderr, text
|
||||
else:
|
||||
self.log_func(text)
|
||||
|
||||
@@ -116,41 +123,44 @@ class ComicVineTalker(QObject):
|
||||
month = None
|
||||
year = None
|
||||
if date_str is not None:
|
||||
parts = date_str.split("-")
|
||||
year = utils.xlate(parts[0], True)
|
||||
parts = date_str.split('-')
|
||||
year = parts[0]
|
||||
if len(parts) > 1:
|
||||
month = utils.xlate(parts[1], True)
|
||||
month = parts[1]
|
||||
if len(parts) > 2:
|
||||
day = utils.xlate(parts[2], True)
|
||||
day = parts[2]
|
||||
return day, month, year
|
||||
|
||||
def testKey(self, key):
|
||||
|
||||
try:
|
||||
test_url = self.api_base_url + "/issue/1/?api_key=" + key + "&format=json&field_list=name"
|
||||
test_url = self.api_base_url + "/issue/1/?api_key=" + \
|
||||
key + "&format=json&field_list=name"
|
||||
resp = urllib2.urlopen(test_url, context=self.ssl)
|
||||
content = resp.read()
|
||||
|
||||
cv_response = requests.get(test_url, headers={"user-agent": "comictagger/" + _version.version}).json()
|
||||
cv_response = json.loads(content)
|
||||
|
||||
# Bogus request, but if the key is wrong, you get error 100: "Invalid
|
||||
# API Key"
|
||||
return cv_response["status_code"] != 100
|
||||
except:
|
||||
return False
|
||||
# Bogus request, but if the key is wrong, you get error 100: "Invalid
|
||||
# API Key"
|
||||
return cv_response['status_code'] != 100
|
||||
|
||||
"""
|
||||
Get the contect from the CV server. If we're in "wait mode" and status code is a rate limit error
|
||||
sleep for a bit and retry.
|
||||
"""
|
||||
|
||||
def getCVContent(self, url, params):
|
||||
def getCVContent(self, url):
|
||||
total_time_waited = 0
|
||||
limit_wait_time = 1
|
||||
counter = 0
|
||||
wait_times = [1, 2, 3, 4]
|
||||
while True:
|
||||
cv_response = self.getUrlContent(url, params)
|
||||
if self.wait_for_rate_limit and cv_response["status_code"] == ComicVineTalkerException.RateLimit:
|
||||
self.writeLog("Rate limit encountered. Waiting for {0} minutes\n".format(limit_wait_time))
|
||||
content = self.getUrlContent(url)
|
||||
cv_response = json.loads(content)
|
||||
if self.wait_for_rate_limit and cv_response[
|
||||
'status_code'] == ComicVineTalkerException.RateLimit:
|
||||
self.writeLog(
|
||||
"Rate limit encountered. Waiting for {0} minutes\n".format(limit_wait_time))
|
||||
time.sleep(limit_wait_time * 60)
|
||||
total_time_waited += limit_wait_time
|
||||
limit_wait_time = wait_times[counter]
|
||||
@@ -159,104 +169,48 @@ class ComicVineTalker(QObject):
|
||||
# don't wait much more than 20 minutes
|
||||
if total_time_waited < 20:
|
||||
continue
|
||||
if cv_response["status_code"] != 1:
|
||||
self.writeLog("Comic Vine query failed with error #{0}: [{1}]. \n".format(cv_response["status_code"], cv_response["error"]))
|
||||
raise ComicVineTalkerException(cv_response["status_code"], cv_response["error"])
|
||||
if cv_response['status_code'] != 1:
|
||||
self.writeLog(
|
||||
"Comic Vine query failed with error #{0}: [{1}]. \n".format(
|
||||
cv_response['status_code'],
|
||||
cv_response['error']))
|
||||
raise ComicVineTalkerException(
|
||||
cv_response['status_code'], cv_response['error'])
|
||||
else:
|
||||
# it's all good
|
||||
break
|
||||
return cv_response
|
||||
|
||||
def getUrlContent(self, url, params):
|
||||
def getUrlContent(self, url):
|
||||
# connect to server:
|
||||
# if there is a 500 error, try a few more times before giving up
|
||||
# any other error, just bail
|
||||
# print("---", url)
|
||||
# print "ATB---", url
|
||||
for tries in range(3):
|
||||
try:
|
||||
resp = requests.get(url, params=params, headers={"user-agent": "comictagger/" + _version.version})
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
if resp.status_code == 500:
|
||||
resp = urllib2.urlopen(url, context=self.ssl)
|
||||
return resp.read()
|
||||
except urllib2.HTTPError as e:
|
||||
if e.getcode() == 500:
|
||||
self.writeLog("Try #{0}: ".format(tries + 1))
|
||||
time.sleep(1)
|
||||
self.writeLog(str(resp.status_code) + "\n")
|
||||
else:
|
||||
self.writeLog(str(e) + "\n")
|
||||
|
||||
if e.getcode() != 500:
|
||||
break
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
except Exception as e:
|
||||
self.writeLog(str(e) + "\n")
|
||||
raise ComicVineTalkerException(ComicVineTalkerException.Network, "Network Error!")
|
||||
raise ComicVineTalkerException(
|
||||
ComicVineTalkerException.Network, "Network Error!")
|
||||
|
||||
raise ComicVineTalkerException(ComicVineTalkerException.Unknown, "Error on Comic Vine server")
|
||||
|
||||
def literalSearchForSeries(self, series_name, callback=None):
|
||||
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
search_series_name = unicodedata.normalize("NFKD", series_name).encode("ascii", "ignore").decode("ascii")
|
||||
|
||||
params = {
|
||||
"api_key": self.api_key,
|
||||
"format": "json",
|
||||
"resources": "volume",
|
||||
"query": search_series_name,
|
||||
"field_list": "volume,name,id,start_year,publisher,image,description,count_of_issues",
|
||||
"page": 1,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
cv_response = self.getCVContent(self.api_base_url + "/search", params)
|
||||
|
||||
search_results = list()
|
||||
|
||||
# see http://api.comicvine.com/documentation/#handling_responses
|
||||
|
||||
limit = cv_response["limit"]
|
||||
current_result_count = cv_response["number_of_page_results"]
|
||||
total_result_count = cv_response["number_of_total_results"]
|
||||
|
||||
# 8 Dec 2018 - Comic Vine changed query results again. Terms are now
|
||||
# ORed together, and we get thousands of results. Good news is the
|
||||
# results are sorted by relevance, so we can be smart about halting
|
||||
# the search.
|
||||
# 1. Don't fetch more than some sane amount of pages.
|
||||
max_results = 50
|
||||
|
||||
total_result_count = min(total_result_count, max_results)
|
||||
|
||||
if callback is None:
|
||||
self.writeLog("Found {0} of {1} results\n".format(cv_response["number_of_page_results"], cv_response["number_of_total_results"]))
|
||||
search_results.extend(cv_response["results"])
|
||||
page = 1
|
||||
|
||||
if callback is not None:
|
||||
callback(current_result_count, total_result_count)
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while current_result_count < total_result_count:
|
||||
if callback is None:
|
||||
self.writeLog("getting another page of results {0} of {1}...\n".format(current_result_count, total_result_count))
|
||||
page += 1
|
||||
|
||||
params["page"] = page
|
||||
cv_response = self.getCVContent(self.api_base_url + "/search", params)
|
||||
|
||||
search_results.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
|
||||
if callback is not None:
|
||||
callback(current_result_count, total_result_count)
|
||||
|
||||
return search_results
|
||||
raise ComicVineTalkerException(
|
||||
ComicVineTalkerException.Unknown, "Error on Comic Vine server")
|
||||
|
||||
def searchForSeries(self, series_name, callback=None, refresh_cache=False):
|
||||
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
search_series_name = unicodedata.normalize("NFKD", series_name).encode("ascii", "ignore").decode("ascii")
|
||||
# comicvine ignores punctuation and accents
|
||||
search_series_name = re.sub(r"[^A-Za-z0-9]+", " ", search_series_name)
|
||||
# remove extra space and articles and all lower case
|
||||
search_series_name = utils.removearticles(search_series_name).lower().strip()
|
||||
# remove cruft from the search string
|
||||
series_name = utils.removearticles(series_name).lower().strip()
|
||||
|
||||
# before we search online, look in our cache, since we might have
|
||||
# done this same search recently
|
||||
@@ -267,119 +221,71 @@ class ComicVineTalker(QObject):
|
||||
if len(cached_search_results) > 0:
|
||||
return cached_search_results
|
||||
|
||||
params = {
|
||||
"api_key": self.api_key,
|
||||
"format": "json",
|
||||
"resources": "volume",
|
||||
"query": search_series_name,
|
||||
"field_list": "volume,name,id,start_year,publisher,image,description,count_of_issues",
|
||||
"page": 1,
|
||||
"limit": 100,
|
||||
}
|
||||
original_series_name = series_name
|
||||
|
||||
cv_response = self.getCVContent(self.api_base_url + "/search", params)
|
||||
# We need to make the series name into an "AND"ed query list
|
||||
query_word_list = series_name.split()
|
||||
and_list = ['AND'] * (len(query_word_list) - 1)
|
||||
and_list.append('')
|
||||
# zipper up the two lists
|
||||
query_list = zip(query_word_list, and_list)
|
||||
# flatten the list
|
||||
query_list = [item for sublist in query_list for item in sublist]
|
||||
# convert back to a string
|
||||
query_string = " ".join(query_list).strip()
|
||||
# print "Query string = ", query_string
|
||||
|
||||
query_string = urllib.quote_plus(query_string.encode("utf-8"))
|
||||
|
||||
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + \
|
||||
query_string + \
|
||||
"&field_list=name,id,start_year,publisher,image,description,count_of_issues"
|
||||
cv_response = self.getCVContent(search_url + "&page=1")
|
||||
|
||||
search_results = list()
|
||||
|
||||
# see http://api.comicvine.com/documentation/#handling_responses
|
||||
|
||||
limit = cv_response["limit"]
|
||||
current_result_count = cv_response["number_of_page_results"]
|
||||
total_result_count = cv_response["number_of_total_results"]
|
||||
|
||||
# 8 Dec 2018 - Comic Vine changed query results again. Terms are now
|
||||
# ORed together, and we get thousands of results. Good news is the
|
||||
# results are sorted by relevance, so we can be smart about halting
|
||||
# the search.
|
||||
# 1. Don't fetch more than some sane amount of pages.
|
||||
max_results = 500
|
||||
# 2. Halt when not all of our search terms are present in a result
|
||||
# 3. Halt when the results contain more (plus threshold) words than
|
||||
# our search
|
||||
result_word_count_max = len(search_series_name.split()) + 3
|
||||
|
||||
total_result_count = min(total_result_count, max_results)
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
|
||||
if callback is None:
|
||||
self.writeLog("Found {0} of {1} results\n".format(cv_response["number_of_page_results"], cv_response["number_of_total_results"]))
|
||||
search_results.extend(cv_response["results"])
|
||||
self.writeLog(
|
||||
"Found {0} of {1} results\n".format(
|
||||
cv_response['number_of_page_results'],
|
||||
cv_response['number_of_total_results']))
|
||||
search_results.extend(cv_response['results'])
|
||||
page = 1
|
||||
|
||||
if callback is not None:
|
||||
callback(current_result_count, total_result_count)
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
stop_searching = False
|
||||
while current_result_count < total_result_count:
|
||||
|
||||
last_result = search_results[-1]["name"]
|
||||
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
last_result = unicodedata.normalize("NFKD", last_result).encode("ascii", "ignore").decode("ascii")
|
||||
# comicvine ignores punctuation and accents
|
||||
last_result = re.sub(r"[^A-Za-z0-9]+", " ", last_result)
|
||||
# remove extra space and articles and all lower case
|
||||
last_result = utils.removearticles(last_result).lower().strip()
|
||||
|
||||
# See if the last result's name has all the of the search terms.
|
||||
# if not, break out of this, loop, we're done.
|
||||
for term in search_series_name.split():
|
||||
if term not in last_result.lower():
|
||||
# print("Term '{}' not in last result. Halting search result fetching".format(term))
|
||||
stop_searching = True
|
||||
break
|
||||
|
||||
# Also, stop searching when the word count of last results is too much longer
|
||||
# than our search terms list
|
||||
if len(last_result.split()) > result_word_count_max:
|
||||
print(
|
||||
"Last result '{}' is too long: max word count: {}; Search terms {}. Halting search result fetching".format(
|
||||
last_result, result_word_count_max, search_series_name.split()
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
stop_searching = True
|
||||
|
||||
if stop_searching:
|
||||
break
|
||||
|
||||
while (current_result_count < total_result_count):
|
||||
if callback is None:
|
||||
self.writeLog("getting another page of results {0} of {1}...\n".format(current_result_count, total_result_count))
|
||||
self.writeLog(
|
||||
"getting another page of results {0} of {1}...\n".format(
|
||||
current_result_count,
|
||||
total_result_count))
|
||||
page += 1
|
||||
|
||||
params["page"] = page
|
||||
cv_response = self.getCVContent(self.api_base_url + "/search", params)
|
||||
cv_response = self.getCVContent(search_url + "&page=" + str(page))
|
||||
|
||||
search_results.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
search_results.extend(cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
if callback is not None:
|
||||
callback(current_result_count, total_result_count)
|
||||
|
||||
# Remove any search results that don't contain all the search terms
|
||||
# (iterate backwards for easy removal)
|
||||
for i in range(len(search_results) - 1, -1, -1):
|
||||
record = search_results[i]
|
||||
for term in search_series_name.split():
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
recordName = unicodedata.normalize("NFKD", record["name"]).encode("ascii", "ignore").decode("ascii")
|
||||
# comicvine ignores punctuation and accents
|
||||
recordName = re.sub(r"[^A-Za-z0-9]+", " ", recordName)
|
||||
# remove extra space and articles and all lower case
|
||||
recordName = utils.removearticles(recordName).lower().strip()
|
||||
|
||||
if term not in recordName:
|
||||
del search_results[i]
|
||||
break
|
||||
|
||||
# for record in search_results:
|
||||
# print(u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year']))
|
||||
# print(record)
|
||||
# record['count_of_issues'] = record['count_of_isssues']
|
||||
# print(u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year']))
|
||||
#print(u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year']))
|
||||
# print(record)
|
||||
#record['count_of_issues'] = record['count_of_isssues']
|
||||
#print(u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year']))
|
||||
|
||||
# cache these search results
|
||||
cvc.add_search_results(series_name, search_results)
|
||||
cvc.add_search_results(original_series_name, search_results)
|
||||
|
||||
return search_results
|
||||
|
||||
@@ -393,12 +299,13 @@ class ComicVineTalker(QObject):
|
||||
if cached_volume_result is not None:
|
||||
return cached_volume_result
|
||||
|
||||
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + str(series_id)
|
||||
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + \
|
||||
str(series_id) + "/?api_key=" + self.api_key + \
|
||||
"&field_list=name,id,start_year,publisher,count_of_issues&format=json"
|
||||
|
||||
params = {"api_key": self.api_key, "format": "json", "field_list": "name,id,start_year,publisher,count_of_issues"}
|
||||
cv_response = self.getCVContent(volume_url, params)
|
||||
cv_response = self.getCVContent(volume_url)
|
||||
|
||||
volume_results = cv_response["results"]
|
||||
volume_results = cv_response['results']
|
||||
|
||||
cvc.add_volume_info(volume_results)
|
||||
|
||||
@@ -414,37 +321,36 @@ class ComicVineTalker(QObject):
|
||||
if cached_volume_issues_result is not None:
|
||||
return cached_volume_issues_result
|
||||
|
||||
params = {
|
||||
"api_key": self.api_key,
|
||||
"filter": "volume:" + str(series_id),
|
||||
"format": "json",
|
||||
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description",
|
||||
}
|
||||
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
|
||||
#---------------------------------
|
||||
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + \
|
||||
str(series_id) + \
|
||||
"&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
|
||||
cv_response = self.getCVContent(issues_url)
|
||||
|
||||
# ------------------------------------
|
||||
#------------------------------------
|
||||
|
||||
limit = cv_response["limit"]
|
||||
current_result_count = cv_response["number_of_page_results"]
|
||||
total_result_count = cv_response["number_of_total_results"]
|
||||
# print("total_result_count", total_result_count)
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
# print "ATB total_result_count", total_result_count
|
||||
|
||||
# print("Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
volume_issues_result = cv_response["results"]
|
||||
#print("ATB Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
volume_issues_result = cv_response['results']
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while current_result_count < total_result_count:
|
||||
# print("getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
|
||||
while (current_result_count < total_result_count):
|
||||
#print("ATB getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
|
||||
page += 1
|
||||
offset += cv_response["number_of_page_results"]
|
||||
offset += cv_response['number_of_page_results']
|
||||
|
||||
params["offset"] = offset
|
||||
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
|
||||
# print issues_url+ "&offset="+str(offset)
|
||||
cv_response = self.getCVContent(
|
||||
issues_url + "&offset=" + str(offset))
|
||||
|
||||
volume_issues_result.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
volume_issues_result.extend(cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
self.repairUrls(volume_issues_result)
|
||||
|
||||
@@ -452,48 +358,51 @@ class ComicVineTalker(QObject):
|
||||
|
||||
return volume_issues_result
|
||||
|
||||
def fetchIssuesByVolumeIssueNumAndYear(self, volume_id_list, issue_number, year):
|
||||
volume_filter = ""
|
||||
def fetchIssuesByVolumeIssueNumAndYear(
|
||||
self, volume_id_list, issue_number, year):
|
||||
volume_filter = "volume:"
|
||||
for vid in volume_id_list:
|
||||
volume_filter += str(vid) + "|"
|
||||
filter = "volume:{},issue_number:{}".format(volume_filter, issue_number)
|
||||
|
||||
intYear = utils.xlate(year, True)
|
||||
if intYear is not None:
|
||||
filter += ",cover_date:{}-1-1|{}-1-1".format(intYear, intYear + 1)
|
||||
year_filter = ""
|
||||
if year is not None and str(year).isdigit():
|
||||
year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(
|
||||
year, int(year) + 1)
|
||||
|
||||
params = {
|
||||
"api_key": self.api_key,
|
||||
"format": "json",
|
||||
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description",
|
||||
"filter": filter,
|
||||
}
|
||||
issue_number = urllib.quote_plus(unicode(issue_number).encode("utf-8"))
|
||||
|
||||
cv_response = self.getCVContent(self.api_base_url + "/issues", params)
|
||||
filter = "&filter=" + volume_filter + \
|
||||
year_filter + ",issue_number:" + issue_number
|
||||
|
||||
# ------------------------------------
|
||||
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + \
|
||||
"&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
|
||||
|
||||
limit = cv_response["limit"]
|
||||
current_result_count = cv_response["number_of_page_results"]
|
||||
total_result_count = cv_response["number_of_total_results"]
|
||||
# print("total_result_count", total_result_count)
|
||||
cv_response = self.getCVContent(issues_url)
|
||||
|
||||
# print("Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
filtered_issues_result = cv_response["results"]
|
||||
#------------------------------------
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
# print "ATB total_result_count", total_result_count
|
||||
|
||||
#print("ATB Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
filtered_issues_result = cv_response['results']
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while current_result_count < total_result_count:
|
||||
# print("getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
|
||||
while (current_result_count < total_result_count):
|
||||
#print("ATB getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
|
||||
page += 1
|
||||
offset += cv_response["number_of_page_results"]
|
||||
offset += cv_response['number_of_page_results']
|
||||
|
||||
params["offset"] = offset
|
||||
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
|
||||
# print issues_url+ "&offset="+str(offset)
|
||||
cv_response = self.getCVContent(
|
||||
issues_url + "&offset=" + str(offset))
|
||||
|
||||
filtered_issues_result.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
filtered_issues_result.extend(cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
self.repairUrls(filtered_issues_result)
|
||||
|
||||
@@ -508,31 +417,35 @@ class ComicVineTalker(QObject):
|
||||
for record in issues_list_results:
|
||||
if IssueString(issue_number).asString() is None:
|
||||
issue_number = 1
|
||||
if IssueString(record["issue_number"]).asString().lower() == IssueString(issue_number).asString().lower():
|
||||
if IssueString(record['issue_number']).asString().lower() == IssueString(
|
||||
issue_number).asString().lower():
|
||||
found = True
|
||||
break
|
||||
|
||||
if found:
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(record["id"])
|
||||
params = {"api_key": self.api_key, "format": "json"}
|
||||
cv_response = self.getCVContent(issue_url, params)
|
||||
issue_results = cv_response["results"]
|
||||
if (found):
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
|
||||
str(record['id']) + "/?api_key=" + \
|
||||
self.api_key + "&format=json"
|
||||
|
||||
cv_response = self.getCVContent(issue_url)
|
||||
issue_results = cv_response['results']
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
# Now, map the Comic Vine data to generic metadata
|
||||
return self.mapCVDataToMetadata(volume_results, issue_results, settings)
|
||||
return self.mapCVDataToMetadata(
|
||||
volume_results, issue_results, settings)
|
||||
|
||||
def fetchIssueDataByIssueID(self, issue_id, settings):
|
||||
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id)
|
||||
params = {"api_key": self.api_key, "format": "json"}
|
||||
cv_response = self.getCVContent(issue_url, params)
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
|
||||
str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
cv_response = self.getCVContent(issue_url)
|
||||
|
||||
issue_results = cv_response["results"]
|
||||
issue_results = cv_response['results']
|
||||
|
||||
volume_results = self.fetchVolumeData(issue_results["volume"]["id"])
|
||||
volume_results = self.fetchVolumeData(issue_results['volume']['id'])
|
||||
|
||||
# Now, map the Comic Vine data to generic metadata
|
||||
md = self.mapCVDataToMetadata(volume_results, issue_results, settings)
|
||||
@@ -544,57 +457,61 @@ class ComicVineTalker(QObject):
|
||||
# Now, map the Comic Vine data to generic metadata
|
||||
metadata = GenericMetadata()
|
||||
|
||||
metadata.series = utils.xlate(issue_results["volume"]["name"])
|
||||
metadata.issue = IssueString(issue_results["issue_number"]).asString()
|
||||
metadata.title = utils.xlate(issue_results["name"])
|
||||
metadata.series = issue_results['volume']['name']
|
||||
|
||||
if volume_results["publisher"] is not None:
|
||||
metadata.publisher = utils.xlate(volume_results["publisher"]["name"])
|
||||
metadata.day, metadata.month, metadata.year = self.parseDateStr(issue_results["cover_date"])
|
||||
num_s = IssueString(issue_results['issue_number']).asString()
|
||||
metadata.issue = num_s
|
||||
metadata.title = issue_results['name']
|
||||
|
||||
metadata.seriesYear = utils.xlate(volume_results["start_year"])
|
||||
metadata.issueCount = utils.xlate(volume_results["count_of_issues"])
|
||||
metadata.comments = self.cleanup_html(issue_results["description"], settings.remove_html_tables)
|
||||
metadata.publisher = volume_results['publisher']['name']
|
||||
metadata.day, metadata.month, metadata.year = self.parseDateStr(
|
||||
issue_results['cover_date'])
|
||||
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.comments = self.cleanup_html(
|
||||
issue_results['description'], settings.remove_html_tables)
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = utils.xlate(volume_results["start_year"])
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
|
||||
ctversion.version, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), issue_results["id"]
|
||||
)
|
||||
# metadata.notes += issue_results['site_detail_url']
|
||||
ctversion.version,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
issue_results['id'])
|
||||
#metadata.notes += issue_results['site_detail_url']
|
||||
|
||||
metadata.webLink = issue_results["site_detail_url"]
|
||||
metadata.webLink = issue_results['site_detail_url']
|
||||
|
||||
person_credits = issue_results["person_credits"]
|
||||
person_credits = issue_results['person_credits']
|
||||
for person in person_credits:
|
||||
if "role" in person:
|
||||
roles = person["role"].split(",")
|
||||
if 'role' in person:
|
||||
roles = person['role'].split(',')
|
||||
for role in roles:
|
||||
# can we determine 'primary' from CV??
|
||||
metadata.addCredit(person["name"], role.title().strip(), False)
|
||||
metadata.addCredit(
|
||||
person['name'], role.title().strip(), False)
|
||||
|
||||
character_credits = issue_results["character_credits"]
|
||||
character_credits = issue_results['character_credits']
|
||||
character_list = list()
|
||||
for character in character_credits:
|
||||
character_list.append(character["name"])
|
||||
character_list.append(character['name'])
|
||||
metadata.characters = utils.listToString(character_list)
|
||||
|
||||
team_credits = issue_results["team_credits"]
|
||||
team_credits = issue_results['team_credits']
|
||||
team_list = list()
|
||||
for team in team_credits:
|
||||
team_list.append(team["name"])
|
||||
team_list.append(team['name'])
|
||||
metadata.teams = utils.listToString(team_list)
|
||||
|
||||
location_credits = issue_results["location_credits"]
|
||||
location_credits = issue_results['location_credits']
|
||||
location_list = list()
|
||||
for location in location_credits:
|
||||
location_list.append(location["name"])
|
||||
location_list.append(location['name'])
|
||||
metadata.locations = utils.listToString(location_list)
|
||||
|
||||
story_arc_credits = issue_results["story_arc_credits"]
|
||||
story_arc_credits = issue_results['story_arc_credits']
|
||||
arc_list = []
|
||||
for arc in story_arc_credits:
|
||||
arc_list.append(arc["name"])
|
||||
arc_list.append(arc['name'])
|
||||
if len(arc_list) > 0:
|
||||
metadata.storyArc = utils.listToString(arc_list)
|
||||
|
||||
@@ -615,8 +532,8 @@ class ComicVineTalker(QObject):
|
||||
if string is None:
|
||||
return ""
|
||||
# find any tables
|
||||
soup = BeautifulSoup(string, "html.parser")
|
||||
tables = soup.findAll("table")
|
||||
soup = BeautifulSoup(string)
|
||||
tables = soup.findAll('table')
|
||||
|
||||
# remove all newlines first
|
||||
string = string.replace("\n", "")
|
||||
@@ -628,19 +545,19 @@ class ComicVineTalker(QObject):
|
||||
string = string.replace("</h4>", "*\n")
|
||||
|
||||
# remove the tables
|
||||
p = re.compile(r"<table[^<]*?>.*?<\/table>")
|
||||
p = re.compile(r'<table[^<]*?>.*?<\/table>')
|
||||
if remove_html_tables:
|
||||
string = p.sub("", string)
|
||||
string = p.sub('', string)
|
||||
string = string.replace("*List of covers and their creators:*", "")
|
||||
else:
|
||||
string = p.sub("{}", string)
|
||||
string = p.sub('{}', string)
|
||||
|
||||
# now strip all other tags
|
||||
p = re.compile(r"<[^<]*?>")
|
||||
newstring = p.sub("", string)
|
||||
p = re.compile(r'<[^<]*?>')
|
||||
newstring = p.sub('', string)
|
||||
|
||||
newstring = newstring.replace(" ", " ")
|
||||
newstring = newstring.replace("&", "&")
|
||||
newstring = newstring.replace(' ', ' ')
|
||||
newstring = newstring.replace('&', '&')
|
||||
|
||||
newstring = newstring.strip()
|
||||
|
||||
@@ -652,15 +569,15 @@ class ComicVineTalker(QObject):
|
||||
rows = []
|
||||
hdrs = []
|
||||
col_widths = []
|
||||
for hdr in table.findAll("th"):
|
||||
for hdr in table.findAll('th'):
|
||||
item = hdr.string.strip()
|
||||
hdrs.append(item)
|
||||
col_widths.append(len(item))
|
||||
rows.append(hdrs)
|
||||
|
||||
for row in table.findAll("tr"):
|
||||
for row in table.findAll('tr'):
|
||||
cols = []
|
||||
col = row.findAll("td")
|
||||
col = row.findAll('td')
|
||||
i = 0
|
||||
for c in col:
|
||||
item = c.string.strip()
|
||||
@@ -675,7 +592,7 @@ class ComicVineTalker(QObject):
|
||||
for w in col_widths:
|
||||
fmtstr += " {{:{}}}|".format(w + 1)
|
||||
width = sum(col_widths) + len(col_widths) * 2
|
||||
print("width=", width)
|
||||
print "width=", width
|
||||
table_text = ""
|
||||
counter = 0
|
||||
for row in rows:
|
||||
@@ -697,45 +614,48 @@ class ComicVineTalker(QObject):
|
||||
|
||||
def fetchIssueDate(self, issue_id):
|
||||
details = self.fetchIssueSelectDetails(issue_id)
|
||||
day, month, year = self.parseDateStr(details["cover_date"])
|
||||
day, month, year = self.parseDateStr(details['cover_date'])
|
||||
return month, year
|
||||
|
||||
def fetchIssueCoverURLs(self, issue_id):
|
||||
details = self.fetchIssueSelectDetails(issue_id)
|
||||
return details["image_url"], details["thumb_image_url"]
|
||||
return details['image_url'], details['thumb_image_url']
|
||||
|
||||
def fetchIssuePageURL(self, issue_id):
|
||||
details = self.fetchIssueSelectDetails(issue_id)
|
||||
return details["site_detail_url"]
|
||||
return details['site_detail_url']
|
||||
|
||||
def fetchIssueSelectDetails(self, issue_id):
|
||||
|
||||
# cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails(issue_id)
|
||||
#cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails(issue_id)
|
||||
cached_details = self.fetchCachedIssueSelectDetails(issue_id)
|
||||
if cached_details["image_url"] is not None:
|
||||
if cached_details['image_url'] is not None:
|
||||
return cached_details
|
||||
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id)
|
||||
|
||||
params = {"api_key": self.api_key, "format": "json", "field_list": "image,cover_date,site_detail_url"}
|
||||
|
||||
cv_response = self.getCVContent(issue_url, params)
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
|
||||
str(issue_id) + "/?api_key=" + self.api_key + \
|
||||
"&format=json&field_list=image,cover_date,site_detail_url"
|
||||
|
||||
details = dict()
|
||||
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
|
||||
|
||||
details["image_url"] = cv_response["results"]["image"]["super_url"]
|
||||
details["thumb_image_url"] = cv_response["results"]["image"]["thumb_url"]
|
||||
details["cover_date"] = cv_response["results"]["cover_date"]
|
||||
details["site_detail_url"] = cv_response["results"]["site_detail_url"]
|
||||
cv_response = self.getCVContent(issue_url)
|
||||
|
||||
if details["image_url"] is not None:
|
||||
self.cacheIssueSelectDetails(
|
||||
issue_id, details["image_url"], details["thumb_image_url"], details["cover_date"], details["site_detail_url"]
|
||||
)
|
||||
details['image_url'] = cv_response['results']['image']['super_url']
|
||||
details['thumb_image_url'] = cv_response[
|
||||
'results']['image']['thumb_url']
|
||||
details['cover_date'] = cv_response['results']['cover_date']
|
||||
details['site_detail_url'] = cv_response['results']['site_detail_url']
|
||||
|
||||
if details['image_url'] is not None:
|
||||
self.cacheIssueSelectDetails(issue_id,
|
||||
details['image_url'],
|
||||
details['thumb_image_url'],
|
||||
details['cover_date'],
|
||||
details['site_detail_url'])
|
||||
# print(details['site_detail_url'])
|
||||
return details
|
||||
|
||||
@@ -746,9 +666,11 @@ class ComicVineTalker(QObject):
|
||||
cvc = ComicVineCacher()
|
||||
return cvc.get_issue_select_details(issue_id)
|
||||
|
||||
def cacheIssueSelectDetails(self, issue_id, image_url, thumb_url, cover_date, page_url):
|
||||
def cacheIssueSelectDetails(
|
||||
self, issue_id, image_url, thumb_url, cover_date, page_url):
|
||||
cvc = ComicVineCacher()
|
||||
cvc.add_issue_select_details(issue_id, image_url, thumb_url, cover_date, page_url)
|
||||
cvc.add_issue_select_details(
|
||||
issue_id, image_url, thumb_url, cover_date, page_url)
|
||||
|
||||
def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
|
||||
url_list = self.fetchCachedAlternateCoverURLs(issue_id)
|
||||
@@ -756,7 +678,8 @@ class ComicVineTalker(QObject):
|
||||
return url_list
|
||||
|
||||
# scrape the CV issue page URL to get the alternate cover URLs
|
||||
content = requests.get(issue_page_url, headers={"user-agent": "comictagger/" + _version.version}).text
|
||||
resp = urllib2.urlopen(issue_page_url, context=self.ssl)
|
||||
content = resp.read()
|
||||
alt_cover_url_list = self.parseOutAltCoverUrls(content)
|
||||
|
||||
# cache this alt cover URL list
|
||||
@@ -765,23 +688,22 @@ class ComicVineTalker(QObject):
|
||||
return alt_cover_url_list
|
||||
|
||||
def parseOutAltCoverUrls(self, page_html):
|
||||
soup = BeautifulSoup(page_html, "html.parser")
|
||||
soup = BeautifulSoup(page_html)
|
||||
|
||||
alt_cover_url_list = []
|
||||
|
||||
# Using knowledge of the layout of the Comic Vine issue page here:
|
||||
# look for the divs that are in the classes 'imgboxart' and
|
||||
# 'issue-cover'
|
||||
div_list = soup.find_all("div")
|
||||
# look for the divs that are in the classes 'content-pod' and
|
||||
# 'alt-cover'
|
||||
div_list = soup.find_all('div')
|
||||
covers_found = 0
|
||||
for d in div_list:
|
||||
if "class" in d.attrs:
|
||||
c = d["class"]
|
||||
if "imgboxart" in c and "issue-cover" in c and d.img["src"].startswith("http"):
|
||||
|
||||
if 'class' in d:
|
||||
c = d['class']
|
||||
if 'imgboxart' in c and 'issue-cover' in c:
|
||||
covers_found += 1
|
||||
if covers_found != 1:
|
||||
alt_cover_url_list.append(d.img["src"])
|
||||
alt_cover_url_list.append(d.img['src'])
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
@@ -800,27 +722,23 @@ class ComicVineTalker(QObject):
|
||||
cvc = ComicVineCacher()
|
||||
cvc.add_alt_covers(issue_id, url_list)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
#-------------------------------------------------------------------------
|
||||
urlFetchComplete = pyqtSignal(str, str, int)
|
||||
|
||||
def asyncFetchIssueCoverURLs(self, issue_id):
|
||||
|
||||
self.issue_id = issue_id
|
||||
details = self.fetchCachedIssueSelectDetails(issue_id)
|
||||
if details["image_url"] is not None:
|
||||
self.urlFetchComplete.emit(details["image_url"], details["thumb_image_url"], self.issue_id)
|
||||
if details['image_url'] is not None:
|
||||
self.urlFetchComplete.emit(
|
||||
details['image_url'],
|
||||
details['thumb_image_url'],
|
||||
self.issue_id)
|
||||
return
|
||||
|
||||
issue_url = (
|
||||
self.api_base_url
|
||||
+ "/issue/"
|
||||
+ CVTypeID.Issue
|
||||
+ "-"
|
||||
+ str(issue_id)
|
||||
+ "/?api_key="
|
||||
+ self.api_key
|
||||
+ "&format=json&field_list=image,cover_date,site_detail_url"
|
||||
)
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
|
||||
str(issue_id) + "/?api_key=" + self.api_key + \
|
||||
"&format=json&field_list=image,cover_date,site_detail_url"
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.asyncFetchIssueCoverURLComplete)
|
||||
self.nam.get(QNetworkRequest(QUrl(issue_url)))
|
||||
@@ -831,22 +749,24 @@ class ComicVineTalker(QObject):
|
||||
data = reply.readAll()
|
||||
|
||||
try:
|
||||
cv_response = json.loads(bytes(data))
|
||||
except Exception as e:
|
||||
print("Comic Vine query failed to get JSON data", file=sys.stderr)
|
||||
print(str(data), file=sys.stderr)
|
||||
cv_response = json.loads(str(data))
|
||||
except:
|
||||
print >> sys.stderr, "Comic Vine query failed to get JSON data"
|
||||
print >> sys.stderr, str(data)
|
||||
return
|
||||
|
||||
if cv_response["status_code"] != 1:
|
||||
print("Comic Vine query failed with error: [{0}]. ".format(cv_response["error"]), file=sys.stderr)
|
||||
if cv_response['status_code'] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format(
|
||||
cv_response['error'])
|
||||
return
|
||||
|
||||
image_url = cv_response["results"]["image"]["super_url"]
|
||||
thumb_url = cv_response["results"]["image"]["thumb_url"]
|
||||
cover_date = cv_response["results"]["cover_date"]
|
||||
page_url = cv_response["results"]["site_detail_url"]
|
||||
image_url = cv_response['results']['image']['super_url']
|
||||
thumb_url = cv_response['results']['image']['thumb_url']
|
||||
cover_date = cv_response['results']['cover_date']
|
||||
page_url = cv_response['results']['site_detail_url']
|
||||
|
||||
self.cacheIssueSelectDetails(self.issue_id, image_url, thumb_url, cover_date, page_url)
|
||||
self.cacheIssueSelectDetails(
|
||||
self.issue_id, image_url, thumb_url, cover_date, page_url)
|
||||
|
||||
self.urlFetchComplete.emit(image_url, thumb_url, self.issue_id)
|
||||
|
||||
@@ -872,12 +792,13 @@ class ComicVineTalker(QObject):
|
||||
# cache this alt cover URL list
|
||||
self.cacheAlternateCoverURLs(self.issue_id, alt_cover_url_list)
|
||||
|
||||
self.altUrlListFetchComplete.emit(alt_cover_url_list, int(self.issue_id))
|
||||
self.altUrlListFetchComplete.emit(
|
||||
alt_cover_url_list, int(self.issue_id))
|
||||
|
||||
def repairUrls(self, issue_list):
|
||||
# make sure there are URLs for the image fields
|
||||
for issue in issue_list:
|
||||
if issue["image"] is None:
|
||||
issue["image"] = dict()
|
||||
issue["image"]["super_url"] = ComicVineTalker.logo_url
|
||||
issue["image"]["thumb_url"] = ComicVineTalker.logo_url
|
||||
if issue['image'] is None:
|
||||
issue['image'] = dict()
|
||||
issue['image']['super_url'] = ComicVineTalker.logo_url
|
||||
issue['image']['thumb_url'] = ComicVineTalker.logo_url
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 widget to display cover images
|
||||
"""A PyQt4 widget to display cover images
|
||||
|
||||
Display cover images from either a local archive, or from Comic Vine.
|
||||
TODO: This should be re-factored using subclasses!
|
||||
@@ -18,18 +18,21 @@ TODO: This should be re-factored using subclasses!
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
#import os
|
||||
|
||||
from comictaggerlib.ui.qtutils import getQImageFromData, reduceWidgetFontSize
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from .imagefetcher import ImageFetcher
|
||||
from .imagepopup import ImagePopup
|
||||
from .pageloader import PageLoader
|
||||
from .settings import ComicTaggerSettings
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from pageloader import PageLoader
|
||||
from imagepopup import ImagePopup
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, getQImageFromData
|
||||
#from genericmetadata import GenericMetadata, PageType
|
||||
#from comicarchive import MetaDataStyle
|
||||
#import utils
|
||||
|
||||
|
||||
def clickable(widget):
|
||||
@@ -63,7 +66,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)
|
||||
|
||||
@@ -72,8 +75,9 @@ 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)
|
||||
@@ -140,7 +144,8 @@ 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):
|
||||
@@ -172,8 +177,10 @@ 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:
|
||||
@@ -221,23 +228,29 @@ 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:
|
||||
@@ -253,14 +266,17 @@ 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)
|
||||
@@ -268,14 +284,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()
|
||||
@@ -295,7 +311,8 @@ 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, Qt.SmoothTransformation)
|
||||
scaled_pixmap = self.current_pixmap.scaled(
|
||||
new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap(scaled_pixmap)
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
#import os
|
||||
|
||||
from .settings import ComicTaggerSettings
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class CreditEditorWindow(QtWidgets.QDialog):
|
||||
class CreditEditorWindow(QtGui.QDialog):
|
||||
|
||||
ModeEdit = 0
|
||||
ModeNew = 1
|
||||
@@ -27,7 +29,8 @@ class CreditEditorWindow(QtWidgets.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
|
||||
|
||||
@@ -87,6 +90,7 @@ class CreditEditorWindow(QtWidgets.QDialog):
|
||||
|
||||
def accept(self):
|
||||
if self.cbRole.currentText() == "" or self.leName.text() == "":
|
||||
QtWidgets.QMessageBox.warning(self, self.tr("Whoops"), self.tr("You need to enter both role and name for a credit."))
|
||||
QtGui.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
|
||||
"You need to enter both role and name for a credit."))
|
||||
else:
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
3
comictaggerlib/ctversion.py
Normal file
3
comictaggerlib/ctversion.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# This file should contain only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version = "1.1.16-beta-rc2"
|
||||
@@ -14,9 +14,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
#import os
|
||||
|
||||
from .settings import ComicTaggerSettings
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
#from settingswindow import SettingsWindow
|
||||
#from filerenamer import FileRenamer
|
||||
#import utils
|
||||
|
||||
|
||||
class ExportConflictOpts:
|
||||
@@ -25,14 +30,16 @@ class ExportConflictOpts:
|
||||
createUnique = 3
|
||||
|
||||
|
||||
class ExportWindow(QtWidgets.QDialog):
|
||||
class ExportWindow(QtGui.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
|
||||
|
||||
@@ -45,7 +52,7 @@ class ExportWindow(QtWidgets.QDialog):
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
|
||||
def accept(self):
|
||||
QtWidgets.QDialog.accept(self)
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
|
||||
self.addToList = self.cbxAddToList.isChecked()
|
||||
|
||||
@@ -16,85 +16,20 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
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
|
||||
import utils
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class FileRenamer:
|
||||
|
||||
def __init__(self, metadata):
|
||||
self.setMetadata(metadata)
|
||||
self.setTemplate("{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})")
|
||||
self.setTemplate(
|
||||
"%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
|
||||
@@ -108,37 +43,114 @@ 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()
|
||||
|
||||
# padding for issue
|
||||
md.issue = IssueString(md.issue).asString(pad=self.issue_zero_padding)
|
||||
# print(u"{0}".format(md))
|
||||
|
||||
template = self.template
|
||||
new_name = self.replaceToken(new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken(new_name, md.volume, '%volume%')
|
||||
|
||||
pathComponents = template.split(os.sep)
|
||||
new_name = ""
|
||||
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%')
|
||||
|
||||
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.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%')
|
||||
|
||||
if ext is None or ext == "":
|
||||
new_name = self.replaceToken(new_name, md.genre, '%genre%')
|
||||
new_name = self.replaceToken(new_name, md.language, '%language_code%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.criticalRating, '%criticalrating%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateSeries, '%alternateseries%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateNumber, '%alternatenumber%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateCount, '%alternatecount%')
|
||||
new_name = self.replaceToken(new_name, md.imprint, '%imprint%')
|
||||
new_name = self.replaceToken(new_name, md.format, '%format%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.maturityRating, '%maturityrating%')
|
||||
new_name = self.replaceToken(new_name, md.storyArc, '%storyarc%')
|
||||
new_name = self.replaceToken(new_name, md.seriesGroup, '%seriesgroup%')
|
||||
new_name = self.replaceToken(new_name, md.scanInfo, '%scaninfo%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
# remove empty braces,brackets, parentheses
|
||||
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name)
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name)
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
|
||||
new_name = re.sub("(\s--)+", " --", new_name)
|
||||
new_name = re.sub("(\s-)+", " -", new_name)
|
||||
|
||||
# remove dash or double dash at end of line
|
||||
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
|
||||
|
||||
# remove duplicate spaces (again!)
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
|
||||
new_name += ext
|
||||
|
||||
# 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("?", "")
|
||||
|
||||
# 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()))
|
||||
return new_name
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding=utf-8
|
||||
"""A PyQt5 widget for managing list of comic archive files"""
|
||||
"""A PyQt4 widget for managing list of comic archive files"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
@@ -15,32 +15,34 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import os
|
||||
#import os
|
||||
#import sys
|
||||
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comictaggerlib.ui.qtutils import centerWindowOnParent, reduceWidgetFontSize
|
||||
|
||||
from . import utils
|
||||
from .comicarchive import ComicArchive
|
||||
from .optionalmsgdialog import OptionalMessageDialog
|
||||
from .settings import ComicTaggerSettings
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from optionalmsgdialog import OptionalMessageDialog
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
|
||||
import utils
|
||||
#from comicarchive import MetaDataStyle
|
||||
#from genericmetadata import GenericMetadata, PageType
|
||||
|
||||
|
||||
class FileTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __lt__(self, other):
|
||||
# return (self.data(Qt.UserRole).toBool() <
|
||||
# other.data(Qt.UserRole).toBool())
|
||||
return self.data(Qt.UserRole) < other.data(Qt.UserRole)
|
||||
return (self.data(Qt.UserRole).toBool() <
|
||||
other.data(Qt.UserRole).toBool())
|
||||
|
||||
|
||||
class FileInfo:
|
||||
class FileInfo():
|
||||
|
||||
def __init__(self, ca):
|
||||
self.ca = ca
|
||||
|
||||
@@ -61,14 +63,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)
|
||||
|
||||
@@ -81,8 +83,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)
|
||||
@@ -106,10 +108,24 @@ 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)
|
||||
@@ -122,7 +138,8 @@ class FileSelectionList(QWidget):
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def getArchiveByRow(self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data(Qt.UserRole)
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
|
||||
Qt.UserRole).toPyObject()
|
||||
return fi.ca
|
||||
|
||||
def getCurrentArchive(self):
|
||||
@@ -138,7 +155,9 @@ 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()
|
||||
@@ -167,23 +186,19 @@ class FileSelectionList(QWidget):
|
||||
filelist = utils.get_recursive_filelist(pathlist)
|
||||
# we now have a list of files to add
|
||||
|
||||
# Prog dialog on Linux flakes out for small range, so scale up
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), parent=self)
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle("Adding Files")
|
||||
# progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.setWindowModality(Qt.ApplicationModal)
|
||||
progdialog.setMinimumDuration(300)
|
||||
centerWindowOnParent(progdialog)
|
||||
# QCoreApplication.processEvents()
|
||||
# progdialog.show()
|
||||
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 + 1)
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
centerWindowOnParent(progdialog)
|
||||
QCoreApplication.processEvents()
|
||||
@@ -191,24 +206,22 @@ class FileSelectionList(QWidget):
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
progdialog.hide()
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
if self.settings.show_no_unrar_warning and self.settings.unrar_lib_path == "" and not ComicTaggerSettings.haveOwnUnrarLib():
|
||||
progdialog.close()
|
||||
if (self.settings.show_no_unrar_warning and
|
||||
self.settings.unrar_exe_path == "" and
|
||||
self.settings.rar_exe_path == "" and
|
||||
platform.system() != "Windows"):
|
||||
for f in filelist:
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
if ext == ".rar" or ext == ".cbr":
|
||||
checked = OptionalMessageDialog.msg(
|
||||
self,
|
||||
"No UnRAR Ability",
|
||||
"""
|
||||
checked = OptionalMessageDialog.msg(self, "No unrar tool",
|
||||
"""
|
||||
It looks like you've tried to open at least one CBR or RAR file.<br><br>
|
||||
In order for ComicTagger to read this kind of file, you will have to configure
|
||||
the location of the unrar 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.
|
||||
""",
|
||||
)
|
||||
the location of the unrar tool in the settings. Until then, ComicTagger
|
||||
will not be able recognize these kind of files.
|
||||
"""
|
||||
)
|
||||
self.settings.show_no_unrar_warning = not checked
|
||||
break
|
||||
|
||||
@@ -216,17 +229,13 @@ class FileSelectionList(QWidget):
|
||||
self.twList.selectRow(firstAdded)
|
||||
else:
|
||||
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
|
||||
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."))
|
||||
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 readable comic archives were found."))
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self.tr("File/Folder Open"),
|
||||
self.tr("No comic archives were found."))
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
@@ -262,14 +271,17 @@ class FileSelectionList(QWidget):
|
||||
return -1
|
||||
|
||||
def addPathItem(self, path):
|
||||
path = str(path)
|
||||
path = unicode(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()
|
||||
@@ -286,10 +298,12 @@ 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)
|
||||
@@ -304,14 +318,16 @@ 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)
|
||||
@@ -332,8 +348,6 @@ 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)
|
||||
@@ -368,8 +382,8 @@ class FileSelectionList(QWidget):
|
||||
ca_list = []
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if item.isSelected():
|
||||
fi = item.data(Qt.UserRole)
|
||||
if self.twList.isItemSelected(item):
|
||||
fi = item.data(Qt.UserRole).toPyObject()
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
return ca_list
|
||||
@@ -381,7 +395,7 @@ class FileSelectionList(QWidget):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if item.isSelected():
|
||||
if self.twList.isItemSelected(item):
|
||||
self.updateRow(r)
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
@@ -391,22 +405,27 @@ 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):
|
||||
@@ -414,7 +433,10 @@ 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
|
||||
@@ -423,12 +445,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: 13 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -14,37 +14,39 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3 as lite
|
||||
import os
|
||||
import datetime
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
|
||||
from . import _version
|
||||
from .settings import ComicTaggerSettings
|
||||
import urllib
|
||||
import ssl
|
||||
#import urllib2
|
||||
|
||||
try:
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
from PyQt4 import QtGui
|
||||
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
|
||||
@@ -64,6 +66,9 @@ 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):
|
||||
@@ -82,11 +87,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:
|
||||
print(url)
|
||||
image_data = requests.get(url, headers={"user-agent": "comictagger/" + _version.version}).content
|
||||
image_data = urllib.urlopen(url, context=self.ssl).read()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise ImageFetcherException("Network Error!")
|
||||
@@ -122,7 +127,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):
|
||||
@@ -136,7 +141,12 @@ 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):
|
||||
|
||||
@@ -148,12 +158,17 @@ 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):
|
||||
|
||||
@@ -171,7 +186,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:
|
||||
|
||||
@@ -14,21 +14,22 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import StringIO
|
||||
import sys
|
||||
from functools import reduce
|
||||
|
||||
try:
|
||||
from PIL import Image, WebPImagePlugin
|
||||
|
||||
from PIL import Image
|
||||
from PIL import 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
|
||||
|
||||
@@ -39,31 +40,33 @@ class ImageHasher(object):
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(io.BytesIO(data))
|
||||
except Exception as e:
|
||||
print("Image data seems corrupted! [{}]".format(e))
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
except:
|
||||
print("Image data seems corrupted!")
|
||||
# just generate a bogus image
|
||||
self.image = Image.new("L", (1, 1))
|
||||
|
||||
def average_hash(self):
|
||||
try:
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
image = self.image.resize(
|
||||
(self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception as e:
|
||||
print("average_hash error:", e)
|
||||
return int(0)
|
||||
sys.exc_clear()
|
||||
print "average_hash error:", e
|
||||
return long(0)
|
||||
|
||||
pixels = list(image.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
|
||||
def compare_value_to_avg(i):
|
||||
return 1 if i > avg else 0
|
||||
return (1 if i > avg else 0)
|
||||
|
||||
bitlist = list(map(compare_value_to_avg, pixels))
|
||||
bitlist = map(compare_value_to_avg, pixels)
|
||||
|
||||
# build up an int value from the bit list, one bit at a time
|
||||
def set_bit(x, idx_val):
|
||||
(idx, val) = idx_val
|
||||
return x | (val << idx)
|
||||
return (x | (val << idx))
|
||||
|
||||
result = reduce(set_bit, enumerate(bitlist), 0)
|
||||
|
||||
@@ -175,16 +178,16 @@ class ImageHasher(object):
|
||||
|
||||
@staticmethod
|
||||
def hamming_distance(h1, h2):
|
||||
if isinstance(h1, int) or isinstance(h1, int):
|
||||
if isinstance(h1, long) or isinstance(h1, int):
|
||||
n1 = h1
|
||||
n2 = h2
|
||||
else:
|
||||
# convert hex strings to ints
|
||||
n1 = int(h1, 16)
|
||||
n2 = int(h2, 16)
|
||||
n1 = long(h1, 16)
|
||||
n2 = long(h2, 16)
|
||||
|
||||
# xor the two numbers
|
||||
n = n1 ^ n2
|
||||
|
||||
# count up the 1's in the binary string
|
||||
return sum(b == "1" for b in bin(n)[2:])
|
||||
return sum(b == '1' for b in bin(n)[2:])
|
||||
|
||||
@@ -14,18 +14,23 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
from .settings import ComicTaggerSettings
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class ImagePopup(QtWidgets.QDialog):
|
||||
class ImagePopup(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile("imagepopup.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui'), self)
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
# self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
@@ -33,23 +38,28 @@ class ImagePopup(QtWidgets.QDialog):
|
||||
|
||||
self.imagePixmap = image_pixmap
|
||||
|
||||
screen_size = QtWidgets.QDesktopWidget().screenGeometry()
|
||||
screen_size = QtGui.QDesktopWidget().screenGeometry()
|
||||
self.resize(screen_size.width(), screen_size.height())
|
||||
self.move(0, 0)
|
||||
|
||||
# This is a total hack. Uses a snapshot of the desktop, and overlays a
|
||||
# translucent screen over it. Probably can do it better by setting opacity of a
|
||||
# widget
|
||||
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.desktopBg = QtGui.QPixmap.grabWindow(
|
||||
QtGui.QApplication.desktop().winId(),
|
||||
0,
|
||||
0,
|
||||
screen_size.width(),
|
||||
screen_size.height())
|
||||
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
|
||||
self.clientBgPixmap = bg.scaled(
|
||||
screen_size.width(), screen_size.height())
|
||||
self.setMask(self.clientBgPixmap.mask())
|
||||
|
||||
self.applyImagePixmap()
|
||||
self.showFullScreen()
|
||||
self.raise_()
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def paintEvent(self, event):
|
||||
self.painter = QtGui.QPainter(self)
|
||||
@@ -62,9 +72,11 @@ class ImagePopup(QtWidgets.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, QtCore.Qt.SmoothTransformation)
|
||||
display_pixmap = self.imagePixmap.scaled(
|
||||
win_w, win_h, QtCore.Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap(display_pixmap)
|
||||
else:
|
||||
display_pixmap = self.imagePixmap
|
||||
|
||||
@@ -14,23 +14,27 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
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
|
||||
import StringIO
|
||||
#import math
|
||||
#import urllib2
|
||||
#import urllib
|
||||
|
||||
try:
|
||||
from PIL import Image, WebPImagePlugin
|
||||
|
||||
from PIL import Image
|
||||
from PIL import 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):
|
||||
@@ -75,7 +79,8 @@ 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
|
||||
@@ -110,16 +115,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(io.StringIO(image_data))
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w, h = im.size
|
||||
return float(h) / float(w)
|
||||
except:
|
||||
@@ -127,16 +132,17 @@ class IssueIdentifier:
|
||||
|
||||
def cropCover(self, image_data):
|
||||
|
||||
im = Image.open(io.StringIO(image_data))
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w, h = im.size
|
||||
|
||||
try:
|
||||
cropped_im = im.crop((int(w / 2), 0, w, h))
|
||||
except Exception as e:
|
||||
print("cropCover() error:", e)
|
||||
sys.exc_clear()
|
||||
print "cropCover() error:", e
|
||||
return None
|
||||
|
||||
output = io.StringIO()
|
||||
output = StringIO.StringIO()
|
||||
cropped_im.save(output, format="PNG")
|
||||
cropped_image_data = output.getvalue()
|
||||
output.close()
|
||||
@@ -153,21 +159,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
|
||||
@@ -187,39 +193,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
|
||||
|
||||
@@ -234,15 +240,24 @@ 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:
|
||||
@@ -254,21 +269,24 @@ 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:
|
||||
@@ -279,15 +297,16 @@ 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)
|
||||
|
||||
@@ -295,11 +314,12 @@ 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)
|
||||
@@ -315,12 +335,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
|
||||
@@ -335,11 +355,13 @@ 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)
|
||||
@@ -355,42 +377,44 @@ 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("Searching for {0} #{1} ...".format(keys["series"], keys["issue_number"]))
|
||||
self.log_msg(u"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 []
|
||||
|
||||
@@ -399,51 +423,61 @@ 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:
|
||||
@@ -453,14 +487,19 @@ 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("Found {0} series that have an issue #{1}".format(len(shortlist), keys["issue_number"]))
|
||||
if keys['year'] is None:
|
||||
self.log_msg(u"Found {0} series that have an issue #{1}".format(
|
||||
len(shortlist), keys['issue_number']))
|
||||
else:
|
||||
self.log_msg("Found {0} series that have an issue #{1} from {2}".format(len(shortlist), keys["issue_number"], keys["year"]))
|
||||
self.log_msg(
|
||||
u"Found {0} series that have an issue #{1} from {2}".format(
|
||||
len(shortlist),
|
||||
keys['issue_number'],
|
||||
keys['year']))
|
||||
|
||||
# now we have a shortlist of volumes with the desired issue number
|
||||
# Do first round of cover matching
|
||||
@@ -470,10 +509,13 @@ class IssueIdentifier:
|
||||
self.callback(counter, len(shortlist) * 3)
|
||||
counter += 1
|
||||
|
||||
self.log_msg("Examining covers for ID: {0} {1} ({2}) ...".format(series["id"], series["name"], series["start_year"]), newline=False)
|
||||
self.log_msg(u"Examining covers for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=False)
|
||||
|
||||
# parse out the cover date
|
||||
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]
|
||||
@@ -481,39 +523,45 @@ 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"] = "{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'] = u"{0} ({1})".format(
|
||||
series['name'], series['start_year'])
|
||||
match['distance'] = score_item['score']
|
||||
match['issue_number'] = keys['issue_number']
|
||||
match['cv_issue_count'] = series['count_of_issues']
|
||||
match['url_image_hash'] = score_item['hash']
|
||||
match['issue_title'] = issue['name']
|
||||
match['issue_id'] = issue['id']
|
||||
match['volume_id'] = series['id']
|
||||
match['month'] = month
|
||||
match['year'] = year
|
||||
match['publisher'] = None
|
||||
if series['publisher'] is not None:
|
||||
match['publisher'] = series['publisher']['name']
|
||||
match['image_url'] = image_url
|
||||
match['thumb_url'] = thumb_url
|
||||
match['page_url'] = page_url
|
||||
match['description'] = issue['description']
|
||||
|
||||
self.match_list.append(match)
|
||||
|
||||
self.log_msg(" --> {0}".format(match["distance"]), newline=False)
|
||||
self.log_msg(" --> {0}".format(match['distance']), newline=False)
|
||||
|
||||
self.log_msg("")
|
||||
|
||||
@@ -523,29 +571,33 @@ 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(
|
||||
"-----> {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(u"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
|
||||
item['series'],
|
||||
item['issue_number'],
|
||||
item['issue_title'],
|
||||
item['month'],
|
||||
item['year'],
|
||||
item['distance']))
|
||||
|
||||
best_score = 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)
|
||||
@@ -560,32 +612,46 @@ class IssueIdentifier:
|
||||
if self.callback is not None:
|
||||
self.callback(counter, len(self.match_list) * 3)
|
||||
counter += 1
|
||||
self.log_msg("Examining alternate covers for ID: {0} {1} ...".format(m["volume_id"], m["series"]), newline=False)
|
||||
self.log_msg(
|
||||
u"Examining alternate covers for ID: {0} {1} ...".format(
|
||||
m['volume_id'],
|
||||
m['series']),
|
||||
newline=False)
|
||||
try:
|
||||
score_item = self.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("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultFoundMatchButBadCoverScore
|
||||
else:
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg("Multiple bad cover matches! Need to use other info...")
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"Multiple bad cover matches! Need to use other info...")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultMultipleMatchesWithBadImageScores
|
||||
return self.match_list
|
||||
else:
|
||||
@@ -594,9 +660,10 @@ 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:
|
||||
@@ -605,41 +672,51 @@ 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("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
|
||||
elif len(self.match_list) == 0:
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.log_msg("No matches found :(")
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
self.search_result = self.ResultNoMatches
|
||||
else:
|
||||
# we've got multiple good matches:
|
||||
self.log_msg("More than one likely candidate.")
|
||||
self.search_result = self.ResultMultipleGoodMatches
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
for item in self.match_list:
|
||||
print_match(item)
|
||||
self.log_msg("--------------------------------------------------------------------------")
|
||||
self.log_msg(
|
||||
u"--------------------------------------------------------------------------")
|
||||
|
||||
return self.match_list
|
||||
|
||||
@@ -14,42 +14,54 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#import sys
|
||||
#import os
|
||||
#import re
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
|
||||
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from .issuestring import IssueString
|
||||
from .settings import ComicTaggerSettings
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
|
||||
class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem):
|
||||
class IssueNumberTableWidgetItem(QtGui.QTableWidgetItem):
|
||||
|
||||
def __lt__(self, other):
|
||||
selfStr = self.data(QtCore.Qt.DisplayRole)
|
||||
otherStr = other.data(QtCore.Qt.DisplayRole)
|
||||
return IssueString(selfStr).asFloat() < IssueString(otherStr).asFloat()
|
||||
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
|
||||
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
|
||||
return (IssueString(selfStr).asFloat() <
|
||||
IssueString(otherStr).asFloat())
|
||||
|
||||
|
||||
class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, settings, series_id, issue_number):
|
||||
super(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 = QtWidgets.QGridLayout(self.coverImageContainer)
|
||||
self.coverWidget = CoverImageWidget(
|
||||
self.coverImageContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.coverImageContainer)
|
||||
gridlayout.addWidget(self.coverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
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
|
||||
@@ -73,25 +85,33 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
issue_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
|
||||
if issue_id == self.initial_id:
|
||||
issue_id, b = self.twList.item(
|
||||
r, 0).data(QtCore.Qt.UserRole).toInt()
|
||||
if (issue_id == self.initial_id):
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def performQuery(self):
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
QtGui.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:
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
if e.code == ComicVineTalkerException.RateLimit:
|
||||
QtWidgets.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage())
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Comic Vine Error"),
|
||||
ComicVineTalker.getRateLimitMessage())
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to list issues!"))
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Network Issue"),
|
||||
self.tr("Could not connect to Comic Vine to list issues!"))
|
||||
return
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
@@ -103,15 +123,15 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
for record in self.issue_list:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record["issue_number"]
|
||||
item_text = record['issue_number']
|
||||
item = IssueNumberTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.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"
|
||||
@@ -119,28 +139,30 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
if len(parts) > 1:
|
||||
item_text = parts[0] + "-" + parts[1]
|
||||
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record["name"]
|
||||
item_text = record['name']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if IssueString(record["issue_number"]).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)
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
@@ -152,16 +174,17 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
|
||||
self.issue_id, b = self.twList.item(
|
||||
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record["id"] == self.issue_id:
|
||||
self.issue_number = record["issue_number"]
|
||||
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
|
||||
|
||||
@@ -14,22 +14,24 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
from .settings import ComicTaggerSettings
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class LogWindow(QtWidgets.QDialog):
|
||||
class LogWindow(QtGui.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)
|
||||
|
||||
@@ -14,85 +14,65 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import sys
|
||||
import signal
|
||||
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()
|
||||
import platform
|
||||
#import os
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from .taggerwindow import TaggerWindow
|
||||
from PyQt4 import QtCore, QtGui
|
||||
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("PyQt5 is not available. ComicTagger is limited to command-line mode.", file=sys.stderr)
|
||||
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
|
||||
|
||||
if opts.no_gui:
|
||||
cli.cli_mode(opts, SETTINGS)
|
||||
cli.cli_mode(opts, settings)
|
||||
else:
|
||||
|
||||
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)
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic("tags.png"))
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
|
||||
|
||||
splash = QtWidgets.QSplashScreen(img)
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(opts.file_list, SETTINGS, opts=opts)
|
||||
tagger_window.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("app.png")))
|
||||
tagger_window = TaggerWindow(opts.file_list, settings, opts=opts)
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
@@ -100,4 +80,8 @@ def ctmain():
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc())
|
||||
QtGui.QMessageBox.critical(
|
||||
QtGui.QMainWindow(),
|
||||
"Error",
|
||||
"Unhandled exception in app:\n" +
|
||||
traceback.format_exc())
|
||||
|
||||
@@ -15,38 +15,48 @@
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
#import sys
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from .settings import ComicTaggerSettings
|
||||
#from imagefetcher import ImageFetcher
|
||||
#from comicarchive import MetaDataStyle
|
||||
#from comicvinetalker import ComicVineTalker
|
||||
#import utils
|
||||
|
||||
|
||||
class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
class MatchSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, matches, comic_archive):
|
||||
super(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 = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
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
|
||||
@@ -64,7 +74,8 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
self.twList.selectRow(0)
|
||||
|
||||
path = self.comic_archive.path
|
||||
self.setWindowTitle("Select correct match: {0}".format(os.path.split(path)[1]))
|
||||
self.setWindowTitle(u"Select correct match: {0}".format(
|
||||
os.path.split(path)[1]))
|
||||
|
||||
def populateTable(self):
|
||||
|
||||
@@ -77,39 +88,39 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
for match in self.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match["series"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match["publisher"] is not None:
|
||||
item_text = "{0}".format(match["publisher"])
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = "Unknown"
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
month_str = ""
|
||||
year_str = "????"
|
||||
if match["month"] is not None:
|
||||
month_str = "-{0:02d}".format(int(match["month"]))
|
||||
if match["year"] is not None:
|
||||
year_str = "{0}".format(match["year"])
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match["issue_title"]
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
@@ -133,16 +144,17 @@ class MatchSelectionWindow(QtWidgets.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)[0]
|
||||
match = self.twList.item(row, 0).data(
|
||||
QtCore.Qt.UserRole).toPyObject()[0]
|
||||
return match
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 dialog to show a message and let the user check a box
|
||||
"""A PyQt4 dialog to show a message and let the user check a box
|
||||
|
||||
Example usage:
|
||||
|
||||
@@ -25,16 +25,18 @@ said_yes, checked = OptionalMessageDialog.question(self, "Question",
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui 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)
|
||||
@@ -46,7 +48,8 @@ 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)
|
||||
@@ -67,7 +70,11 @@ 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)
|
||||
|
||||
@@ -82,15 +89,28 @@ 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_()
|
||||
|
||||
|
||||
@@ -14,22 +14,23 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import getopt
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import getopt
|
||||
import platform
|
||||
import os
|
||||
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 ...]]
|
||||
@@ -111,7 +112,6 @@ 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,9 +184,12 @@ 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
|
||||
|
||||
@@ -207,9 +210,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):
|
||||
@@ -229,16 +232,18 @@ 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:
|
||||
@@ -246,7 +251,8 @@ 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, "--")
|
||||
@@ -254,41 +260,15 @@ 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)
|
||||
@@ -310,8 +290,6 @@ 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":
|
||||
@@ -321,7 +299,8 @@ 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"):
|
||||
@@ -361,8 +340,10 @@ 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(_version.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(ctversion.version))
|
||||
print(
|
||||
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
@@ -396,7 +377,9 @@ 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)
|
||||
@@ -405,7 +388,6 @@ 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))
|
||||
@@ -418,17 +400,22 @@ 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)
|
||||
|
||||
@@ -15,39 +15,47 @@
|
||||
# limitations under the License.
|
||||
|
||||
import platform
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from .settings import ComicTaggerSettings
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
|
||||
class PageBrowserWindow(QtWidgets.QDialog):
|
||||
class PageBrowserWindow(QtGui.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 = QtWidgets.QGridLayout(self.pageContainer)
|
||||
self.pageWidget = CoverImageWidget(
|
||||
self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.pageContainer)
|
||||
gridlayout.addWidget(self.pageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.setWindowFlags(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(QtWidgets.QDialogButtonBox.Close).setDefault(True)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Close).setDefault(True)
|
||||
if platform.system() == "Darwin":
|
||||
self.btnPrev.setText("<<")
|
||||
self.btnNext.setText(">>")
|
||||
else:
|
||||
self.btnPrev.setIcon(QtGui.QIcon(ComicTaggerSettings.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)
|
||||
@@ -96,9 +104,11 @@ class PageBrowserWindow(QtWidgets.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))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQt5 widget for editing the page list info"""
|
||||
"""A PyQt4 widget for editing the page list info"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
from operator import attrgetter, itemgetter
|
||||
#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 .comicarchive import MetaDataStyle
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from .genericmetadata import GenericMetadata, PageType
|
||||
from .settings import ComicTaggerSettings
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
#from pageloader import PageLoader
|
||||
|
||||
|
||||
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.setEnabled(True)
|
||||
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
self.pageWidget = CoverImageWidget(
|
||||
self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QGridLayout(self.pageContainer)
|
||||
gridlayout.addWidget(self.pageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -88,17 +88,28 @@ 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)
|
||||
@@ -108,90 +119,31 @@ 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.SetSelection(selection)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
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.SetSelection(selection)
|
||||
self.modified.emit()
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def itemMoveEvent(self, s):
|
||||
# print "move event: ", s, self.listWidget.currentRow()
|
||||
@@ -204,7 +156,7 @@ class PageListEditor(QWidget):
|
||||
self.modified.emit()
|
||||
|
||||
def changePageType(self, i):
|
||||
new_type = self.comboBox.itemData(i)
|
||||
new_type = self.comboBox.itemData(i).toString()
|
||||
if self.getCurrentPageType() != new_type:
|
||||
self.setCurrentPageType(new_type)
|
||||
self.emitFrontCoverChange()
|
||||
@@ -217,8 +169,9 @@ 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)[0]["Image"])
|
||||
#idx = int(str (self.listWidget.item(row).text()))
|
||||
idx = int(self.listWidget.item(row).data(
|
||||
Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.pageWidget.setArchive(self.comic_archive, idx)
|
||||
@@ -227,29 +180,30 @@ class PageListEditor(QWidget):
|
||||
frontCover = 0
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
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"])
|
||||
page_dict = item.data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict and page_dict[
|
||||
'Type'] == PageType.FrontCover:
|
||||
frontCover = int(page_dict['Image'])
|
||||
break
|
||||
return frontCover
|
||||
|
||||
def getCurrentPageType(self):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] # .toPyObject()[0]
|
||||
if "Type" in page_dict:
|
||||
return page_dict["Type"]
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict:
|
||||
return page_dict['Type']
|
||||
else:
|
||||
return ""
|
||||
|
||||
def setCurrentPageType(self, t):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] # .toPyObject()[0]
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
|
||||
|
||||
if t == "":
|
||||
if "Type" in page_dict:
|
||||
del page_dict["Type"]
|
||||
if 'Type' in page_dict:
|
||||
del(page_dict['Type'])
|
||||
else:
|
||||
page_dict["Type"] = 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
|
||||
@@ -276,16 +230,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)[0]) # .toPyObject()[0]
|
||||
page_list.append(item.data(Qt.UserRole).toPyObject()[0])
|
||||
return page_list
|
||||
|
||||
def emitFrontCoverChange(self):
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, uic
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comictaggerlib.ui.qtutils import getQImageFromData
|
||||
#from comicarchive import ComicArchive
|
||||
#import utils
|
||||
|
||||
|
||||
class PageLoader(QtCore.QThread):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A PyQT5 dialog to show ID log and progress"""
|
||||
"""A PyQT4 dialog to show ID log and progress"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
@@ -14,20 +14,25 @@
|
||||
# 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 comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
|
||||
from .settings import ComicTaggerSettings
|
||||
from settings import ComicTaggerSettings
|
||||
#import utils
|
||||
|
||||
|
||||
class IDProgressWindow(QtWidgets.QDialog):
|
||||
class IDProgressWindow(QtGui.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)
|
||||
|
||||
@@ -16,25 +16,28 @@
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
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
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from comicarchive import MetaDataStyle
|
||||
import utils
|
||||
|
||||
|
||||
class RenameWindow(QtWidgets.QDialog):
|
||||
class RenameWindow(QtGui.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
|
||||
@@ -47,8 +50,10 @@ class RenameWindow(QtWidgets.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 = []
|
||||
@@ -70,49 +75,37 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
||||
self.renamer.setMetadata(md)
|
||||
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
|
||||
new_name = self.renamer.determineName(ca.path, ext=new_ext)
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow(row)
|
||||
folder_item = QtWidgets.QTableWidgetItem()
|
||||
old_name_item = QtWidgets.QTableWidgetItem()
|
||||
new_name_item = QtWidgets.QTableWidgetItem()
|
||||
folder_item = QtGui.QTableWidgetItem()
|
||||
old_name_item = QtGui.QTableWidgetItem()
|
||||
new_name_item = QtGui.QTableWidgetItem()
|
||||
|
||||
item_text = os.path.split(ca.path)[0]
|
||||
folder_item.setFlags(QtCore.Qt.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
|
||||
@@ -135,44 +128,36 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
|
||||
def accept(self):
|
||||
|
||||
progdialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog = QtGui.QProgressDialog(
|
||||
"", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog.setWindowTitle("Renaming Archives")
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setMinimumDuration(100)
|
||||
centerWindowOnParent(progdialog)
|
||||
# progdialog.show()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
progdialog.show()
|
||||
|
||||
for idx, item in enumerate(self.rename_list):
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
idx += 1
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(item["new_name"])
|
||||
centerWindowOnParent(progdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
idx += 1
|
||||
progdialog.setLabelText(item['new_name'])
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(item["archive"].path))
|
||||
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
|
||||
folder = self.settings.rename_dir.strip()
|
||||
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
|
||||
|
||||
if os.path.join(folder, item["new_name"]) == item["archive"].path:
|
||||
print(item["new_name"], "Filename is already good!")
|
||||
if item['new_name'] == os.path.basename(item['archive'].path):
|
||||
print item['new_name'], "Filename is already good!"
|
||||
continue
|
||||
|
||||
if not item["archive"].isWritable(check_rar_status=False):
|
||||
if not item['archive'].isWritable(check_rar_status=False):
|
||||
continue
|
||||
|
||||
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
|
||||
os.rename(item["archive"].path, new_abs_path)
|
||||
folder = os.path.dirname(os.path.abspath(item['archive'].path))
|
||||
new_abs_path = utils.unique_file(
|
||||
os.path.join(folder, item['new_name']))
|
||||
|
||||
item["archive"].rename(new_abs_path)
|
||||
os.rename(item['archive'].path, new_abs_path)
|
||||
|
||||
progdialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
item['archive'].rename(new_abs_path)
|
||||
|
||||
QtWidgets.QDialog.accept(self)
|
||||
progdialog.close()
|
||||
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
@@ -14,59 +14,64 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import configparser
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import configparser
|
||||
import platform
|
||||
import codecs
|
||||
import uuid
|
||||
|
||||
from . import utils
|
||||
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
|
||||
folder = folder.decode(filename_encoding)
|
||||
return folder
|
||||
|
||||
@staticmethod
|
||||
def defaultLibunrarPath():
|
||||
return ComicTaggerSettings.baseDir() + "/libunrar.so"
|
||||
|
||||
@staticmethod
|
||||
def haveOwnUnrarLib():
|
||||
return os.path.exists(ComicTaggerSettings.defaultLibunrarPath())
|
||||
frozen_win_exe_path = None
|
||||
|
||||
@staticmethod
|
||||
def baseDir():
|
||||
if getattr(sys, "frozen", None):
|
||||
return sys._MEIPASS
|
||||
if getattr(sys, 'frozen', None):
|
||||
if platform.system() == "Darwin":
|
||||
return sys._MEIPASS
|
||||
else: # Windows
|
||||
# Preserve this value, in case sys.argv gets changed importing
|
||||
# a plugin script
|
||||
if ComicTaggerSettings.frozen_win_exe_path is None:
|
||||
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname(
|
||||
os.path.abspath(sys.argv[0]))
|
||||
return ComicTaggerSettings.frozen_win_exe_path
|
||||
else:
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@staticmethod
|
||||
def 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_lib_path = ""
|
||||
self.unrar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
self.check_for_new_version = False
|
||||
self.check_for_new_version = True
|
||||
self.send_usage_stats = False
|
||||
|
||||
# automatic settings
|
||||
@@ -85,7 +90,7 @@ class ComicTaggerSettings:
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
@@ -102,7 +107,6 @@ 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
|
||||
|
||||
@@ -117,12 +121,10 @@ class ComicTaggerSettings:
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
|
||||
# Rename settings
|
||||
self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
self.rename_dir = ""
|
||||
self.rename_move_dir = False
|
||||
|
||||
# Auto-tag stickies
|
||||
self.save_on_low_confidence = False
|
||||
@@ -152,7 +154,7 @@ class ComicTaggerSettings:
|
||||
else:
|
||||
self.load()
|
||||
|
||||
# take a crack at finding rar exe, if not set already
|
||||
# take a crack at finding rar exes, if not set already
|
||||
if self.rar_exe_path == "":
|
||||
if platform.system() == "Windows":
|
||||
# look in some likely places for Windows machines
|
||||
@@ -166,261 +168,355 @@ 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.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 != "":
|
||||
if self.unrar_exe_path == "":
|
||||
if platform.system() != "Windows":
|
||||
# see if it's in the path of unix user
|
||||
if utils.which("unrar") is not None:
|
||||
self.unrar_exe_path = utils.which("unrar")
|
||||
if self.unrar_exe_path != "":
|
||||
self.save()
|
||||
|
||||
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
|
||||
# make sure unrar/rar programs are now in the path for the UnRAR class to
|
||||
# use
|
||||
utils.addtopath(os.path.dirname(self.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.rar_exe_path))
|
||||
|
||||
def reset(self):
|
||||
os.unlink(self.settings_file)
|
||||
self.__init__()
|
||||
|
||||
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")
|
||||
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")
|
||||
self.rar_exe_path = self.config.get('settings', 'rar_exe_path')
|
||||
self.unrar_exe_path = self.config.get('settings', 'unrar_exe_path')
|
||||
if self.config.has_option('settings', 'check_for_new_version'):
|
||||
self.check_for_new_version = self.config.getboolean(
|
||||
'settings', 'check_for_new_version')
|
||||
if self.config.has_option('settings', 'send_usage_stats'):
|
||||
self.send_usage_stats = self.config.getboolean(
|
||||
'settings', 'send_usage_stats')
|
||||
|
||||
if self.config.has_option("auto", "install_id"):
|
||||
self.install_id = self.config.get("auto", "install_id")
|
||||
if self.config.has_option("auto", "last_selected_load_data_style"):
|
||||
self.last_selected_load_data_style = self.config.getint("auto", "last_selected_load_data_style")
|
||||
if self.config.has_option("auto", "last_selected_save_data_style"):
|
||||
self.last_selected_save_data_style = self.config.getint("auto", "last_selected_save_data_style")
|
||||
if self.config.has_option("auto", "last_opened_folder"):
|
||||
self.last_opened_folder = self.config.get("auto", "last_opened_folder")
|
||||
if self.config.has_option("auto", "last_main_window_width"):
|
||||
self.last_main_window_width = self.config.getint("auto", "last_main_window_width")
|
||||
if self.config.has_option("auto", "last_main_window_height"):
|
||||
self.last_main_window_height = self.config.getint("auto", "last_main_window_height")
|
||||
if self.config.has_option("auto", "last_main_window_x"):
|
||||
self.last_main_window_x = self.config.getint("auto", "last_main_window_x")
|
||||
if self.config.has_option("auto", "last_main_window_y"):
|
||||
self.last_main_window_y = self.config.getint("auto", "last_main_window_y")
|
||||
if self.config.has_option("auto", "last_form_side_width"):
|
||||
self.last_form_side_width = self.config.getint("auto", "last_form_side_width")
|
||||
if self.config.has_option("auto", "last_list_side_width"):
|
||||
self.last_list_side_width = self.config.getint("auto", "last_list_side_width")
|
||||
if self.config.has_option("auto", "last_filelist_sorted_column"):
|
||||
self.last_filelist_sorted_column = self.config.getint("auto", "last_filelist_sorted_column")
|
||||
if self.config.has_option("auto", "last_filelist_sorted_order"):
|
||||
self.last_filelist_sorted_order = self.config.getint("auto", "last_filelist_sorted_order")
|
||||
if self.config.has_option('auto', 'install_id'):
|
||||
self.install_id = self.config.get('auto', 'install_id')
|
||||
if self.config.has_option('auto', 'last_selected_load_data_style'):
|
||||
self.last_selected_load_data_style = self.config.getint(
|
||||
'auto', 'last_selected_load_data_style')
|
||||
if self.config.has_option('auto', 'last_selected_save_data_style'):
|
||||
self.last_selected_save_data_style = self.config.getint(
|
||||
'auto', 'last_selected_save_data_style')
|
||||
if self.config.has_option('auto', 'last_opened_folder'):
|
||||
self.last_opened_folder = self.config.get(
|
||||
'auto', 'last_opened_folder')
|
||||
if self.config.has_option('auto', 'last_main_window_width'):
|
||||
self.last_main_window_width = self.config.getint(
|
||||
'auto', 'last_main_window_width')
|
||||
if self.config.has_option('auto', 'last_main_window_height'):
|
||||
self.last_main_window_height = self.config.getint(
|
||||
'auto', 'last_main_window_height')
|
||||
if self.config.has_option('auto', 'last_main_window_x'):
|
||||
self.last_main_window_x = self.config.getint(
|
||||
'auto', 'last_main_window_x')
|
||||
if self.config.has_option('auto', 'last_main_window_y'):
|
||||
self.last_main_window_y = self.config.getint(
|
||||
'auto', 'last_main_window_y')
|
||||
if self.config.has_option('auto', 'last_form_side_width'):
|
||||
self.last_form_side_width = self.config.getint(
|
||||
'auto', 'last_form_side_width')
|
||||
if self.config.has_option('auto', 'last_list_side_width'):
|
||||
self.last_list_side_width = self.config.getint(
|
||||
'auto', 'last_list_side_width')
|
||||
if self.config.has_option('auto', 'last_filelist_sorted_column'):
|
||||
self.last_filelist_sorted_column = self.config.getint(
|
||||
'auto', 'last_filelist_sorted_column')
|
||||
if self.config.has_option('auto', 'last_filelist_sorted_order'):
|
||||
self.last_filelist_sorted_order = self.config.getint(
|
||||
'auto', 'last_filelist_sorted_order')
|
||||
|
||||
if self.config.has_option("identifier", "id_length_delta_thresh"):
|
||||
self.id_length_delta_thresh = self.config.getint("identifier", "id_length_delta_thresh")
|
||||
if self.config.has_option("identifier", "id_publisher_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_dir"):
|
||||
self.rename_dir = self.config.get("rename", "rename_dir")
|
||||
if self.config.has_option("rename", "rename_move_dir"):
|
||||
self.rename_move_dir = self.config.getboolean("rename", "rename_move_dir")
|
||||
if self.config.has_option('rename', 'rename_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("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")
|
||||
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')
|
||||
|
||||
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_lib_path", self.unrar_lib_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_exe_path', self.unrar_exe_path)
|
||||
self.config.set('settings', 'send_usage_stats', self.send_usage_stats)
|
||||
|
||||
if not self.config.has_section("auto"):
|
||||
self.config.add_section("auto")
|
||||
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_dir", self.rename_dir)
|
||||
self.config.set("rename", "rename_move_dir", self.rename_move_dir)
|
||||
self.config.set('rename', 'rename_template', self.rename_template)
|
||||
self.config.set(
|
||||
'rename',
|
||||
'rename_issue_number_padding',
|
||||
self.rename_issue_number_padding)
|
||||
self.config.set(
|
||||
'rename',
|
||||
'rename_use_smart_string_cleanup',
|
||||
self.rename_use_smart_string_cleanup)
|
||||
self.config.set('rename', 'rename_extension_based_on_archive',
|
||||
self.rename_extension_based_on_archive)
|
||||
|
||||
if not self.config.has_section("autotag"):
|
||||
self.config.add_section("autotag")
|
||||
self.config.set("autotag", "save_on_low_confidence", self.save_on_low_confidence)
|
||||
self.config.set("autotag", "dont_use_year_when_identifying", self.dont_use_year_when_identifying)
|
||||
self.config.set("autotag", "assume_1_if_no_issue_num", self.assume_1_if_no_issue_num)
|
||||
self.config.set("autotag", "ignore_leading_numbers_in_filename", self.ignore_leading_numbers_in_filename)
|
||||
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
|
||||
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
|
||||
self.config.set("autotag", "auto_imprint", self.auto_imprint)
|
||||
if not self.config.has_section('autotag'):
|
||||
self.config.add_section('autotag')
|
||||
self.config.set(
|
||||
'autotag', 'save_on_low_confidence', self.save_on_low_confidence)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'dont_use_year_when_identifying',
|
||||
self.dont_use_year_when_identifying)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'assume_1_if_no_issue_num',
|
||||
self.assume_1_if_no_issue_num)
|
||||
self.config.set('autotag', 'ignore_leading_numbers_in_filename',
|
||||
self.ignore_leading_numbers_in_filename)
|
||||
self.config.set('autotag', 'remove_archive_after_successful_match',
|
||||
self.remove_archive_after_successful_match)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'wait_and_retry_on_rate_limit',
|
||||
self.wait_and_retry_on_rate_limit)
|
||||
|
||||
with codecs.open(self.settings_file, "wb", "utf8") as configfile:
|
||||
with codecs.open(self.settings_file, 'wb', 'utf8') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
|
||||
# make sure the basedir is cached, in case we're on Windows running a
|
||||
# script from frozen binary
|
||||
ComicTaggerSettings.baseDir()
|
||||
|
||||
@@ -14,124 +14,90 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
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 . 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>To write to CBR/RAR archives,
|
||||
<html><head/><body><p>In order to write to CBR/RAR archives,
|
||||
you will need to have the tools from
|
||||
<span style=" text-decoration: underline; color:#0000ff;">
|
||||
<a href="http://www.win-rar.com/download.html">WINRar</a></span>
|
||||
installed. (ComicTagger only uses the command-line rar tool,
|
||||
which is free to use.)</p></body></html>
|
||||
<a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span>
|
||||
</a> installed. </p></body></html>
|
||||
"""
|
||||
|
||||
linuxRarHelp = """
|
||||
<html><head/><body><p>To write to CBR/RAR archives,
|
||||
you will need to have the shareware rar tool from RARLab installed.
|
||||
Your package manager should have rar (e.g. "apt-get install rar"). If not, download it
|
||||
<span style=" text-decoration: underline; color:#0000ff;">
|
||||
<a href="https://www.rarlab.com/download.htm">here</a></span>,
|
||||
and install in your path. </p></body></html>
|
||||
<html><head/><body><p>In order to read/write to CBR/RAR archives,
|
||||
you will need to have the shareware tools from WinRar installed.
|
||||
Your package manager should have unrar, and probably rar.
|
||||
If not, download them <a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">here</span>
|
||||
</a>, and install in your path. </p></body></html>
|
||||
"""
|
||||
|
||||
macRarHelp = """
|
||||
<html><head/><body><p>To write to CBR/RAR archives,
|
||||
you will need the rar tool. The easiest way to get this is
|
||||
to install <span style=" text-decoration: underline; color:#0000ff;">
|
||||
<a href="https://brew.sh/">homebrew</a></span>.
|
||||
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
|
||||
"""
|
||||
|
||||
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>
|
||||
<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>
|
||||
"""
|
||||
|
||||
|
||||
class SettingsWindow(QtWidgets.QDialog):
|
||||
class SettingsWindow(QtGui.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)
|
||||
@@ -147,74 +113,16 @@ class SettingsWindow(QtWidgets.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.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)
|
||||
self.leUnrarExePath.setText(self.settings.unrar_exe_path)
|
||||
self.leNameLengthDeltaThresh.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.tePublisherBlacklist.setPlainText(
|
||||
self.settings.id_publisher_blacklist)
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
|
||||
@@ -245,53 +153,29 @@ class SettingsWindow(QtWidgets.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())
|
||||
|
||||
# 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!
|
||||
# make sure unrar/rar program is now in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
@@ -301,16 +185,18 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
|
||||
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_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 = str(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key.strip()
|
||||
self.settings.cv_api_key = unicode(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
@@ -322,50 +208,53 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
|
||||
self.settings.rename_template = str(self.leRenameTemplate.text())
|
||||
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.settings.rename_issue_number_padding = int(
|
||||
self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
|
||||
self.settings.rename_dir = self.leDirectory.text()
|
||||
|
||||
self.settings.save()
|
||||
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.")
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
def selectRar(self):
|
||||
self.selectFile(self.leRarExePath, "RAR")
|
||||
|
||||
def selectUnrar(self):
|
||||
self.selectFile(self.leUnrarLibPath, "UnRAR")
|
||||
self.selectFile(self.leUnrarExePath, "UnRAR")
|
||||
|
||||
def clearCache(self):
|
||||
ImageFetcher().clearCache()
|
||||
ComicVineCacher().clearCache()
|
||||
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
|
||||
QtGui.QMessageBox.information(
|
||||
self, self.name, "Cache has been cleared.")
|
||||
|
||||
def testAPIKey(self):
|
||||
if ComicVineTalker().testKey(str(self.leKey.text()).strip()):
|
||||
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
|
||||
if ComicVineTalker().testKey(unicode(self.leKey.text())):
|
||||
QtGui.QMessageBox.information(
|
||||
self, "API Key Test", "Key is valid!")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
|
||||
QtGui.QMessageBox.warning(
|
||||
self, "API Key Test", "Key is NOT valid.")
|
||||
|
||||
def resetSettings(self):
|
||||
self.settings.reset()
|
||||
self.settingsToForm()
|
||||
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
self.name,
|
||||
self.name +
|
||||
" have been returned to default values.")
|
||||
|
||||
def selectFile(self, control, name):
|
||||
|
||||
dialog = QtWidgets.QFileDialog(self)
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
|
||||
dialog = QtGui.QFileDialog(self)
|
||||
dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
if name == "RAR":
|
||||
filter = self.tr("Rar Program (Rar.exe)")
|
||||
else:
|
||||
filter = self.tr("Libraries (*.dll)")
|
||||
filter = self.tr("Programs (*.exe)")
|
||||
dialog.setNameFilter(filter)
|
||||
else:
|
||||
# QtCore.QDir.Executable | QtCore.QDir.Files)
|
||||
@@ -373,26 +262,11 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
pass
|
||||
|
||||
dialog.setDirectory(os.path.dirname(str(control.text())))
|
||||
if name == "RAR":
|
||||
dialog.setWindowTitle("Find " + name + " program")
|
||||
else:
|
||||
dialog.setWindowTitle("Find " + name + " library")
|
||||
dialog.setWindowTitle("Find " + name + " program")
|
||||
|
||||
if dialog.exec_():
|
||||
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
@@ -1,106 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>702</width>
|
||||
<height>452</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Template Help</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="textBrowser">
|
||||
<property name="html">
|
||||
<string><html>
|
||||
<head/>
|
||||
<body>
|
||||
<h1 style="text-align: center">Template help</h1>
|
||||
<p>The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
|
||||
|
||||
<a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p>
|
||||
<pre>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})
|
||||
Spider-Geddon 1 (2018)
|
||||
|
||||
{series} #{issue} - {title}
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
</pre>
|
||||
</body>
|
||||
</html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -44,7 +44,7 @@
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="7" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="cbxSpecifySearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
@@ -129,16 +129,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="cbxAutoImprint">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Checks the publisher against a list of imprints.</p></body></html></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">
|
||||
@@ -155,7 +145,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLineEdit" name="leSearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
@@ -165,7 +155,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
|
||||
@@ -37,9 +37,6 @@
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -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 PyQt5 import QtGui
|
||||
|
||||
try:
|
||||
from PyQt4 import QtGui
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
@@ -55,13 +55,16 @@ 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:
|
||||
import io
|
||||
|
||||
from PIL import Image, WebPImagePlugin
|
||||
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
import StringIO
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
@@ -75,13 +78,14 @@ 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(io.StringIO(image_data))
|
||||
output = io.StringIO()
|
||||
im.save(output, format="PNG")
|
||||
success = img.loadFromData(output.getvalue())
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
output = StringIO.StringIO()
|
||||
im.save(output, format="TIFF")
|
||||
img.loadFromData(output.getvalue())
|
||||
success = True
|
||||
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
|
||||
|
||||
@@ -116,6 +116,118 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="grpBoxRar">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="lblRarHelp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>In order to read/write to CBR/RAR archives, you will need to have the shareware tools from <a href="www.win-rar.com/download.html"><span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a> installed. </p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>RAR program</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRarExePath">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btnBrowseRar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="lblUnrar">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>UnRAR program</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="leUnrarExePath">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QPushButton" name="btnBrowseUnrar">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
@@ -417,7 +529,7 @@
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>251</width>
|
||||
<height>199</height>
|
||||
<height>192</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
@@ -503,7 +615,7 @@
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="lblTemplate">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Template:</string>
|
||||
</property>
|
||||
@@ -512,78 +624,31 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRenameTemplate">
|
||||
<property name="toolTip">
|
||||
<string><pre>The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
|
||||
Accepts the following variables:
|
||||
{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}
|
||||
</pre></string>
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
|
||||
%genre%<br/>
|
||||
%language_code%<br/>
|
||||
%criticalrating%<br/>
|
||||
%alternateseries%<br/>
|
||||
%alternatenumber%<br/>
|
||||
%alternatecount%<br/>
|
||||
%imprint%<br/>
|
||||
%format%<br/>
|
||||
%maturityrating%<br/>
|
||||
%storyarc%<br/>
|
||||
%seriesgroup%<br/>
|
||||
%scaninfo%
|
||||
</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="btnTemplateHelp">
|
||||
<property name="text">
|
||||
<string>Template Help</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="lblPadding">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Issue # Zero Padding</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="leIssueNumPadding">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@@ -602,7 +667,7 @@ Examples:
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSmartCleanup">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html></string>
|
||||
@@ -612,203 +677,17 @@ Examples:
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxChangeExtension">
|
||||
<property name="text">
|
||||
<string>Change Extension Based On Archive Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="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><html><head/><body><p>In order to read CBR/RAR archives, you will need to have the unrar library from <a href="www.win-rar.com/download.html"><span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a> installed. </p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<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><html><head/><body><p>In order to write to CBR/RAR archives, you will need to have the shareware tools from <a href="www.win-rar.com/download.html"><span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a> installed. </p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -512,26 +512,6 @@
|
||||
</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><html><head/><body><p>Checks the publisher against a list of imprints.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Auto Imprint</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@@ -1196,10 +1176,8 @@
|
||||
<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">
|
||||
@@ -1395,22 +1373,6 @@
|
||||
<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/>
|
||||
|
||||
@@ -14,57 +14,61 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import platform
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
from . import _version
|
||||
import platform
|
||||
import urllib2
|
||||
#import os
|
||||
#import urllib
|
||||
|
||||
try:
|
||||
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject:
|
||||
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":
|
||||
params["platform"] = "win"
|
||||
plat = "win"
|
||||
elif platform.system() == "Linux":
|
||||
params["platform"] = "lin"
|
||||
plat = "lin"
|
||||
elif platform.system() == "Darwin":
|
||||
params["platform"] = "mac"
|
||||
plat = "mac"
|
||||
else:
|
||||
params["platform"] = "other"
|
||||
plat = "other"
|
||||
args = "?uuid={0}&platform={1}&version={2}".format(
|
||||
uuid, plat, ctversion.version)
|
||||
if not getattr(sys, 'frozen', None):
|
||||
args += "&src=T"
|
||||
|
||||
if not getattr(sys, "frozen", None):
|
||||
params["src"] = "T"
|
||||
|
||||
return (base_url, params)
|
||||
return base_url + args
|
||||
|
||||
def getLatestVersion(self, uuid, use_stats=True):
|
||||
|
||||
try:
|
||||
url, params = self.getRequestUrl(uuid, use_stats)
|
||||
new_version = requests.get(url, params=params).text
|
||||
resp = urllib2.urlopen(self.getRequestUrl(uuid, use_stats))
|
||||
new_version = resp.read()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
@@ -75,14 +79,15 @@ class VersionChecker(QObject):
|
||||
versionRequestComplete = pyqtSignal(str)
|
||||
|
||||
def asyncGetLatestVersion(self, uuid, use_stats):
|
||||
url, params = self.getRequestUrl(uuid, use_stats)
|
||||
|
||||
url = self.getRequestUrl(uuid, use_stats)
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.asyncGetLatestVersionComplete)
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url + "?" + urllib.parse.urlencode(params)))))
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url))))
|
||||
|
||||
def asyncGetLatestVersionComplete(self, reply):
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
if (reply.error() != QNetworkReply.NoError):
|
||||
return
|
||||
|
||||
# read in the response
|
||||
|
||||
@@ -14,19 +14,26 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt5.QtCore import QUrl, pyqtSignal
|
||||
#import sys
|
||||
#import time
|
||||
#import os
|
||||
|
||||
from comictaggerlib.ui.qtutils import centerWindowOnParent, reduceWidgetFontSize
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal
|
||||
#from PyQt4.QtCore import QObject
|
||||
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
|
||||
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
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from issueselectionwindow import IssueSelectionWindow
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from progresswindow import IDProgressWindow
|
||||
from settings import ComicTaggerSettings
|
||||
from matchselectionwindow import MatchSelectionWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
|
||||
class SearchThread(QtCore.QThread):
|
||||
@@ -34,21 +41,20 @@ class SearchThread(QtCore.QThread):
|
||||
searchComplete = pyqtSignal()
|
||||
progressUpdate = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, series_name, refresh, literal=False):
|
||||
def __init__(self, series_name, refresh):
|
||||
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
|
||||
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)
|
||||
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
|
||||
@@ -84,26 +90,29 @@ class IdentifyThread(QtCore.QThread):
|
||||
self.identifyComplete.emit()
|
||||
|
||||
|
||||
class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self, parent, series_name, issue_number, year, issue_count, cover_index_list, comic_archive, settings, autoselect=False, literal=False
|
||||
):
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, series_name, issue_number, year, issue_count,
|
||||
cover_index_list, comic_archive, settings, autoselect=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 = QtWidgets.QGridLayout(self.imageContainer)
|
||||
self.imageWidget = CoverImageWidget(
|
||||
self.imageContainer, CoverImageWidget.URLMode)
|
||||
gridlayout = QtGui.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
|
||||
@@ -113,7 +122,6 @@ class VolumeSelectionWindow(QtWidgets.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)
|
||||
@@ -127,7 +135,8 @@ class VolumeSelectionWindow(QtWidgets.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
|
||||
@@ -135,20 +144,24 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.btnRequery.setEnabled(enabled)
|
||||
self.btnIssues.setEnabled(enabled)
|
||||
self.btnAutoSelect.setEnabled(enabled)
|
||||
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(enabled)
|
||||
self.buttonBox.button(QtGui.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:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select", "You need to load a comic first!")
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Auto-Select", "You need to load a comic first!")
|
||||
return
|
||||
|
||||
if self.issue_number is None or self.issue_number == "":
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select", "Can't auto-select without an issue number (yet!)")
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select",
|
||||
"Can't auto-select without an issue number (yet!)")
|
||||
return
|
||||
|
||||
self.iddialog = IDProgressWindow(self)
|
||||
@@ -179,7 +192,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.iddialog.exec_()
|
||||
|
||||
def logIDOutput(self, text):
|
||||
print(str(text), end=" ")
|
||||
print unicode(text),
|
||||
self.iddialog.textEdit.ensureCursorVisible()
|
||||
self.iddialog.textEdit.insertPlainText(text)
|
||||
|
||||
@@ -199,22 +212,33 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
found_match = None
|
||||
choices = False
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " No matches found :-(")
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!"
|
||||
)
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " Found a match, but not with the first page of the archive.")
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found a match, but not with the first page of the archive.")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually.")
|
||||
QtGui.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:
|
||||
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " Found multiple likely matches. Please select.")
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
@@ -228,18 +252,19 @@ class VolumeSelectionWindow(QtWidgets.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 += " (" + str(record["start_year"]) + ")"
|
||||
if record['id'] == self.volume_id:
|
||||
title = record['name']
|
||||
title += " (" + unicode(record['start_year']) + ")"
|
||||
title += " - "
|
||||
break
|
||||
|
||||
@@ -254,23 +279,26 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
def selectByID(self):
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
volume_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
|
||||
if volume_id == self.volume_id:
|
||||
volume_id, b = self.twList.item(
|
||||
r, 0).data(QtCore.Qt.UserRole).toInt()
|
||||
if (volume_id == self.volume_id):
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def performQuery(self, refresh=False):
|
||||
|
||||
self.progdialog = QtWidgets.QProgressDialog("Searching Online", "Cancel", 0, 100, self)
|
||||
self.progdialog = QtGui.QProgressDialog(
|
||||
"Searching Online", "Cancel", 0, 100, self)
|
||||
self.progdialog.setWindowTitle("Online Search")
|
||||
self.progdialog.canceled.connect(self.searchCanceled)
|
||||
self.progdialog.setModal(True)
|
||||
self.progdialog.setMinimumDuration(300)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
self.search_thread = SearchThread(self.series_name, refresh, self.literal)
|
||||
|
||||
self.search_thread = SearchThread(self.series_name, refresh)
|
||||
self.search_thread.searchComplete.connect(self.searchComplete)
|
||||
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
|
||||
self.search_thread.start()
|
||||
|
||||
# QtCore.QCoreApplication.processEvents()
|
||||
self.progdialog.exec_()
|
||||
|
||||
def searchCanceled(self):
|
||||
@@ -287,17 +315,21 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
def searchProgressUpdate(self, current, total):
|
||||
self.progdialog.setMaximum(total)
|
||||
self.progdialog.setValue(current + 1)
|
||||
self.progdialog.setValue(current)
|
||||
|
||||
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:
|
||||
QtWidgets.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage())
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Comic Vine Error"),
|
||||
ComicVineTalker.getRateLimitMessage())
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to search for series!"))
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Network Issue"),
|
||||
self.tr("Could not connect to Comic Vine to search for series!"))
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
@@ -312,31 +344,32 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
for record in self.cv_search_results:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record["name"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = record['name']
|
||||
item = QtGui.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 = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = str(record['start_year'])
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record["count_of_issues"]
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item_text = record['count_of_issues']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.DisplayRole, record["count_of_issues"])
|
||||
item.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 = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
@@ -349,7 +382,8 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
if len(self.cv_search_results) == 0:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtWidgets.QMessageBox.information(self, "Search Result", "No matches found!")
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Search Result", "No matches found!")
|
||||
|
||||
if self.immediate_autoselect and len(self.cv_search_results) > 0:
|
||||
# defer the immediate autoselect so this dialog has time to pop up
|
||||
@@ -370,14 +404,15 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
|
||||
self.volume_id, b = self.twList.item(
|
||||
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
|
||||
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record["id"] == self.volume_id:
|
||||
if record["description"] is None:
|
||||
if record['id'] == self.volume_id:
|
||||
if record['description'] is None:
|
||||
self.teDetails.setText("")
|
||||
else:
|
||||
self.teDetails.setText(record["description"])
|
||||
self.imageWidget.setURL(record["image"]["super_url"])
|
||||
self.teDetails.setText(record['description'])
|
||||
self.imageWidget.setURL(record['image']['super_url'])
|
||||
break
|
||||
|
||||
1
current_version.txt
Normal file
1
current_version.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.1.16-beta-rc
|
||||
@@ -1,11 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Encoding=UTF-8
|
||||
Name=ComicTagger
|
||||
GenericName=Comic Metadata Editor
|
||||
Comment=A cross-platform GUI/CLI app for writing metadata to comic archives
|
||||
Exec=%%CTSCRIPT%% %F
|
||||
Icon=/usr/local/share/comictagger/app.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=text/plain;
|
||||
Categories=Application;
|
||||
@@ -1,4 +0,0 @@
|
||||
This file is a placeholder that will automaticlly be replaced with a symlink to
|
||||
the local machine's Python framework python binary.
|
||||
|
||||
When pip does an uninstall, it will remove the link.
|
||||
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>main.sh</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>app.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.comictagger.comictagger</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ComicTagger</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>%%CTVERSION%%</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>%%CTVERSION%%</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This is a lot of hoop-jumping to get the absolute path
|
||||
# of this script, so that we can use the Symlinked python
|
||||
# binary to call the CT script. This is all so that the
|
||||
# Mac menu doesn't say "Python".
|
||||
|
||||
realpath()
|
||||
{
|
||||
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
|
||||
}
|
||||
|
||||
CTSCRIPT=%%CTSCRIPT%%
|
||||
|
||||
THIS=$(realpath $0)
|
||||
THIS_FOLDER=$(dirname $THIS)
|
||||
"$THIS_FOLDER/ComicTagger" "$CTSCRIPT"
|
||||
@@ -1,4 +0,0 @@
|
||||
This file is a placeholder that will automatically be replaced with a Windows
|
||||
shortcut on the user's desktop.
|
||||
|
||||
When pip does an uninstall, it will remove the shortcut file.
|
||||
@@ -1,52 +0,0 @@
|
||||
# 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])]),
|
||||
],
|
||||
)
|
||||
11
google/gadgets/social.xml
Normal file
11
google/gadgets/social.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&width=292&colorscheme=light&show_faces =false&border_color&stream=false&header=false&height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|
||||
10
google/gadgets/twitter.xml
Normal file
10
google/gadgets/twitter.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|
||||
260
google/googlecode_upload.py
Executable file
260
google/googlecode_upload.py
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
|
||||
# Author: danderson@google.com (David Anderson)
|
||||
#
|
||||
# Script for uploading files to a Google Code project.
|
||||
#
|
||||
# This is intended to be both a useful script for people who want to
|
||||
# streamline project uploads and a reference implementation for
|
||||
# uploading files to Google Code projects.
|
||||
#
|
||||
# To upload a file to Google Code, you need to provide a path to the
|
||||
# file on your local machine, a small summary of what the file is, a
|
||||
# project name, and a valid account that is a member or owner of that
|
||||
# project. You can optionally provide a list of labels that apply to
|
||||
# the file. The file will be uploaded under the same name that it has
|
||||
# in your local filesystem (that is, the "basename" or last path
|
||||
# component). Run the script with '--help' to get the exact syntax
|
||||
# and available options.
|
||||
#
|
||||
# Note that the upload script requests that you enter your
|
||||
# googlecode.com password. This is NOT your Gmail account password!
|
||||
# This is the password you use on googlecode.com for committing to
|
||||
# Subversion and uploading files. You can find your password by going
|
||||
# to http://code.google.com/hosting/settings when logged in with your
|
||||
# Gmail account. If you have already committed to your project's
|
||||
# Subversion repository, the script will automatically retrieve your
|
||||
# credentials from there (unless disabled, see the output of '--help'
|
||||
# for details).
|
||||
#
|
||||
# If you are looking at this script as a reference for implementing
|
||||
# your own Google Code file uploader, then you should take a look at
|
||||
# the upload() function, which is the meat of the uploader. You
|
||||
# basically need to build a multipart/form-data POST request with the
|
||||
# right fields and send it to https://PROJECT.googlecode.com/files .
|
||||
# Authenticate the request using HTTP Basic authentication, as is
|
||||
# shown below.
|
||||
#
|
||||
# Licensed under the terms of the Apache Software License 2.0:
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Questions, comments, feature requests and patches are most welcome.
|
||||
# Please direct all of these to the Google Code users group:
|
||||
# http://groups.google.com/group/google-code-hosting
|
||||
|
||||
"""Google Code file uploader script.
|
||||
"""
|
||||
|
||||
__author__ = 'danderson@google.com (David Anderson)'
|
||||
|
||||
import httplib
|
||||
import os.path
|
||||
import optparse
|
||||
import getpass
|
||||
import base64
|
||||
import sys
|
||||
|
||||
|
||||
def upload(file, project_name, user_name, password, summary, labels=None):
|
||||
"""Upload a file to a Google Code project's file server.
|
||||
|
||||
Args:
|
||||
file: The local path to the file.
|
||||
project_name: The name of your project on Google Code.
|
||||
user_name: Your Google account name.
|
||||
password: The googlecode.com password for your account.
|
||||
Note that this is NOT your global Google Account password!
|
||||
summary: A small description for the file.
|
||||
labels: an optional list of label strings with which to tag the file.
|
||||
|
||||
Returns: a tuple:
|
||||
http_status: 201 if the upload succeeded, something else if an
|
||||
error occured.
|
||||
http_reason: The human-readable string associated with http_status
|
||||
file_url: If the upload succeeded, the URL of the file on Google
|
||||
Code, None otherwise.
|
||||
"""
|
||||
# The login is the user part of user@gmail.com. If the login provided
|
||||
# is in the full user@domain form, strip it down.
|
||||
if user_name.endswith('@gmail.com'):
|
||||
user_name = user_name[:user_name.index('@gmail.com')]
|
||||
|
||||
form_fields = [('summary', summary)]
|
||||
if labels is not None:
|
||||
form_fields.extend([('label', l.strip()) for l in labels])
|
||||
|
||||
content_type, body = encode_upload_request(form_fields, file)
|
||||
|
||||
upload_host = '%s.googlecode.com' % project_name
|
||||
upload_uri = '/files'
|
||||
auth_token = base64.b64encode('%s:%s' % (user_name, password))
|
||||
headers = {
|
||||
'Authorization': 'Basic %s' % auth_token,
|
||||
'User-Agent': 'Googlecode.com uploader v0.9.4',
|
||||
'Content-Type': content_type,
|
||||
}
|
||||
|
||||
server = httplib.HTTPSConnection(upload_host)
|
||||
server.request('POST', upload_uri, body, headers)
|
||||
resp = server.getresponse()
|
||||
server.close()
|
||||
|
||||
if resp.status == 201:
|
||||
location = resp.getheader('Location', None)
|
||||
else:
|
||||
location = None
|
||||
return resp.status, resp.reason, location
|
||||
|
||||
|
||||
def encode_upload_request(fields, file_path):
|
||||
"""Encode the given fields and file into a multipart form body.
|
||||
|
||||
fields is a sequence of (name, value) pairs. file is the path of
|
||||
the file to upload. The file will be uploaded to Google Code with
|
||||
the same file name.
|
||||
|
||||
Returns: (content_type, body) ready for httplib.HTTP instance
|
||||
"""
|
||||
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
|
||||
CRLF = '\r\n'
|
||||
|
||||
body = []
|
||||
|
||||
# Add the metadata about the upload first
|
||||
for key, value in fields:
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
'',
|
||||
value,
|
||||
])
|
||||
|
||||
# Now add the file itself
|
||||
file_name = os.path.basename(file_path)
|
||||
f = open(file_path, 'rb')
|
||||
file_content = f.read()
|
||||
f.close()
|
||||
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="filename"; filename="%s"'
|
||||
% file_name,
|
||||
# The upload server determines the mime-type, no need to set it.
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
file_content,
|
||||
])
|
||||
|
||||
# Finalize the form body
|
||||
body.extend(['--' + BOUNDARY + '--', ''])
|
||||
|
||||
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
|
||||
|
||||
|
||||
def upload_find_auth(file_path, project_name, summary, labels=None,
|
||||
user_name=None, password=None, tries=3):
|
||||
"""Find credentials and upload a file to a Google Code project's file server.
|
||||
|
||||
file_path, project_name, summary, and labels are passed as-is to upload.
|
||||
|
||||
Args:
|
||||
file_path: The local path to the file.
|
||||
project_name: The name of your project on Google Code.
|
||||
summary: A small description for the file.
|
||||
labels: an optional list of label strings with which to tag the file.
|
||||
config_dir: Path to Subversion configuration directory, 'none', or None.
|
||||
user_name: Your Google account name.
|
||||
tries: How many attempts to make.
|
||||
"""
|
||||
if user_name is None or password is None:
|
||||
from netrc import netrc
|
||||
authenticators = netrc().authenticators("code.google.com")
|
||||
if authenticators:
|
||||
if user_name is None:
|
||||
user_name = authenticators[0]
|
||||
if password is None:
|
||||
password = authenticators[2]
|
||||
|
||||
while tries > 0:
|
||||
if user_name is None:
|
||||
# Read username if not specified or loaded from svn config, or on
|
||||
# subsequent tries.
|
||||
sys.stdout.write('Please enter your googlecode.com username: ')
|
||||
sys.stdout.flush()
|
||||
user_name = sys.stdin.readline().rstrip()
|
||||
if password is None:
|
||||
# Read password if not loaded from svn config, or on subsequent
|
||||
# tries.
|
||||
print 'Please enter your googlecode.com password.'
|
||||
print '** Note that this is NOT your Gmail account password! **'
|
||||
print 'It is the password you use to access Subversion repositories,'
|
||||
print 'and can be found here: http://code.google.com/hosting/settings'
|
||||
password = getpass.getpass()
|
||||
|
||||
status, reason, url = upload(
|
||||
file_path, project_name, user_name, password, summary, labels)
|
||||
# Returns 403 Forbidden instead of 401 Unauthorized for bad
|
||||
# credentials as of 2007-07-17.
|
||||
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
|
||||
# Rest for another try.
|
||||
user_name = password = None
|
||||
tries = tries - 1
|
||||
else:
|
||||
# We're done.
|
||||
break
|
||||
|
||||
return status, reason, url
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
|
||||
'-p PROJECT [options] FILE')
|
||||
parser.add_option('-s', '--summary', dest='summary',
|
||||
help='Short description of the file')
|
||||
parser.add_option('-p', '--project', dest='project',
|
||||
help='Google Code project name')
|
||||
parser.add_option('-u', '--user', dest='user',
|
||||
help='Your Google Code username')
|
||||
parser.add_option('-w', '--password', dest='password',
|
||||
help='Your Google Code password')
|
||||
parser.add_option(
|
||||
'-l',
|
||||
'--labels',
|
||||
dest='labels',
|
||||
help='An optional list of comma-separated labels to attach '
|
||||
'to the file')
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if not options.summary:
|
||||
parser.error('File summary is missing.')
|
||||
elif not options.project:
|
||||
parser.error('Project name is missing.')
|
||||
elif len(args) < 1:
|
||||
parser.error('File to upload not provided.')
|
||||
elif len(args) > 1:
|
||||
parser.error('Only one file may be specified.')
|
||||
|
||||
file_path = args[0]
|
||||
|
||||
if options.labels:
|
||||
labels = options.labels.split(',')
|
||||
else:
|
||||
labels = None
|
||||
|
||||
status, reason, url = upload_find_auth(file_path, options.project,
|
||||
options.summary, labels,
|
||||
options.user, options.password)
|
||||
if url:
|
||||
print 'The file was uploaded successfully.'
|
||||
print 'URL: %s' % url
|
||||
return 0
|
||||
else:
|
||||
print 'An error occurred. Your file was not uploaded.'
|
||||
print 'Google Code upload server said: %s (%s)' % (reason, status)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -1,14 +0,0 @@
|
||||
[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 +0,0 @@
|
||||
comicapi[CBR] @ git+https://github.com/lordwelch/comicapi
|
||||
@@ -1 +0,0 @@
|
||||
pyqt5
|
||||
@@ -1 +0,0 @@
|
||||
filetype
|
||||
@@ -1,7 +1,5 @@
|
||||
beautifulsoup4 >= 4.1
|
||||
configparser
|
||||
natsort
|
||||
pathvalidate
|
||||
pillow>=4.3.0
|
||||
requests
|
||||
comicapi @ git+https://github.com/lordwelch/comicapi
|
||||
beautifulsoup4 >= 4.1
|
||||
unrar==0.3
|
||||
natsort==3.5.2
|
||||
PyPDF2==1.24
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
pyinstaller
|
||||
setuptools>=42
|
||||
setuptools_scm[toml]>=3.4
|
||||
wheel
|
||||
158
scripts/dupe.ui
158
scripts/dupe.ui
@@ -1,158 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,687 +1,84 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python
|
||||
"""Find all duplicate comics"""
|
||||
|
||||
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
|
||||
#import sys
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
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()
|
||||
#from comictaggerlib.issuestring import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
def main():
|
||||
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()
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
global app
|
||||
workdir = args.w
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
file_list = utils.get_recursive_filelist(args.paths)
|
||||
|
||||
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.
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
|
||||
return
|
||||
|
||||
window = MainWindow(file_list, style, workdir)
|
||||
window.show()
|
||||
app.exec()
|
||||
shutil.rmtree(workdir, True)
|
||||
filelist = utils.get_recursive_filelist(sys.argv[1:])
|
||||
|
||||
# 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)))
|
||||
|
||||
def sigint_handler(*args):
|
||||
"""Handler for the SIGINT signal."""
|
||||
sys.stderr.write('\r')
|
||||
QtWidgets.QApplication.quit()
|
||||
print >> sys.stderr, fmt_str.format("") + "\r",
|
||||
print "--------------------------------------------------------------------------"
|
||||
print "Found {0} comics with {1} tags".format(len(comic_list), MetaDataStyle.name[style])
|
||||
print "--------------------------------------------------------------------------"
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
# 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.issuestring import *
|
||||
from comictaggerlib.settings import *
|
||||
|
||||
# import comictaggerlib.utils
|
||||
from comictaggerlib.issuestring import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
def main():
|
||||
@@ -70,14 +69,15 @@ 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()
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<?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>
|
||||
@@ -18,15 +18,14 @@ 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):
|
||||
@@ -53,7 +52,8 @@ 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,7 +93,8 @@ 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
|
||||
@@ -101,10 +102,11 @@ 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()
|
||||
|
||||
@@ -18,18 +18,14 @@
|
||||
# limitations under the License.
|
||||
|
||||
import shutil
|
||||
|
||||
from comicapi.comicarchive import *
|
||||
#import sys
|
||||
#import os
|
||||
#import platform
|
||||
|
||||
from comictaggerlib.settings import *
|
||||
|
||||
# import sys
|
||||
# import os
|
||||
# import platform
|
||||
|
||||
|
||||
# from comicapi.issuestring import *
|
||||
# import comicapi.utils
|
||||
from comicapi.comicarchive import *
|
||||
#from comicapi.issuestring import *
|
||||
#import comicapi.utils
|
||||
|
||||
|
||||
def make_folder(folder):
|
||||
@@ -56,7 +52,8 @@ 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]
|
||||
@@ -81,7 +78,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)))
|
||||
@@ -109,10 +106,13 @@ 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()
|
||||
|
||||
@@ -17,27 +17,34 @@
|
||||
|
||||
import argparse
|
||||
import json
|
||||
#import sys
|
||||
#import os
|
||||
#import re
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.filerenamer import *
|
||||
from comictaggerlib.settings import *
|
||||
|
||||
# import sys
|
||||
# import os
|
||||
# import re
|
||||
|
||||
|
||||
# import comictaggerlib.utils
|
||||
from comictaggerlib.filerenamer import *
|
||||
#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
|
||||
@@ -53,7 +60,8 @@ 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)
|
||||
|
||||
@@ -88,11 +96,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)
|
||||
|
||||
@@ -145,12 +153,11 @@ 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()
|
||||
|
||||
@@ -19,67 +19,133 @@ 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 os
|
||||
import shutil
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
import shutil
|
||||
|
||||
import comictaggerlib.utils
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.comicarchive 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("Usage: {0} [comic_folder]".format(sys.argv[0]), file=sys.stderr)
|
||||
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
|
||||
return
|
||||
|
||||
if sys.argv[1] == "-n":
|
||||
filelist = utils.get_recursive_filelist(sys.argv[2:])
|
||||
else:
|
||||
filelist = utils.get_recursive_filelist(sys.argv[1:])
|
||||
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,
|
||||
default_image_path="/home/timmy/build/source/comictagger-test/comictaggerlib/graphics/nocover.png",
|
||||
)
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
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. 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)
|
||||
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 mod:
|
||||
for num, p in enumerate(pgs):
|
||||
p["Image"] = str(num)
|
||||
md.pages = pgs
|
||||
ca.writeCIX(md)
|
||||
# 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 __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
# limitations under the License.
|
||||
|
||||
import shutil
|
||||
#import sys
|
||||
#import os
|
||||
#import tempfile
|
||||
#import zipfile
|
||||
|
||||
import Image
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
|
||||
# import comictaggerlib.utils
|
||||
from comictaggerlib.comicarchive import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
subfolder_name = "ORIGINALS"
|
||||
@@ -49,7 +52,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()):
|
||||
@@ -63,7 +66,8 @@ 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
|
||||
|
||||
@@ -95,7 +99,8 @@ 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):
|
||||
@@ -113,7 +118,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()
|
||||
@@ -128,7 +133,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)
|
||||
@@ -146,7 +151,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
|
||||
@@ -181,5 +186,5 @@ def main():
|
||||
ca.writeCIX(cix_md)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -16,15 +16,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.comicvinetalker import *
|
||||
from comictaggerlib.issueidentifier import *
|
||||
from comictaggerlib.settings import *
|
||||
|
||||
# import comictaggerlib.utils
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.issueidentifier import *
|
||||
from comictaggerlib.comicvinetalker import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
def main():
|
||||
@@ -33,7 +32,8 @@ 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,7 +45,8 @@ 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)
|
||||
@@ -58,15 +59,16 @@ 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
135
setup.py
@@ -1,86 +1,45 @@
|
||||
# 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
|
||||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup
|
||||
import comictaggerlib.ctversion
|
||||
|
||||
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()
|
||||
with open('requirements.txt') as f:
|
||||
required = f.read().splitlines()
|
||||
|
||||
setup(name="comictagger",
|
||||
install_requires=required,
|
||||
version=comictaggerlib.ctversion.version,
|
||||
description="A cross-platform GUI/CLI app for writing metadata to comic archives",
|
||||
author="Anthony Beville",
|
||||
author_email="comictagger@gmail.com",
|
||||
url="http://code.google.com/p/comictagger/",
|
||||
download_url="https://pypi.python.org/packages/source/c/comictagger/comictagger-{0}.zip".format(comictaggerlib.ctversion.version),
|
||||
packages=["comictaggerlib", "comicapi", "comicapi/UnRAR2"],
|
||||
package_data={
|
||||
'comictaggerlib': ['ui/*.ui', 'graphics/*'],
|
||||
'comicapi/UnRAR2': ['UnRARDLL/*.*', 'UnRARDLL/x64/*.*'],
|
||||
},
|
||||
scripts=["comictagger.py"],
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Environment :: MacOS X",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Other/Nonlisted Topic",
|
||||
"Topic :: Multimedia :: Graphics"
|
||||
],
|
||||
license="Apache License 2.0",
|
||||
|
||||
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="""
|
||||
long_description="""
|
||||
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
|
||||
|
||||
Features:
|
||||
@@ -89,9 +48,19 @@ 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).
|
||||
* Reads and writes rar, zip and tar archives (external tools needed for writing RAR)
|
||||
* 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.
|
||||
* Can run without PyQt5 installed
|
||||
""",
|
||||
)
|
||||
|
||||
Requires:
|
||||
|
||||
* python 2.6 or 2.7
|
||||
* configparser
|
||||
* python imaging (PIL) >= 1.1.6
|
||||
* beautifulsoup > 4.1
|
||||
|
||||
Optional requirement (for GUI):
|
||||
|
||||
* pyqt4
|
||||
"""
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user