Compare commits

...

69 Commits

Author SHA1 Message Date
0484d05462 bump version to 1.1.16-beta-rc2 2017-04-17 17:12:54 +02:00
0766bf7064 fix drag and drop issues on macOS 2017-04-17 17:00:47 +02:00
7e8fc143fd #87 fix ssl context in several places. update comicvine api url. 2017-04-17 17:00:00 +02:00
43a913294e update release notes 2017-04-07 19:48:40 +02:00
70e28c7863 handle missing libunrar. update macos makefile. remove version check window. bump version. 2017-04-07 19:38:36 +02:00
14713d8ad0 #87 fix ssl comicvine communication 2017-04-07 17:28:10 +02:00
4ff2061568 Merge pull request #74 from Alkpone/master
Bugs in move2folder.py script
2015-03-22 10:49:21 +01:00
08c402149b Prevent error when no file has been detected
Script raised an unhandled exception:  local variable 'fmt_str' referenced before assignment
Traceback (most recent call last):
  File "/volume1/@appstore/comictagger/comictaggerlib/options.py", line 233, in launch_script
    script.main()
  File "/volume1/@appstore/comictagger/scripts/move2folder.py", line 90, in main
    print >> sys.stderr, fmt_str.format("")
UnboundLocalError: local variable 'fmt_str' referenced before assignment
2015-03-21 14:32:55 +01:00
184dbf0684 Prevent error when running the script
Script raised an unhandled exception:  coercing to Unicode: need string or buffer, NoneType found
Traceback (most recent call last):
  File "/root/comictagger/comictaggerlib/options.py", line 233, in launch_script
    script.main()
  File "scripts/move2folder.py", line 80, in main
    ca = ComicArchive(filename, settings.rar_exe_path)
  File "/root/comictagger/comicapi/comicarchive.py", line 648, in __init__
    with open(fname, 'rb') as fd:
TypeError: coercing to Unicode: need string or buffer, NoneType found
2015-03-21 14:17:05 +01:00
ed0050ba05 fixed typo 2015-03-06 11:26:47 +01:00
68030a1024 updated to unrar 0.3 2015-03-01 16:14:01 +01:00
983ad1fcf4 Merge branch 'fcanc-master' 2015-03-01 15:44:11 +01:00
d959ac0401 Huge code cleanup
- `autopep8 -aa` for general cleanup;
- Changed order of imports, they should be ordered into 3 groups:
1. standard library imports;
2. 3rd party packages;
3. project imports.
- I commented various imports that were reported as unused by my IDE.
If everything goes fine we can consider to delete them;
- The Apache license disclaimers are now comments since triple-quotes
should be used only for docstrings;
- Fix - `utils.centerWindowOnParent` did not resolve, changed to
`centerWindowOnParent`
2015-02-22 03:30:32 +01:00
2a550db02a Merge pull request #1 from davide-romanini/master
Merge davide-romanini commits
2015-02-18 20:44:28 +01:00
6369fa5fda updated readme 2015-02-16 16:34:38 +01:00
d5a13a4206 various fixes after merging comicstream-integr 2015-02-16 16:19:38 +01:00
b2532ce03a Merge branch 'comicstream-integr' 2015-02-16 16:18:00 +01:00
79a67d8c29 Merge pull request #71 from branch 'fcanc-master' 2015-02-16 14:51:57 +01:00
d9bd38674c added new dependencies to requirements.txt. with new unrar needs UNRAR_LIB_PATH to be set to start 2015-02-16 14:27:13 +01:00
a0154aaaae Merge commit '17f74cf2968a4e0aa01d7309afe7e1407b8abef2' into comicstream-integr 2015-02-16 14:09:21 +01:00
17f74cf296 Squashed 'comicapi/' changes from b7d2458..18f87d3
18f87d3 using comicapi subtree classes

