Compare commits
165 Commits
1.1.3-beta
...
1.1.16-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43a913294e | ||
|
|
70e28c7863 | ||
|
|
14713d8ad0 | ||
|
|
4ff2061568 | ||
|
|
08c402149b | ||
|
|
184dbf0684 | ||
|
|
ed0050ba05 | ||
|
|
68030a1024 | ||
|
|
983ad1fcf4 | ||
|
|
d959ac0401 | ||
|
|
2a550db02a | ||
|
|
6369fa5fda | ||
|
|
d5a13a4206 | ||
|
|
b2532ce03a | ||
|
|
79a67d8c29 | ||
|
|
d9bd38674c | ||
|
|
a0154aaaae | ||
|
|
17f74cf296 | ||
|
|
3f112cd578 | ||
|
|
f6439049d8 | ||
|
|
2fe818872c | ||
|
|
a419969b85 | ||
|
|
ee52448f17 | ||
|
|
79103990fa | ||
|
|
22dbafbc00 | ||
|
|
0df283778c | ||
|
|
a6282b5449 | ||
|
|
5574280ad6 | ||
|
|
19b907b742 | ||
|
|
a9ff8f37b0 | ||
|
|
0769111f8c | ||
|
|
cf6ae8b5ae | ||
|
|
1d6846ced3 | ||
|
|
d516d80093 | ||
|
|
bf9ab71fd9 | ||
|
|
33b00ad323 | ||
|
|
301ff084f1 | ||
|
|
0c146bb245 | ||
|
|
08cc4a1acb | ||
|
|
f97a1653d9 | ||
|
|
d9dbab301a | ||
|
|
3d93197101 | ||
|
|
752a1d8923 | ||
|
|
68002daffa | ||
|
|
ad5062c582 | ||
|
|
2680468f34 | ||
|
|
6156fc296a | ||
|
|
0feed294d4 | ||
|
|
e57736b955 | ||
|
|
70fcdc0129 | ||
|
|
9a64195ebd | ||
|
|
b0f229f851 | ||
|
|
877a5ccd85 | ||
|
|
c0f2e2f771 | ||
|
|
0adfc9beb3 | ||
|
|
d0bc41d7ee | ||
|
|
fa46a065a4 | ||
|
|
8fcd5ba7d6 | ||
|
|
759cdc6b40 | ||
|
|
1405d9ff0e | ||
|
|
d8fcbbad0a | ||
|
|
3eca25db34 | ||
|
|
c8a5a89369 | ||
|
|
ff578ea819 | ||
|
|
1c730c25d5 | ||
|
|
35b7b39b86 | ||
|
|
719c711484 | ||
|
|
afbbc9d00c | ||
|
|
b8e0a45fc8 | ||
|
|
b7360dd33e | ||
|
|
d9f1956426 | ||
|
|
b5c7f36410 | ||
|
|
0b0663d935 | ||
|
|
eee1f65436 | ||
|
|
9a8d4149f2 | ||
|
|
b02a205668 | ||
|
|
57284dfbed | ||
|
|
afcbde7fc6 | ||
|
|
151fac5bf1 | ||
|
|
57c1efdab9 | ||
|
|
6b272cef87 | ||
|
|
1cdc732739 | ||
|
|
d1b00d162d | ||
|
|
3dd3980bc1 | ||
|
|
cbf475eb26 | ||
|
|
ac8b575659 | ||
|
|
ac8ef286a4 | ||
|
|
f567dc37be | ||
|
|
15c5fc5258 | ||
|
|
cc985b52a5 | ||
|
|
910b0386be | ||
|
|
0fece23405 | ||
|
|
eee320e0c7 | ||
|
|
accabf8e21 | ||
|
|
acc253d35c | ||
|
|
ede0154efe | ||
|
|
5b805b1428 | ||
|
|
2e6b2a89db | ||
|
|
c028bb4ddc | ||
|
|
b70beb5684 | ||
|
|
128af4521b | ||
|
|
43cf7a80c8 | ||
|
|
3223ed190c | ||
|
|
9e2817c037 | ||
|
|
6e7bd10fb9 | ||
|
|
c099205779 | ||
|
|
47d8da0e80 | ||
|
|
0f7e88e58c | ||
|
|
65902a15b1 | ||
|
|
a68b2babeb | ||
|
|
4098802e43 | ||
|
|
9c14258e9f | ||
|
|
33bdbe8be8 | ||
|
|
a76864c109 | ||
|
|
cb68d07751 | ||
|
|
8e9fccdbbc | ||
|
|
39990fc2b4 | ||
|
|
e8c315d834 | ||
|
|
f8a06a8746 | ||
|
|
9415087da7 | ||
|
|
9aee5c32eb | ||
|
|
fcdb4a3889 | ||
|
|
534a326258 | ||
|
|
0390ff5919 | ||
|
|
b800ae1751 | ||
|
|
a2c17982d3 | ||
|
|
0347befae6 | ||
|
|
af54b79790 | ||
|
|
dd04ae98a0 | ||
|
|
31b76fba92 | ||
|
|
9f4a4b0eb0 | ||
|
|
575a23c6bf | ||
|
|
5d84f09359 | ||
|
|
3072583482 | ||
|
|
8d867cf78a | ||
|
|
36c79b5a2a | ||
|
|
dfdaf731b4 | ||
|
|
67bff8586c | ||
|
|
9e4cbea6e4 | ||
|
|
d150b2ce54 | ||
|
|
a20949cc4d | ||
|
|
e3fceb20a2 | ||
|
|
f4e00d9ef3 | ||
|
|
1980bd5988 | ||
|
|
db54affc74 | ||
|
|
0edb9444ef | ||
|
|
b22c25f53f | ||
|
|
76e6666a79 | ||
|
|
a804a10e0e | ||
|
|
fe413b12c1 | ||
|
|
e38dc2f063 | ||
|
|
5e5418090b | ||
|
|
56c1f8582a | ||
|
|
00f8c0a280 | ||
|
|
1d915eb155 | ||
|
|
b7b8060ef2 | ||
|
|
2d190b076a | ||
|
|
cd92b1afea | ||
|
|
4d21a001d6 | ||
|
|
4af59d2315 | ||
|
|
c9c98b6c11 | ||
|
|
1ff43db2ce | ||
|
|
822f6b4729 | ||
|
|
44a8dc6815 | ||
|
|
a35576895c |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/.idea/
|
||||
/nbproject/
|
||||
*.pyc
|
||||
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
||||
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
|
||||
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)
|
||||
@@ -49,10 +49,11 @@ remove_test_install:
|
||||
# #-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"
|
||||
#$(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 \
|
||||
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
This is a fork derived from google code:
|
||||
|
||||
https://code.google.com/p/comictagger/
|
||||
|
||||
|
||||
Changes in this fork:
|
||||
- using different unrar library https://pypi.python.org/pypi/unrar/. The previous one used unrar.dll on windows and
|
||||
hackish wrapping of unrar command on linux, while this new one should use unrarlib on both platforms. From my tests
|
||||
it is more stable and faster. *Requires unrarlib availability, check unrar module documentation for more
|
||||
information*.
|
||||
- extracted core libraries in its own package comicapi, shared in a new repository using git subtree for better
|
||||
alignment with comicstreamer
|
||||
- support for *day of month* field in the GUI
|
||||
- merge of changes from fcanc fork
|
||||
|
||||
Todo:
|
||||
- more tests in non-linux platforms
|
||||
- repackage for simple user installation
|
||||
|
||||
Follows original readme:
|
||||
|
||||
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
|
||||
|
||||
Features:
|
||||
|
||||
* Runs on Mac OSX, Microsoft Windows, and Linux systems
|
||||
* Communicates with an online database (Comic Vine) for acquiring metadata
|
||||
* Uses image processing to automatically match a given archive with the correct issue data
|
||||
* Batch processing in the GUI for tagging hundreds or more comics at a time
|
||||
* Reads and writes multiple tagging schemes ( ComicBookLover and ComicRack, with more planned).
|
||||
* Reads and writes RAR and Zip archives (external tools needed for writing RAR)
|
||||
* Command line interface (CLI) on all platforms (including Windows), which supports batch operations, and which can be
|
||||
used in native scripts for complex operations. For example, to recursively scrape and tag all archives in a folder
|
||||
comictagger.py -R -s -o -f -t cr -v -i --nooverwrite /path/to/comics/
|
||||
|
||||
For details, screen-shots, release notes, and more, visit http://code.google.com/p/comictagger/
|
||||
|
||||
Requires:
|
||||
|
||||
* python 2.6 or 2.7
|
||||
* configparser
|
||||
* python imaging (PIL) >= 1.1.6
|
||||
* beautifulsoup > 4.1
|
||||
|
||||
Optional requirement (for GUI):
|
||||
|
||||
* pyqt4
|
||||
|
||||
Install and run:
|
||||
|
||||
* ComicTagger can be run directly from this directory, using the launcher script "comictagger.py"
|
||||
|
||||
* To install on your system use: "python setup.py install". Take note in the output where comictagger.py goes!
|
||||
31
README.txt
31
README.txt
@@ -1,31 +0,0 @@
|
||||
ComicTagger is a multi-platform app for writing metadata to comic archives, 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 recusrively scrape and tag all archives in a folder
|
||||
comictagger.py -R -s -o -f -t cr -v -i --nooverwrite /path/to/comics/
|
||||
|
||||
For details, screenshots, 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!
|
||||
@@ -33,7 +33,7 @@ similar to the C interface provided by UnRAR. There is also a
|
||||
higher level interface which makes some common operations easier.
|
||||
"""
|
||||
|
||||
__version__ = '0.99.2'
|
||||
__version__ = '0.99.3'
|
||||
|
||||
try:
|
||||
WindowsError
|
||||
@@ -12,13 +12,11 @@ def cleanup(dir='test'):
|
||||
os.removedirs(os.path.join(path, dir))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# reuse RarArchive object, en
|
||||
# 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">""")
|
||||
@@ -98,7 +96,8 @@ list(UnRAR2.RarFile('test_nulls.rar').infoiter())
|
||||
|
||||
# extract files from an archive with protected files
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="protected").extract()
|
||||
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
|
||||
@@ -33,6 +33,7 @@ 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"
|
||||
@@ -59,10 +60,10 @@ def call_unrar(params):
|
||||
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():
|
||||
@@ -73,18 +74,42 @@ class RarFileImplementation(object):
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
while not (line.startswith('UNRAR')):
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
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()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:]
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
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:
|
||||
self.comment = None
|
||||
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
|
||||
@@ -97,7 +122,8 @@ class RarFileImplementation(object):
|
||||
|
||||
def infoiter(self):
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
|
||||
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"):
|
||||
@@ -106,33 +132,48 @@ class RarFileImplementation(object):
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not line.startswith('--------------'):
|
||||
while not line.startswith('-----------'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
if line.find("CRC failed")>=0:
|
||||
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+")
|
||||
while not line.startswith('--------------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
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
|
||||
#!!!ATB - changed this because it was choking when a folder or file started with a space.
|
||||
#!!! now, just strip off the first char in the string
|
||||
data['filename'] = accum[0].rstrip()[1:]
|
||||
info = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(info[0])
|
||||
attr = info[5]
|
||||
data['filename'] = " ".join(fields[4:])
|
||||
data['size'] = int(fields[1])
|
||||
attr = fields[0]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
|
||||
data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
@@ -153,7 +194,7 @@ class RarFileImplementation(object):
|
||||
if overwrite:
|
||||
options.append('o+')
|
||||
else:
|
||||
options.append('o-')
|
||||
options.append('o-')
|
||||
if not path.endswith(os.sep):
|
||||
path += os.sep
|
||||
names = []
|
||||
@@ -167,7 +208,7 @@ class RarFileImplementation(object):
|
||||
names.append(path)
|
||||
proc = self.call(command, options, names)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if stderrdata.find("CRC failed")>=0:
|
||||
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
|
||||
raise IncorrectRARPassword
|
||||
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,23 @@
|
||||
#!/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"
|
||||
|
||||
print os.environ.get("UNRAR_LIB_PATH", None)
|
||||
print bundle_dir
|
||||
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
if __name__ == '__main__':
|
||||
ctmain()
|
||||
ctmain()
|
||||
|
||||
@@ -1,226 +1,245 @@
|
||||
"""
|
||||
A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
"""A PyQT4 dialog to select from automated issue matches"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#from comicvinetalker import ComicVineTalker
|
||||
#import utils
|
||||
|
||||
|
||||
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('autotagmatchwindow.ui' ), self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
gridlayout.addWidget( self.altCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
volume_id = 0
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
def __init__(self, parent, match_set_list, style, fetch_func):
|
||||
super(AutoTagMatchWindow, self).__init__(parent)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
|
||||
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Write Tags")
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
self.fetch_func = fetch_func
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.current_match_set_idx = 0
|
||||
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.skipButton.clicked.connect(self.skipToNext)
|
||||
|
||||
self.updateData()
|
||||
reduceWidgetFontSize(self.twList)
|
||||
reduceWidgetFontSize(self.teDescription, 1)
|
||||
|
||||
def updateData( self):
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.current_match_set = self.match_set_list[ self.current_match_set_idx ]
|
||||
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")
|
||||
|
||||
if self.current_match_set_idx + 1 == len( self.match_set_list ):
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
#self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.skipButton.setText(self.tr("Skip"))
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle( u"Select correct match or skip ({0} of {1}): {2}".format(
|
||||
self.current_match_set_idx+1,
|
||||
len( self.match_set_list ),
|
||||
os.path.split(path)[1] ))
|
||||
|
||||
def populateTable( self ):
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
self.fetch_func = fetch_func
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
self.current_match_set_idx = 0
|
||||
|
||||
row = 0
|
||||
for match in self.current_match_set.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
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)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.skipButton.clicked.connect(self.skipToNext)
|
||||
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
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 = 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'])
|
||||
self.updateData()
|
||||
|
||||
item_text = year_str + month_str
|
||||
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)
|
||||
def updateData(self):
|
||||
|
||||
item_text = match['issue_title']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
self.current_match_set = self.match_set_list[
|
||||
self.current_match_set_idx]
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
if self.current_match_set_idx + 1 == len(self.match_set_list):
|
||||
self.buttonBox.button(
|
||||
QtGui.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
# self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.skipButton.setText(self.tr("Skip"))
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
|
||||
|
||||
def setCoverImage( self ):
|
||||
ca = self.current_match_set.ca
|
||||
self.archiveCoverWidget.setArchive(ca)
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle(
|
||||
u"Select correct match or skip ({0} of {1}): {2}".format(
|
||||
self.current_match_set_idx + 1,
|
||||
len(self.match_set_list),
|
||||
os.path.split(path)[1])
|
||||
)
|
||||
|
||||
def currentMatch( self ):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
|
||||
return match
|
||||
|
||||
def accept(self):
|
||||
def populateTable(self):
|
||||
|
||||
self.saveMatch()
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len( self.match_set_list ):
|
||||
# no more items
|
||||
QtGui.QDialog.accept(self)
|
||||
else:
|
||||
self.updateData()
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
def skipToNext( self ):
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len( self.match_set_list ):
|
||||
# no more items
|
||||
QtGui.QDialog.reject(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def reject(self):
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Cancel Matching"),
|
||||
self.tr("Are you sure you wish to cancel the matching process?"),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
QtGui.QDialog.reject(self)
|
||||
|
||||
def saveMatch( self ):
|
||||
|
||||
match = self.currentMatch()
|
||||
ca = self.current_match_set.ca
|
||||
row = 0
|
||||
for match in self.current_match_set.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
md = ca.readMetadata( self.style )
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.fetch_func( match )
|
||||
if cv_md is None:
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!"))
|
||||
return
|
||||
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)
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay( cv_md )
|
||||
success = ca.writeMetadata( md, self.style )
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
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 = 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 = 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']
|
||||
if item_text is None:
|
||||
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)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
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.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(self.currentMatch()['description'])
|
||||
|
||||
def setCoverImage(self):
|
||||
ca = self.current_match_set.ca
|
||||
self.archiveCoverWidget.setArchive(ca)
|
||||
|
||||
def currentMatch(self):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data(
|
||||
QtCore.Qt.UserRole).toPyObject()[0]
|
||||
return match
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.saveMatch()
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len(self.match_set_list):
|
||||
# no more items
|
||||
QtGui.QDialog.accept(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def skipToNext(self):
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len(self.match_set_list):
|
||||
# no more items
|
||||
QtGui.QDialog.reject(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def reject(self):
|
||||
reply = QtGui.QMessageBox.question(
|
||||
self,
|
||||
self.tr("Cancel Matching"),
|
||||
self.tr("Are you sure you wish to cancel the matching process?"),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
QtGui.QDialog.reject(self)
|
||||
|
||||
def saveMatch(self):
|
||||
|
||||
match = self.currentMatch()
|
||||
ca = self.current_match_set.ca
|
||||
|
||||
md = ca.readMetadata(self.style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.fetch_func(match)
|
||||
if cv_md is None:
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr(
|
||||
"Could not connect to Comic Vine to get issue details!"))
|
||||
return
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay(cv_md)
|
||||
success = ca.writeMetadata(md, self.style)
|
||||
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr(
|
||||
"Saving the tags to the archive seemed to fail!"))
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
"""
|
||||
A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
"""A PyQT4 dialog to show ID log and progress"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
import utils
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#import utils
|
||||
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
self.isdone = False
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('autotagprogresswindow.ui'), self)
|
||||
|
||||
def setTestImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblTest )
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def setCoverImage( self, img_data , label):
|
||||
if img_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( img_data )
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
label.setScaledContents(True)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
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)
|
||||
|
||||
reduceWidgetFontSize(self.textEdit)
|
||||
|
||||
def setArchiveImage(self, img_data):
|
||||
self.setCoverImage(img_data, self.archiveCoverWidget)
|
||||
|
||||
def setTestImage(self, img_data):
|
||||
self.setCoverImage(img_data, self.testCoverWidget)
|
||||
|
||||
def setCoverImage(self, img_data, widget):
|
||||
widget.setImageData(img_data)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
||||
@@ -1,104 +1,127 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
"""A PyQT4 dialog to confirm and set options for auto-tag"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
#from settingswindow import SettingsWindow
|
||||
#from filerenamer import FileRenamer
|
||||
#import utils
|
||||
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
def __init__(self, parent, settings, msg):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
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.cbxRemoveAfterSuccess.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxSpecifySearchString.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.leNameLengthMatchTolerance.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.leSearchString.setEnabled( False )
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('autotagstartwindow.ui'), self)
|
||||
self.label.setText(msg)
|
||||
|
||||
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>""" )
|
||||
|
||||
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
|
||||
|
||||
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.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
self.ignoreLeadingDigitsInFilename = False
|
||||
self.removeAfterSuccess = False
|
||||
self.searchString = None
|
||||
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
def searchStringToggle(self):
|
||||
enable = self.cbxSpecifySearchString.isChecked()
|
||||
self.leSearchString.setEnabled( enable )
|
||||
self.settings = settings
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxDontUseYear.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
|
||||
QtCore.Qt.Unchecked)
|
||||
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.leNameLengthMatchTolerance.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.leSearchString.setEnabled(False)
|
||||
|
||||
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())
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
||||
self.searchString = None
|
||||
|
||||
if self.settings.save_on_low_confidence:
|
||||
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.dont_use_year_when_identifying:
|
||||
self.cbxDontUseYear.setCheckState(QtCore.Qt.Checked)
|
||||
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)
|
||||
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)
|
||||
|
||||
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>""")
|
||||
|
||||
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
|
||||
|
||||
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.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
self.ignoreLeadingDigitsInFilename = False
|
||||
self.removeAfterSuccess = False
|
||||
self.waitAndRetryOnRateLimit = False
|
||||
self.searchString = None
|
||||
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
|
||||
|
||||
def searchStringToggle(self):
|
||||
enable = self.cbxSpecifySearchString.isChecked()
|
||||
self.leSearchString.setEnabled(enable)
|
||||
|
||||
def 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.waitAndRetryOnRateLimit = self.cbxWaitForRateLimit.isChecked()
|
||||
|
||||
# persist some settings
|
||||
self.settings.save_on_low_confidence = self.autoSaveOnLow
|
||||
self.settings.dont_use_year_when_identifying = self.dontUseYear
|
||||
self.settings.assume_1_if_no_issue_num = self.assumeIssueOne
|
||||
self.settings.ignore_leading_numbers_in_filename = self.ignoreLeadingDigitsInFilename
|
||||
self.settings.remove_archive_after_successful_match = self.removeAfterSuccess
|
||||
self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
||||
self.searchString = None
|
||||
|
||||
@@ -1,99 +1,97 @@
|
||||
"""
|
||||
Class to manage modifying metadata specifically for CBL/CBI
|
||||
"""
|
||||
"""A class to manage modifying metadata specifically for CBL/CBI"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
import utils
|
||||
#import os
|
||||
|
||||
#import utils
|
||||
|
||||
|
||||
class CBLTransformer:
|
||||
def __init__( self, metadata, settings ):
|
||||
self.metadata = metadata
|
||||
self.settings = settings
|
||||
|
||||
|
||||
def apply( self ):
|
||||
# helper funcs
|
||||
def append_to_tags_if_unique( item ):
|
||||
if item.lower() not in (tag.lower() for tag in self.metadata.tags):
|
||||
self.metadata.tags.append( item )
|
||||
|
||||
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(',') ]
|
||||
for item in items:
|
||||
append_to_tags_if_unique( item )
|
||||
def __init__(self, metadata, settings):
|
||||
self.metadata = metadata
|
||||
self.settings = settings
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
|
||||
# helper
|
||||
def setLonePrimary( role_list ):
|
||||
lone_credit = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
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
|
||||
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'] )
|
||||
if c is None and count == 0:
|
||||
c, count = setLonePrimary( ['penciler', 'penciller'] )
|
||||
if c is not None:
|
||||
c['primary'] = False
|
||||
self.metadata.addCredit( c['person'], 'Artist', True )
|
||||
def apply(self):
|
||||
# helper funcs
|
||||
def append_to_tags_if_unique(item):
|
||||
if item.lower() not in (tag.lower() for tag in self.metadata.tags):
|
||||
self.metadata.tags.append(item)
|
||||
|
||||
if self.settings.copy_characters_to_tags:
|
||||
add_string_list_to_tags( self.metadata.characters )
|
||||
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(',')]
|
||||
for item in items:
|
||||
append_to_tags_if_unique(item)
|
||||
|
||||
if self.settings.copy_teams_to_tags:
|
||||
add_string_list_to_tags( self.metadata.teams )
|
||||
|
||||
if self.settings.copy_locations_to_tags:
|
||||
add_string_list_to_tags( self.metadata.locations )
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.metadata.webLink is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.webLink
|
||||
# helper
|
||||
def setLonePrimary(role_list):
|
||||
lone_credit = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
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
|
||||
return lone_credit, count
|
||||
|
||||
return self.metadata
|
||||
# need to loop three times, once for 'writer', 'artist', and then
|
||||
# 'penciler' if no artist
|
||||
setLonePrimary(['writer'])
|
||||
c, count = setLonePrimary(['artist'])
|
||||
if c is None and count == 0:
|
||||
c, count = setLonePrimary(['penciler', 'penciller'])
|
||||
if c is not None:
|
||||
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)
|
||||
|
||||
if self.settings.copy_teams_to_tags:
|
||||
add_string_list_to_tags(self.metadata.teams)
|
||||
|
||||
if self.settings.copy_locations_to_tags:
|
||||
add_string_list_to_tags(self.metadata.locations)
|
||||
|
||||
if self.settings.copy_storyarcs_to_tags:
|
||||
add_string_list_to_tags(self.metadata.storyArc)
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.metadata.webLink is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.webLink
|
||||
|
||||
return self.metadata
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,260 +1 @@
|
||||
"""
|
||||
A python class to encapsulate CoMet data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 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.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import zipfile
|
||||
from pprint import pprint
|
||||
import xml.etree.ElementTree as ET
|
||||
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 )
|
||||
|
||||
from comicapi.comet import *
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,152 +1 @@
|
||||
"""
|
||||
A python class to encapsulate the ComicBookInfo data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 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 )
|
||||
|
||||
#verify that the string actually contains CBI data in JSON format
|
||||
def validateString( self, string ):
|
||||
|
||||
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/' + 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
|
||||
|
||||
from comicapi.comicbookinfo import *
|
||||
|
||||
@@ -1,291 +1 @@
|
||||
"""
|
||||
A python class to encapsulate ComicRack's ComicInfo.xml data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 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.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import zipfile
|
||||
from pprint import pprint
|
||||
import xml.etree.ElementTree as ET
|
||||
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'
|
||||
):
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), n.tag )
|
||||
|
||||
if n.tag == 'CoverArtist':
|
||||
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 )
|
||||
|
||||
from comicapi.comicinfoxml import *
|
||||
|
||||
@@ -1,426 +1,469 @@
|
||||
"""
|
||||
A python class to manage caching of data from Comic Vine
|
||||
"""
|
||||
"""A python class to manage caching of data from Comic Vine"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
|
||||
from pprint import pprint
|
||||
# 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 sqlite3 as lite
|
||||
import sys
|
||||
import os
|
||||
import datetime
|
||||
#import sys
|
||||
#from pprint import pprint
|
||||
|
||||
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")
|
||||
|
||||
#verify that cache is from same version as this one
|
||||
data = ""
|
||||
try:
|
||||
with open( self.version_file, 'rb' ) as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
if data != ctversion.version:
|
||||
self.clearCache()
|
||||
|
||||
if not os.path.exists( self.db_file ):
|
||||
self.create_cache_db()
|
||||
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")
|
||||
|
||||
def clearCache( self ):
|
||||
try:
|
||||
os.unlink( self.db_file )
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.unlink( self.version_file )
|
||||
except:
|
||||
pass
|
||||
# verify that cache is from same version as this one
|
||||
data = ""
|
||||
try:
|
||||
with open(self.version_file, 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
if data != ctversion.version:
|
||||
self.clearCache()
|
||||
|
||||
def create_cache_db( self ):
|
||||
|
||||
#create the version file
|
||||
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()
|
||||
if not os.path.exists(self.db_file):
|
||||
self.create_cache_db()
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
# create tables
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
#name,id,start_year,publisher,image,description,count_of_issues
|
||||
cur.execute("CREATE TABLE VolumeSearchCache(" +
|
||||
"search_term TEXT," +
|
||||
"id INT," +
|
||||
"name TEXT," +
|
||||
"start_year INT," +
|
||||
"publisher TEXT," +
|
||||
"count_of_issues INT," +
|
||||
"image_url TEXT," +
|
||||
"description TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')) ) "
|
||||
)
|
||||
|
||||
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) )"
|
||||
)
|
||||
def clearCache(self):
|
||||
try:
|
||||
os.unlink(self.db_file)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.unlink(self.version_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
cur.execute("CREATE TABLE AltCovers(" +
|
||||
"issue_id INT," +
|
||||
"url_list TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (issue_id) )"
|
||||
)
|
||||
def create_cache_db(self):
|
||||
|
||||
cur.execute("CREATE TABLE Issues(" +
|
||||
"id INT," +
|
||||
"volume_id INT," +
|
||||
"name TEXT," +
|
||||
"issue_number TEXT," +
|
||||
"image_url TEXT," +
|
||||
"image_hash TEXT," +
|
||||
"thumb_image_url TEXT," +
|
||||
"thumb_image_hash TEXT," +
|
||||
"publish_month TEXT," +
|
||||
"publish_year TEXT," +
|
||||
"site_detail_url TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id ) )"
|
||||
)
|
||||
# create the version file
|
||||
with open(self.version_file, 'w') as f:
|
||||
f.write(ctversion.version)
|
||||
|
||||
def add_search_results( self, search_term, cv_search_results ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
# this will wipe out any existing version
|
||||
open(self.db_file, 'w').close()
|
||||
|
||||
with con:
|
||||
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() ])
|
||||
|
||||
# now add in new results
|
||||
for record in cv_search_results:
|
||||
timestamp = datetime.datetime.now()
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
if record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = record['publisher']['name']
|
||||
|
||||
if record['image'] is None:
|
||||
url = ""
|
||||
else:
|
||||
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'],
|
||||
pub_name,
|
||||
record['count_of_issues'],
|
||||
url,
|
||||
record['description'])
|
||||
)
|
||||
|
||||
def get_search_results( self, search_term ):
|
||||
|
||||
results = list()
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
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) ] )
|
||||
|
||||
# fetch
|
||||
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]
|
||||
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
# create tables
|
||||
with con:
|
||||
|
||||
def add_alt_covers( self, issue_id, url_list ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
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'))) ")
|
||||
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [ issue_id ])
|
||||
|
||||
url_list_str = utils.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(
|
||||
"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))")
|
||||
|
||||
def get_alt_covers( self, issue_id ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
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) ] )
|
||||
|
||||
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
if row is None :
|
||||
return None
|
||||
else:
|
||||
url_list_str = row[0]
|
||||
if len(url_list_str) == 0:
|
||||
return []
|
||||
raw_list = url_list_str.split(",")
|
||||
url_list = []
|
||||
for item in raw_list:
|
||||
url_list.append( str(item).strip())
|
||||
return url_list
|
||||
|
||||
def add_volume_info( self, cv_volume_record ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
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))")
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
if cv_volume_record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = cv_volume_record['publisher']['name']
|
||||
def add_search_results(self, search_term, cv_search_results):
|
||||
|
||||
data = {
|
||||
"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
|
||||
}
|
||||
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
# now add in issues
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
for issue in cv_volume_record['issues']:
|
||||
|
||||
data = {
|
||||
"volume_id": cv_volume_record['id'],
|
||||
"name": issue['name'],
|
||||
"issue_number": issue['issue_number'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "issues" , "id", issue['id'], data)
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute(
|
||||
"DELETE FROM VolumeSearchCache WHERE search_term = ?", [
|
||||
search_term.lower()])
|
||||
|
||||
def get_volume_info( self, volume_id ):
|
||||
|
||||
result = None
|
||||
# now add in new results
|
||||
for record in cv_search_results:
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
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) ] )
|
||||
if record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = record['publisher']['name']
|
||||
|
||||
# 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 Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None :
|
||||
return result
|
||||
|
||||
result = dict()
|
||||
|
||||
#since ID is primary key, there is only one row
|
||||
result['id'] = row[0]
|
||||
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()
|
||||
if record['image'] is None:
|
||||
url = ""
|
||||
else:
|
||||
url = record['image']['super_url']
|
||||
|
||||
cur.execute("SELECT id,name,issue_number,image_url,image_hash 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['image_url'] = row[3]
|
||||
record['image_hash'] = row[4]
|
||||
|
||||
result['issues'].append(record)
|
||||
|
||||
return result
|
||||
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'],
|
||||
pub_name,
|
||||
record['count_of_issues'],
|
||||
url,
|
||||
record['description']))
|
||||
|
||||
def get_search_results(self, search_term):
|
||||
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, publish_month, publish_year, site_detail_url ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
results = list()
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
"image_url": image_url,
|
||||
"thumb_image_url": thumb_image_url,
|
||||
"publish_month": publish_month,
|
||||
"publish_year": publish_year,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "issues" , "id", issue_id, data)
|
||||
|
||||
|
||||
# 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)])
|
||||
|
||||
def get_issue_select_details( self, issue_id ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year,site_detail_url FROM Issues WHERE id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
# fetch
|
||||
cur.execute(
|
||||
"SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
|
||||
rows = cur.fetchall()
|
||||
# now process the results
|
||||
for record in rows:
|
||||
|
||||
details = dict()
|
||||
if row is None or row[0] is None :
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['publish_month'] = None
|
||||
details['publish_year'] = None
|
||||
details['site_detail_url'] = None
|
||||
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]
|
||||
|
||||
else:
|
||||
details['image_url'] = row[0]
|
||||
details['thumb_image_url'] = row[1]
|
||||
details['publish_month'] = row[2]
|
||||
details['publish_year'] = row[3]
|
||||
details['site_detail_url'] = row[4]
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def upsert( self, cur, tablename, pkname, pkval, data):
|
||||
"""
|
||||
This does an insert if the given PK doesn't exist, and an update it if does
|
||||
"""
|
||||
|
||||
# TODO - look into checking if UPDATE is needed
|
||||
# TODO - should the cursor be created here, and not up the stack?
|
||||
|
||||
ins_count = len(data) + 1
|
||||
results.append(result)
|
||||
|
||||
keys = ""
|
||||
vals = list()
|
||||
ins_slots = ""
|
||||
set_slots = ""
|
||||
|
||||
for key in data:
|
||||
|
||||
if keys != "":
|
||||
keys += ", "
|
||||
if ins_slots != "":
|
||||
ins_slots += ", "
|
||||
if set_slots != "":
|
||||
set_slots += ", "
|
||||
|
||||
keys += key
|
||||
vals.append( data[key] )
|
||||
ins_slots += "?"
|
||||
set_slots += key + " = ?"
|
||||
return results
|
||||
|
||||
keys += ", " + pkname
|
||||
vals.append( pkval )
|
||||
ins_slots += ", ?"
|
||||
condition = pkname + " = ?"
|
||||
def add_alt_covers(self, issue_id, url_list):
|
||||
|
||||
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 )
|
||||
cur.execute( sql_upd , vals )
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [issue_id])
|
||||
|
||||
url_list_str = utils.listToString(url_list)
|
||||
# now add in new record
|
||||
cur.execute("INSERT INTO AltCovers " +
|
||||
"(issue_id, url_list) " +
|
||||
"VALUES(?, ?)",
|
||||
(issue_id,
|
||||
url_list_str)
|
||||
)
|
||||
|
||||
def get_alt_covers(self, issue_id):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
# 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)])
|
||||
|
||||
cur.execute(
|
||||
"SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
else:
|
||||
url_list_str = row[0]
|
||||
if len(url_list_str) == 0:
|
||||
return []
|
||||
raw_list = url_list_str.split(",")
|
||||
url_list = []
|
||||
for item in raw_list:
|
||||
url_list.append(str(item).strip())
|
||||
return url_list
|
||||
|
||||
def add_volume_info(self, cv_volume_record):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
if cv_volume_record['publisher'] is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = cv_volume_record['publisher']['name']
|
||||
|
||||
data = {
|
||||
"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
|
||||
}
|
||||
self.upsert(cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
|
||||
def add_volume_issues_info(self, volume_id, cv_volume_issues):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
# add in issues
|
||||
|
||||
for issue in cv_volume_issues:
|
||||
|
||||
data = {
|
||||
"volume_id": volume_id,
|
||||
"name": issue['name'],
|
||||
"issue_number": issue['issue_number'],
|
||||
"site_detail_url": issue['site_detail_url'],
|
||||
"cover_date": issue['cover_date'],
|
||||
"super_url": issue['image']['super_url'],
|
||||
"thumb_url": issue['image']['thumb_url'],
|
||||
"description": issue['description'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert(cur, "issues", "id", issue['id'], data)
|
||||
|
||||
def get_volume_info(self, volume_id):
|
||||
|
||||
result = None
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
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)])
|
||||
|
||||
# fetch
|
||||
cur.execute(
|
||||
"SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?",
|
||||
[volume_id])
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None:
|
||||
return result
|
||||
|
||||
result = dict()
|
||||
|
||||
# since ID is primary key, there is only one row
|
||||
result['id'] = row[0]
|
||||
result['name'] = row[1]
|
||||
result['publisher'] = dict()
|
||||
result['publisher']['name'] = row[2]
|
||||
result['count_of_issues'] = row[3]
|
||||
result['start_year'] = row[4]
|
||||
result['issues'] = list()
|
||||
|
||||
return result
|
||||
|
||||
def get_volume_issues_info(self, volume_id):
|
||||
|
||||
result = None
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = 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)])
|
||||
|
||||
# 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])
|
||||
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]
|
||||
|
||||
results.append(record)
|
||||
|
||||
if len(results) == 0:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
def add_issue_select_details(
|
||||
self,
|
||||
issue_id,
|
||||
image_url,
|
||||
thumb_image_url,
|
||||
cover_date,
|
||||
site_detail_url):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
"super_url": image_url,
|
||||
"thumb_url": thumb_image_url,
|
||||
"cover_date": cover_date,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert(cur, "issues", "id", issue_id, data)
|
||||
|
||||
def get_issue_select_details(self, issue_id):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
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
|
||||
|
||||
else:
|
||||
details['image_url'] = row[0]
|
||||
details['thumb_image_url'] = row[1]
|
||||
details['cover_date'] = row[2]
|
||||
details['site_detail_url'] = row[3]
|
||||
|
||||
return details
|
||||
|
||||
def upsert(self, cur, tablename, pkname, pkval, data):
|
||||
"""This does an insert if the given PK doesn't exist, and an
|
||||
update it if does
|
||||
|
||||
TODO: look into checking if UPDATE is needed
|
||||
TODO: should the cursor be created here, and not up the stack?
|
||||
"""
|
||||
|
||||
ins_count = len(data) + 1
|
||||
|
||||
keys = ""
|
||||
vals = list()
|
||||
ins_slots = ""
|
||||
set_slots = ""
|
||||
|
||||
for key in data:
|
||||
|
||||
if keys != "":
|
||||
keys += ", "
|
||||
if ins_slots != "":
|
||||
ins_slots += ", "
|
||||
if set_slots != "":
|
||||
set_slots += ", "
|
||||
|
||||
keys += key
|
||||
vals.append(data[key])
|
||||
ins_slots += "?"
|
||||
set_slots += key + " = ?"
|
||||
|
||||
keys += ", " + pkname
|
||||
vals.append(pkval)
|
||||
ins_slots += ", ?"
|
||||
condition = pkname + " = ?"
|
||||
|
||||
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)
|
||||
cur.execute(sql_upd, vals)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,290 +1,325 @@
|
||||
"""
|
||||
A PyQt4 widget display cover images from either local archive, or from ComicVine
|
||||
"""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!
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
#import os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from comicarchive import MetaDataStyle
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from imagefetcher import ImageFetcher
|
||||
from pageloader import PageLoader
|
||||
from imagepopup import ImagePopup
|
||||
import utils
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, getQImageFromData
|
||||
#from genericmetadata import GenericMetadata, PageType
|
||||
#from comicarchive import MetaDataStyle
|
||||
#import utils
|
||||
|
||||
|
||||
# helper func to allow a label to be clickable
|
||||
def clickable(widget):
|
||||
"""# Allow a label to be clickable"""
|
||||
|
||||
class Filter(QObject):
|
||||
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
class Filter(QObject):
|
||||
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
|
||||
|
||||
class CoverImageWidget(QWidget):
|
||||
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
|
||||
def __init__(self, parent, mode ):
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self)
|
||||
|
||||
utils.reduceWidgetFontSize( self.label )
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
DataMode = 3
|
||||
|
||||
self.mode = mode
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.page_loader = None
|
||||
self.showControls = True
|
||||
def __init__(self, parent, mode, expand_on_click=True):
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
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 )
|
||||
self.resetWidget()
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self)
|
||||
|
||||
self.updateContent()
|
||||
reduceWidgetFontSize(self.label)
|
||||
|
||||
def resetWidget(self):
|
||||
self.comic_archive = None
|
||||
self.issue_id = None
|
||||
self.comicVine = None
|
||||
self.cover_fetcher = None
|
||||
self.url_list = []
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = None
|
||||
self.imageIndex = -1
|
||||
self.imageCount = 1
|
||||
|
||||
def clear( self ):
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
def incrementImage( self ):
|
||||
self.imageIndex += 1
|
||||
if self.imageIndex == self.imageCount:
|
||||
self.imageIndex = 0
|
||||
self.updateContent()
|
||||
self.mode = mode
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.page_loader = None
|
||||
self.showControls = True
|
||||
|
||||
def decrementImage( self ):
|
||||
self.imageIndex -= 1
|
||||
if self.imageIndex == -1:
|
||||
self.imageIndex = self.imageCount -1
|
||||
self.updateContent()
|
||||
|
||||
def setArchive( self, ca, page=0 ):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.resetWidget()
|
||||
self.comic_archive = ca
|
||||
self.imageIndex = page
|
||||
self.imageCount = ca.getNumberOfPages()
|
||||
self.updateContent()
|
||||
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
|
||||
self.btnRight.setIcon(
|
||||
QIcon(ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
def setURL( self, url ):
|
||||
if self.mode == CoverImageWidget.URLMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.url_list = [ url ]
|
||||
self.imageIndex = 0
|
||||
self.imageCount = 1
|
||||
self.updateContent()
|
||||
self.btnLeft.clicked.connect(self.decrementImage)
|
||||
self.btnRight.clicked.connect(self.incrementImage)
|
||||
self.resetWidget()
|
||||
if expand_on_click:
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
else:
|
||||
self.lblImage.setToolTip("")
|
||||
|
||||
def setIssueID( self, issue_id ):
|
||||
if self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.issue_id = issue_id
|
||||
self.updateContent()
|
||||
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete )
|
||||
self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
|
||||
def primaryUrlFetchComplete( self, primary_url, thumb_url, issue_id ):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateContent()
|
||||
def resetWidget(self):
|
||||
self.comic_archive = None
|
||||
self.issue_id = None
|
||||
self.comicVine = None
|
||||
self.cover_fetcher = None
|
||||
self.url_list = []
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = None
|
||||
self.imageIndex = -1
|
||||
self.imageCount = 1
|
||||
self.imageData = None
|
||||
|
||||
#defer the alt cover search
|
||||
QTimer.singleShot(1, self.startAltCoverSearch)
|
||||
def clear(self):
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
def startAltCoverSearch( self ):
|
||||
def incrementImage(self):
|
||||
self.imageIndex += 1
|
||||
if self.imageIndex == self.imageCount:
|
||||
self.imageIndex = 0
|
||||
self.updateContent()
|
||||
|
||||
# now we need to get the list of alt cover URLs
|
||||
self.label.setText("Searching for alt. covers...")
|
||||
|
||||
# page URL should already be cached, so no need to defer
|
||||
self.comicVine = ComicVineTalker()
|
||||
issue_page_url = self.comicVine.fetchIssuePageURL( self.issue_id )
|
||||
self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete )
|
||||
self.comicVine.asyncFetchAlternateCoverURLs( int(self.issue_id), issue_page_url)
|
||||
|
||||
def altCoverUrlListFetchComplete( self, url_list, issue_id ):
|
||||
if len(url_list) > 0:
|
||||
self.url_list.extend(url_list)
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateControls()
|
||||
def decrementImage(self):
|
||||
self.imageIndex -= 1
|
||||
if self.imageIndex == -1:
|
||||
self.imageIndex = self.imageCount - 1
|
||||
self.updateContent()
|
||||
|
||||
def setPage( self, pagenum ):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.imageIndex = pagenum
|
||||
self.updateContent()
|
||||
|
||||
def updateContent( self ):
|
||||
self.updateImage()
|
||||
self.updateControls()
|
||||
|
||||
def updateImage( self ):
|
||||
if self.imageIndex == -1:
|
||||
self.loadDefault()
|
||||
elif self.mode in [ CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode ]:
|
||||
self.loadURL()
|
||||
else:
|
||||
self.loadPage()
|
||||
|
||||
def updateControls( self ):
|
||||
if not self.showControls:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
self.label.hide()
|
||||
return
|
||||
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.btnLeft.setEnabled(False)
|
||||
self.btnRight.setEnabled(False)
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
else:
|
||||
self.btnLeft.setEnabled(True)
|
||||
self.btnRight.setEnabled(True)
|
||||
self.btnLeft.show()
|
||||
self.btnRight.show()
|
||||
|
||||
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))
|
||||
else:
|
||||
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...."
|
||||
|
||||
# called when the image is done loading from internet
|
||||
def coverRemoteFetchComplete( self, image_data, issue_id ):
|
||||
img = QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
#print "ATB cover fetch complete!"
|
||||
def setArchive(self, ca, page=0):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.resetWidget()
|
||||
self.comic_archive = ca
|
||||
self.imageIndex = page
|
||||
self.imageCount = ca.getNumberOfPages()
|
||||
self.updateContent()
|
||||
|
||||
def loadPage( self ):
|
||||
if self.comic_archive is not None:
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = PageLoader( self.comic_archive, self.imageIndex )
|
||||
self.page_loader.loadComplete.connect( self.pageLoadComplete )
|
||||
self.page_loader.start()
|
||||
def setURL(self, url):
|
||||
if self.mode == CoverImageWidget.URLMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
def pageLoadComplete( self, img ):
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.page_loader = None
|
||||
|
||||
def loadDefault( self ):
|
||||
self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
#print "loadDefault called"
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.url_list = [url]
|
||||
self.imageIndex = 0
|
||||
self.imageCount = 1
|
||||
self.updateContent()
|
||||
|
||||
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()
|
||||
#print "ATB resizeEvent deltas", resize_event.size().width(), resize_event.size().height()
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
"""
|
||||
new_h = self.frame.height() + delta_h
|
||||
new_w = self.frame.width() + delta_w
|
||||
print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
print "ATB self.frame", self.frame.width(), self.frame.height()
|
||||
print "ATB self.", self.width(), self.height()
|
||||
|
||||
frame_w = new_w
|
||||
frame_h = new_h
|
||||
"""
|
||||
new_h = self.frame.height()
|
||||
new_w = self.frame.width()
|
||||
frame_w = self.frame.width()
|
||||
frame_h = self.frame.height()
|
||||
def setIssueID(self, issue_id):
|
||||
if self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
self.issue_id = issue_id
|
||||
|
||||
#print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
#print "ATB self.frame", frame_w, frame_h
|
||||
#print "ATB new size", new_w, new_h
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap( scaled_pixmap )
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize( img_w, img_h )
|
||||
self.lblImage.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
|
||||
|
||||
def showPopup( self ):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect(
|
||||
self.primaryUrlFetchComplete)
|
||||
self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id))
|
||||
|
||||
def setImageData(self, image_data):
|
||||
if self.mode == CoverImageWidget.DataMode:
|
||||
self.resetWidget()
|
||||
|
||||
if image_data is None:
|
||||
self.imageIndex = -1
|
||||
else:
|
||||
self.imageIndex = 0
|
||||
self.imageData = image_data
|
||||
|
||||
self.updateContent()
|
||||
|
||||
def primaryUrlFetchComplete(self, primary_url, thumb_url, issue_id):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateContent()
|
||||
|
||||
# defer the alt cover search
|
||||
QTimer.singleShot(1, self.startAltCoverSearch)
|
||||
|
||||
def startAltCoverSearch(self):
|
||||
|
||||
# now we need to get the list of alt cover URLs
|
||||
self.label.setText("Searching for alt. covers...")
|
||||
|
||||
# page URL should already be cached, so no need to defer
|
||||
self.comicVine = ComicVineTalker()
|
||||
issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id)
|
||||
self.comicVine.altUrlListFetchComplete.connect(
|
||||
self.altCoverUrlListFetchComplete)
|
||||
self.comicVine.asyncFetchAlternateCoverURLs(
|
||||
int(self.issue_id), issue_page_url)
|
||||
|
||||
def altCoverUrlListFetchComplete(self, url_list, issue_id):
|
||||
if len(url_list) > 0:
|
||||
self.url_list.extend(url_list)
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateControls()
|
||||
|
||||
def setPage(self, pagenum):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.imageIndex = pagenum
|
||||
self.updateContent()
|
||||
|
||||
def updateContent(self):
|
||||
self.updateImage()
|
||||
self.updateControls()
|
||||
|
||||
def updateImage(self):
|
||||
if self.imageIndex == -1:
|
||||
self.loadDefault()
|
||||
elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]:
|
||||
self.loadURL()
|
||||
elif self.mode == CoverImageWidget.DataMode:
|
||||
self.coverRemoteFetchComplete(self.imageData, 0)
|
||||
else:
|
||||
self.loadPage()
|
||||
|
||||
def updateControls(self):
|
||||
if not self.showControls or self.mode == CoverImageWidget.DataMode:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
self.label.hide()
|
||||
return
|
||||
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.btnLeft.setEnabled(False)
|
||||
self.btnRight.setEnabled(False)
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
else:
|
||||
self.btnLeft.setEnabled(True)
|
||||
self.btnRight.setEnabled(True)
|
||||
self.btnLeft.show()
|
||||
self.btnRight.show()
|
||||
|
||||
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))
|
||||
else:
|
||||
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...")
|
||||
|
||||
# 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!")
|
||||
|
||||
def loadPage(self):
|
||||
if self.comic_archive is not None:
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
|
||||
self.page_loader.loadComplete.connect(self.pageLoadComplete)
|
||||
self.page_loader.start()
|
||||
|
||||
def pageLoadComplete(self, img):
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap(0, 0)
|
||||
self.page_loader = None
|
||||
|
||||
def loadDefault(self):
|
||||
self.current_pixmap = QPixmap(
|
||||
ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
#print("loadDefault called")
|
||||
self.setDisplayPixmap(0, 0)
|
||||
|
||||
def resizeEvent(self, resize_event):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - \
|
||||
resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - \
|
||||
resize_event.oldSize().height()
|
||||
# print "ATB resizeEvent deltas", resize_event.size().width(),
|
||||
# resize_event.size().height()
|
||||
self.setDisplayPixmap(delta_w, delta_h)
|
||||
|
||||
def setDisplayPixmap(self, delta_w, delta_h):
|
||||
"""The deltas let us know what the new width and height of the label will be"""
|
||||
|
||||
#new_h = self.frame.height() + delta_h
|
||||
#new_w = self.frame.width() + delta_w
|
||||
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
# print "ATB self.frame", self.frame.width(), self.frame.height()
|
||||
# print "ATB self.", self.width(), self.height()
|
||||
|
||||
#frame_w = new_w
|
||||
#frame_h = new_h
|
||||
|
||||
new_h = self.frame.height()
|
||||
new_w = self.frame.width()
|
||||
frame_w = self.frame.width()
|
||||
frame_h = self.frame.height()
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0
|
||||
if new_w < 0:
|
||||
new_w = 0
|
||||
|
||||
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
# print "ATB self.frame", frame_w, frame_h
|
||||
# print "ATB new size", new_w, new_h
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(
|
||||
new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap(scaled_pixmap)
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2)
|
||||
|
||||
def showPopup(self):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
|
||||
@@ -1,99 +1,96 @@
|
||||
"""
|
||||
A PyQT4 dialog to edit credits
|
||||
"""
|
||||
"""A PyQT4 dialog to edit credits"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
import os
|
||||
|
||||
|
||||
class CreditEditorWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
ModeEdit = 0
|
||||
ModeNew = 1
|
||||
|
||||
|
||||
def __init__(self, parent, mode, role, name, primary ):
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('crediteditorwindow.ui' ), self)
|
||||
|
||||
self.mode = mode
|
||||
|
||||
if self.mode == self.ModeEdit:
|
||||
self.setWindowTitle("Edit Credit")
|
||||
else:
|
||||
self.setWindowTitle("New Credit")
|
||||
|
||||
# Add the entries to the role combobox
|
||||
self.cbRole.addItem( "" )
|
||||
self.cbRole.addItem( "Writer" )
|
||||
self.cbRole.addItem( "Artist" )
|
||||
self.cbRole.addItem( "Penciller" )
|
||||
self.cbRole.addItem( "Inker" )
|
||||
self.cbRole.addItem( "Colorist" )
|
||||
self.cbRole.addItem( "Letterer" )
|
||||
self.cbRole.addItem( "Cover Artist" )
|
||||
self.cbRole.addItem( "Editor" )
|
||||
self.cbRole.addItem( "Other" )
|
||||
self.cbRole.addItem( "Plotter" )
|
||||
self.cbRole.addItem( "Scripter" )
|
||||
|
||||
self.leName.setText( name )
|
||||
|
||||
if role is not None and role != "":
|
||||
i = self.cbRole.findText( role )
|
||||
if i == -1:
|
||||
self.cbRole.setEditText( role )
|
||||
else:
|
||||
self.cbRole.setCurrentIndex( i )
|
||||
ModeEdit = 0
|
||||
ModeNew = 1
|
||||
|
||||
if primary:
|
||||
self.cbPrimary.setCheckState( QtCore.Qt.Checked )
|
||||
|
||||
self.cbRole.currentIndexChanged.connect(self.roleChanged)
|
||||
self.cbRole.editTextChanged.connect(self.roleChanged)
|
||||
|
||||
self.updatePrimaryButton()
|
||||
def __init__(self, parent, mode, role, name, primary):
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
def updatePrimaryButton( self ):
|
||||
enabled =self.currentRoleCanBePrimary()
|
||||
self.cbPrimary.setEnabled( enabled )
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('crediteditorwindow.ui'), self)
|
||||
|
||||
def currentRoleCanBePrimary( self ):
|
||||
role = self.cbRole.currentText()
|
||||
if str(role).lower() == "writer" or str(role).lower() == "artist":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def roleChanged( self, s ):
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def getCredits( self ):
|
||||
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
|
||||
return self.cbRole.currentText(), self.leName.text(), primary
|
||||
self.mode = mode
|
||||
|
||||
if self.mode == self.ModeEdit:
|
||||
self.setWindowTitle("Edit Credit")
|
||||
else:
|
||||
self.setWindowTitle("New Credit")
|
||||
|
||||
def accept( self ):
|
||||
if self.cbRole.currentText() == "" or self.leName.text() == "":
|
||||
QtGui.QMessageBox.warning(self, self.tr("Whoops"), self.tr("You need to enter both role and name for a credit."))
|
||||
else:
|
||||
QtGui.QDialog.accept(self)
|
||||
# Add the entries to the role combobox
|
||||
self.cbRole.addItem("")
|
||||
self.cbRole.addItem("Writer")
|
||||
self.cbRole.addItem("Artist")
|
||||
self.cbRole.addItem("Penciller")
|
||||
self.cbRole.addItem("Inker")
|
||||
self.cbRole.addItem("Colorist")
|
||||
self.cbRole.addItem("Letterer")
|
||||
self.cbRole.addItem("Cover Artist")
|
||||
self.cbRole.addItem("Editor")
|
||||
self.cbRole.addItem("Other")
|
||||
self.cbRole.addItem("Plotter")
|
||||
self.cbRole.addItem("Scripter")
|
||||
|
||||
self.leName.setText(name)
|
||||
|
||||
if role is not None and role != "":
|
||||
i = self.cbRole.findText(role)
|
||||
if i == -1:
|
||||
self.cbRole.setEditText(role)
|
||||
else:
|
||||
self.cbRole.setCurrentIndex(i)
|
||||
|
||||
if primary:
|
||||
self.cbPrimary.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
self.cbRole.currentIndexChanged.connect(self.roleChanged)
|
||||
self.cbRole.editTextChanged.connect(self.roleChanged)
|
||||
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def updatePrimaryButton(self):
|
||||
enabled = self.currentRoleCanBePrimary()
|
||||
self.cbPrimary.setEnabled(enabled)
|
||||
|
||||
def currentRoleCanBePrimary(self):
|
||||
role = self.cbRole.currentText()
|
||||
if str(role).lower() == "writer" or str(role).lower() == "artist":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def roleChanged(self, s):
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def getCredits(self):
|
||||
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
|
||||
return self.cbRole.currentText(), self.leName.text(), primary
|
||||
|
||||
def accept(self):
|
||||
if self.cbRole.currentText() == "" or self.leName.text() == "":
|
||||
QtGui.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
|
||||
"You need to enter both role and name for a credit."))
|
||||
else:
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# This file should contain only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="1.1.3-beta"
|
||||
version = "1.1.16-beta-rc"
|
||||
|
||||
@@ -1,65 +1,64 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for export to zip
|
||||
"""
|
||||
"""A PyQT4 dialog to confirm and set options for export to zip"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
#from settingswindow import SettingsWindow
|
||||
#from filerenamer import FileRenamer
|
||||
#import utils
|
||||
|
||||
|
||||
class ExportConflictOpts:
|
||||
dontCreate = 1
|
||||
overwrite = 2
|
||||
createUnique = 3
|
||||
|
||||
dontCreate = 1
|
||||
overwrite = 2
|
||||
createUnique = 3
|
||||
|
||||
|
||||
class ExportWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
def __init__(self, parent, settings, msg):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAddToList.setCheckState( QtCore.Qt.Checked )
|
||||
self.radioDontCreate.setChecked( True )
|
||||
|
||||
self.deleteOriginal = False
|
||||
self.addToList = True
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui'), self)
|
||||
self.label.setText(msg)
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
|
||||
self.addToList = self.cbxAddToList.isChecked()
|
||||
if self.radioDontCreate.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
elif self.radioCreateNew.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.createUnique
|
||||
#else:
|
||||
# self.fileConflictBehavior = ExportConflictOpts.overwrite
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.cbxAddToList.setCheckState(QtCore.Qt.Checked)
|
||||
self.radioDontCreate.setChecked(True)
|
||||
|
||||
self.deleteOriginal = False
|
||||
self.addToList = True
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
|
||||
def accept(self):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
|
||||
self.addToList = self.cbxAddToList.isChecked()
|
||||
if self.radioDontCreate.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
elif self.radioCreateNew.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.createUnique
|
||||
# else:
|
||||
# self.fileConflictBehavior = ExportConflictOpts.overwrite
|
||||
|
||||
@@ -1,235 +1 @@
|
||||
"""
|
||||
Functions for parsing comic info from filename
|
||||
|
||||
This should probably be re-written, but, well, it mostly works!
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 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 fixSpaces( self, string, remove_dashes=True ):
|
||||
if remove_dashes:
|
||||
placeholders = ['[-_]',' +']
|
||||
else:
|
||||
placeholders = ['[_]',' +']
|
||||
for ph in placeholders:
|
||||
string = re.sub(ph, ' ', string )
|
||||
return string.strip()
|
||||
|
||||
# check for silly .1 or .5 style issue strings
|
||||
# allow up to 5 chars total
|
||||
def isPointIssue( self, word ):
|
||||
ret = False
|
||||
try:
|
||||
float(word)
|
||||
if (len(word) < 5 and not word.isdigit()):
|
||||
ret = True
|
||||
except ValueError:
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
def getIssueCount( self,filename ):
|
||||
|
||||
count = ""
|
||||
# replace any name seperators 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 ):
|
||||
|
||||
found = False
|
||||
issue = ''
|
||||
|
||||
# first, look for multiple "--", this mean's 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 = filename.split("--")[0]
|
||||
elif "___" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
|
||||
filename = filename.split("__")[0]
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
|
||||
# remove parenthetical phrases
|
||||
filename = re.sub( "\(.*\)", "", filename)
|
||||
filename = re.sub( "\[.*\]", "", filename)
|
||||
|
||||
# guess based on position
|
||||
|
||||
# replace any name seperators with spaces
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
word_list = tmpstr.split(' ')
|
||||
|
||||
#before we search, remove any kind of likely "of X" phrase
|
||||
for i in range(0, len(word_list)-2):
|
||||
if ( word_list[i].isdigit() and
|
||||
word_list[i+1] == "of" and
|
||||
word_list[i+2].isdigit() ):
|
||||
word_list[i+1] ="XXX"
|
||||
word_list[i+2] ="XXX"
|
||||
|
||||
|
||||
# first look for the last "#" followed by a digit in the filename. this is almost certainly the issue number
|
||||
#issnum = re.search('#\d+', filename)
|
||||
matchlist = re.findall("#\d+", filename)
|
||||
if len(matchlist) > 0:
|
||||
#get the last item
|
||||
issue = matchlist[ len(matchlist) - 1]
|
||||
issue = issue[1:]
|
||||
found = True
|
||||
|
||||
|
||||
# assume the last number in the filename that is under 4 digits is the issue number
|
||||
if not found:
|
||||
for word in reversed(word_list):
|
||||
if len(word) > 0 and word[0] == "#":
|
||||
word = word[1:]
|
||||
if (
|
||||
(word.isdigit() and len(word) < 4) or
|
||||
(self.isPointIssue(word))
|
||||
):
|
||||
issue = word
|
||||
found = True
|
||||
#print 'Assuming issue number is ' + str(issue) + ' based on the position.'
|
||||
break
|
||||
|
||||
if not found:
|
||||
# try a regex
|
||||
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
|
||||
if issnum:
|
||||
issue = issnum.group()
|
||||
found = True
|
||||
#print 'Got the issue using regex. Issue is ' + issue
|
||||
|
||||
return issue.strip()
|
||||
|
||||
def getSeriesName(self, filename, issue ):
|
||||
|
||||
# use the issue number string to split the filename string
|
||||
# assume first element of list is the series name, plus cruft
|
||||
#!!! this could fail in the case of small numerics in the series name!!!
|
||||
|
||||
# TODO: we really should pass in the *INDEX* of the issue, that makes
|
||||
# finding it easier
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
tmpstr = self.fixSpaces(filename, remove_dashes=False)
|
||||
|
||||
#remove pound signs. this might mess up the series name if there is a# in it.
|
||||
tmpstr = tmpstr.replace("#", " ")
|
||||
|
||||
if issue != "":
|
||||
# assume that issue substr has at least one space before it
|
||||
issue_str = " " + str(issue)
|
||||
series = tmpstr.split(issue_str)[0]
|
||||
else:
|
||||
# no issue to work off of
|
||||
#!!! TODO we should look for the year, and split from that
|
||||
# and if that doesn't exist, remove parenthetical phrases
|
||||
series = tmpstr
|
||||
series = re.sub( "\(.*\)", "", tmpstr)
|
||||
|
||||
volume = ""
|
||||
|
||||
series = series.rstrip("#")
|
||||
|
||||
# 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)
|
||||
|
||||
return series.strip(), volume.strip()
|
||||
|
||||
def getYear( self,filename):
|
||||
|
||||
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-numerics
|
||||
year = re.sub("[^0-9]", "", year)
|
||||
return year
|
||||
|
||||
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 decodings
|
||||
# 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", ")")
|
||||
|
||||
# ----HACK
|
||||
# remove the first word that word is a 3 digit number.
|
||||
# some story arcs collection packs do this, but it's ugly
|
||||
# this will probably break something, i.e. "100 bullets"
|
||||
#word = filename.split(' ')[0]
|
||||
#if len(word) == 3 and word[0] =='0' and word.isdigit():
|
||||
# filename = filename[4:]
|
||||
# ----HACK -
|
||||
|
||||
self.issue = self.getIssueNumber(filename)
|
||||
self.series, self.volume = self.getSeriesName(filename, self.issue)
|
||||
self.year = self.getYear(filename)
|
||||
self.issue_count = self.getIssueCount(filename)
|
||||
|
||||
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
|
||||
|
||||
from comicapi.filenameparser import *
|
||||
|
||||
@@ -1,140 +1,156 @@
|
||||
"""
|
||||
Functions for renaming files based on metadata
|
||||
"""
|
||||
"""Functions for renaming files based on metadata"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
import re
|
||||
import datetime
|
||||
|
||||
import utils
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class FileRenamer:
|
||||
def __init__( self, metadata ):
|
||||
self.setMetadata( metadata )
|
||||
self.setTemplate( "%series% v%volume% #%issue% (of %issuecount%) (%year%)" )
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
|
||||
def setMetadata( self, metadata ):
|
||||
self.metdata = metadata
|
||||
def __init__(self, metadata):
|
||||
self.setMetadata(metadata)
|
||||
self.setTemplate(
|
||||
"%series% v%volume% #%issue% (of %issuecount%) (%year%)")
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
|
||||
def setIssueZeroPadding( self, count ):
|
||||
self.issue_zero_padding = count
|
||||
def setMetadata(self, metadata):
|
||||
self.metdata = metadata
|
||||
|
||||
def setSmartCleanup( self, on ):
|
||||
self.smart_cleanup = on
|
||||
def setIssueZeroPadding(self, count):
|
||||
self.issue_zero_padding = count
|
||||
|
||||
def setTemplate( self, template ):
|
||||
self.template = template
|
||||
|
||||
def replaceToken( self, text, value, token ):
|
||||
#helper func
|
||||
def isToken( word ):
|
||||
return (word[0] == "%" and word[-1:] == "%")
|
||||
def setSmartCleanup(self, on):
|
||||
self.smart_cleanup = on
|
||||
|
||||
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 ):
|
||||
def setTemplate(self, template):
|
||||
self.template = template
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
preferred_encoding = utils.get_actual_preferred_encoding()
|
||||
def replaceToken(self, text, value, token):
|
||||
# helper func
|
||||
def isToken(word):
|
||||
return (word[0] == "%" and word[-1:] == "%")
|
||||
|
||||
#print u"{0}".format(md)
|
||||
|
||||
new_name = self.replaceToken( new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken( new_name, md.volume, '%volume%')
|
||||
|
||||
if md.issue is not None:
|
||||
issue_str = u"{0}".format( IssueString(md.issue).asString(pad=self.issue_zero_padding) )
|
||||
else:
|
||||
issue_str = None
|
||||
new_name = self.replaceToken( new_name, issue_str, '%issue%')
|
||||
|
||||
new_name = self.replaceToken( new_name, md.issueCount, '%issuecount%')
|
||||
new_name = self.replaceToken( new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken( new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken( new_name, md.title, '%title%')
|
||||
new_name = self.replaceToken( new_name, md.month, '%month%')
|
||||
month_name = None
|
||||
if md.month is not None:
|
||||
if (type(md.month) == str and md.month.isdigit()) or type(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 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()
|
||||
|
||||
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 )
|
||||
# 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] = ""
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]+\s+", "- ", new_name )
|
||||
new_name = re.sub("(\s-)+", " -", new_name )
|
||||
text_list = [x for x in text_list if token not in x]
|
||||
return " ".join(text_list)
|
||||
else:
|
||||
return text.replace(token, "")
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext( filename )[1]
|
||||
def determineName(self, filename, ext=None):
|
||||
|
||||
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("?", "")
|
||||
|
||||
return new_name
|
||||
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
preferred_encoding = utils.get_actual_preferred_encoding()
|
||||
|
||||
# print(u"{0}".format(md))
|
||||
|
||||
new_name = self.replaceToken(new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken(new_name, md.volume, '%volume%')
|
||||
|
||||
if md.issue is not None:
|
||||
issue_str = u"{0}".format(
|
||||
IssueString(md.issue).asString(pad=self.issue_zero_padding))
|
||||
else:
|
||||
issue_str = None
|
||||
new_name = self.replaceToken(new_name, issue_str, '%issue%')
|
||||
|
||||
new_name = self.replaceToken(new_name, md.issueCount, '%issuecount%')
|
||||
new_name = self.replaceToken(new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken(new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken(new_name, md.title, '%title%')
|
||||
new_name = self.replaceToken(new_name, md.month, '%month%')
|
||||
month_name = None
|
||||
if md.month is not None:
|
||||
if (isinstance(md.month, str) and md.month.isdigit()) or isinstance(
|
||||
md.month, int):
|
||||
if int(md.month) in range(1, 13):
|
||||
dt = datetime.datetime(1970, int(md.month), 1, 0, 0)
|
||||
month_name = dt.strftime(
|
||||
u"%B".encode(preferred_encoding)).decode(preferred_encoding)
|
||||
new_name = self.replaceToken(new_name, month_name, '%month_name%')
|
||||
|
||||
new_name = self.replaceToken(new_name, md.genre, '%genre%')
|
||||
new_name = self.replaceToken(new_name, md.language, '%language_code%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.criticalRating, '%criticalrating%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateSeries, '%alternateseries%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateNumber, '%alternatenumber%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateCount, '%alternatecount%')
|
||||
new_name = self.replaceToken(new_name, md.imprint, '%imprint%')
|
||||
new_name = self.replaceToken(new_name, md.format, '%format%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.maturityRating, '%maturityrating%')
|
||||
new_name = self.replaceToken(new_name, md.storyArc, '%storyarc%')
|
||||
new_name = self.replaceToken(new_name, md.seriesGroup, '%seriesgroup%')
|
||||
new_name = self.replaceToken(new_name, md.scanInfo, '%scaninfo%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
# remove empty braces,brackets, parentheses
|
||||
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name)
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name)
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
|
||||
new_name = re.sub("(\s--)+", " --", new_name)
|
||||
new_name = re.sub("(\s-)+", " -", new_name)
|
||||
|
||||
# remove dash or double dash at end of line
|
||||
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
|
||||
|
||||
# remove duplicate spaces (again!)
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
|
||||
new_name += ext
|
||||
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(" :", " -")
|
||||
new_name = new_name.replace(": ", " - ")
|
||||
new_name = new_name.replace(":", "-")
|
||||
new_name = new_name.replace("?", "")
|
||||
|
||||
return new_name
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
A PyQt4 widget for managing list of comic archive files
|
||||
"""
|
||||
"""A PyQt4 widget for managing list of comic archive files"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 platform
|
||||
import os
|
||||
import sys
|
||||
#import os
|
||||
#import sys
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
@@ -29,361 +27,430 @@ from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from comicarchive import MetaDataStyle
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from optionalmsgdialog import OptionalMessageDialog
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
|
||||
import utils
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(FileTableWidget, self).__init__(parent)
|
||||
|
||||
|
||||
self.setColumnCount(5)
|
||||
self.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
|
||||
self.horizontalHeader().setStretchLastSection( True )
|
||||
#from comicarchive import MetaDataStyle
|
||||
#from genericmetadata import GenericMetadata, PageType
|
||||
|
||||
|
||||
class FileTableWidgetItem(QTableWidgetItem):
|
||||
def __lt__(self, other):
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.data(Qt.UserRole).toBool() <
|
||||
other.data(Qt.UserRole).toBool())
|
||||
|
||||
|
||||
class FileInfo( ):
|
||||
def __init__(self, ca ):
|
||||
self.ca = ca
|
||||
class FileInfo():
|
||||
|
||||
def __init__(self, ca):
|
||||
self.ca = ca
|
||||
|
||||
|
||||
class FileSelectionList(QWidget):
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
listCleared = pyqtSignal()
|
||||
|
||||
fileColNum = 0
|
||||
CRFlagColNum = 1
|
||||
CBLFlagColNum = 2
|
||||
typeColNum = 3
|
||||
readonlyColNum = 4
|
||||
folderColNum = 5
|
||||
dataColNum = fileColNum
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
listCleared = pyqtSignal()
|
||||
|
||||
def __init__(self, parent , settings ):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
fileColNum = 0
|
||||
CRFlagColNum = 1
|
||||
CBLFlagColNum = 2
|
||||
typeColNum = 3
|
||||
readonlyColNum = 4
|
||||
folderColNum = 5
|
||||
dataColNum = fileColNum
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
def __init__(self, parent, settings):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
self.currentItem = None
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.modifiedFlag = False
|
||||
|
||||
selectAllAction = QAction("Select All", self)
|
||||
removeAction = QAction("Remove Selected Items", self)
|
||||
self.separator = QAction("",self)
|
||||
self.separator.setSeparator(True)
|
||||
|
||||
selectAllAction.setShortcut( 'Ctrl+A' )
|
||||
removeAction.setShortcut( 'Ctrl+X' )
|
||||
|
||||
selectAllAction.triggered.connect(self.selectAll)
|
||||
removeAction.triggered.connect(self.removeSelection)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui'), self)
|
||||
|
||||
self.addAction(selectAllAction)
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
self.settings = settings
|
||||
|
||||
def addAppAction( self, action ):
|
||||
self.insertAction( None , action )
|
||||
|
||||
def setModifiedFlag( self, modified ):
|
||||
self.modifiedFlag = modified
|
||||
|
||||
def selectAll( self ):
|
||||
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), True )
|
||||
reduceWidgetFontSize(self.twList)
|
||||
|
||||
def deselectAll( self ):
|
||||
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), False )
|
||||
self.twList.setColumnCount(6)
|
||||
#self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
|
||||
# self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
|
||||
|
||||
def removeArchiveList( self, ca_list ):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for ca in ca_list:
|
||||
for row in range(self.twList.rowCount()):
|
||||
row_ca = self.getArchiveByRow( row )
|
||||
if row_ca == ca:
|
||||
self.twList.removeRow(row)
|
||||
break
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def getArchiveByRow( self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data( Qt.UserRole ).toPyObject()
|
||||
return fi.ca
|
||||
|
||||
def getCurrentArchive( self ):
|
||||
return self.getArchiveByRow( self.twList.currentRow() )
|
||||
|
||||
def removeSelection( self ):
|
||||
row_list = []
|
||||
for item in self.twList.selectedItems():
|
||||
if item.column() == 0:
|
||||
row_list.append(item.row())
|
||||
self.currentItem = None
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.modifiedFlag = False
|
||||
|
||||
if len(row_list) == 0:
|
||||
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?"):
|
||||
return
|
||||
|
||||
row_list.sort()
|
||||
row_list.reverse()
|
||||
selectAllAction = QAction("Select All", self)
|
||||
removeAction = QAction("Remove Selected Items", self)
|
||||
self.separator = QAction("", self)
|
||||
self.separator.setSeparator(True)
|
||||
|
||||
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
|
||||
self.twList.setSortingEnabled(False)
|
||||
selectAllAction.setShortcut('Ctrl+A')
|
||||
removeAction.setShortcut('Ctrl+X')
|
||||
|
||||
for i in row_list:
|
||||
self.twList.removeRow(i)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
|
||||
def addPathList( self, pathlist ):
|
||||
|
||||
filelist = utils.get_recursive_filelist( pathlist )
|
||||
|
||||
# we now have a list of files to add
|
||||
selectAllAction.triggered.connect(self.selectAll)
|
||||
removeAction.triggered.connect(self.removeSelection)
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle( "Adding Files" )
|
||||
#progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.setWindowModality(Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
|
||||
firstAdded = None
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx,f in enumerate(filelist):
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem( f )
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
progdialog.close()
|
||||
if firstAdded is not None:
|
||||
self.twList.selectRow(firstAdded)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
# Adjust column size
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setColumnWidth(FileSelectionList.CRFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.CBLFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.readonlyColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.typeColNum, 45)
|
||||
if self.twList.columnWidth(FileSelectionList.fileColNum) > 250:
|
||||
self.twList.setColumnWidth(FileSelectionList.fileColNum, 250)
|
||||
if self.twList.columnWidth(FileSelectionList.folderColNum ) > 200:
|
||||
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
|
||||
self.addAction(selectAllAction)
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
|
||||
def isListDupe( self, path ):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.getArchiveByRow( r )
|
||||
if ca.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def addPathItem( self, path):
|
||||
path = unicode( path )
|
||||
path = os.path.abspath( path )
|
||||
#print "processing", path
|
||||
|
||||
if self.isListDupe(path):
|
||||
return None
|
||||
|
||||
ca = ComicArchive( path, self.settings )
|
||||
|
||||
if ca.seemsToBeAComicArchive() :
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
|
||||
fi = FileInfo( ca )
|
||||
|
||||
filename_item = QTableWidgetItem()
|
||||
folder_item = QTableWidgetItem()
|
||||
cix_item = FileTableWidgetItem()
|
||||
cbi_item = FileTableWidgetItem()
|
||||
readonly_item = FileTableWidgetItem()
|
||||
type_item = QTableWidgetItem()
|
||||
|
||||
filename_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
filename_item.setData( Qt.UserRole , fi )
|
||||
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
|
||||
|
||||
folder_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
|
||||
def getSorting(self):
|
||||
col = self.twList.horizontalHeader().sortIndicatorSection()
|
||||
order = self.twList.horizontalHeader().sortIndicatorOrder()
|
||||
return col, order
|
||||
|
||||
type_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
|
||||
def setSorting(self, col, order):
|
||||
col = self.twList.horizontalHeader().setSortIndicator(col, order)
|
||||
|
||||
cix_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
|
||||
def addAppAction(self, action):
|
||||
self.insertAction(None, action)
|
||||
|
||||
cbi_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
|
||||
def setModifiedFlag(self, modified):
|
||||
self.modifiedFlag = modified
|
||||
|
||||
readonly_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
readonly_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
|
||||
|
||||
self.updateRow( row )
|
||||
|
||||
return row
|
||||
def selectAll(self):
|
||||
self.twList.setRangeSelected(
|
||||
QTableWidgetSelectionRange(
|
||||
0,
|
||||
0,
|
||||
self.twList.rowCount() -
|
||||
1,
|
||||
5),
|
||||
True)
|
||||
|
||||
def updateRow( self, row ):
|
||||
fi = self.twList.item( row, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
|
||||
def deselectAll(self):
|
||||
self.twList.setRangeSelected(
|
||||
QTableWidgetSelectionRange(
|
||||
0,
|
||||
0,
|
||||
self.twList.rowCount() -
|
||||
1,
|
||||
5),
|
||||
False)
|
||||
|
||||
filename_item = self.twList.item( row, FileSelectionList.fileColNum )
|
||||
folder_item = self.twList.item( row, FileSelectionList.folderColNum )
|
||||
cix_item = self.twList.item( row, FileSelectionList.CRFlagColNum )
|
||||
cbi_item = self.twList.item( row, FileSelectionList.CBLFlagColNum )
|
||||
type_item = self.twList.item( row, FileSelectionList.typeColNum )
|
||||
readonly_item = self.twList.item( row, FileSelectionList.readonlyColNum )
|
||||
def removeArchiveList(self, ca_list):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for ca in ca_list:
|
||||
for row in range(self.twList.rowCount()):
|
||||
row_ca = self.getArchiveByRow(row)
|
||||
if row_ca == ca:
|
||||
self.twList.removeRow(row)
|
||||
break
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[0]
|
||||
folder_item.setText( item_text )
|
||||
folder_item.setData( Qt.ToolTipRole, item_text )
|
||||
def getArchiveByRow(self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
|
||||
Qt.UserRole).toPyObject()
|
||||
return fi.ca
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[1]
|
||||
filename_item.setText( item_text )
|
||||
filename_item.setData( Qt.ToolTipRole, item_text )
|
||||
def getCurrentArchive(self):
|
||||
return self.getArchiveByRow(self.twList.currentRow())
|
||||
|
||||
if fi.ca.isZip():
|
||||
item_text = "ZIP"
|
||||
elif fi.ca.isRar():
|
||||
item_text = "RAR"
|
||||
else:
|
||||
item_text = ""
|
||||
type_item.setText( item_text )
|
||||
type_item.setData( Qt.ToolTipRole, item_text )
|
||||
def removeSelection(self):
|
||||
row_list = []
|
||||
for item in self.twList.selectedItems():
|
||||
if item.column() == 0:
|
||||
row_list.append(item.row())
|
||||
|
||||
if len(row_list) == 0:
|
||||
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?"):
|
||||
return
|
||||
|
||||
row_list.sort()
|
||||
row_list.reverse()
|
||||
|
||||
self.twList.currentItemChanged.disconnect(self.currentItemChangedCB)
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for i in row_list:
|
||||
self.twList.removeRow(i)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
# since on a removal, we select row 0, make sure callback occurs if
|
||||
# we're already there
|
||||
if self.twList.currentRow() == 0:
|
||||
self.currentItemChangedCB(self.twList.currentItem(), None)
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
|
||||
def addPathList(self, pathlist):
|
||||
|
||||
filelist = utils.get_recursive_filelist(pathlist)
|
||||
# we now have a list of files to add
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle("Adding Files")
|
||||
# progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.setWindowModality(Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
|
||||
firstAdded = None
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx, f in enumerate(filelist):
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
centerWindowOnParent(progdialog)
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem(f)
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
progdialog.close()
|
||||
if (self.settings.show_no_unrar_warning and
|
||||
self.settings.unrar_exe_path == "" and
|
||||
self.settings.rar_exe_path == "" and
|
||||
platform.system() != "Windows"):
|
||||
for f in filelist:
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
if ext == ".rar" or ext == ".cbr":
|
||||
checked = OptionalMessageDialog.msg(self, "No unrar tool",
|
||||
"""
|
||||
It looks like you've tried to open at least one CBR or RAR file.<br><br>
|
||||
In order for ComicTagger to read this kind of file, you will have to configure
|
||||
the location of the unrar tool in the settings. Until then, ComicTagger
|
||||
will not be able recognize these kind of files.
|
||||
"""
|
||||
)
|
||||
self.settings.show_no_unrar_warning = not checked
|
||||
break
|
||||
|
||||
if firstAdded is not None:
|
||||
self.twList.selectRow(firstAdded)
|
||||
else:
|
||||
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
|
||||
QMessageBox.information(self, self.tr("File Open"), self.tr(
|
||||
"Selected file doesn't seem to be a comic archive."))
|
||||
else:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self.tr("File/Folder Open"),
|
||||
self.tr("No comic archives were found."))
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
# Adjust column size
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setColumnWidth(FileSelectionList.CRFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.CBLFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.readonlyColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.typeColNum, 45)
|
||||
if self.twList.columnWidth(FileSelectionList.fileColNum) > 250:
|
||||
self.twList.setColumnWidth(FileSelectionList.fileColNum, 250)
|
||||
if self.twList.columnWidth(FileSelectionList.folderColNum) > 200:
|
||||
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
|
||||
|
||||
def isListDupe(self, path):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.getArchiveByRow(r)
|
||||
if ca.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def getCurrentListRow(self, path):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.getArchiveByRow(r)
|
||||
if ca.path == path:
|
||||
return r
|
||||
r = r + 1
|
||||
|
||||
return -1
|
||||
|
||||
def addPathItem(self, 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'))
|
||||
|
||||
if ca.seemsToBeAComicArchive():
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow(row)
|
||||
|
||||
fi = FileInfo(ca)
|
||||
|
||||
filename_item = QTableWidgetItem()
|
||||
folder_item = QTableWidgetItem()
|
||||
cix_item = FileTableWidgetItem()
|
||||
cbi_item = FileTableWidgetItem()
|
||||
readonly_item = FileTableWidgetItem()
|
||||
type_item = QTableWidgetItem()
|
||||
|
||||
filename_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
filename_item.setData(Qt.UserRole, fi)
|
||||
self.twList.setItem(
|
||||
row, FileSelectionList.fileColNum, filename_item)
|
||||
|
||||
folder_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.twList.setItem(
|
||||
row, FileSelectionList.folderColNum, folder_item)
|
||||
|
||||
type_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
|
||||
|
||||
cix_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
|
||||
|
||||
cbi_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
|
||||
|
||||
readonly_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
readonly_item.setTextAlignment(Qt.AlignHCenter)
|
||||
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()
|
||||
|
||||
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
|
||||
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
|
||||
cix_item = self.twList.item(row, FileSelectionList.CRFlagColNum)
|
||||
cbi_item = self.twList.item(row, FileSelectionList.CBLFlagColNum)
|
||||
type_item = self.twList.item(row, FileSelectionList.typeColNum)
|
||||
readonly_item = self.twList.item(row, FileSelectionList.readonlyColNum)
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[0]
|
||||
folder_item.setText(item_text)
|
||||
folder_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[1]
|
||||
filename_item.setText(item_text)
|
||||
filename_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
if fi.ca.isZip():
|
||||
item_text = "ZIP"
|
||||
elif fi.ca.isRar():
|
||||
item_text = "RAR"
|
||||
else:
|
||||
item_text = ""
|
||||
type_item.setText(item_text)
|
||||
type_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
if fi.ca.hasCIX():
|
||||
cix_item.setCheckState(Qt.Checked)
|
||||
cix_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cix_item.setData(Qt.UserRole, False)
|
||||
cix_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if fi.ca.hasCBI():
|
||||
cbi_item.setCheckState(Qt.Checked)
|
||||
cbi_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cbi_item.setData(Qt.UserRole, False)
|
||||
cbi_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if not fi.ca.isWritable():
|
||||
readonly_item.setCheckState(Qt.Checked)
|
||||
readonly_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
readonly_item.setData(Qt.UserRole, False)
|
||||
readonly_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
# Reading these will force them into the ComicArchive's cache
|
||||
fi.ca.readCIX()
|
||||
fi.ca.hasCBI()
|
||||
|
||||
def getSelectedArchiveList(self):
|
||||
ca_list = []
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
fi = item.data(Qt.UserRole).toPyObject()
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
return ca_list
|
||||
|
||||
def updateCurrentRow(self):
|
||||
self.updateRow(self.twList.currentRow())
|
||||
|
||||
def updateSelectedRows(self):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
self.updateRow(r)
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def currentItemChangedCB(self, curr, prev):
|
||||
|
||||
new_idx = curr.row()
|
||||
old_idx = -1
|
||||
if prev is not None:
|
||||
old_idx = prev.row()
|
||||
#print("old {0} new {1}".format(old_idx, new_idx))
|
||||
|
||||
if old_idx == new_idx:
|
||||
return
|
||||
|
||||
# don't allow change if modified
|
||||
if prev is not None and new_idx != old_idx:
|
||||
if not self.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)
|
||||
# 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()
|
||||
self.selectionChanged.emit(QVariant(fi))
|
||||
|
||||
def revertSelection(self):
|
||||
self.twList.selectRow(self.twList.currentRow())
|
||||
|
||||
def modifiedFlagVerification(self, title, desc):
|
||||
if self.modifiedFlag:
|
||||
reply = QMessageBox.question(self,
|
||||
self.tr(title),
|
||||
self.tr(desc),
|
||||
QMessageBox.Yes, QMessageBox.No)
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if fi.ca.hasCIX():
|
||||
cix_item.setCheckState(Qt.Checked)
|
||||
cix_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cix_item.setData(Qt.UserRole, False)
|
||||
cix_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if fi.ca.hasCBI():
|
||||
cbi_item.setCheckState(Qt.Checked)
|
||||
cbi_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cbi_item.setData(Qt.UserRole, False)
|
||||
cbi_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if not fi.ca.isWritable():
|
||||
readonly_item.setCheckState(Qt.Checked)
|
||||
readonly_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
readonly_item.setData(Qt.UserRole, False)
|
||||
readonly_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
|
||||
# Reading these will force them into the ComicArchive's cache
|
||||
fi.ca.readCIX()
|
||||
fi.ca.hasCBI()
|
||||
|
||||
def getSelectedArchiveList( self ):
|
||||
ca_list = []
|
||||
for r in range( self.twList.rowCount() ):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
fi = item.data( Qt.UserRole ).toPyObject()
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
return ca_list
|
||||
|
||||
def updateCurrentRow( self ):
|
||||
self.updateRow( self.twList.currentRow() )
|
||||
|
||||
def updateSelectedRows( self ):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for r in range( self.twList.rowCount() ):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
self.updateRow( r )
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def currentItemChangedCB( self, curr, prev ):
|
||||
|
||||
new_idx = curr.row()
|
||||
old_idx = -1
|
||||
if prev is not None:
|
||||
old_idx = prev.row()
|
||||
#print "old {0} new {1}".format(old_idx, new_idx)
|
||||
|
||||
if old_idx == new_idx:
|
||||
return
|
||||
|
||||
# don't allow change if modified
|
||||
if prev is not None and new_idx != old_idx:
|
||||
if not self.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 )
|
||||
# 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()
|
||||
self.selectionChanged.emit( QVariant(fi))
|
||||
|
||||
def revertSelection( self ):
|
||||
self.twList.selectRow( self.twList.currentRow() )
|
||||
|
||||
|
||||
def modifiedFlagVerification( self, title, desc):
|
||||
if self.modifiedFlag:
|
||||
reply = QMessageBox.question(self,
|
||||
self.tr(title),
|
||||
self.tr(desc),
|
||||
QMessageBox.Yes, QMessageBox.No )
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Attempt to use a special checkbox widget in the cell.
|
||||
# Couldn't figure out how to disable it with "enabled" colors
|
||||
#w = QWidget()
|
||||
#cb = QCheckBox(w)
|
||||
#cb.setCheckState(Qt.Checked)
|
||||
# cb.setCheckState(Qt.Checked)
|
||||
#layout = QHBoxLayout()
|
||||
#layout.addWidget( cb )
|
||||
#layout.setAlignment(Qt.AlignHCenter)
|
||||
#layout.setMargin(2)
|
||||
#w.setLayout(layout)
|
||||
#self.twList.setCellWidget( row, 2, w )
|
||||
# layout.addWidget(cb)
|
||||
# layout.setAlignment(Qt.AlignHCenter)
|
||||
# layout.setMargin(2)
|
||||
# w.setLayout(layout)
|
||||
#self.twList.setCellWidget(row, 2, w)
|
||||
|
||||
@@ -1,316 +1 @@
|
||||
"""
|
||||
A python 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 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
|
||||
|
||||
# These page info classes are exactly the same as the CIX scheme, since it's unique
|
||||
class PageType:
|
||||
FrontCover = "FrontCover"
|
||||
InnerCover = "InnerCover"
|
||||
Roundup = "Roundup"
|
||||
Story = "Story"
|
||||
Advertisment = "Advertisment"
|
||||
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 type(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 c.has_key('primary') 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 c.has_key('primary') 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
|
||||
from comicapi.genericmetadata import *
|
||||
|
||||
@@ -1,198 +1,191 @@
|
||||
"""
|
||||
A python class to manage fetching and caching of images by URL
|
||||
"""
|
||||
"""A class to manage fetching and caching of images by URL"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sqlite3 as lite
|
||||
import os
|
||||
import datetime
|
||||
import shutil
|
||||
import tempfile
|
||||
import urllib2, urllib
|
||||
import urllib
|
||||
#import urllib2
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
from PyQt4 import QtGui
|
||||
try:
|
||||
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():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
class QByteArray():
|
||||
pass
|
||||
class pyqtSignal():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
def emit(a,b,c):
|
||||
pass
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
class QByteArray():
|
||||
pass
|
||||
|
||||
class pyqtSignal():
|
||||
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
def emit(a, b, c):
|
||||
pass
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class ImageFetcherException(Exception):
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
class ImageFetcher(QObject):
|
||||
|
||||
fetchComplete = pyqtSignal( QByteArray , int)
|
||||
|
||||
fetchComplete = pyqtSignal(QByteArray, int)
|
||||
|
||||
def __init__(self ):
|
||||
QObject.__init__(self)
|
||||
def __init__(self):
|
||||
QObject.__init__(self)
|
||||
|
||||
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
|
||||
self.db_file = os.path.join( self.settings_folder, "image_url_cache.db" )
|
||||
self.cache_folder = os.path.join( self.settings_folder, "image_cache" )
|
||||
|
||||
if not os.path.exists( self.db_file ):
|
||||
self.create_image_db()
|
||||
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
|
||||
self.db_file = os.path.join(self.settings_folder, "image_url_cache.db")
|
||||
self.cache_folder = os.path.join(self.settings_folder, "image_cache")
|
||||
|
||||
def clearCache( self ):
|
||||
os.unlink( self.db_file )
|
||||
if os.path.isdir( self.cache_folder ):
|
||||
shutil.rmtree( self.cache_folder )
|
||||
if not os.path.exists(self.db_file):
|
||||
self.create_image_db()
|
||||
|
||||
def clearCache(self):
|
||||
os.unlink(self.db_file)
|
||||
if os.path.isdir(self.cache_folder):
|
||||
shutil.rmtree(self.cache_folder)
|
||||
|
||||
def fetch( self, url, user_data=None, blocking=False ):
|
||||
"""
|
||||
If called with blocking=True, this will block until the image is
|
||||
fetched.
|
||||
|
||||
If called with blocking=False, this will run the fetch in the
|
||||
background, and emit a signal when done
|
||||
"""
|
||||
def fetch(self, url, user_data=None, blocking=False):
|
||||
"""
|
||||
If called with blocking=True, this will block until the image is
|
||||
fetched.
|
||||
If called with blocking=False, this will run the fetch in the
|
||||
background, and emit a signal when done
|
||||
"""
|
||||
|
||||
self.user_data = user_data
|
||||
self.fetched_url = url
|
||||
|
||||
# first look in the DB
|
||||
image_data = self.get_image_from_cache( url )
|
||||
|
||||
if blocking:
|
||||
if image_data is None:
|
||||
try:
|
||||
image_data = urllib.urlopen(url).read()
|
||||
except Exception as e:
|
||||
print e
|
||||
raise ImageFetcherException("Network Error!")
|
||||
self.user_data = user_data
|
||||
self.fetched_url = url
|
||||
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache( self.fetched_url, image_data )
|
||||
return image_data
|
||||
|
||||
else:
|
||||
|
||||
# if we found it, just emit the signal asap
|
||||
if image_data is not None:
|
||||
self.fetchComplete.emit( QByteArray(image_data), self.user_data )
|
||||
return
|
||||
|
||||
# didn't find it. look online
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.finishRequest)
|
||||
self.nam.get(QNetworkRequest(QUrl(url)))
|
||||
|
||||
#we'll get called back when done...
|
||||
|
||||
|
||||
# first look in the DB
|
||||
image_data = self.get_image_from_cache(url)
|
||||
|
||||
def finishRequest(self, reply):
|
||||
|
||||
# read in the image data
|
||||
image_data = reply.readAll()
|
||||
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache( self.fetched_url, image_data )
|
||||
if blocking:
|
||||
if image_data is None:
|
||||
try:
|
||||
image_data = urllib.urlopen(url).read()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise ImageFetcherException("Network Error!")
|
||||
|
||||
self.fetchComplete.emit( QByteArray(image_data), self.user_data )
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
return image_data
|
||||
|
||||
|
||||
|
||||
def create_image_db( self ):
|
||||
|
||||
# this will wipe out any existing version
|
||||
open( self.db_file, 'w').close()
|
||||
else:
|
||||
|
||||
# wipe any existing image cache folder too
|
||||
if os.path.isdir( self.cache_folder ):
|
||||
shutil.rmtree( self.cache_folder )
|
||||
os.makedirs( self.cache_folder )
|
||||
# if we found it, just emit the signal asap
|
||||
if image_data is not None:
|
||||
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
|
||||
return
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
# create tables
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
# didn't find it. look online
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.finishRequest)
|
||||
self.nam.get(QNetworkRequest(QUrl(url)))
|
||||
|
||||
cur.execute("CREATE TABLE Images(" +
|
||||
"url TEXT," +
|
||||
"filename TEXT," +
|
||||
"timestamp TEXT," +
|
||||
"PRIMARY KEY (url) )"
|
||||
)
|
||||
# we'll get called back when done...
|
||||
|
||||
def finishRequest(self, reply):
|
||||
|
||||
def add_image_to_cache( self, url, image_data ):
|
||||
# read in the image data
|
||||
image_data = reply.readAll()
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
# save the image to the cache
|
||||
self.add_image_to_cache(self.fetched_url, image_data)
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
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 )
|
||||
)
|
||||
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
|
||||
|
||||
def get_image_from_cache( self, url ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
|
||||
cur.execute("SELECT filename FROM Images WHERE url=?", [ url ])
|
||||
row = cur.fetchone()
|
||||
def create_image_db(self):
|
||||
|
||||
if row is None :
|
||||
return None
|
||||
else:
|
||||
filename = row[0]
|
||||
image_data = None
|
||||
# this will wipe out any existing version
|
||||
open(self.db_file, 'w').close()
|
||||
|
||||
try:
|
||||
with open( filename, 'rb' ) as f:
|
||||
image_data = f.read()
|
||||
f.close()
|
||||
except IOError as e:
|
||||
pass
|
||||
|
||||
return image_data
|
||||
# wipe any existing image cache folder too
|
||||
if os.path.isdir(self.cache_folder):
|
||||
shutil.rmtree(self.cache_folder)
|
||||
os.makedirs(self.cache_folder)
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
# create tables
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
cur.execute("CREATE TABLE Images(" +
|
||||
"url TEXT," +
|
||||
"filename TEXT," +
|
||||
"timestamp TEXT," +
|
||||
"PRIMARY KEY (url))"
|
||||
)
|
||||
|
||||
def add_image_to_cache(self, url, image_data):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
tmp_fd, filename = tempfile.mkstemp(
|
||||
dir=self.cache_folder, prefix="img")
|
||||
f = os.fdopen(tmp_fd, 'w+b')
|
||||
f.write(image_data)
|
||||
f.close()
|
||||
|
||||
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)",
|
||||
(url,
|
||||
filename,
|
||||
timestamp)
|
||||
)
|
||||
|
||||
def get_image_from_cache(self, url):
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
|
||||
cur.execute("SELECT filename FROM Images WHERE url=?", [url])
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
else:
|
||||
filename = row[0]
|
||||
image_data = None
|
||||
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
image_data = f.read()
|
||||
f.close()
|
||||
except IOError as e:
|
||||
pass
|
||||
|
||||
return image_data
|
||||
|
||||
@@ -1,198 +1,193 @@
|
||||
"""
|
||||
A pthyon class to manage creating image content hashes, and calculate hamming distances
|
||||
"""
|
||||
"""A class to manage creating image content hashes, and calculate hamming distances"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 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
|
||||
# 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
|
||||
# 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.
|
||||
|
||||
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 StringIO
|
||||
import sys
|
||||
from functools import reduce
|
||||
|
||||
try:
|
||||
import Image
|
||||
pil_available = True
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
pil_available = False
|
||||
|
||||
|
||||
class ImageHasher(object):
|
||||
def __init__(self, path=None, data=None, width=8, height=8):
|
||||
#self.hash_size = size
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
if path is None and data is None:
|
||||
raise IOError
|
||||
else:
|
||||
try:
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
except:
|
||||
print "Image data seems corrupted!"
|
||||
# just generate a bogus image
|
||||
self.image = Image.new( "L", (1,1))
|
||||
def __init__(self, path=None, data=None, width=8, height=8):
|
||||
#self.hash_size = size
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def average_hash(self):
|
||||
try:
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "average_hash error:", e
|
||||
return long(0)
|
||||
|
||||
pixels = list(image.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
|
||||
def compare_value_to_avg(i):
|
||||
return ( 1 if i > avg else 0 )
|
||||
|
||||
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) ):
|
||||
return (x | (val << idx))
|
||||
|
||||
result = reduce(set_bit, enumerate(bitlist), 0)
|
||||
|
||||
#print "{0:016x}".format(result)
|
||||
return result
|
||||
if path is None and data is None:
|
||||
raise IOError
|
||||
else:
|
||||
try:
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
except:
|
||||
print("Image data seems corrupted!")
|
||||
# just generate a bogus image
|
||||
self.image = Image.new("L", (1, 1))
|
||||
|
||||
def average_hash2( self ):
|
||||
pass
|
||||
"""
|
||||
# Got this one from somewhere on the net. Not a clue how the 'convolve2d'
|
||||
# works!
|
||||
def average_hash(self):
|
||||
try:
|
||||
image = self.image.resize(
|
||||
(self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "average_hash error:", e
|
||||
return long(0)
|
||||
|
||||
from numpy import array
|
||||
from scipy.signal import convolve2d
|
||||
|
||||
im = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert('L')
|
||||
pixels = list(image.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
|
||||
in_data = array((im.getdata())).reshape(self.width, self.height)
|
||||
filt = array([[0,1,0],[1,-4,1],[0,1,0]])
|
||||
filt_data = convolve2d(in_data,filt,mode='same',boundary='symm').flatten()
|
||||
|
||||
result = reduce(lambda x, (y, z): x | (z << y),
|
||||
enumerate(map(lambda i: 0 if i < 0 else 1, filt_data)),
|
||||
0)
|
||||
#print "{0:016x}".format(result)
|
||||
return result
|
||||
"""
|
||||
|
||||
def dct_average_hash(self):
|
||||
pass
|
||||
"""
|
||||
# Algorithm source: http://syntaxcandy.blogspot.com/2012/08/perceptual-hash.html
|
||||
|
||||
1. Reduce size. Like Average Hash, pHash starts with a small image.
|
||||
However, the image is larger than 8x8; 32x32 is a good size. This
|
||||
is really done to simplify the DCT computation and not because it
|
||||
is needed to reduce the high frequencies.
|
||||
def compare_value_to_avg(i):
|
||||
return (1 if i > avg else 0)
|
||||
|
||||
2. Reduce color. The image is reduced to a grayscale just to further
|
||||
simplify the number of computations.
|
||||
|
||||
3. Compute the DCT. The DCT separates the image into a collection of
|
||||
frequencies and scalars. While JPEG uses an 8x8 DCT, this algorithm
|
||||
uses a 32x32 DCT.
|
||||
|
||||
4. Reduce the DCT. This is the magic step. While the DCT is 32x32,
|
||||
just keep the top-left 8x8. Those represent the lowest frequencies in
|
||||
the picture.
|
||||
|
||||
5. Compute the average value. Like the Average Hash, compute the mean DCT
|
||||
value (using only the 8x8 DCT low-frequency values and excluding the first
|
||||
term since the DC coefficient can be significantly different from the other
|
||||
values and will throw off the average). Thanks to David Starkweather for the
|
||||
added information about pHash. He wrote: "the dct hash is based on the low 2D
|
||||
DCT coefficients starting at the second from lowest, leaving out the first DC
|
||||
term. This excludes completely flat image information (i.e. solid colors) from
|
||||
being included in the hash description."
|
||||
|
||||
6. Further reduce the DCT. This is the magic step. Set the 64 hash bits to 0 or
|
||||
1 depending on whether each of the 64 DCT values is above or below the average
|
||||
value. The result doesn't tell us the actual low frequencies; it just tells us
|
||||
the very-rough relative scale of the frequencies to the mean. The result will not
|
||||
vary as long as the overall structure of the image remains the same; this can
|
||||
survive gamma and color histogram adjustments without a problem.
|
||||
|
||||
7. Construct the hash. Set the 64 bits into a 64-bit integer. The order does not
|
||||
matter, just as long as you are consistent.
|
||||
"""
|
||||
"""
|
||||
import numpy
|
||||
import scipy.fftpack
|
||||
numpy.set_printoptions(threshold=10000, linewidth=200, precision=2, suppress=True)
|
||||
bitlist = map(compare_value_to_avg, pixels)
|
||||
|
||||
# Step 1,2
|
||||
im = self.image.resize((32, 32), Image.ANTIALIAS).convert("L")
|
||||
in_data = numpy.asarray(im)
|
||||
|
||||
# Step 3
|
||||
dct = scipy.fftpack.dct( in_data.astype(float) )
|
||||
|
||||
# Step 4
|
||||
# Just skip the top and left rows when slicing, as suggested somewhere else...
|
||||
lofreq_dct = dct[1:9, 1:9].flatten()
|
||||
|
||||
# Step 5
|
||||
avg = ( lofreq_dct.sum() ) / ( lofreq_dct.size )
|
||||
median = numpy.median( lofreq_dct )
|
||||
# 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))
|
||||
|
||||
thresh = avg
|
||||
result = reduce(set_bit, enumerate(bitlist), 0)
|
||||
|
||||
# Step 6
|
||||
def compare_value_to_thresh(i):
|
||||
return ( 1 if i > thresh else 0 )
|
||||
|
||||
bitlist = map(compare_value_to_thresh, lofreq_dct)
|
||||
|
||||
#Step 7
|
||||
def set_bit( x, (idx, val) ):
|
||||
return (x | (val << idx))
|
||||
|
||||
result = reduce(set_bit, enumerate(bitlist), long(0))
|
||||
|
||||
|
||||
#print "{0:016x}".format(result)
|
||||
return result
|
||||
"""
|
||||
|
||||
#accepts 2 hashes (longs or hex strings) and returns the hamming distance
|
||||
|
||||
@staticmethod
|
||||
def hamming_distance(h1, h2):
|
||||
if type(h1) == long or type(h1) == int:
|
||||
n1 = h1
|
||||
n2 = h2
|
||||
else:
|
||||
# convert hex strings to ints
|
||||
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:] )
|
||||
# print("{0:016x}".format(result))
|
||||
return result
|
||||
|
||||
def average_hash2(self):
|
||||
pass
|
||||
"""
|
||||
# Got this one from somewhere on the net. Not a clue how the 'convolve2d'
|
||||
# works!
|
||||
|
||||
from numpy import array
|
||||
from scipy.signal import convolve2d
|
||||
|
||||
im = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert('L')
|
||||
|
||||
in_data = array((im.getdata())).reshape(self.width, self.height)
|
||||
filt = array([[0,1,0],[1,-4,1],[0,1,0]])
|
||||
filt_data = convolve2d(in_data,filt,mode='same',boundary='symm').flatten()
|
||||
|
||||
result = reduce(lambda x, (y, z): x | (z << y),
|
||||
enumerate(map(lambda i: 0 if i < 0 else 1, filt_data)),
|
||||
0)
|
||||
#print("{0:016x}".format(result))
|
||||
return result
|
||||
"""
|
||||
|
||||
def dct_average_hash(self):
|
||||
pass
|
||||
"""
|
||||
# Algorithm source: http://syntaxcandy.blogspot.com/2012/08/perceptual-hash.html
|
||||
|
||||
1. Reduce size. Like Average Hash, pHash starts with a small image.
|
||||
However, the image is larger than 8x8; 32x32 is a good size. This
|
||||
is really done to simplify the DCT computation and not because it
|
||||
is needed to reduce the high frequencies.
|
||||
|
||||
2. Reduce color. The image is reduced to a grayscale just to further
|
||||
simplify the number of computations.
|
||||
|
||||
3. Compute the DCT. The DCT separates the image into a collection of
|
||||
frequencies and scalars. While JPEG uses an 8x8 DCT, this algorithm
|
||||
uses a 32x32 DCT.
|
||||
|
||||
4. Reduce the DCT. This is the magic step. While the DCT is 32x32,
|
||||
just keep the top-left 8x8. Those represent the lowest frequencies in
|
||||
the picture.
|
||||
|
||||
5. Compute the average value. Like the Average Hash, compute the mean DCT
|
||||
value (using only the 8x8 DCT low-frequency values and excluding the first
|
||||
term since the DC coefficient can be significantly different from the other
|
||||
values and will throw off the average). Thanks to David Starkweather for the
|
||||
added information about pHash. He wrote: "the dct hash is based on the low 2D
|
||||
DCT coefficients starting at the second from lowest, leaving out the first DC
|
||||
term. This excludes completely flat image information (i.e. solid colors) from
|
||||
being included in the hash description."
|
||||
|
||||
6. Further reduce the DCT. This is the magic step. Set the 64 hash bits to 0 or
|
||||
1 depending on whether each of the 64 DCT values is above or below the average
|
||||
value. The result doesn't tell us the actual low frequencies; it just tells us
|
||||
the very-rough relative scale of the frequencies to the mean. The result will not
|
||||
vary as long as the overall structure of the image remains the same; this can
|
||||
survive gamma and color histogram adjustments without a problem.
|
||||
|
||||
7. Construct the hash. Set the 64 bits into a 64-bit integer. The order does not
|
||||
matter, just as long as you are consistent.
|
||||
"""
|
||||
"""
|
||||
import numpy
|
||||
import scipy.fftpack
|
||||
numpy.set_printoptions(threshold=10000, linewidth=200, precision=2, suppress=True)
|
||||
|
||||
# Step 1,2
|
||||
im = self.image.resize((32, 32), Image.ANTIALIAS).convert("L")
|
||||
in_data = numpy.asarray(im)
|
||||
|
||||
# Step 3
|
||||
dct = scipy.fftpack.dct(in_data.astype(float))
|
||||
|
||||
# Step 4
|
||||
# Just skip the top and left rows when slicing, as suggested somewhere else...
|
||||
lofreq_dct = dct[1:9, 1:9].flatten()
|
||||
|
||||
# Step 5
|
||||
avg = (lofreq_dct.sum()) / (lofreq_dct.size)
|
||||
median = numpy.median(lofreq_dct)
|
||||
|
||||
thresh = avg
|
||||
|
||||
# Step 6
|
||||
def compare_value_to_thresh(i):
|
||||
return (1 if i > thresh else 0)
|
||||
|
||||
bitlist = map(compare_value_to_thresh, lofreq_dct)
|
||||
|
||||
#Step 7
|
||||
def set_bit(x, (idx, val)):
|
||||
return (x | (val << idx))
|
||||
|
||||
result = reduce(set_bit, enumerate(bitlist), long(0))
|
||||
|
||||
|
||||
#print("{0:016x}".format(result))
|
||||
return result
|
||||
"""
|
||||
|
||||
# accepts 2 hashes (longs or hex strings) and returns the hamming distance
|
||||
|
||||
@staticmethod
|
||||
def hamming_distance(h1, h2):
|
||||
if isinstance(h1, long) or isinstance(h1, int):
|
||||
n1 = h1
|
||||
n2 = h2
|
||||
else:
|
||||
# convert hex strings to ints
|
||||
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:])
|
||||
|
||||
@@ -1,86 +1,92 @@
|
||||
"""
|
||||
A PyQT4 widget to display a popup image
|
||||
"""
|
||||
"""A PyQT4 widget to display a popup image"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class ImagePopup(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui' ), self)
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
#self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
self.setWindowState(QtCore.Qt.WindowFullScreen)
|
||||
|
||||
self.imagePixmap = image_pixmap
|
||||
|
||||
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
|
||||
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())
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui'), self)
|
||||
|
||||
self.applyImagePixmap()
|
||||
self.showFullScreen()
|
||||
self.raise_( )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
def paintEvent (self, event):
|
||||
self.painter = QtGui.QPainter(self)
|
||||
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.painter.drawPixmap(0, 0, self.desktopBg)
|
||||
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
|
||||
self.painter.end()
|
||||
# self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
self.setWindowState(QtCore.Qt.WindowFullScreen)
|
||||
|
||||
def applyImagePixmap( self ):
|
||||
win_h = self.height()
|
||||
win_w = self.width()
|
||||
|
||||
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)
|
||||
self.lblImage.setPixmap( display_pixmap )
|
||||
else:
|
||||
display_pixmap = self.imagePixmap
|
||||
self.lblImage.setPixmap( display_pixmap )
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize( img_w, img_h )
|
||||
self.lblImage.move( (win_w - img_w)/2, (win_h - img_h)/2 )
|
||||
self.imagePixmap = image_pixmap
|
||||
|
||||
def mousePressEvent( self , event):
|
||||
self.close()
|
||||
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
|
||||
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_()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def paintEvent(self, event):
|
||||
self.painter = QtGui.QPainter(self)
|
||||
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.painter.drawPixmap(0, 0, self.desktopBg)
|
||||
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
|
||||
self.painter.end()
|
||||
|
||||
def applyImagePixmap(self):
|
||||
win_h = self.height()
|
||||
win_w = self.width()
|
||||
|
||||
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)
|
||||
self.lblImage.setPixmap(display_pixmap)
|
||||
else:
|
||||
display_pixmap = self.imagePixmap
|
||||
self.lblImage.setPixmap(display_pixmap)
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.close()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,146 +1,190 @@
|
||||
"""
|
||||
A PyQT4 dialog to select specific issue from list
|
||||
"""
|
||||
"""A PyQT4 dialog to select specific issue from list"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
import os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
|
||||
class IssueNumberTableWidgetItem(QtGui.QTableWidgetItem):
|
||||
|
||||
def __lt__(self, other):
|
||||
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
|
||||
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
|
||||
return (IssueString(selfStr).asFloat() <
|
||||
IssueString(otherStr).asFloat())
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.coverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
gridlayout.addWidget( self.coverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
volume_id = 0
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
def __init__(self, parent, settings, series_id, issue_number):
|
||||
super(IssueSelectionWindow, self).__init__(parent)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('issueselectionwindow.ui'), self)
|
||||
|
||||
self.series_id = series_id
|
||||
self.settings = settings
|
||||
self.url_fetch_thread = None
|
||||
|
||||
if issue_number is None or issue_number == "":
|
||||
self.issue_number = 1
|
||||
else:
|
||||
self.issue_number = issue_number
|
||||
self.coverWidget = CoverImageWidget(
|
||||
self.coverImageContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.coverImageContainer)
|
||||
gridlayout.addWidget(self.coverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.initial_id = None
|
||||
self.performQuery()
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
#now that the list has been sorted, find the initial record, and select it
|
||||
if self.initial_id is None:
|
||||
self.twList.selectRow( 0 )
|
||||
else:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
issue_id, b = self.twList.item( r, 0 ).data( QtCore.Qt.UserRole ).toInt()
|
||||
if (issue_id == self.initial_id):
|
||||
self.twList.selectRow( r )
|
||||
break
|
||||
|
||||
def performQuery( self ):
|
||||
reduceWidgetFontSize(self.twList)
|
||||
reduceWidgetFontSize(self.teDescription, 1)
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
try:
|
||||
comicVine = ComicVineTalker( )
|
||||
volume_data = comicVine.fetchVolumeData( self.series_id )
|
||||
except ComicVineTalkerException:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to list issues!"))
|
||||
return
|
||||
self.series_id = series_id
|
||||
self.settings = settings
|
||||
self.url_fetch_thread = None
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.issue_list = volume_data['issues']
|
||||
if issue_number is None or issue_number == "":
|
||||
self.issue_number = 1
|
||||
else:
|
||||
self.issue_number = issue_number
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
self.initial_id = None
|
||||
self.performQuery()
|
||||
|
||||
row = 0
|
||||
for record in self.issue_list:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record['issue_number']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setData(QtCore.Qt.DisplayRole, float(item_text))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = record['name']
|
||||
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)
|
||||
|
||||
if IssueString(record['issue_number']).asString() == IssueString(self.issue_number).asString():
|
||||
self.initial_id = record['id']
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 0 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
# now that the list has been sorted, find the initial record, and
|
||||
# select it
|
||||
if self.initial_id is None:
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
issue_id, b = self.twList.item(
|
||||
r, 0).data(QtCore.Qt.UserRole).toInt()
|
||||
if (issue_id == self.initial_id):
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
def performQuery(self):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.issue_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
|
||||
QtGui.QApplication.setOverrideCursor(
|
||||
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record['id'] == self.issue_id:
|
||||
self.issue_number = record['issue_number']
|
||||
self.coverWidget.setIssueID( int(self.issue_id) )
|
||||
break
|
||||
|
||||
try:
|
||||
comicVine = ComicVineTalker()
|
||||
volume_data = comicVine.fetchVolumeData(self.series_id)
|
||||
self.issue_list = comicVine.fetchIssuesByVolume(self.series_id)
|
||||
except ComicVineTalkerException as e:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
if e.code == ComicVineTalkerException.RateLimit:
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Comic Vine Error"),
|
||||
ComicVineTalker.getRateLimitMessage())
|
||||
else:
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Network Issue"),
|
||||
self.tr("Could not connect to Comic Vine to list issues!"))
|
||||
return
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for record in self.issue_list:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
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.DisplayRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = record['cover_date']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
# remove the day of "YYYY-MM-DD"
|
||||
parts = item_text.split("-")
|
||||
if len(parts) > 1:
|
||||
item_text = parts[0] + "-" + parts[1]
|
||||
|
||||
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']
|
||||
if item_text is None:
|
||||
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']
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.issue_id, b = self.twList.item(
|
||||
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record['id'] == self.issue_id:
|
||||
self.issue_number = record['issue_number']
|
||||
self.coverWidget.setIssueID(int(self.issue_id))
|
||||
if record['description'] is None:
|
||||
self.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(record['description'])
|
||||
|
||||
break
|
||||
|
||||
@@ -1,101 +1 @@
|
||||
"""
|
||||
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"
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 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):
|
||||
|
||||
if text is None:
|
||||
self.num = None
|
||||
self.suffix = ""
|
||||
return
|
||||
|
||||
self.text = str(text)
|
||||
#strip out non float-y stuff
|
||||
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
|
||||
|
||||
if tmp_num_str == "":
|
||||
self.num = None
|
||||
self.suffix = self.text
|
||||
|
||||
else:
|
||||
if tmp_num_str.count(".") > 1:
|
||||
#make sure it's a valid float or int.
|
||||
parts = tmp_num_str.split('.')
|
||||
self.num = float( parts[0] + '.' + parts[1] )
|
||||
else:
|
||||
self.num = float( tmp_num_str )
|
||||
|
||||
self.suffix = ""
|
||||
parts = self.text.split(tmp_num_str)
|
||||
if len( parts ) > 1 :
|
||||
self.suffix = parts[1]
|
||||
|
||||
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
|
||||
return self.num
|
||||
|
||||
def asInt( self ):
|
||||
#return the int version of the float
|
||||
if self.num is None:
|
||||
return None
|
||||
return int( self.num )
|
||||
|
||||
|
||||
from comicapi.issuestring import *
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
"""
|
||||
A PyQT4 dialog to a text file or log
|
||||
"""
|
||||
"""A PyQT4 dialog to a text file or log"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class LogWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(LogWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
def setText( self, text ):
|
||||
self.textEdit.setPlainText( text )
|
||||
def __init__(self, parent):
|
||||
super(LogWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
def setText(self, text):
|
||||
self.textEdit.setPlainText(text)
|
||||
|
||||
@@ -1,79 +1,87 @@
|
||||
"""
|
||||
A python app to (automatically) tag comic archives
|
||||
"""
|
||||
"""A python app to (automatically) tag comic archives"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 signal
|
||||
import os
|
||||
import traceback
|
||||
import platform
|
||||
#import os
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
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
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from taggerwindow import TaggerWindow
|
||||
except ImportError as e:
|
||||
qt_available = False
|
||||
#---------------------------------------
|
||||
|
||||
def ctmain():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
|
||||
|
||||
if opts.no_gui:
|
||||
cli.cli_mode( opts, settings )
|
||||
else:
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
|
||||
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.file_list, settings )
|
||||
tagger_window.show()
|
||||
# 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.only_set_key:
|
||||
print("Key set")
|
||||
return
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish( tagger_window )
|
||||
ComicVineTalker.api_key = settings.cv_api_key
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception, e:
|
||||
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
|
||||
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
|
||||
|
||||
if opts.no_gui:
|
||||
cli.cli_mode(opts, settings)
|
||||
else:
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
|
||||
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(opts.file_list, settings, opts=opts)
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish(tagger_window)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.critical(
|
||||
QtGui.QMainWindow(),
|
||||
"Error",
|
||||
"Unhandled exception in app:\n" +
|
||||
traceback.format_exc())
|
||||
|
||||
@@ -1,153 +1,160 @@
|
||||
"""
|
||||
A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
"""A PyQT4 dialog to select from automated issue matches"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
#from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#from comicarchive import MetaDataStyle
|
||||
#from comicvinetalker import ComicVineTalker
|
||||
#import utils
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
gridlayout.addWidget( self.altCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
volume_id = 0
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
def __init__(self, parent, matches, comic_archive):
|
||||
super(MatchSelectionWindow, self).__init__(parent)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode)
|
||||
gridlayout = QtGui.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.matches = matches
|
||||
self.comic_archive = comic_archive
|
||||
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.archiveCoverWidget = CoverImageWidget(
|
||||
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.updateData()
|
||||
reduceWidgetFontSize(self.twList)
|
||||
reduceWidgetFontSize(self.teDescription, 1)
|
||||
|
||||
def updateData( self):
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.comic_archive.path
|
||||
self.setWindowTitle( u"Select correct match: {0}".format(
|
||||
os.path.split(path)[1] ))
|
||||
|
||||
def populateTable( self ):
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
self.matches = matches
|
||||
self.comic_archive = comic_archive
|
||||
|
||||
row = 0
|
||||
for match in self.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
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)
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
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 = 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'])
|
||||
self.updateData()
|
||||
|
||||
item_text = year_str + month_str
|
||||
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)
|
||||
def updateData(self):
|
||||
|
||||
item_text = match['issue_title']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
path = self.comic_archive.path
|
||||
self.setWindowTitle(u"Select correct match: {0}".format(
|
||||
os.path.split(path)[1]))
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
def populateTable(self):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
|
||||
|
||||
def setCoverImage( self ):
|
||||
self.archiveCoverWidget.setArchive( self.comic_archive)
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
def currentMatch( self ):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
|
||||
return match
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for match in self.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
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 = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
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 = 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 = 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']
|
||||
if item_text is None:
|
||||
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)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
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.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(self.currentMatch()['description'])
|
||||
|
||||
def setCoverImage(self):
|
||||
self.archiveCoverWidget.setArchive(self.comic_archive)
|
||||
|
||||
def currentMatch(self):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data(
|
||||
QtCore.Qt.UserRole).toPyObject()[0]
|
||||
return match
|
||||
|
||||
@@ -1,106 +1,117 @@
|
||||
"""
|
||||
A PyQt4 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:
|
||||
|
||||
checked = OptionalMessageDialog.msg(self, "Disclaimer",
|
||||
"This is beta software, and you are using it at your own risk!",
|
||||
)
|
||||
|
||||
said_yes, checked = OptionalMessageDialog.question(self, "Question",
|
||||
"Are you sure you wish to do this?",
|
||||
)
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
|
||||
"""
|
||||
example usage:
|
||||
|
||||
checked = OptionalMessageDialog.msg( self, "Disclaimer",
|
||||
"This is beta software, and you are using it at your own risk!",
|
||||
)
|
||||
|
||||
said_yes, checked = OptionalMessageDialog.question( self, "Question",
|
||||
"Are you sure you wish to do this?",
|
||||
)
|
||||
"""
|
||||
# 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.
|
||||
|
||||
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 ):
|
||||
QDialog.__init__(self, parent)
|
||||
|
||||
self.setWindowTitle( title )
|
||||
self.was_accepted = False
|
||||
|
||||
l = QVBoxLayout( self )
|
||||
|
||||
self.theLabel = QLabel( msg )
|
||||
self.theLabel.setWordWrap(True)
|
||||
self.theLabel.setTextFormat( Qt.RichText )
|
||||
self.theLabel.setOpenExternalLinks(True)
|
||||
self.theLabel.setTextInteractionFlags( Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard )
|
||||
|
||||
l.addWidget( self.theLabel )
|
||||
l.insertSpacing ( -1, 10 )
|
||||
|
||||
if check_text is None:
|
||||
if style == StyleQuestion:
|
||||
check_text = "Remember this answer"
|
||||
else:
|
||||
check_text = "Don't show this dialog again"
|
||||
|
||||
self.theCheckBox = QCheckBox(check_text)
|
||||
|
||||
self.theCheckBox.setCheckState( check_state )
|
||||
|
||||
l.addWidget( self.theCheckBox )
|
||||
def __init__(self, parent, style, title, msg,
|
||||
check_state=Qt.Unchecked, check_text=None):
|
||||
QDialog.__init__(self, parent)
|
||||
|
||||
btnbox_style = QDialogButtonBox.Ok
|
||||
if style == StyleQuestion:
|
||||
btnbox_style = QDialogButtonBox.Yes|QDialogButtonBox.No
|
||||
|
||||
self.theButtonBox = QDialogButtonBox(
|
||||
btnbox_style,
|
||||
parent=self,
|
||||
accepted=self.accept,
|
||||
rejected=self.reject)
|
||||
|
||||
l.addWidget( self.theButtonBox )
|
||||
|
||||
def accept( self ):
|
||||
self.was_accepted = True
|
||||
QDialog.accept(self)
|
||||
self.setWindowTitle(title)
|
||||
self.was_accepted = False
|
||||
|
||||
def reject( self ):
|
||||
self.was_accepted = False
|
||||
QDialog.reject(self)
|
||||
l = QVBoxLayout(self)
|
||||
|
||||
@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 )
|
||||
self.theLabel = QLabel(msg)
|
||||
self.theLabel.setWordWrap(True)
|
||||
self.theLabel.setTextFormat(Qt.RichText)
|
||||
self.theLabel.setOpenExternalLinks(True)
|
||||
self.theLabel.setTextInteractionFlags(
|
||||
Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
|
||||
d.exec_()
|
||||
return d.theCheckBox.isChecked()
|
||||
l.addWidget(self.theLabel)
|
||||
l.insertSpacing(-1, 10)
|
||||
|
||||
@staticmethod
|
||||
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.exec_()
|
||||
|
||||
return d.was_accepted, d.theCheckBox.isChecked()
|
||||
if check_text is None:
|
||||
if style == StyleQuestion:
|
||||
check_text = "Remember this answer"
|
||||
else:
|
||||
check_text = "Don't show this message again"
|
||||
|
||||
self.theCheckBox = QCheckBox(check_text)
|
||||
|
||||
self.theCheckBox.setCheckState(check_state)
|
||||
|
||||
l.addWidget(self.theCheckBox)
|
||||
|
||||
btnbox_style = QDialogButtonBox.Ok
|
||||
if style == StyleQuestion:
|
||||
btnbox_style = QDialogButtonBox.Yes | QDialogButtonBox.No
|
||||
|
||||
self.theButtonBox = QDialogButtonBox(
|
||||
btnbox_style,
|
||||
parent=self,
|
||||
accepted=self.accept,
|
||||
rejected=self.reject)
|
||||
|
||||
l.addWidget(self.theButtonBox)
|
||||
|
||||
def accept(self):
|
||||
self.was_accepted = True
|
||||
QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
self.was_accepted = False
|
||||
QDialog.reject(self)
|
||||
|
||||
@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.exec_()
|
||||
return d.theCheckBox.isChecked()
|
||||
|
||||
@staticmethod
|
||||
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.exec_()
|
||||
|
||||
return d.was_accepted, d.theCheckBox.isChecked()
|
||||
|
||||
@@ -1,345 +1,424 @@
|
||||
"""
|
||||
CLI options class for comictagger app
|
||||
"""
|
||||
"""CLI options class for ComicTagger app"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 getopt
|
||||
import platform
|
||||
import os
|
||||
import traceback
|
||||
import ctversion
|
||||
import utils
|
||||
|
||||
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 LIST]
|
||||
help_text = """Usage: {0} [option] ... [file [files ...]]
|
||||
|
||||
A utility for reading and writing metadata to comic archives.
|
||||
|
||||
If no options are given, {0} will run in windowed mode
|
||||
If no options are given, {0} will run in windowed mode.
|
||||
|
||||
-p, --print Print out tag info from file. Specify type
|
||||
(via -t) to get only info of that tag type.
|
||||
--raw With -p, will print out the raw tag block(s)
|
||||
from the file.
|
||||
-d, --delete Deletes the tag block of specified type (via
|
||||
-t).
|
||||
-c, --copy=SOURCE Copy the specified source tag block to
|
||||
destination style specified via -t
|
||||
(potentially lossy operation).
|
||||
-s, --save Save out tags as specified type (via -t).
|
||||
Must specify also at least -o, -p, or -m.
|
||||
--nooverwrite Don't modify tag block if it already exists
|
||||
(relevant for -s or -c).
|
||||
-1, --assume-issue-one Assume issue number is 1 if not found
|
||||
(relevant for -s).
|
||||
-n, --dryrun Don't actually modify file (only relevant for
|
||||
-d, -s, or -r).
|
||||
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or
|
||||
"COMET" (as either ComicRack, ComicBookLover,
|
||||
or CoMet style tags, respectively).
|
||||
-f, --parsefilename Parse the filename to get some info,
|
||||
specifically series name, issue number,
|
||||
volume, and publication year.
|
||||
-i, --interactive Interactively query the user when there are
|
||||
multiple matches for an online search.
|
||||
--nosummary Suppress the default summary after a save
|
||||
operation.
|
||||
-o, --online Search online and attempt to identify file
|
||||
using existing metadata and images in archive.
|
||||
May be used in conjunction with -f and -m.
|
||||
--id=ID Use the issue ID when searching online.
|
||||
Overrides all other metadata.
|
||||
-m, --metadata=LIST Explicitly define, as a list, some tags to be
|
||||
used. e.g.:
|
||||
"series=Plastic Man, publisher=Quality Comics"
|
||||
"series=Kickers^, Inc., issue=1, year=1986"
|
||||
Name-Value pairs are comma separated. Use a
|
||||
"^" to escape an "=" or a ",", as shown in
|
||||
the example above. Some names that can be
|
||||
used: series, issue, issueCount, year,
|
||||
publisher, title
|
||||
-r, --rename Rename the file based on specified tag style.
|
||||
--noabort Don't abort save operation when online match
|
||||
is of low confidence.
|
||||
-e, --export-to-zip Export RAR archive to Zip format.
|
||||
--delete-rar Delete original RAR archive after successful
|
||||
export to Zip.
|
||||
--abort-on-conflict Don't export to zip if intended new filename
|
||||
exists (otherwise, creates a new unique
|
||||
filename).
|
||||
-S, --script=FILE Run an "add-on" python script that uses the
|
||||
ComicTagger library for custom processing.
|
||||
Script arguments can follow the script name.
|
||||
-R, --recursive Recursively include files in sub-folders.
|
||||
--cv-api-key=KEY Use the given Comic Vine API Key (persisted
|
||||
in settings).
|
||||
--only-set-cv-key Only set the Comic Vine API key and quit.
|
||||
-w, --wait-on-cv-rate-limit When encountering a Comic Vine rate limit
|
||||
error, wait and retry query.
|
||||
-v, --verbose Be noisy when doing what it does.
|
||||
--terse Don't say much (for print mode).
|
||||
--version Display version.
|
||||
-h, --help Display this message.
|
||||
|
||||
-p, --print Print out tag info from file. Specify type
|
||||
(via -t) to get only info of that tag type
|
||||
--raw With -p, will print out the raw tag block(s)
|
||||
from the file
|
||||
-d, --delete Deletes the tag block of specified type (via -t)
|
||||
-c, --copy=SOURCE Copy the specified source tag block to destination style
|
||||
specified via via -t (potentially lossy operation)
|
||||
-s, --save Save out tags as specified type (via -t)
|
||||
Must specify also at least -o, -p, or -m
|
||||
--nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c )
|
||||
-n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r)
|
||||
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either
|
||||
ComicRack, ComicBookLover, or CoMet style tags, respectivly)
|
||||
-f, --parsefilename Parse the filename to get some info, specifically
|
||||
series name, issue number, volume, and publication
|
||||
year
|
||||
-i, --interactive Interactively query the user when there are multiple matches for
|
||||
an online search
|
||||
--nosummary Suppress the default summary after a save operation
|
||||
-o, --online Search online and attempt to identify file using
|
||||
existing metadata and images in archive. May be used
|
||||
in conjuntion with -f and -m
|
||||
--id=ID Use the issue ID when searching online. Overrides all other metadata
|
||||
-m, --metadata=LIST Explicity define, as a list, some tags to be used
|
||||
e.g. "series=Plastic Man , publisher=Quality Comics"
|
||||
"series=Kickers^, Inc., issue=1, year=1986"
|
||||
Name-Value pairs are comma separated. Use a "^" to
|
||||
escape an "=" or a ",", as shown in the example above
|
||||
Some names that can be used:
|
||||
series, issue, issueCount, year, publisher, title
|
||||
-r, --rename Rename the file based on specified tag style.
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
-e, --export-to-zip Export RAR archive to Zip format
|
||||
--delete-rar Delete original RAR archive after successful export to Zip
|
||||
--abort-on-conflict Don't export to zip if intended new filename exists (Otherwise, creates
|
||||
a new unique filename)
|
||||
-S, --script=FILE Run an "add-on" python script that uses the comictagger library for custom
|
||||
processing. Script arguments can follow the script name
|
||||
-R, --recursive Recursively include files in sub-folders
|
||||
-v, --verbose Be noisy when doing what it does
|
||||
--terse Don't say much (for print mode)
|
||||
--version Display version
|
||||
-h, --help Display this message
|
||||
|
||||
For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.data_style = None
|
||||
self.no_gui = False
|
||||
self.filename = None
|
||||
self.verbose = False
|
||||
self.terse = False
|
||||
self.metadata = None
|
||||
self.print_tags = False
|
||||
self.copy_tags = False
|
||||
self.delete_tags = False
|
||||
self.export_to_zip = False
|
||||
self.abort_export_on_conflict = False
|
||||
self.delete_rar_after_export = False
|
||||
self.search_online = False
|
||||
self.dryrun = False
|
||||
self.abortOnLowConfidence = True
|
||||
self.save_tags = False
|
||||
self.parse_filename = False
|
||||
self.show_save_summary = True
|
||||
self.raw = False
|
||||
self.cv_api_key = None
|
||||
self.only_set_key = False
|
||||
self.rename_file = False
|
||||
self.no_overwrite = False
|
||||
self.interactive = False
|
||||
self.issue_id = None
|
||||
self.recursive = False
|
||||
self.run_script = False
|
||||
self.script = None
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.assume_issue_is_one_if_not_set = False
|
||||
self.file_list = []
|
||||
|
||||
def __init__(self):
|
||||
self.data_style = None
|
||||
self.no_gui = False
|
||||
self.filename = None
|
||||
self.verbose = False
|
||||
self.terse = False
|
||||
self.metadata = None
|
||||
self.print_tags = False
|
||||
self.copy_tags = False
|
||||
self.delete_tags = False
|
||||
self.export_to_zip = False
|
||||
self.abort_export_on_conflict = False
|
||||
self.delete_rar_after_export = False
|
||||
self.search_online = False
|
||||
self.dryrun = False
|
||||
self.abortOnLowConfidence = True
|
||||
self.save_tags = False
|
||||
self.parse_filename = False
|
||||
self.show_save_summary = True
|
||||
self.raw = False
|
||||
self.rename_file = False
|
||||
self.no_overwrite = False
|
||||
self.interactive = False
|
||||
self.issue_id = None
|
||||
self.recursive = False
|
||||
self.run_script = False
|
||||
self.script = None
|
||||
self.file_list = []
|
||||
|
||||
def display_msg_and_quit( self, msg, code, show_help=False ):
|
||||
appname = os.path.basename(sys.argv[0])
|
||||
if msg is not None:
|
||||
print( msg )
|
||||
if show_help:
|
||||
print self.help_text.format(appname)
|
||||
else:
|
||||
print "For more help, run with '--help'"
|
||||
sys.exit(code)
|
||||
|
||||
def parseMetadataFromString( self, mdstr ):
|
||||
# The metadata string is a comma separated list of name-value pairs
|
||||
# The names match the attributes of the internal metadata struct (for now)
|
||||
# The caret is the special "escape character", since it's not common in
|
||||
# natural language text
|
||||
def display_msg_and_quit(self, msg, code, show_help=False):
|
||||
appname = os.path.basename(sys.argv[0])
|
||||
if msg is not None:
|
||||
print(msg)
|
||||
if show_help:
|
||||
print(self.help_text.format(appname))
|
||||
else:
|
||||
print("For more help, run with '--help'")
|
||||
sys.exit(code)
|
||||
|
||||
# example = "series=Kickers^, Inc. ,issue=1, year=1986"
|
||||
|
||||
escaped_comma = "^,"
|
||||
escaped_equals = "^="
|
||||
replacement_token = "<_~_>"
|
||||
|
||||
md = GenericMetadata()
|
||||
def parseMetadataFromString(self, mdstr):
|
||||
"""The metadata string is a comma separated list of name-value pairs
|
||||
The names match the attributes of the internal metadata struct (for now)
|
||||
The caret is the special "escape character", since it's not common in
|
||||
natural language text
|
||||
|
||||
# First, replace escaped commas with with a unique token (to be changed back later)
|
||||
mdstr = mdstr.replace( escaped_comma, replacement_token)
|
||||
tmp_list = mdstr.split(",")
|
||||
md_list = []
|
||||
for item in tmp_list:
|
||||
item = item.replace( replacement_token, "," )
|
||||
md_list.append(item)
|
||||
|
||||
# Now build a nice dict from the list
|
||||
md_dict = dict()
|
||||
for item in md_list:
|
||||
# Make sure to fix any escaped equal signs
|
||||
i = item.replace( escaped_equals, replacement_token)
|
||||
key,value = i.split("=")
|
||||
value = value.replace( replacement_token, "=" ).strip()
|
||||
key = key.strip()
|
||||
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 )
|
||||
else:
|
||||
md_dict[key] = value
|
||||
|
||||
# Map the dict to the metadata object
|
||||
for key in md_dict:
|
||||
if not hasattr(md, key):
|
||||
print "Warning: '{0}' is not a valid tag name".format(key)
|
||||
else:
|
||||
md.isEmpty = False
|
||||
setattr( md, key, md_dict[key] )
|
||||
#print md
|
||||
return md
|
||||
|
||||
def parseCmdLineArgs(self):
|
||||
|
||||
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
|
||||
# remove the PSN ("process serial number") argument from OS/X
|
||||
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a ]
|
||||
else:
|
||||
input_args = sys.argv[1:]
|
||||
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt( input_args,
|
||||
"hpdt:fm:vonsrc:ieRS:",
|
||||
[ "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" ] )
|
||||
example = "series=Kickers^, Inc. ,issue=1, year=1986"
|
||||
"""
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_msg_and_quit( str(err), 2 )
|
||||
|
||||
# process options
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
self.display_msg_and_quit( None, 0, show_help=True )
|
||||
if o in ("-v", "--verbose"):
|
||||
self.verbose = True
|
||||
if o in ("-S", "--script"):
|
||||
self.run_script = True
|
||||
self.script = a
|
||||
if o in ("-R", "--recursive"):
|
||||
self.recursive = True
|
||||
if o in ("-p", "--print"):
|
||||
self.print_tags = True
|
||||
if o in ("-d", "--delete"):
|
||||
self.delete_tags = True
|
||||
if o in ("-i", "--interactive"):
|
||||
self.interactive = True
|
||||
if o in ("-c", "--copy"):
|
||||
self.copy_tags = True
|
||||
if a.lower() == "cr":
|
||||
self.copy_source = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.copy_source = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.copy_source = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit( "Invalid copy tag source type", 1 )
|
||||
if o in ("-o", "--online"):
|
||||
self.search_online = True
|
||||
if o in ("-n", "--dryrun"):
|
||||
self.dryrun = True
|
||||
if o in ("-m", "--metadata"):
|
||||
self.metadata = self.parseMetadataFromString(a)
|
||||
if o in ("-s", "--save"):
|
||||
self.save_tags = True
|
||||
if o in ("-r", "--rename"):
|
||||
self.rename_file = True
|
||||
if o in ("-e", "--export_to_zip"):
|
||||
self.export_to_zip = True
|
||||
if o == "--delete-rar":
|
||||
self.delete_rar_after_export = True
|
||||
if o == "--abort-on-conflict":
|
||||
self.abort_export_on_conflict = True
|
||||
if o in ("-f", "--parsefilename"):
|
||||
self.parse_filename = True
|
||||
if o == "--id":
|
||||
self.issue_id = a
|
||||
if o == "--raw":
|
||||
self.raw = True
|
||||
if o == "--noabort":
|
||||
self.abortOnLowConfidence = False
|
||||
if o == "--terse":
|
||||
self.terse = True
|
||||
if o == "--nosummary":
|
||||
self.show_save_summary = False
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--version":
|
||||
print "ComicTagger {0}: Copyright (c) 2012-2013 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":
|
||||
self.data_style = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.data_style = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.data_style = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit( "Invalid tag type", 1 )
|
||||
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip:
|
||||
self.no_gui = True
|
||||
escaped_comma = "^,"
|
||||
escaped_equals = "^="
|
||||
replacement_token = "<_~_>"
|
||||
|
||||
count = 0
|
||||
if self.run_script: count += 1
|
||||
if self.print_tags: count += 1
|
||||
if self.delete_tags: count += 1
|
||||
if self.save_tags: count += 1
|
||||
if self.copy_tags: count += 1
|
||||
if self.rename_file: count += 1
|
||||
if self.export_to_zip: count +=1
|
||||
|
||||
if count > 1:
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, or run script", 1 )
|
||||
md = GenericMetadata()
|
||||
|
||||
if self.script is not None:
|
||||
# we were given a script. special case for the args:
|
||||
# 1. ignore everthing before the -S,
|
||||
# 2. pass all the ones that follow (including script name) to the script
|
||||
script_args = list()
|
||||
for idx, arg in enumerate(sys.argv):
|
||||
if arg in [ '-S', '--script']:
|
||||
#found script!
|
||||
script_args = sys.argv[idx+1:]
|
||||
break
|
||||
sys.argv = script_args
|
||||
if not os.path.exists(self.script):
|
||||
print "Can't find {0}".format( self.script )
|
||||
else:
|
||||
# I *think* this makes sense:
|
||||
# assume the base name of the file is the module name
|
||||
# add the folder of the given file to the python path
|
||||
# import module
|
||||
dirname = os.path.dirname(self.script)
|
||||
module_name = os.path.splitext(os.path.basename(self.script))[0]
|
||||
sys.path = [dirname] + sys.path
|
||||
try:
|
||||
script = __import__(module_name)
|
||||
|
||||
# Determine if the entry point exists before trying to run it
|
||||
if "main" in dir(script):
|
||||
script.main()
|
||||
else:
|
||||
print "Can't find entry point \"main()\" in module \"{0}\"".format( module_name )
|
||||
except Exception as e:
|
||||
print "Script raised an unhandled exception: ", e
|
||||
print traceback.format_exc()
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if len(args) > 0:
|
||||
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))
|
||||
if len(self.file_list) > 0:
|
||||
self.filename = self.file_list[0]
|
||||
else:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
# First, replace escaped commas with with a unique token (to be changed
|
||||
# back later)
|
||||
mdstr = mdstr.replace(escaped_comma, replacement_token)
|
||||
tmp_list = mdstr.split(",")
|
||||
md_list = []
|
||||
for item in tmp_list:
|
||||
item = item.replace(replacement_token, ",")
|
||||
md_list.append(item)
|
||||
|
||||
if 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 )
|
||||
|
||||
if self.save_tags and self.data_style is None:
|
||||
self.display_msg_and_quit( "Please specify the type to save with -t", 1 )
|
||||
# Now build a nice dict from the list
|
||||
md_dict = dict()
|
||||
for item in md_list:
|
||||
# Make sure to fix any escaped equal signs
|
||||
i = item.replace(escaped_equals, replacement_token)
|
||||
key, value = i.split("=")
|
||||
value = value.replace(replacement_token, "=").strip()
|
||||
key = key.strip()
|
||||
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)
|
||||
else:
|
||||
md_dict[key] = value
|
||||
|
||||
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 )
|
||||
|
||||
#if self.rename_file and self.data_style is None:
|
||||
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )
|
||||
|
||||
if self.recursive:
|
||||
self.file_list = utils.get_recursive_filelist( self.file_list )
|
||||
# Map the dict to the metadata object
|
||||
for key in md_dict:
|
||||
if not hasattr(md, key):
|
||||
print("Warning: '{0}' is not a valid tag name".format(key))
|
||||
else:
|
||||
md.isEmpty = False
|
||||
setattr(md, key, md_dict[key])
|
||||
# print(md)
|
||||
return md
|
||||
|
||||
def launch_script(self, scriptfile):
|
||||
# we were given a script. special case for the args:
|
||||
# 1. ignore everything before the -S,
|
||||
# 2. pass all the ones that follow (including script name) to the
|
||||
# script
|
||||
script_args = list()
|
||||
for idx, arg in enumerate(sys.argv):
|
||||
if arg in ['-S', '--script']:
|
||||
# found script!
|
||||
script_args = sys.argv[idx + 1:]
|
||||
break
|
||||
sys.argv = script_args
|
||||
if not os.path.exists(scriptfile):
|
||||
print("Can't find {0}".format(scriptfile))
|
||||
else:
|
||||
# I *think* this makes sense:
|
||||
# assume the base name of the file is the module name
|
||||
# add the folder of the given file to the python path
|
||||
# import module
|
||||
dirname = os.path.dirname(scriptfile)
|
||||
module_name = os.path.splitext(os.path.basename(scriptfile))[0]
|
||||
sys.path = [dirname] + sys.path
|
||||
try:
|
||||
script = __import__(module_name)
|
||||
|
||||
# Determine if the entry point exists before trying to run it
|
||||
if "main" in dir(script):
|
||||
script.main()
|
||||
else:
|
||||
print(
|
||||
"Can't find entry point \"main()\" in module \"{0}\"".format(module_name))
|
||||
except Exception as e:
|
||||
print "Script raised an unhandled exception: ", e
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
def parseCmdLineArgs(self):
|
||||
|
||||
if platform.system() == "Darwin" and hasattr(
|
||||
sys, "frozen") and sys.frozen == 1:
|
||||
# remove the PSN ("process serial number") argument from OS/X
|
||||
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a]
|
||||
else:
|
||||
input_args = sys.argv[1:]
|
||||
|
||||
# first check if we're launching a script:
|
||||
for n in range(len(input_args)):
|
||||
if (input_args[n] in ["-S", "--script"] and
|
||||
n + 1 < len(input_args)):
|
||||
# insert a "--" which will cause getopt to ignore the remaining args
|
||||
# so they will be passed to the script
|
||||
input_args.insert(n + 2, "--")
|
||||
break
|
||||
|
||||
# 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"])
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_msg_and_quit(str(err), 2)
|
||||
|
||||
# process options
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
self.display_msg_and_quit(None, 0, show_help=True)
|
||||
if o in ("-v", "--verbose"):
|
||||
self.verbose = True
|
||||
if o in ("-S", "--script"):
|
||||
self.run_script = True
|
||||
self.script = a
|
||||
if o in ("-R", "--recursive"):
|
||||
self.recursive = True
|
||||
if o in ("-p", "--print"):
|
||||
self.print_tags = True
|
||||
if o in ("-d", "--delete"):
|
||||
self.delete_tags = True
|
||||
if o in ("-i", "--interactive"):
|
||||
self.interactive = True
|
||||
if o in ("-c", "--copy"):
|
||||
self.copy_tags = True
|
||||
if a.lower() == "cr":
|
||||
self.copy_source = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.copy_source = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.copy_source = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit(
|
||||
"Invalid copy tag source type", 1)
|
||||
if o in ("-o", "--online"):
|
||||
self.search_online = True
|
||||
if o in ("-n", "--dryrun"):
|
||||
self.dryrun = True
|
||||
if o in ("-m", "--metadata"):
|
||||
self.metadata = self.parseMetadataFromString(a)
|
||||
if o in ("-s", "--save"):
|
||||
self.save_tags = True
|
||||
if o in ("-r", "--rename"):
|
||||
self.rename_file = True
|
||||
if o in ("-e", "--export_to_zip"):
|
||||
self.export_to_zip = True
|
||||
if o == "--delete-rar":
|
||||
self.delete_rar_after_export = True
|
||||
if o == "--abort-on-conflict":
|
||||
self.abort_export_on_conflict = True
|
||||
if o in ("-f", "--parsefilename"):
|
||||
self.parse_filename = True
|
||||
if o in ("-w", "--wait-on-cv-rate-limit"):
|
||||
self.wait_and_retry_on_rate_limit = True
|
||||
if o == "--id":
|
||||
self.issue_id = a
|
||||
if o == "--raw":
|
||||
self.raw = True
|
||||
if o == "--noabort":
|
||||
self.abortOnLowConfidence = False
|
||||
if o == "--terse":
|
||||
self.terse = True
|
||||
if o == "--nosummary":
|
||||
self.show_save_summary = False
|
||||
if o in ("-1", "--assume-issue-one"):
|
||||
self.assume_issue_is_one_if_not_set = True
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--cv-api-key":
|
||||
self.cv_api_key = a
|
||||
if o == "--only-set-cv-key":
|
||||
self.only_set_key = True
|
||||
if o == "--version":
|
||||
print(
|
||||
"ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(ctversion.version))
|
||||
print(
|
||||
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
self.data_style = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.data_style = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.data_style = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit("Invalid tag type", 1)
|
||||
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip or self.only_set_key:
|
||||
self.no_gui = True
|
||||
|
||||
count = 0
|
||||
if self.run_script:
|
||||
count += 1
|
||||
if self.print_tags:
|
||||
count += 1
|
||||
if self.delete_tags:
|
||||
count += 1
|
||||
if self.save_tags:
|
||||
count += 1
|
||||
if self.copy_tags:
|
||||
count += 1
|
||||
if self.rename_file:
|
||||
count += 1
|
||||
if self.export_to_zip:
|
||||
count += 1
|
||||
if self.only_set_key:
|
||||
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)
|
||||
|
||||
if self.script is not None:
|
||||
self.launch_script(self.script)
|
||||
|
||||
if len(args) > 0:
|
||||
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))
|
||||
if len(self.file_list) > 0:
|
||||
self.filename = self.file_list[0]
|
||||
else:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
|
||||
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.delete_tags and self.data_style is None:
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# if self.rename_file and self.data_style is None:
|
||||
# self.display_msg_and_quit("Please specify the type to use for renaming with -t", 1)
|
||||
|
||||
if self.recursive:
|
||||
self.file_list = utils.get_recursive_filelist(self.file_list)
|
||||
|
||||
@@ -1,110 +1,114 @@
|
||||
"""
|
||||
A PyQT4 dialog to show pages of a comic archive
|
||||
"""
|
||||
"""A PyQT4 dialog to show pages of a comic archive"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 platform
|
||||
import sys
|
||||
#import sys
|
||||
#import os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
|
||||
class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, metadata):
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui' ), self)
|
||||
|
||||
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.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
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.btnNext.clicked.connect( self.nextPage )
|
||||
self.btnPrev.clicked.connect( self.prevPage )
|
||||
self.show()
|
||||
|
||||
self.btnNext.setEnabled( False )
|
||||
self.btnPrev.setEnabled( False )
|
||||
|
||||
def reset( self ):
|
||||
self.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = None
|
||||
|
||||
self.btnNext.setEnabled( False )
|
||||
self.btnPrev.setEnabled( False )
|
||||
self.pageWidget.clear()
|
||||
def __init__(self, parent, metadata):
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
def setComicArchive(self, ca):
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui'), self)
|
||||
|
||||
self.comic_archive = ca
|
||||
self.page_count = ca.getNumberOfPages()
|
||||
self.current_page_num = 0
|
||||
self.pageWidget.setArchive( self.comic_archive )
|
||||
self.setPage()
|
||||
|
||||
if self.page_count > 1:
|
||||
self.btnNext.setEnabled( True )
|
||||
self.btnPrev.setEnabled( True )
|
||||
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
|
||||
|
||||
def nextPage(self):
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.current_page_num += 1
|
||||
else:
|
||||
self.current_page_num = 0
|
||||
self.setPage()
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
def prevPage(self):
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.current_page_num -= 1
|
||||
else:
|
||||
self.current_page_num = self.page_count - 1
|
||||
self.setPage()
|
||||
|
||||
def setPage( self ):
|
||||
if self.metadata is not None:
|
||||
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.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
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.btnNext.clicked.connect(self.nextPage)
|
||||
self.btnPrev.clicked.connect(self.prevPage)
|
||||
self.show()
|
||||
|
||||
self.btnNext.setEnabled(False)
|
||||
self.btnPrev.setEnabled(False)
|
||||
|
||||
def reset(self):
|
||||
self.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = None
|
||||
|
||||
self.btnNext.setEnabled(False)
|
||||
self.btnPrev.setEnabled(False)
|
||||
self.pageWidget.clear()
|
||||
|
||||
def setComicArchive(self, ca):
|
||||
|
||||
self.comic_archive = ca
|
||||
self.page_count = ca.getNumberOfPages()
|
||||
self.current_page_num = 0
|
||||
self.pageWidget.setArchive(self.comic_archive)
|
||||
self.setPage()
|
||||
|
||||
if self.page_count > 1:
|
||||
self.btnNext.setEnabled(True)
|
||||
self.btnPrev.setEnabled(True)
|
||||
|
||||
def nextPage(self):
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.current_page_num += 1
|
||||
else:
|
||||
self.current_page_num = 0
|
||||
self.setPage()
|
||||
|
||||
def prevPage(self):
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.current_page_num -= 1
|
||||
else:
|
||||
self.current_page_num = self.page_count - 1
|
||||
self.setPage()
|
||||
|
||||
def setPage(self):
|
||||
if self.metadata is not None:
|
||||
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))
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
"""
|
||||
A PyQt4 widget for editing the page list info
|
||||
"""
|
||||
"""A PyQt4 widget for editing the page list info"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
#import os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
@@ -27,247 +23,259 @@ from PyQt4 import uic
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from comicarchive import MetaDataStyle
|
||||
from pageloader import PageLoader
|
||||
from coverimagewidget import CoverImageWidget
|
||||
#from pageloader import PageLoader
|
||||
|
||||
|
||||
def itemMoveEvents( widget ):
|
||||
def itemMoveEvents(widget):
|
||||
|
||||
class Filter(QObject):
|
||||
|
||||
mysignal = pyqtSignal( str )
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
#print event.type()
|
||||
if event.type() == QEvent.ChildRemoved:
|
||||
#print "ChildRemoved"
|
||||
self.mysignal.emit("finish")
|
||||
if event.type() == QEvent.ChildAdded:
|
||||
#print "ChildAdded"
|
||||
self.mysignal.emit("start")
|
||||
return True
|
||||
|
||||
return False
|
||||
class Filter(QObject):
|
||||
|
||||
mysignal = pyqtSignal(str)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
# print(event.type())
|
||||
if event.type() == QEvent.ChildRemoved:
|
||||
# print("ChildRemoved")
|
||||
self.mysignal.emit("finish")
|
||||
if event.type() == QEvent.ChildAdded:
|
||||
# print("ChildAdded")
|
||||
self.mysignal.emit("start")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.mysignal
|
||||
|
||||
filter = Filter( widget )
|
||||
widget.installEventFilter( filter )
|
||||
return filter.mysignal
|
||||
|
||||
class PageListEditor(QWidget):
|
||||
|
||||
firstFrontCoverChanged = pyqtSignal( int )
|
||||
listOrderChanged = pyqtSignal( )
|
||||
modified = pyqtSignal( )
|
||||
|
||||
pageTypeNames = {
|
||||
PageType.FrontCover: "Front Cover",
|
||||
PageType.InnerCover: "Inner Cover",
|
||||
PageType.Advertisment: "Advertisment",
|
||||
PageType.Roundup: "Roundup",
|
||||
PageType.Story: "Story",
|
||||
PageType.Editorial: "Editorial",
|
||||
PageType.Letters: "Letters",
|
||||
PageType.Preview: "Preview",
|
||||
PageType.BackCover: "Back Cover",
|
||||
PageType.Other: "Other",
|
||||
PageType.Deleted: "Deleted",
|
||||
}
|
||||
firstFrontCoverChanged = pyqtSignal(int)
|
||||
listOrderChanged = pyqtSignal()
|
||||
modified = pyqtSignal()
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui' ), self)
|
||||
pageTypeNames = {
|
||||
PageType.FrontCover: "Front Cover",
|
||||
PageType.InnerCover: "Inner Cover",
|
||||
PageType.Advertisement: "Advertisement",
|
||||
PageType.Roundup: "Roundup",
|
||||
PageType.Story: "Story",
|
||||
PageType.Editorial: "Editorial",
|
||||
PageType.Letters: "Letters",
|
||||
PageType.Preview: "Preview",
|
||||
PageType.BackCover: "Back Cover",
|
||||
PageType.Other: "Other",
|
||||
PageType.Deleted: "Deleted",
|
||||
}
|
||||
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QGridLayout( self.pageContainer )
|
||||
gridlayout.addWidget( self.pageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
self.pageWidget.showControls = False
|
||||
def __init__(self, parent):
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
self.resetPage()
|
||||
|
||||
# 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.Advertisment], PageType.Advertisment )
|
||||
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 )
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui'), self)
|
||||
|
||||
self.listWidget.itemSelectionChanged.connect( self.changePage )
|
||||
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
|
||||
self.comboBox.activated.connect( self.changePageType )
|
||||
self.btnUp.clicked.connect( self.moveCurrentUp )
|
||||
self.btnDown.clicked.connect( self.moveCurrentDown )
|
||||
self.pre_move_row = -1
|
||||
self.first_front_page = None
|
||||
self.pageWidget = CoverImageWidget(
|
||||
self.pageContainer, CoverImageWidget.ArchiveMode)
|
||||
gridlayout = QGridLayout(self.pageContainer)
|
||||
gridlayout.addWidget(self.pageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
def resetPage( self ):
|
||||
self.pageWidget.clear()
|
||||
self.comboBox.setDisabled(True)
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
|
||||
def moveCurrentUp( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem ( row )
|
||||
self.listWidget.insertItem( row-1, item )
|
||||
self.listWidget.setCurrentRow( row-1 )
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
self.resetPage()
|
||||
|
||||
def moveCurrentDown( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
if row < self.listWidget.count()-1:
|
||||
item = self.listWidget.takeItem ( row )
|
||||
self.listWidget.insertItem( row+1, item )
|
||||
self.listWidget.setCurrentRow( row+1 )
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def itemMoveEvent(self, s):
|
||||
#print "move event: ", s, self.listWidget.currentRow()
|
||||
if s == "start":
|
||||
self.pre_move_row = self.listWidget.currentRow()
|
||||
if s == "finish":
|
||||
if self.pre_move_row != self.listWidget.currentRow():
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
# 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)
|
||||
|
||||
def changePageType( self , i):
|
||||
new_type = self.comboBox.itemData(i).toString()
|
||||
if self.getCurrentPageType() != new_type:
|
||||
self.setCurrentPageType( new_type )
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
self.listWidget.itemSelectionChanged.connect(self.changePage)
|
||||
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
|
||||
self.comboBox.activated.connect(self.changePageType)
|
||||
self.btnUp.clicked.connect(self.moveCurrentUp)
|
||||
self.btnDown.clicked.connect(self.moveCurrentDown)
|
||||
self.pre_move_row = -1
|
||||
self.first_front_page = None
|
||||
|
||||
def changePage( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
pagetype = self.getCurrentPageType()
|
||||
|
||||
i = self.comboBox.findData( pagetype )
|
||||
self.comboBox.setCurrentIndex( i )
|
||||
|
||||
#idx = int(str (self.listWidget.item( row ).text()))
|
||||
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
|
||||
def resetPage(self):
|
||||
self.pageWidget.clear()
|
||||
self.comboBox.setDisabled(True)
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.pageWidget.setArchive( self.comic_archive, idx )
|
||||
def moveCurrentUp(self):
|
||||
row = self.listWidget.currentRow()
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row - 1, item)
|
||||
self.listWidget.setCurrentRow(row - 1)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def getFirstFrontCover( self ):
|
||||
frontCover = 0
|
||||
for i in range( self.listWidget.count() ):
|
||||
item = self.listWidget.item( i )
|
||||
page_dict = item.data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict and page_dict['Type'] == PageType.FrontCover:
|
||||
frontCover = int(page_dict['Image'])
|
||||
break
|
||||
return frontCover
|
||||
|
||||
def moveCurrentDown(self):
|
||||
row = self.listWidget.currentRow()
|
||||
if row < self.listWidget.count() - 1:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row + 1, item)
|
||||
self.listWidget.setCurrentRow(row + 1)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def getCurrentPageType( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict:
|
||||
return page_dict['Type']
|
||||
else:
|
||||
return ""
|
||||
|
||||
def setCurrentPageType( self, t ):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
|
||||
def itemMoveEvent(self, s):
|
||||
# print "move event: ", s, self.listWidget.currentRow()
|
||||
if s == "start":
|
||||
self.pre_move_row = self.listWidget.currentRow()
|
||||
if s == "finish":
|
||||
if self.pre_move_row != self.listWidget.currentRow():
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
if t == "":
|
||||
if 'Type' in page_dict:
|
||||
del(page_dict['Type'])
|
||||
else:
|
||||
page_dict['Type'] = str(t)
|
||||
def changePageType(self, i):
|
||||
new_type = self.comboBox.itemData(i).toString()
|
||||
if self.getCurrentPageType() != new_type:
|
||||
self.setCurrentPageType(new_type)
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
item = self.listWidget.item( row )
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (page_dict,) )
|
||||
item.setText( self.listEntryText( page_dict ) )
|
||||
|
||||
def changePage(self):
|
||||
row = self.listWidget.currentRow()
|
||||
pagetype = self.getCurrentPageType()
|
||||
|
||||
def setData( self, comic_archive, pages_list ):
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
if pages_list is not None and len(pages_list) > 0:
|
||||
self.comboBox.setDisabled(False)
|
||||
i = self.comboBox.findData(pagetype)
|
||||
self.comboBox.setCurrentIndex(i)
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect( self.changePage )
|
||||
#idx = int(str (self.listWidget.item(row).text()))
|
||||
idx = int(self.listWidget.item(row).data(
|
||||
Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
self.listWidget.clear()
|
||||
for p in pages_list:
|
||||
item = QListWidgetItem( self.listEntryText( p ) )
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (p, ))
|
||||
|
||||
self.listWidget.addItem( item )
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.listWidget.itemSelectionChanged.connect( self.changePage )
|
||||
self.listWidget.setCurrentRow ( 0 )
|
||||
if self.comic_archive is not None:
|
||||
self.pageWidget.setArchive(self.comic_archive, idx)
|
||||
|
||||
def listEntryText(self, page_dict):
|
||||
text = str(int(page_dict['Image']) + 1)
|
||||
if 'Type' in page_dict:
|
||||
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
|
||||
return text
|
||||
|
||||
def getPageList( self ):
|
||||
page_list = []
|
||||
for i in range( self.listWidget.count() ):
|
||||
item = self.listWidget.item( i )
|
||||
page_list.append( item.data(Qt.UserRole).toPyObject()[0] )
|
||||
return page_list
|
||||
|
||||
def emitFrontCoverChange( self ):
|
||||
if self.first_front_page != self.getFirstFrontCover():
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.firstFrontCoverChanged.emit( self.first_front_page )
|
||||
def getFirstFrontCover(self):
|
||||
frontCover = 0
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
page_dict = item.data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict and page_dict[
|
||||
'Type'] == PageType.FrontCover:
|
||||
frontCover = int(page_dict['Image'])
|
||||
break
|
||||
return frontCover
|
||||
|
||||
def setMetadataStyle( self, data_style ):
|
||||
def getCurrentPageType(self):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict:
|
||||
return page_dict['Type']
|
||||
else:
|
||||
return ""
|
||||
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.comboBox.palette()
|
||||
|
||||
inactive_palette3 = self.comboBox.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
def setCurrentPageType(self, t):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(Qt.UserRole).toPyObject()[0]
|
||||
|
||||
if t == "":
|
||||
if 'Type' in page_dict:
|
||||
del(page_dict['Type'])
|
||||
else:
|
||||
page_dict['Type'] = str(t)
|
||||
|
||||
if data_style == MetaDataStyle.CIX:
|
||||
self.btnUp.setEnabled( True )
|
||||
self.btnDown.setEnabled( True )
|
||||
self.comboBox.setEnabled( True )
|
||||
self.listWidget.setEnabled( True )
|
||||
|
||||
self.listWidget.setPalette(active_palette)
|
||||
|
||||
elif data_style == MetaDataStyle.CBI:
|
||||
self.btnUp.setEnabled( False )
|
||||
self.btnDown.setEnabled( False )
|
||||
self.comboBox.setEnabled( False )
|
||||
self.listWidget.setEnabled( False )
|
||||
item = self.listWidget.item(row)
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (page_dict,))
|
||||
item.setText(self.listEntryText(page_dict))
|
||||
|
||||
self.listWidget.setPalette(inactive_palette3)
|
||||
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
# make sure combo is disabled when no list
|
||||
if self.comic_archive is None:
|
||||
self.comboBox.setEnabled( False )
|
||||
def setData(self, comic_archive, pages_list):
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
if pages_list is not None and len(pages_list) > 0:
|
||||
self.comboBox.setDisabled(False)
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect(self.changePage)
|
||||
|
||||
self.listWidget.clear()
|
||||
for p in pages_list:
|
||||
item = QListWidgetItem(self.listEntryText(p))
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (p,))
|
||||
|
||||
self.listWidget.addItem(item)
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.listWidget.itemSelectionChanged.connect(self.changePage)
|
||||
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']] + ")"
|
||||
return text
|
||||
|
||||
def getPageList(self):
|
||||
page_list = []
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
page_list.append(item.data(Qt.UserRole).toPyObject()[0])
|
||||
return page_list
|
||||
|
||||
def emitFrontCoverChange(self):
|
||||
if self.first_front_page != self.getFirstFrontCover():
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.firstFrontCoverChanged.emit(self.first_front_page)
|
||||
|
||||
def setMetadataStyle(self, data_style):
|
||||
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.comboBox.palette()
|
||||
|
||||
inactive_palette3 = self.comboBox.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
|
||||
if data_style == MetaDataStyle.CIX:
|
||||
self.btnUp.setEnabled(True)
|
||||
self.btnDown.setEnabled(True)
|
||||
self.comboBox.setEnabled(True)
|
||||
self.listWidget.setEnabled(True)
|
||||
|
||||
self.listWidget.setPalette(active_palette)
|
||||
|
||||
elif data_style == MetaDataStyle.CBI:
|
||||
self.btnUp.setEnabled(False)
|
||||
self.btnDown.setEnabled(False)
|
||||
self.comboBox.setEnabled(False)
|
||||
self.listWidget.setEnabled(False)
|
||||
|
||||
self.listWidget.setPalette(inactive_palette3)
|
||||
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
# make sure combo is disabled when no list
|
||||
if self.comic_archive is None:
|
||||
self.comboBox.setEnabled(False)
|
||||
|
||||
@@ -1,77 +1,70 @@
|
||||
"""
|
||||
A PyQT4 class to load a page image from a ComicArchive in a background thread
|
||||
"""
|
||||
"""A PyQT4 class to load a page image from a ComicArchive in a background thread"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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.
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comicarchive import ComicArchive
|
||||
from comictaggerlib.ui.qtutils import getQImageFromData
|
||||
#from comicarchive import ComicArchive
|
||||
#import utils
|
||||
|
||||
"""
|
||||
This class holds onto a reference of each instance in a list
|
||||
since problems occur if the ref count goes to zero and the GC
|
||||
tries to reap the object while the thread is going.
|
||||
|
||||
If the client class wants to stop the thread, they should mark
|
||||
it as "abandoned", and no signals will be issued
|
||||
"""
|
||||
|
||||
class PageLoader( QtCore.QThread ):
|
||||
class PageLoader(QtCore.QThread):
|
||||
|
||||
loadComplete = pyqtSignal( QtGui.QImage )
|
||||
|
||||
instanceList = []
|
||||
mutex = QtCore.QMutex()
|
||||
"""
|
||||
This class holds onto a reference of each instance in a list since
|
||||
problems occur if the ref count goes to zero and the GC tries to reap
|
||||
the object while the thread is going.
|
||||
If the client class wants to stop the thread, they should mark it as
|
||||
"abandoned", and no signals will be issued.
|
||||
"""
|
||||
|
||||
"""
|
||||
Remove all finished threads from the list
|
||||
"""
|
||||
@staticmethod
|
||||
def reapInstances():
|
||||
for obj in reversed(PageLoader.instanceList ):
|
||||
if obj.isFinished():
|
||||
PageLoader.instanceList.remove(obj)
|
||||
loadComplete = pyqtSignal(QtGui.QImage)
|
||||
|
||||
def __init__(self, ca, page_num ):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.ca = ca
|
||||
self.page_num = page_num
|
||||
self.abandoned = False
|
||||
instanceList = []
|
||||
mutex = QtCore.QMutex()
|
||||
|
||||
# remove any old instances, and then add ourself
|
||||
PageLoader.mutex.lock()
|
||||
PageLoader.reapInstances()
|
||||
PageLoader.instanceList.append( self )
|
||||
PageLoader.mutex.unlock()
|
||||
|
||||
def run(self):
|
||||
image_data = self.ca.getPage( self.page_num )
|
||||
if self.abandoned:
|
||||
return
|
||||
# Remove all finished threads from the list
|
||||
@staticmethod
|
||||
def reapInstances():
|
||||
for obj in reversed(PageLoader.instanceList):
|
||||
if obj.isFinished():
|
||||
PageLoader.instanceList.remove(obj)
|
||||
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
def __init__(self, ca, page_num):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.ca = ca
|
||||
self.page_num = page_num
|
||||
self.abandoned = False
|
||||
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
self.loadComplete.emit( img )
|
||||
# remove any old instances, and then add ourself
|
||||
PageLoader.mutex.lock()
|
||||
PageLoader.reapInstances()
|
||||
PageLoader.instanceList.append(self)
|
||||
PageLoader.mutex.unlock()
|
||||
|
||||
|
||||
def run(self):
|
||||
image_data = self.ca.getPage(self.page_num)
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
if image_data is not None:
|
||||
img = getQImageFromData(image_data)
|
||||
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
self.loadComplete.emit(img)
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
"""
|
||||
A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
"""A PyQT4 dialog to show ID log and progress"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
from settings import ComicTaggerSettings
|
||||
import utils
|
||||
#import utils
|
||||
|
||||
|
||||
class IDProgressWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(IDProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
def __init__(self, parent):
|
||||
super(IDProgressWindow, self).__init__(parent)
|
||||
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui'), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
|
||||
reduceWidgetFontSize(self.textEdit)
|
||||
|
||||
@@ -1,157 +1,163 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm rename
|
||||
"""
|
||||
"""A PyQT4 dialog to confirm rename"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from comicarchive import MetaDataStyle
|
||||
|
||||
import os
|
||||
import utils
|
||||
|
||||
|
||||
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]))
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
def __init__(self, parent, comic_archive_list, data_style, settings):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
|
||||
self.btnSettings.clicked.connect( self.modifySettings )
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui'), self)
|
||||
self.label.setText(
|
||||
"Preview (based on {0} tags):".format(
|
||||
MetaDataStyle.name[data_style]))
|
||||
|
||||
def configRenamer( self ):
|
||||
self.renamer = FileRenamer( None )
|
||||
self.renamer.setTemplate( self.settings.rename_template )
|
||||
self.renamer.setIssueZeroPadding( self.settings.rename_issue_number_padding )
|
||||
self.renamer.setSmartCleanup( self.settings.rename_use_smart_string_cleanup )
|
||||
|
||||
def doPreview( self ):
|
||||
self.rename_list = []
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for ca in self.comic_archive_list:
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
|
||||
new_ext = None # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
self.btnSettings.clicked.connect(self.modifySettings)
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
md = ca.readMetadata(self.data_style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
self.renamer.setMetadata( md )
|
||||
new_name = self.renamer.determineName( ca.path, ext=new_ext )
|
||||
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)
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
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)
|
||||
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)
|
||||
self.twList.setItem(row, 1, old_name_item)
|
||||
old_name_item.setText( item_text )
|
||||
old_name_item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
def doPreview(self):
|
||||
self.rename_list = []
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
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
|
||||
self.rename_list.append( dict_item)
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
# Adjust column sizes
|
||||
self.twList.setVisible( False )
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setVisible( True )
|
||||
if self.twList.columnWidth(0) > 200:
|
||||
self.twList.setColumnWidth(0, 200)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modifySettings( self ):
|
||||
settingswin = SettingsWindow( self, self.settings )
|
||||
settingswin.setModal(True)
|
||||
settingswin.showRenameTab()
|
||||
settingswin.exec_()
|
||||
if settingswin.result():
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def accept( self ):
|
||||
for ca in self.comic_archive_list:
|
||||
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog.setWindowTitle( "Renaming Archives" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.show()
|
||||
new_ext = None # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
for idx,item in enumerate(self.rename_list):
|
||||
md = ca.readMetadata(self.data_style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
||||
self.renamer.setMetadata(md)
|
||||
new_name = self.renamer.determineName(ca.path, ext=new_ext)
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
idx += 1
|
||||
progdialog.setLabelText( item['new_name'] )
|
||||
|
||||
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):
|
||||
continue
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( item['archive'].path ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, item['new_name'] ) )
|
||||
|
||||
os.rename( item['archive'].path, new_abs_path)
|
||||
|
||||
item['archive'].rename( new_abs_path )
|
||||
|
||||
progdialog.close()
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow(row)
|
||||
folder_item = QtGui.QTableWidgetItem()
|
||||
old_name_item = QtGui.QTableWidgetItem()
|
||||
new_name_item = QtGui.QTableWidgetItem()
|
||||
|
||||
QtGui.QDialog.accept(self)
|
||||
item_text = os.path.split(ca.path)[0]
|
||||
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)
|
||||
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)
|
||||
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
|
||||
self.rename_list.append(dict_item)
|
||||
|
||||
# Adjust column sizes
|
||||
self.twList.setVisible(False)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setVisible(True)
|
||||
if self.twList.columnWidth(0) > 200:
|
||||
self.twList.setColumnWidth(0, 200)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modifySettings(self):
|
||||
settingswin = SettingsWindow(self, self.settings)
|
||||
settingswin.setModal(True)
|
||||
settingswin.showRenameTab()
|
||||
settingswin.exec_()
|
||||
if settingswin.result():
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def accept(self):
|
||||
|
||||
progdialog = QtGui.QProgressDialog(
|
||||
"", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog.setWindowTitle("Renaming Archives")
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
for idx, item in enumerate(self.rename_list):
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
idx += 1
|
||||
progdialog.setLabelText(item['new_name'])
|
||||
|
||||
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):
|
||||
continue
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(item['archive'].path))
|
||||
new_abs_path = utils.unique_file(
|
||||
os.path.join(folder, item['new_name']))
|
||||
|
||||
os.rename(item['archive'].path, new_abs_path)
|
||||
|
||||
item['archive'].rename(new_abs_path)
|
||||
|
||||
progdialog.close()
|
||||
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
@@ -1,288 +1,522 @@
|
||||
"""
|
||||
Settings class for comictagger app
|
||||
"""
|
||||
"""Settings class for ComicTagger app"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
import configparser
|
||||
import platform
|
||||
import codecs
|
||||
import uuid
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
class ComicTaggerSettings:
|
||||
|
||||
@staticmethod
|
||||
def getSettingsFolder():
|
||||
if platform.system() == "Windows":
|
||||
return os.path.join( os.environ['APPDATA'], 'ComicTagger' )
|
||||
else:
|
||||
return os.path.join( os.path.expanduser('~') , '.ComicTagger')
|
||||
@staticmethod
|
||||
def getSettingsFolder():
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
if platform.system() == "Windows":
|
||||
folder = os.path.join(os.environ['APPDATA'], 'ComicTagger')
|
||||
else:
|
||||
folder = os.path.join(os.path.expanduser('~'), '.ComicTagger')
|
||||
if folder is not None:
|
||||
folder = folder.decode(filename_encoding)
|
||||
return folder
|
||||
|
||||
frozen_win_exe_path = None
|
||||
|
||||
@staticmethod
|
||||
def baseDir():
|
||||
if getattr(sys, 'frozen', None):
|
||||
if platform.system() == "Darwin":
|
||||
return sys._MEIPASS
|
||||
else: # Windows
|
||||
#Preserve this value, in case sys.argv gets changed importing a plugin script
|
||||
if ComicTaggerSettings.frozen_win_exe_path is None:
|
||||
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return ComicTaggerSettings.frozen_win_exe_path
|
||||
else:
|
||||
return os.path.dirname( os.path.abspath( __file__) )
|
||||
frozen_win_exe_path = None
|
||||
|
||||
@staticmethod
|
||||
def getGraphic( filename ):
|
||||
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')
|
||||
return os.path.join( ui_folder, filename )
|
||||
@staticmethod
|
||||
def baseDir():
|
||||
if getattr(sys, 'frozen', None):
|
||||
if platform.system() == "Darwin":
|
||||
return sys._MEIPASS
|
||||
else: # Windows
|
||||
# Preserve this value, in case sys.argv gets changed importing
|
||||
# a plugin script
|
||||
if ComicTaggerSettings.frozen_win_exe_path is None:
|
||||
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname(
|
||||
os.path.abspath(sys.argv[0]))
|
||||
return ComicTaggerSettings.frozen_win_exe_path
|
||||
else:
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def setDefaultValues( self ):
|
||||
@staticmethod
|
||||
def getGraphic(filename):
|
||||
graphic_folder = os.path.join(
|
||||
ComicTaggerSettings.baseDir(), 'graphics')
|
||||
return os.path.join(graphic_folder, filename)
|
||||
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.unrar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
|
||||
# automatic settings
|
||||
self.last_selected_save_data_style = 0
|
||||
self.last_selected_load_data_style = 0
|
||||
self.last_opened_folder = ""
|
||||
self.last_main_window_width = 0
|
||||
self.last_main_window_height = 0
|
||||
self.last_main_window_x = 0
|
||||
self.last_main_window_y = 0
|
||||
self.last_form_side_width = -1
|
||||
self.last_list_side_width = -1
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
self.copy_characters_to_tags = False
|
||||
self.copy_teams_to_tags = False
|
||||
self.copy_locations_to_tags = False
|
||||
self.copy_notes_to_comments = False
|
||||
self.copy_weblink_to_comments = False
|
||||
self.apply_cbl_transform_on_cv_import = False
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
@staticmethod
|
||||
def getUIFile(filename):
|
||||
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), 'ui')
|
||||
return os.path.join(ui_folder, filename)
|
||||
|
||||
# Rename settings
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
def setDefaultValues(self):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.settings_file = ""
|
||||
self.folder = ""
|
||||
self.setDefaultValues()
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.unrar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
self.check_for_new_version = True
|
||||
self.send_usage_stats = False
|
||||
|
||||
self.config = configparser.RawConfigParser()
|
||||
self.folder = ComicTaggerSettings.getSettingsFolder()
|
||||
|
||||
if not os.path.exists( self.folder ):
|
||||
os.makedirs( self.folder )
|
||||
|
||||
self.settings_file = os.path.join( self.folder, "settings")
|
||||
|
||||
# if config file doesn't exist, write one out
|
||||
if not os.path.exists( self.settings_file ):
|
||||
self.save()
|
||||
else:
|
||||
self.load()
|
||||
|
||||
# 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 machine
|
||||
if os.path.exists( "C:\Program Files\WinRAR\Rar.exe" ):
|
||||
self.rar_exe_path = "C:\Program Files\WinRAR\Rar.exe"
|
||||
elif os.path.exists( "C:\Program Files (x86)\WinRAR\Rar.exe" ):
|
||||
self.rar_exe_path = "C:\Program Files (x86)\WinRAR\Rar.exe"
|
||||
else:
|
||||
# see if it's in the path of unix user
|
||||
if utils.which("rar") is not None:
|
||||
self.rar_exe_path = utils.which("rar")
|
||||
if self.rar_exe_path != "":
|
||||
self.save()
|
||||
|
||||
if self.unrar_exe_path == "":
|
||||
if platform.system() != "Windows":
|
||||
# see if it's in the path of unix user
|
||||
if utils.which("unrar") is not None:
|
||||
self.unrar_exe_path = utils.which("unrar")
|
||||
if self.unrar_exe_path != "":
|
||||
self.save()
|
||||
|
||||
# make sure unrar/rar program is 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))
|
||||
# automatic settings
|
||||
self.install_id = uuid.uuid4().hex
|
||||
self.last_selected_save_data_style = 0
|
||||
self.last_selected_load_data_style = 0
|
||||
self.last_opened_folder = ""
|
||||
self.last_main_window_width = 0
|
||||
self.last_main_window_height = 0
|
||||
self.last_main_window_x = 0
|
||||
self.last_main_window_y = 0
|
||||
self.last_form_side_width = -1
|
||||
self.last_list_side_width = -1
|
||||
self.last_filelist_sorted_column = -1
|
||||
self.last_filelist_sorted_order = 0
|
||||
|
||||
def reset( self ):
|
||||
os.unlink( self.settings_file )
|
||||
self.__init__()
|
||||
|
||||
def load(self):
|
||||
|
||||
self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
|
||||
|
||||
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
|
||||
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
|
||||
|
||||
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' )
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
self.dont_notify_about_this_version = ""
|
||||
self.ask_about_usage_stats = True
|
||||
self.show_no_unrar_warning = True
|
||||
|
||||
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' )
|
||||
# filename parsing settings
|
||||
self.parse_scan_info = True
|
||||
|
||||
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('comicvine', 'use_series_start_as_volume'):
|
||||
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
self.clear_form_before_populating_from_cv = False
|
||||
self.remove_html_tables = False
|
||||
self.cv_api_key = ""
|
||||
|
||||
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_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' )
|
||||
|
||||
def save( self ):
|
||||
# CBL Tranform settings
|
||||
|
||||
if not self.config.has_section( 'settings' ):
|
||||
self.config.add_section( 'settings' )
|
||||
|
||||
self.config.set( 'settings', 'rar_exe_path', self.rar_exe_path )
|
||||
self.config.set( 'settings', 'unrar_exe_path', self.unrar_exe_path )
|
||||
self.assume_lone_credit_is_primary = False
|
||||
self.copy_characters_to_tags = False
|
||||
self.copy_teams_to_tags = False
|
||||
self.copy_locations_to_tags = False
|
||||
self.copy_storyarcs_to_tags = False
|
||||
self.copy_notes_to_comments = False
|
||||
self.copy_weblink_to_comments = False
|
||||
self.apply_cbl_transform_on_cv_import = False
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
|
||||
if not self.config.has_section( 'auto' ):
|
||||
self.config.add_section( 'auto' )
|
||||
# Rename settings
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
|
||||
self.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 )
|
||||
# Auto-tag stickies
|
||||
self.save_on_low_confidence = False
|
||||
self.dont_use_year_when_identifying = False
|
||||
self.assume_1_if_no_issue_num = False
|
||||
self.ignore_leading_numbers_in_filename = False
|
||||
self.remove_archive_after_successful_match = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
|
||||
if not self.config.has_section( 'identifier' ):
|
||||
self.config.add_section( 'identifier' )
|
||||
def __init__(self):
|
||||
|
||||
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.settings_file = ""
|
||||
self.folder = ""
|
||||
self.setDefaultValues()
|
||||
|
||||
if not self.config.has_section( 'dialogflags' ):
|
||||
self.config.add_section( 'dialogflags' )
|
||||
self.config = configparser.RawConfigParser()
|
||||
self.folder = ComicTaggerSettings.getSettingsFolder()
|
||||
|
||||
self.config.set( 'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar )
|
||||
self.config.set( 'dialogflags', 'show_disclaimer', self.show_disclaimer )
|
||||
if not os.path.exists(self.folder):
|
||||
os.makedirs(self.folder)
|
||||
|
||||
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.settings_file = os.path.join(self.folder, "settings")
|
||||
|
||||
if not self.config.has_section( 'cbl_transform' ):
|
||||
self.config.add_section( 'cbl_transform' )
|
||||
# if config file doesn't exist, write one out
|
||||
if not os.path.exists(self.settings_file):
|
||||
self.save()
|
||||
else:
|
||||
self.load()
|
||||
|
||||
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_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 )
|
||||
# take a crack at finding rar exes, if not set already
|
||||
if self.rar_exe_path == "":
|
||||
if platform.system() == "Windows":
|
||||
# look in some likely places for Windows machines
|
||||
if os.path.exists("C:\Program Files\WinRAR\Rar.exe"):
|
||||
self.rar_exe_path = "C:\Program Files\WinRAR\Rar.exe"
|
||||
elif os.path.exists("C:\Program Files (x86)\WinRAR\Rar.exe"):
|
||||
self.rar_exe_path = "C:\Program Files (x86)\WinRAR\Rar.exe"
|
||||
else:
|
||||
# see if it's in the path of unix user
|
||||
if utils.which("rar") is not None:
|
||||
self.rar_exe_path = utils.which("rar")
|
||||
if self.rar_exe_path != "":
|
||||
self.save()
|
||||
|
||||
if not self.config.has_section( 'rename' ):
|
||||
self.config.add_section( 'rename' )
|
||||
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()
|
||||
|
||||
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 )
|
||||
|
||||
with codecs.open( self.settings_file, 'wb', 'utf8') as configfile:
|
||||
self.config.write(configfile)
|
||||
# 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))
|
||||
|
||||
#make sure the basedir is cached, in case we're on windows running a script from frozen binary
|
||||
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.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('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('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(
|
||||
'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('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')
|
||||
|
||||
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')
|
||||
|
||||
self.config.set('auto', 'install_id', self.install_id)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_selected_load_data_style',
|
||||
self.last_selected_load_data_style)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_selected_save_data_style',
|
||||
self.last_selected_save_data_style)
|
||||
self.config.set('auto', 'last_opened_folder', self.last_opened_folder)
|
||||
self.config.set(
|
||||
'auto', 'last_main_window_width', self.last_main_window_width)
|
||||
self.config.set(
|
||||
'auto', 'last_main_window_height', self.last_main_window_height)
|
||||
self.config.set('auto', 'last_main_window_x', self.last_main_window_x)
|
||||
self.config.set('auto', 'last_main_window_y', self.last_main_window_y)
|
||||
self.config.set(
|
||||
'auto', 'last_form_side_width', self.last_form_side_width)
|
||||
self.config.set(
|
||||
'auto', 'last_list_side_width', self.last_list_side_width)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_filelist_sorted_column',
|
||||
self.last_filelist_sorted_column)
|
||||
self.config.set(
|
||||
'auto',
|
||||
'last_filelist_sorted_order',
|
||||
self.last_filelist_sorted_order)
|
||||
|
||||
if not self.config.has_section('identifier'):
|
||||
self.config.add_section('identifier')
|
||||
|
||||
self.config.set(
|
||||
'identifier',
|
||||
'id_length_delta_thresh',
|
||||
self.id_length_delta_thresh)
|
||||
self.config.set(
|
||||
'identifier',
|
||||
'id_publisher_blacklist',
|
||||
self.id_publisher_blacklist)
|
||||
|
||||
if not self.config.has_section('dialogflags'):
|
||||
self.config.add_section('dialogflags')
|
||||
|
||||
self.config.set(
|
||||
'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar)
|
||||
self.config.set('dialogflags', 'show_disclaimer', self.show_disclaimer)
|
||||
self.config.set(
|
||||
'dialogflags',
|
||||
'dont_notify_about_this_version',
|
||||
self.dont_notify_about_this_version)
|
||||
self.config.set(
|
||||
'dialogflags', 'ask_about_usage_stats', self.ask_about_usage_stats)
|
||||
self.config.set(
|
||||
'dialogflags', 'show_no_unrar_warning', self.show_no_unrar_warning)
|
||||
|
||||
if not self.config.has_section('filenameparser'):
|
||||
self.config.add_section('filenameparser')
|
||||
|
||||
self.config.set(
|
||||
'filenameparser', 'parse_scan_info', self.parse_scan_info)
|
||||
|
||||
if not self.config.has_section('comicvine'):
|
||||
self.config.add_section('comicvine')
|
||||
|
||||
self.config.set(
|
||||
'comicvine',
|
||||
'use_series_start_as_volume',
|
||||
self.use_series_start_as_volume)
|
||||
self.config.set('comicvine', 'clear_form_before_populating_from_cv',
|
||||
self.clear_form_before_populating_from_cv)
|
||||
self.config.set(
|
||||
'comicvine', 'remove_html_tables', self.remove_html_tables)
|
||||
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
|
||||
|
||||
if not self.config.has_section('cbl_transform'):
|
||||
self.config.add_section('cbl_transform')
|
||||
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'assume_lone_credit_is_primary',
|
||||
self.assume_lone_credit_is_primary)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_characters_to_tags',
|
||||
self.copy_characters_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_locations_to_tags',
|
||||
self.copy_locations_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_storyarcs_to_tags',
|
||||
self.copy_storyarcs_to_tags)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_notes_to_comments',
|
||||
self.copy_notes_to_comments)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'copy_weblink_to_comments',
|
||||
self.copy_weblink_to_comments)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'apply_cbl_transform_on_cv_import',
|
||||
self.apply_cbl_transform_on_cv_import)
|
||||
self.config.set(
|
||||
'cbl_transform',
|
||||
'apply_cbl_transform_on_bulk_operation',
|
||||
self.apply_cbl_transform_on_bulk_operation)
|
||||
|
||||
if not self.config.has_section('rename'):
|
||||
self.config.add_section('rename')
|
||||
|
||||
self.config.set('rename', 'rename_template', self.rename_template)
|
||||
self.config.set(
|
||||
'rename',
|
||||
'rename_issue_number_padding',
|
||||
self.rename_issue_number_padding)
|
||||
self.config.set(
|
||||
'rename',
|
||||
'rename_use_smart_string_cleanup',
|
||||
self.rename_use_smart_string_cleanup)
|
||||
self.config.set('rename', 'rename_extension_based_on_archive',
|
||||
self.rename_extension_based_on_archive)
|
||||
|
||||
if not self.config.has_section('autotag'):
|
||||
self.config.add_section('autotag')
|
||||
self.config.set(
|
||||
'autotag', 'save_on_low_confidence', self.save_on_low_confidence)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'dont_use_year_when_identifying',
|
||||
self.dont_use_year_when_identifying)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'assume_1_if_no_issue_num',
|
||||
self.assume_1_if_no_issue_num)
|
||||
self.config.set('autotag', 'ignore_leading_numbers_in_filename',
|
||||
self.ignore_leading_numbers_in_filename)
|
||||
self.config.set('autotag', 'remove_archive_after_successful_match',
|
||||
self.remove_archive_after_successful_match)
|
||||
self.config.set(
|
||||
'autotag',
|
||||
'wait_and_retry_on_rate_limit',
|
||||
self.wait_and_retry_on_rate_limit)
|
||||
|
||||
with codecs.open(self.settings_file, 'wb', 'utf8') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
# make sure the basedir is cached, in case we're on Windows running a
|
||||
# script from frozen binary
|
||||
ComicTaggerSettings.baseDir()
|
||||
|
||||
@@ -1,230 +1,272 @@
|
||||
"""
|
||||
A PyQT4 dialog to enter app settings
|
||||
"""
|
||||
"""A PyQT4 dialog to enter app settings"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 platform
|
||||
import os
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from comicvinetalker import ComicVineTalker
|
||||
from imagefetcher import ImageFetcher
|
||||
import utils
|
||||
|
||||
|
||||
windowsRarHelp = """
|
||||
<html><head/><body><p>In order to write to CBR/RAR archives,
|
||||
you will need to have the tools from
|
||||
<html><head/><body><p>In order to write to CBR/RAR archives,
|
||||
you will need to have the tools from
|
||||
<a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span>
|
||||
</a> installed. </p></body></html>
|
||||
"""
|
||||
|
||||
|
||||
linuxRarHelp = """
|
||||
<html><head/><body><p>In order to read/write to CBR/RAR archives, you will
|
||||
need to have the shareware tools from WinRar installed. Your package manager
|
||||
should have unrar, and probably rar. If not, download them <a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">here</span></a>, and install in your path.</p>
|
||||
</body></html>
|
||||
<html><head/><body><p>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>In order to read/write to CBR/RAR archives,
|
||||
you will need the shareware tools from <a href="http://www.win-rar.com/download.html">
|
||||
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a>.
|
||||
</p></body></html>
|
||||
<html><head/><body><p>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(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, settings ):
|
||||
super(SettingsWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
def __init__(self, parent, settings):
|
||||
super(SettingsWindow, self).__init__(parent)
|
||||
|
||||
self.settings = settings
|
||||
self.name = "Settings"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
self.lblUnrar.hide()
|
||||
self.leUnrarExePath.hide()
|
||||
self.btnBrowseUnrar.hide()
|
||||
self.lblRarHelp.setText( windowsRarHelp )
|
||||
|
||||
elif platform.system() == "Linux":
|
||||
self.lblRarHelp.setText( linuxRarHelp )
|
||||
|
||||
elif platform.system() == "Darwin":
|
||||
self.lblRarHelp.setText( macRarHelp )
|
||||
self.name = "Preferences"
|
||||
|
||||
self.setWindowTitle("ComicTagger " + self.name)
|
||||
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
|
||||
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>""" )
|
||||
|
||||
self.leNameLengthDeltaThresh.setToolTip(nldtTip)
|
||||
|
||||
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)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui'), self)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthDeltaThresh.setValidator(validator)
|
||||
self.settings = settings
|
||||
self.name = "Settings"
|
||||
|
||||
self.settingsToForm()
|
||||
|
||||
self.btnBrowseRar.clicked.connect(self.selectRar)
|
||||
self.btnBrowseUnrar.clicked.connect(self.selectUnrar)
|
||||
self.btnClearCache.clicked.connect(self.clearCache)
|
||||
self.btnResetSettings.clicked.connect(self.resetSettings)
|
||||
if platform.system() == "Windows":
|
||||
self.lblUnrar.hide()
|
||||
self.leUnrarExePath.hide()
|
||||
self.btnBrowseUnrar.hide()
|
||||
self.lblRarHelp.setText(windowsRarHelp)
|
||||
|
||||
def settingsToForm( self ):
|
||||
|
||||
# Copy values from settings to form
|
||||
self.leRarExePath.setText( self.settings.rar_exe_path )
|
||||
self.leUnrarExePath.setText( self.settings.unrar_exe_path )
|
||||
self.leNameLengthDeltaThresh.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.tePublisherBlacklist.setPlainText( self.settings.id_publisher_blacklist )
|
||||
elif platform.system() == "Linux":
|
||||
self.lblRarHelp.setText(linuxRarHelp)
|
||||
|
||||
if self.settings.use_series_start_as_volume:
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_characters_to_tags:
|
||||
self.cbxCopyCharactersToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_teams_to_tags:
|
||||
self.cbxCopyTeamsToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_locations_to_tags:
|
||||
self.cbxCopyLocationsToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_notes_to_comments:
|
||||
self.cbxCopyNotesToComments.setCheckState( QtCore.Qt.Checked)
|
||||
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)
|
||||
if self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setCheckState( QtCore.Qt.Checked)
|
||||
elif platform.system() == "Darwin":
|
||||
self.lblRarHelp.setText(macRarHelp)
|
||||
self.name = "Preferences"
|
||||
|
||||
self.leRenameTemplate.setText( self.settings.rename_template )
|
||||
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 )
|
||||
self.setWindowTitle("ComicTagger " + self.name)
|
||||
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
|
||||
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>""")
|
||||
|
||||
def accept( self ):
|
||||
|
||||
# Copy values from form to settings and save
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
|
||||
|
||||
# make sure unrar/rar program is now in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
self.leNameLengthDeltaThresh.setToolTip(nldtTip)
|
||||
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
|
||||
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()
|
||||
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
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_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
|
||||
def selectRar( self ):
|
||||
self.selectFile( self.leRarExePath, "RAR" )
|
||||
|
||||
def selectUnrar( self ):
|
||||
self.selectFile( self.leUnrarExePath, "UnRAR" )
|
||||
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)
|
||||
|
||||
def clearCache( self ):
|
||||
ImageFetcher().clearCache()
|
||||
ComicVineCacher( ).clearCache()
|
||||
QtGui.QMessageBox.information(self, self.name, "Cache has been cleared.")
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
|
||||
|
||||
def resetSettings( self ):
|
||||
self.settings.reset()
|
||||
self.settingsToForm()
|
||||
QtGui.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
|
||||
|
||||
def selectFile( self, control, name ):
|
||||
|
||||
dialog = QtGui.QFileDialog(self)
|
||||
dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
if name == "RAR":
|
||||
filter = self.tr("Rar Program (Rar.exe)")
|
||||
else:
|
||||
filter = self.tr("Programs (*.exe)")
|
||||
dialog.setNameFilter(filter)
|
||||
else:
|
||||
dialog.setFilter(QtCore.QDir.Files) #QtCore.QDir.Executable | QtCore.QDir.Files)
|
||||
pass
|
||||
|
||||
dialog.setDirectory(os.path.dirname(str(control.text())))
|
||||
dialog.setWindowTitle("Find " + name + " program")
|
||||
|
||||
if (dialog.exec_()):
|
||||
fileList = dialog.selectedFiles()
|
||||
control.setText( str(fileList[0]) )
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthDeltaThresh.setValidator(validator)
|
||||
|
||||
def showRenameTab( self ):
|
||||
self.tabWidget.setCurrentIndex(4)
|
||||
|
||||
self.settingsToForm()
|
||||
|
||||
self.btnBrowseRar.clicked.connect(self.selectRar)
|
||||
self.btnBrowseUnrar.clicked.connect(self.selectUnrar)
|
||||
self.btnClearCache.clicked.connect(self.clearCache)
|
||||
self.btnResetSettings.clicked.connect(self.resetSettings)
|
||||
self.btnTestKey.clicked.connect(self.testAPIKey)
|
||||
|
||||
def settingsToForm(self):
|
||||
|
||||
# Copy values from settings to form
|
||||
self.leRarExePath.setText(self.settings.rar_exe_path)
|
||||
self.leUnrarExePath.setText(self.settings.unrar_exe_path)
|
||||
self.leNameLengthDeltaThresh.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.tePublisherBlacklist.setPlainText(
|
||||
self.settings.id_publisher_blacklist)
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.parse_scan_info:
|
||||
self.cbxParseScanInfo.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.use_series_start_as_volume:
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.clear_form_before_populating_from_cv:
|
||||
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.remove_html_tables:
|
||||
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
|
||||
self.leKey.setText(str(self.settings.cv_api_key))
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_characters_to_tags:
|
||||
self.cbxCopyCharactersToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_teams_to_tags:
|
||||
self.cbxCopyTeamsToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_locations_to_tags:
|
||||
self.cbxCopyLocationsToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_storyarcs_to_tags:
|
||||
self.cbxCopyStoryArcsToTags.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.copy_notes_to_comments:
|
||||
self.cbxCopyNotesToComments.setCheckState(QtCore.Qt.Checked)
|
||||
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)
|
||||
if self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(
|
||||
QtCore.Qt.Checked)
|
||||
|
||||
self.leRenameTemplate.setText(self.settings.rename_template)
|
||||
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)
|
||||
|
||||
def accept(self):
|
||||
|
||||
# Copy values from form to settings and save
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
|
||||
|
||||
# make sure unrar/rar program is now in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
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.parse_scan_info = self.cbxParseScanInfo.isChecked()
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
|
||||
self.settings.cv_api_key = unicode(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key
|
||||
self.settings.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()
|
||||
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.settings.copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
|
||||
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
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_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
def selectRar(self):
|
||||
self.selectFile(self.leRarExePath, "RAR")
|
||||
|
||||
def selectUnrar(self):
|
||||
self.selectFile(self.leUnrarExePath, "UnRAR")
|
||||
|
||||
def clearCache(self):
|
||||
ImageFetcher().clearCache()
|
||||
ComicVineCacher().clearCache()
|
||||
QtGui.QMessageBox.information(
|
||||
self, self.name, "Cache has been cleared.")
|
||||
|
||||
def testAPIKey(self):
|
||||
if ComicVineTalker().testKey(unicode(self.leKey.text())):
|
||||
QtGui.QMessageBox.information(
|
||||
self, "API Key Test", "Key is valid!")
|
||||
else:
|
||||
QtGui.QMessageBox.warning(
|
||||
self, "API Key Test", "Key is NOT valid.")
|
||||
|
||||
def resetSettings(self):
|
||||
self.settings.reset()
|
||||
self.settingsToForm()
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
self.name,
|
||||
self.name +
|
||||
" have been returned to default values.")
|
||||
|
||||
def selectFile(self, control, name):
|
||||
|
||||
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("Programs (*.exe)")
|
||||
dialog.setNameFilter(filter)
|
||||
else:
|
||||
# QtCore.QDir.Executable | QtCore.QDir.Files)
|
||||
dialog.setFilter(QtCore.QDir.Files)
|
||||
pass
|
||||
|
||||
dialog.setDirectory(os.path.dirname(str(control.text())))
|
||||
dialog.setWindowTitle("Find " + name + " program")
|
||||
|
||||
if (dialog.exec_()):
|
||||
fileList = dialog.selectedFiles()
|
||||
control.setText(str(fileList[0]))
|
||||
|
||||
def showRenameTab(self):
|
||||
self.tabWidget.setCurrentIndex(5)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
0
comictaggerlib/ui/__init__.py
Normal file
0
comictaggerlib/ui/__init__.py
Normal file
@@ -6,21 +6,18 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>907</width>
|
||||
<height>507</height>
|
||||
<width>943</width>
|
||||
<height>467</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
@@ -38,45 +35,73 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</column>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -21,6 +21,44 @@
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QWidget" name="archiveCoverContainer">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QWidget" name="testCoverContainer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
@@ -66,50 +104,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="lblTest">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>607</width>
|
||||
<height>319</height>
|
||||
<width>519</width>
|
||||
<height>378</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@@ -25,183 +25,180 @@
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="cbxSpecifySearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
<string>Specify series search string for all selected archives:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
<property name="text">
|
||||
<string>Save on low confidence match</string>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save on low confidence match</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxIgnoreLeadingDigitsInFilename">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Ignore leading (sequence) numbers in filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxRemoveAfterSuccess">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove archives from list after successful tagging</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSpecifySearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Specify series search string for all selected archives</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="leSearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QLineEdit" name="leNameLengthMatchTolerance">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Adjust Name Length Match Tolerance:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="cbxIgnoreLeadingDigitsInFilename">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
<property name="text">
|
||||
<string>Ignore leading (sequence) numbers in filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="cbxRemoveAfterSuccess">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove archives from list after successful tagging</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="cbxWaitForRateLimit">
|
||||
<property name="text">
|
||||
<string>Wait and retry when Comic Vine rate limit is exceeded (experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<widget class="QLineEdit" name="leNameLengthMatchTolerance">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLineEdit" name="leSearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Adjust Name Length Match Tolerance:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
||||
@@ -6,51 +6,90 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>657</width>
|
||||
<height>400</height>
|
||||
<width>872</width>
|
||||
<height>550</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Issue</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Issue</string>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</column>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Issue</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -6,21 +6,18 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>907</width>
|
||||
<height>507</height>
|
||||
<width>943</width>
|
||||
<height>467</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
@@ -38,45 +35,73 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</column>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
91
comictaggerlib/ui/qtutils.py
Normal file
91
comictaggerlib/ui/qtutils.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Some utilities for the GUI"""
|
||||
|
||||
#import StringIO
|
||||
|
||||
#from PIL import Image
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
|
||||
try:
|
||||
from PyQt4 import QtGui
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
|
||||
if qt_available:
|
||||
|
||||
def reduceWidgetFontSize(widget, delta=2):
|
||||
f = widget.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize(f.pointSize() - delta)
|
||||
widget.setFont(f)
|
||||
|
||||
def centerWindowOnScreen(window):
|
||||
"""Center the window on screen.
|
||||
|
||||
This implementation will handle the window
|
||||
being resized or the screen resolution changing.
|
||||
"""
|
||||
# Get the current screens' dimensions...
|
||||
screen = QtGui.QDesktopWidget().screenGeometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calculated as screen width - window width
|
||||
# / 2
|
||||
hpos = (screen.width() - window.width()) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = (screen.height() - window.height()) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos, vpos)
|
||||
|
||||
def centerWindowOnParent(window):
|
||||
|
||||
top_level = window
|
||||
while top_level.parent() is not None:
|
||||
top_level = top_level.parent()
|
||||
|
||||
# Get the current screens' dimensions...
|
||||
main_window_size = top_level.geometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calculated as screen width - window width
|
||||
# /2
|
||||
hpos = (main_window_size.width() - window.width()) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = (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())
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
import StringIO
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
def getQImageFromData(image_data):
|
||||
img = QtGui.QImage()
|
||||
success = img.loadFromData(image_data)
|
||||
if not success:
|
||||
try:
|
||||
if pil_available:
|
||||
# Qt doesn't understand the format, but maybe PIL does
|
||||
# so try to convert the image data to uncompressed tiff
|
||||
# format
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
output = StringIO.StringIO()
|
||||
im.save(output, format="TIFF")
|
||||
img.loadFromData(output.getvalue())
|
||||
success = True
|
||||
except Exception as e:
|
||||
pass
|
||||
# if still nothing, go with default image
|
||||
if not success:
|
||||
img.load(ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
return img
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>674</width>
|
||||
<height>428</height>
|
||||
<width>702</width>
|
||||
<height>432</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -21,6 +21,12 @@
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@@ -35,7 +41,7 @@
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="btnResetSettings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@@ -48,7 +54,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="lblDefaultSettings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@@ -64,7 +70,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="btnClearCache">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@@ -77,7 +83,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@@ -93,6 +99,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxCheckForNewVersion">
|
||||
<property name="text">
|
||||
<string>Check for new version on startup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -307,24 +320,179 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<widget class="QWidget" name="tab_6">
|
||||
<attribute name="title">
|
||||
<string>Comic Vine</string>
|
||||
<string>Filename Parser</string>
|
||||
</attribute>
|
||||
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
|
||||
<widget class="QCheckBox" name="cbxParseScanInfo">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>240</width>
|
||||
<width>421</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Series Start Date as Volume</string>
|
||||
<string>Parse Scan Info From Filename (Experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Comic Vine</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="grpBoxCVTop">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
|
||||
<property name="text">
|
||||
<string>Use Series Start Date as Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
|
||||
<property name="text">
|
||||
<string>Clear Form Before Importing Comic Vine data</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxRemoveHtmlTables">
|
||||
<property name="text">
|
||||
<string>Remove HTML tables from CV summary field</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_4">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="grpBoxKey">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_8">
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leKey">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Comic Vine API Key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="lblKeyHelp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>A personal API key from <a href="http://www.comicvine.com/api/"><span style=" text-decoration: underline; color:#0000ff;">Comic Vine</span></a> is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.</p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btnTestKey">
|
||||
<property name="text">
|
||||
<string>Tesk Key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_4">
|
||||
<attribute name="title">
|
||||
<string>CBL</string>
|
||||
@@ -360,11 +528,18 @@
|
||||
<rect>
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>246</width>
|
||||
<height>182</height>
|
||||
<width>251</width>
|
||||
<height>192</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Locations to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
|
||||
<property name="text">
|
||||
@@ -386,27 +561,27 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Locations to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyNotesToComments">
|
||||
<property name="text">
|
||||
<string>Copy Notes to Comments</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyWebLinkToComments">
|
||||
<property name="text">
|
||||
<string>Copy Web Link to Comments</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyStoryArcsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Story Arcs to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
||||
@@ -402,14 +402,26 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<widget class="QLabel" name="lblDay">
|
||||
<property name="text">
|
||||
<string># Issues</string>
|
||||
<string>Day</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="leIssueCount">
|
||||
<widget class="QLineEdit" name="lePubDay">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
@@ -419,27 +431,44 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string># Issues</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="leIssueCount">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="inputMethodHints">
|
||||
<set>Qt::ImhDigitsOnly</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="leVolumeNum">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string># Volumes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="leVolumeCount">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
@@ -449,14 +478,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_22">
|
||||
<property name="text">
|
||||
<string>Alt.Issue</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="7" column="1">
|
||||
<widget class="QLineEdit" name="leAltIssueNum">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
@@ -469,14 +498,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>Alt. # Issues</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="8" column="1">
|
||||
<widget class="QLineEdit" name="leAltIssueCount">
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
|
||||
@@ -42,17 +42,20 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
@@ -102,15 +105,15 @@
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDetails">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>200</height>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
|
||||
@@ -1,628 +1 @@
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
Some generic utilities
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 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 getattr(sys, 'frozen', None) and 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 recursivly, and all files underneath
|
||||
if type(p) == str:
|
||||
#make sure string is unicode
|
||||
p = p.decode(filename_encoding) #, 'replace')
|
||||
elif type(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 type(f) == str:
|
||||
#make sure string is unicode
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
elif type(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']
|
||||
|
||||
# returns executable path, if it exists
|
||||
def which(program):
|
||||
|
||||
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(",", "")
|
||||
newText = newText.replace("-", " ")
|
||||
|
||||
return newText
|
||||
|
||||
|
||||
def unique_file(file_name):
|
||||
counter = 1
|
||||
file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext')
|
||||
while 1:
|
||||
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
|
||||
# oficial 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 == None:
|
||||
return None
|
||||
else:
|
||||
return lang_dict[ iso ]
|
||||
|
||||
|
||||
try:
|
||||
from PyQt4 import QtGui
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
|
||||
if qt_available:
|
||||
def reduceWidgetFontSize( widget , delta = 2):
|
||||
f = widget.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - delta )
|
||||
widget.setFont( f )
|
||||
|
||||
def centerWindowOnScreen( window ):
|
||||
"""
|
||||
Center the window on screen. This implemention will handle the window
|
||||
being resized or the screen resolution changing.
|
||||
"""
|
||||
# Get the current screens' dimensions...
|
||||
screen = QtGui.QDesktopWidget().screenGeometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calulated as screenwidth - windowwidth /2
|
||||
hpos = ( screen.width() - window.width() ) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = ( screen.height() - window.height() ) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos, vpos)
|
||||
|
||||
def centerWindowOnParent( window ):
|
||||
|
||||
top_level = window
|
||||
while top_level.parent() is not None:
|
||||
top_level = top_level.parent()
|
||||
|
||||
# Get the current screens' dimensions...
|
||||
main_window_size = top_level.geometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calulated as screenwidth - windowwidth /2
|
||||
hpos = ( main_window_size.width() - window.width() ) / 2
|
||||
# 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())
|
||||
from comicapi.utils import *
|
||||
|
||||
99
comictaggerlib/versionchecker.py
Normal file
99
comictaggerlib/versionchecker.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Version checker"""
|
||||
|
||||
# Copyright 2013 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 platform
|
||||
import urllib2
|
||||
#import os
|
||||
#import urllib
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
|
||||
def __init__(self, *args):
|
||||
pass
|
||||
|
||||
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 = ""
|
||||
|
||||
if use_stats:
|
||||
if platform.system() == "Windows":
|
||||
plat = "win"
|
||||
elif platform.system() == "Linux":
|
||||
plat = "lin"
|
||||
elif platform.system() == "Darwin":
|
||||
plat = "mac"
|
||||
else:
|
||||
plat = "other"
|
||||
args = "?uuid={0}&platform={1}&version={2}".format(
|
||||
uuid, plat, ctversion.version)
|
||||
if not getattr(sys, 'frozen', None):
|
||||
args += "&src=T"
|
||||
|
||||
return base_url + args
|
||||
|
||||
def getLatestVersion(self, uuid, use_stats=True):
|
||||
|
||||
try:
|
||||
resp = urllib2.urlopen(self.getRequestUrl(uuid, use_stats))
|
||||
new_version = resp.read()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return None
|
||||
return new_version.strip()
|
||||
|
||||
versionRequestComplete = pyqtSignal(str)
|
||||
|
||||
def asyncGetLatestVersion(self, uuid, use_stats):
|
||||
|
||||
url = self.getRequestUrl(uuid, use_stats)
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect(self.asyncGetLatestVersionComplete)
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url))))
|
||||
|
||||
def asyncGetLatestVersionComplete(self, reply):
|
||||
if (reply.error() != QNetworkReply.NoError):
|
||||
return
|
||||
|
||||
# read in the response
|
||||
new_version = str(reply.readAll())
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return
|
||||
|
||||
self.versionRequestComplete.emit(new_version.strip())
|
||||
@@ -1,375 +1,418 @@
|
||||
"""
|
||||
A PyQT4 dialog to select specific series/volume from list
|
||||
"""
|
||||
"""A PyQT4 dialog to select specific series/volume from list"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 time
|
||||
#import os
|
||||
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import QObject
|
||||
from PyQt4.QtCore import QUrl,pyqtSignal
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal
|
||||
#from PyQt4.QtCore import QObject
|
||||
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from issueselectionwindow import IssueSelectionWindow
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from imagefetcher import ImageFetcher
|
||||
from progresswindow import IDProgressWindow
|
||||
from settings import ComicTaggerSettings
|
||||
from matchselectionwindow import MatchSelectionWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class SearchThread( QtCore.QThread):
|
||||
|
||||
searchComplete = pyqtSignal()
|
||||
progressUpdate = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, series_name, refresh):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.series_name = series_name
|
||||
self.refresh = refresh
|
||||
|
||||
def run(self):
|
||||
comicVine = ComicVineTalker( )
|
||||
try:
|
||||
self.cv_error = False
|
||||
self.cv_search_results = comicVine.searchForSeries( self.series_name, callback=self.prog_callback, refresh_cache=self.refresh )
|
||||
except ComicVineTalkerException:
|
||||
self.cv_search_results = []
|
||||
self.cv_error = True
|
||||
finally:
|
||||
self.searchComplete.emit()
|
||||
|
||||
def prog_callback(self, current, total):
|
||||
self.progressUpdate.emit(current, total)
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
|
||||
class IdentifyThread( QtCore.QThread):
|
||||
class SearchThread(QtCore.QThread):
|
||||
|
||||
identifyComplete = pyqtSignal( )
|
||||
identifyLogMsg = pyqtSignal( str )
|
||||
identifyProgress = pyqtSignal( int, int )
|
||||
searchComplete = pyqtSignal()
|
||||
progressUpdate = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, identifier):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.identifier = identifier
|
||||
self.identifier.setOutputFunction( self.logOutput )
|
||||
self.identifier.setProgressCallback( self.progressCallback )
|
||||
def __init__(self, series_name, refresh):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.series_name = series_name
|
||||
self.refresh = refresh
|
||||
self.error_code = None
|
||||
|
||||
def logOutput(self, text):
|
||||
self.identifyLogMsg.emit( text )
|
||||
def run(self):
|
||||
comicVine = ComicVineTalker()
|
||||
try:
|
||||
self.cv_error = False
|
||||
self.cv_search_results = comicVine.searchForSeries(
|
||||
self.series_name,
|
||||
callback=self.prog_callback,
|
||||
refresh_cache=self.refresh)
|
||||
except ComicVineTalkerException as e:
|
||||
self.cv_search_results = []
|
||||
self.cv_error = True
|
||||
self.error_code = e.code
|
||||
|
||||
def progressCallback(self, cur, total):
|
||||
self.identifyProgress.emit( cur, total )
|
||||
|
||||
def run(self):
|
||||
matches =self.identifier.search()
|
||||
self.identifyComplete.emit( )
|
||||
finally:
|
||||
self.searchComplete.emit()
|
||||
|
||||
def prog_callback(self, current, total):
|
||||
self.progressUpdate.emit(current, total)
|
||||
|
||||
|
||||
class IdentifyThread(QtCore.QThread):
|
||||
|
||||
identifyComplete = pyqtSignal()
|
||||
identifyLogMsg = pyqtSignal(str)
|
||||
identifyProgress = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, identifier):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.identifier = identifier
|
||||
self.identifier.setOutputFunction(self.logOutput)
|
||||
self.identifier.setProgressCallback(self.progressCallback)
|
||||
|
||||
def logOutput(self, text):
|
||||
self.identifyLogMsg.emit(text)
|
||||
|
||||
def progressCallback(self, cur, total):
|
||||
self.identifyProgress.emit(cur, total)
|
||||
|
||||
def run(self):
|
||||
matches = self.identifier.search()
|
||||
self.identifyComplete.emit()
|
||||
|
||||
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
|
||||
|
||||
self.imageWidget = CoverImageWidget( self.imageContainer, CoverImageWidget.URLMode )
|
||||
gridlayout = QtGui.QGridLayout( self.imageContainer )
|
||||
gridlayout.addWidget( self.imageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
def __init__(self, parent, series_name, issue_number, year, issue_count,
|
||||
cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
utils.reduceWidgetFontSize( self.teDetails, 1 )
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
uic.loadUi(
|
||||
ComicTaggerSettings.getUIFile('volumeselectionwindow.ui'), self)
|
||||
|
||||
self.settings = settings
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
self.year = year
|
||||
self.volume_id = 0
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.btnRequery.clicked.connect(self.requery)
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
self.imageWidget = CoverImageWidget(
|
||||
self.imageContainer, CoverImageWidget.URLMode)
|
||||
gridlayout = QtGui.QGridLayout(self.imageContainer)
|
||||
gridlayout.addWidget(self.imageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def updateButtons( self ):
|
||||
if self.cv_search_results is not None and len(self.cv_search_results) > 0:
|
||||
enabled = True
|
||||
else:
|
||||
enabled = False
|
||||
|
||||
self.btnRequery.setEnabled( enabled )
|
||||
self.btnIssues.setEnabled( enabled )
|
||||
self.btnAutoSelect.setEnabled( enabled )
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled( enabled )
|
||||
|
||||
def requery( self, ):
|
||||
self.performQuery( refresh=True )
|
||||
self.twList.selectRow(0)
|
||||
reduceWidgetFontSize(self.teDetails, 1)
|
||||
reduceWidgetFontSize(self.twList)
|
||||
|
||||
def autoSelect( self ):
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
if self.comic_archive is None:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select", "You need to load a comic first!")
|
||||
return
|
||||
|
||||
if self.issue_number is None or self.issue_number == "":
|
||||
QtGui.QMessageBox.information(self,"Auto-Select", "Can't auto-select without an issue number (yet!)")
|
||||
return
|
||||
|
||||
self.iddialog = IDProgressWindow( self)
|
||||
self.iddialog.setModal(True)
|
||||
self.iddialog.rejected.connect( self.identifyCancel )
|
||||
self.iddialog.show()
|
||||
|
||||
self.ii = IssueIdentifier( self.comic_archive, self.settings )
|
||||
|
||||
md = GenericMetadata()
|
||||
md.series = self.series_name
|
||||
md.issue = self.issue_number
|
||||
md.year = self.year
|
||||
self.settings = settings
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
self.year = year
|
||||
self.issue_count = issue_count
|
||||
self.volume_id = 0
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
|
||||
self.ii.setAdditionalMetadata( md )
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
print self.cover_index_list
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
self.id_thread = IdentifyThread( self.ii )
|
||||
self.id_thread.identifyComplete.connect( self.identifyComplete )
|
||||
self.id_thread.identifyLogMsg.connect( self.logIDOutput )
|
||||
self.id_thread.identifyProgress.connect( self.identifyProgress )
|
||||
|
||||
self.id_thread.start()
|
||||
|
||||
self.iddialog.exec_()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.btnRequery.clicked.connect(self.requery)
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
def logIDOutput( self, text ):
|
||||
print unicode(text),
|
||||
self.iddialog.textEdit.ensureCursorVisible()
|
||||
self.iddialog.textEdit.insertPlainText(text)
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def identifyProgress( self, cur, total ):
|
||||
self.iddialog.progressBar.setMaximum( total )
|
||||
self.iddialog.progressBar.setValue( cur )
|
||||
def updateButtons(self):
|
||||
if self.cv_search_results is not None and len(
|
||||
self.cv_search_results) > 0:
|
||||
enabled = True
|
||||
else:
|
||||
enabled = False
|
||||
|
||||
def identifyCancel( self ):
|
||||
self.ii.cancel = True
|
||||
|
||||
def identifyComplete( self ):
|
||||
self.btnRequery.setEnabled(enabled)
|
||||
self.btnIssues.setEnabled(enabled)
|
||||
self.btnAutoSelect.setEnabled(enabled)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(enabled)
|
||||
|
||||
matches = self.ii.match_list
|
||||
result = self.ii.search_result
|
||||
match_index = 0
|
||||
|
||||
found_match = None
|
||||
choices = False
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage :
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but not with the first page of the archive.")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
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:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
def requery(self,):
|
||||
self.performQuery(refresh=True)
|
||||
self.twList.selectRow(0)
|
||||
|
||||
if choices:
|
||||
selector = MatchSelectionWindow( self, matches, self.comic_archive )
|
||||
selector.setModal(True)
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a list index
|
||||
found_match = selector.currentMatch()
|
||||
|
||||
if found_match is not None:
|
||||
self.iddialog.accept()
|
||||
def autoSelect(self):
|
||||
|
||||
self.volume_id = found_match['volume_id']
|
||||
self.issue_number = found_match['issue_number']
|
||||
self.selectByID()
|
||||
self.showIssues()
|
||||
if self.comic_archive is None:
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Auto-Select", "You need to load a comic first!")
|
||||
return
|
||||
|
||||
def showIssues( self ):
|
||||
selector = IssueSelectionWindow( self, self.settings, self.volume_id, self.issue_number )
|
||||
title = ""
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
title = record['name']
|
||||
title += " (" + unicode(record['start_year']) + ")"
|
||||
title += " - "
|
||||
break
|
||||
|
||||
selector.setWindowTitle( title + "Select Issue")
|
||||
selector.setModal( True )
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a volume ID
|
||||
self.issue_number = selector.issue_number
|
||||
self.accept()
|
||||
return
|
||||
if self.issue_number is None or self.issue_number == "":
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select",
|
||||
"Can't auto-select without an issue number (yet!)")
|
||||
return
|
||||
|
||||
def selectByID( self ):
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
volume_id, b = self.twList.item( r, 0 ).data( QtCore.Qt.UserRole ).toInt()
|
||||
if (volume_id == self.volume_id):
|
||||
self.twList.selectRow( r )
|
||||
break
|
||||
|
||||
def performQuery( self, refresh=False ):
|
||||
|
||||
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.iddialog = IDProgressWindow(self)
|
||||
self.iddialog.setModal(True)
|
||||
self.iddialog.rejected.connect(self.identifyCancel)
|
||||
self.iddialog.show()
|
||||
|
||||
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()
|
||||
self.ii = IssueIdentifier(self.comic_archive, self.settings)
|
||||
|
||||
#QtCore.QCoreApplication.processEvents()
|
||||
self.progdialog.exec_()
|
||||
md = GenericMetadata()
|
||||
md.series = self.series_name
|
||||
md.issue = self.issue_number
|
||||
md.year = self.year
|
||||
md.issueCount = self.issue_count
|
||||
|
||||
self.ii.setAdditionalMetadata(md)
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
|
||||
def searchCanceled( self ):
|
||||
print "query cancelled"
|
||||
self.search_thread.searchComplete.disconnect( self.searchComplete )
|
||||
self.search_thread.progressUpdate.disconnect( self.searchProgressUpdate )
|
||||
self.progdialog.canceled.disconnect( self.searchCanceled )
|
||||
self.progdialog.reject()
|
||||
QtCore.QTimer.singleShot(200, self.closeMe)
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
def closeMe( self ):
|
||||
print "closeme"
|
||||
self.reject()
|
||||
self.id_thread = IdentifyThread(self.ii)
|
||||
self.id_thread.identifyComplete.connect(self.identifyComplete)
|
||||
self.id_thread.identifyLogMsg.connect(self.logIDOutput)
|
||||
self.id_thread.identifyProgress.connect(self.identifyProgress)
|
||||
|
||||
self.id_thread.start()
|
||||
|
||||
def searchProgressUpdate( self , current, total ):
|
||||
self.progdialog.setMaximum(total)
|
||||
self.progdialog.setValue(current)
|
||||
self.iddialog.exec_()
|
||||
|
||||
def searchComplete( self ):
|
||||
self.progdialog.accept()
|
||||
if self.search_thread.cv_error:
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to search for series!"))
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
self.updateButtons()
|
||||
def logIDOutput(self, text):
|
||||
print unicode(text),
|
||||
self.iddialog.textEdit.ensureCursorVisible()
|
||||
self.iddialog.textEdit.insertPlainText(text)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
def identifyProgress(self, cur, total):
|
||||
self.iddialog.progressBar.setMaximum(total)
|
||||
self.iddialog.progressBar.setValue(cur)
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
row = 0
|
||||
for record in self.cv_search_results:
|
||||
self.twList.insertRow(row)
|
||||
def identifyCancel(self):
|
||||
self.ii.cancel = True
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem( item_text )
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = str(record['start_year'])
|
||||
item = 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)
|
||||
def identifyComplete(self):
|
||||
|
||||
item_text = record['count_of_issues']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if record['publisher'] is not None:
|
||||
item_text = record['publisher']['name']
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
matches = self.ii.match_list
|
||||
result = self.ii.search_result
|
||||
match_index = 0
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.DescendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
if len( self.cv_search_results ) == 0:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtGui.QMessageBox.information(self,"Search Result", "No matches found!")
|
||||
found_match = None
|
||||
choices = False
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found a match, but not with the first page of the archive.")
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
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:
|
||||
QtGui.QMessageBox.information(
|
||||
self,
|
||||
"Auto-Select Result",
|
||||
" Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
|
||||
if self.immediate_autoselect and len( self.cv_search_results ) > 0:
|
||||
# defer the immediate autoselect so this dialog has time to pop up
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
|
||||
|
||||
def doImmediateAutoselect( self ):
|
||||
self.immediate_autoselect = False
|
||||
self.autoSelect()
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.showIssues()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
if choices:
|
||||
selector = MatchSelectionWindow(self, matches, self.comic_archive)
|
||||
selector.setModal(True)
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
# we should now have a list index
|
||||
found_match = selector.currentMatch()
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
if found_match is not None:
|
||||
self.iddialog.accept()
|
||||
|
||||
self.volume_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
|
||||
self.volume_id = found_match['volume_id']
|
||||
self.issue_number = found_match['issue_number']
|
||||
self.selectByID()
|
||||
self.showIssues()
|
||||
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
def showIssues(self):
|
||||
selector = IssueSelectionWindow(
|
||||
self, self.settings, self.volume_id, self.issue_number)
|
||||
title = ""
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
title = record['name']
|
||||
title += " (" + unicode(record['start_year']) + ")"
|
||||
title += " - "
|
||||
break
|
||||
|
||||
self.teDetails.setText ( record['description'] )
|
||||
self.imageWidget.setURL( record['image']['super_url'] )
|
||||
break
|
||||
selector.setWindowTitle(title + "Select Issue")
|
||||
selector.setModal(True)
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
# we should now have a volume ID
|
||||
self.issue_number = selector.issue_number
|
||||
self.accept()
|
||||
return
|
||||
|
||||
def selectByID(self):
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
volume_id, b = self.twList.item(
|
||||
r, 0).data(QtCore.Qt.UserRole).toInt()
|
||||
if (volume_id == self.volume_id):
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def performQuery(self, refresh=False):
|
||||
|
||||
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.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):
|
||||
print("query cancelled")
|
||||
self.search_thread.searchComplete.disconnect(self.searchComplete)
|
||||
self.search_thread.progressUpdate.disconnect(self.searchProgressUpdate)
|
||||
self.progdialog.canceled.disconnect(self.searchCanceled)
|
||||
self.progdialog.reject()
|
||||
QtCore.QTimer.singleShot(200, self.closeMe)
|
||||
|
||||
def closeMe(self):
|
||||
print("closeme")
|
||||
self.reject()
|
||||
|
||||
def searchProgressUpdate(self, current, total):
|
||||
self.progdialog.setMaximum(total)
|
||||
self.progdialog.setValue(current)
|
||||
|
||||
def searchComplete(self):
|
||||
self.progdialog.accept()
|
||||
if self.search_thread.cv_error:
|
||||
if self.search_thread.error_code == ComicVineTalkerException.RateLimit:
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
self.tr("Comic Vine Error"),
|
||||
ComicVineTalker.getRateLimitMessage())
|
||||
else:
|
||||
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
|
||||
self.updateButtons()
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
row = 0
|
||||
for record in self.cv_search_results:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.UserRole, record['id'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = str(record['start_year'])
|
||||
item = 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 = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if record['publisher'] is not None:
|
||||
item_text = record['publisher']['name']
|
||||
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.DescendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
if len(self.cv_search_results) == 0:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtGui.QMessageBox.information(
|
||||
self, "Search Result", "No matches found!")
|
||||
|
||||
if self.immediate_autoselect and len(self.cv_search_results) > 0:
|
||||
# defer the immediate autoselect so this dialog has time to pop up
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
|
||||
|
||||
def doImmediateAutoselect(self):
|
||||
self.immediate_autoselect = False
|
||||
self.autoSelect()
|
||||
|
||||
def cellDoubleClicked(self, r, c):
|
||||
self.showIssues()
|
||||
|
||||
def currentItemChanged(self, curr, prev):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.volume_id, b = self.twList.item(
|
||||
curr.row(), 0).data(QtCore.Qt.UserRole).toInt()
|
||||
|
||||
# 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:
|
||||
self.teDetails.setText("")
|
||||
else:
|
||||
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
|
||||
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>
|
||||
@@ -57,200 +57,204 @@ import sys
|
||||
|
||||
|
||||
def upload(file, project_name, user_name, password, summary, labels=None):
|
||||
"""Upload a file to a Google Code project's file server.
|
||||
"""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.
|
||||
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')]
|
||||
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])
|
||||
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)
|
||||
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,
|
||||
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()
|
||||
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
|
||||
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.
|
||||
"""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.
|
||||
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'
|
||||
Returns: (content_type, body) ready for httplib.HTTP instance
|
||||
"""
|
||||
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
|
||||
CRLF = '\r\n'
|
||||
|
||||
body = []
|
||||
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()
|
||||
|
||||
# Add the metadata about the upload first
|
||||
for key, value in fields:
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
'',
|
||||
value,
|
||||
])
|
||||
['--' + 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,
|
||||
])
|
||||
|
||||
# Now add the file itself
|
||||
file_name = os.path.basename(file_path)
|
||||
f = open(file_path, 'rb')
|
||||
file_content = f.read()
|
||||
f.close()
|
||||
# Finalize the form body
|
||||
body.extend(['--' + BOUNDARY + '--', ''])
|
||||
|
||||
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)
|
||||
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.
|
||||
"""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.
|
||||
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]
|
||||
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()
|
||||
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
|
||||
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
|
||||
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')
|
||||
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()
|
||||
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.')
|
||||
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]
|
||||
file_path = args[0]
|
||||
|
||||
if options.labels:
|
||||
labels = options.labels.split(',')
|
||||
else:
|
||||
labels = None
|
||||
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
|
||||
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())
|
||||
sys.exit(main())
|
||||
|
||||
11
mac/Makefile
11
mac/Makefile
@@ -1,6 +1,7 @@
|
||||
#PYINSTALLER_CMD := VERSIONER_PYTHON_PREFER_32_BIT=yes arch -i386 python $(HOME)/pyinstaller-2.0/pyinstaller.py
|
||||
PYINSTALLER_CMD := python $(HOME)/pyinstaller-2.0/pyinstaller.py
|
||||
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
|
||||
#PYINSTALLER_CMD := python $(HOME)/pyinstaller-2.0/pyinstaller.py
|
||||
PYINSTALLER_CMD := pyinstaller
|
||||
TAGGER_BASE ?= ../
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
|
||||
APP_NAME := ComicTagger
|
||||
@@ -10,15 +11,17 @@ MAC_BASE := $(TAGGER_BASE)/mac
|
||||
DIST_DIR := $(MAC_BASE)/dist
|
||||
STAGING := $(MAC_BASE)/$(APP_NAME)
|
||||
APP_BUNDLE := $(DIST_DIR)/$(APP_NAME).app
|
||||
VOLUME_NAME := $(APP_NAME)-$(VERSION_STR)
|
||||
VOLUME_NAME := "$(APP_NAME)-$(VERSION_STR)"
|
||||
DMG_FILE := $(VOLUME_NAME).dmg
|
||||
|
||||
all: clean dist diskimage
|
||||
|
||||
dist:
|
||||
$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -o $(MAC_BASE) -w -n $(APP_NAME) -s
|
||||
#$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -o $(MAC_BASE) -w -n $(APP_NAME) -s
|
||||
$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -w -n $(APP_NAME) -s
|
||||
cp -a $(TAGGER_SRC)/ui $(APP_BUNDLE)/Contents/MacOS
|
||||
cp -a $(TAGGER_SRC)/graphics $(APP_BUNDLE)/Contents/MacOS
|
||||
cp $(MAC_BASE)/libunrar.so $(APP_BUNDLE)/Contents/MacOS
|
||||
cp $(MAC_BASE)/app.icns $(APP_BUNDLE)/Contents/Resources/icon-windowed.icns
|
||||
# fix the version string in the Info.plist
|
||||
sed -i -e 's/0\.0\.0/$(VERSION_STR)/' $(MAC_BASE)/dist/ComicTagger.app/Contents/Info.plist
|
||||
|
||||
@@ -1,129 +1,221 @@
|
||||
---------------------------------
|
||||
1.1.3-beta - 25-Feb-2013
|
||||
---------------------------------
|
||||
Bug Fixes:
|
||||
* Fixed a bug when renaming on non-English systems
|
||||
* Fixed issue when saving settings on non-English systems
|
||||
* Fixed a bug when comic contains non-RGB images
|
||||
* Fixed a rare crash when comic image is not-RGB format
|
||||
* Fixed sequence order of ComicInfo.xml items
|
||||
|
||||
Note:
|
||||
New requirement for users of the python package: "configparser"
|
||||
|
||||
---------------------------------
|
||||
1.1.2-beta - 14-Feb-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
* Source is now packaged using Python distutils
|
||||
* Recursive mode for CLI
|
||||
* Run custom add-on scripts from CLI
|
||||
* Minor UI tweaks
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.0-beta - 06-Feb-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
* Enhanced identification process to use alternative covers from ComicVine
|
||||
* Post auto-tag manual matching now includes single low-confidence matches (CLI & GUI)
|
||||
* Page and cover view mini-browser available throughout app. Most images can be
|
||||
double-clicked for embiggened view
|
||||
* Export-to-zip in CLI (very handy in scripts!)
|
||||
* More rename template variables
|
||||
* Misc GUI & CLI Tweaks
|
||||
|
||||
---------------------------------
|
||||
1.0.3-beta - 31-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Misc bug fixes and enhancements
|
||||
|
||||
---------------------------------
|
||||
1.0.2-beta - 25-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
More verbose logging during auto-tag
|
||||
Added %month% and %month_name% for renaming
|
||||
Better parsing of volume numbers in file name
|
||||
Bugs:
|
||||
Better exception handling with corrupted image data
|
||||
Fixed issues with RAR reading on OS X
|
||||
Other minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.0.1-beta - 24-Jan-2013
|
||||
---------------------------------
|
||||
Bug Fix:
|
||||
Fixed an issue where unicode strings can't be printed to OS X Console
|
||||
|
||||
---------------------------------
|
||||
1.0.0-beta - 23-Jan-2013
|
||||
---------------------------------
|
||||
Version 1! New multi-file processing in GUI!
|
||||
|
||||
GUI Changes:
|
||||
Open multiple files and/or folders via drag/drop or file dialog
|
||||
File management list for easy viewing and selection
|
||||
Batch tag remove
|
||||
Batch export as zip
|
||||
Batch rename
|
||||
Batch tag copy
|
||||
Batch auto-tag (automatic identification and save!)
|
||||
|
||||
---------------------------------
|
||||
0.9.5-beta - 16-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Added CLI option to search by comicvine issue ID
|
||||
Some image loading optimizations
|
||||
Bug Fix: Some CBL fields that should have been ints were written as strings
|
||||
|
||||
---------------------------------
|
||||
0.9.4-beta - 7-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Better handling of non-ascii characters in filenames and data
|
||||
Add CBL Transform to copy Web Link and Notes to comments
|
||||
Minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.3-beta - 19-Dec-2012
|
||||
---------------------------------
|
||||
Changes:
|
||||
File rename in GUI
|
||||
Setting for file rename
|
||||
Option to use series start year as volume
|
||||
Added "CBL Transform" to handle primary credits copying data into the generic tags field
|
||||
Bug Fix: unicode characters in credits caused crash
|
||||
Bug Fix: bad or non-image data in file caused crash
|
||||
|
||||
Note:
|
||||
The user should clear the cache and delete the existing settings when first running this version.
|
||||
|
||||
---------------------------------
|
||||
0.9.2-beta - 13-Dec-2012
|
||||
---------------------------------
|
||||
Page List/Type editing in GUI
|
||||
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
|
||||
Fixed RAR writing bug on windows
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.1-beta - 07-Dec-2012
|
||||
---------------------------------
|
||||
Export as ZIP Archive
|
||||
Added help menu option for websites
|
||||
Added Primary Credit Flag editing
|
||||
Menu enhancements
|
||||
CLI Enhancements:
|
||||
Interactive selection of matches
|
||||
Tag copy
|
||||
Better output
|
||||
CoMet support
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.0-beta - 30-Nov-2012
|
||||
---------------------------------
|
||||
Initial beta release
|
||||
---------------------------------
|
||||
1.1.16-beta-rc - 07-Apr-2017
|
||||
---------------------------------
|
||||
* Fix ComicVine SSL problems (issue #87)
|
||||
|
||||
---------------------------------
|
||||
1.1.15-beta - 13-Jun-2014
|
||||
---------------------------------
|
||||
* WebP support
|
||||
* Added user-configurable API key for Comic Vine access
|
||||
* Experimental option to wait and retry after exceeding Comic Vine rate limit
|
||||
|
||||
---------------------------------
|
||||
1.1.14-beta - 13-Apr-2014
|
||||
---------------------------------
|
||||
* Make sure app gets raised when enforcing single instance
|
||||
* Added warning dialog for when opening rar files, and no (un)rar tool
|
||||
* remove pil from python package requirements
|
||||
|
||||
---------------------------------
|
||||
1.1.13-beta - 9-Apr-2014
|
||||
---------------------------------
|
||||
* Handle non-ascii user names properly
|
||||
* better parsing of html table in summary text, and optional removal
|
||||
* Python package should auto-install requirements
|
||||
* Specify default GUI tag style on command-line
|
||||
* enforce single GUI instance
|
||||
* new CBL transform to copy story arcs to generic tags
|
||||
* Persist some auto-tag settings
|
||||
|
||||
---------------------------------
|
||||
1.1.12-beta - 23-Mar-2014
|
||||
---------------------------------
|
||||
* Fixed noisy version update error
|
||||
|
||||
---------------------------------
|
||||
1.1.11-beta - 23-Mar-2014
|
||||
---------------------------------
|
||||
* Updated unrar library to hand Rar tools 5.0 and greater
|
||||
* Other misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.10-beta - 30-Jan-2014
|
||||
---------------------------------
|
||||
* Updated series query to match changes on Comic Vine side
|
||||
* Added a message when not able to open a file or folder
|
||||
* Fixed an issue where series names with periods would fail on search
|
||||
* Other misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.9-beta - 8-May-2013
|
||||
---------------------------------
|
||||
* Filename parser and identification enhancements
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.8-beta - 21-Apr-2013
|
||||
---------------------------------
|
||||
* Handle occasional error 500 from Comic Vine by retrying a few times
|
||||
* Nicer handling of colon (":") in file rename
|
||||
* Fixed command-line option parsing issue for add-on scripts
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.7-beta - 12-Apr-2013
|
||||
---------------------------------
|
||||
* Added description and cover date to issue selection dialogs
|
||||
* Added notification of new version
|
||||
* Added setting to attempt to parse scan info from file name
|
||||
* Last sorted column in the file list is now remembered
|
||||
* Added CLI option ('-1') to assume issue #1 if not found/parsed
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.6-beta - 3-Apr-2013
|
||||
---------------------------------
|
||||
* More ComicVine API-related fixes
|
||||
* More efficient automated search using new CV API issue filters
|
||||
* Minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.5-beta - 30-Mar-2013
|
||||
---------------------------------
|
||||
* More updates for handling changes to ComicVine API and result sets
|
||||
* Even better handling of non-numeric issue "numbers" ("½", "X")
|
||||
|
||||
---------------------------------
|
||||
1.1.4-beta - 27-Mar-2013
|
||||
---------------------------------
|
||||
* Updated to match the changes to the ComicVine API and result sets
|
||||
* Better handling of weird issue numbers ("0.1", "6au")
|
||||
|
||||
---------------------------------
|
||||
1.1.3-beta - 25-Feb-2013
|
||||
---------------------------------
|
||||
Bug Fixes:
|
||||
* Fixed a bug when renaming on non-English systems
|
||||
* Fixed issue when saving settings on non-English systems
|
||||
* Fixed a bug when comic contains non-RGB images
|
||||
* Fixed a rare crash when comic image is not-RGB format
|
||||
* Fixed sequence order of ComicInfo.xml items
|
||||
|
||||
Note:
|
||||
New requirement for users of the python package: "configparser"
|
||||
|
||||
---------------------------------
|
||||
1.1.2-beta - 14-Feb-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
* Source is now packaged using Python distutils
|
||||
* Recursive mode for CLI
|
||||
* Run custom add-on scripts from CLI
|
||||
* Minor UI tweaks
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.0-beta - 06-Feb-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
* Enhanced identification process to use alternative covers from ComicVine
|
||||
* Post auto-tag manual matching now includes single low-confidence matches (CLI & GUI)
|
||||
* Page and cover view mini-browser available throughout app. Most images can be
|
||||
double-clicked for enlarged view
|
||||
* Export-to-zip in CLI (very handy in scripts!)
|
||||
* More rename template variables
|
||||
* Misc GUI & CLI Tweaks
|
||||
|
||||
---------------------------------
|
||||
1.0.3-beta - 31-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Misc bug fixes and enhancements
|
||||
|
||||
---------------------------------
|
||||
1.0.2-beta - 25-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
More verbose logging during auto-tag
|
||||
Added %month% and %month_name% for renaming
|
||||
Better parsing of volume numbers in file name
|
||||
Bugs:
|
||||
Better exception handling with corrupted image data
|
||||
Fixed issues with RAR reading on OS X
|
||||
Other minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.0.1-beta - 24-Jan-2013
|
||||
---------------------------------
|
||||
Bug Fix:
|
||||
Fixed an issue where unicode strings can't be printed to OS X Console
|
||||
|
||||
---------------------------------
|
||||
1.0.0-beta - 23-Jan-2013
|
||||
---------------------------------
|
||||
Version 1! New multi-file processing in GUI!
|
||||
|
||||
GUI Changes:
|
||||
Open multiple files and/or folders via drag/drop or file dialog
|
||||
File management list for easy viewing and selection
|
||||
Batch tag remove
|
||||
Batch export as zip
|
||||
Batch rename
|
||||
Batch tag copy
|
||||
Batch auto-tag (automatic identification and save!)
|
||||
|
||||
---------------------------------
|
||||
0.9.5-beta - 16-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Added CLI option to search by Comic Vine issue ID
|
||||
Some image loading optimizations
|
||||
Bug Fix: Some CBL fields that should have been ints were written as strings
|
||||
|
||||
---------------------------------
|
||||
0.9.4-beta - 7-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Better handling of non-ascii characters in file names and data
|
||||
Add CBL Transform to copy Web Link and Notes to comments
|
||||
Minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.3-beta - 19-Dec-2012
|
||||
---------------------------------
|
||||
Changes:
|
||||
File rename in GUI
|
||||
Setting for file rename
|
||||
Option to use series start year as volume
|
||||
Added "CBL Transform" to handle primary credits copying data into the generic tags field
|
||||
Bug Fix: unicode characters in credits caused crash
|
||||
Bug Fix: bad or non-image data in file caused crash
|
||||
|
||||
Note:
|
||||
The user should clear the cache and delete the existing settings when first running this version.
|
||||
|
||||
---------------------------------
|
||||
0.9.2-beta - 13-Dec-2012
|
||||
---------------------------------
|
||||
Page List/Type editing in GUI
|
||||
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
|
||||
Fixed RAR writing bug on windows
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.1-beta - 07-Dec-2012
|
||||
---------------------------------
|
||||
Export as ZIP Archive
|
||||
Added help menu option for websites
|
||||
Added Primary Credit Flag editing
|
||||
Menu enhancements
|
||||
CLI Enhancements:
|
||||
Interactive selection of matches
|
||||
Tag copy
|
||||
Better output
|
||||
CoMet support
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.0-beta - 30-Nov-2012
|
||||
---------------------------------
|
||||
Initial beta release
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
configparser
|
||||
beautifulsoup4 >= 4.1
|
||||
PIL >= 1.1.6
|
||||
unrar==0.3
|
||||
natsort==3.5.2
|
||||
PyPDF2==1.24
|
||||
|
||||
@@ -7,7 +7,7 @@ the setup.py file.
|
||||
|
||||
To run via the ComicTagger app, invoke:
|
||||
|
||||
# comictagger.py -S script.py [script args]
|
||||
$ comictagger.py -S script.py [script args]
|
||||
|
||||
(This will work also for binary distributions on Mac and Windows. No need for
|
||||
an extra python install.)
|
||||
@@ -19,12 +19,10 @@ via the app.
|
||||
|
||||
This feature is UNSUPPORTED, and is for the convenience of development-minded
|
||||
users of ComicTagger. The comictaggerlib module will remain largely
|
||||
undocumented, and it will up to the crafty script developer to look through
|
||||
undocumented, and it will be up to the crafty script developer to look through
|
||||
the code to discern APIs and such.
|
||||
|
||||
That said, if there are questions, please post in the forums, and hopefully we
|
||||
can get your add-on scripts working!
|
||||
|
||||
http://comictagger.forumotion.com/
|
||||
|
||||
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
find all duplicate comics
|
||||
"""
|
||||
"""Find all duplicate comics"""
|
||||
|
||||
import sys
|
||||
#import sys
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.issuestring import *
|
||||
import comictaggerlib.utils
|
||||
#from comictaggerlib.issuestring import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
def main():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "usage: {0} comic_folder ".format(sys.argv[0])
|
||||
return
|
||||
|
||||
filelist = utils.get_recursive_filelist( sys.argv[1:] )
|
||||
|
||||
#first find all comics with metadata
|
||||
print >> sys.stderr, "reading in all comics..."
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings )
|
||||
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 )))
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
|
||||
return
|
||||
|
||||
print >> sys.stderr, fmt_str.format( "" ) + "\r",
|
||||
print "-----------------------------------------------"
|
||||
print "Found {0} comics with {1} tags".format( len(comic_list), MetaDataStyle.name[style])
|
||||
print "-----------------------------------------------"
|
||||
filelist = utils.get_recursive_filelist(sys.argv[1:])
|
||||
|
||||
#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)
|
||||
# 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)))
|
||||
|
||||
# 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))
|
||||
print >> sys.stderr, fmt_str.format("") + "\r",
|
||||
print "--------------------------------------------------------------------------"
|
||||
print "Found {0} comics with {1} tags".format(len(comic_list), MetaDataStyle.name[style])
|
||||
print "--------------------------------------------------------------------------"
|
||||
|
||||
#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))
|
||||
# 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)
|
||||
|
||||
for dupe_set in dupe_set_list:
|
||||
ca = ComicArchive(dupe_set[0], settings )
|
||||
md = ca.readMetadata( style )
|
||||
print "{0} #{1} ({2})".format( md.series, md.issue, md.year )
|
||||
for filename in dupe_set:
|
||||
print "------------->{0}".format( filename )
|
||||
# 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()
|
||||
main()
|
||||
|
||||
@@ -1,84 +1,83 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Print out a line-by-line list of basic tag info from all comics
|
||||
"""
|
||||
"""Print out a line-by-line list of basic tag info from all comics"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# Copyright 2012 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
#import os
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.issuestring import *
|
||||
import comictaggerlib.utils
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
def main():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "usage: {0} comic_folder ".format(sys.argv[0])
|
||||
return
|
||||
|
||||
filelist = utils.get_recursive_filelist( sys.argv[1:] )
|
||||
|
||||
#first read in metadata from all files
|
||||
metadata_list = []
|
||||
max_name_len = 2
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings )
|
||||
if ca.hasMetadata( style ):
|
||||
#make a list of paired filenames and metadata objects
|
||||
metadata_list.append((filename, ca.readMetadata( 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()
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
|
||||
return
|
||||
|
||||
print >> sys.stderr, fmt_str.format( "" ) + "\r",
|
||||
print "-----------------------------------------------"
|
||||
print "Found {0} comics with {1} tags".format( len(metadata_list), MetaDataStyle.name[style])
|
||||
print "-----------------------------------------------"
|
||||
filelist = utils.get_recursive_filelist(sys.argv[1:])
|
||||
|
||||
# now, figure out column widths
|
||||
w0 = 4
|
||||
w1 = 4
|
||||
for filename,md in metadata_list:
|
||||
if not md.isEmpty:
|
||||
w0 = max( len((os.path.split(filename)[1])), w0)
|
||||
if md.series is not None:
|
||||
w1 = max( len(md.series), w1)
|
||||
w0 += 2
|
||||
|
||||
# build a format string
|
||||
fmt_str = u"{0:" + str(w0) + "} {1:" + str(w1) + "} #{2:6} ({3})"
|
||||
# first read in metadata from all files
|
||||
metadata_list = []
|
||||
max_name_len = 2
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
if ca.hasMetadata(style):
|
||||
# make a list of paired file names and metadata objects
|
||||
metadata_list.append((filename, ca.readMetadata(style)))
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
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()
|
||||
|
||||
print >> sys.stderr, fmt_str.format("") + "\r",
|
||||
print "--------------------------------------------------------------------------"
|
||||
print "Found {0} comics with {1} tags".format(len(metadata_list), MetaDataStyle.name[style])
|
||||
print "--------------------------------------------------------------------------"
|
||||
|
||||
# now, figure out column widths
|
||||
w0 = 4
|
||||
w1 = 4
|
||||
for filename, md in metadata_list:
|
||||
if not md.isEmpty:
|
||||
w0 = max(len((os.path.split(filename)[1])), w0)
|
||||
if md.series is not None:
|
||||
w1 = max(len(md.series), w1)
|
||||
w0 += 2
|
||||
|
||||
# build a format string
|
||||
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)
|
||||
|
||||
# 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__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,107 +1,112 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
make some tree structures and symbolic links to comic files based on metadata
|
||||
oragnizing by date and series, in different trees
|
||||
Make some tree structures and symbolic links to comic files based on metadata
|
||||
organizing by date and series, in different trees
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
# Copyright 2012 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 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):
|
||||
if not os.path.exists(folder):
|
||||
try:
|
||||
os.makedirs(folder)
|
||||
except Exception as e:
|
||||
print "{0} Can't make {1} -- quitting".format(e, folder)
|
||||
quit()
|
||||
|
||||
|
||||
def make_link(source, link):
|
||||
if not os.path.exists(link):
|
||||
os.symlink(os.path.abspath(source), link)
|
||||
|
||||
def make_folder( folder ):
|
||||
if not os.path.exists( folder ):
|
||||
try:
|
||||
os.makedirs(folder)
|
||||
except Exception as e:
|
||||
print "{0} Can't make {1} -- quitting".format(e, folder)
|
||||
quit()
|
||||
|
||||
def make_link( source, link ):
|
||||
if not os.path.exists( link ):
|
||||
os.symlink( os.path.abspath(source) , link )
|
||||
|
||||
def main():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
if platform.system() == "Windows":
|
||||
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])
|
||||
return
|
||||
|
||||
comic_root = sys.argv[1]
|
||||
link_root = sys.argv[2]
|
||||
|
||||
print "root is : ", comic_root
|
||||
filelist = utils.get_recursive_filelist( [ comic_root ] )
|
||||
make_folder( link_root )
|
||||
|
||||
#first find all comics with metadata
|
||||
print "reading in all comics..."
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings )
|
||||
if ca.seemsToBeAComicArchive() and ca.hasMetadata( style ):
|
||||
if platform.system() == "Windows":
|
||||
print >> sys.stderr, "Sorry, this script works only on UNIX systems"
|
||||
|
||||
comic_list.append((filename, ca.readMetadata( 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()
|
||||
if len(sys.argv) < 3:
|
||||
print >> sys.stderr, "Usage: {0} [comic_root][link_root]".format(
|
||||
sys.argv[0])
|
||||
return
|
||||
|
||||
print >> sys.stderr, fmt_str.format( "" )
|
||||
print "Found {0} tagged comics.".format( len(comic_list))
|
||||
comic_root = sys.argv[1]
|
||||
link_root = sys.argv[2]
|
||||
|
||||
print "Root is:", comic_root
|
||||
filelist = utils.get_recursive_filelist([comic_root])
|
||||
make_folder(link_root)
|
||||
|
||||
# first find all comics with metadata
|
||||
print "Reading in all comics..."
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
if ca.seemsToBeAComicArchive() and ca.hasMetadata(style):
|
||||
|
||||
comic_list.append((filename, ca.readMetadata(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()
|
||||
|
||||
print >> sys.stderr, fmt_str.format("")
|
||||
print "Found {0} tagged comics.".format(len(comic_list))
|
||||
|
||||
# walk through the comic list and add subdirs and links for each one
|
||||
for filename, md in comic_list:
|
||||
print >> sys.stderr, fmt_str.format(filename) + "\r",
|
||||
sys.stderr.flush()
|
||||
|
||||
# do date organizing:
|
||||
if md.month is not None:
|
||||
month_str = "{0:02d}".format(int(md.month))
|
||||
else:
|
||||
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)))
|
||||
|
||||
# do publisher/series organizing:
|
||||
fixed_series_name = md.series
|
||||
if fixed_series_name is not None:
|
||||
# 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))
|
||||
make_folder(series_folder)
|
||||
make_link(filename, os.path.join(
|
||||
series_folder, os.path.basename(filename)))
|
||||
|
||||
# walk through the comic list and add subdirs and links for each one
|
||||
for filename, md in comic_list:
|
||||
print >> sys.stderr, fmt_str.format( filename ) + "\r",
|
||||
sys.stderr.flush()
|
||||
|
||||
#do date organizing:
|
||||
if md.month is not None:
|
||||
month_str = "{0:02d}".format(int(md.month))
|
||||
else:
|
||||
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)) )
|
||||
|
||||
#do publisher/series organizing:
|
||||
fixed_series_name = md.series
|
||||
if fixed_series_name is not None:
|
||||
# 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))
|
||||
make_folder( series_folder )
|
||||
make_link( filename, os.path.join(series_folder, os.path.basename(filename)) )
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
118
scripts/move2folder.py
Executable file
118
scripts/move2folder.py
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/python
|
||||
"""Moves comic files based on metadata organizing in a tree by Publisher/Series (Volume)"""
|
||||
|
||||
# This script is based on make_links.py by Anthony Beville
|
||||
|
||||
# Copyright 2015 Fabio Cancedda, Anthony Beville
|
||||
|
||||
# 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 shutil
|
||||
#import sys
|
||||
#import os
|
||||
#import platform
|
||||
|
||||
from comictaggerlib.settings import *
|
||||
from comicapi.comicarchive import *
|
||||
#from comicapi.issuestring import *
|
||||
#import comicapi.utils
|
||||
|
||||
|
||||
def make_folder(folder):
|
||||
if not os.path.exists(folder):
|
||||
try:
|
||||
os.makedirs(folder)
|
||||
except Exception as e:
|
||||
print "{0} Can't make {1} -- quitting".format(e, folder)
|
||||
quit()
|
||||
|
||||
|
||||
def move_file(source, filename):
|
||||
if not os.path.exists(filename):
|
||||
shutil.move(os.path.abspath(source), filename)
|
||||
|
||||
|
||||
def main():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
if platform.system() == "Windows":
|
||||
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])
|
||||
return
|
||||
|
||||
comic_root = sys.argv[1]
|
||||
tree_root = sys.argv[2]
|
||||
|
||||
print "Root is:", comic_root
|
||||
if not os.path.exists(comic_root):
|
||||
print >> sys.stderr, "The comic root doesn't seem a directory or it doesn't exists. -- quitting"
|
||||
return
|
||||
|
||||
filelist = utils.get_recursive_filelist([comic_root])
|
||||
|
||||
if len(filelist) == 0:
|
||||
print >> sys.stderr, "The comic root seems empty. -- quitting"
|
||||
return
|
||||
|
||||
make_folder(tree_root)
|
||||
|
||||
# first find all comics with metadata
|
||||
print "Reading in all comics..."
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
fmt_str = ""
|
||||
for filename in filelist:
|
||||
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)))
|
||||
|
||||
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()
|
||||
|
||||
print >> sys.stderr, fmt_str.format("")
|
||||
|
||||
print "Found {0} tagged comics.".format(len(comic_list))
|
||||
|
||||
# walk through the comic list and moves each one
|
||||
for filename, md in comic_list:
|
||||
print >> sys.stderr, fmt_str.format(filename) + "\r",
|
||||
sys.stderr.flush()
|
||||
|
||||
# do publisher/series organizing:
|
||||
series_name = md.series
|
||||
publisher_name = md.publisher
|
||||
start_year = md.volume
|
||||
if series_name is not None:
|
||||
# some tweaks to keep various filesystems happy
|
||||
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) + ")")
|
||||
make_folder(series_folder)
|
||||
move_file(filename, os.path.join(
|
||||
series_folder, os.path.basename(filename)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
163
scripts/name_fixer.py
Executable file
163
scripts/name_fixer.py
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/python
|
||||
"""Fix the comic file names using a list of transforms"""
|
||||
|
||||
# Copyright 2013 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 argparse
|
||||
import json
|
||||
#import sys
|
||||
#import os
|
||||
#import re
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
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')
|
||||
parsed_args = parser.parse_args(input_args)
|
||||
|
||||
return parsed_args
|
||||
|
||||
|
||||
def calculate_rename(ca, md, settings):
|
||||
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer(md)
|
||||
renamer.setTemplate(
|
||||
"%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%")
|
||||
renamer.setIssueZeroPadding(0)
|
||||
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
|
||||
|
||||
return renamer.determineName(ca.path, ext=new_ext)
|
||||
|
||||
|
||||
def perform_rename(filelist):
|
||||
for old_name, new_name in filelist:
|
||||
folder = os.path.dirname(os.path.abspath(old_name))
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
|
||||
|
||||
os.rename(old_name, new_abs_path)
|
||||
print u"Renamed '{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
default_xform_list = [
|
||||
["^2000AD$", "2000 AD"],
|
||||
["^G\.{0,1}I\.{0,1}Joe$", "G.I. Joe"],
|
||||
]
|
||||
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
parsed_args = parse_args()
|
||||
|
||||
# parsed_args.noconfirm
|
||||
if parsed_args.transforms is not None:
|
||||
print "Reading in transforms from:", parsed_args.transforms
|
||||
json_data = open(parsed_args.transforms).read()
|
||||
data = json.loads(json_data)
|
||||
xform_list = data['xforms']
|
||||
else:
|
||||
xform_list = default_xform_list
|
||||
|
||||
#pprint( xform_list, indent=4)
|
||||
|
||||
filelist = utils.get_recursive_filelist(parsed_args.paths)
|
||||
|
||||
# first find all comics
|
||||
print "Reading in all comics..."
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
fmt_str = ""
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
# do we care if it already has metadata?
|
||||
if ca.seemsToBeAComicArchive() and not ca.hasMetadata(style):
|
||||
|
||||
comic_list.append(ca)
|
||||
|
||||
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()
|
||||
|
||||
print >> sys.stderr, fmt_str.format("")
|
||||
print "Found {0} comics.".format(len(comic_list))
|
||||
|
||||
modify_list = list()
|
||||
# walk through the comic list fix the file names
|
||||
for ca in comic_list:
|
||||
|
||||
# 1. parse the filename into a MD object
|
||||
md = ca.metadataFromFilename()
|
||||
# 2. walk through list of transforms
|
||||
if md.series is not None and md.series != "":
|
||||
for pattern, replacement in xform_list:
|
||||
# apply each transform
|
||||
new_series = re.sub(pattern, replacement, md.series)
|
||||
if new_series != md.series:
|
||||
md.series = new_series
|
||||
new_name = calculate_rename(ca, md, settings)
|
||||
|
||||
# found a match. add to proposed list, and bail on this
|
||||
# file
|
||||
modify_list.append((ca.path, new_name))
|
||||
break
|
||||
|
||||
print "{0} filenames to modify".format(len(modify_list))
|
||||
if len(modify_list) > 0:
|
||||
if parsed_args.noconfirm:
|
||||
print "Not confirming before rename"
|
||||
else:
|
||||
for old_name, new_name in modify_list:
|
||||
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'):
|
||||
print "exiting without rename."
|
||||
sys.exit(0)
|
||||
|
||||
perform_rename(modify_list)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,25 +1,23 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Create new comic archives from old one, removing pages marked as ads
|
||||
and deleted. Walks recursivly through the given folders. Originals
|
||||
are kept in a subfolder at the level of the original
|
||||
and deleted. Walks recursively through the given folders. Originals
|
||||
are kept in a sub-folder at the level of the original
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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
|
||||
@@ -32,118 +30,122 @@ from comictaggerlib.settings import *
|
||||
from comictaggerlib.comicarchive import *
|
||||
|
||||
subfolder_name = "PRE_AD_REMOVAL"
|
||||
unwanted_types = [ 'Deleted', 'Advertisment' ]
|
||||
unwanted_types = ['Deleted', 'Advertisement']
|
||||
|
||||
|
||||
def main():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
# this can only work with files with ComicRack tags
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "usage: {0} comic_folder ".format(sys.argv[0])
|
||||
return
|
||||
# this can only work with files with ComicRack tags
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
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:
|
||||
|
||||
ca = ComicArchive(filename, settings )
|
||||
if (ca.isZip or ca.isRar()) and ca.hasMetadata( style ):
|
||||
md = ca.readMetadata( style )
|
||||
if len(md.pages) != 0:
|
||||
for p in md.pages:
|
||||
if p.has_key('Type') and p['Type'] in unwanted_types:
|
||||
#This one has pages to remove. add to list!
|
||||
modify_list.append((filename, md))
|
||||
break
|
||||
|
||||
#now actually process those files
|
||||
for filename,md in modify_list:
|
||||
ca = ComicArchive(filename, settings )
|
||||
curr_folder = os.path.dirname( filename )
|
||||
curr_subfolder = os.path.join( curr_folder, subfolder_name )
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
|
||||
return
|
||||
|
||||
#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 p.has_key('Type') 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 p.has_key('Type'):
|
||||
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()
|
||||
filelist = utils.get_recursive_filelist(sys.argv[1:])
|
||||
|
||||
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.
|
||||
# first read in CIX metadata from all files, make a list of candidates
|
||||
modify_list = []
|
||||
for filename in filelist:
|
||||
|
||||
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 )
|
||||
md.pages = new_pages
|
||||
ca.writeMetadata( style, md )
|
||||
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:
|
||||
for p in md.pages:
|
||||
if 'Type' in p and p['Type'] in unwanted_types:
|
||||
# This one has pages to remove. add to list!
|
||||
modify_list.append((filename, md))
|
||||
break
|
||||
|
||||
# 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__':
|
||||
main()
|
||||
main()
|
||||
|
||||
190
scripts/shrink.py
Normal file
190
scripts/shrink.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/python
|
||||
"""Reduce the image size of pages in the comic archive"""
|
||||
|
||||
# Copyright 2013 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 shutil
|
||||
#import sys
|
||||
#import os
|
||||
#import tempfile
|
||||
#import zipfile
|
||||
|
||||
import Image
|
||||
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.comicarchive import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
subfolder_name = "ORIGINALS"
|
||||
max_height = 2000
|
||||
|
||||
|
||||
def main():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
# this can only work with files with ComicRack tags
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
|
||||
return
|
||||
|
||||
filelist = utils.get_recursive_filelist(sys.argv[1:])
|
||||
|
||||
# first make a list of all comic archive files
|
||||
comics_list = []
|
||||
max_name_len = 2
|
||||
fmt_str = u"{{0:{0}}}".format(max_name_len)
|
||||
for filename in filelist:
|
||||
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
if (ca.seemsToBeAComicArchive()):
|
||||
# Check the images in the file, see if we need to reduce any
|
||||
|
||||
for idx in range(ca.getNumberOfPages()):
|
||||
in_data = ca.getPage(idx)
|
||||
if in_data is not None:
|
||||
try:
|
||||
im = Image.open(StringIO.StringIO(in_data))
|
||||
w, h = im.size
|
||||
if h > max_height:
|
||||
comics_list.append(ca)
|
||||
|
||||
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()
|
||||
break
|
||||
|
||||
except IOError:
|
||||
# doesn't appear to be an image
|
||||
pass
|
||||
|
||||
print >> sys.stderr, fmt_str.format("") + "\r",
|
||||
print "--------------------------------------------------------------------------"
|
||||
print "Found {0} comics with over-large pages".format(len(comics_list))
|
||||
print "--------------------------------------------------------------------------"
|
||||
|
||||
for item in comics_list:
|
||||
print item.path
|
||||
|
||||
# now actually process those files with over-large pages
|
||||
for ca in comics_list:
|
||||
filename = ca.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("Processing: " + 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)
|
||||
|
||||
cix_md = None
|
||||
if ca.hasCIX():
|
||||
cix_md = ca.readCIX()
|
||||
|
||||
try:
|
||||
zout = zipfile.ZipFile(tmp_name, 'w')
|
||||
|
||||
# Check the images in the file, see if we want to reduce them
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
for idx in range(ca.getNumberOfPages()):
|
||||
name = ca.getPageName(idx)
|
||||
in_data = ca.getPage(idx)
|
||||
out_data = in_data
|
||||
if in_data is not None:
|
||||
try:
|
||||
im = Image.open(StringIO.StringIO(in_data))
|
||||
w, h = im.size
|
||||
if h > max_height:
|
||||
# resize the image
|
||||
hpercent = (max_height / float(h))
|
||||
wsize = int((float(w) * float(hpercent)))
|
||||
size = (wsize, max_height)
|
||||
im = im.resize(size, Image.ANTIALIAS)
|
||||
|
||||
output = StringIO.StringIO()
|
||||
im.save(output, format="JPEG", quality=85)
|
||||
out_data = output.getvalue()
|
||||
output.close()
|
||||
|
||||
except IOError:
|
||||
# doesn't appear to be an image
|
||||
pass
|
||||
|
||||
else:
|
||||
# page is empty?? nothing to write
|
||||
out_data = ""
|
||||
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
|
||||
# write out the new resized image
|
||||
zout.writestr(name, out_data)
|
||||
|
||||
# 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, w/o page info
|
||||
if cix_md is not None:
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
cix_md.pages = []
|
||||
ca.writeCIX(cix_md)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,73 +1,74 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
test archive cover against comicvine for a given issue ID
|
||||
"""Test archive cover against Comic Vine for a given issue ID
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
# Copyright 2013 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
|
||||
# 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
|
||||
# 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.
|
||||
"""
|
||||
# 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 sys
|
||||
#import os
|
||||
|
||||
import comictaggerlib.utils
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.issueidentifier import *
|
||||
from comictaggerlib.comicvinetalker import *
|
||||
#import comictaggerlib.utils
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print >> sys.stderr, "usage: {0} comicfile issueid".format(sys.argv[0])
|
||||
return
|
||||
|
||||
filename = sys.argv[1]
|
||||
issue_id = sys.argv[2]
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print >> sys.stderr, "Usage: {0} [comicfile][issueid]".format(
|
||||
sys.argv[0])
|
||||
return
|
||||
|
||||
filename = sys.argv[1]
|
||||
issue_id = sys.argv[2]
|
||||
|
||||
if not os.path.exists(filename):
|
||||
print >> sys.stderr, filename + ": not found!"
|
||||
return
|
||||
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print >> sys.stderr, "Sorry, but " + \
|
||||
filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
ii = IssueIdentifier(ca, settings)
|
||||
|
||||
# calculate the hashes of the first two pages
|
||||
cover_image_data = ca.getPage(0)
|
||||
cover_hash0 = ii.calculateHash(cover_image_data)
|
||||
cover_image_data = ca.getPage(1)
|
||||
cover_hash1 = ii.calculateHash(cover_image_data)
|
||||
hash_list = [cover_hash0, cover_hash1]
|
||||
|
||||
comicVine = ComicVineTalker()
|
||||
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 "Looks like a match!"
|
||||
else:
|
||||
print "Bad score, maybe not a match?"
|
||||
print result['url']
|
||||
|
||||
if not os.path.exists(filename):
|
||||
print >> sys.stderr, filename + ": not found!"
|
||||
return
|
||||
|
||||
ca = ComicArchive(filename, settings )
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print >> sys.stderr, "Sorry, but "+ filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
# calculate the hashes of the first two pages
|
||||
cover_image_data = ca.getPage( 0 )
|
||||
cover_hash0 = ii.calculateHash( cover_image_data )
|
||||
cover_image_data = ca.getPage( 1 )
|
||||
cover_hash1 = ii.calculateHash( cover_image_data )
|
||||
hash_list = [ cover_hash0, cover_hash1 ]
|
||||
|
||||
comicVine = ComicVineTalker( )
|
||||
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 "Looks like a match!"
|
||||
else:
|
||||
print "Bad score, maybe not a match?"
|
||||
print result['url']
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
8
scripts/xforms
Normal file
8
scripts/xforms
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"_comment": "This file contains JSON data. Any backslashes should be escaped with another backslash",
|
||||
"xforms":
|
||||
[
|
||||
[ "^2000AD$", "2000 AD" ],
|
||||
[ "^G\\.{0,1}I\\.{0,1}Joe$", "G.I. Joe" ]
|
||||
]
|
||||
}
|
||||
84
setup.py
84
setup.py
@@ -1,42 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from distutils.core import setup
|
||||
from setuptools import setup
|
||||
import comictaggerlib.ctversion
|
||||
|
||||
setup(name = "comictagger",
|
||||
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 = "http://comictagger.googlecode.com/files/comictagger-{0}.zip".format(comictaggerlib.ctversion.version),
|
||||
packages = [ "comictaggerlib", "comictaggerlib/UnRAR2" ] ,
|
||||
package_data = {
|
||||
'comictaggerlib': ['ui/*.ui', 'graphics/*'] ,
|
||||
'comictaggerlib/UnRAR2': ['UnRARDLL/*.*', 'UnRARDLL/x64/*.*'] ,
|
||||
},
|
||||
scripts = ["comictagger.py"],
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Environment :: MacOS X",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Other/Nonlisted Topic",
|
||||
"Topic :: Multimedia :: Graphics"
|
||||
],
|
||||
license = "Apache License 2.0",
|
||||
|
||||
long_description = """
|
||||
ComicTagger is a multi-platform app for writing metadata to comic archives, written in Python and PyQt.
|
||||
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",
|
||||
|
||||
long_description="""
|
||||
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
|
||||
|
||||
Features:
|
||||
|
||||
@@ -46,17 +50,17 @@ Features:
|
||||
* 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.
|
||||
* Command line interface (CLI) on all platforms (including Windows), which supports batch operations, and which can be used in native scripts for complex operations.
|
||||
|
||||
Requires:
|
||||
|
||||
* python 2.6 or 2.7
|
||||
* configparser
|
||||
* configparser
|
||||
* python imaging (PIL) >= 1.1.6
|
||||
* beautifulsoup > 4.1
|
||||
|
||||
|
||||
Optional requirement (for GUI):
|
||||
|
||||
* pyqt4
|
||||
"""
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
44
todo.txt
44
todo.txt
@@ -1,27 +1,52 @@
|
||||
TOP!:
|
||||
Does utils.get_actual_preferred_encoding() work on Mac python source version??
|
||||
(And does it matter?)
|
||||
|
||||
-----------------------------------------------------
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
Rename dialog:
|
||||
check-box for rows?
|
||||
manual edit the preview?
|
||||
|
||||
Maybe replace configparser -- seems to be causing all sorts of problems
|
||||
|
||||
Feature Requests:
|
||||
Move CBR to other folder after conversion to ZIP
|
||||
Pre-process series name before identification
|
||||
(using a list of regex transforms)
|
||||
(GC #24) Multiple options for -t i.e. "-t cr,cbl"
|
||||
(GC #18 ) Option for handling colon in rename
|
||||
(GC #31 ) Specify CV Series ID for auto-tag
|
||||
Re-org - move to new folder based on template
|
||||
|
||||
Denied Requests (for now):
|
||||
Auto-rename on auto-tag
|
||||
Re-zip (to remove compression)
|
||||
|
||||
|
||||
Selective fields on CLI print (use -m option. Maybe internally remove all but specified fields in MD object before print )
|
||||
|
||||
Docs:
|
||||
Auto-Tagging Tips:
|
||||
Multiple Passes with different options
|
||||
|
||||
Multiple Passes with different options
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
|
||||
Zip flakes out when filename differs from index (or whatever) i.e "\" vs "/". Python issue
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Big Future Features
|
||||
-----------------------------------------------------
|
||||
Support for ACBF metadata in CBZ
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
Batch Edit
|
||||
(GC #29) Batch Edit
|
||||
Form Mode: Single vs Batch
|
||||
|
||||
-----------------------------------------------------
|
||||
@@ -48,10 +73,7 @@ Google App engine to store hashes
|
||||
|
||||
Filename parsing:
|
||||
Rework how series name is separated from issue
|
||||
|
||||
Support marvel's "AU" issues...
|
||||
Mostly done, gotta wait and see what CV does
|
||||
|
||||
|
||||
Internal GenericMetadata - Make Characters, Genre into lists?
|
||||
|
||||
-----------------------------------------------------
|
||||
@@ -67,8 +89,12 @@ Release Process
|
||||
Make dmg on Mac
|
||||
Make zip on Mac or Linux
|
||||
Tag the repository
|
||||
Upload packages
|
||||
Announce on Forum and Main Page
|
||||
Manually upload release packages to Google Drive
|
||||
Update the Downloads wiki page with direct links
|
||||
"make upload" to the pypi site
|
||||
Announce on Forum and Main Page and Twitter and Facebook
|
||||
MacUpdate
|
||||
Update appspot value
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# rm, cp, grep, cut, cat
|
||||
|
||||
HOMEPATH ?= $(HOME)
|
||||
TAGGER_BASE:= $(HOMEPATH)/Dropbox/tagger/comictagger
|
||||
TAGGER_BASE?= $(HOMEPATH)/Dropbox/tagger/comictagger
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
DIST_DIR := $(TAGGER_BASE)\windows\dist
|
||||
NSIS_CMD := "C:\Program Files (x86)\NSIS\makensis.exe"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ComicTagger - Copyright 2012 Anthony Beville
|
||||
ComicTagger - Copyright 2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
Reference in New Issue
Block a user