Compare commits
207 Commits
1.1.0-beta
...
1.1.13-bet
Author | SHA1 | Date | |
---|---|---|---|
752a1d8923 | |||
68002daffa | |||
ad5062c582 | |||
2680468f34 | |||
6156fc296a | |||
0feed294d4 | |||
e57736b955 | |||
70fcdc0129 | |||
9a64195ebd | |||
b0f229f851 | |||
877a5ccd85 | |||
c0f2e2f771 | |||
0adfc9beb3 | |||
d0bc41d7ee | |||
fa46a065a4 | |||
8fcd5ba7d6 | |||
759cdc6b40 | |||
1405d9ff0e | |||
d8fcbbad0a | |||
3eca25db34 | |||
c8a5a89369 | |||
ff578ea819 | |||
1c730c25d5 | |||
35b7b39b86 | |||
719c711484 | |||
afbbc9d00c | |||
b8e0a45fc8 | |||
b7360dd33e | |||
d9f1956426 | |||
b5c7f36410 | |||
0b0663d935 | |||
eee1f65436 | |||
9a8d4149f2 | |||
b02a205668 | |||
57284dfbed | |||
afcbde7fc6 | |||
151fac5bf1 | |||
57c1efdab9 | |||
6b272cef87 | |||
1cdc732739 | |||
d1b00d162d | |||
3dd3980bc1 | |||
cbf475eb26 | |||
ac8b575659 | |||
ac8ef286a4 | |||
f567dc37be | |||
15c5fc5258 | |||
cc985b52a5 | |||
910b0386be | |||
0fece23405 | |||
eee320e0c7 | |||
accabf8e21 | |||
acc253d35c | |||
ede0154efe | |||
5b805b1428 | |||
2e6b2a89db | |||
c028bb4ddc | |||
b70beb5684 | |||
128af4521b | |||
43cf7a80c8 | |||
3223ed190c | |||
9e2817c037 | |||
6e7bd10fb9 | |||
c099205779 | |||
47d8da0e80 | |||
0f7e88e58c | |||
65902a15b1 | |||
a68b2babeb | |||
4098802e43 | |||
9c14258e9f | |||
33bdbe8be8 | |||
a76864c109 | |||
cb68d07751 | |||
8e9fccdbbc | |||
39990fc2b4 | |||
e8c315d834 | |||
f8a06a8746 | |||
9415087da7 | |||
9aee5c32eb | |||
fcdb4a3889 | |||
534a326258 | |||
0390ff5919 | |||
b800ae1751 | |||
a2c17982d3 | |||
0347befae6 | |||
af54b79790 | |||
dd04ae98a0 | |||
31b76fba92 | |||
9f4a4b0eb0 | |||
575a23c6bf | |||
5d84f09359 | |||
3072583482 | |||
8d867cf78a | |||
36c79b5a2a | |||
dfdaf731b4 | |||
67bff8586c | |||
9e4cbea6e4 | |||
d150b2ce54 | |||
a20949cc4d | |||
e3fceb20a2 | |||
f4e00d9ef3 | |||
1980bd5988 | |||
db54affc74 | |||
0edb9444ef | |||
b22c25f53f | |||
76e6666a79 | |||
a804a10e0e | |||
fe413b12c1 | |||
e38dc2f063 | |||
5e5418090b | |||
56c1f8582a | |||
00f8c0a280 | |||
1d915eb155 | |||
b7b8060ef2 | |||
2d190b076a | |||
cd92b1afea | |||
4d21a001d6 | |||
4af59d2315 | |||
c9c98b6c11 | |||
1ff43db2ce | |||
822f6b4729 | |||
44a8dc6815 | |||
a35576895c | |||
631662b30c | |||
cbe3f5a2dc | |||
73f8bd426b | |||
0642604480 | |||
1d95f5076e | |||
53b0c2e8f9 | |||
f59f5fe981 | |||
67545d8a13 | |||
ab3e3b40c4 | |||
188024c2db | |||
324b56a623 | |||
782d424392 | |||
cf63bfda9d | |||
903d4c647c | |||
407b83fe90 | |||
27edc80d2b | |||
01f48f8b91 | |||
527e690170 | |||
d100572aa4 | |||
42640c4ad5 | |||
a61972e503 | |||
464e147223 | |||
8759784561 | |||
ee5b4a689e | |||
71ccf1eea8 | |||
a9ee7c463b | |||
6f683a71c7 | |||
24b192b22c | |||
b6b1a4737f | |||
00202cc865 | |||
235524b06d | |||
8a7f822970 | |||
ff3f048bb4 | |||
abda202f32 | |||
2d4ac84de0 | |||
86732e7827 | |||
693b5b1978 | |||
e3d3ecfd31 | |||
ce6b81ab73 | |||
501365b5a3 | |||
c6741d4392 | |||
42feae53dd | |||
c65695b8dc | |||
4da71e262b | |||
c519fd33d5 | |||
07ef0211b9 | |||
c45b56a5b6 | |||
6f27fc7669 | |||
4530ac017c | |||
400fe6efa3 | |||
ac7a12d18d | |||
c2ff11fab7 | |||
34019ff338 | |||
176bc43888 | |||
2e290c4c74 | |||
74a374d46b | |||
58f5f10c78 | |||
7d8ed954a9 | |||
078b3cef3c | |||
22ef0250ca | |||
cc53162dcc | |||
fa309cfcef | |||
4d57b0cf79 | |||
6ea5d28609 | |||
9d56a2ce9a | |||
811759478a | |||
28e2d93314 | |||
93b3117699 | |||
10e6a1019e | |||
2024555780 | |||
e15c3fa3e6 | |||
8aa6403f51 | |||
fb5fca1dc4 | |||
75d5b1a695 | |||
e56d9bddbf | |||
7d9aa70dc0 | |||
6d72ed2a69 | |||
9b584f78a0 | |||
dfe0e74f9c | |||
a11c08a2ee | |||
9159204883 | |||
605e27ce99 | |||
2dc08b36ea | |||
60dae4f1fb |
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include README.txt
|
||||
include release_notes.txt
|
||||
include requirements.txt
|
||||
recursive-include scripts *.py *.txt
|
64
Makefile
@ -1,25 +1,61 @@
|
||||
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
|
||||
VERSION_STR := $(shell grep version $(TAGGER_BASE)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
|
||||
|
||||
TAGGER_BASE ?= $(HOME)/Dropbox/tagger/comictagger
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
|
||||
PASSWORD := $(shell cat $(TAGGER_BASE)/project_password.txt)
|
||||
UPLOAD_TOOL := $(TAGGER_BASE)/google/googlecode_upload.py
|
||||
all: clean
|
||||
|
||||
clean:
|
||||
rm -f *~ *.pyc *.pyo
|
||||
rm -f logdict*.log
|
||||
rm -rf *~ *.pyc *.pyo
|
||||
rm -rf scripts/*.pyc
|
||||
cd comictaggerlib; rm -f *~ *.pyc *.pyo
|
||||
rm -rf dist MANIFEST
|
||||
rm -rf *.deb
|
||||
rm -rf logdict*.log
|
||||
make -C mac clean
|
||||
make -C windows clean
|
||||
rm -rf build
|
||||
|
||||
zip:
|
||||
cd release; \
|
||||
rm -rf *zip comictagger-src-$(VERSION_STR) ; \
|
||||
svn export https://comictagger.googlecode.com/svn/trunk/ comictagger-src-$(VERSION_STR); \
|
||||
zip -r comictagger-src-$(VERSION_STR).zip comictagger-src-$(VERSION_STR); \
|
||||
rm -rf comictagger-src-$(VERSION_STR)
|
||||
|
||||
pydist:
|
||||
mkdir -p release
|
||||
rm -f release/*.zip
|
||||
python setup.py sdist --formats=zip #,gztar
|
||||
mv dist/comictagger-$(VERSION_STR).zip release
|
||||
@echo When satisfied with release, do this:
|
||||
@echo make svn_tag
|
||||
|
||||
remove_test_install:
|
||||
sudo rm -rf /usr/local/bin/comictagger.py
|
||||
sudo rm -rf /usr/local/lib/python2.7/dist-packages/comictagger*
|
||||
|
||||
#deb:
|
||||
# fpm -s python -t deb \
|
||||
# -n 'comictagger' \
|
||||
# --category 'utilities' \
|
||||
# --maintainer 'comictagger@gmail.com' \
|
||||
# --after-install debian_scripts/after_install.sh \
|
||||
# --before-remove debian_scripts/before_remove.sh \
|
||||
# -d 'python >= 2.6' \
|
||||
# -d 'python < 2.8' \
|
||||
# -d 'python-imaging' \
|
||||
# -d 'python-bs4' \
|
||||
# --deb-suggests 'rar' \
|
||||
# --deb-suggests 'unrar-free' \
|
||||
# --python-install-bin /usr/share/comictagger \
|
||||
# --python-install-lib /usr/share/comictagger \
|
||||
# setup.py
|
||||
#
|
||||
# # For now, don't require PyQt, since command-line is available without it
|
||||
# #-d 'python-qt4 >= 4.8'
|
||||
|
||||
upload:
|
||||
#$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Source" -l Featured,Type-Source -u beville -w $(PASSWORD) "release/comictagger-$(VERSION_STR).zip"
|
||||
#$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Mac OS X" -l Featured,Type-Archive -u beville -w $(PASSWORD) "release/ComicTagger-$(VERSION_STR).dmg"
|
||||
#$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Windows" -l Featured,Type-Installer -u beville -w $(PASSWORD) "release/ComicTagger v$(VERSION_STR).exe"
|
||||
#python setup.py register
|
||||
python setup.py sdist --formats=zip upload
|
||||
|
||||
svn_tag:
|
||||
svn copy https://comictagger.googlecode.com/svn/trunk \
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
||||
|
31
README.txt
Normal file
@ -0,0 +1,31 @@
|
||||
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!
|
604
comictagger.py
@ -1,601 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
"""
|
||||
A python script to tag comic archives
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
from pprint import pprint
|
||||
import json
|
||||
import platform
|
||||
import locale
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from taggerwindow import TaggerWindow
|
||||
except ImportError as e:
|
||||
qt_available = False
|
||||
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options, MetaDataStyle
|
||||
from comicarchive import ComicArchive
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
|
||||
import utils
|
||||
import codecs
|
||||
|
||||
class MultipleMatch():
|
||||
def __init__( self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
class OnlineMatchResults():
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
|
||||
return cv_md
|
||||
|
||||
def actual_metadata_save( ca, opts, md ):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print >> sys.stderr,"The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print >> sys.stderr,"Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written"
|
||||
else:
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
return True
|
||||
|
||||
def display_match_set_for_choice( label, match_set, opts, settings ):
|
||||
print "{0} -- {1}:".format(match_set.filename, label )
|
||||
|
||||
# sort match list by year
|
||||
match_set.matches.sort(key=lambda k: k['year'])
|
||||
|
||||
for (counter,m) in enumerate(match_set.matches):
|
||||
counter += 1
|
||||
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
|
||||
m['series'],
|
||||
m['issue_number'],
|
||||
m['publisher'],
|
||||
m['month'],
|
||||
m['year'],
|
||||
m['issue_title'])
|
||||
if opts.interactive:
|
||||
while True:
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1,len(match_set.matches)+1)) or i == 's':
|
||||
break
|
||||
if i != 's':
|
||||
i = int(i) - 1
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = ComicArchive( match_set.filename )
|
||||
if settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( settings.rar_exe_path )
|
||||
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
|
||||
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings)
|
||||
md.overlay( cv_md )
|
||||
actual_metadata_save( ca, opts, md )
|
||||
|
||||
|
||||
def post_process_matches( match_results, opts, settings ):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len( match_results.goodMatches ) > 0:
|
||||
print "\nSuccessful matches:"
|
||||
print "------------------"
|
||||
for f in match_results.goodMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.noMatches ) > 0:
|
||||
print "\nNo matches:"
|
||||
print "------------------"
|
||||
for f in match_results.noMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.writeFailures ) > 0:
|
||||
print "\nFile Write Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.writeFailures:
|
||||
print f
|
||||
|
||||
if len( match_results.fetchDataFailures ) > 0:
|
||||
print "\nNetwork Data Fetch Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.fetchDataFailures:
|
||||
print f
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
#just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nArchives with multiple high-confidence matches:"
|
||||
print "------------------"
|
||||
for match_set in match_results.multipleMatches:
|
||||
display_match_set_for_choice( "Multiple high-confidence matches", match_set, opts, settings )
|
||||
|
||||
if len( match_results.lowConfidenceMatches ) > 0:
|
||||
print "\nArchives with low-confidence matches:"
|
||||
print "------------------"
|
||||
for match_set in match_results.lowConfidenceMatches:
|
||||
if len( match_set.matches) == 1:
|
||||
label = "Single low-confidence match"
|
||||
else:
|
||||
label = "Multiple low-confidence matches"
|
||||
|
||||
display_match_set_for_choice( label, match_set, opts, settings )
|
||||
|
||||
|
||||
def cli_mode( opts, settings ):
|
||||
if len( opts.file_list ) < 1:
|
||||
print >> sys.stderr,"You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli( f, opts, settings, match_results )
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches( match_results, opts, settings )
|
||||
|
||||
|
||||
def create_local_metadata( opts, ca, has_desired_tags ):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.setDefaultPageList( ca.getNumberOfPages() )
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata( opts.data_style )
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
|
||||
return md
|
||||
|
||||
def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
batch_mode = len( opts.file_list ) > 1
|
||||
|
||||
ca = ComicArchive(filename)
|
||||
if settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( settings.rar_exe_path )
|
||||
|
||||
if not os.path.lexists( filename ):
|
||||
print >> sys.stderr,"Cannot find "+ filename
|
||||
return
|
||||
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print >> sys.stderr,"Sorry, but "+ filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
|
||||
print >> sys.stderr,"This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [ False, False, False ]
|
||||
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
|
||||
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
|
||||
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
|
||||
if batch_mode:
|
||||
brief = "{0}: ".format(filename)
|
||||
|
||||
if ca.isZip(): brief += "ZIP archive "
|
||||
elif ca.isRar(): brief += "RAR archive "
|
||||
elif ca.isFolder(): brief += "Folder archive "
|
||||
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
|
||||
brief += "none "
|
||||
else:
|
||||
if has[ MetaDataStyle.CBI ]: brief += "CBL "
|
||||
if has[ MetaDataStyle.CIX ]: brief += "CR "
|
||||
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print brief
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if has[ MetaDataStyle.CIX ]:
|
||||
print "------ComicRack tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
|
||||
else:
|
||||
print u"{0}".format(ca.readCIX())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if has[ MetaDataStyle.CBI ]:
|
||||
print "------ComicBookLover tags--------"
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print u"{0}".format(ca.readCBI())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[ MetaDataStyle.COMET ]:
|
||||
print "------CoMet tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCoMet())
|
||||
else:
|
||||
print u"{0}".format(ca.readCoMet())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if has[ opts.data_style ]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata( opts.data_style ):
|
||||
print "{0}: Tag removal seemed to fail!".format( filename )
|
||||
else:
|
||||
print "{0}: Removed {1} tags.".format( filename, style_name )
|
||||
else:
|
||||
print "{0}: dry-run. {1} tags not removed".format( filename, style_name )
|
||||
else:
|
||||
print "{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print "{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print "{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[ opts.copy_source ]
|
||||
if has[ opts.copy_source ]:
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print u"{0}: Tag copy seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
|
||||
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print u"Processing {0}...".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
|
||||
# now, search online
|
||||
if opts.search_online:
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print >> sys.stderr,"Network error while getting issue details. Save aborted"
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
if cv_md is None:
|
||||
print >> sys.stderr,"No match for ID {0} was found.".format(opts.issue_id)
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
else:
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print >> sys.stderr,"No metadata given to search online with!"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
if low_confidence:
|
||||
print >> sys.stderr,"Online search: Multiple low confidence matches. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
else:
|
||||
print >> sys.stderr,"Online search: Multiple good matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print >> sys.stderr,"Online search: Low confidence match. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if not found_match:
|
||||
print >> sys.stderr,"Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings)
|
||||
if cv_md is None:
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
md.overlay( cv_md )
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save( ca, opts, md ):
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[ opts.data_style ]
|
||||
else:
|
||||
use_tags = False
|
||||
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print >> sys.stderr, msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
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( settings.rename_template )
|
||||
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print >> sys.stderr, msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.rename( filename, new_abs_path )
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
|
||||
|
||||
elif opts.export_to_zip:
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if not ca.isRar():
|
||||
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
|
||||
return
|
||||
|
||||
rar_file = os.path.abspath( os.path.abspath( filename ) )
|
||||
new_file = os.path.splitext(rar_file)[0] + ".cbz"
|
||||
|
||||
if opts.abort_export_on_conflict and os.path.lexists( new_file ):
|
||||
print msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1])
|
||||
return
|
||||
|
||||
new_file = utils.unique_file( os.path.join( new_file ) )
|
||||
|
||||
delete_success = False
|
||||
export_success = False
|
||||
if not opts.dryrun:
|
||||
if ca.exportAsZip( new_file ):
|
||||
export_success = True
|
||||
if opts.delete_rar_after_export:
|
||||
try:
|
||||
os.unlink( rar_file )
|
||||
except:
|
||||
print >> sys.stderr, msg_hdr + "Error deleting original RAR after export"
|
||||
delete_success = False
|
||||
else:
|
||||
delete_success = True
|
||||
else:
|
||||
# last export failed, so remove the zip, if it exists
|
||||
if os.path.lexists( new_file ):
|
||||
os.remove( new_file )
|
||||
else:
|
||||
msg = msg_hdr + u"Dry-run: Would try to create {0}".format(os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export:
|
||||
msg += u" and delete orginal."
|
||||
print msg
|
||||
return
|
||||
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += u"Archive exported successfully to: {0}".format( os.path.split(new_file)[1] )
|
||||
if opts.delete_rar_after_export and delete_success:
|
||||
msg += u" (Original deleted) "
|
||||
else:
|
||||
msg += u"Archive failed to export!"
|
||||
|
||||
print msg
|
||||
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def main():
|
||||
|
||||
# try to make stdout encodings happy for unicode
|
||||
if platform.system() == "Darwin":
|
||||
preferred_encoding = "utf-8"
|
||||
else:
|
||||
preferred_encoding = locale.getpreferredencoding()
|
||||
sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout)
|
||||
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
|
||||
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
|
||||
settings = ComicTaggerSettings()
|
||||
# make sure unrar program is in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(settings.unrar_exe_path))
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print >> sys.stderr, "QT is not available."
|
||||
|
||||
if opts.no_gui:
|
||||
cli_mode( opts, settings )
|
||||
|
||||
else:
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/tags.png' ))
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.file_list, settings )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish( tagger_window )
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception, e:
|
||||
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ctmain()
|
@ -33,7 +33,7 @@ similar to the C interface provided by UnRAR. There is also a
|
||||
higher level interface which makes some common operations easier.
|
||||
"""
|
||||
|
||||
__version__ = '0.99.2'
|
||||
__version__ = '0.99.3'
|
||||
|
||||
try:
|
||||
WindowsError
|
@ -12,13 +12,11 @@ def cleanup(dir='test'):
|
||||
os.removedirs(os.path.join(path, dir))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# reuse RarArchive object, en
|
||||
# basic test
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test.rar')
|
||||
rarc.infolist()
|
||||
assert rarc.comment == "This is a test."
|
||||
for info in rarc.infoiter():
|
||||
saveinfo = info
|
||||
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
|
||||
@ -98,7 +96,8 @@ list(UnRAR2.RarFile('test_nulls.rar').infoiter())
|
||||
|
||||
# extract files from an archive with protected files
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="protected").extract()
|
||||
rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected")
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
@ -33,6 +33,7 @@ from rar_exceptions import *
|
||||
class UnpackerNotInstalled(Exception): pass
|
||||
|
||||
rar_executable_cached = None
|
||||
rar_executable_version = None
|
||||
|
||||
def call_unrar(params):
|
||||
"Calls rar/unrar command line executable, returns stdout pipe"
|
||||
@ -59,10 +60,10 @@ def call_unrar(params):
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
global rar_executable_version
|
||||
self.password = password
|
||||
|
||||
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', []).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
@ -73,18 +74,42 @@ class RarFileImplementation(object):
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
while not (line.startswith('UNRAR')):
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
signature = line
|
||||
# The code below is mighty flaky
|
||||
# and will probably crash on localized versions of RAR
|
||||
# but I see no safe way to rewrite it using a CLI tool
|
||||
if signature.startswith("UNRAR 4"):
|
||||
rar_executable_version = 4
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:] # strip out "Comment:" part
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
else:
|
||||
self.comment = None
|
||||
elif signature.startswith("UNRAR 5"):
|
||||
rar_executable_version = 5
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:]
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
while not line.startswith('Archive:'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
self.comment = '\n'.join(accum[:-1]).strip()
|
||||
else:
|
||||
self.comment = None
|
||||
else:
|
||||
self.comment = None
|
||||
raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: "
|
||||
+ signature.split(" ")[1])
|
||||
|
||||
|
||||
def escaped_password(self):
|
||||
return '-' if self.password == None else self.password
|
||||
@ -97,7 +122,8 @@ class RarFileImplementation(object):
|
||||
|
||||
def infoiter(self):
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
|
||||
command = "v" if rar_executable_version == 4 else "l"
|
||||
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
@ -106,33 +132,48 @@ class RarFileImplementation(object):
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not line.startswith('--------------'):
|
||||
while not line.startswith('-----------'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
if line.find("CRC failed")>=0:
|
||||
if line.startswith("CRC failed") or line.startswith("Checksum error"):
|
||||
raise IncorrectRARPassword
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
i = 0
|
||||
re_spaces = re.compile(r"\s+")
|
||||
while not line.startswith('--------------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
if rar_executable_version == 4:
|
||||
while not line.startswith('-----------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
# asterisks mark password-encrypted files
|
||||
data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files
|
||||
fields = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(fields[0])
|
||||
attr = fields[5]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
elif rar_executable_version == 5:
|
||||
while not line.startswith('-----------'):
|
||||
fields = line.strip().lstrip("*").split()
|
||||
data = {}
|
||||
data['index'] = i
|
||||
#!!!ATB - changed this because it was choking when a folder or file started with a space.
|
||||
#!!! now, just strip off the first char in the string
|
||||
data['filename'] = accum[0].rstrip()[1:]
|
||||
info = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(info[0])
|
||||
attr = info[5]
|
||||
data['filename'] = " ".join(fields[4:])
|
||||
data['size'] = int(fields[1])
|
||||
attr = fields[0]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
|
||||
data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
@ -153,7 +194,7 @@ class RarFileImplementation(object):
|
||||
if overwrite:
|
||||
options.append('o+')
|
||||
else:
|
||||
options.append('o-')
|
||||
options.append('o-')
|
||||
if not path.endswith(os.sep):
|
||||
path += os.sep
|
||||
names = []
|
||||
@ -167,7 +208,7 @@ class RarFileImplementation(object):
|
||||
names.append(path)
|
||||
proc = self.call(command, options, names)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if stderrdata.find("CRC failed")>=0:
|
||||
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
|
||||
raise IncorrectRARPassword
|
||||
return res
|
||||
|
0
comictaggerlib/__init__.py
Normal file
@ -3,7 +3,7 @@ A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -26,7 +26,7 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
@ -38,7 +38,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, match_set_list, style, fetch_func):
|
||||
super(AutoTagMatchWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
@ -51,6 +51,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
utils.reduceWidgetFontSize( self.teDescription, 1 )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
@ -132,7 +133,9 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match['issue_title']
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
@ -159,6 +162,10 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
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
|
@ -3,7 +3,7 @@ A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -22,6 +22,7 @@ import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
@ -30,9 +31,18 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
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
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
@ -42,23 +52,16 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
self.setCoverImage( img_data, self.archiveCoverWidget)
|
||||
|
||||
def setTestImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblTest )
|
||||
self.setCoverImage( img_data, self.testCoverWidget)
|
||||
|
||||
def setCoverImage( self, img_data , label):
|
||||
if img_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( img_data )
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
label.setScaledContents(True)
|
||||
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
|
@ -3,7 +3,7 @@ A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -32,7 +32,7 @@ class AutoTagStartWindow(QtGui.QDialog):
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
@ -48,6 +48,17 @@ class AutoTagStartWindow(QtGui.QDialog):
|
||||
self.cbxSpecifySearchString.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.leNameLengthMatchTolerance.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.leSearchString.setEnabled( False )
|
||||
|
||||
if self.settings.save_on_low_confidence:
|
||||
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)
|
||||
|
||||
nlmtTip = (
|
||||
""" <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
|
||||
@ -97,6 +108,13 @@ class AutoTagStartWindow(QtGui.QDialog):
|
||||
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
|
||||
self.nameLengthMatchTolerance = int(self.leNameLengthMatchTolerance.text())
|
||||
|
||||
#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
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
@ -3,7 +3,7 @@ Class to manage modifying metadata specifically for CBL/CBI
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -74,6 +74,9 @@ class CBLTransformer:
|
||||
|
||||
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:
|
539
comictaggerlib/cli.py
Normal file
@ -0,0 +1,539 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""
|
||||
Comic tagger CLI functions
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
from pprint import pprint
|
||||
import json
|
||||
import platform
|
||||
import locale
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options
|
||||
from comicarchive import ComicArchive, MetaDataStyle
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
|
||||
import utils
|
||||
import codecs
|
||||
|
||||
class MultipleMatch():
|
||||
def __init__( self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
class OnlineMatchResults():
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
|
||||
return cv_md
|
||||
|
||||
def actual_metadata_save( ca, opts, md ):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print >> sys.stderr,"The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print >> sys.stderr,"Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written"
|
||||
else:
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
return True
|
||||
|
||||
def display_match_set_for_choice( label, match_set, opts, settings ):
|
||||
print u"{0} -- {1}:".format(match_set.filename, label )
|
||||
|
||||
# sort match list by year
|
||||
match_set.matches.sort(key=lambda k: k['year'])
|
||||
|
||||
for (counter,m) in enumerate(match_set.matches):
|
||||
counter += 1
|
||||
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
|
||||
m['series'],
|
||||
m['issue_number'],
|
||||
m['publisher'],
|
||||
m['month'],
|
||||
m['year'],
|
||||
m['issue_title'])
|
||||
if opts.interactive:
|
||||
while True:
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1,len(match_set.matches)+1)) or i == 's':
|
||||
break
|
||||
if i != 's':
|
||||
i = int(i) - 1
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = ComicArchive( match_set.filename, settings.rar_exe_path )
|
||||
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
|
||||
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings)
|
||||
md.overlay( cv_md )
|
||||
actual_metadata_save( ca, opts, md )
|
||||
|
||||
|
||||
def post_process_matches( match_results, opts, settings ):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len( match_results.goodMatches ) > 0:
|
||||
print "\nSuccessful matches:"
|
||||
print "------------------"
|
||||
for f in match_results.goodMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.noMatches ) > 0:
|
||||
print "\nNo matches:"
|
||||
print "------------------"
|
||||
for f in match_results.noMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.writeFailures ) > 0:
|
||||
print "\nFile Write Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.writeFailures:
|
||||
print f
|
||||
|
||||
if len( match_results.fetchDataFailures ) > 0:
|
||||
print "\nNetwork Data Fetch Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.fetchDataFailures:
|
||||
print f
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
#just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nArchives with multiple high-confidence matches:"
|
||||
print "------------------"
|
||||
for match_set in match_results.multipleMatches:
|
||||
display_match_set_for_choice( "Multiple high-confidence matches", match_set, opts, settings )
|
||||
|
||||
if len( match_results.lowConfidenceMatches ) > 0:
|
||||
print "\nArchives with low-confidence matches:"
|
||||
print "------------------"
|
||||
for match_set in match_results.lowConfidenceMatches:
|
||||
if len( match_set.matches) == 1:
|
||||
label = "Single low-confidence match"
|
||||
else:
|
||||
label = "Multiple low-confidence matches"
|
||||
|
||||
display_match_set_for_choice( label, match_set, opts, settings )
|
||||
|
||||
|
||||
def cli_mode( opts, settings ):
|
||||
if len( opts.file_list ) < 1:
|
||||
print >> sys.stderr,"You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
if type(f) == str:
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli( f, opts, settings, match_results )
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches( match_results, opts, settings )
|
||||
|
||||
|
||||
def create_local_metadata( opts, ca, has_desired_tags ):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.setDefaultPageList( ca.getNumberOfPages() )
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata( opts.data_style )
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
|
||||
return md
|
||||
|
||||
def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
batch_mode = len( opts.file_list ) > 1
|
||||
|
||||
ca = ComicArchive(filename, settings.rar_exe_path)
|
||||
|
||||
if not os.path.lexists( filename ):
|
||||
print >> sys.stderr,"Cannot find "+ filename
|
||||
return
|
||||
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print >> sys.stderr,"Sorry, but "+ filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
|
||||
print >> sys.stderr,"This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [ False, False, False ]
|
||||
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
|
||||
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
|
||||
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
|
||||
if batch_mode:
|
||||
brief = u"{0}: ".format(filename)
|
||||
|
||||
if ca.isZip(): brief += "ZIP archive "
|
||||
elif ca.isRar(): brief += "RAR archive "
|
||||
elif ca.isFolder(): brief += "Folder archive "
|
||||
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
|
||||
brief += "none "
|
||||
else:
|
||||
if has[ MetaDataStyle.CBI ]: brief += "CBL "
|
||||
if has[ MetaDataStyle.CIX ]: brief += "CR "
|
||||
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print brief
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if has[ MetaDataStyle.CIX ]:
|
||||
print "------ComicRack tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
|
||||
else:
|
||||
print u"{0}".format(ca.readCIX())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if has[ MetaDataStyle.CBI ]:
|
||||
print "------ComicBookLover tags--------"
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print u"{0}".format(ca.readCBI())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[ MetaDataStyle.COMET ]:
|
||||
print "------CoMet tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCoMet())
|
||||
else:
|
||||
print u"{0}".format(ca.readCoMet())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if has[ opts.data_style ]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata( opts.data_style ):
|
||||
print u"{0}: Tag removal seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Removed {1} tags.".format( filename, style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not removed".format( filename, style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print u"{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[ opts.copy_source ]
|
||||
if has[ opts.copy_source ]:
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print u"{0}: Tag copy seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
|
||||
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print u"Processing {0}...".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
if md.issue is None or md.issue == "":
|
||||
if opts.assume_issue_is_one_if_not_set:
|
||||
md.issue = "1"
|
||||
|
||||
# now, search online
|
||||
if opts.search_online:
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print >> sys.stderr,"Network error while getting issue details. Save aborted"
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
if cv_md is None:
|
||||
print >> sys.stderr,"No match for ID {0} was found.".format(opts.issue_id)
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
else:
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print >> sys.stderr,"No metadata given to search online with!"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
if low_confidence:
|
||||
print >> sys.stderr,"Online search: Multiple low confidence matches. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
else:
|
||||
print >> sys.stderr,"Online search: Multiple good matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print >> sys.stderr,"Online search: Low confidence match. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if not found_match:
|
||||
print >> sys.stderr,"Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings)
|
||||
if cv_md is None:
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
md.overlay( cv_md )
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save( ca, opts, md ):
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[ opts.data_style ]
|
||||
else:
|
||||
use_tags = False
|
||||
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print >> sys.stderr, msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
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( settings.rename_template )
|
||||
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print >> sys.stderr, msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.rename( filename, new_abs_path )
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
|
||||
|
||||
elif opts.export_to_zip:
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if not ca.isRar():
|
||||
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
|
||||
return
|
||||
|
||||
rar_file = os.path.abspath( os.path.abspath( filename ) )
|
||||
new_file = os.path.splitext(rar_file)[0] + ".cbz"
|
||||
|
||||
if opts.abort_export_on_conflict and os.path.lexists( new_file ):
|
||||
print msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1])
|
||||
return
|
||||
|
||||
new_file = utils.unique_file( os.path.join( new_file ) )
|
||||
|
||||
delete_success = False
|
||||
export_success = False
|
||||
if not opts.dryrun:
|
||||
if ca.exportAsZip( new_file ):
|
||||
export_success = True
|
||||
if opts.delete_rar_after_export:
|
||||
try:
|
||||
os.unlink( rar_file )
|
||||
except:
|
||||
print >> sys.stderr, msg_hdr + "Error deleting original RAR after export"
|
||||
delete_success = False
|
||||
else:
|
||||
delete_success = True
|
||||
else:
|
||||
# last export failed, so remove the zip, if it exists
|
||||
if os.path.lexists( new_file ):
|
||||
os.remove( new_file )
|
||||
else:
|
||||
msg = msg_hdr + u"Dry-run: Would try to create {0}".format(os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export:
|
||||
msg += u" and delete orginal."
|
||||
print msg
|
||||
return
|
||||
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += u"Archive exported successfully to: {0}".format( os.path.split(new_file)[1] )
|
||||
if opts.delete_rar_after_export and delete_success:
|
||||
msg += u" (Original deleted) "
|
||||
else:
|
||||
msg += u"Archive failed to export!"
|
||||
|
||||
print msg
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ A python class to encapsulate CoMet data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
@ -3,7 +3,7 @@ A python class to represent a single comic, be it file or folder of images
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -25,6 +25,8 @@ import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
import platform
|
||||
import locale
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import _subprocess
|
||||
import time
|
||||
@ -40,13 +42,18 @@ sys.path.insert(0, os.path.abspath(".") )
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
|
||||
from options import Options, MetaDataStyle
|
||||
from settings import ComicTaggerSettings
|
||||
from comicinfoxml import ComicInfoXml
|
||||
from comicbookinfo import ComicBookInfo
|
||||
from comet import CoMet
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from filenameparser import FileNameParser
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
COMET = 2
|
||||
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
|
||||
|
||||
class ZipArchiver:
|
||||
|
||||
@ -65,15 +72,16 @@ class ZipArchiver:
|
||||
def readArchiveFile( self, archive_file ):
|
||||
data = ""
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
|
||||
try:
|
||||
data = zf.read( archive_file )
|
||||
except zipfile.BadZipfile as e:
|
||||
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
zf.close()
|
||||
raise IOError
|
||||
except Exception as e:
|
||||
zf.close()
|
||||
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
raise IOError
|
||||
finally:
|
||||
zf.close()
|
||||
@ -102,12 +110,16 @@ class ZipArchiver:
|
||||
except:
|
||||
return False
|
||||
|
||||
def getArchiveFilenameList( self ):
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
namelist = zf.namelist()
|
||||
zf.close()
|
||||
return namelist
|
||||
|
||||
def getArchiveFilenameList( self ):
|
||||
try:
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
namelist = zf.namelist()
|
||||
zf.close()
|
||||
return namelist
|
||||
except Exception as e:
|
||||
print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path)
|
||||
return []
|
||||
|
||||
# zip helper func
|
||||
def rebuildZipFile( self, exclude_list ):
|
||||
|
||||
@ -215,7 +227,7 @@ class ZipArchiver:
|
||||
if not self.writeZipComment( self.path, comment ):
|
||||
return False
|
||||
except Exception as e:
|
||||
print >> sys.stderr, "Error while copying to {0}: {1}".format(self.path, e)
|
||||
print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@ -227,9 +239,9 @@ class ZipArchiver:
|
||||
class RarArchiver:
|
||||
|
||||
devnull = None
|
||||
def __init__( self, path ):
|
||||
def __init__( self, path, rar_exe_path ):
|
||||
self.path = path
|
||||
self.rar_exe_path = None
|
||||
self.rar_exe_path = rar_exe_path
|
||||
|
||||
if RarArchiver.devnull is None:
|
||||
RarArchiver.devnull = open(os.devnull, "w")
|
||||
@ -294,22 +306,22 @@ class RarArchiver:
|
||||
entries = rarc.read_files( archive_file )
|
||||
|
||||
if entries[0][0].size != len(entries[0][1]):
|
||||
print >> sys.stderr, "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
|
||||
print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
|
||||
entries[0][0].size,len(entries[0][1]), self.path, archive_file, tries)
|
||||
continue
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print >> sys.stderr, "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print >> sys.stderr, "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
break
|
||||
|
||||
else:
|
||||
#Success"
|
||||
#entries is a list of of tuples: ( rarinfo, filedata)
|
||||
if tries > 1:
|
||||
print >> sys.stderr, "Attempted read_files() {0} times".format(tries)
|
||||
print >> sys.stderr, u"Attempted read_files() {0} times".format(tries)
|
||||
if (len(entries) == 1):
|
||||
return entries[0][1]
|
||||
else:
|
||||
@ -385,7 +397,7 @@ class RarArchiver:
|
||||
namelist.append( item.filename )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print >> sys.stderr, "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -403,7 +415,7 @@ class RarArchiver:
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print >> sys.stderr, "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -503,20 +515,22 @@ class ComicArchive:
|
||||
class ArchiveType:
|
||||
Zip, Rar, Folder, Unknown = range(4)
|
||||
|
||||
def __init__( self, path ):
|
||||
def __init__( self, path, rar_exe_path=None ):
|
||||
self.path = path
|
||||
|
||||
self.rar_exe_path = rar_exe_path
|
||||
self.ci_xml_filename = 'ComicInfo.xml'
|
||||
self.comet_default_filename = 'CoMet.xml'
|
||||
self.resetCache()
|
||||
|
||||
if self.zipTest():
|
||||
if self.rarTest():
|
||||
self.archive_type = self.ArchiveType.Rar
|
||||
self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path )
|
||||
|
||||
elif self.zipTest():
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
self.archiver = ZipArchiver( self.path )
|
||||
|
||||
elif self.rarTest():
|
||||
self.archive_type = self.ArchiveType.Rar
|
||||
self.archiver = RarArchiver( self.path )
|
||||
|
||||
elif os.path.isdir( self.path ):
|
||||
self.archive_type = self.ArchiveType.Folder
|
||||
self.archiver = FolderArchiver( self.path )
|
||||
@ -525,9 +539,9 @@ class ComicArchive:
|
||||
self.archiver = UnknownArchiver( self.path )
|
||||
|
||||
if ComicArchive.logo_data is None:
|
||||
fname = os.path.join(ComicTaggerSettings.baseDir(), 'graphics','nocover.png' )
|
||||
fname = ComicTaggerSettings.getGraphic('nocover.png')
|
||||
with open(fname, 'rb') as fd:
|
||||
ComicArchive.logo_data = fd.read()
|
||||
ComicArchive.logo_data = fd.read()
|
||||
|
||||
# Clears the cached data
|
||||
def resetCache( self ):
|
||||
@ -548,10 +562,6 @@ class ComicArchive:
|
||||
def rename( self, path ):
|
||||
self.path = path
|
||||
self.archiver.path = path
|
||||
|
||||
def setExternalRarProgram( self, rar_exe_path ):
|
||||
if self.isRar():
|
||||
self.archiver.rar_exe_path = rar_exe_path
|
||||
|
||||
def zipTest( self ):
|
||||
return zipfile.is_zipfile( self.path )
|
||||
@ -578,7 +588,7 @@ class ComicArchive:
|
||||
if self.archive_type == self.ArchiveType.Unknown :
|
||||
return False
|
||||
|
||||
elif check_rar_status and self.isRar() and self.archiver.rar_exe_path is None:
|
||||
elif check_rar_status and self.isRar() and self.rar_exe_path is None:
|
||||
return False
|
||||
|
||||
elif not os.access(self.path, os.W_OK):
|
||||
@ -603,9 +613,9 @@ class ComicArchive:
|
||||
ext = os.path.splitext(self.path)[1].lower()
|
||||
|
||||
if (
|
||||
( self.isZip() or self.isRar() or self.isFolder() )
|
||||
( self.isZip() or self.isRar() ) #or self.isFolder() )
|
||||
and
|
||||
( self.getNumberOfPages() > 2)
|
||||
( self.getNumberOfPages() > 0)
|
||||
|
||||
):
|
||||
return True
|
||||
@ -666,13 +676,16 @@ class ComicArchive:
|
||||
try:
|
||||
image_data = self.archiver.readArchiveFile( filename )
|
||||
except IOError:
|
||||
print >> sys.stderr, "Error reading in page. Substituting logo page."
|
||||
print >> sys.stderr, u"Error reading in page. Substituting logo page."
|
||||
image_data = ComicArchive.logo_data
|
||||
|
||||
return image_data
|
||||
|
||||
def getPageName( self, index ):
|
||||
|
||||
if index is None:
|
||||
return None
|
||||
|
||||
page_list = self.getPageNameList()
|
||||
|
||||
num_pages = len( page_list )
|
||||
@ -681,6 +694,56 @@ class ComicArchive:
|
||||
|
||||
return page_list[index]
|
||||
|
||||
def getScannerPageIndex( self ):
|
||||
|
||||
scanner_page_index = None
|
||||
|
||||
#make a guess at the scanner page
|
||||
name_list = self.getPageNameList()
|
||||
count = self.getNumberOfPages()
|
||||
|
||||
#too few pages to really know
|
||||
if count < 5:
|
||||
return None
|
||||
|
||||
# count the length of every filename, and count occurences
|
||||
length_buckets = dict()
|
||||
for name in name_list:
|
||||
fname = os.path.split(name)[1]
|
||||
length = len(fname)
|
||||
if length_buckets.has_key( length ):
|
||||
length_buckets[ length ] += 1
|
||||
else:
|
||||
length_buckets[ length ] = 1
|
||||
|
||||
# sort by most common
|
||||
sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True)
|
||||
|
||||
# statistical mode occurence is first
|
||||
mode_length = sorted_buckets[0][0]
|
||||
|
||||
# we are only going to consider the final image file:
|
||||
final_name = os.path.split(name_list[count-1])[1]
|
||||
|
||||
common_length_list = list()
|
||||
for name in name_list:
|
||||
if len(os.path.split(name)[1]) == mode_length:
|
||||
common_length_list.append( os.path.split(name)[1] )
|
||||
|
||||
prefix = os.path.commonprefix(common_length_list)
|
||||
|
||||
if mode_length <= 7 and prefix == "":
|
||||
#probably all numbers
|
||||
if len(final_name) > mode_length:
|
||||
scanner_page_index = count-1
|
||||
|
||||
# see if the last page doesn't start with the same prefix as most others
|
||||
elif not final_name.startswith(prefix):
|
||||
scanner_page_index = count-1
|
||||
|
||||
return scanner_page_index
|
||||
|
||||
|
||||
def getPageNameList( self , sort_list=True):
|
||||
|
||||
if self.page_list is None:
|
||||
@ -689,7 +752,14 @@ class ComicArchive:
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files.sort(key=lambda x: x.lower())
|
||||
def keyfunc(k):
|
||||
#hack to account for some weird scanner ID pages
|
||||
basename=os.path.split(k)[1]
|
||||
if basename < '0':
|
||||
k = os.path.join(os.path.split(k)[0], "z" + basename)
|
||||
return k.lower()
|
||||
|
||||
files.sort(key=keyfunc)
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
@ -857,7 +927,7 @@ class ComicArchive:
|
||||
try:
|
||||
raw_comet = self.archiver.readArchiveFile( self.comet_filename )
|
||||
except IOError:
|
||||
print >> sys.stderr, "Error reading in raw CoMet!"
|
||||
print >> sys.stderr, u"Error reading in raw CoMet!"
|
||||
raw_comet = ""
|
||||
return raw_comet
|
||||
|
||||
@ -908,7 +978,7 @@ class ComicArchive:
|
||||
data = self.archiver.readArchiveFile( n )
|
||||
except:
|
||||
data = ""
|
||||
print >> sys.stderr, "Error reading in Comet XML for validation!"
|
||||
print >> sys.stderr, u"Error reading in Comet XML for validation!"
|
||||
if CoMet().validateString( data ):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
@ -946,7 +1016,7 @@ class ComicArchive:
|
||||
|
||||
|
||||
|
||||
def metadataFromFilename( self ):
|
||||
def metadataFromFilename( self , parse_scan_info=True):
|
||||
|
||||
metadata = GenericMetadata()
|
||||
|
||||
@ -963,6 +1033,9 @@ class ComicArchive:
|
||||
metadata.year = fnp.year
|
||||
if fnp.issue_count != "":
|
||||
metadata.issueCount = fnp.issue_count
|
||||
if parse_scan_info:
|
||||
if fnp.remainder != "":
|
||||
metadata.scanInfo = fnp.remainder
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
@ -3,7 +3,7 @@ A python class to encapsulate the ComicBookInfo data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
@ -3,7 +3,7 @@ A python class to encapsulate ComicRack's ComicInfo.xml data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -89,36 +89,21 @@ class ComicInfoXml:
|
||||
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( 'Title', md.title )
|
||||
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( 'Publisher', md.publisher )
|
||||
assign( 'Imprint', md.imprint )
|
||||
assign( 'Genre', md.genre )
|
||||
assign( 'Web', md.webLink )
|
||||
assign( 'PageCount', md.pageCount )
|
||||
assign( 'Format', md.format )
|
||||
assign( 'LanguageISO', md.language )
|
||||
assign( 'Manga', md.manga )
|
||||
assign( 'Characters', md.characters )
|
||||
assign( 'Teams', md.teams )
|
||||
assign( 'Locations', md.locations )
|
||||
assign( 'ScanInformation', md.scanInfo )
|
||||
assign( 'StoryArc', md.storyArc )
|
||||
assign( 'SeriesGroup', md.seriesGroup )
|
||||
assign( 'AgeRating', md.maturityRating )
|
||||
|
||||
if md.blackAndWhite is not None and md.blackAndWhite:
|
||||
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
|
||||
assign( 'Day', md.day )
|
||||
|
||||
# need to specially process the credits, since they are structured differently than CIX
|
||||
credit_writer_list = list()
|
||||
@ -181,7 +166,23 @@ class ComicInfoXml:
|
||||
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')
|
||||
@ -229,6 +230,7 @@ class ComicInfoXml:
|
||||
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' )
|
||||
@ -258,12 +260,14 @@ class ComicInfoXml:
|
||||
n.tag == 'Letterer' or
|
||||
n.tag == 'Editor'
|
||||
):
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), n.tag )
|
||||
if n.text is not None:
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), n.tag )
|
||||
|
||||
if n.tag == 'CoverArtist':
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), "Cover" )
|
||||
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" )
|
@ -3,7 +3,7 @@ A python class to manage caching of data from Comic Vine
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -110,13 +110,11 @@ class ComicVineCacher:
|
||||
"volume_id INT," +
|
||||
"name TEXT," +
|
||||
"issue_number TEXT," +
|
||||
"image_url TEXT," +
|
||||
"image_hash TEXT," +
|
||||
"thumb_image_url TEXT," +
|
||||
"thumb_image_hash TEXT," +
|
||||
"publish_month TEXT," +
|
||||
"publish_year TEXT," +
|
||||
"super_url TEXT," +
|
||||
"thumb_url TEXT," +
|
||||
"cover_date TEXT," +
|
||||
"site_detail_url TEXT," +
|
||||
"description TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id ) )"
|
||||
)
|
||||
@ -263,18 +261,34 @@ class ComicVineCacher:
|
||||
}
|
||||
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
|
||||
# now add in issues
|
||||
|
||||
for issue in cv_volume_record['issues']:
|
||||
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": cv_volume_record['id'],
|
||||
"name": issue['name'],
|
||||
"issue_number": issue['issue_number'],
|
||||
"timestamp": timestamp
|
||||
"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 ):
|
||||
|
||||
@ -288,10 +302,6 @@ class ComicVineCacher:
|
||||
# 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) ] )
|
||||
|
||||
# purge stale issue info - probably issue data won't change much....
|
||||
a_month_ago = datetime.datetime.today()-datetime.timedelta(days=30)
|
||||
cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
@ -311,25 +321,51 @@ class ComicVineCacher:
|
||||
result['count_of_issues'] = row[3]
|
||||
result['start_year'] = row[4]
|
||||
result['issues'] = list()
|
||||
|
||||
return result
|
||||
|
||||
cur.execute("SELECT id,name,issue_number,image_url,image_hash FROM Issues WHERE volume_id = ?", [ volume_id ] )
|
||||
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['image_url'] = row[3]
|
||||
record['image_hash'] = row[4]
|
||||
|
||||
result['issues'].append(record)
|
||||
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)
|
||||
|
||||
return result
|
||||
if len(results) == 0:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, publish_month, publish_year, site_detail_url ):
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
@ -339,10 +375,9 @@ class ComicVineCacher:
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
"image_url": image_url,
|
||||
"thumb_image_url": thumb_image_url,
|
||||
"publish_month": publish_month,
|
||||
"publish_year": publish_year,
|
||||
"super_url": image_url,
|
||||
"thumb_url": thumb_image_url,
|
||||
"cover_date": cover_date,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
@ -357,23 +392,21 @@ class ComicVineCacher:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year,site_detail_url FROM Issues WHERE id=?", [ issue_id ])
|
||||
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[0] is None :
|
||||
if row is None or row[0] is None :
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['publish_month'] = None
|
||||
details['publish_year'] = None
|
||||
details['cover_date'] = None
|
||||
details['site_detail_url'] = None
|
||||
|
||||
else:
|
||||
details['image_url'] = row[0]
|
||||
details['thumb_image_url'] = row[1]
|
||||
details['publish_month'] = row[2]
|
||||
details['publish_year'] = row[3]
|
||||
details['site_detail_url'] = row[4]
|
||||
details['cover_date'] = row[2]
|
||||
details['site_detail_url'] = row[3]
|
||||
|
||||
return details
|
||||
|
@ -3,7 +3,7 @@ A python class to manage communication with Comic Vine's REST API
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -24,6 +24,7 @@ from pprint import pprint
|
||||
import urllib2, urllib
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import datetime
|
||||
import ctversion
|
||||
import sys
|
||||
@ -49,15 +50,22 @@ from comicvinecacher import ComicVineCacher
|
||||
from genericmetadata import GenericMetadata
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class CVTypeID:
|
||||
Volume = "4050"
|
||||
Issue = "4000"
|
||||
|
||||
class ComicVineTalkerException(Exception):
|
||||
pass
|
||||
|
||||
class ComicVineTalker(QObject):
|
||||
|
||||
logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png"
|
||||
|
||||
def __init__(self, api_key=""):
|
||||
QObject.__init__(self)
|
||||
|
||||
self.api_base_url = "http://www.comicvine.com/api"
|
||||
|
||||
# key that is registered to comictagger
|
||||
self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
|
||||
|
||||
@ -74,9 +82,22 @@ class ComicVineTalker(QObject):
|
||||
else:
|
||||
self.log_func( text )
|
||||
|
||||
def parseDateStr( self, date_str):
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
if date_str is not None:
|
||||
parts = date_str.split('-')
|
||||
year = parts[0]
|
||||
if len(parts) > 1:
|
||||
month = parts[1]
|
||||
if len(parts) > 2:
|
||||
day = parts[2]
|
||||
return day, month, year
|
||||
|
||||
def testKey( self ):
|
||||
|
||||
test_url = "http://api.comicvine.com/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
|
||||
test_url = self.api_base_url + "/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
|
||||
resp = urllib2.urlopen( test_url )
|
||||
content = resp.read()
|
||||
|
||||
@ -86,12 +107,28 @@ class ComicVineTalker(QObject):
|
||||
return cv_response[ 'status_code' ] != 100
|
||||
|
||||
def getUrlContent( self, url ):
|
||||
try:
|
||||
resp = urllib2.urlopen( url )
|
||||
return resp.read()
|
||||
except Exception as e:
|
||||
self.writeLog( str(e) )
|
||||
raise ComicVineTalkerException("Network Error!")
|
||||
# connect to server:
|
||||
# if there is a 500 error, try a few more times before giving up
|
||||
# any other error, just bail
|
||||
#print "ATB---", url
|
||||
for tries in range(3):
|
||||
try:
|
||||
resp = urllib2.urlopen( url )
|
||||
return resp.read()
|
||||
except urllib2.HTTPError as e:
|
||||
if e.getcode() == 500:
|
||||
self.writeLog( "Try #{0}: ".format(tries+1) )
|
||||
time.sleep(1)
|
||||
self.writeLog( str(e) + "\n" )
|
||||
|
||||
if e.getcode() != 500:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.writeLog( str(e) + "\n" )
|
||||
raise ComicVineTalkerException("Network Error!")
|
||||
|
||||
raise ComicVineTalkerException("Error on Comic Vine server")
|
||||
|
||||
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
|
||||
|
||||
@ -108,12 +145,23 @@ class ComicVineTalker(QObject):
|
||||
return cached_search_results
|
||||
|
||||
original_series_name = series_name
|
||||
|
||||
series_name = urllib.quote_plus(series_name.encode("utf-8"))
|
||||
#series_name = urllib.quote_plus(unicode(series_name))
|
||||
search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
|
||||
|
||||
# We need to make the series name into an "AND"ed query list
|
||||
query_word_list = series_name.split()
|
||||
and_list = ['AND'] * (len(query_word_list)-1)
|
||||
and_list.append('')
|
||||
# zipper up the two lists
|
||||
query_list = zip(query_word_list, and_list)
|
||||
# flatten the list
|
||||
query_list = [ item for sublist in query_list for item in sublist]
|
||||
# convert back to a string
|
||||
query_string = " ".join( query_list ).strip()
|
||||
#print "Query string = ", query_string
|
||||
|
||||
query_string = urllib.quote_plus(query_string.encode("utf-8"))
|
||||
|
||||
content = self.getUrlContent(search_url)
|
||||
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + query_string + "&field_list=name,id,start_year,publisher,image,description,count_of_issues"
|
||||
content = self.getUrlContent(search_url + "&page=1")
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
@ -132,7 +180,7 @@ class ComicVineTalker(QObject):
|
||||
if callback is None:
|
||||
self.writeLog( "Found {0} of {1} results\n".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
search_results.extend( cv_response['results'])
|
||||
offset = 0
|
||||
page = 1
|
||||
|
||||
if callback is not None:
|
||||
callback( current_result_count, total_result_count )
|
||||
@ -141,8 +189,9 @@ class ComicVineTalker(QObject):
|
||||
while ( current_result_count < total_result_count ):
|
||||
if callback is None:
|
||||
self.writeLog("getting another page of results {0} of {1}...\n".format( current_result_count, total_result_count))
|
||||
offset += limit
|
||||
content = self.getUrlContent(search_url + "&offset="+str(offset))
|
||||
page += 1
|
||||
|
||||
content = self.getUrlContent(search_url + "&page="+str(page))
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
@ -157,10 +206,10 @@ class ComicVineTalker(QObject):
|
||||
|
||||
|
||||
#for record in search_results:
|
||||
# print( "{0}: {1} ({2})".format(record['id'], smart_str(record['name']) , record['start_year'] ) )
|
||||
# print( "{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
|
||||
|
||||
#print "{0}: {1} ({2})".format(search_results['results'][0]['id'], smart_str(search_results['results'][0]['name']) , search_results['results'][0]['start_year'] )
|
||||
# #print( u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
|
||||
# #print record
|
||||
# #record['count_of_issues'] = record['count_of_isssues']
|
||||
#print u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year'] )
|
||||
|
||||
# cache these search results
|
||||
cvc.add_search_results( original_series_name, search_results )
|
||||
@ -178,7 +227,7 @@ class ComicVineTalker(QObject):
|
||||
return cached_volume_result
|
||||
|
||||
|
||||
volume_url = "http://api.comicvine.com/volume/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + str(series_id) + "/?api_key=" + self.api_key + "&field_list=name,id,start_year,publisher,count_of_issues&format=json"
|
||||
|
||||
content = self.getUrlContent(volume_url)
|
||||
cv_response = json.loads(content)
|
||||
@ -192,22 +241,130 @@ class ComicVineTalker(QObject):
|
||||
cvc.add_volume_info( volume_results )
|
||||
|
||||
return volume_results
|
||||
|
||||
|
||||
def fetchIssuesByVolume( self, series_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
cached_volume_issues_result = cvc.get_volume_issues_info( series_id )
|
||||
|
||||
if cached_volume_issues_result is not None:
|
||||
return cached_volume_issues_result
|
||||
|
||||
#---------------------------------
|
||||
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + str(series_id) + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
|
||||
content = self.getUrlContent(issues_url)
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
#------------------------------------
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
#print "ATB total_result_count", total_result_count
|
||||
|
||||
#print "ATB Found {0} of {1} results".format( cv_response['number_of_page_results'], cv_response['number_of_total_results'])
|
||||
volume_issues_result = cv_response['results']
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
#print "ATB getting another page of issue results {0} of {1}...".format( current_result_count, total_result_count)
|
||||
page += 1
|
||||
offset += cv_response['number_of_page_results']
|
||||
|
||||
#print issues_url+ "&offset="+str(offset)
|
||||
content = self.getUrlContent(issues_url + "&offset="+str(offset))
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
volume_issues_result.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
self.repairUrls( volume_issues_result )
|
||||
|
||||
cvc.add_volume_issues_info( series_id, volume_issues_result )
|
||||
|
||||
return volume_issues_result
|
||||
|
||||
|
||||
def fetchIssuesByVolumeIssueNumAndYear( self, volume_id_list, issue_number, year ):
|
||||
volume_filter = "volume:"
|
||||
for vid in volume_id_list:
|
||||
volume_filter += str(vid) + "|"
|
||||
|
||||
year_filter = ""
|
||||
if year is not None and str(year).isdigit():
|
||||
year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(year, int(year)+1)
|
||||
|
||||
issue_number = urllib.quote_plus(unicode(issue_number).encode("utf-8"))
|
||||
|
||||
filter = "&filter=" + volume_filter + year_filter + ",issue_number:" + issue_number
|
||||
|
||||
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
|
||||
|
||||
content = self.getUrlContent(issues_url)
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
#------------------------------------
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
#print "ATB total_result_count", total_result_count
|
||||
|
||||
#print "ATB Found {0} of {1} results\n".format( cv_response['number_of_page_results'], cv_response['number_of_total_results'])
|
||||
filtered_issues_result = cv_response['results']
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
#print "ATB getting another page of issue results {0} of {1}...\n".format( current_result_count, total_result_count)
|
||||
page += 1
|
||||
offset += cv_response['number_of_page_results']
|
||||
|
||||
#print issues_url+ "&offset="+str(offset)
|
||||
content = self.getUrlContent(issues_url + "&offset="+str(offset))
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
filtered_issues_result.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
self.repairUrls( filtered_issues_result )
|
||||
|
||||
return filtered_issues_result
|
||||
|
||||
|
||||
|
||||
def fetchIssueData( self, series_id, issue_number, settings ):
|
||||
|
||||
volume_results = self.fetchVolumeData( series_id )
|
||||
issues_list_results = self.fetchIssuesByVolume( series_id )
|
||||
|
||||
found = False
|
||||
for record in volume_results['issues']:
|
||||
if IssueString(issue_number).asFloat() is None:
|
||||
for record in issues_list_results:
|
||||
if IssueString(issue_number).asString() is None:
|
||||
issue_number = 1
|
||||
if float(record['issue_number']) == IssueString(issue_number).asFloat():
|
||||
if IssueString(record['issue_number']).asString().lower() == IssueString(issue_number).asString().lower():
|
||||
found = True
|
||||
break
|
||||
|
||||
if (found):
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
|
||||
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
@ -224,7 +381,7 @@ class ComicVineTalker(QObject):
|
||||
|
||||
def fetchIssueDataByIssueID( self, issue_id, settings ):
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
@ -248,14 +405,14 @@ class ComicVineTalker(QObject):
|
||||
metadata.series = issue_results['volume']['name']
|
||||
|
||||
num_s = IssueString(issue_results['issue_number']).asString()
|
||||
|
||||
metadata.issue = num_s
|
||||
metadata.title = issue_results['name']
|
||||
|
||||
metadata.publisher = volume_results['publisher']['name']
|
||||
metadata.month = issue_results['publish_month']
|
||||
metadata.year = issue_results['publish_year']
|
||||
metadata.day, metadata.month, metadata.year = self.parseDateStr( issue_results['cover_date'] )
|
||||
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.comments = self.cleanup_html(issue_results['description'])
|
||||
metadata.comments = self.cleanup_html(issue_results['description'], settings.remove_html_tables)
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
@ -268,11 +425,12 @@ class ComicVineTalker(QObject):
|
||||
metadata.webLink = issue_results['site_detail_url']
|
||||
|
||||
person_credits = issue_results['person_credits']
|
||||
for person in person_credits:
|
||||
for role in person['roles']:
|
||||
# can we determine 'primary' from CV??
|
||||
role_name = role['role'].title()
|
||||
metadata.addCredit( person['name'], role['role'].title(), False )
|
||||
for person in person_credits:
|
||||
if person.has_key('role'):
|
||||
roles = person['role'].split(',')
|
||||
for role in roles:
|
||||
# can we determine 'primary' from CV??
|
||||
metadata.addCredit( person['name'], role.title().strip(), False )
|
||||
|
||||
character_credits = issue_results['character_credits']
|
||||
character_list = list()
|
||||
@ -300,9 +458,25 @@ class ComicVineTalker(QObject):
|
||||
metadata.storyArc = utils.listToString(arc_list)
|
||||
|
||||
return metadata
|
||||
|
||||
def cleanup_html( self, string):
|
||||
def cleanup_html( self, string, remove_html_tables):
|
||||
"""
|
||||
converter = html2text.HTML2Text()
|
||||
#converter.emphasis_mark = '*'
|
||||
#converter.ignore_links = True
|
||||
converter.body_width = 0
|
||||
|
||||
print html2text.html2text(string)
|
||||
return string
|
||||
#return converter.handle(string)
|
||||
"""
|
||||
|
||||
|
||||
if string is None:
|
||||
return ""
|
||||
# find any tables
|
||||
soup = BeautifulSoup(string)
|
||||
tables = soup.findAll('table')
|
||||
|
||||
# remove all newlines first
|
||||
string = string.replace("\n", "")
|
||||
|
||||
@ -311,6 +485,14 @@ class ComicVineTalker(QObject):
|
||||
string = string.replace("</p>", "\n\n")
|
||||
string = string.replace("<h4>", "*")
|
||||
string = string.replace("</h4>", "*\n")
|
||||
|
||||
#remove the tables
|
||||
p = re.compile(r'<table[^<]*?>.*?<\/table>')
|
||||
if remove_html_tables:
|
||||
string = p.sub('',string)
|
||||
string = string.replace("*List of covers and their creators:*","")
|
||||
else:
|
||||
string = p.sub('{}',string)
|
||||
|
||||
# now strip all other tags
|
||||
p = re.compile(r'<[^<]*?>')
|
||||
@ -321,13 +503,63 @@ class ComicVineTalker(QObject):
|
||||
|
||||
newstring = newstring.strip()
|
||||
|
||||
if not remove_html_tables:
|
||||
# now rebuild the tables into text from BSoup
|
||||
try:
|
||||
table_strings = []
|
||||
for table in tables:
|
||||
rows = []
|
||||
hdrs = []
|
||||
col_widths = []
|
||||
for hdr in table.findAll('th'):
|
||||
item = hdr.string.strip()
|
||||
hdrs.append(item)
|
||||
col_widths.append(len(item))
|
||||
rows.append(hdrs)
|
||||
|
||||
for row in table.findAll('tr'):
|
||||
cols = []
|
||||
col = row.findAll('td')
|
||||
i = 0
|
||||
for c in col:
|
||||
item = c.string.strip()
|
||||
cols.append(item)
|
||||
if len(item) > col_widths[i]:
|
||||
col_widths[i] = len(item)
|
||||
i += 1
|
||||
if len(cols) != 0:
|
||||
rows.append(cols)
|
||||
# now we have the data, make it into text
|
||||
fmtstr =""
|
||||
for w in col_widths:
|
||||
fmtstr += " {{:{}}}|".format(w+1)
|
||||
width = sum(col_widths) + len(col_widths)*2
|
||||
print "width=" , width
|
||||
table_text = ""
|
||||
counter = 0
|
||||
for row in rows:
|
||||
table_text += fmtstr.format(*row) + "\n"
|
||||
if counter == 0 and len(hdrs)!= 0:
|
||||
table_text += "-" * width + "\n"
|
||||
counter += 1
|
||||
|
||||
table_strings.append(table_text)
|
||||
|
||||
newstring = newstring.format(*table_strings)
|
||||
except:
|
||||
# we caught an error rebuilding the table.
|
||||
# just bail and remove the formatting
|
||||
print "table parse error"
|
||||
newstring.replace("{}", "")
|
||||
|
||||
|
||||
return newstring
|
||||
|
||||
def fetchIssueDate( self, issue_id ):
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['publish_month'], details['publish_year']
|
||||
|
||||
day, month, year = self.parseDateStr( details['cover_date'] )
|
||||
return month, year
|
||||
|
||||
def fetchIssueCoverURLs( self, issue_id ):
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['image_url'], details['thumb_image_url']
|
||||
@ -343,15 +575,14 @@ class ComicVineTalker(QObject):
|
||||
if cached_details['image_url'] is not None:
|
||||
return cached_details
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year,site_detail_url"
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,cover_date,site_detail_url"
|
||||
|
||||
content = self.getUrlContent(issue_url)
|
||||
|
||||
details = dict()
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['publish_month'] = None
|
||||
details['publish_year'] = None
|
||||
details['cover_date'] = None
|
||||
details['site_detail_url'] = None
|
||||
|
||||
cv_response = json.loads(content)
|
||||
@ -361,16 +592,14 @@ class ComicVineTalker(QObject):
|
||||
|
||||
details['image_url'] = cv_response['results']['image']['super_url']
|
||||
details['thumb_image_url'] = cv_response['results']['image']['thumb_url']
|
||||
details['publish_year'] = cv_response['results']['publish_year']
|
||||
details['publish_month'] = cv_response['results']['publish_month']
|
||||
details['cover_date'] = cv_response['results']['cover_date']
|
||||
details['site_detail_url'] = cv_response['results']['site_detail_url']
|
||||
|
||||
if details['image_url'] is not None:
|
||||
self.cacheIssueSelectDetails( issue_id,
|
||||
details['image_url'],
|
||||
details['thumb_image_url'],
|
||||
details['publish_month'],
|
||||
details['publish_year'],
|
||||
details['cover_date'],
|
||||
details['site_detail_url'] )
|
||||
#print details['site_detail_url']
|
||||
return details
|
||||
@ -382,18 +611,16 @@ class ComicVineTalker(QObject):
|
||||
cvc = ComicVineCacher( )
|
||||
return cvc.get_issue_select_details( issue_id )
|
||||
|
||||
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, month, year, page_url ):
|
||||
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, cover_date, page_url ):
|
||||
cvc = ComicVineCacher( )
|
||||
cvc.add_issue_select_details( issue_id, image_url, thumb_url, month, year, page_url )
|
||||
cvc.add_issue_select_details( issue_id, image_url, thumb_url, cover_date, page_url )
|
||||
|
||||
|
||||
def fetchAlternateCoverURLs(self, issue_id):
|
||||
def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
|
||||
url_list = self.fetchCachedAlternateCoverURLs( issue_id )
|
||||
if url_list is not None:
|
||||
return url_list
|
||||
|
||||
issue_page_url = self.fetchIssuePageURL( issue_id )
|
||||
|
||||
# scrape the CV issue page URL to get the alternate cover URLs
|
||||
resp = urllib2.urlopen( issue_page_url )
|
||||
content = resp.read()
|
||||
@ -412,11 +639,14 @@ class ComicVineTalker(QObject):
|
||||
# Using knowledge of the layout of the ComicVine issue page here:
|
||||
# look for the divs that are in the classes 'content-pod' and 'alt-cover'
|
||||
div_list = soup.find_all( 'div')
|
||||
covers_found = 0
|
||||
for d in div_list:
|
||||
if d.has_key('class'):
|
||||
c = d['class']
|
||||
if 'content-pod' in c and 'alt-cover' in c:
|
||||
alt_cover_url_list.append( d.img['src'] )
|
||||
if 'imgboxart' in c and 'issue-cover' in c:
|
||||
covers_found += 1
|
||||
if covers_found != 1:
|
||||
alt_cover_url_list.append( d.img['src'] )
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
@ -446,7 +676,7 @@ class ComicVineTalker(QObject):
|
||||
self.urlFetchComplete.emit( details['image_url'],details['thumb_image_url'], self.issue_id )
|
||||
return
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year,site_detail_url"
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,cover_date,site_detail_url"
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(issue_url)))
|
||||
@ -455,18 +685,24 @@ class ComicVineTalker(QObject):
|
||||
|
||||
# read in the response
|
||||
data = reply.readAll()
|
||||
cv_response = json.loads(str(data))
|
||||
|
||||
try:
|
||||
cv_response = json.loads(str(data))
|
||||
except:
|
||||
print >> sys.stderr, "Comic Vine query failed to get JSON data"
|
||||
print >> sys.stderr, str(data)
|
||||
return
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return
|
||||
|
||||
image_url = cv_response['results']['image']['super_url']
|
||||
thumb_url = cv_response['results']['image']['thumb_url']
|
||||
year = cv_response['results']['publish_year']
|
||||
month = cv_response['results']['publish_month']
|
||||
cover_date = cv_response['results']['cover_date']
|
||||
page_url = cv_response['results']['site_detail_url']
|
||||
|
||||
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, month, year, page_url )
|
||||
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, cover_date, page_url )
|
||||
|
||||
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )
|
||||
|
||||
@ -495,3 +731,11 @@ class ComicVineTalker(QObject):
|
||||
|
||||
self.altUrlListFetchComplete.emit( alt_cover_url_list, int(self.issue_id) )
|
||||
|
||||
def repairUrls(self, issue_list):
|
||||
#make sure there are URLs for the image fields
|
||||
for issue in issue_list:
|
||||
if issue['image'] is None:
|
||||
issue['image'] = dict()
|
||||
issue['image']['super_url'] = ComicVineTalker.logo_url
|
||||
issue['image']['thumb_url'] = ComicVineTalker.logo_url
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""
|
||||
A PyQt4 widget display cover images from either local archive, or from ComicVine
|
||||
|
||||
(TODO: This should be re-factored using subclasses!)
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -26,7 +28,7 @@ from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from pageloader import PageLoader
|
||||
@ -59,11 +61,12 @@ class CoverImageWidget(QWidget):
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
DataMode = 3
|
||||
|
||||
def __init__(self, parent, mode ):
|
||||
def __init__(self, parent, mode, expand_on_click = True ):
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'coverimagewidget.ui' ), self )
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self)
|
||||
|
||||
utils.reduceWidgetFontSize( self.label )
|
||||
|
||||
@ -72,13 +75,16 @@ class CoverImageWidget(QWidget):
|
||||
self.page_loader = None
|
||||
self.showControls = True
|
||||
|
||||
self.btnLeft.setIcon(QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/left.png' )))
|
||||
self.btnRight.setIcon(QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/right.png' )))
|
||||
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
|
||||
self.btnRight.setIcon(QIcon(ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
self.btnLeft.clicked.connect( self.decrementImage )
|
||||
self.btnRight.clicked.connect( self.incrementImage )
|
||||
self.resetWidget()
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
if expand_on_click:
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
else:
|
||||
self.lblImage.setToolTip( "" )
|
||||
|
||||
self.updateContent()
|
||||
|
||||
@ -93,11 +99,12 @@ class CoverImageWidget(QWidget):
|
||||
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:
|
||||
@ -138,7 +145,19 @@ class CoverImageWidget(QWidget):
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete )
|
||||
self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
|
||||
|
||||
def setImageData( self, image_data ):
|
||||
if self.mode == CoverImageWidget.DataMode:
|
||||
self.resetWidget()
|
||||
|
||||
if image_data is None:
|
||||
self.imageIndex = -1
|
||||
else:
|
||||
self.imageIndex = 0
|
||||
self.imageData = image_data
|
||||
|
||||
self.updateContent()
|
||||
|
||||
def primaryUrlFetchComplete( self, primary_url, thumb_url, issue_id ):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
@ -179,11 +198,13 @@ class CoverImageWidget(QWidget):
|
||||
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:
|
||||
if not self.showControls or self.mode == CoverImageWidget.DataMode:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
self.label.hide()
|
||||
@ -236,7 +257,7 @@ class CoverImageWidget(QWidget):
|
||||
self.page_loader = None
|
||||
|
||||
def loadDefault( self ):
|
||||
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
|
||||
self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
#print "loadDefault called"
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
|
||||
@ -288,3 +309,4 @@ class CoverImageWidget(QWidget):
|
||||
|
||||
def showPopup( self ):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to edit credits
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -33,7 +33,7 @@ class CreditEditorWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, mode, role, name, primary ):
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'crediteditorwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('crediteditorwindow.ui' ), self)
|
||||
|
||||
self.mode = mode
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="1.1.0-beta"
|
||||
version="1.1.13-beta"
|
@ -3,7 +3,7 @@ A PyQT4 dialog to confirm and set options for export to zip
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -36,7 +36,7 @@ class ExportWindow(QtGui.QDialog):
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
277
comictaggerlib/filenameparser.py
Normal file
@ -0,0 +1,277 @@
|
||||
"""
|
||||
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
|
@ -3,7 +3,7 @@ Functions for renaming files based on metadata
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,6 +21,7 @@ limitations under the License.
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import utils
|
||||
from issuestring import IssueString
|
||||
|
||||
class FileRenamer:
|
||||
@ -71,6 +72,7 @@ class FileRenamer:
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
preferred_encoding = utils.get_actual_preferred_encoding()
|
||||
|
||||
#print u"{0}".format(md)
|
||||
|
||||
@ -93,7 +95,7 @@ class FileRenamer:
|
||||
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("%B")
|
||||
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%')
|
||||
@ -116,12 +118,20 @@ class FileRenamer:
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name )
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name )
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]+\s+", "- ", new_name )
|
||||
new_name = re.sub("(\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]
|
||||
@ -130,7 +140,9 @@ class FileRenamer:
|
||||
|
||||
# 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(":", "-")
|
||||
new_name = new_name.replace("?", "")
|
||||
|
||||
return new_name
|
@ -4,7 +4,7 @@ A PyQt4 widget for managing list of comic archive files
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -29,8 +29,8 @@ from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from comicarchive import MetaDataStyle
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
import utils
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
@ -71,7 +71,7 @@ class FileSelectionList(QWidget):
|
||||
def __init__(self, parent , settings ):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
@ -98,6 +98,14 @@ class FileSelectionList(QWidget):
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
|
||||
def getSorting(self):
|
||||
col = self.twList.horizontalHeader().sortIndicatorSection()
|
||||
order = self.twList.horizontalHeader().sortIndicatorOrder()
|
||||
return col, order
|
||||
|
||||
def setSorting(self, col, order):
|
||||
col = self.twList.horizontalHeader().setSortIndicator( col, order)
|
||||
|
||||
def addAppAction( self, action ):
|
||||
self.insertAction( None , action )
|
||||
|
||||
@ -154,25 +162,16 @@ class FileSelectionList(QWidget):
|
||||
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 = []
|
||||
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
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
p = p.decode(filename_encoding, 'replace')
|
||||
|
||||
if os.path.isdir( unicode(p)):
|
||||
for root,dirs,files in os.walk( unicode(p) ):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root,unicode(f)))
|
||||
else:
|
||||
filelist.append(unicode(p))
|
||||
|
||||
filelist = utils.get_recursive_filelist( pathlist )
|
||||
|
||||
# we now have a list of files to add
|
||||
|
||||
@ -199,7 +198,13 @@ class FileSelectionList(QWidget):
|
||||
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
|
||||
@ -231,12 +236,9 @@ class FileSelectionList(QWidget):
|
||||
if self.isListDupe(path):
|
||||
return None
|
||||
|
||||
ca = ComicArchive( path )
|
||||
if self.settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( self.settings.rar_exe_path )
|
||||
ca = ComicArchive( path, self.settings.rar_exe_path )
|
||||
|
||||
if ca.seemsToBeAComicArchive() :
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
|
@ -8,7 +8,7 @@
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -31,7 +31,7 @@ class PageType:
|
||||
InnerCover = "InnerCover"
|
||||
Roundup = "Roundup"
|
||||
Story = "Story"
|
||||
Advertisment = "Advertisment"
|
||||
Advertisement = "Advertisement"
|
||||
Editorial = "Editorial"
|
||||
Letters = "Letters"
|
||||
Preview = "Preview"
|
||||
@ -63,6 +63,7 @@ class GenericMetadata:
|
||||
self.publisher = None
|
||||
self.month = None
|
||||
self.year = None
|
||||
self.day = None
|
||||
self.issueCount = None
|
||||
self.volume = None
|
||||
self.genre = None
|
||||
@ -125,6 +126,7 @@ class GenericMetadata:
|
||||
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 )
|
||||
@ -256,8 +258,9 @@ class GenericMetadata:
|
||||
add_attr_string( "issueCount" )
|
||||
add_attr_string( "title" )
|
||||
add_attr_string( "publisher" )
|
||||
add_attr_string( "month" )
|
||||
add_attr_string( "year" )
|
||||
add_attr_string( "month" )
|
||||
add_attr_string( "day" )
|
||||
add_attr_string( "volume" )
|
||||
add_attr_string( "volumeCount" )
|
||||
add_attr_string( "genre" )
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 362 B After Width: | Height: | Size: 362 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
@ -3,7 +3,7 @@ A python class to manage fetching and caching of images by URL
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
@ -1,4 +1,22 @@
|
||||
"""
|
||||
A pthyon class to manage creating image content hashes, and calculate hamming distances
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
import StringIO
|
||||
import sys
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 widget to display a popup image
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -29,7 +29,7 @@ class ImagePopup(QtGui.QDialog):
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'imagepopup.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui' ), self)
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
@ -48,7 +48,7 @@ class ImagePopup(QtGui.QDialog):
|
||||
# widget
|
||||
self.desktopBg = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop ().winId(),
|
||||
0,0, screen_size.width(), screen_size.height())
|
||||
bg = QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/popup_bg.png' ))
|
||||
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
|
||||
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height())
|
||||
self.setMask(self.clientBgPixmap.mask())
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select specific issue from list
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -20,6 +20,7 @@ limitations under the License.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
@ -32,6 +33,13 @@ from issuestring import IssueString
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class IssueNumberTableWidgetItem(QtGui.QTableWidgetItem):
|
||||
def __lt__(self, other):
|
||||
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
|
||||
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
|
||||
return (IssueString(selfStr).asFloat() <
|
||||
IssueString(otherStr).asFloat())
|
||||
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
@ -39,7 +47,7 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, settings, series_id, issue_number):
|
||||
super(IssueSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'issueselectionwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('issueselectionwindow.ui' ), self)
|
||||
|
||||
self.coverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
@ -47,6 +55,7 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
utils.reduceWidgetFontSize( self.teDescription, 1 )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
@ -85,6 +94,7 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
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!"))
|
||||
@ -92,8 +102,6 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.issue_list = volume_data['issues']
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
@ -102,20 +110,35 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record['issue_number']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item = IssueNumberTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setData(QtCore.Qt.DisplayRole, float(item_text))
|
||||
item.setData(QtCore.Qt.DisplayRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = record['name']
|
||||
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() == IssueString(self.issue_number).asString():
|
||||
if IssueString(record['issue_number']).asString().lower() == IssueString(self.issue_number).asString().lower():
|
||||
self.initial_id = record['id']
|
||||
|
||||
row += 1
|
||||
@ -142,5 +165,10 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
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
|
||||
|
@ -8,11 +8,12 @@ e.g.:
|
||||
"0"
|
||||
"-1"
|
||||
"5AU"
|
||||
"100-2"
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -34,31 +35,63 @@ import re
|
||||
class IssueString:
|
||||
def __init__(self, text):
|
||||
|
||||
if text is None:
|
||||
self.num = None
|
||||
self.suffix = ""
|
||||
return
|
||||
|
||||
self.text = str(text)
|
||||
#strip out non float-y stuff
|
||||
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
|
||||
# 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 tmp_num_str == "":
|
||||
self.num = None
|
||||
self.suffix = self.text
|
||||
|
||||
if text is None:
|
||||
return
|
||||
|
||||
if type(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:
|
||||
if tmp_num_str.count(".") > 1:
|
||||
#make sure it's a valid float or int.
|
||||
parts = tmp_num_str.split('.')
|
||||
self.num = float( parts[0] + '.' + parts[1] )
|
||||
else:
|
||||
self.num = float( tmp_num_str )
|
||||
|
||||
self.suffix = ""
|
||||
parts = self.text.split(tmp_num_str)
|
||||
if len( parts ) > 1 :
|
||||
self.suffix = parts[1]
|
||||
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
|
@ -3,7 +3,7 @@ A PyQT4 dialog to a text file or log
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -30,7 +30,7 @@ class LogWindow(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(LogWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'logwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
79
comictaggerlib/main.py
Executable file
@ -0,0 +1,79 @@
|
||||
"""
|
||||
A python app to (automatically) tag comic archives
|
||||
"""
|
||||
|
||||
"""
|
||||
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 signal
|
||||
import os
|
||||
import traceback
|
||||
import platform
|
||||
|
||||
import utils
|
||||
import cli
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options
|
||||
|
||||
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()
|
||||
|
||||
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, opts=opts )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish( tagger_window )
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception, e:
|
||||
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )
|
||||
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -26,7 +26,7 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
@ -38,7 +38,7 @@ class MatchSelectionWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, matches, comic_archive):
|
||||
super(MatchSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'matchselectionwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
@ -51,6 +51,7 @@ class MatchSelectionWindow(QtGui.QDialog):
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
utils.reduceWidgetFontSize( self.teDescription, 1 )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
@ -116,6 +117,8 @@ class MatchSelectionWindow(QtGui.QDialog):
|
||||
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)
|
||||
@ -142,7 +145,11 @@ class MatchSelectionWindow(QtGui.QDialog):
|
||||
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)
|
||||
|
@ -3,7 +3,7 @@ A PyQt4 dialog to show a message and let the user check a box
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
@ -3,7 +3,7 @@ CLI options class for comictagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -22,22 +22,17 @@ import sys
|
||||
import getopt
|
||||
import platform
|
||||
import os
|
||||
|
||||
import traceback
|
||||
import ctversion
|
||||
import utils
|
||||
try:
|
||||
import argparse
|
||||
except:
|
||||
pass
|
||||
|
||||
from genericmetadata import GenericMetadata
|
||||
|
||||
class Enum(set):
|
||||
def __getattr__(self, name):
|
||||
if name in self:
|
||||
return name
|
||||
raise AttributeError
|
||||
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
COMET = 2
|
||||
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
|
||||
|
||||
from comicarchive import MetaDataStyle
|
||||
from versionchecker import VersionChecker
|
||||
|
||||
class Options:
|
||||
help_text = """
|
||||
@ -57,6 +52,7 @@ If no options are given, {0} will run in windowed mode
|
||||
-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)
|
||||
@ -82,11 +78,16 @@ If no options are given, {0} will run in windowed mode
|
||||
-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)
|
||||
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
|
||||
-h, --help Display this message
|
||||
|
||||
For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
"""
|
||||
|
||||
|
||||
@ -114,6 +115,10 @@ If no options are given, {0} will run in windowed mode
|
||||
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 ):
|
||||
@ -175,6 +180,41 @@ If no options are given, {0} will run in windowed mode
|
||||
#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)
|
||||
|
||||
def parseCmdLineArgs(self):
|
||||
|
||||
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
|
||||
@ -183,14 +223,23 @@ If no options are given, {0} will run in windowed mode
|
||||
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:ie",
|
||||
"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="
|
||||
"export-to-zip", "delete-rar", "abort-on-conflict" ] )
|
||||
"interactive", "nosummary", "version", "id=" , "recursive", "script=",
|
||||
"export-to-zip", "delete-rar", "abort-on-conflict", "assume-issue-one" ] )
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_msg_and_quit( str(err), 2 )
|
||||
@ -201,6 +250,11 @@ If no options are given, {0} will run in windowed mode
|
||||
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"):
|
||||
@ -245,11 +299,18 @@ If no options are given, {0} will run in windowed mode
|
||||
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-2013 Anthony Beville".format(ctversion.version)
|
||||
print "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)"
|
||||
print "ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(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":
|
||||
@ -260,11 +321,12 @@ If no options are given, {0} will run in windowed mode
|
||||
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
|
||||
|
||||
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
|
||||
@ -273,8 +335,11 @@ If no options are given, {0} will run in windowed mode
|
||||
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, or export", 1 )
|
||||
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, 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
|
||||
@ -282,7 +347,8 @@ If no options are given, {0} will run in windowed mode
|
||||
self.file_list = []
|
||||
for item in args:
|
||||
self.file_list.extend(glob.glob(item))
|
||||
self.filename = self.file_list[0]
|
||||
if len(self.file_list) > 0:
|
||||
self.filename = self.file_list[0]
|
||||
else:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
@ -302,3 +368,5 @@ If no options are given, {0} will run in windowed mode
|
||||
#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 )
|
@ -3,7 +3,7 @@ A PyQT4 dialog to show pages of a comic archive
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -30,7 +30,7 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, metadata):
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui' ), self)
|
||||
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.pageContainer )
|
||||
@ -52,8 +52,8 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.btnPrev.setText("<<")
|
||||
self.btnNext.setText(">>")
|
||||
else:
|
||||
self.btnPrev.setIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/left.png' )))
|
||||
self.btnNext.setIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/right.png' )))
|
||||
self.btnPrev.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('left.png' )))
|
||||
self.btnNext.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
self.btnNext.clicked.connect( self.nextPage )
|
||||
self.btnPrev.clicked.connect( self.prevPage )
|
@ -3,7 +3,7 @@ A PyQt4 widget for editing the page list info
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -26,7 +26,7 @@ from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from pageloader import PageLoader
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
@ -64,7 +64,7 @@ class PageListEditor(QWidget):
|
||||
pageTypeNames = {
|
||||
PageType.FrontCover: "Front Cover",
|
||||
PageType.InnerCover: "Inner Cover",
|
||||
PageType.Advertisment: "Advertisment",
|
||||
PageType.Advertisement: "Advertisement",
|
||||
PageType.Roundup: "Roundup",
|
||||
PageType.Story: "Story",
|
||||
PageType.Editorial: "Editorial",
|
||||
@ -78,7 +78,7 @@ class PageListEditor(QWidget):
|
||||
def __init__(self, parent ):
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui' ), self)
|
||||
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QGridLayout( self.pageContainer )
|
||||
@ -92,7 +92,7 @@ class PageListEditor(QWidget):
|
||||
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.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 )
|
@ -3,7 +3,7 @@ A PyQT4 class to load a page image from a ComicArchive in a background thread
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
@ -3,7 +3,7 @@ A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -30,7 +30,7 @@ class IDProgressWindow(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(IDProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
@ -3,7 +3,7 @@ A PyQT4 dialog to confirm rename
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -23,7 +23,7 @@ from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
|
||||
import os
|
||||
import utils
|
||||
@ -33,7 +33,7 @@ class RenameWindow(QtGui.QDialog):
|
||||
def __init__( self, parent, comic_archive_list, data_style, settings ):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui' ), self)
|
||||
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
@ -72,7 +72,7 @@ class RenameWindow(QtGui.QDialog):
|
||||
|
||||
md = ca.readMetadata(self.data_style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
||||
self.renamer.setMetadata( md )
|
||||
new_name = self.renamer.determineName( ca.path, ext=new_ext )
|
||||
|
@ -3,7 +3,7 @@ Settings class for comictagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,10 @@ limitations under the License.
|
||||
#import sys
|
||||
import os
|
||||
import sys
|
||||
import ConfigParser
|
||||
import configparser
|
||||
import platform
|
||||
import codecs
|
||||
import uuid
|
||||
|
||||
import utils
|
||||
|
||||
@ -30,27 +32,51 @@ class ComicTaggerSettings:
|
||||
|
||||
@staticmethod
|
||||
def getSettingsFolder():
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
if platform.system() == "Windows":
|
||||
return os.path.join( os.environ['APPDATA'], 'ComicTagger' )
|
||||
folder = os.path.join( os.environ['APPDATA'], 'ComicTagger' )
|
||||
else:
|
||||
return os.path.join( os.path.expanduser('~') , '.ComicTagger')
|
||||
|
||||
folder = os.path.join( os.path.expanduser('~') , '.ComicTagger')
|
||||
if folder is not None:
|
||||
folder = folder.decode(filename_encoding)
|
||||
return folder
|
||||
|
||||
frozen_win_exe_path = None
|
||||
|
||||
@staticmethod
|
||||
def baseDir():
|
||||
if platform.system() == "Darwin" and getattr(sys, 'frozen', None):
|
||||
return sys._MEIPASS
|
||||
if getattr(sys, 'frozen', None):
|
||||
if platform.system() == "Darwin":
|
||||
return sys._MEIPASS
|
||||
else: # Windows
|
||||
#Preserve this value, in case sys.argv gets changed importing a plugin script
|
||||
if ComicTaggerSettings.frozen_win_exe_path is None:
|
||||
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return ComicTaggerSettings.frozen_win_exe_path
|
||||
else:
|
||||
#print "ATB basename", os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return os.path.dirname( os.path.abspath( __file__) )
|
||||
|
||||
@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 )
|
||||
|
||||
def setDefaultValues( self ):
|
||||
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.unrar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
self.check_for_new_version = True
|
||||
self.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 = ""
|
||||
@ -60,6 +86,8 @@ class ComicTaggerSettings:
|
||||
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
|
||||
@ -68,9 +96,16 @@ class ComicTaggerSettings:
|
||||
# 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
|
||||
self.clear_form_before_populating_from_cv = False
|
||||
self.remove_html_tables = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
@ -78,6 +113,7 @@ class ComicTaggerSettings:
|
||||
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
|
||||
@ -88,6 +124,13 @@ class ComicTaggerSettings:
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
|
||||
#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
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -95,7 +138,7 @@ class ComicTaggerSettings:
|
||||
self.folder = ""
|
||||
self.setDefaultValues()
|
||||
|
||||
self.config = ConfigParser.RawConfigParser()
|
||||
self.config = configparser.RawConfigParser()
|
||||
self.folder = ComicTaggerSettings.getSettingsFolder()
|
||||
|
||||
if not os.path.exists( self.folder ):
|
||||
@ -131,18 +174,35 @@ class ComicTaggerSettings:
|
||||
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))
|
||||
|
||||
def reset( self ):
|
||||
os.unlink( self.settings_file )
|
||||
self.__init__()
|
||||
|
||||
def load(self):
|
||||
|
||||
self.config.read( self.settings_file )
|
||||
|
||||
def readline_generator(f):
|
||||
line = f.readline()
|
||||
while line:
|
||||
yield line
|
||||
line = f.readline()
|
||||
|
||||
#self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
|
||||
self.config.read_file(readline_generator(codecs.open(self.settings_file, "r", "utf8")))
|
||||
|
||||
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
|
||||
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
|
||||
|
||||
if self.config.has_option('settings', 'check_for_new_version'):
|
||||
self.check_for_new_version = self.config.getboolean( 'settings', 'check_for_new_version' )
|
||||
if self.config.has_option('settings', 'send_usage_stats'):
|
||||
self.send_usage_stats = self.config.getboolean( 'settings', 'send_usage_stats' )
|
||||
|
||||
if self.config.has_option('auto', 'install_id'):
|
||||
self.install_id = self.config.get( 'auto', 'install_id' )
|
||||
if self.config.has_option('auto', 'last_selected_load_data_style'):
|
||||
self.last_selected_load_data_style = self.config.getint( 'auto', 'last_selected_load_data_style' )
|
||||
if self.config.has_option('auto', 'last_selected_save_data_style'):
|
||||
@ -161,19 +221,35 @@ class ComicTaggerSettings:
|
||||
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('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('cbl_transform', 'assume_lone_credit_is_primary'):
|
||||
self.assume_lone_credit_is_primary = self.config.getboolean( 'cbl_transform', 'assume_lone_credit_is_primary' )
|
||||
@ -184,7 +260,9 @@ class ComicTaggerSettings:
|
||||
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' )
|
||||
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'):
|
||||
@ -200,18 +278,32 @@ class ComicTaggerSettings:
|
||||
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' )
|
||||
|
||||
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 )
|
||||
@ -221,6 +313,8 @@ class ComicTaggerSettings:
|
||||
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' )
|
||||
@ -233,11 +327,20 @@ class ComicTaggerSettings:
|
||||
|
||||
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 )
|
||||
|
||||
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 )
|
||||
|
||||
if not self.config.has_section( 'cbl_transform' ):
|
||||
self.config.add_section( 'cbl_transform' )
|
||||
@ -246,6 +349,7 @@ class ComicTaggerSettings:
|
||||
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 )
|
||||
@ -258,7 +362,17 @@ class ComicTaggerSettings:
|
||||
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 )
|
||||
|
||||
with open( self.settings_file, 'wb') as configfile:
|
||||
with codecs.open( self.settings_file, 'wb', 'utf8') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
#make sure the basedir is cached, in case we're on windows running a script from frozen binary
|
||||
ComicTaggerSettings.baseDir()
|
@ -3,7 +3,7 @@ A PyQT4 dialog to enter app settings
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -54,7 +54,7 @@ class SettingsWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, settings ):
|
||||
super(SettingsWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'settingswindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
@ -119,8 +119,18 @@ class SettingsWindow(QtGui.QDialog):
|
||||
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)
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked)
|
||||
@ -130,6 +140,8 @@ class SettingsWindow(QtGui.QDialog):
|
||||
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:
|
||||
@ -153,24 +165,32 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
|
||||
|
||||
# make sure unrar program is now in the path for the UnRAR class
|
||||
# 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.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()
|
||||
@ -184,7 +204,6 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
|
||||
def selectRar( self ):
|
||||
self.selectFile( self.leRarExePath, "RAR" )
|
||||
|
||||
@ -225,5 +244,5 @@ class SettingsWindow(QtGui.QDialog):
|
||||
control.setText( str(fileList[0]) )
|
||||
|
||||
def showRenameTab( self ):
|
||||
self.tabWidget.setCurrentIndex(4)
|
||||
self.tabWidget.setCurrentIndex(5)
|
||||
|
@ -4,7 +4,7 @@ The main window of the ComicTagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,7 +21,9 @@ limitations under the License.
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import QUrl,pyqtSignal
|
||||
from PyQt4 import QtNetwork
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import locale
|
||||
import platform
|
||||
@ -30,9 +32,10 @@ import pprint
|
||||
import json
|
||||
import webbrowser
|
||||
import re
|
||||
import pickle
|
||||
|
||||
from volumeselectionwindow import VolumeSelectionWindow
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from comicinfoxml import ComicInfoXml
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
@ -55,6 +58,7 @@ from autotagstartwindow import AutoTagStartWindow
|
||||
from autotagprogresswindow import AutoTagProgressWindow
|
||||
from autotagmatchwindow import AutoTagMatchWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from versionchecker import VersionChecker
|
||||
|
||||
import utils
|
||||
import ctversion
|
||||
@ -72,21 +76,49 @@ class MultipleMatch():
|
||||
def __init__( self, ca, match_list):
|
||||
self.ca = ca
|
||||
self.matches = match_list
|
||||
|
||||
# this reads the environment and inits the right locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
appName = "ComicTagger"
|
||||
version = ctversion.version
|
||||
|
||||
def __init__(self, file_list, settings, parent = None):
|
||||
def __init__(self, file_list, settings, parent = None, opts=None):
|
||||
super(TaggerWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui' ), self)
|
||||
self.settings = settings
|
||||
|
||||
#----------------------------------
|
||||
# prevent multiple instances
|
||||
socket = QtNetwork.QLocalSocket(self)
|
||||
socket.connectToServer(settings.install_id)
|
||||
alive = socket.waitForConnected(3000)
|
||||
if alive:
|
||||
print "Another application with key [{}] is already running".format( settings.install_id)
|
||||
# send file list to other instance
|
||||
if len(file_list) > 0:
|
||||
socket.write(pickle.dumps(file_list))
|
||||
if not socket.waitForBytesWritten(3000):
|
||||
print socket.errorString().toLatin1()
|
||||
socket.disconnectFromServer()
|
||||
sys.exit()
|
||||
else:
|
||||
# listen on a socket to prevent multiple instances
|
||||
self.socketServer = QtNetwork.QLocalServer(self)
|
||||
self.socketServer.newConnection.connect(self.onIncomingSocketConnection)
|
||||
ok = self.socketServer.listen(settings.install_id)
|
||||
if not ok:
|
||||
if self.socketServer.serverError() == QtNetwork.QAbstractSocket.AddressInUseError:
|
||||
#print "Resetting unresponsive socket with key [{}]".format(settings.install_id)
|
||||
self.socketServer.removeServer(settings.install_id)
|
||||
ok = self.socketServer.listen(settings.install_id)
|
||||
if not ok:
|
||||
print "Cannot start local socket with key [{}]. Reason: %s ".format(settings.install_id, self.socketServer.errorString())
|
||||
sys.exit()
|
||||
#print "Registering as single instance with key [{}]".format(settings.install_id)
|
||||
#----------------------------------
|
||||
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
@ -103,11 +135,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
|
||||
self.fileSelectionList.listCleared.connect( self.fileListCleared )
|
||||
|
||||
# ATB: Disable the list...
|
||||
#self.splitter.setSizes([100,0])
|
||||
#self.splitter.setHandleWidth(0)
|
||||
#self.splitter.handle(1).setDisabled(True)
|
||||
self.fileSelectionList.setSorting(self.settings.last_filelist_sorted_column,
|
||||
self.settings.last_filelist_sorted_order)
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# walk through all the lablels in the main form, and make them
|
||||
@ -122,8 +151,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
self.scrollAreaWidgetContents.adjustSize()
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('app.png')))
|
||||
|
||||
if opts is not None and opts.data_style is not None and opts.data_style != MetaDataStyle.COMET:
|
||||
#respect the command line option tag type
|
||||
settings.last_selected_save_data_style = opts.data_style
|
||||
settings.last_selected_load_data_style = opts.data_style
|
||||
|
||||
self.save_data_style = settings.last_selected_save_data_style
|
||||
self.load_data_style = settings.last_selected_load_data_style
|
||||
|
||||
@ -151,7 +185,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
#TODO set up an RE validator for issueNum that allows
|
||||
# for all sorts of wacky things
|
||||
|
||||
|
||||
# tweak some control fonts
|
||||
utils.reduceWidgetFontSize( self.lblFilename, 1 )
|
||||
utils.reduceWidgetFontSize( self.lblArchiveType )
|
||||
utils.reduceWidgetFontSize( self.lblTagList )
|
||||
utils.reduceWidgetFontSize( self.lblPageCount )
|
||||
|
||||
#make sure some editable comboboxes don't take drop actions
|
||||
self.cbFormat.lineEdit().setAcceptDrops(False)
|
||||
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
|
||||
@ -163,6 +203,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.btnAddCredit.clicked.connect(self.addCredit)
|
||||
self.btnRemoveCredit.clicked.connect(self.removeCredit)
|
||||
self.twCredits.cellDoubleClicked.connect(self.editCredit)
|
||||
self.connectDirtyFlagSignals()
|
||||
self.pageListEditor.modified.connect(self.setDirtyFlag)
|
||||
self.pageListEditor.firstFrontCoverChanged.connect( self.frontCoverChanged )
|
||||
self.pageListEditor.listOrderChanged.connect( self.pageListOrderChanged )
|
||||
self.tabWidget.currentChanged.connect( self.tabChanged )
|
||||
@ -203,7 +245,23 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
"""
|
||||
)
|
||||
self.settings.show_disclaimer = not checked
|
||||
|
||||
|
||||
if self.settings.ask_about_usage_stats:
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Anonymous Stats"),
|
||||
self.tr(
|
||||
"Is it okay if ComicTagger occasionally sends some anonymous usage statistics? Nothing nefarious, "
|
||||
"just trying to get a better idea of how the app is being used.\n\nThanks for your support!"
|
||||
),
|
||||
QtGui.QMessageBox.Yes|QtGui.QMessageBox.Default, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
self.settings.send_usage_stats = True
|
||||
self.settings.ask_about_usage_stats = False
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.checkLatestVersionOnline()
|
||||
|
||||
def sigint_handler(self, *args):
|
||||
# defer the actual close in the app loop thread
|
||||
QtCore.QTimer.singleShot(200, self.close)
|
||||
@ -312,7 +370,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionApplyCBLTransform.setStatusTip( 'Modify tags specifically for CBL format' )
|
||||
self.actionApplyCBLTransform.triggered.connect( self.applyCBLTransform )
|
||||
|
||||
#self.actionClearEntryForm.setShortcut( 'Ctrl+C' )
|
||||
self.actionClearEntryForm.setShortcut( 'Ctrl+Shift+C' )
|
||||
self.actionClearEntryForm.setStatusTip( 'Clear all the data on the screen' )
|
||||
self.actionClearEntryForm.triggered.connect( self.clearForm )
|
||||
|
||||
@ -329,16 +387,16 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionComicTaggerForum.triggered.connect( self.showForum )
|
||||
|
||||
# ToolBar
|
||||
|
||||
self.actionLoad.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/open.png')) )
|
||||
self.actionLoadFolder.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/longbox.png')) )
|
||||
self.actionWrite_Tags.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/save.png')) )
|
||||
self.actionParse_Filename.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/parse.png')) )
|
||||
self.actionSearchOnline.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/search.png')) )
|
||||
self.actionAutoIdentify.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/auto.png')) )
|
||||
self.actionAutoTag.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/autotag.png')) )
|
||||
self.actionClearEntryForm.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/clear.png')) )
|
||||
self.actionPageBrowser.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/browse.png') ))
|
||||
|
||||
self.actionLoad.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('open.png')))
|
||||
self.actionLoadFolder.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('longbox.png')))
|
||||
self.actionWrite_Tags.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('save.png')))
|
||||
self.actionParse_Filename.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('parse.png')))
|
||||
self.actionSearchOnline.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('search.png')))
|
||||
self.actionAutoIdentify.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('auto.png')))
|
||||
self.actionAutoTag.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('autotag.png')))
|
||||
self.actionClearEntryForm.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('clear.png')))
|
||||
self.actionPageBrowser.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('browse.png')))
|
||||
|
||||
self.toolBar.addAction( self.actionLoad )
|
||||
self.toolBar.addAction( self.actionLoadFolder )
|
||||
@ -451,14 +509,14 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
msgBox = QtGui.QMessageBox()
|
||||
msgBox.setWindowTitle( self.tr("About " + self.appName ) )
|
||||
msgBox.setTextFormat( QtCore.Qt.RichText )
|
||||
msgBox.setIconPixmap( QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/about.png' )) )
|
||||
msgBox.setIconPixmap( QtGui.QPixmap(ComicTaggerSettings.getGraphic('about.png')) )
|
||||
msgBox.setText( "<br><br><br>"
|
||||
+ self.appName + " v" + self.version + "<br>"
|
||||
+ "(c)2012 Anthony Beville<br><br>"
|
||||
+ "(c)2014 Anthony Beville<br><br>"
|
||||
+ "<a href='{0}'>{0}</a><br><br>".format(website)
|
||||
+ "<a href='mailto:{0}'>{0}</a><br><br>".format(email)
|
||||
+ "License: <a href='{0}'>{1}</a>".format(license_link, license_name) )
|
||||
|
||||
|
||||
msgBox.setStandardButtons( QtGui.QMessageBox.Ok )
|
||||
msgBox.exec_()
|
||||
|
||||
@ -484,7 +542,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
def actualLoadCurrentArchive( self ):
|
||||
if self.metadata.isEmpty:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
self.metadata = self.comic_archive.metadataFromFilename( self.settings.parse_scan_info)
|
||||
if len(self.metadata.pages) == 0:
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
@ -560,7 +618,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
filename = os.path.basename( ca.path )
|
||||
filename = os.path.splitext(filename)[0]
|
||||
filename = FileNameParser().fixSpaces(filename)
|
||||
filename = FileNameParser().fixSpaces(filename, False)
|
||||
|
||||
self.lblFilename.setText( filename )
|
||||
|
||||
@ -850,7 +908,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if self.comic_archive is not None:
|
||||
#copy the form onto metadata object
|
||||
self.formToMetadata()
|
||||
new_metadata = self.comic_archive.metadataFromFilename( )
|
||||
new_metadata = self.comic_archive.metadataFromFilename(self.settings.parse_scan_info)
|
||||
if new_metadata is not None:
|
||||
self.metadata.overlay( new_metadata )
|
||||
self.metadataToForm()
|
||||
@ -897,7 +955,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
def queryOnline(self, autoselect=False):
|
||||
|
||||
issue_number = str(self.leIssueNum.text()).strip()
|
||||
issue_number = unicode(self.leIssueNum.text()).strip()
|
||||
|
||||
if autoselect and issue_number == "":
|
||||
QtGui.QMessageBox.information(self,"Automatic Identify Search", "Can't auto-identify without an issue number (yet!)")
|
||||
@ -908,14 +966,17 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Online Search"), self.tr("Need to enter a series name to search."))
|
||||
return
|
||||
|
||||
|
||||
|
||||
year = str(self.lePubYear.text()).strip()
|
||||
if year == "":
|
||||
year = None
|
||||
|
||||
issue_count = str(self.leIssueCount.text()).strip()
|
||||
if issue_count == "":
|
||||
issue_count = None
|
||||
|
||||
cover_index_list = self.metadata.getCoverPageIndexList()
|
||||
selector = VolumeSelectionWindow( self, series_name, issue_number, year, cover_index_list, self.comic_archive, self.settings, autoselect )
|
||||
selector = VolumeSelectionWindow( self, series_name, issue_number, year, issue_count, cover_index_list, self.comic_archive, self.settings, autoselect )
|
||||
|
||||
title = "Search: '" + series_name + "' - "
|
||||
selector.setWindowTitle( title + "Select Series")
|
||||
@ -942,6 +1003,9 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
new_metadata = CBLTransformer( new_metadata, self.settings ).apply()
|
||||
|
||||
if self.settings.clear_form_before_populating_from_cv:
|
||||
self.clearForm()
|
||||
|
||||
self.metadata.overlay( new_metadata )
|
||||
# Now push the new combined data into the edit controls
|
||||
@ -953,25 +1017,6 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def commitMetadata(self):
|
||||
|
||||
if ( self.metadata is not None and self.comic_archive is not None):
|
||||
|
||||
if self.comic_archive.isRar() and self.save_data_style == MetaDataStyle.CBI:
|
||||
if self.settings.ask_about_cbi_in_rar:
|
||||
checked = OptionalMessageDialog.msg( self, "RAR and ComicBookLover",
|
||||
"""
|
||||
You are about to write a CBL tag block to a RAR archive!
|
||||
While technically possible, no known reader can read those tags from RAR
|
||||
yet.<br><br>
|
||||
If you would like this feature in the ComicBookLover apps, please go to their
|
||||
forums and add your voice to a feature request!
|
||||
<a href=http://forums.comicbooklover.com/categories/ipad-features>
|
||||
http://forums.comicbooklover.com/categories/ipad-features</a><br>
|
||||
<a href=http://forums.comicbooklover.com/categories/mac-features>
|
||||
http://forums.comicbooklover.com/categories/mac-features</a>
|
||||
""",
|
||||
)
|
||||
self.settings.ask_about_cbi_in_rar = not checked
|
||||
|
||||
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Save Tags"),
|
||||
self.tr("Are you sure you wish to save " + MetaDataStyle.name[self.save_data_style] + " tags to this archive?"),
|
||||
@ -1537,7 +1582,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
# read in metadata, and parse file name if not there
|
||||
md = ca.readMetadata( self.save_data_style )
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
||||
if dlg.ignoreLeadingDigitsInFilename and md.series is not None:
|
||||
#remove all leading numbers
|
||||
md.series = re.sub( "([\d.]*)(.*)", "\\2", md.series)
|
||||
@ -1750,6 +1795,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.settings.last_main_window_y = self.y()
|
||||
self.settings.last_form_side_width = self.splitter.sizes()[0]
|
||||
self.settings.last_list_side_width = self.splitter.sizes()[1]
|
||||
self.settings.last_filelist_sorted_column, self.settings.last_filelist_sorted_order = self.fileSelectionList.getSorting()
|
||||
self.settings.save()
|
||||
|
||||
|
||||
@ -1854,4 +1900,35 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def tabChanged( self, idx ):
|
||||
if idx == 0:
|
||||
self.splitterMovedEvent( 0, 0)
|
||||
|
||||
def checkLatestVersionOnline( self ):
|
||||
self.versionChecker = VersionChecker()
|
||||
self.versionChecker.versionRequestComplete.connect( self.versionCheckComplete )
|
||||
self.versionChecker.asyncGetLatestVersion( self.settings.install_id, self.settings.send_usage_stats )
|
||||
|
||||
def versionCheckComplete( self, new_version ):
|
||||
if ( new_version != self.version and
|
||||
new_version != self.settings.dont_notify_about_this_version):
|
||||
website = "http://code.google.com/p/comictagger"
|
||||
checked = OptionalMessageDialog.msg( self, "New version available!",
|
||||
"New version ({0}) available!<br>(You are currently running {1})<br><br>".format( new_version, self.version) +
|
||||
"Visit <a href='{0}'>{0}</a> for more info.<br><br>".format(website),
|
||||
QtCore.Qt.Unchecked,
|
||||
"Don't tell me about this version again")
|
||||
if checked:
|
||||
self.settings.dont_notify_about_this_version = new_version
|
||||
|
||||
def onIncomingSocketConnection(self):
|
||||
# accept connection from other instance.
|
||||
# read in the file list if they're giving it,
|
||||
# and add to our own list
|
||||
localSocket = self.socketServer.nextPendingConnection()
|
||||
if not localSocket.waitForReadyRead(3000):
|
||||
print localSocket.errorString().toLatin1()
|
||||
return
|
||||
byteArray = localSocket.readAll()
|
||||
if len(byteArray) > 0:
|
||||
obj = pickle.loads(byteArray)
|
||||
localSocket.disconnectFromServer()
|
||||
if type(obj) is list:
|
||||
self.fileSelectionList.addPathList( obj )
|
@ -6,21 +6,18 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>907</width>
|
||||
<height>507</height>
|
||||
<width>943</width>
|
||||
<height>467</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
@ -38,45 +35,73 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</column>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
@ -21,6 +21,44 @@
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QWidget" name="archiveCoverContainer">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QWidget" name="testCoverContainer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
@ -66,50 +104,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="lblTest">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
162
comictaggerlib/ui/issueselectionwindow.ui
Normal file
@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogIssueSelect</class>
|
||||
<widget class="QDialog" name="dialogIssueSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>872</width>
|
||||
<height>550</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Issue</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Issue</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="coverImageContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<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>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -6,21 +6,18 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>907</width>
|
||||
<height>507</height>
|
||||
<width>943</width>
|
||||
<height>467</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
@ -38,45 +35,73 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</column>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
@ -35,7 +35,7 @@
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="btnResetSettings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@ -48,7 +48,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="lblDefaultSettings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@ -64,7 +64,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="btnClearCache">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@ -77,7 +77,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@ -93,6 +93,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxCheckForNewVersion">
|
||||
<property name="text">
|
||||
<string>Check for new version on startup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@ -307,6 +314,24 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_6">
|
||||
<attribute name="title">
|
||||
<string>Filename Parser</string>
|
||||
</attribute>
|
||||
<widget class="QCheckBox" name="cbxParseScanInfo">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>421</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Parse Scan Info From Filename (Experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Comic Vine</string>
|
||||
@ -316,7 +341,7 @@
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>240</width>
|
||||
<width>251</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -324,6 +349,32 @@
|
||||
<string>Use Series Start Date as Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>50</y>
|
||||
<width>341</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Clear Form Before Importing Comic Vine data</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QCheckBox" name="cbxRemoveHtmlTables">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>70</y>
|
||||
<width>351</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove HTML tables from CV summary field</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_4">
|
||||
<attribute name="title">
|
||||
@ -360,11 +411,18 @@
|
||||
<rect>
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>246</width>
|
||||
<height>182</height>
|
||||
<width>251</width>
|
||||
<height>192</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Locations to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
|
||||
<property name="text">
|
||||
@ -386,27 +444,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>
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>575</height>
|
||||
<height>621</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -91,23 +91,26 @@
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<width>230</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<width>230</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="margin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblFilename">
|
||||
<property name="sizePolicy">
|
||||
@ -1092,7 +1095,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>21</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
@ -42,17 +42,20 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
@ -102,15 +105,15 @@
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDetails">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>200</height>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="readOnly">
|
@ -5,7 +5,7 @@ Some generic utilities
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -19,9 +19,63 @@ 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:
|
||||
@ -31,10 +85,16 @@ def listToString( l ):
|
||||
string += item
|
||||
return string
|
||||
|
||||
def addtopath( dir ):
|
||||
# TODO only add if not there already
|
||||
if dir is not None and dir != "":
|
||||
os.environ['PATH'] = dir + os.pathsep + os.environ['PATH']
|
||||
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):
|
||||
@ -66,9 +126,13 @@ def removearticles( text ):
|
||||
|
||||
# now get rid of some other junk
|
||||
newText = newText.replace(":", "")
|
||||
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
|
||||
|
94
comictaggerlib/versionchecker.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""
|
||||
Version checker
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import urllib,urllib2
|
||||
import ctversion
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
class pyqtSignal():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
def emit(a,b,c):
|
||||
pass
|
||||
|
||||
class VersionChecker(QObject):
|
||||
|
||||
def getRequestUrl( self, uuid, use_stats ):
|
||||
|
||||
base_url = "http://comictagger1.appspot.com/latest"
|
||||
args = ""
|
||||
|
||||
if use_stats:
|
||||
if platform.system() == "Windows":
|
||||
plat = "win"
|
||||
elif platform.system() == "Linux":
|
||||
plat = "lin"
|
||||
elif platform.system() == "Darwin":
|
||||
plat = "mac"
|
||||
else:
|
||||
plat = "other"
|
||||
args = "?uuid={0}&platform={1}&version={2}".format(uuid, plat, ctversion.version)
|
||||
if not getattr(sys, 'frozen', None):
|
||||
args += "&src=T"
|
||||
|
||||
return base_url+args
|
||||
|
||||
def getLatestVersion( self, uuid, use_stats=True):
|
||||
|
||||
try:
|
||||
resp = urllib2.urlopen( self.getRequestUrl(uuid, use_stats ))
|
||||
new_version = resp.read()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return None
|
||||
return new_version.strip()
|
||||
|
||||
versionRequestComplete = pyqtSignal( str )
|
||||
|
||||
def asyncGetLatestVersion( self, uuid, use_stats ):
|
||||
|
||||
url = self.getRequestUrl( uuid, use_stats )
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncGetLatestVersionComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url))))
|
||||
|
||||
def asyncGetLatestVersionComplete( self, reply ):
|
||||
if (reply.error() != QNetworkReply.NoError):
|
||||
return
|
||||
|
||||
# read in the response
|
||||
new_version = str(reply.readAll())
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return
|
||||
|
||||
self.versionRequestComplete.emit( new_version.strip() )
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select specific series/volume from list
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -87,10 +87,10 @@ class IdentifyThread( QtCore.QThread):
|
||||
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
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(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
|
||||
|
||||
self.imageWidget = CoverImageWidget( self.imageContainer, CoverImageWidget.URLMode )
|
||||
gridlayout = QtGui.QGridLayout( self.imageContainer )
|
||||
@ -108,6 +108,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
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
|
||||
@ -161,10 +162,11 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
md.series = self.series_name
|
||||
md.issue = self.issue_number
|
||||
md.year = self.year
|
||||
md.issueCount = self.issue_count
|
||||
|
||||
self.ii.setAdditionalMetadata( md )
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
print self.cover_index_list
|
||||
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
self.id_thread = IdentifyThread( self.ii )
|
||||
@ -369,7 +371,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
|
||||
self.teDetails.setText ( record['description'] )
|
||||
if record['description'] is None:
|
||||
self.teDetails.setText ( "" )
|
||||
else:
|
||||
self.teDetails.setText ( record['description'] )
|
||||
self.imageWidget.setURL( record['image']['super_url'] )
|
||||
break
|
1
current_version.txt
Normal file
@ -0,0 +1 @@
|
||||
1.1.7-beta
|
@ -1,232 +0,0 @@
|
||||
"""
|
||||
Functions for parsing comic info from filename
|
||||
|
||||
This should probably be re-written, but, well, it mostly works!
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
# Some portions of this code were modified from pyComicMetaThis project
|
||||
# http://code.google.com/p/pycomicmetathis/
|
||||
|
||||
import re
|
||||
import os
|
||||
from urllib import unquote
|
||||
|
||||
class FileNameParser:
|
||||
def fixSpaces( self, string ):
|
||||
placeholders = ['[-_]',' +']
|
||||
for ph in placeholders:
|
||||
string = re.sub(ph, ' ', string )
|
||||
return string.strip()
|
||||
|
||||
# check for silly .1 or .5 style issue strings
|
||||
# allow up to 5 chars total
|
||||
def isPointIssue( self, word ):
|
||||
ret = False
|
||||
try:
|
||||
float(word)
|
||||
if (len(word) < 5 and not word.isdigit()):
|
||||
ret = True
|
||||
except ValueError:
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
def getIssueCount( self,filename ):
|
||||
|
||||
count = ""
|
||||
# replace any name seperators with spaces
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
found = False
|
||||
|
||||
match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE)
|
||||
if match:
|
||||
count = match.group()
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
|
||||
if match:
|
||||
count = match.group()
|
||||
found = True
|
||||
|
||||
|
||||
count = count.lstrip("0")
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def getIssueNumber( self, filename ):
|
||||
|
||||
found = False
|
||||
issue = ''
|
||||
|
||||
# first, look for multiple "--", this mean's it's formatted differently from most:
|
||||
if "--" in filename:
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
|
||||
filename = filename.split("--")[0]
|
||||
elif "___" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
|
||||
filename = filename.split("__")[0]
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
|
||||
# remove parenthetical phrases
|
||||
filename = re.sub( "\(.*\)", "", filename)
|
||||
filename = re.sub( "\[.*\]", "", filename)
|
||||
|
||||
# guess based on position
|
||||
|
||||
# replace any name seperators with spaces
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
word_list = tmpstr.split(' ')
|
||||
|
||||
#before we search, remove any kind of likely "of X" phrase
|
||||
for i in range(0, len(word_list)-2):
|
||||
if ( word_list[i].isdigit() and
|
||||
word_list[i+1] == "of" and
|
||||
word_list[i+2].isdigit() ):
|
||||
word_list[i+1] ="XXX"
|
||||
word_list[i+2] ="XXX"
|
||||
|
||||
|
||||
# first look for the last "#" followed by a digit in the filename. this is almost certainly the issue number
|
||||
#issnum = re.search('#\d+', filename)
|
||||
matchlist = re.findall("#\d+", filename)
|
||||
if len(matchlist) > 0:
|
||||
#get the last item
|
||||
issue = matchlist[ len(matchlist) - 1]
|
||||
issue = issue[1:]
|
||||
found = True
|
||||
|
||||
|
||||
# assume the last number in the filename that is under 4 digits is the issue number
|
||||
if not found:
|
||||
for word in reversed(word_list):
|
||||
if len(word) > 0 and word[0] == "#":
|
||||
word = word[1:]
|
||||
if (
|
||||
(word.isdigit() and len(word) < 4) or
|
||||
(self.isPointIssue(word))
|
||||
):
|
||||
issue = word
|
||||
found = True
|
||||
#print 'Assuming issue number is ' + str(issue) + ' based on the position.'
|
||||
break
|
||||
|
||||
if not found:
|
||||
# try a regex
|
||||
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
|
||||
if issnum:
|
||||
issue = issnum.group()
|
||||
found = True
|
||||
#print 'Got the issue using regex. Issue is ' + issue
|
||||
|
||||
return issue.strip()
|
||||
|
||||
def getSeriesName(self, filename, issue ):
|
||||
|
||||
# use the issue number string to split the filename string
|
||||
# assume first element of list is the series name, plus cruft
|
||||
#!!! this could fail in the case of small numerics in the series name!!!
|
||||
|
||||
# TODO: we really should pass in the *INDEX* of the issue, that makes
|
||||
# finding it easier
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
|
||||
#remove pound signs. this might mess up the series name if there is a# in it.
|
||||
tmpstr = tmpstr.replace("#", " ")
|
||||
|
||||
if issue != "":
|
||||
# assume that issue substr has at least one space before it
|
||||
issue_str = " " + str(issue)
|
||||
series = tmpstr.split(issue_str)[0]
|
||||
else:
|
||||
# no issue to work off of
|
||||
#!!! TODO we should look for the year, and split from that
|
||||
# and if that doesn't exist, remove parenthetical phrases
|
||||
series = tmpstr
|
||||
series = re.sub( "\(.*\)", "", tmpstr)
|
||||
|
||||
volume = ""
|
||||
|
||||
series = series.rstrip("#")
|
||||
|
||||
# search for volume number
|
||||
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
|
||||
if match:
|
||||
series = match.group(1)
|
||||
volume = match.group(3)
|
||||
|
||||
return series.strip(), volume.strip()
|
||||
|
||||
def getYear( self,filename):
|
||||
|
||||
year = ""
|
||||
# look for four digit number with "(" ")" or "--" around it
|
||||
match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
|
||||
if match:
|
||||
year = match.group()
|
||||
# remove non-numerics
|
||||
year = re.sub("[^0-9]", "", year)
|
||||
return year
|
||||
|
||||
def parseFilename( self, filename ):
|
||||
|
||||
# remove the path
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
# remove the extension
|
||||
filename = os.path.splitext(filename)[0]
|
||||
|
||||
#url decode, just in case
|
||||
filename = unquote(filename)
|
||||
|
||||
# sometimes archives get messed up names from too many decodings
|
||||
# often url encodings will break and leave "_28" and "_29" in place
|
||||
# of "(" and ")" see if there are a number of these, and replace them
|
||||
if filename.count("_28") > 1 and filename.count("_29") > 1:
|
||||
filename = filename.replace("_28", "(")
|
||||
filename = filename.replace("_29", ")")
|
||||
|
||||
# ----HACK
|
||||
# remove the first word that word is a 3 digit number.
|
||||
# some story arcs collection packs do this, but it's ugly
|
||||
# this will probably break something, i.e. "100 bullets"
|
||||
#word = filename.split(' ')[0]
|
||||
#if len(word) == 3 and word[0] =='0' and word.isdigit():
|
||||
# filename = filename[4:]
|
||||
# ----HACK -
|
||||
|
||||
self.issue = self.getIssueNumber(filename)
|
||||
self.series, self.volume = self.getSeriesName(filename, self.issue)
|
||||
self.year = self.getYear(filename)
|
||||
self.issue_count = self.getIssueCount(filename)
|
||||
|
||||
if self.issue != "":
|
||||
# strip off leading zeros
|
||||
self.issue = self.issue.lstrip("0")
|
||||
if self.issue == "":
|
||||
self.issue = "0"
|
||||
if self.issue[0] == ".":
|
||||
self.issue = "0" + self.issue
|
||||
|
11
google/gadgets/social.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&width=292&colorscheme=light&show_faces =false&border_color&stream=false&header=false&height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|
10
google/gadgets/twitter.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|
256
google/googlecode_upload.py
Executable file
@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
|
||||
# Author: danderson@google.com (David Anderson)
|
||||
#
|
||||
# Script for uploading files to a Google Code project.
|
||||
#
|
||||
# This is intended to be both a useful script for people who want to
|
||||
# streamline project uploads and a reference implementation for
|
||||
# uploading files to Google Code projects.
|
||||
#
|
||||
# To upload a file to Google Code, you need to provide a path to the
|
||||
# file on your local machine, a small summary of what the file is, a
|
||||
# project name, and a valid account that is a member or owner of that
|
||||
# project. You can optionally provide a list of labels that apply to
|
||||
# the file. The file will be uploaded under the same name that it has
|
||||
# in your local filesystem (that is, the "basename" or last path
|
||||
# component). Run the script with '--help' to get the exact syntax
|
||||
# and available options.
|
||||
#
|
||||
# Note that the upload script requests that you enter your
|
||||
# googlecode.com password. This is NOT your Gmail account password!
|
||||
# This is the password you use on googlecode.com for committing to
|
||||
# Subversion and uploading files. You can find your password by going
|
||||
# to http://code.google.com/hosting/settings when logged in with your
|
||||
# Gmail account. If you have already committed to your project's
|
||||
# Subversion repository, the script will automatically retrieve your
|
||||
# credentials from there (unless disabled, see the output of '--help'
|
||||
# for details).
|
||||
#
|
||||
# If you are looking at this script as a reference for implementing
|
||||
# your own Google Code file uploader, then you should take a look at
|
||||
# the upload() function, which is the meat of the uploader. You
|
||||
# basically need to build a multipart/form-data POST request with the
|
||||
# right fields and send it to https://PROJECT.googlecode.com/files .
|
||||
# Authenticate the request using HTTP Basic authentication, as is
|
||||
# shown below.
|
||||
#
|
||||
# Licensed under the terms of the Apache Software License 2.0:
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Questions, comments, feature requests and patches are most welcome.
|
||||
# Please direct all of these to the Google Code users group:
|
||||
# http://groups.google.com/group/google-code-hosting
|
||||
|
||||
"""Google Code file uploader script.
|
||||
"""
|
||||
|
||||
__author__ = 'danderson@google.com (David Anderson)'
|
||||
|
||||
import httplib
|
||||
import os.path
|
||||
import optparse
|
||||
import getpass
|
||||
import base64
|
||||
import sys
|
||||
|
||||
|
||||
def upload(file, project_name, user_name, password, summary, labels=None):
|
||||
"""Upload a file to a Google Code project's file server.
|
||||
|
||||
Args:
|
||||
file: The local path to the file.
|
||||
project_name: The name of your project on Google Code.
|
||||
user_name: Your Google account name.
|
||||
password: The googlecode.com password for your account.
|
||||
Note that this is NOT your global Google Account password!
|
||||
summary: A small description for the file.
|
||||
labels: an optional list of label strings with which to tag the file.
|
||||
|
||||
Returns: a tuple:
|
||||
http_status: 201 if the upload succeeded, something else if an
|
||||
error occured.
|
||||
http_reason: The human-readable string associated with http_status
|
||||
file_url: If the upload succeeded, the URL of the file on Google
|
||||
Code, None otherwise.
|
||||
"""
|
||||
# The login is the user part of user@gmail.com. If the login provided
|
||||
# is in the full user@domain form, strip it down.
|
||||
if user_name.endswith('@gmail.com'):
|
||||
user_name = user_name[:user_name.index('@gmail.com')]
|
||||
|
||||
form_fields = [('summary', summary)]
|
||||
if labels is not None:
|
||||
form_fields.extend([('label', l.strip()) for l in labels])
|
||||
|
||||
content_type, body = encode_upload_request(form_fields, file)
|
||||
|
||||
upload_host = '%s.googlecode.com' % project_name
|
||||
upload_uri = '/files'
|
||||
auth_token = base64.b64encode('%s:%s'% (user_name, password))
|
||||
headers = {
|
||||
'Authorization': 'Basic %s' % auth_token,
|
||||
'User-Agent': 'Googlecode.com uploader v0.9.4',
|
||||
'Content-Type': content_type,
|
||||
}
|
||||
|
||||
server = httplib.HTTPSConnection(upload_host)
|
||||
server.request('POST', upload_uri, body, headers)
|
||||
resp = server.getresponse()
|
||||
server.close()
|
||||
|
||||
if resp.status == 201:
|
||||
location = resp.getheader('Location', None)
|
||||
else:
|
||||
location = None
|
||||
return resp.status, resp.reason, location
|
||||
|
||||
|
||||
def encode_upload_request(fields, file_path):
|
||||
"""Encode the given fields and file into a multipart form body.
|
||||
|
||||
fields is a sequence of (name, value) pairs. file is the path of
|
||||
the file to upload. The file will be uploaded to Google Code with
|
||||
the same file name.
|
||||
|
||||
Returns: (content_type, body) ready for httplib.HTTP instance
|
||||
"""
|
||||
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
|
||||
CRLF = '\r\n'
|
||||
|
||||
body = []
|
||||
|
||||
# Add the metadata about the upload first
|
||||
for key, value in fields:
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
'',
|
||||
value,
|
||||
])
|
||||
|
||||
# Now add the file itself
|
||||
file_name = os.path.basename(file_path)
|
||||
f = open(file_path, 'rb')
|
||||
file_content = f.read()
|
||||
f.close()
|
||||
|
||||
body.extend(
|
||||
['--' + BOUNDARY,
|
||||
'Content-Disposition: form-data; name="filename"; filename="%s"'
|
||||
% file_name,
|
||||
# The upload server determines the mime-type, no need to set it.
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
file_content,
|
||||
])
|
||||
|
||||
# Finalize the form body
|
||||
body.extend(['--' + BOUNDARY + '--', ''])
|
||||
|
||||
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
|
||||
|
||||
|
||||
def upload_find_auth(file_path, project_name, summary, labels=None,
|
||||
user_name=None, password=None, tries=3):
|
||||
"""Find credentials and upload a file to a Google Code project's file server.
|
||||
|
||||
file_path, project_name, summary, and labels are passed as-is to upload.
|
||||
|
||||
Args:
|
||||
file_path: The local path to the file.
|
||||
project_name: The name of your project on Google Code.
|
||||
summary: A small description for the file.
|
||||
labels: an optional list of label strings with which to tag the file.
|
||||
config_dir: Path to Subversion configuration directory, 'none', or None.
|
||||
user_name: Your Google account name.
|
||||
tries: How many attempts to make.
|
||||
"""
|
||||
if user_name is None or password is None:
|
||||
from netrc import netrc
|
||||
authenticators = netrc().authenticators("code.google.com")
|
||||
if authenticators:
|
||||
if user_name is None:
|
||||
user_name = authenticators[0]
|
||||
if password is None:
|
||||
password = authenticators[2]
|
||||
|
||||
while tries > 0:
|
||||
if user_name is None:
|
||||
# Read username if not specified or loaded from svn config, or on
|
||||
# subsequent tries.
|
||||
sys.stdout.write('Please enter your googlecode.com username: ')
|
||||
sys.stdout.flush()
|
||||
user_name = sys.stdin.readline().rstrip()
|
||||
if password is None:
|
||||
# Read password if not loaded from svn config, or on subsequent tries.
|
||||
print 'Please enter your googlecode.com password.'
|
||||
print '** Note that this is NOT your Gmail account password! **'
|
||||
print 'It is the password you use to access Subversion repositories,'
|
||||
print 'and can be found here: http://code.google.com/hosting/settings'
|
||||
password = getpass.getpass()
|
||||
|
||||
status, reason, url = upload(file_path, project_name, user_name, password,
|
||||
summary, labels)
|
||||
# Returns 403 Forbidden instead of 401 Unauthorized for bad
|
||||
# credentials as of 2007-07-17.
|
||||
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
|
||||
# Rest for another try.
|
||||
user_name = password = None
|
||||
tries = tries - 1
|
||||
else:
|
||||
# We're done.
|
||||
break
|
||||
|
||||
return status, reason, url
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
|
||||
'-p PROJECT [options] FILE')
|
||||
parser.add_option('-s', '--summary', dest='summary',
|
||||
help='Short description of the file')
|
||||
parser.add_option('-p', '--project', dest='project',
|
||||
help='Google Code project name')
|
||||
parser.add_option('-u', '--user', dest='user',
|
||||
help='Your Google Code username')
|
||||
parser.add_option('-w', '--password', dest='password',
|
||||
help='Your Google Code password')
|
||||
parser.add_option('-l', '--labels', dest='labels',
|
||||
help='An optional list of comma-separated labels to attach '
|
||||
'to the file')
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if not options.summary:
|
||||
parser.error('File summary is missing.')
|
||||
elif not options.project:
|
||||
parser.error('Project name is missing.')
|
||||
elif len(args) < 1:
|
||||
parser.error('File to upload not provided.')
|
||||
elif len(args) > 1:
|
||||
parser.error('Only one file may be specified.')
|
||||
|
||||
file_path = args[0]
|
||||
|
||||
if options.labels:
|
||||
labels = options.labels.split(',')
|
||||
else:
|
||||
labels = None
|
||||
|
||||
status, reason, url = upload_find_auth(file_path, options.project,
|
||||
options.summary, labels,
|
||||
options.user, options.password)
|
||||
if url:
|
||||
print 'The file was uploaded successfully.'
|
||||
print 'URL: %s' % url
|
||||
return 0
|
||||
else:
|
||||
print 'An error occurred. Your file was not uploaded.'
|
||||
print 'Google Code upload server said: %s (%s)' % (reason, status)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
@ -1,123 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogIssueSelect</class>
|
||||
<widget class="QDialog" name="dialogIssueSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>657</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Issue</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Issue</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="coverImageContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<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>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|