git-subtree-dir: comicapi
git-subtree-split: 18f87d35b1b2cf5e135fad353419eda11209a6be
2015-02-16 14:09:21 +01:00
3f112cd578 Merge commit 'f6439049d8d8b5a4709f1b78afbfd289d00e8c25' as 'comicapi' 2015-02-16 13:27:21 +01:00
f6439049d8 Squashed 'comicapi/' content from commit b7d2458
git-subtree-dir: comicapi
git-subtree-split: b7d2458b80467a47be1d1d58b31ffcac62c2743c
2015-02-16 13:27:21 +01:00
2fe818872c removed splitted comicapi 2015-02-16 13:25:35 +01:00
a419969b85 autopep8 -aa
—aggressive, level 2
2015-02-15 12:55:04 +01:00
ee52448f17 autopep8 -a
—aggressive, level 1
2015-02-15 12:44:09 +01:00
79103990fa autopep8
automatically formats Python code to conform to the PEP 8 style guide —
default usage (whitespace changes only)
2015-02-15 11:44:00 +01:00
22dbafbc00 Code cleanup, round 1
Some formatting cleanup, plus print modernization, & typos correction.
2015-02-14 00:08:07 +01:00
0df283778c Indentation
Replaced tabs with spaces, and removed some trailing spaces.
2015-02-12 23:57:46 +01:00
a6282b5449 Move2folder script
Added a script to organize comics in a folder tree by Publisher/Series
(Volume).
2015-02-12 19:15:17 +01:00
5574280ad6 Filename parser tweaks
Fixes the Scan Info tag being left blank when the filename doesn’t
provide an issue number.
2015-02-12 19:09:33 +01:00
19b907b742 refactor (continue) 2015-02-11 19:45:45 +01:00
a9ff8f37b0 refactor core comicarchive classes in its own package comicapi 2015-02-11 19:45:02 +01:00
0769111f8c #70 added support for the day field on the gui 2015-02-09 21:50:02 +01:00
cf6ae8b5ae aligned with comicstreamer updates
refactor qt specific functions in utils.py in new ui.qtutils module
2015-02-02 17:20:48 +01:00
1d6846ced3 gitignore
changed to README.md for github.
2015-01-23 17:42:22 +01:00
d516d80093 Removed unused FileTableWidget, and explicitly set the column count. This fixes a problem on ArchLinux systems
git-svn-id: http://comictagger.googlecode.com/svn/trunk@744 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-07-06 18:19:50 +00:00
bf9ab71fd9 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@737 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-14 03:56:46 +00:00
33b00ad323 Text tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@736 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-14 03:56:32 +00:00
301ff084f1 fixes for webp, api key handling, and CV rate limit
git-svn-id: http://comictagger.googlecode.com/svn/trunk@734 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-13 06:26:44 +00:00
0c146bb245 minor fix
git-svn-id: http://comictagger.googlecode.com/svn/trunk@733 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-13 06:26:13 +00:00
08cc4a1acb Use pip-installed pyinstaller
git-svn-id: http://comictagger.googlecode.com/svn/trunk@732 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-13 06:25:35 +00:00
f97a1653d9 dos-ified release_notes file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@728 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-18 15:44:38 +00:00
d9dbab301a prep for release
git-svn-id: http://comictagger.googlecode.com/svn/trunk@727 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-18 15:42:05 +00:00
3d93197101 Added warning when rar is tried be loaded, and unrar tool isn't known
Fixed a bug when erroneous message is show when file is attempted to be reloaded

git-svn-id: http://comictagger.googlecode.com/svn/trunk@726 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-12 06:08:07 +00:00
752a1d8923 actual version bump
git-svn-id: http://comictagger.googlecode.com/svn/trunk@714 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 04:04:40 +00:00
68002daffa bumped version number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@713 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 04:02:13 +00:00
ad5062c582 Persist some auto-tag options
git-svn-id: http://comictagger.googlecode.com/svn/trunk@712 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 03:21:24 +00:00
2680468f34 New CBL transform to copy story arcs to generic tags
git-svn-id: http://comictagger.googlecode.com/svn/trunk@711 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 02:06:44 +00:00
6156fc296a Added settings option to auto-clear form when importing from CV
added settings option to remove html tables from CV summary

git-svn-id: http://comictagger.googlecode.com/svn/trunk@710 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 01:52:14 +00:00
0feed294d4 Avoid an exception condition
git-svn-id: http://comictagger.googlecode.com/svn/trunk@709 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 01:50:40 +00:00
e57736b955 Decouple comicarchive from settings
Enforce single instance of GUI app

git-svn-id: http://comictagger.googlecode.com/svn/trunk@708 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:13:04 +00:00
70fcdc0129 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@707 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:12:05 +00:00
9a64195ebd Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@706 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:10:18 +00:00
b0f229f851 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@705 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:09:03 +00:00
877a5ccd85 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@704 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:08:22 +00:00
c0f2e2f771 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@703 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:07:39 +00:00
0adfc9beb3 properly decode the user settings path
git-svn-id: http://comictagger.googlecode.com/svn/trunk@702 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:46:56 +00:00
d0bc41d7ee Allow user to specify the GUI start up tag style on the command line
git-svn-id: http://comictagger.googlecode.com/svn/trunk@701 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:44:47 +00:00
fa46a065a4 fixed some spelling errors
git-svn-id: http://comictagger.googlecode.com/svn/trunk@700 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:43:21 +00:00
8fcd5ba7d6 try to parse table HTML in the comment field
git-svn-id: http://comictagger.googlecode.com/svn/trunk@699 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:42:11 +00:00
759cdc6b40 use the requirements in the setup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@698 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:40:22 +00:00
1405d9ff0e more process tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@692 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 22:28:50 +00:00
d8fcbbad0a Upload the zip package to pypi index site also
git-svn-id: http://comictagger.googlecode.com/svn/trunk@691 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 22:28:03 +00:00
3eca25db34 changed build checklist
git-svn-id: http://comictagger.googlecode.com/svn/trunk@688 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:39:16 +00:00
c8a5a89369 changed download URL to point at google drive site
git-svn-id: http://comictagger.googlecode.com/svn/trunk@687 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:38:55 +00:00
ff578ea819 bumped version to 1.1.12
git-svn-id: http://comictagger.googlecode.com/svn/trunk@686 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:38:22 +00:00
1c730c25d5 removed auto-upload to google code site
git-svn-id: http://comictagger.googlecode.com/svn/trunk@685 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:38:02 +00:00
35b7b39b86 Don't choke when the version string server fails.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@683 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 20:59:35 +00:00
90 changed files with 13933 additions and 12272 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.idea/
/nbproject/
*.pyc

View File

@ -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
View 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!

View File

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

1
comicapi/__init__.py Normal file
View File

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

276
comicapi/comet.py Normal file
View 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

File diff suppressed because it is too large Load Diff

144
comicapi/comicbookinfo.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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]

View File

@ -1,5 +1,20 @@
#!/usr/bin/env python
import os
import sys
if getattr(sys, 'frozen', False):
# we are running in a bundle
frozen = 'ever so'
bundle_dir = sys._MEIPASS
else:
# we are running in a normal Python environment
bundle_dir = os.path.dirname(os.path.abspath(__file__))
# setup libunrar
if not os.environ.get("UNRAR_LIB_PATH", None):
os.environ["UNRAR_LIB_PATH"] = bundle_dir + "/libunrar.so"
from comictaggerlib.main import ctmain
if __name__ == '__main__':
ctmain()
ctmain()

View File

@ -1,233 +1,245 @@
"""
A PyQT4 dialog to select from automated issue matches
"""
"""A PyQT4 dialog to select from automated issue matches"""
"""
Copyright 2012-2014 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('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, match_set_list, style, fetch_func):
super(AutoTagMatchWindow, self).__init__(parent)
utils.reduceWidgetFontSize( self.twList )
utils.reduceWidgetFontSize( self.teDescription, 1 )
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']
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.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'] )
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)
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!"))

View File

@ -1,69 +1,69 @@
"""
A PyQT4 dialog to show ID log and progress
"""
"""A PyQT4 dialog to show ID log and progress"""
"""
Copyright 2012-2014 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
from coverimagewidget import CoverImageWidget
import utils
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.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.DataMode, False )
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
gridlayout.addWidget( self.archiveCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
self.testCoverWidget = CoverImageWidget( self.testCoverContainer, CoverImageWidget.DataMode, False )
gridlayout = QtGui.QGridLayout( self.testCoverContainer )
gridlayout.addWidget( self.testCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
self.isdone = False
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
uic.loadUi(
ComicTaggerSettings.getUIFile('autotagprogresswindow.ui'), self)
utils.reduceWidgetFontSize( self.textEdit )
def setArchiveImage( self, img_data):
self.setCoverImage( img_data, self.archiveCoverWidget)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtGui.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
def setTestImage( self, img_data):
self.setCoverImage( img_data, self.testCoverWidget)
self.testCoverWidget = CoverImageWidget(
self.testCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtGui.QGridLayout(self.testCoverContainer)
gridlayout.addWidget(self.testCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
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
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

View File

@ -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-2014 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

View File

@ -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-2014 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

View File

@ -1,260 +1 @@
"""
A python 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.
"""
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

View File

@ -1,152 +1 @@
"""
A python 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 )
#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 *

View File

@ -1,293 +1 @@
"""
A python 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.
"""
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'
):
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 )
from comicapi.comicinfoxml import *

View File

@ -1,459 +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-2014 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," +
"super_url TEXT," +
"thumb_url TEXT," +
"cover_date TEXT," +
"site_detail_url TEXT," +
"description TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id ) )"
)
# create 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)
with con:
con.text_factory = unicode
cur = con.cursor()
def add_volume_issues_info( self, volume_id, cv_volume_issues ):
con = lite.connect( self.db_file )
# remove all previous entries with this search term
cur.execute(
"DELETE FROM VolumeSearchCache WHERE search_term = ?", [
search_term.lower()])
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
# now add in new results
for record in cv_search_results:
timestamp = datetime.datetime.now()
# add in issues
if record['publisher'] is None:
pub_name = ""
else:
pub_name = record['publisher']['name']
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)
if record['image'] is None:
url = ""
else:
url = record['image']['super_url']
def get_volume_info( self, volume_id ):
result = None
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']))
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_search_results(self, search_term):
def get_volume_issues_info( self, volume_id ):
result = None
results = list()
con = lite.connect(self.db_file)
with con:
con.text_factory = unicode
cur = con.cursor()
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
# 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 add_issue_select_details( self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url ):
con = lite.connect( self.db_file )
# fetch
cur.execute(
"SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
rows = cur.fetchall()
# now process the results
for record in rows:
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)
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]
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()
results.append(result)
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
return results
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
def add_alt_covers(self, issue_id, url_list):
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 + " = ?"
con = lite.connect(self.db_file)
keys += ", " + pkname
vals.append( pkval )
ins_slots += ", ?"
condition = pkname + " = ?"
with con:
con.text_factory = unicode
cur = con.cursor()
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 )
# 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

View File

@ -1,312 +1,325 @@
"""
A PyQt4 widget display cover images from either local archive, or from ComicVine
"""A PyQt4 widget to display cover images
(TODO: This should be re-factored using subclasses!)
Display cover images from either a local archive, or from Comic Vine.
TODO: This should be re-factored using subclasses!
"""
"""
Copyright 2012-2014 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
DataMode = 3
def __init__(self, parent, mode, expand_on_click = True ):
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()
if expand_on_click:
clickable(self.lblImage).connect(self.showPopup)
else:
self.lblImage.setToolTip( "" )
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
self.imageData = None
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 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
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()
def clear(self):
self.resetWidget()
self.updateContent()
#defer the alt cover search
QTimer.singleShot(1, self.startAltCoverSearch)
def incrementImage(self):
self.imageIndex += 1
if self.imageIndex == self.imageCount:
self.imageIndex = 0
self.updateContent()
def startAltCoverSearch( self ):
def decrementImage(self):
self.imageIndex -= 1
if self.imageIndex == -1:
self.imageIndex = self.imageCount - 1
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 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 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 = QImage()
img.loadFromData( image_data )
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap( 0, 0)
#print "ATB cover fetch complete!"
def setURL(self, url):
if self.mode == CoverImageWidget.URLMode:
self.resetWidget()
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()
self.url_list = [url]
self.imageIndex = 0
self.imageCount = 1
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)
def setIssueID(self, issue_id):
if self.mode == CoverImageWidget.AltCoverMode:
self.resetWidget()
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()
self.issue_id = issue_id
new_h -= 4
new_w -= 4
if new_h < 0:
new_h = 0;
if new_w < 0:
new_w = 0;
self.comicVine = ComicVineTalker()
self.comicVine.urlFetchComplete.connect(
self.primaryUrlFetchComplete)
self.comicVine.asyncFetchIssueCoverURLs(int(self.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)
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)

View File

@ -1,99 +1,96 @@
"""
A PyQT4 dialog to edit credits
"""
"""A PyQT4 dialog to edit credits"""
"""
Copyright 2012-2014 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)

View File

@ -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.11-beta"
version = "1.1.16-beta-rc2"

View File

@ -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-2014 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

View File

@ -1,277 +1 @@
"""
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 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 ):
# Returns a tuple of issue number string, and start and end indexs 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 seperators 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 sufix
# 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-numerics
year = re.sub("[^0-9]", "", year)
return year
def getRemainder( self, filename, year, count, 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 year != "":
remainder = remainder.replace(year,"",1)
if count != "":
remainder = remainder.replace("of "+count,"",1)
remainder = remainder.replace("()","")
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 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", ")")
self.issue, issue_start, issue_end = self.getIssueNumber(filename)
self.series, self.volume = self.getSeriesName(filename, issue_start)
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, 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
from comicapi.filenameparser import *

View File

@ -1,150 +1,156 @@
"""
Functions for renaming files based on metadata
"""
"""Functions for renaming files based on metadata"""
"""
Copyright 2012-2014 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 duplicate spaces
new_name = u" ".join(new_name.split())
text_list = [x for x in text_list if token not in x]
return " ".join(text_list)
else:
return text.replace(token, "")
# 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())
def determineName(self, filename, ext=None):
if ext is None:
ext = os.path.splitext( filename )[1]
md = self.metdata
new_name = self.template
preferred_encoding = utils.get_actual_preferred_encoding()
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
# 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

View File

@ -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-2014 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,378 +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 getSorting(self):
col = self.twList.horizontalHeader().sortIndicatorSection()
order = self.twList.horizontalHeader().sortIndicatorOrder()
return col, order
reduceWidgetFontSize(self.twList)
def setSorting(self, col, order):
col = self.twList.horizontalHeader().setSortIndicator( col, order)
self.twList.setColumnCount(6)
#self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
# self.twList.horizontalHeader().setStretchLastSection(True)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
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 )
self.currentItem = None
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.modifiedFlag = False
def deselectAll( self ):
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), False )
selectAllAction = QAction("Select All", self)
removeAction = QAction("Remove Selected Items", self)
self.separator = QAction("", self)
self.separator.setSeparator(True)
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())
selectAllAction.setShortcut('Ctrl+A')
removeAction.setShortcut('Ctrl+X')
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.triggered.connect(self.selectAll)
removeAction.triggered.connect(self.removeSelection)
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
self.twList.setSortingEnabled(False)
self.addAction(selectAllAction)
self.addAction(removeAction)
self.addAction(self.separator)
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
def getSorting(self):
col = self.twList.horizontalHeader().sortIndicatorSection()
order = self.twList.horizontalHeader().sortIndicatorOrder()
return col, order
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)
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 setSorting(self, col, order):
col = self.twList.horizontalHeader().setSortIndicator(col, order)
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 addAppAction(self, action):
self.insertAction(None, action)
type_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
def setModifiedFlag(self, modified):
self.modifiedFlag = modified
cix_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
cix_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
def selectAll(self):
self.twList.setRangeSelected(
QTableWidgetSelectionRange(
0,
0,
self.twList.rowCount() -
1,
5),
True)
cbi_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
cbi_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
def deselectAll(self):
self.twList.setRangeSelected(
QTableWidgetSelectionRange(
0,
0,
self.twList.rowCount() -
1,
5),
False)
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 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 updateRow( self, row ):
fi = self.twList.item( row, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
def getArchiveByRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole).toPyObject()
return fi.ca
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 getCurrentArchive(self):
return self.getArchiveByRow(self.twList.currentRow())
item_text = os.path.split(fi.ca.path)[0]
folder_item.setText( item_text )
folder_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())
item_text = os.path.split(fi.ca.path)[1]
filename_item.setText( item_text )
filename_item.setData( Qt.ToolTipRole, item_text )
if len(row_list) == 0:
return
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 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)

View File

@ -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-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
# 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 = "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 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 *

View File

@ -1,198 +1,195 @@
"""
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-2014 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 ssl
#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()
# always use a tls context for urlopen
self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
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 clearCache(self):
os.unlink(self.db_file)
if os.path.isdir(self.cache_folder):
shutil.rmtree(self.cache_folder)
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!")
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
"""
# 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...
self.user_data = user_data
self.fetched_url = 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 )
# first look in the DB
image_data = self.get_image_from_cache(url)
self.fetchComplete.emit( QByteArray(image_data), self.user_data )
if blocking:
if image_data is None:
try:
image_data = urllib.urlopen(url, context=self.ssl).read()
except Exception as e:
print(e)
raise ImageFetcherException("Network Error!")
def create_image_db( self ):
# this will wipe out any existing version
open( self.db_file, 'w').close()
# save the image to the cache
self.add_image_to_cache(self.fetched_url, image_data)
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 )
else:
con = lite.connect( self.db_file )
# create tables
with con:
cur = con.cursor()
# 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
cur.execute("CREATE TABLE Images(" +
"url TEXT," +
"filename TEXT," +
"timestamp TEXT," +
"PRIMARY KEY (url) )"
)
# 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...
def add_image_to_cache( self, url, image_data ):
def finishRequest(self, reply):
con = lite.connect( self.db_file )
# read in the image data
image_data = reply.readAll()
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 )
)
# save the image to the cache
self.add_image_to_cache(self.fetched_url, image_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()
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
if row is None :
return None
else:
filename = row[0]
image_data = None
def create_image_db(self):
try:
with open( filename, 'rb' ) as f:
image_data = f.read()
f.close()
except IOError as e:
pass
return image_data
# this will wipe out any existing version
open(self.db_file, 'w').close()
# 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

View File

@ -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:])

View File

@ -1,86 +1,92 @@
"""
A PyQT4 widget to display a popup image
"""
"""A PyQT4 widget to display a popup image"""
"""
Copyright 2012-2014 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

View File

@ -1,174 +1,190 @@
"""
A PyQT4 dialog to select specific issue from list
"""
"""A PyQT4 dialog to select specific issue from list"""
"""
Copyright 2012-2014 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
import re
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())
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 )
utils.reduceWidgetFontSize( self.teDescription, 1 )
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 )
self.issue_list = comicVine.fetchIssuesByVolume( 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)
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 = 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)
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
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 )
# 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
QtGui.QApplication.restoreOverrideCursor()
def performQuery(self):
def cellDoubleClicked( self, r, c ):
self.accept()
def currentItemChanged( self, curr, prev ):
QtGui.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
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()
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
# 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
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

View File

@ -1,128 +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"
"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
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
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 *

View File

@ -1,40 +1,37 @@
"""
A PyQT4 dialog to a text file or log
"""
"""A PyQT4 dialog to a text file or log"""
"""
Copyright 2012-2014 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)

View File

@ -1,79 +1,87 @@
"""
A python app to (automatically) tag comic archives
"""
"""A python app to (automatically) tag comic archives"""
"""
Copyright 2012-2014 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())

View File

@ -1,160 +1,160 @@
"""
A PyQT4 dialog to select from automated issue matches
"""
"""A PyQT4 dialog to select from automated issue matches"""
"""
Copyright 2012-2014 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 )
utils.reduceWidgetFontSize( self.teDescription, 1 )
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']
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.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'] )
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)
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

View File

@ -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-2014 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()

View File

@ -1,372 +1,424 @@
"""
CLI options class for comictagger app
"""
"""CLI options class for ComicTagger app"""
"""
Copyright 2012-2014 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:
pass
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 )
-1, --assume-issue-one Assume issue number is 1 if not found ( relevent for -s )
-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.assume_issue_is_one_if_not_set = False
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 launch_script(self, scriptfile):
# 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(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)
example = "series=Kickers^, Inc. ,issue=1, year=1986"
"""
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:vonsrc: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" ] )
escaped_comma = "^,"
escaped_equals = "^="
replacement_token = "<_~_>"
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 in ("-1", "--assume-issue-one"):
self.assume_issue_is_one_if_not_set = True
if o == "--nooverwrite":
self.no_overwrite = 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)"
new_version = VersionChecker().getLatestVersion("", False)
if new_version is not None and new_version != ctversion.version:
print "----------------------------------------"
print "New version available online: {0}".format(new_version)
print "----------------------------------------"
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
md = GenericMetadata()
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 )
# 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.script is not None:
self.launch_script( self.script )
# 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 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
# 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
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 )
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)
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 )
# 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)

View File

@ -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-2014 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))

View File

@ -1,24 +1,20 @@
"""
A PyQt4 widget for editing the page list info
"""
"""A PyQt4 widget for editing the page list info"""
"""
Copyright 2012-2014 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)

View File

@ -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-2014 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)

View File

@ -1,42 +1,38 @@
"""
A PyQT4 dialog to show ID log and progress
"""
"""A PyQT4 dialog to show ID log and progress"""
"""
Copyright 2012-2014 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)

View File

@ -1,157 +1,163 @@
"""
A PyQT4 dialog to confirm rename
"""
"""A PyQT4 dialog to confirm rename"""
"""
Copyright 2012-2014 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)

View File

@ -1,24 +1,19 @@
"""
Settings class for comictagger app
"""
"""Settings class for ComicTagger app"""
"""
Copyright 2012-2014 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
@ -28,308 +23,500 @@ 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
self.check_for_new_version = True
self.send_usage_stats = False
# automatic settings
self.install_id = uuid.uuid4().hex
self.last_selected_save_data_style = 0
self.last_selected_load_data_style = 0
self.last_opened_folder = ""
self.last_main_window_width = 0
self.last_main_window_height = 0
self.last_main_window_x = 0
self.last_main_window_y = 0
self.last_form_side_width = -1
self.last_list_side_width = -1
self.last_filelist_sorted_column = -1
self.last_filelist_sorted_order = 0
# identifier settings
self.id_length_delta_thresh = 5
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
# Show/ask dialog flags
self.ask_about_cbi_in_rar = True
self.show_disclaimer = True
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
#filename parsing settings
self.parse_scan_info = 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):
# identifier settings
self.id_length_delta_thresh = 5
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
def readline_generator(f):
line = f.readline()
while line:
yield line
line = f.readline()
# 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
#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' )
# filename parsing settings
self.parse_scan_info = 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' )
# 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('filenameparser', 'parse_scan_info'):
self.parse_scan_info = self.config.getboolean( 'filenameparser', 'parse_scan_info' )
# CBL Tranform settings
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('comicvine', 'use_series_start_as_volume'):
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
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 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 ):
# 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
if not self.config.has_section( 'settings' ):
self.config.add_section( 'settings' )
# Auto-tag stickies
self.save_on_low_confidence = False
self.dont_use_year_when_identifying = False
self.assume_1_if_no_issue_num = False
self.ignore_leading_numbers_in_filename = False
self.remove_archive_after_successful_match = False
self.wait_and_retry_on_rate_limit = False
self.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 )
def __init__(self):
if not self.config.has_section( 'auto' ):
self.config.add_section( 'auto' )
self.settings_file = ""
self.folder = ""
self.setDefaultValues()
self.config.set( 'auto', 'install_id', self.install_id )
self.config.set( 'auto', 'last_selected_load_data_style', self.last_selected_load_data_style )
self.config.set( 'auto', 'last_selected_save_data_style', self.last_selected_save_data_style )
self.config.set( 'auto', 'last_opened_folder', self.last_opened_folder )
self.config.set( 'auto', 'last_main_window_width', self.last_main_window_width )
self.config.set( 'auto', 'last_main_window_height', self.last_main_window_height )
self.config.set( 'auto', 'last_main_window_x', self.last_main_window_x )
self.config.set( 'auto', 'last_main_window_y', self.last_main_window_y )
self.config.set( 'auto', 'last_form_side_width', self.last_form_side_width )
self.config.set( 'auto', 'last_list_side_width', self.last_list_side_width )
self.config.set( 'auto', 'last_filelist_sorted_column', self.last_filelist_sorted_column )
self.config.set( 'auto', 'last_filelist_sorted_order', self.last_filelist_sorted_order )
self.config = configparser.RawConfigParser()
self.folder = ComicTaggerSettings.getSettingsFolder()
if not self.config.has_section( 'identifier' ):
self.config.add_section( 'identifier' )
if not os.path.exists(self.folder):
os.makedirs(self.folder)
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 = os.path.join(self.folder, "settings")
if not self.config.has_section( 'dialogflags' ):
self.config.add_section( 'dialogflags' )
# 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( '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 )
# 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( '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 )
if self.unrar_exe_path == "":
if platform.system() != "Windows":
# see if it's in the path of unix user
if utils.which("unrar") is not None:
self.unrar_exe_path = utils.which("unrar")
if self.unrar_exe_path != "":
self.save()
if not self.config.has_section( 'cbl_transform' ):
self.config.add_section( 'cbl_transform' )
# 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))
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 )
def reset(self):
os.unlink(self.settings_file)
self.__init__()
if not self.config.has_section( 'rename' ):
self.config.add_section( 'rename' )
def load(self):
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)
def readline_generator(f):
line = f.readline()
while line:
yield line
line = f.readline()
#make sure the basedir is cached, in case we're on windows running a script from frozen binary
#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()

View File

@ -1,239 +1,272 @@
"""
A PyQT4 dialog to enter app settings
"""
"""A PyQT4 dialog to enter app settings"""
"""
Copyright 2012-2014 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.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.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")
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)
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()
validator = QtGui.QIntValidator(1, 4, self)
self.leIssueNumPadding.setValidator(validator)
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" )
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthDeltaThresh.setValidator(validator)
def clearCache( self ):
ImageFetcher().clearCache()
ComicVineCacher( ).clearCache()
QtGui.QMessageBox.information(self, self.name, "Cache has been cleared.")
self.settingsToForm()
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]) )
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 showRenameTab( self ):
self.tabWidget.setCurrentIndex(5)
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

View File

View File

@ -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 &quot;1&quot;</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 &quot;1&quot;</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/>

View 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

View File

@ -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>
@ -336,19 +342,156 @@
<attribute name="title">
<string>Comic Vine</string>
</attribute>
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
<property name="geometry">
<rect>
<x>30</x>
<y>30</y>
<width>240</width>
<height>25</height>
</rect>
</property>
<property name="text">
<string>Use Series Start Date as Volume</string>
</property>
</widget>
<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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A personal API key from &lt;a href=&quot;http://www.comicvine.com/api/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Comic Vine&lt;/span&gt;&lt;/a&gt; is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item row="1" column="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">
@ -385,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">
@ -411,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>

View File

@ -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>

View File

@ -1,632 +1 @@
# 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 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("-", " ")
# since the CV api changed, searches for series names with periods
# now explicity 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
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 *

View File

@ -1,91 +1,99 @@
"""
Version checker
"""
"""Version checker"""
"""
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 platform
import urllib,urllib2
import ctversion
import urllib2
#import os
#import urllib
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
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
# 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"
def getRequestUrl(self, uuid, use_stats):
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()
base_url = "http://comictagger1.appspot.com/latest"
args = ""
versionRequestComplete = pyqtSignal( str )
def asyncGetLatestVersion( self, uuid, use_stats ):
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"
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 ):
# read in the response
new_version = str(reply.readAll())
return base_url + args
if new_version is None or new_version == "":
return
def getLatestVersion(self, uuid, use_stats=True):
self.versionRequestComplete.emit( new_version.strip() )
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())

View File

@ -1,379 +1,418 @@
"""
A PyQT4 dialog to select specific series/volume from list
"""
"""A PyQT4 dialog to select specific series/volume from list"""
"""
Copyright 2012-2014 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, issue_count, 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.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.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
md.issueCount = self.issue_count
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
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.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.updateButtons()
self.performQuery()
self.twList.selectRow(0)
def logIDOutput( self, text ):
print unicode(text),
self.iddialog.textEdit.ensureCursorVisible()
self.iddialog.textEdit.insertPlainText(text)
def updateButtons(self):
if self.cv_search_results is not None and len(
self.cv_search_results) > 0:
enabled = True
else:
enabled = False
def identifyProgress( self, cur, total ):
self.iddialog.progressBar.setMaximum( total )
self.iddialog.progressBar.setValue( cur )
self.btnRequery.setEnabled(enabled)
self.btnIssues.setEnabled(enabled)
self.btnAutoSelect.setEnabled(enabled)
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(enabled)
def identifyCancel( self ):
self.ii.cancel = True
def identifyComplete( self ):
def requery(self,):
self.performQuery(refresh=True)
self.twList.selectRow(0)
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 autoSelect(self):
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()
if self.comic_archive is None:
QtGui.QMessageBox.information(
self, "Auto-Select", "You need to load a comic first!")
return
self.volume_id = found_match['volume_id']
self.issue_number = found_match['issue_number']
self.selectByID()
self.showIssues()
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 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
self.iddialog = IDProgressWindow(self)
self.iddialog.setModal(True)
self.iddialog.rejected.connect(self.identifyCancel)
self.iddialog.show()
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.ii = IssueIdentifier(self.comic_archive, self.settings)
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()
md = GenericMetadata()
md.series = self.series_name
md.issue = self.issue_number
md.year = self.year
md.issueCount = self.issue_count
#QtCore.QCoreApplication.processEvents()
self.progdialog.exec_()
self.ii.setAdditionalMetadata(md)
self.ii.onlyUseAdditionalMetaData = True
self.ii.cover_page_index = int(self.cover_index_list[0])
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.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)
def closeMe( self ):
print "closeme"
self.reject()
self.id_thread.start()
self.iddialog.exec_()
def searchProgressUpdate( self , current, total ):
self.progdialog.setMaximum(total)
self.progdialog.setValue(current)
def logIDOutput(self, text):
print unicode(text),
self.iddialog.textEdit.ensureCursorVisible()
self.iddialog.textEdit.insertPlainText(text)
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 identifyProgress(self, cur, total):
self.iddialog.progressBar.setMaximum(total)
self.iddialog.progressBar.setValue(cur)
self.twList.setSortingEnabled(False)
def identifyCancel(self):
self.ii.cancel = True
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
row = 0
for record in self.cv_search_results:
self.twList.insertRow(row)
def identifyComplete(self):
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)
matches = self.ii.match_list
result = self.ii.search_result
match_index = 0
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
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
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 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 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 found_match is not None:
self.iddialog.accept()
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.volume_id = found_match['volume_id']
self.issue_number = found_match['issue_number']
self.selectByID()
self.showIssues()
self.volume_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
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
# 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
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

View File

@ -1 +1 @@
1.1.7-beta
1.1.16-beta-rc

View File

@ -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())

View File

@ -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

View File

@ -1,186 +1,221 @@
---------------------------------
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
---------------------------------
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 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.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

View File

@ -1,3 +1,5 @@
configparser
beautifulsoup4 >= 4.1
PIL >= 1.1.6
unrar==0.3
natsort==3.5.2
PyPDF2==1.24

View File

@ -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/

View File

@ -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()

View File

@ -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()

View File

@ -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
View 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()

View File

@ -1,151 +1,163 @@
#!/usr/bin/python
"""
fix the comic file names using a list of transforms
"""
"""Fix the comic file names using a list of transforms"""
"""
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 re
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
#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)
input_args = sys.argv[1:]
return parsed_args
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 caclulate_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 ) )
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)
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()
default_xform_list = [
["^2000AD$", "2000 AD"],
["^G\.{0,1}I\.{0,1}Joe$", "G.I. Joe"],
]
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)
utils.fix_output_encoding()
settings = ComicTaggerSettings()
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 )
# do we care if it already has metadata?
if ca.seemsToBeAComicArchive() and not ca.hasMetadata( style ):
style = MetaDataStyle.CIX
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()
parsed_args = parse_args()
print >> sys.stderr, fmt_str.format( "" )
print "Found {0} comics.".format( len(comic_list))
# 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
modify_list = list()
# walk through the comic list fix the filenames
for ca in comic_list:
# 1. parse the filename into a MD object
md = ca.metadataFromFilename()
# 2. walk thru 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 = caclulate_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)
#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)
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()
main()

View File

@ -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()

View File

@ -1,189 +1,190 @@
#!/usr/bin/python
"""
Reduce the image size of pages in the comic archive
"""
"""Reduce the image size of pages in the comic archive"""
"""
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 tempfile
import zipfile
import shutil
#import sys
#import os
#import tempfile
#import zipfile
import Image
import comictaggerlib.utils
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()
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 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 )
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
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 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 )
filelist = utils.get_recursive_filelist(sys.argv[1:])
#skip any of our generated subfolders...
if os.path.basename(curr_folder) == subfolder_name:
continue
sys.stdout.write("Processing: " + filename)
# 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:
# 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()
ca = ComicArchive(filename, settings.rar_exe_path)
if (ca.seemsToBeAComicArchive()):
# Check the images in the file, see if we need to reduce any
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()
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)
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.
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
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 )
cix_md.pages = []
ca.writeCIX( cix_md )
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()
main()

View File

@ -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()

View File

@ -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
"""
)
"""
)

View File

@ -1,3 +1,7 @@
TOP!:
Does utils.get_actual_preferred_encoding() work on Mac python source version??
(And does it matter?)
-----------------------------------------------------
Features
-----------------------------------------------------
@ -11,12 +15,10 @@ Feature Requests:
Move CBR to other folder after conversion to ZIP
Pre-process series name before identification
(using a list of regex transforms)
(GC #28) Save auto-tag options
(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
repair incorrect extension
Denied Requests (for now):
Auto-rename on auto-tag
@ -40,7 +42,7 @@ Zip flakes out when filename differs from index (or whatever) i.e "\" vs "/". P
-----------------------------------------------------
Big Future Features
-----------------------------------------------------
Support for ACBF metatdata in CBZ
Support for ACBF metadata in CBZ
GCD scraper or DB reader
@ -87,8 +89,10 @@ Release Process
Make dmg on Mac
Make zip on Mac or Linux
Tag the repository
Upload packages
Announce on Forum and Main Page and Twitter
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