Compare commits
80 Commits
1.0.0-beta
...
1.1.0-beta
Author | SHA1 | Date | |
---|---|---|---|
85728d33bb | |||
2ade08aa89 | |||
50909962d3 | |||
cc02023730 | |||
5bdc40b9f5 | |||
4f3e63db07 | |||
b8893b853f | |||
6da6f38673 | |||
369dcbb5a1 | |||
ec010f29e8 | |||
22867bc9e6 | |||
dde1913e07 | |||
5b5842a5f8 | |||
fbf086886f | |||
c1ff6c4b26 | |||
99b110d052 | |||
3df498eed4 | |||
b5ab2a6ac9 | |||
5c91960f04 | |||
3b52fd3213 | |||
9366457b88 | |||
1cb7ef66db | |||
ee6a05deae | |||
c978883584 | |||
9b5508ecba | |||
8e1c6fae7c | |||
59e662f5a7 | |||
6486d97ee3 | |||
8c088440c5 | |||
320ee1c5d1 | |||
e123720354 | |||
d39d4e79ad | |||
8d7eeece30 | |||
3b64e1a3ec | |||
81ae9bd635 | |||
27846772e9 | |||
baf697b919 | |||
59ede8d446 | |||
8b748a3343 | |||
75471aaddc | |||
7225f261f1 | |||
c466264d43 | |||
14e801b717 | |||
af4b467814 | |||
1b3feaa167 | |||
2526fa0ca8 | |||
a878d36dcf | |||
90de6433b6 | |||
d9abc364f1 | |||
e542b6df1f | |||
a7a6b085f1 | |||
0078f76e8c | |||
1a01cb60d9 | |||
0f81ce4c24 | |||
c4ef4137d0 | |||
cdc6d71356 | |||
2357a6378e | |||
9503d0fef4 | |||
c46dda4540 | |||
894c23f64f | |||
9360fa954c | |||
74408e56fd | |||
dd8e54fa6b | |||
b378840878 | |||
c0a6406dc9 | |||
df3544e734 | |||
d40de5b67e | |||
25b63dfc65 | |||
70f50c8595 | |||
e9aba4e119 | |||
53aca0ee08 | |||
9aa41823b4 | |||
8d4a336b50 | |||
3c96e68fde | |||
93f316b820 | |||
ccde71f9d0 | |||
7186c6792a | |||
e8961ed299 | |||
1f050436d3 | |||
79a9cf1b40 |
9
Makefile
9
Makefile
@ -6,12 +6,13 @@ all: clean
|
||||
clean:
|
||||
rm -f *~ *.pyc *.pyo
|
||||
rm -f logdict*.log
|
||||
|
||||
|
||||
make -C mac clean
|
||||
make -C windows clean
|
||||
|
||||
zip:
|
||||
cd release; \
|
||||
rm -rf *zip comictagger-src-$(VERSION_STR) ; \
|
||||
svn checkout https://comictagger.googlecode.com/svn/trunk/ 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)
|
||||
|
||||
@ -21,4 +22,4 @@ zip:
|
||||
svn_tag:
|
||||
svn copy https://comictagger.googlecode.com/svn/trunk \
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
||||
|
||||
|
@ -120,7 +120,9 @@ class RarFileImplementation(object):
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
data['filename'] = accum[0].strip()
|
||||
#!!!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]
|
||||
|
@ -26,6 +26,10 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from options import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
|
||||
class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
@ -36,9 +40,25 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip"))
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
gridlayout.addWidget( self.altCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
|
||||
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Next")
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Write Tags")
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
@ -56,18 +76,18 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
self.current_match_set = self.match_set_list[ self.current_match_set_idx ]
|
||||
|
||||
|
||||
if self.current_match_set_idx + 1 == len( self.match_set_list ):
|
||||
self.skipButton.setDisabled(True)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
#self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.skipButton.setText(self.tr("Skip"))
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.current_row = 0
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle( "Select correct match ({0} of {1}): {2}".format(
|
||||
self.setWindowTitle( u"Select correct match or skip ({0} of {1}): {2}".format(
|
||||
self.current_match_set_idx+1,
|
||||
len( self.match_set_list ),
|
||||
os.path.split(path)[1] ))
|
||||
@ -85,6 +105,8 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
@ -93,21 +115,37 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
else:
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = ""
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
item_text = u"{0}/".format(match['month'])
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
item_text += u"{0}".format(match['year'])
|
||||
else:
|
||||
item_text += u"????"
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match['issue_title']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
@ -119,34 +157,18 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.current_row = curr.row()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
|
||||
self.cover_fetcher.fetch( self.current_match_set.matches[self.current_row]['img_url'] )
|
||||
|
||||
# called when the image is done loading
|
||||
def coverFetchComplete( self, image_data, issue_id ):
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
|
||||
|
||||
def setCoverImage( self ):
|
||||
ca = self.current_match_set.ca
|
||||
cover_idx = ca.readMetadata(self.style).getCoverPageIndexList()[0]
|
||||
image_data = ca.getPage( cover_idx )
|
||||
self.labelCover.setScaledContents(True)
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelCover.setPixmap(QtGui.QPixmap(img))
|
||||
else:
|
||||
self.labelCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.archiveCoverWidget.setArchive(ca)
|
||||
|
||||
def currentMatch( self ):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
|
||||
return match
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.saveMatch()
|
||||
@ -180,7 +202,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
def saveMatch( self ):
|
||||
|
||||
match = self.current_match_set.matches[self.current_row]
|
||||
match = self.currentMatch()
|
||||
ca = self.current_match_set.ca
|
||||
|
||||
md = ca.readMetadata( self.style )
|
||||
@ -196,6 +218,8 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay( cv_md )
|
||||
success = ca.writeMetadata( md, self.style )
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>831</width>
|
||||
<height>506</height>
|
||||
<width>907</width>
|
||||
<height>507</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -18,32 +18,27 @@
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelCover">
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</height>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
@ -54,7 +49,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
@ -77,34 +72,27 @@
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<widget class="QWidget" name="altCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</height>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -22,7 +22,7 @@ import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
import utils
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
@ -34,13 +34,12 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
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' )))
|
||||
self.isdone = False
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# make font for scroll window a smidge smaller
|
||||
f = self.textEdit.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
self.textEdit.setFont( f )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
|
@ -35,21 +35,70 @@ class AutoTagStartWindow(QtGui.QDialog):
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxSaveOnLowConfidence.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAssumeIssueOne.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxRemoveAfterSuccess.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxSpecifySearchString.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.leNameLengthMatchTolerance.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.leSearchString.setEnabled( False )
|
||||
|
||||
nlmtTip = (
|
||||
""" <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The higher
|
||||
it is, the more likely to have a good match, but each search will take longer and
|
||||
use more bandwidth. Too low, and only the very closest lexical matches will be
|
||||
explored.</html>""" )
|
||||
|
||||
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
|
||||
|
||||
ssTip = (
|
||||
"""<html>
|
||||
The <b>series search string</b> specifies the search string to be used for all selected archives.
|
||||
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
|
||||
should be from the same series.
|
||||
</html>"""
|
||||
)
|
||||
self.leSearchString.setToolTip(ssTip)
|
||||
self.cbxSpecifySearchString.setToolTip(ssTip)
|
||||
|
||||
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthMatchTolerance.setValidator(validator)
|
||||
|
||||
self.cbxSpecifySearchString.stateChanged.connect(self.searchStringToggle)
|
||||
|
||||
self.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
self.ignoreLeadingDigitsInFilename = False
|
||||
self.removeAfterSuccess = False
|
||||
self.searchString = None
|
||||
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
|
||||
|
||||
def searchStringToggle(self):
|
||||
enable = self.cbxSpecifySearchString.isChecked()
|
||||
self.leSearchString.setEnabled( enable )
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
|
||||
|
||||
self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
|
||||
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
|
||||
self.nameLengthMatchTolerance = int(self.leNameLengthMatchTolerance.text())
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
||||
self.searchString = None
|
||||
|
||||
|
@ -9,10 +9,16 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>524</width>
|
||||
<height>248</height>
|
||||
<width>607</width>
|
||||
<height>319</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Auto-Tag</string>
|
||||
</property>
|
||||
@ -40,20 +46,16 @@
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
</property>
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -63,13 +65,129 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxIgnoreLeadingDigitsInFilename">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Ignore leading (sequence) numbers in filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxRemoveAfterSuccess">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove archives from list after successful tagging</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSpecifySearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Specify series search string for all selected archives</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="leSearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QLineEdit" name="leNameLengthMatchTolerance">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Adjust Name Length Match Tolerance:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
123
comicarchive.py
123
comicarchive.py
@ -46,7 +46,7 @@ from comicbookinfo import ComicBookInfo
|
||||
from comet import CoMet
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from filenameparser import FileNameParser
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class ZipArchiver:
|
||||
|
||||
@ -67,10 +67,14 @@ class ZipArchiver:
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
try:
|
||||
data = zf.read( archive_file )
|
||||
except zipfile.BadZipfile:
|
||||
print "bad zipfile: {0} :: {1}".format(self.path, archive_file)
|
||||
except Exception:
|
||||
print "bad zipfile: {0} :: {1}".format(self.path, archive_file)
|
||||
except zipfile.BadZipfile as e:
|
||||
print >> sys.stderr, "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)
|
||||
raise IOError
|
||||
finally:
|
||||
zf.close()
|
||||
return data
|
||||
@ -108,7 +112,7 @@ class ZipArchiver:
|
||||
def rebuildZipFile( self, exclude_list ):
|
||||
|
||||
# this recompresses the zip archive, without the files in the exclude_list
|
||||
#print "Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||
#print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||
|
||||
# generate temp file
|
||||
tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) )
|
||||
@ -200,7 +204,7 @@ class ZipArchiver:
|
||||
try:
|
||||
zout = zipfile.ZipFile (self.path, 'w')
|
||||
for fname in otherArchive.getArchiveFilenameList():
|
||||
data = otherArchive.readArchiveFile( fname )
|
||||
data = otherArchive.readArchiveFile( fname )
|
||||
if data is not None:
|
||||
zout.writestr( fname, data )
|
||||
zout.close()
|
||||
@ -211,7 +215,7 @@ class ZipArchiver:
|
||||
if not self.writeZipComment( self.path, comment ):
|
||||
return False
|
||||
except Exception as e:
|
||||
print "Error while copying to {0}: {1}".format(self.path, e)
|
||||
print >> sys.stderr, "Error while copying to {0}: {1}".format(self.path, e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@ -284,27 +288,34 @@ class RarArchiver:
|
||||
rarc = self.getRARObj()
|
||||
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries+1
|
||||
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(
|
||||
entries[0][0].size,len(entries[0][1]), self.path, archive_file, tries)
|
||||
continue
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print e, "in readArchiveFile! try %s" % tries
|
||||
print >> sys.stderr, "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print "Unexpected exception in readArchiveFile! {0}".format( e )
|
||||
print >> sys.stderr, "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)
|
||||
if (len(entries) == 1):
|
||||
return entries[0][1]
|
||||
else:
|
||||
return None
|
||||
raise IOError
|
||||
|
||||
return None
|
||||
raise IOError
|
||||
|
||||
|
||||
|
||||
@ -364,13 +375,17 @@ class RarArchiver:
|
||||
#return namelist
|
||||
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries+1
|
||||
namelist = [ item.filename for item in rarc.infolist() ]
|
||||
#namelist = [ item.filename for item in rarc.infolist() ]
|
||||
namelist = []
|
||||
for item in rarc.infolist():
|
||||
if item.size != 0:
|
||||
namelist.append( item.filename )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print e, "in getArchiveFilenameList! try %s" % tries
|
||||
print >> sys.stderr, "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -382,13 +397,13 @@ class RarArchiver:
|
||||
|
||||
def getRARObj( self ):
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries+1
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print e, "in getRARObj! try %s" % tries
|
||||
print >> sys.stderr, "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -471,7 +486,7 @@ class UnknownArchiver:
|
||||
return ""
|
||||
def setArchiveComment( self, comment ):
|
||||
return False
|
||||
def readArchiveFilen( self ):
|
||||
def readArchiveFile( self ):
|
||||
return ""
|
||||
def writeArchiveFile( self, archive_file, data ):
|
||||
return False
|
||||
@ -482,7 +497,9 @@ class UnknownArchiver:
|
||||
|
||||
#------------------------------------------------------------------
|
||||
class ComicArchive:
|
||||
|
||||
|
||||
logo_data = None
|
||||
|
||||
class ArchiveType:
|
||||
Zip, Rar, Folder, Unknown = range(4)
|
||||
|
||||
@ -507,6 +524,11 @@ class ComicArchive:
|
||||
self.archive_type = self.ArchiveType.Unknown
|
||||
self.archiver = UnknownArchiver( self.path )
|
||||
|
||||
if ComicArchive.logo_data is None:
|
||||
fname = os.path.join(ComicTaggerSettings.baseDir(), 'graphics','nocover.png' )
|
||||
with open(fname, 'rb') as fd:
|
||||
ComicArchive.logo_data = fd.read()
|
||||
|
||||
# Clears the cached data
|
||||
def resetCache( self ):
|
||||
self.has_cix = None
|
||||
@ -518,7 +540,11 @@ class ComicArchive:
|
||||
self.cix_md = None
|
||||
self.cbi_md = None
|
||||
self.comet_md = None
|
||||
|
||||
|
||||
def loadCache( self, style_list ):
|
||||
for style in style_list:
|
||||
self.readMetadata(style)
|
||||
|
||||
def rename( self, path ):
|
||||
self.path = path
|
||||
self.archiver.path = path
|
||||
@ -637,7 +663,11 @@ class ComicArchive:
|
||||
filename = self.getPageName( index )
|
||||
|
||||
if filename is not None:
|
||||
image_data = self.archiver.readArchiveFile( filename )
|
||||
try:
|
||||
image_data = self.archiver.readArchiveFile( filename )
|
||||
except IOError:
|
||||
print >> sys.stderr, "Error reading in page. Substituting logo page."
|
||||
image_data = ComicArchive.logo_data
|
||||
|
||||
return image_data
|
||||
|
||||
@ -664,7 +694,7 @@ class ComicArchive:
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
for name in files:
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] and os.path.basename(name)[0] != "." ):
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif" ] and os.path.basename(name)[0] != "." ):
|
||||
self.page_list.append(name)
|
||||
|
||||
return self.page_list
|
||||
@ -713,8 +743,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cbi = True
|
||||
self.cbi_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
@ -725,15 +754,14 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cbi = False
|
||||
self.cbi_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
def readCIX( self ):
|
||||
if self.cix_md is None:
|
||||
raw_cix = self.readRawCIX()
|
||||
if raw_cix is None:
|
||||
if raw_cix is None or raw_cix == "":
|
||||
self.cix_md = GenericMetadata()
|
||||
else:
|
||||
self.cix_md = ComicInfoXml().metadataFromString( raw_cix )
|
||||
@ -753,8 +781,12 @@ class ComicArchive:
|
||||
def readRawCIX( self ):
|
||||
if not self.hasCIX():
|
||||
return None
|
||||
|
||||
return self.archiver.readArchiveFile( self.ci_xml_filename )
|
||||
try:
|
||||
raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename )
|
||||
except IOError:
|
||||
print "Error reading in raw CIX!"
|
||||
raw_cix = ""
|
||||
return raw_cix
|
||||
|
||||
def writeCIX(self, metadata):
|
||||
|
||||
@ -765,8 +797,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cix = True
|
||||
self.cix_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
@ -777,8 +808,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cix = False
|
||||
self.cix_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
@ -798,7 +828,7 @@ class ComicArchive:
|
||||
def readCoMet( self ):
|
||||
if self.comet_md is None:
|
||||
raw_comet = self.readRawCoMet()
|
||||
if raw_comet is None:
|
||||
if raw_comet is None or raw_comet == "":
|
||||
self.comet_md = GenericMetadata()
|
||||
else:
|
||||
self.comet_md = CoMet().metadataFromString( raw_comet )
|
||||
@ -821,10 +851,15 @@ class ComicArchive:
|
||||
|
||||
def readRawCoMet( self ):
|
||||
if not self.hasCoMet():
|
||||
print self.path, "doesn't have CoMet data!"
|
||||
print >> sys.stderr, self.path, "doesn't have CoMet data!"
|
||||
return None
|
||||
|
||||
return self.archiver.readArchiveFile( self.comet_filename )
|
||||
try:
|
||||
raw_comet = self.archiver.readArchiveFile( self.comet_filename )
|
||||
except IOError:
|
||||
print >> sys.stderr, "Error reading in raw CoMet!"
|
||||
raw_comet = ""
|
||||
return raw_comet
|
||||
|
||||
def writeCoMet(self, metadata):
|
||||
|
||||
@ -843,8 +878,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_comet = True
|
||||
self.comet_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
@ -855,8 +889,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_comet = False
|
||||
self.comet_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
@ -871,7 +904,11 @@ class ComicArchive:
|
||||
if ( os.path.dirname(n) == "" and
|
||||
os.path.splitext(n)[1].lower() == '.xml'):
|
||||
# read in XML file, and validate it
|
||||
data = self.archiver.readArchiveFile( n )
|
||||
try:
|
||||
data = self.archiver.readArchiveFile( n )
|
||||
except:
|
||||
data = ""
|
||||
print >> sys.stderr, "Error reading in Comet XML for validation!"
|
||||
if CoMet().validateString( data ):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
|
@ -115,7 +115,7 @@ class ComicBookInfo:
|
||||
#helper func
|
||||
def toInt(s):
|
||||
i = None
|
||||
if type(s) == str or type(s) == int:
|
||||
if type(s) in [ str, unicode, int ]:
|
||||
try:
|
||||
i = int(s)
|
||||
except ValueError:
|
||||
|
207
comictagger.py
207
comictagger.py
@ -62,8 +62,10 @@ class OnlineMatchResults():
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.writeFailures = []
|
||||
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
@ -72,7 +74,7 @@ def actual_issue_data_fetch( match, settings ):
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
@ -85,18 +87,50 @@ def actual_metadata_save( ca, opts, md ):
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print "The tag save seemed to fail!"
|
||||
print >> sys.stderr,"The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print "Save complete."
|
||||
print >> sys.stderr,"Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print "dry-run option was set, so nothing was written"
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written"
|
||||
else:
|
||||
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
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
|
||||
@ -119,44 +153,37 @@ def post_process_matches( match_results, opts, settings ):
|
||||
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:
|
||||
#jusr quit if we're not interactive or showing the summary
|
||||
#just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nMultiple matches:"
|
||||
print "\nArchives with multiple high-confidence matches:"
|
||||
print "------------------"
|
||||
for mm in match_results.multipleMatches:
|
||||
print mm.filename
|
||||
for (counter,m) in enumerate(mm.matches):
|
||||
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(len(mm.matches))) or i == 's':
|
||||
break
|
||||
if i != 's':
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = ComicArchive( mm.filename )
|
||||
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
|
||||
cv_md = actual_issue_data_fetch(mm.matches[int(i)], settings)
|
||||
md.overlay( cv_md )
|
||||
actual_metadata_save( ca, opts, md )
|
||||
|
||||
|
||||
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 "You must specify at least one filename. Use the -h option for more info"
|
||||
print >> sys.stderr,"You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
@ -195,13 +222,17 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
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 "Sorry, but "+ filename + " is not a comic archive!"
|
||||
print >> sys.stderr,"Sorry, but "+ filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
|
||||
print "This archive is not writable for that tag type"
|
||||
print >> sys.stderr,"This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [ False, False, False ]
|
||||
@ -314,7 +345,7 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print u"Processing {0}: ".format(filename)
|
||||
print u"Processing {0}...".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
|
||||
@ -325,12 +356,14 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
print >> sys.stderr,"Network error while getting issue details. Save aborted"
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
if cv_md is None:
|
||||
print "No match for ID {0} was found.".format(opts.issue_id)
|
||||
return 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()
|
||||
@ -338,7 +371,8 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "No metadata given to search online with!"
|
||||
print >> sys.stderr,"No metadata given to search online with!"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
@ -374,15 +408,20 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
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 "Online search: Low confidence match. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
print >> sys.stderr,"Online search: Low confidence match. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
print >> sys.stderr,"Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
@ -392,6 +431,7 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
# 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 )
|
||||
@ -416,7 +456,7 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print msg_hdr + "Can't rename without series name"
|
||||
print >> sys.stderr, msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
new_ext = None # default
|
||||
@ -434,7 +474,7 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print msg_hdr + "Filename is already good!"
|
||||
print >> sys.stderr, msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
@ -449,15 +489,70 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
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
|
||||
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
|
||||
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()
|
||||
@ -470,7 +565,7 @@ def main():
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print "QT is not available."
|
||||
print >> sys.stderr, "QT is not available."
|
||||
|
||||
if opts.no_gui:
|
||||
cli_mode( opts, settings )
|
||||
|
@ -27,6 +27,7 @@ import datetime
|
||||
|
||||
import ctversion
|
||||
from settings import ComicTaggerSettings
|
||||
import utils
|
||||
|
||||
class ComicVineCacher:
|
||||
|
||||
@ -97,6 +98,13 @@ class ComicVineCacher:
|
||||
"PRIMARY KEY (id) )"
|
||||
)
|
||||
|
||||
cur.execute("CREATE TABLE AltCovers(" +
|
||||
"issue_id INT," +
|
||||
"url_list TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (issue_id) )"
|
||||
)
|
||||
|
||||
cur.execute("CREATE TABLE Issues(" +
|
||||
"id INT," +
|
||||
"volume_id INT," +
|
||||
@ -108,6 +116,7 @@ class ComicVineCacher:
|
||||
"thumb_image_hash TEXT," +
|
||||
"publish_month TEXT," +
|
||||
"publish_year TEXT," +
|
||||
"site_detail_url TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id ) )"
|
||||
)
|
||||
@ -149,7 +158,7 @@ class ComicVineCacher:
|
||||
url,
|
||||
record['description'])
|
||||
)
|
||||
|
||||
|
||||
def get_search_results( self, search_term ):
|
||||
|
||||
results = list()
|
||||
@ -184,7 +193,52 @@ class ComicVineCacher:
|
||||
|
||||
return results
|
||||
|
||||
def add_alt_covers( self, issue_id, url_list ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [ issue_id ])
|
||||
|
||||
url_list_str = utils.listToString(url_list)
|
||||
# now add in new record
|
||||
cur.execute("INSERT INTO AltCovers " +
|
||||
"(issue_id, url_list ) " +
|
||||
"VALUES( ?, ? )" ,
|
||||
( issue_id,
|
||||
url_list_str)
|
||||
)
|
||||
|
||||
|
||||
def get_alt_covers( self, issue_id ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change much....
|
||||
a_month_ago = datetime.datetime.today()-datetime.timedelta(days=30)
|
||||
cur.execute( "DELETE FROM AltCovers WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
if row is None :
|
||||
return None
|
||||
else:
|
||||
url_list_str = row[0]
|
||||
if len(url_list_str) == 0:
|
||||
return []
|
||||
raw_list = url_list_str.split(",")
|
||||
url_list = []
|
||||
for item in raw_list:
|
||||
url_list.append( str(item).strip())
|
||||
return url_list
|
||||
|
||||
def add_volume_info( self, cv_volume_record ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
@ -275,7 +329,7 @@ class ComicVineCacher:
|
||||
return result
|
||||
|
||||
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, publish_month, publish_year ):
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, publish_month, publish_year, site_detail_url ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
@ -289,6 +343,7 @@ class ComicVineCacher:
|
||||
"thumb_image_url": thumb_image_url,
|
||||
"publish_month": publish_month,
|
||||
"publish_year": publish_year,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "issues" , "id", issue_id, data)
|
||||
@ -302,13 +357,25 @@ class ComicVineCacher:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year FROM Issues WHERE id=?", [ issue_id ])
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year,site_detail_url FROM Issues WHERE id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
|
||||
details = dict()
|
||||
if row[0] is None :
|
||||
return None, None, None, None
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['publish_month'] = None
|
||||
details['publish_year'] = None
|
||||
details['site_detail_url'] = None
|
||||
|
||||
else:
|
||||
return row[0],row[1],row[2],row[3]
|
||||
details['image_url'] = row[0]
|
||||
details['thumb_image_url'] = row[1]
|
||||
details['publish_month'] = row[2]
|
||||
details['publish_year'] = row[3]
|
||||
details['site_detail_url'] = row[4]
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def upsert( self, cur, tablename, pkname, pkval, data):
|
||||
|
@ -26,6 +26,8 @@ import math
|
||||
import re
|
||||
import datetime
|
||||
import ctversion
|
||||
import sys
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
@ -59,6 +61,18 @@ class ComicVineTalker(QObject):
|
||||
# key that is registered to comictagger
|
||||
self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
|
||||
|
||||
self.log_func = None
|
||||
|
||||
def setLogFunc( self , log_func ):
|
||||
self.log_func = log_func
|
||||
|
||||
def writeLog( self , text ):
|
||||
if self.log_func is None:
|
||||
#sys.stdout.write(text.encode( errors='replace') )
|
||||
#sys.stdout.flush()
|
||||
print >> sys.stderr, text
|
||||
else:
|
||||
self.log_func( text )
|
||||
|
||||
def testKey( self ):
|
||||
|
||||
@ -76,7 +90,7 @@ class ComicVineTalker(QObject):
|
||||
resp = urllib2.urlopen( url )
|
||||
return resp.read()
|
||||
except Exception as e:
|
||||
print e
|
||||
self.writeLog( str(e) )
|
||||
raise ComicVineTalkerException("Network Error!")
|
||||
|
||||
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
|
||||
@ -104,7 +118,7 @@ class ComicVineTalker(QObject):
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
|
||||
search_results = list()
|
||||
@ -116,7 +130,7 @@ class ComicVineTalker(QObject):
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
|
||||
if callback is None:
|
||||
print ("Found {0} of {1} results".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
self.writeLog( "Found {0} of {1} results\n".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
search_results.extend( cv_response['results'])
|
||||
offset = 0
|
||||
|
||||
@ -126,14 +140,14 @@ class ComicVineTalker(QObject):
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
if callback is None:
|
||||
print ("getting another page of results {0} of {1}...".format( current_result_count, total_result_count))
|
||||
self.writeLog("getting another page of results {0} of {1}...\n".format( current_result_count, total_result_count))
|
||||
offset += limit
|
||||
content = self.getUrlContent(search_url + "&offset="+str(offset))
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
search_results.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
@ -170,7 +184,7 @@ class ComicVineTalker(QObject):
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
|
||||
volume_results = cv_response['results']
|
||||
@ -188,7 +202,7 @@ class ComicVineTalker(QObject):
|
||||
for record in volume_results['issues']:
|
||||
if IssueString(issue_number).asFloat() is None:
|
||||
issue_number = 1
|
||||
if float(record['issue_number']) == float(issue_number):
|
||||
if float(record['issue_number']) == IssueString(issue_number).asFloat():
|
||||
found = True
|
||||
break
|
||||
|
||||
@ -198,7 +212,7 @@ class ComicVineTalker(QObject):
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
issue_results = cv_response['results']
|
||||
|
||||
@ -214,7 +228,7 @@ class ComicVineTalker(QObject):
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
|
||||
issue_results = cv_response['results']
|
||||
@ -311,36 +325,55 @@ class ComicVineTalker(QObject):
|
||||
return newstring
|
||||
|
||||
def fetchIssueDate( self, issue_id ):
|
||||
image_url, thumb_url, month,year = self.fetchIssueSelectDetails( issue_id )
|
||||
return month, year
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['publish_month'], details['publish_year']
|
||||
|
||||
def fetchIssueCoverURLs( self, issue_id ):
|
||||
image_url, thumb_url, month,year = self.fetchIssueSelectDetails( issue_id )
|
||||
return image_url, thumb_url
|
||||
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['image_url'], details['thumb_image_url']
|
||||
|
||||
def fetchIssuePageURL( self, issue_id ):
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['site_detail_url']
|
||||
|
||||
def fetchIssueSelectDetails( self, issue_id ):
|
||||
|
||||
cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if cached_image_url is not None:
|
||||
return cached_image_url,cached_thumb_url, cached_month, cached_year
|
||||
#cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
cached_details = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if cached_details['image_url'] is not None:
|
||||
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"
|
||||
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"
|
||||
|
||||
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['site_detail_url'] = None
|
||||
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return None, None,None,None
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return details
|
||||
|
||||
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']
|
||||
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['site_detail_url'] = cv_response['results']['site_detail_url']
|
||||
|
||||
if image_url is not None:
|
||||
self.cacheIssueSelectDetails( issue_id, image_url,thumb_url, month, year )
|
||||
return image_url,thumb_url,month,year
|
||||
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['site_detail_url'] )
|
||||
#print details['site_detail_url']
|
||||
return details
|
||||
|
||||
def fetchCachedIssueSelectDetails( self, issue_id ):
|
||||
|
||||
@ -349,23 +382,71 @@ class ComicVineTalker(QObject):
|
||||
cvc = ComicVineCacher( )
|
||||
return cvc.get_issue_select_details( issue_id )
|
||||
|
||||
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, month, year ):
|
||||
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, month, year, page_url ):
|
||||
cvc = ComicVineCacher( )
|
||||
cvc.add_issue_select_details( issue_id, image_url, thumb_url, month, year )
|
||||
cvc.add_issue_select_details( issue_id, image_url, thumb_url, month, year, page_url )
|
||||
|
||||
|
||||
def fetchAlternateCoverURLs(self, issue_id):
|
||||
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()
|
||||
alt_cover_url_list = self.parseOutAltCoverUrls( content)
|
||||
|
||||
# cache this alt cover URL list
|
||||
self.cacheAlternateCoverURLs( issue_id, alt_cover_url_list )
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
def parseOutAltCoverUrls( self, page_html ):
|
||||
soup = BeautifulSoup( page_html )
|
||||
|
||||
alt_cover_url_list = []
|
||||
|
||||
# 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')
|
||||
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'] )
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
def fetchCachedAlternateCoverURLs( self, issue_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
url_list = cvc.get_alt_covers( issue_id )
|
||||
if url_list is not None:
|
||||
return url_list
|
||||
else:
|
||||
return None
|
||||
|
||||
def cacheAlternateCoverURLs( self, issue_id, url_list ):
|
||||
cvc = ComicVineCacher( )
|
||||
cvc.add_alt_covers( issue_id, url_list )
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
urlFetchComplete = pyqtSignal( str , str, int)
|
||||
|
||||
def asyncFetchIssueCoverURLs( self, issue_id ):
|
||||
|
||||
self.issue_id = issue_id
|
||||
cached_image_url,cached_thumb_url,month,year = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if cached_image_url is not None:
|
||||
self.urlFetchComplete.emit( cached_image_url,cached_thumb_url, self.issue_id )
|
||||
details = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if details['image_url'] is not None:
|
||||
self.urlFetchComplete.emit( details['image_url'],details['thumb_image_url'], self.issue_id )
|
||||
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"
|
||||
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"
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(issue_url)))
|
||||
@ -376,16 +457,41 @@ class ComicVineTalker(QObject):
|
||||
data = reply.readAll()
|
||||
cv_response = json.loads(str(data))
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
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']
|
||||
page_url = cv_response['results']['site_detail_url']
|
||||
|
||||
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, month, year )
|
||||
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, month, year, page_url )
|
||||
|
||||
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )
|
||||
|
||||
altUrlListFetchComplete = pyqtSignal( list, int)
|
||||
|
||||
def asyncFetchAlternateCoverURLs( self, issue_id, issue_page_url ):
|
||||
# This async version requires the issue page url to be provided!
|
||||
self.issue_id = issue_id
|
||||
url_list = self.fetchCachedAlternateCoverURLs( issue_id )
|
||||
if url_list is not None:
|
||||
self.altUrlListFetchComplete.emit( url_list, int(self.issue_id) )
|
||||
return
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchAlternateCoverURLsComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(str(issue_page_url))))
|
||||
|
||||
|
||||
def asyncFetchAlternateCoverURLsComplete( self, reply ):
|
||||
# read in the response
|
||||
html = str(reply.readAll())
|
||||
alt_cover_url_list = self.parseOutAltCoverUrls( html )
|
||||
|
||||
# cache this alt cover URL list
|
||||
self.cacheAlternateCoverURLs( self.issue_id, alt_cover_url_list )
|
||||
|
||||
self.altUrlListFetchComplete.emit( alt_cover_url_list, int(self.issue_id) )
|
||||
|
||||
|
290
coverimagewidget.py
Normal file
290
coverimagewidget.py
Normal file
@ -0,0 +1,290 @@
|
||||
"""
|
||||
A PyQt4 widget display cover images from either local archive, or from ComicVine
|
||||
"""
|
||||
|
||||
"""
|
||||
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 os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from pageloader import PageLoader
|
||||
from imagepopup import ImagePopup
|
||||
import utils
|
||||
|
||||
# helper func to allow a label to be clickable
|
||||
def clickable(widget):
|
||||
|
||||
class Filter(QObject):
|
||||
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
|
||||
|
||||
class CoverImageWidget(QWidget):
|
||||
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
|
||||
def __init__(self, parent, mode ):
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'coverimagewidget.ui' ), self )
|
||||
|
||||
utils.reduceWidgetFontSize( self.label )
|
||||
|
||||
self.mode = mode
|
||||
self.comicVine = ComicVineTalker()
|
||||
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.clicked.connect( self.decrementImage )
|
||||
self.btnRight.clicked.connect( self.incrementImage )
|
||||
self.resetWidget()
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
|
||||
self.updateContent()
|
||||
|
||||
def resetWidget(self):
|
||||
self.comic_archive = None
|
||||
self.issue_id = None
|
||||
self.comicVine = None
|
||||
self.cover_fetcher = None
|
||||
self.url_list = []
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = None
|
||||
self.imageIndex = -1
|
||||
self.imageCount = 1
|
||||
|
||||
def clear( self ):
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
def incrementImage( self ):
|
||||
self.imageIndex += 1
|
||||
if self.imageIndex == self.imageCount:
|
||||
self.imageIndex = 0
|
||||
self.updateContent()
|
||||
|
||||
def decrementImage( self ):
|
||||
self.imageIndex -= 1
|
||||
if self.imageIndex == -1:
|
||||
self.imageIndex = self.imageCount -1
|
||||
self.updateContent()
|
||||
|
||||
def setArchive( self, ca, page=0 ):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.resetWidget()
|
||||
self.comic_archive = ca
|
||||
self.imageIndex = page
|
||||
self.imageCount = ca.getNumberOfPages()
|
||||
self.updateContent()
|
||||
|
||||
def setURL( self, url ):
|
||||
if self.mode == CoverImageWidget.URLMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.url_list = [ url ]
|
||||
self.imageIndex = 0
|
||||
self.imageCount = 1
|
||||
self.updateContent()
|
||||
|
||||
def setIssueID( self, issue_id ):
|
||||
if self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.issue_id = issue_id
|
||||
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete )
|
||||
self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
|
||||
def primaryUrlFetchComplete( self, primary_url, thumb_url, issue_id ):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateContent()
|
||||
|
||||
#defer the alt cover search
|
||||
QTimer.singleShot(1, self.startAltCoverSearch)
|
||||
|
||||
def startAltCoverSearch( self ):
|
||||
|
||||
# now we need to get the list of alt cover URLs
|
||||
self.label.setText("Searching for alt. covers...")
|
||||
|
||||
# page URL should already be cached, so no need to defer
|
||||
self.comicVine = ComicVineTalker()
|
||||
issue_page_url = self.comicVine.fetchIssuePageURL( self.issue_id )
|
||||
self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete )
|
||||
self.comicVine.asyncFetchAlternateCoverURLs( int(self.issue_id), issue_page_url)
|
||||
|
||||
def altCoverUrlListFetchComplete( self, url_list, issue_id ):
|
||||
if len(url_list) > 0:
|
||||
self.url_list.extend(url_list)
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateControls()
|
||||
|
||||
def setPage( self, pagenum ):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.imageIndex = pagenum
|
||||
self.updateContent()
|
||||
|
||||
def updateContent( self ):
|
||||
self.updateImage()
|
||||
self.updateControls()
|
||||
|
||||
def updateImage( self ):
|
||||
if self.imageIndex == -1:
|
||||
self.loadDefault()
|
||||
elif self.mode in [ CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode ]:
|
||||
self.loadURL()
|
||||
else:
|
||||
self.loadPage()
|
||||
|
||||
def updateControls( self ):
|
||||
if not self.showControls:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
self.label.hide()
|
||||
return
|
||||
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.btnLeft.setEnabled(False)
|
||||
self.btnRight.setEnabled(False)
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
else:
|
||||
self.btnLeft.setEnabled(True)
|
||||
self.btnRight.setEnabled(True)
|
||||
self.btnLeft.show()
|
||||
self.btnRight.show()
|
||||
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.label.setText("")
|
||||
elif self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.label.setText("Cover {0} ( of {1} )".format(self.imageIndex+1, self.imageCount))
|
||||
else:
|
||||
self.label.setText("Page {0} ( of {1} )".format(self.imageIndex+1, self.imageCount))
|
||||
|
||||
def loadURL( self ):
|
||||
self.loadDefault()
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete)
|
||||
self.cover_fetcher.fetch( self.url_list[self.imageIndex] )
|
||||
#print "ATB cover fetch started...."
|
||||
|
||||
# called when the image is done loading from internet
|
||||
def coverRemoteFetchComplete( self, image_data, issue_id ):
|
||||
img = QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
#print "ATB cover fetch complete!"
|
||||
|
||||
def loadPage( self ):
|
||||
if self.comic_archive is not None:
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = PageLoader( self.comic_archive, self.imageIndex )
|
||||
self.page_loader.loadComplete.connect( self.pageLoadComplete )
|
||||
self.page_loader.start()
|
||||
|
||||
def pageLoadComplete( self, img ):
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.page_loader = None
|
||||
|
||||
def loadDefault( self ):
|
||||
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
|
||||
#print "loadDefault called"
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
#print "ATB resizeEvent deltas", resize_event.size().width(), resize_event.size().height()
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
"""
|
||||
new_h = self.frame.height() + delta_h
|
||||
new_w = self.frame.width() + delta_w
|
||||
print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
print "ATB self.frame", self.frame.width(), self.frame.height()
|
||||
print "ATB self.", self.width(), self.height()
|
||||
|
||||
frame_w = new_w
|
||||
frame_h = new_h
|
||||
"""
|
||||
new_h = self.frame.height()
|
||||
new_w = self.frame.width()
|
||||
frame_w = self.frame.width()
|
||||
frame_h = self.frame.height()
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
|
||||
#print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
#print "ATB self.frame", frame_w, frame_h
|
||||
#print "ATB new size", new_w, new_h
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap( scaled_pixmap )
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize( img_w, img_h )
|
||||
self.lblImage.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
|
||||
|
||||
def showPopup( self ):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
123
coverimagewidget.ui
Normal file
123
coverimagewidget.ui
Normal file
@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>coverImageWidget</class>
|
||||
<widget class="QWidget" name="coverImageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>292</width>
|
||||
<height>353</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnLeft">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnRight">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<widget class="QLabel" name="lblImage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>60</x>
|
||||
<y>50</y>
|
||||
<width>91</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Double-click to expand</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="1.0.0-beta"
|
||||
version="1.1.0-beta"
|
||||
|
@ -39,6 +39,9 @@ class ExportWindow(QtGui.QDialog):
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState( QtCore.Qt.Unchecked )
|
||||
|
@ -9,18 +9,24 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>515</width>
|
||||
<height>307</height>
|
||||
<width>533</width>
|
||||
<height>202</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Export to Zip Archive</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
|
@ -85,7 +85,13 @@ class FileNameParser:
|
||||
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
|
||||
@ -101,18 +107,29 @@ class FileNameParser:
|
||||
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
|
||||
for word in reversed(word_list):
|
||||
if 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:
|
||||
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
|
||||
@ -133,7 +150,7 @@ class FileNameParser:
|
||||
# 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.
|
||||
@ -154,13 +171,11 @@ class FileNameParser:
|
||||
|
||||
series = series.rstrip("#")
|
||||
|
||||
# search for volume number
|
||||
match = re.search('(?<= [vV])(\d+)\s*$', series)
|
||||
# search for volume number
|
||||
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
|
||||
if match:
|
||||
volume = match.group()
|
||||
series = series.replace(" V"+ volume, " v"+ volume)
|
||||
series = series.split("v"+volume)[0]
|
||||
volume = volume.lstrip("0")
|
||||
series = match.group(1)
|
||||
volume = match.group(3)
|
||||
|
||||
return series.strip(), volume.strip()
|
||||
|
||||
@ -197,9 +212,9 @@ class FileNameParser:
|
||||
# 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:]
|
||||
#word = filename.split(' ')[0]
|
||||
#if len(word) == 3 and word[0] =='0' and word.isdigit():
|
||||
# filename = filename[4:]
|
||||
# ----HACK -
|
||||
|
||||
self.issue = self.getIssueNumber(filename)
|
||||
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
from issuestring import IssueString
|
||||
|
||||
class FileRenamer:
|
||||
@ -86,6 +87,27 @@ class FileRenamer:
|
||||
new_name = self.replaceToken( new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken( new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken( new_name, md.title, '%title%')
|
||||
new_name = self.replaceToken( new_name, md.month, '%month%')
|
||||
month_name = None
|
||||
if md.month is not None:
|
||||
if (type(md.month) == str and md.month.isdigit()) or type(md.month) == int:
|
||||
if int(md.month) in range(1,13):
|
||||
dt = datetime.datetime( 1970, int(md.month), 1, 0, 0)
|
||||
month_name = dt.strftime("%B")
|
||||
new_name = self.replaceToken( new_name, month_name, '%month_name%')
|
||||
|
||||
new_name = self.replaceToken( new_name, md.genre, '%genre%')
|
||||
new_name = self.replaceToken( new_name, md.language, '%language_code%')
|
||||
new_name = self.replaceToken( new_name, md.criticalRating , '%criticalrating%')
|
||||
new_name = self.replaceToken( new_name, md.alternateSeries, '%alternateseries%')
|
||||
new_name = self.replaceToken( new_name, md.alternateNumber, '%alternatenumber%')
|
||||
new_name = self.replaceToken( new_name, md.alternateCount, '%alternatecount%')
|
||||
new_name = self.replaceToken( new_name, md.imprint, '%imprint%')
|
||||
new_name = self.replaceToken( new_name, md.format, '%format%')
|
||||
new_name = self.replaceToken( new_name, md.maturityRating, '%maturityrating%')
|
||||
new_name = self.replaceToken( new_name, md.storyArc, '%storyarc%')
|
||||
new_name = self.replaceToken( new_name, md.seriesGroup, '%seriesgroup%')
|
||||
new_name = self.replaceToken( new_name, md.scanInfo, '%scaninfo%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
@ -109,6 +131,7 @@ class FileRenamer:
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(":", "-")
|
||||
new_name = new_name.replace("?", "")
|
||||
|
||||
return new_name
|
||||
|
||||
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
@ -30,6 +31,7 @@ from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
import utils
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
|
||||
@ -72,11 +74,9 @@ class FileSelectionList(QWidget):
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
#self.twList = FileTableWidget( self )
|
||||
#gridlayout = QGridLayout( self )
|
||||
#gridlayout.addWidget( self.twList )
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
#self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
self.currentItem = None
|
||||
@ -162,6 +162,11 @@ class FileSelectionList(QWidget):
|
||||
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:
|
||||
@ -173,7 +178,8 @@ class FileSelectionList(QWidget):
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle( "Adding Files" )
|
||||
progdialog.setWindowModality(Qt.WindowModal)
|
||||
#progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.setWindowModality(Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
|
||||
firstAdded = None
|
||||
@ -184,6 +190,7 @@ class FileSelectionList(QWidget):
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem( f )
|
||||
if firstAdded is None and row is not None:
|
||||
|
BIN
graphics/left.png
Normal file
BIN
graphics/left.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 46 KiB |
BIN
graphics/popup_bg.png
Normal file
BIN
graphics/popup_bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 362 B |
BIN
graphics/right.png
Normal file
BIN
graphics/right.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
86
imagepopup.py
Normal file
86
imagepopup.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""
|
||||
A PyQT4 widget to display a popup image
|
||||
"""
|
||||
|
||||
"""
|
||||
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
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class ImagePopup(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'imagepopup.ui' ), self)
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
#self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
self.setWindowState(QtCore.Qt.WindowFullScreen)
|
||||
|
||||
self.imagePixmap = image_pixmap
|
||||
|
||||
screen_size = QtGui.QDesktopWidget().screenGeometry()
|
||||
self.resize(screen_size.width(), screen_size.height())
|
||||
self.move( 0, 0)
|
||||
|
||||
# This is a total hack. Uses a snapshot of the desktop, and overlays a
|
||||
# translucent screen over it. Probably can do it better by setting opacity of a
|
||||
# widget
|
||||
self.desktopBg = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop ().winId(),
|
||||
0,0, screen_size.width(), screen_size.height())
|
||||
bg = QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/popup_bg.png' ))
|
||||
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height())
|
||||
self.setMask(self.clientBgPixmap.mask())
|
||||
|
||||
self.applyImagePixmap()
|
||||
self.showFullScreen()
|
||||
self.raise_( )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def paintEvent (self, event):
|
||||
self.painter = QtGui.QPainter(self)
|
||||
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.painter.drawPixmap(0, 0, self.desktopBg)
|
||||
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
|
||||
self.painter.end()
|
||||
|
||||
def applyImagePixmap( self ):
|
||||
win_h = self.height()
|
||||
win_w = self.width()
|
||||
|
||||
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
|
||||
# scale the pixmap to fit in the frame
|
||||
display_pixmap = self.imagePixmap.scaled(win_w, win_h, QtCore.Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap( display_pixmap )
|
||||
else:
|
||||
display_pixmap = self.imagePixmap
|
||||
self.lblImage.setPixmap( display_pixmap )
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize( img_w, img_h )
|
||||
self.lblImage.move( (win_w - img_w)/2, (win_h - img_h)/2 )
|
||||
|
||||
def mousePressEvent( self , event):
|
||||
self.close()
|
38
imagepopup.ui
Normal file
38
imagepopup.ui
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QDialog" name="Form">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>817</width>
|
||||
<height>455</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="windowOpacity">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<widget class="QLabel" name="lblImage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>300</x>
|
||||
<y>120</y>
|
||||
<width>66</width>
|
||||
<height>17</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -38,6 +38,11 @@ from issuestring import IssueString
|
||||
|
||||
import utils
|
||||
|
||||
class IssueIdentifierNetworkError(Exception):
|
||||
pass
|
||||
class IssueIdentifierCancelled(Exception):
|
||||
pass
|
||||
|
||||
class IssueIdentifier:
|
||||
|
||||
ResultNoMatches = 0
|
||||
@ -54,7 +59,10 @@ class IssueIdentifier:
|
||||
self.onlyUseAdditionalMetaData = False
|
||||
|
||||
# a decent hamming score, good enough to call it a match
|
||||
self.min_score_thresh = 20
|
||||
self.min_score_thresh = 16
|
||||
|
||||
# for alternate covers, be more stringent, since we're a bit more scattershot in comparisons
|
||||
self.min_alternate_score_thresh = 12
|
||||
|
||||
# the min distance a hamming score must be to separate itself from closest neighbor
|
||||
self.min_score_distance = 4
|
||||
@ -66,7 +74,6 @@ class IssueIdentifier:
|
||||
self.length_delta_thresh = settings.id_length_delta_thresh
|
||||
|
||||
# used to eliminate unlikely publishers
|
||||
#self.publisher_blacklist = [ 'panini comics', 'abril', 'scholastic book services' ]
|
||||
self.publisher_blacklist = [ s.strip().lower() for s in settings.id_publisher_blacklist.split(',') ]
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
@ -75,7 +82,7 @@ class IssueIdentifier:
|
||||
self.coverUrlCallback = None
|
||||
self.search_result = self.ResultNoMatches
|
||||
self.cover_page_index = 0
|
||||
|
||||
|
||||
def setScoreMinThreshold( self, thresh ):
|
||||
self.min_score_thresh = thresh
|
||||
|
||||
@ -86,7 +93,7 @@ class IssueIdentifier:
|
||||
self.additional_metadata = md
|
||||
|
||||
def setNameLengthDeltaThreshold( self, delta ):
|
||||
self.length_delta_thresh = md
|
||||
self.length_delta_thresh = delta
|
||||
|
||||
def setPublisherBlackList( self, blacklist ):
|
||||
self.publisher_blacklist = blacklist
|
||||
@ -119,8 +126,14 @@ class IssueIdentifier:
|
||||
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w,h = im.size
|
||||
|
||||
cropped_im = im.crop( (int(w/2), 0, w, h) )
|
||||
|
||||
try:
|
||||
cropped_im = im.crop( (int(w/2), 0, w, h) )
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "cropCover() error:", e
|
||||
return None
|
||||
|
||||
output = StringIO.StringIO()
|
||||
cropped_im.save(output, format="JPEG")
|
||||
cropped_image_data = output.getvalue()
|
||||
@ -202,7 +215,7 @@ class IssueIdentifier:
|
||||
|
||||
@staticmethod
|
||||
def defaultWriteOutput( text ):
|
||||
sys.stdout.write(text)
|
||||
sys.stdout.write( text )
|
||||
sys.stdout.flush()
|
||||
|
||||
def log_msg( self, msg , newline=True ):
|
||||
@ -210,6 +223,104 @@ class IssueIdentifier:
|
||||
if newline:
|
||||
self.output_function("\n")
|
||||
|
||||
def getIssueCoverMatchScore( self, comicVine, issue_id, localCoverHashList, useRemoteAlternates = False , use_log=True):
|
||||
|
||||
# localHashes is a list of pre-calculated hashs.
|
||||
# useRemoteAlternates - indicates to use alternate covers from CV
|
||||
|
||||
# first get the primary cover image
|
||||
primary_img_url, primary_thumb_url = comicVine.fetchIssueCoverURLs( issue_id )
|
||||
|
||||
try:
|
||||
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
|
||||
except ImageFetcherException:
|
||||
self.log_msg( "Network issue while fetching cover image from ComicVine. Aborting...")
|
||||
raise IssueIdentifierNetworkError
|
||||
|
||||
if self.cancel == True:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
# alert the GUI, if needed
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback( url_image_data )
|
||||
|
||||
remote_cover_list = []
|
||||
item = dict()
|
||||
item['url'] = primary_img_url
|
||||
|
||||
item['hash'] = self.calculateHash( url_image_data )
|
||||
remote_cover_list.append( item )
|
||||
|
||||
if self.cancel == True:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
if useRemoteAlternates:
|
||||
alt_img_url_list = comicVine.fetchAlternateCoverURLs( issue_id )
|
||||
|
||||
for alt_url in alt_img_url_list:
|
||||
try:
|
||||
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
|
||||
except ImageFetcherException:
|
||||
self.log_msg( "Network issue while fetching alt. cover image from ComicVine. Aborting...")
|
||||
raise IssueIdentifierNetworkError
|
||||
|
||||
if self.cancel == True:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
# alert the GUI, if needed
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback( alt_url_image_data )
|
||||
|
||||
item = dict()
|
||||
item['url'] = alt_url
|
||||
item['hash'] = self.calculateHash( alt_url_image_data )
|
||||
remote_cover_list.append( item )
|
||||
|
||||
if self.cancel == True:
|
||||
raise IssueIdentifierCancelled
|
||||
|
||||
if use_log and useRemoteAlternates:
|
||||
self.log_msg( "[{0} alt. covers]".format(len(remote_cover_list)-1), False )
|
||||
if use_log:
|
||||
self.log_msg( "[ ", False )
|
||||
|
||||
score_list = []
|
||||
done = False
|
||||
for local_cover_hash in localCoverHashList:
|
||||
for remote_cover_item in remote_cover_list:
|
||||
score = ImageHasher.hamming_distance(local_cover_hash, remote_cover_item['hash'] )
|
||||
score_item = dict()
|
||||
score_item['score'] = score
|
||||
score_item['url'] = remote_cover_item['url']
|
||||
score_item['hash'] = remote_cover_item['hash']
|
||||
score_list.append( score_item )
|
||||
if use_log:
|
||||
self.log_msg( "{0} ".format(score), False )
|
||||
|
||||
if score <= self.strong_score_thresh:
|
||||
# such a good score, we can quit now, since for sure we have a winner
|
||||
done = True
|
||||
break
|
||||
if done:
|
||||
break
|
||||
|
||||
if use_log:
|
||||
self.log_msg( " ]", False )
|
||||
|
||||
best_score_item = min(score_list, key=lambda x:x['score'])
|
||||
|
||||
return best_score_item
|
||||
|
||||
"""
|
||||
def validate( self, issue_id ):
|
||||
# create hash list
|
||||
score = self.getIssueMatchScore( issue_id, hash_list, useRemoteAlternates = True )
|
||||
if score < 20:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
"""
|
||||
|
||||
def search( self ):
|
||||
|
||||
ca = self.comic_archive
|
||||
@ -235,9 +346,10 @@ class IssueIdentifier:
|
||||
aspect_ratio = self.getAspectRatio( cover_image_data )
|
||||
if aspect_ratio < 1.0:
|
||||
right_side_image_data = self.cropCover( cover_image_data )
|
||||
narrow_cover_hash = self.calculateHash( right_side_image_data )
|
||||
print "narrow_cover_hash", narrow_cover_hash
|
||||
|
||||
if right_side_image_data is not None:
|
||||
narrow_cover_hash = self.calculateHash( right_side_image_data )
|
||||
self.log_msg(unicode(str(narrow_cover_hash)))
|
||||
|
||||
#self.log_msg( "Cover hash = {0:016x}".format(cover_hash) )
|
||||
|
||||
keys = self.getSearchKeys()
|
||||
@ -259,6 +371,7 @@ class IssueIdentifier:
|
||||
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
|
||||
comicVine = ComicVineTalker( )
|
||||
comicVine.setLogFunc( self.output_function )
|
||||
|
||||
#self.log_msg( ( "Searching for " + keys['series'] + "...")
|
||||
self.log_msg( u"Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
|
||||
@ -272,7 +385,7 @@ class IssueIdentifier:
|
||||
if self.cancel == True:
|
||||
return []
|
||||
|
||||
series_shortlist = []
|
||||
series_second_round_list = []
|
||||
|
||||
#self.log_msg( "Removing results with too long names, banned publishers, or future start dates" )
|
||||
for item in cv_search_results:
|
||||
@ -298,34 +411,33 @@ class IssueIdentifier:
|
||||
publisher_approved = False
|
||||
|
||||
if length_approved and publisher_approved and date_approved:
|
||||
series_shortlist.append(item)
|
||||
series_second_round_list.append(item)
|
||||
|
||||
# if we don't think it's an issue number 1, remove any series' that are one-shots
|
||||
if keys['issue_number'] != '1':
|
||||
if keys['issue_number'] not in [ '1', '0' ]:
|
||||
#self.log_msg( "Removing one-shots" )
|
||||
series_shortlist[:] = [x for x in series_shortlist if not x['count_of_issues'] == 1]
|
||||
series_second_round_list[:] = [x for x in series_second_round_list if not x['count_of_issues'] == 1]
|
||||
|
||||
self.log_msg( "Searching in " + str(len(series_shortlist)) +" series" )
|
||||
self.log_msg( "Searching in " + str(len(series_second_round_list)) +" series" )
|
||||
|
||||
if self.callback is not None:
|
||||
self.callback( 0, len(series_shortlist))
|
||||
self.callback( 0, len(series_second_round_list))
|
||||
|
||||
# now sort the list by name length
|
||||
series_shortlist.sort(key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
# Now we've got a list of series that we can dig into,
|
||||
# and look for matching issue number, date, and cover image
|
||||
series_second_round_list.sort(key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
# Now we've got a list of series that we can dig into look for matching issue number
|
||||
counter = 0
|
||||
for series in series_shortlist:
|
||||
shortlist = []
|
||||
for series in series_second_round_list:
|
||||
if self.callback is not None:
|
||||
self.callback( counter, len(series_second_round_list)*3)
|
||||
counter += 1
|
||||
self.callback( counter, len(series_shortlist))
|
||||
|
||||
self.log_msg( u"Fetching info for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=False )
|
||||
series['start_year']), newline=True )
|
||||
|
||||
try:
|
||||
cv_series_results = comicVine.fetchVolumeData( series['id'] )
|
||||
@ -339,59 +451,67 @@ class IssueIdentifier:
|
||||
|
||||
# look for a matching issue number
|
||||
if num_s == keys['issue_number']:
|
||||
# found a matching issue number! now get the issue data
|
||||
img_url, thumb_url = comicVine.fetchIssueCoverURLs( issue['id'] )
|
||||
month, year = comicVine.fetchIssueDate( issue['id'] )
|
||||
|
||||
if self.cancel == True:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
# now, if we have an issue year key given, reject this one if not a match
|
||||
month, year = comicVine.fetchIssueDate( issue['id'] )
|
||||
if keys['year'] is not None:
|
||||
if keys['year'] != year:
|
||||
if unicode(keys['year']) != unicode(year):
|
||||
break
|
||||
try:
|
||||
url_image_data = ImageFetcher().fetch(thumb_url, blocking=True)
|
||||
except ImageFetcherException:
|
||||
self.log_msg( "Network issue while fetching cover image from ComicVine. Aborting...")
|
||||
return []
|
||||
|
||||
if self.cancel == True:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback( url_image_data )
|
||||
|
||||
url_image_hash = self.calculateHash( url_image_data )
|
||||
score = ImageHasher.hamming_distance(cover_hash, url_image_hash)
|
||||
|
||||
# found a matching issue number! add it to short list
|
||||
shortlist.append( (series, cv_series_results, issue) )
|
||||
|
||||
# if we have a cropped version of the cover, check that one also, and use the best score
|
||||
if narrow_cover_hash is not None:
|
||||
score2 = ImageHasher.hamming_distance(narrow_cover_hash, url_image_hash)
|
||||
score = min( score, score2 )
|
||||
if keys['year'] is None:
|
||||
self.log_msg( "Found {0} series that have an issue #{1}".format(len(shortlist), keys['issue_number']) )
|
||||
else:
|
||||
self.log_msg( "Found {0} series that have an issue #{1} from {2}".format(len(shortlist), keys['issue_number'], keys['year'] ))
|
||||
|
||||
|
||||
# now we have a shortlist of volumes with the desired issue number
|
||||
# Do first round of cover matching
|
||||
counter = len(shortlist)
|
||||
for series, cv_series_results, issue in shortlist:
|
||||
if self.callback is not None:
|
||||
self.callback( counter, len(shortlist)*3)
|
||||
counter += 1
|
||||
|
||||
self.log_msg( u"Examining covers for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=False )
|
||||
|
||||
# now, if we have an issue year key given, reject this one if not a match
|
||||
month, year = comicVine.fetchIssueDate( issue['id'] )
|
||||
|
||||
match = dict()
|
||||
match['series'] = u"{0} ({1})".format(series['name'], series['start_year'])
|
||||
match['distance'] = score
|
||||
match['issue_number'] = num_s
|
||||
match['url_image_hash'] = url_image_hash
|
||||
match['issue_title'] = issue['name']
|
||||
match['img_url'] = img_url
|
||||
match['issue_id'] = issue['id']
|
||||
match['volume_id'] = series['id']
|
||||
match['month'] = month
|
||||
match['year'] = year
|
||||
match['publisher'] = None
|
||||
if series['publisher'] is not None:
|
||||
match['publisher'] = series['publisher']['name']
|
||||
|
||||
self.match_list.append(match)
|
||||
# Now check the cover match against the primary image
|
||||
hash_list = [ cover_hash ]
|
||||
if narrow_cover_hash is not None:
|
||||
hash_list.append(narrow_cover_hash)
|
||||
try:
|
||||
score_item = self.getIssueCoverMatchScore( comicVine, issue['id'], hash_list, useRemoteAlternates = False )
|
||||
except:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
match = dict()
|
||||
match['series'] = u"{0} ({1})".format(series['name'], series['start_year'])
|
||||
match['distance'] = score_item['score']
|
||||
match['issue_number'] = keys['issue_number']
|
||||
match['url_image_hash'] = score_item['hash']
|
||||
match['issue_title'] = issue['name']
|
||||
match['img_url'] = score_item['url']
|
||||
match['issue_id'] = issue['id']
|
||||
match['volume_id'] = series['id']
|
||||
match['month'] = month
|
||||
match['year'] = year
|
||||
match['publisher'] = None
|
||||
if series['publisher'] is not None:
|
||||
match['publisher'] = series['publisher']['name']
|
||||
|
||||
self.match_list.append(match)
|
||||
|
||||
self.log_msg( " --> {0}".format(match['distance']), newline=False )
|
||||
|
||||
self.log_msg( " --> {0}".format(match['distance']), newline=False )
|
||||
|
||||
break
|
||||
self.log_msg( "" )
|
||||
|
||||
if len(self.match_list) == 0:
|
||||
@ -407,7 +527,7 @@ class IssueIdentifier:
|
||||
for i in self.match_list:
|
||||
l.append( i['distance'] )
|
||||
|
||||
self.log_msg( "Compared {0} covers".format(len(self.match_list)), newline=False)
|
||||
self.log_msg( "Compared to covers in {0} issue(s):".format(len(self.match_list)), newline=False)
|
||||
self.log_msg( str(l))
|
||||
|
||||
def print_match(item):
|
||||
@ -421,41 +541,66 @@ class IssueIdentifier:
|
||||
|
||||
best_score = self.match_list[0]['distance']
|
||||
|
||||
if len(self.match_list) == 1:
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
if best_score > self.min_score_thresh:
|
||||
self.log_msg( "Very weak score for the cover. Maybe it's not the cover..." )
|
||||
|
||||
self.log_msg( "Comparing to some other archive pages now..." )
|
||||
found = False
|
||||
for i in range( min(3, ca.getNumberOfPages())):
|
||||
image_data = ca.getPage(i)
|
||||
page_hash = self.calculateHash( image_data )
|
||||
distance = ImageHasher.hamming_distance(page_hash, self.match_list[0]['url_image_hash'])
|
||||
if distance <= self.strong_score_thresh:
|
||||
self.log_msg( "Found a great match (distance = {0}) on page {1}!".format(distance, i+1) )
|
||||
found = True
|
||||
break
|
||||
elif distance < self.min_score_thresh:
|
||||
self.log_msg( "Found a good match (distance = {0}) on page {1}".format(distance, i) )
|
||||
found = True
|
||||
self.log_msg( ".", newline=False )
|
||||
if best_score >= self.min_score_thresh:
|
||||
# we have 1 or more low-confidence matches (all bad cover scores)
|
||||
# look at a few more pages in the archive, and also alternate covers online
|
||||
self.log_msg( "Very weak scores for the cover. Analyzing alternate pages and covers..." )
|
||||
hash_list = [ cover_hash ]
|
||||
if narrow_cover_hash is not None:
|
||||
hash_list.append(narrow_cover_hash)
|
||||
for i in range( 1, min(3, ca.getNumberOfPages())):
|
||||
image_data = ca.getPage(i)
|
||||
page_hash = self.calculateHash( image_data )
|
||||
hash_list.append( page_hash )
|
||||
|
||||
second_match_list = []
|
||||
counter = 2*len(self.match_list)
|
||||
for m in self.match_list:
|
||||
if self.callback is not None:
|
||||
self.callback( counter, len(self.match_list)*3)
|
||||
counter += 1
|
||||
self.log_msg( u"Examining alternate covers for ID: {0} {1} ...".format(
|
||||
m['volume_id'],
|
||||
m['series']), newline=False )
|
||||
try:
|
||||
score_item = self.getIssueCoverMatchScore( comicVine, m['issue_id'], hash_list, useRemoteAlternates = True )
|
||||
except:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
self.log_msg("--->{0}".format(score_item['score']))
|
||||
self.log_msg( "" )
|
||||
if not found:
|
||||
self.log_msg( "No matching pages in the issue. Bummer" )
|
||||
self.search_result = self.ResultFoundMatchButBadCoverScore
|
||||
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
return self.match_list
|
||||
|
||||
elif best_score > self.min_score_thresh and len(self.match_list) > 1:
|
||||
self.log_msg( "No good image matches! Need to use other info..." )
|
||||
self.search_result = self.ResultMultipleMatchesWithBadImageScores
|
||||
if score_item['score'] < self.min_alternate_score_thresh:
|
||||
second_match_list.append(m)
|
||||
m['distance'] = score_item['score']
|
||||
|
||||
return self.match_list
|
||||
|
||||
if len( second_match_list ) == 0:
|
||||
if len( self.match_list) == 1:
|
||||
self.log_msg( "No matching pages in the issue." )
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.search_result = self.ResultFoundMatchButBadCoverScore
|
||||
else:
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.log_msg( u"Multiple bad cover matches! Need to use other info..." )
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.search_result = self.ResultMultipleMatchesWithBadImageScores
|
||||
return self.match_list
|
||||
else:
|
||||
# We did good, found something!
|
||||
self.log_msg( "Success in secondary/alternate cover matching!" )
|
||||
|
||||
self.match_list = second_match_list
|
||||
# sort new list by image match scores
|
||||
self.match_list.sort(key=lambda k: k['distance'])
|
||||
best_score = self.match_list[0]['distance']
|
||||
self.log_msg("[Second round cover matching: best score = {0}]".format(best_score))
|
||||
# now drop down into the rest of the processing
|
||||
|
||||
if self.callback is not None:
|
||||
self.callback( 99, 100)
|
||||
|
||||
#now pare down list, remove any item more than specified distant from the top scores
|
||||
for item in reversed(self.match_list):
|
||||
if item['distance'] > best_score + self.min_score_distance:
|
||||
@ -468,10 +613,11 @@ class IssueIdentifier:
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
|
||||
elif len(self.match_list) == 0:
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.log_msg( "No matches found :(" )
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.search_result = self.ResultNoMatches
|
||||
else:
|
||||
print
|
||||
self.log_msg( "More than one likley candiate." )
|
||||
self.search_result = self.ResultMultipleGoodMatches
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
|
@ -29,6 +29,8 @@ from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
@ -38,7 +40,18 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
super(IssueSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'issueselectionwindow.ui' ), self)
|
||||
|
||||
|
||||
self.coverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
gridlayout.addWidget( self.coverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.series_id = series_id
|
||||
self.settings = settings
|
||||
self.url_fetch_thread = None
|
||||
@ -90,6 +103,7 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
item_text = record['issue_number']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setData(QtCore.Qt.DisplayRole, float(item_text))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
@ -97,6 +111,7 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
@ -119,35 +134,13 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
|
||||
self.issue_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record['id'] == self.issue_id:
|
||||
|
||||
if record['id'] == self.issue_id:
|
||||
self.issue_number = record['issue_number']
|
||||
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.cv = ComicVineTalker( )
|
||||
self.cv.urlFetchComplete.connect( self.urlFetchComplete )
|
||||
self.cv.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
|
||||
self.coverWidget.setIssueID( int(self.issue_id) )
|
||||
break
|
||||
|
||||
# called when the cover URL has been fetched
|
||||
def urlFetchComplete( self, image_url, thumb_url, issue_id ):
|
||||
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
|
||||
self.cover_fetcher.fetch( str(image_url), user_data=issue_id )
|
||||
|
||||
# called when the image is done loading
|
||||
def coverFetchComplete( self, image_data, issue_id ):
|
||||
if self.issue_id == issue_id:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
|
||||
|
@ -20,11 +20,6 @@
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
@ -59,11 +54,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<widget class="QWidget" name="coverImageContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
@ -72,18 +67,6 @@
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -32,7 +32,9 @@ class LogWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'logwindow.ui' ), self)
|
||||
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
def setText( self, text ):
|
||||
self.textEdit.setPlainText( text )
|
@ -26,26 +26,55 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from options import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
|
||||
class MatchSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, matches):
|
||||
def __init__(self, parent, matches, comic_archive):
|
||||
super(MatchSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'matchselectionwindow.ui' ), self)
|
||||
|
||||
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
gridlayout.addWidget( self.altCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.matches = matches
|
||||
self.populateTable( )
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.comic_archive = comic_archive
|
||||
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
self.current_row = 0
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
|
||||
self.updateData()
|
||||
|
||||
def updateData( self):
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.comic_archive.path
|
||||
self.setWindowTitle( u"Select correct match: {0}".format(
|
||||
os.path.split(path)[1] ))
|
||||
|
||||
def populateTable( self ):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
@ -59,35 +88,47 @@ class MatchSelectionWindow(QtGui.QDialog):
|
||||
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
"""
|
||||
item_text = u"{0}".format(match['issue_number'])
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
"""
|
||||
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = ""
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
item_text = u"{0}/".format(match['month'])
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
item_text += u"{0}".format(match['year'])
|
||||
else:
|
||||
item_text += u"????"
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match['issue_title']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
@ -99,19 +140,14 @@ class MatchSelectionWindow(QtGui.QDialog):
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
|
||||
|
||||
def setCoverImage( self ):
|
||||
self.archiveCoverWidget.setArchive( self.comic_archive)
|
||||
|
||||
def currentMatch( self ):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
|
||||
return match
|
||||
|
||||
self.current_row = curr.row()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
|
||||
self.cover_fetcher.fetch( self.matches[self.current_row]['img_url'] )
|
||||
|
||||
# called when the image is done loading
|
||||
def coverFetchComplete( self, image_data, issue_id ):
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
|
||||
|
@ -6,25 +6,39 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>831</width>
|
||||
<height>506</height>
|
||||
<width>907</width>
|
||||
<height>507</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<item row="0" column="1">
|
||||
<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">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
@ -35,7 +49,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
@ -58,34 +72,27 @@
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<widget class="QWidget" name="altCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
36
options.py
36
options.py
@ -78,12 +78,16 @@ If no options are given, {0} will run in windowed mode
|
||||
Some names that can be used:
|
||||
series, issue, issueCount, year, publisher, title
|
||||
-r, --rename Rename the file based on specified tag style.
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
-e, --export-to-zip Export RAR archive to Zip format
|
||||
--delete-rar Delete original RAR archive after successful export to Zip
|
||||
--abort-on-conflict Don't export to zip if intended new filename exists (Otherwise, creates
|
||||
a new unique filename)
|
||||
-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
|
||||
"""
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
@ -96,6 +100,9 @@ If no options are given, {0} will run in windowed mode
|
||||
self.print_tags = False
|
||||
self.copy_tags = False
|
||||
self.delete_tags = False
|
||||
self.export_to_zip = False
|
||||
self.abort_export_on_conflict = False
|
||||
self.delete_rar_after_export = False
|
||||
self.search_online = False
|
||||
self.dryrun = False
|
||||
self.abortOnLowConfidence = True
|
||||
@ -179,14 +186,15 @@ If no options are given, {0} will run in windowed mode
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt( input_args,
|
||||
"hpdt:fm:vonsrc:i",
|
||||
"hpdt:fm:vonsrc:ie",
|
||||
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
|
||||
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
|
||||
"interactive", "nosummary", "version", "id=" ])
|
||||
|
||||
"interactive", "nosummary", "version", "id="
|
||||
"export-to-zip", "delete-rar", "abort-on-conflict" ] )
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_msg_and_quit( str(err), 2 )
|
||||
|
||||
|
||||
# process options
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
@ -219,6 +227,12 @@ If no options are given, {0} will run in windowed mode
|
||||
self.save_tags = True
|
||||
if o in ("-r", "--rename"):
|
||||
self.rename_file = True
|
||||
if o in ("-e", "--export_to_zip"):
|
||||
self.export_to_zip = True
|
||||
if o == "--delete-rar":
|
||||
self.delete_rar_after_export = True
|
||||
if o == "--abort-on-conflict":
|
||||
self.abort_export_on_conflict = True
|
||||
if o in ("-f", "--parsefilename"):
|
||||
self.parse_filename = True
|
||||
if o == "--id":
|
||||
@ -234,7 +248,8 @@ If no options are given, {0} will run in windowed mode
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--version":
|
||||
print "ComicTagger version: ", ctversion.version
|
||||
print "ComicTagger {0}: Copyright (c) 2012-2013 Anthony Beville".format(ctversion.version)
|
||||
print "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)"
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
@ -246,7 +261,7 @@ If no options are given, {0} will run in windowed mode
|
||||
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:
|
||||
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
|
||||
@ -255,9 +270,10 @@ If no options are given, {0} will run in windowed mode
|
||||
if self.save_tags: count += 1
|
||||
if self.copy_tags: count += 1
|
||||
if self.rename_file: count += 1
|
||||
if self.export_to_zip: count +=1
|
||||
|
||||
if count > 1:
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, or export", 1 )
|
||||
|
||||
if len(args) > 0:
|
||||
if platform.system() == "Windows":
|
||||
@ -272,7 +288,7 @@ If no options are given, {0} will run in windowed mode
|
||||
self.file_list = args
|
||||
|
||||
if self.no_gui and self.filename is None:
|
||||
self.display_msg_and_quit( "Command requires a filename!", 1 )
|
||||
self.display_msg_and_quit( "Command requires at least one filename!", 1 )
|
||||
|
||||
if self.delete_tags and self.data_style is None:
|
||||
self.display_msg_and_quit( "Please specify the type to delete with -t", 1 )
|
||||
|
@ -18,11 +18,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
@ -31,14 +32,29 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
|
||||
|
||||
self.lblPage.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblPage.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored)
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.pageContainer )
|
||||
gridlayout.addWidget( self.pageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.comic_archive = None
|
||||
self.current_pixmap = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Close).setDefault(True)
|
||||
if platform.system() == "Darwin":
|
||||
self.btnPrev.setText("<<")
|
||||
self.btnNext.setText(">>")
|
||||
else:
|
||||
self.btnPrev.setIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/left.png' )))
|
||||
self.btnNext.setIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/right.png' )))
|
||||
|
||||
self.btnNext.clicked.connect( self.nextPage )
|
||||
self.btnPrev.clicked.connect( self.prevPage )
|
||||
self.show()
|
||||
@ -46,72 +62,49 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.btnNext.setEnabled( False )
|
||||
self.btnPrev.setEnabled( False )
|
||||
|
||||
def reset( self ):
|
||||
self.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = None
|
||||
|
||||
self.btnNext.setEnabled( False )
|
||||
self.btnPrev.setEnabled( False )
|
||||
self.pageWidget.clear()
|
||||
|
||||
def setComicArchive(self, ca):
|
||||
|
||||
self.comic_archive = ca
|
||||
self.page_count = ca.getNumberOfPages()
|
||||
self.current_page_num = 0
|
||||
|
||||
self.pageWidget.setArchive( self.comic_archive )
|
||||
self.setPage()
|
||||
|
||||
if self.page_count > 1:
|
||||
self.btnNext.setEnabled( True )
|
||||
self.btnPrev.setEnabled( True )
|
||||
|
||||
def nextPage(self):
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.current_page_num += 1
|
||||
else:
|
||||
self.current_page_num = 0
|
||||
self.setPage()
|
||||
|
||||
def prevPage(self):
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.current_page_num -= 1
|
||||
else:
|
||||
self.current_page_num = self.page_count - 1
|
||||
self.setPage()
|
||||
|
||||
def setPage( self ):
|
||||
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
|
||||
image_data = self.comic_archive.getPage( archive_page_index )
|
||||
|
||||
if image_data is not None:
|
||||
self.setCurrentPixmap( image_data )
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
if self.metadata is not None:
|
||||
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
|
||||
else:
|
||||
archive_page_index = self.current_page_num
|
||||
|
||||
self.pageWidget.setPage( archive_page_index )
|
||||
self.setWindowTitle("Page Browser - Page {0} (of {1}) ".format(self.current_page_num+1, self.page_count ) )
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.btnNext.setEnabled( True )
|
||||
else:
|
||||
self.btnNext.setEnabled( False )
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.btnPrev.setEnabled( True )
|
||||
else:
|
||||
self.btnPrev.setEnabled( False )
|
||||
|
||||
|
||||
def setCurrentPixmap( self, image_data ):
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.current_pixmap = QtGui.QPixmap(QtGui.QPixmap(img))
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
new_h = self.lblPage.height() + delta_h
|
||||
new_w = self.lblPage.width() + delta_w
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, QtCore.Qt.KeepAspectRatio)
|
||||
self.lblPage.setPixmap( scaled_pixmap )
|
||||
#QtCore.QCoreApplication.processEvents()
|
||||
|
||||
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>429</width>
|
||||
<height>637</height>
|
||||
<width>369</width>
|
||||
<height>582</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -20,10 +20,31 @@
|
||||
<string>Page Browser</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblPage">
|
||||
<widget class="QWidget" name="pageContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@ -36,37 +57,47 @@
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>20</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMaximumSize</enum>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnPrev">
|
||||
<property name="text">
|
||||
<string><<</string>
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
@ -81,10 +112,26 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnNext">
|
||||
<property name="text">
|
||||
<string>>></string>
|
||||
<string/>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -28,6 +28,8 @@ from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
from pageloader import PageLoader
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
|
||||
def itemMoveEvents( widget ):
|
||||
|
||||
@ -78,9 +80,11 @@ class PageListEditor(QWidget):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
|
||||
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
self.page_loader = None
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QGridLayout( self.pageContainer )
|
||||
gridlayout.addWidget( self.pageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.resetPage()
|
||||
|
||||
@ -107,8 +111,10 @@ class PageListEditor(QWidget):
|
||||
self.first_front_page = None
|
||||
|
||||
def resetPage( self ):
|
||||
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.pageWidget.clear()
|
||||
self.comboBox.setDisabled(True)
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
|
||||
def moveCurrentUp( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
@ -157,18 +163,8 @@ class PageListEditor(QWidget):
|
||||
#idx = int(str (self.listWidget.item( row ).text()))
|
||||
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.page_loader = PageLoader( self.comic_archive, idx )
|
||||
self.page_loader.loadComplete.connect( self.actualChangePageImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualChangePageImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.pageWidget.setArchive( self.comic_archive, idx )
|
||||
|
||||
def getFirstFrontCover( self ):
|
||||
frontCover = 0
|
||||
@ -203,44 +199,13 @@ class PageListEditor(QWidget):
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (page_dict,) )
|
||||
item.setText( self.listEntryText( page_dict ) )
|
||||
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
new_h = self.frame.height() + delta_h
|
||||
new_w = self.frame.width() + delta_w
|
||||
|
||||
frame_w = new_w
|
||||
frame_h = new_h
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.label.setPixmap( scaled_pixmap )
|
||||
|
||||
# ,pve and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.label.resize( img_w, img_h )
|
||||
self.label.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
|
||||
|
||||
|
||||
def setData( self, comic_archive, pages_list ):
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
if pages_list is not None and len(pages_list) > 0:
|
||||
self.comboBox.setDisabled(False)
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect( self.changePage )
|
||||
|
||||
@ -256,7 +221,7 @@ class PageListEditor(QWidget):
|
||||
self.listWidget.setCurrentRow ( 0 )
|
||||
|
||||
def listEntryText(self, page_dict):
|
||||
text = page_dict['Image']
|
||||
text = str(int(page_dict['Image']) + 1)
|
||||
if 'Type' in page_dict:
|
||||
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
|
||||
return text
|
||||
@ -278,9 +243,9 @@ class PageListEditor(QWidget):
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.label.palette()
|
||||
active_palette = self.comboBox.palette()
|
||||
|
||||
inactive_palette3 = self.label.palette()
|
||||
inactive_palette3 = self.comboBox.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
|
||||
|
||||
@ -302,8 +267,7 @@ class PageListEditor(QWidget):
|
||||
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
def showEvent( self, event ):
|
||||
# make sure to adjust the size and pos of the pixmap based on frame size
|
||||
self.setDisplayPixmap( 0,0 )
|
||||
|
||||
|
||||
# make sure combo is disabled when no list
|
||||
if self.comic_archive is None:
|
||||
self.comboBox.setEnabled( False )
|
||||
|
@ -98,32 +98,13 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<widget class="QWidget" name="pageContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>90</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>151</width>
|
||||
<height>141</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -22,7 +22,7 @@ import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
import utils
|
||||
|
||||
class IDProgressWindow(QtGui.QDialog):
|
||||
|
||||
@ -31,13 +31,12 @@ class IDProgressWindow(QtGui.QDialog):
|
||||
super(IDProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# make font for scroll window a smidge smaller
|
||||
f = self.textEdit.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
self.textEdit.setFont( f )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
|
||||
|
@ -1,3 +1,38 @@
|
||||
---------------------------------
|
||||
1.1.0-beta - 06-Feb-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
* Enhanced identification process to use alternative covers from ComicVine
|
||||
* Post auto-tag manual matching now includes single low-confidence matches (CLI & GUI)
|
||||
* Page and cover view mini-browser available throughout app. Most images can be
|
||||
double-clicked for embiggened view
|
||||
* Export-to-zip in CLI (very handy in scripts!)
|
||||
* More rename template variables
|
||||
* Misc GUI & CLI Tweaks
|
||||
|
||||
---------------------------------
|
||||
1.0.3-beta - 31-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Misc bug fixes and enhancements
|
||||
|
||||
---------------------------------
|
||||
1.0.2-beta - 25-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
More verbose logging during auto-tag
|
||||
Added %month% and %month_name% for renaming
|
||||
Better parsing of volume numbers in file name
|
||||
Bugs:
|
||||
Better exception handling with corrupted image data
|
||||
Fixed issues with RAR reading on OS X
|
||||
Other minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.0.1-beta - 24-Jan-2013
|
||||
---------------------------------
|
||||
Bug Fix:
|
||||
Fixed an issue where unicode strings can't be printed to OS X Console
|
||||
|
||||
---------------------------------
|
||||
1.0.0-beta - 23-Jan-2013
|
||||
@ -68,4 +103,4 @@ Note:
|
||||
---------------------------------
|
||||
0.9.0-beta - 30-Nov-2012
|
||||
---------------------------------
|
||||
Initial beta release
|
||||
Initial beta release
|
||||
|
@ -36,6 +36,10 @@ class RenameWindow(QtGui.QDialog):
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
|
@ -63,7 +63,7 @@ class ComicTaggerSettings:
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Scholastic Book Services"
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
|
@ -56,8 +56,10 @@ class SettingsWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'settingswindow.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
|
||||
self.settings = settings
|
||||
self.name = "Settings"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
@ -79,7 +81,7 @@ class SettingsWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
nldtTip = (
|
||||
""" <html>The <b>Name Length Delta Threshold</b> is for eliminating automatic
|
||||
""" <html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The higher
|
||||
it is, the more likely to have a good match, but each search will take longer and
|
||||
use more bandwidth. Too low, and only the very closest lexical matches will be
|
||||
|
@ -263,7 +263,7 @@
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Name Length Delta Threshold:</string>
|
||||
<string>Default Name Length Match Tolerance:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -275,6 +275,12 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
@ -443,7 +449,20 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRenameTemplate">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%publisher%<br/>%title%</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
|
||||
%genre%<br/>
|
||||
%language_code%<br/>
|
||||
%criticalrating%<br/>
|
||||
%alternateseries%<br/>
|
||||
%alternatenumber%<br/>
|
||||
%alternatecount%<br/>
|
||||
%imprint%<br/>
|
||||
%format%<br/>
|
||||
%maturityrating%<br/>
|
||||
%storyarc%<br/>
|
||||
%seriesgroup%<br/>
|
||||
%scaninfo%
|
||||
</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
307
taggerwindow.py
307
taggerwindow.py
@ -1,6 +1,6 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
The main window of the comictagger app
|
||||
The main window of the ComicTagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
@ -29,6 +29,7 @@ import os
|
||||
import pprint
|
||||
import json
|
||||
import webbrowser
|
||||
import re
|
||||
|
||||
from volumeselectionwindow import VolumeSelectionWindow
|
||||
from options import MetaDataStyle
|
||||
@ -53,6 +54,8 @@ from issueidentifier import IssueIdentifier
|
||||
from autotagstartwindow import AutoTagStartWindow
|
||||
from autotagprogresswindow import AutoTagProgressWindow
|
||||
from autotagmatchwindow import AutoTagMatchWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
@ -61,6 +64,7 @@ class OnlineMatchResults():
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
@ -72,28 +76,6 @@ class MultipleMatch():
|
||||
# this reads the environment and inits the right locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
# helper func to allow a label to be clickable
|
||||
def clickable(widget):
|
||||
|
||||
class Filter(QtCore.QObject):
|
||||
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QtCore.QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
|
||||
|
||||
|
||||
class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
appName = "ComicTagger"
|
||||
@ -104,7 +86,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
|
||||
self.settings = settings
|
||||
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.pageListEditor = PageListEditor( self.tabPages )
|
||||
gridlayout = QtGui.QGridLayout( self.tabPages )
|
||||
gridlayout.addWidget( self.pageListEditor )
|
||||
@ -122,13 +109,6 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
#self.splitter.setHandleWidth(0)
|
||||
#self.splitter.handle(1).setDisabled(True)
|
||||
|
||||
# This is ugly, and should probably be done in a resource file
|
||||
if platform.system() == "Linux":
|
||||
self.scrollAreaWidgetContents.resize( self.scrollAreaWidgetContents.width(), 630)
|
||||
#if platform.system() == "Darwin":
|
||||
# self.scrollAreaWidgetContents.resize( 550, self.scrollAreaWidgetContents.height())
|
||||
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# walk through all the lablels in the main form, and make them
|
||||
# a smidge smaller
|
||||
@ -139,14 +119,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
f.setItalic( True )
|
||||
child.setFont( f )
|
||||
|
||||
#---------------------------
|
||||
|
||||
self.scrollAreaWidgetContents.adjustSize()
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
|
||||
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.save_data_style = settings.last_selected_save_data_style
|
||||
self.load_data_style = settings.last_selected_load_data_style
|
||||
|
||||
@ -155,6 +132,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.statusBar()
|
||||
self.populateComboBoxes()
|
||||
|
||||
self.page_browser = None
|
||||
self.resetApp()
|
||||
|
||||
# set up some basic field validators
|
||||
@ -185,13 +163,9 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.btnAddCredit.clicked.connect(self.addCredit)
|
||||
self.btnRemoveCredit.clicked.connect(self.removeCredit)
|
||||
self.twCredits.cellDoubleClicked.connect(self.editCredit)
|
||||
clickable(self.lblCover).connect(self.showPageBrowser)
|
||||
|
||||
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 )
|
||||
|
||||
self.updateStyleTweaks()
|
||||
|
||||
@ -201,6 +175,9 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.splitter.setSizes([ self.settings.last_form_side_width , self.settings.last_list_side_width])
|
||||
self.raise_()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
self.resizeEvent( None )
|
||||
|
||||
self.splitter.splitterMoved.connect( self.splitterMovedEvent )
|
||||
|
||||
self.fileSelectionList.addAppAction( self.actionAutoIdentify )
|
||||
self.fileSelectionList.addAppAction( self.actionAutoTag )
|
||||
@ -233,19 +210,18 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
def resetApp( self ):
|
||||
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.archiveCoverWidget.clear()
|
||||
self.comic_archive = None
|
||||
self.dirtyFlag = False
|
||||
self.clearForm()
|
||||
self.pageListEditor.resetPage()
|
||||
|
||||
if self.page_browser is not None:
|
||||
self.page_browser.reset()
|
||||
self.updateAppTitle()
|
||||
self.updateMenus()
|
||||
self.updateInfoBox()
|
||||
|
||||
self.droppedFile = None
|
||||
self.page_browser = None
|
||||
self.page_loader = None
|
||||
|
||||
|
||||
@ -316,6 +292,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionRename.setStatusTip( 'Rename archive based on tags' )
|
||||
self.actionRename.triggered.connect( self.renameArchive )
|
||||
|
||||
self.actionSettings.setShortcut( 'Ctrl+Shift+S' )
|
||||
self.actionSettings.setStatusTip( 'Configure ComicTagger' )
|
||||
self.actionSettings.triggered.connect( self.showSettings )
|
||||
|
||||
@ -390,18 +367,22 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if rar_count != 0:
|
||||
dlg = ExportWindow( self, self.settings,
|
||||
self.tr("You have selected {0} archive(s) to export to Zip format. New archives will be created in the same folder as the original.\n\nPlease choose options below, and select OK.\n".format(rar_count) ))
|
||||
dlg.adjustSize( )
|
||||
dlg.setModal( True )
|
||||
if not dlg.exec_():
|
||||
return
|
||||
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, rar_count, self)
|
||||
progdialog.setWindowTitle( "Exporting as ZIP" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
prog_idx = 0
|
||||
|
||||
new_archives_to_add = []
|
||||
archives_to_remove = []
|
||||
skipped_list = []
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
|
||||
for ca in ca_list:
|
||||
if ca.isRar():
|
||||
@ -411,6 +392,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
original_path = os.path.abspath( ca.path )
|
||||
@ -419,11 +401,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if os.path.lexists( export_name ):
|
||||
if dlg.fileConflictBehavior == ExportConflictOpts.dontCreate:
|
||||
export_name = None
|
||||
skipped_list.append( ca.path )
|
||||
elif dlg.fileConflictBehavior == ExportConflictOpts.createUnique:
|
||||
export_name = utils.unique_file( export_name )
|
||||
|
||||
if export_name is not None:
|
||||
if ca.exportAsZip( export_name ):
|
||||
success_count += 1
|
||||
if dlg.addToList:
|
||||
new_archives_to_add.append( export_name )
|
||||
if dlg.deleteOriginal:
|
||||
@ -431,15 +415,31 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
os.unlink( ca.path )
|
||||
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Export as Zip Archive"),
|
||||
self.tr("An error occured while exporting {0} to {1}. Batch operation aborted!".format(original_path, export_name)))
|
||||
break
|
||||
# last export failed, so remove the zip, if it exists
|
||||
failed_list.append( ca.path )
|
||||
if os.path.lexists( export_name ):
|
||||
os.remove( export_name )
|
||||
|
||||
progdialog.close()
|
||||
|
||||
self.fileSelectionList.addPathList( new_archives_to_add )
|
||||
self.fileSelectionList.removeArchiveList( archives_to_remove )
|
||||
# ATB maybe show a summary of files created here...?
|
||||
|
||||
summary = u"Successfully created {0} Zip archive(s).".format( success_count )
|
||||
if len( skipped_list ) > 0:
|
||||
summary += u"\n\nThe following {0} RAR archive(s) were skipped due to file name conflicts:\n".format( len( skipped_list ) )
|
||||
for f in skipped_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
if len( failed_list ) > 0:
|
||||
summary += u"\n\nThe following {0} RAR archive(s) failed to export due to read/write errors:\n".format( len( failed_list ) )
|
||||
for f in failed_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
|
||||
dlg = LogWindow( self )
|
||||
dlg.setText( summary )
|
||||
dlg.setWindowTitle( "Archive Export to Zip Summary" )
|
||||
dlg.exec_()
|
||||
|
||||
|
||||
def aboutApp( self ):
|
||||
|
||||
@ -485,6 +485,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def actualLoadCurrentArchive( self ):
|
||||
if self.metadata.isEmpty:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
if len(self.metadata.pages) == 0:
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
self.updateCoverImage()
|
||||
@ -498,21 +499,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.clearDirtyFlag() # also updates the app title
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
self.updateAppTitle()
|
||||
|
||||
def updateCoverImage( self ):
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
cover_idx = self.metadata.getCoverPageIndexList()[0]
|
||||
|
||||
self.page_loader = PageLoader( self.comic_archive, cover_idx )
|
||||
self.page_loader.loadComplete.connect( self.actualUpdateCoverImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualUpdateCoverImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(img))
|
||||
self.lblCover.setScaledContents(True)
|
||||
self.archiveCoverWidget.setArchive( self.comic_archive, cover_idx)
|
||||
|
||||
def updateMenus( self ):
|
||||
|
||||
@ -754,10 +745,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
item_text = role
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
self.twCredits.setItem(row, 1, item)
|
||||
|
||||
|
||||
item_text = name
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twCredits.setItem(row, 2, item)
|
||||
|
||||
@ -988,6 +982,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.formToMetadata()
|
||||
|
||||
success = self.comic_archive.writeMetadata( self.metadata, self.save_data_style )
|
||||
self.comic_archive.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
@ -1384,10 +1379,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, has_md_count, self)
|
||||
progdialog.setWindowTitle( "Removing Tags" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
prog_idx = 0
|
||||
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.hasMetadata( style ):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1396,21 +1393,32 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.hasMetadata( style ) and ca.isWritable():
|
||||
if not ca.removeMetadata( style ):
|
||||
if has_md_count == 1:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"), self.tr("The tag removal operation seemed to fail!"))
|
||||
else:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"),
|
||||
self.tr("The tag removal operation seemed to fail for {0} Operation aborted!".format(ca.path)))
|
||||
break
|
||||
failed_list.append( ca.path )
|
||||
else:
|
||||
success_count += 1
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
progdialog.close()
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
summary = u"Successfully removed tags in {0} archive(s).".format( success_count )
|
||||
if len( failed_list ) > 0:
|
||||
summary += u"\n\nThe remove operation failed in the following {0} archive(s):\n".format( len( failed_list ) )
|
||||
for f in failed_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
|
||||
dlg = LogWindow( self )
|
||||
dlg.setText( summary )
|
||||
dlg.setWindowTitle( "Tag Remove Summary" )
|
||||
#dlg.adjustSize()
|
||||
dlg.exec_()
|
||||
|
||||
def copyTags( self ):
|
||||
# copy the indicated tags in the archive
|
||||
@ -1448,10 +1456,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, has_src_count, self)
|
||||
progdialog.setWindowTitle( "Copying Tags" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
prog_idx = 0
|
||||
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.hasMetadata( src_style ):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1460,6 +1470,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.hasMetadata( src_style ) and ca.isWritable():
|
||||
@ -1469,16 +1480,27 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
md = CBLTransformer( md, self.settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, dest_style ):
|
||||
if has_md_count == 1:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"), self.tr("The tag removal operation seemed to fail!"))
|
||||
else:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"),
|
||||
self.tr("The tag removal operation seemed to fail for {0} Operation aborted!".format(ca.path)))
|
||||
break
|
||||
failed_list.append( ca.path )
|
||||
else:
|
||||
success_count += 1
|
||||
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
progdialog.close()
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
summary = u"Successfully copied tags in {0} archive(s).".format( success_count )
|
||||
if len( failed_list ) > 0:
|
||||
summary += u"\n\nThe copy operation failed in the following {0} archive(s):\n".format( len( failed_list ) )
|
||||
for f in failed_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
|
||||
dlg = LogWindow( self )
|
||||
dlg.setText( summary )
|
||||
dlg.setWindowTitle( "Tag Copy Summary" )
|
||||
dlg.exec_()
|
||||
|
||||
def actualIssueDataFetch( self, match ):
|
||||
|
||||
@ -1499,7 +1521,15 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
return cv_md
|
||||
|
||||
|
||||
def autoTagLog( self, text ):
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
if self.atprogdialog is not None:
|
||||
self.atprogdialog.textEdit.insertPlainText(text)
|
||||
self.atprogdialog.textEdit.ensureCursorVisible()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def identifyAndTagSingleArchive( self, ca, match_results, dlg):
|
||||
success = False
|
||||
ii = IssueIdentifier( ca, self.settings )
|
||||
@ -1507,29 +1537,29 @@ 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()
|
||||
if dlg.ignoreLeadingDigitsInFilename and md.series is not None:
|
||||
#remove all leading numbers
|
||||
md.series = re.sub( "([\d.]*)(.*)", "\\2", md.series)
|
||||
|
||||
# use the dialog specified search string
|
||||
if dlg.searchString is not None:
|
||||
md.series = dlg.searchString
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "!!!!No metadata given to search online with!"
|
||||
return False, match_results
|
||||
|
||||
def myoutput( text ):
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
self.atprogdialog.textEdit.ensureCursorVisible()
|
||||
self.atprogdialog.textEdit.insertPlainText(text)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if dlg.dontUseYear:
|
||||
md.year = None
|
||||
if dlg.assumeIssueOne and ( md.issue is None or md.issue == ""):
|
||||
md.issue = "1"
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.setOutputFunction( self.autoTagLog )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
ii.setCoverURLCallback( self.atprogdialog.setTestImage )
|
||||
ii.setCoverURLCallback( self.atprogdialog.setTestImage )
|
||||
ii.setNameLengthDeltaThreshold( dlg.nameLengthMatchTolerance )
|
||||
|
||||
matches = ii.search()
|
||||
|
||||
@ -1538,6 +1568,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
no_match = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
@ -1555,32 +1586,40 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(ca,matches))
|
||||
if low_confidence:
|
||||
self.autoTagLog( "Online search: Multiple low-confidence matches. Save aborted\n" )
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(ca,matches))
|
||||
else:
|
||||
self.autoTagLog( "Online search: Multiple matches. Save aborted\n" )
|
||||
match_results.multipleMatches.append(MultipleMatch(ca,matches))
|
||||
elif low_confidence and not dlg.autoSaveOnLow:
|
||||
print "Online search: Low confidence match. Save aborted"
|
||||
match_results.noMatches.append(ca.path)
|
||||
self.autoTagLog( "Online search: Low confidence match. Save aborted\n" )
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(ca,matches))
|
||||
elif not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
self.autoTagLog( "Online search: No match found. Save aborted\n" )
|
||||
match_results.noMatches.append(ca.path)
|
||||
else:
|
||||
|
||||
# a single match!
|
||||
if low_confidence:
|
||||
self.autoTagLog( "Online search: Low confidence match, but saving anyways, as indicated...\n" )
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.actualIssueDataFetch( matches[0] )
|
||||
if cv_md is None:
|
||||
match_results.fetchDataFailures.append(ca.path)
|
||||
|
||||
if cv_md is not None:
|
||||
|
||||
if cv_md is not None:
|
||||
md.overlay( cv_md )
|
||||
|
||||
if not ca.writeMetadata( md, self.save_data_style ):
|
||||
match_results.writeFailures.append(ca.path)
|
||||
self.autoTagLog( "Save failed ;-(\n" )
|
||||
else:
|
||||
match_results.goodMatches.append(ca.path)
|
||||
success = True
|
||||
self.autoTagLog( "Save complete!\n" )
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
return success, match_results
|
||||
|
||||
def autoTag( self ):
|
||||
@ -1597,23 +1636,29 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
atstartdlg = AutoTagStartWindow( self, self.settings,
|
||||
self.tr("You have selected {0} archive(s) to automatically identify and write {1} tags to.\n\n".format(len(ca_list), MetaDataStyle.name[style]) +
|
||||
"Please choose options below, and select OK.\n" ))
|
||||
"Please choose options below, and select OK to Auto-Tag.\n" ))
|
||||
atstartdlg.adjustSize( )
|
||||
atstartdlg.setModal( True )
|
||||
if not atstartdlg.exec_():
|
||||
return
|
||||
|
||||
|
||||
self.atprogdialog = AutoTagProgressWindow( self)
|
||||
self.atprogdialog.setModal(True)
|
||||
#self.progdialog.rejected.connect( self.identifyCancel )
|
||||
self.atprogdialog.show()
|
||||
self.atprogdialog.progressBar.setMaximum( len(ca_list) )
|
||||
self.atprogdialog.setWindowTitle( "Auto-Tagging" )
|
||||
|
||||
|
||||
self.autoTagLog( u"========================================================================\n" )
|
||||
self.autoTagLog( u"Auto-Tagging Started for {0} items\n".format(len(ca_list)))
|
||||
|
||||
prog_idx = 0
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
archives_to_remove = []
|
||||
for ca in ca_list:
|
||||
self.autoTagLog( u"============================================================\n" )
|
||||
self.autoTagLog( u"Auto-Tagging {0} of {1}\n".format(prog_idx+1, len(ca_list)))
|
||||
self.autoTagLog( u"{0}\n".format(ca.path) )
|
||||
cover_idx = ca.readMetadata(style).getCoverPageIndexList()[0]
|
||||
image_data = ca.getPage( cover_idx )
|
||||
self.atprogdialog.setArchiveImage( image_data )
|
||||
@ -1625,36 +1670,50 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.atprogdialog.progressBar.setValue( prog_idx )
|
||||
prog_idx += 1
|
||||
self.atprogdialog.label.setText( ca.path )
|
||||
utils.centerWindowOnParent( self.atprogdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.isWritable():
|
||||
success, match_results = self.identifyAndTagSingleArchive( ca, match_results, atstartdlg )
|
||||
|
||||
self.atprogdialog.close()
|
||||
if success and atstartdlg.removeAfterSuccess:
|
||||
archives_to_remove.append( ca )
|
||||
|
||||
self.atprogdialog.close()
|
||||
|
||||
if atstartdlg.removeAfterSuccess:
|
||||
self.fileSelectionList.removeArchiveList( archives_to_remove )
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
|
||||
self.loadArchive( self.fileSelectionList.getCurrentArchive() )
|
||||
self.atprogdialog = None
|
||||
|
||||
summary = ""
|
||||
summary += "Successfully tagged archives: {0}\n".format( len(match_results.goodMatches))
|
||||
|
||||
if len ( match_results.multipleMatches ) > 0:
|
||||
summary += "Archives with multiple matches: {0}\n".format( len(match_results.multipleMatches))
|
||||
if len ( match_results.noMatches ) > 0:
|
||||
summary += "Archives with no matches: {0}\n".format( len(match_results.noMatches))
|
||||
if len ( match_results.fetchDataFailures ) > 0:
|
||||
summary += "Archives that failed due to data fetch errors: {0}\n".format( len(match_results.fetchDataFailures))
|
||||
if len ( match_results.writeFailures ) > 0:
|
||||
summary += "Archives that failed due to file writing errors: {0}\n".format( len(match_results.writeFailures))
|
||||
summary = u""
|
||||
summary += u"Successfully tagged archives: {0}\n".format( len(match_results.goodMatches))
|
||||
|
||||
if len ( match_results.multipleMatches ) > 0:
|
||||
summary += "\n\nDo you want to manually select the ones with multiple matches now?"
|
||||
summary += u"Archives with multiple matches: {0}\n".format( len(match_results.multipleMatches))
|
||||
if len ( match_results.lowConfidenceMatches ) > 0:
|
||||
summary += u"Archives with one or more low-confidence matches: {0}\n".format( len(match_results.lowConfidenceMatches))
|
||||
if len ( match_results.noMatches ) > 0:
|
||||
summary += u"Archives with no matches: {0}\n".format( len(match_results.noMatches))
|
||||
if len ( match_results.fetchDataFailures ) > 0:
|
||||
summary += u"Archives that failed due to data fetch errors: {0}\n".format( len(match_results.fetchDataFailures))
|
||||
if len ( match_results.writeFailures ) > 0:
|
||||
summary += u"Archives that failed due to file writing errors: {0}\n".format( len(match_results.writeFailures))
|
||||
|
||||
self.autoTagLog( summary )
|
||||
|
||||
sum_selectable = len ( match_results.multipleMatches ) + len(match_results.lowConfidenceMatches)
|
||||
if sum_selectable > 0:
|
||||
summary += u"\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
|
||||
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Auto-Tag Summary"),
|
||||
self.tr(u"Auto-Tag Summary"),
|
||||
self.tr(summary),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
|
||||
match_results.multipleMatches.extend( match_results.lowConfidenceMatches )
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
matchdlg = AutoTagMatchWindow( self, match_results.multipleMatches, style, self.actualIssueDataFetch)
|
||||
matchdlg.setModal( True )
|
||||
@ -1779,4 +1838,20 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actualLoadCurrentArchive()
|
||||
|
||||
def fileListCleared( self ):
|
||||
self.resetApp()
|
||||
self.resetApp()
|
||||
|
||||
def splitterMovedEvent( self, w1, w2 ):
|
||||
scrollbar_w = 0
|
||||
if self.scrollArea.verticalScrollBar().isVisible():
|
||||
scrollbar_w = self.scrollArea.verticalScrollBar().width()
|
||||
|
||||
new_w = self.scrollArea.width() - scrollbar_w - 5
|
||||
self.scrollAreaWidgetContents.resize( new_w, self.scrollAreaWidgetContents.height())
|
||||
|
||||
def resizeEvent( self, ev ):
|
||||
self.splitterMovedEvent( 0, 0)
|
||||
|
||||
def tabChanged( self, idx ):
|
||||
if idx == 0:
|
||||
self.splitterMovedEvent( 0, 0)
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>571</height>
|
||||
<height>575</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -202,7 +202,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblCover">
|
||||
<widget class="QWidget" name="coverImageContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@ -211,31 +211,16 @@
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<height>330</height>
|
||||
<width>230</width>
|
||||
<height>380</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<height>330</height>
|
||||
<width>230</width>
|
||||
<height>380</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -301,13 +286,13 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>-64</y>
|
||||
<y>0</y>
|
||||
<width>470</width>
|
||||
<height>520</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -1107,7 +1092,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>25</height>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
||||
|
41
todo.txt
41
todo.txt
@ -1,47 +1,29 @@
|
||||
-----------------------------------------------------
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
|
||||
New menu graphics
|
||||
open folder vs file
|
||||
|
||||
Rename dialog:
|
||||
check-box for rows?
|
||||
manual edit the preview?
|
||||
|
||||
Multi-file:
|
||||
|
||||
Batch Functions:
|
||||
Auto-Tag
|
||||
Interactive dialog at end
|
||||
Manually change cover image on left??
|
||||
|
||||
Rename
|
||||
check-box for rows?
|
||||
manual edit the preview?
|
||||
|
||||
Docs:
|
||||
Explain the style comboboxes
|
||||
Basics of Batch operations
|
||||
Define Manual Search - Manual
|
||||
vs Auto-Identify - Uses form, doesn't save
|
||||
vs Auto-Tag - Doesn't use form, saves
|
||||
Screen shots
|
||||
|
||||
Auto-Tagging Tips:
|
||||
Multiple Passes with different options
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
RAR Password -- childrens crusade 3
|
||||
|
||||
Zip flakes out when filename differs from index (or whatever) i.e "\" vs "/". Python issue
|
||||
|
||||
-----------------------------------------------------
|
||||
Big Future Features
|
||||
-----------------------------------------------------
|
||||
Scrape alternate Covers from ComicVine issue pages
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
Auto search:
|
||||
Searching w/o issue #
|
||||
|
||||
Form Mode: Single vs Batch
|
||||
Batch Edit
|
||||
Form Mode: Single vs Batch
|
||||
|
||||
-----------------------------------------------------
|
||||
Small(er) Future Feature
|
||||
@ -92,3 +74,4 @@ Release Process
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
rename 's/([A-Za-z]+)(\d+)(.cb[rz])/$1 $2$3/' *.cb?
|
||||
|
46
utils.py
46
utils.py
@ -520,3 +520,49 @@ def getLanguageFromISO( iso ):
|
||||
else:
|
||||
return lang_dict[ iso ]
|
||||
|
||||
|
||||
try:
|
||||
from PyQt4 import QtGui
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
|
||||
if qt_available:
|
||||
def reduceWidgetFontSize( widget , delta = 2):
|
||||
f = widget.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - delta )
|
||||
widget.setFont( f )
|
||||
|
||||
def centerWindowOnScreen( window ):
|
||||
"""
|
||||
Center the window on screen. This implemention will handle the window
|
||||
being resized or the screen resolution changing.
|
||||
"""
|
||||
# Get the current screens' dimensions...
|
||||
screen = QtGui.QDesktopWidget().screenGeometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calulated as screenwidth - windowwidth /2
|
||||
hpos = ( screen.width() - window.width() ) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = ( screen.height() - window.height() ) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos, vpos)
|
||||
|
||||
def centerWindowOnParent( window ):
|
||||
|
||||
top_level = window
|
||||
while top_level.parent() is not None:
|
||||
top_level = top_level.parent()
|
||||
|
||||
# Get the current screens' dimensions...
|
||||
main_window_size = top_level.geometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calulated as screenwidth - windowwidth /2
|
||||
hpos = ( main_window_size.width() - window.width() ) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = ( main_window_size.height() - window.height() ) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos + main_window_size.left(), vpos + main_window_size.top())
|
||||
|
@ -34,6 +34,8 @@ from imagefetcher import ImageFetcher
|
||||
from progresswindow import IDProgressWindow
|
||||
from settings import ComicTaggerSettings
|
||||
from matchselectionwindow import MatchSelectionWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class SearchThread( QtCore.QThread):
|
||||
|
||||
@ -90,6 +92,18 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
|
||||
|
||||
self.imageWidget = CoverImageWidget( self.imageContainer, CoverImageWidget.URLMode )
|
||||
gridlayout = QtGui.QGridLayout( self.imageContainer )
|
||||
gridlayout.addWidget( self.imageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.teDetails, 1 )
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
@ -108,7 +122,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def updateButtons( self ):
|
||||
@ -180,54 +194,43 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
result = self.ii.search_result
|
||||
match_index = 0
|
||||
|
||||
found_match = False
|
||||
found_match = None
|
||||
choices = False
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = True
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage :
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but not with the first page of the archive.")
|
||||
found_match = True
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually.")
|
||||
choices = True
|
||||
elif result == self.ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleGoodMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
selector = MatchSelectionWindow( self, matches )
|
||||
selector = MatchSelectionWindow( self, matches, self.comic_archive )
|
||||
selector.setModal(True)
|
||||
|
||||
title = self.series_name
|
||||
title += " #" + self.issue_number
|
||||
if self.year is not None:
|
||||
title += " (" + str(self.year) + ")"
|
||||
title += " - "
|
||||
|
||||
selector.setWindowTitle( title + "Select Match")
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a list index
|
||||
found_match = True
|
||||
match_index = selector.current_row
|
||||
found_match = selector.currentMatch()
|
||||
|
||||
if found_match:
|
||||
if found_match is not None:
|
||||
self.iddialog.accept()
|
||||
|
||||
self.volume_id = matches[match_index]['volume_id']
|
||||
self.issue_number = matches[match_index]['issue_number']
|
||||
self.volume_id = found_match['volume_id']
|
||||
self.issue_number = found_match['issue_number']
|
||||
self.selectByID()
|
||||
self.showIssues()
|
||||
|
||||
def showIssues( self ):
|
||||
selector = IssueSelectionWindow( self, self.settings, self.volume_id, self.issue_number )
|
||||
selector.setModal(True)
|
||||
|
||||
title = ""
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
@ -237,6 +240,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
break
|
||||
|
||||
selector.setWindowTitle( title + "Select Issue")
|
||||
selector.setModal( True )
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a volume ID
|
||||
@ -304,23 +308,27 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem( item_text )
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = str(record['start_year'])
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record['count_of_issues']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if record['publisher'] is not None:
|
||||
item_text = record['publisher']['name']
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
@ -363,21 +371,5 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
if record['id'] == self.volume_id:
|
||||
|
||||
self.teDetails.setText ( record['description'] )
|
||||
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
url = record['image']['super_url']
|
||||
self.fetcher = ImageFetcher( )
|
||||
self.fetcher.fetchComplete.connect(self.finishRequest)
|
||||
self.fetcher.fetch( url, user_data=record['id'] )
|
||||
|
||||
|
||||
def finishRequest(self, image_data, user_data):
|
||||
# called when the image is done loading
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.setCover( img )
|
||||
|
||||
|
||||
def setCover( self, img ):
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
self.imageWidget.setURL( record['image']['super_url'] )
|
||||
break
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>801</width>
|
||||
<height>470</height>
|
||||
<width>849</width>
|
||||
<height>476</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -20,7 +20,7 @@
|
||||
<item row="0" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<widget class="QWidget" name="imageContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
@ -33,18 +33,6 @@
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -67,11 +55,6 @@
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
@ -130,11 +113,6 @@
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -149,7 +127,7 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnAutoSelect">
|
||||
<property name="text">
|
||||
<string>Auto-Select</string>
|
||||
<string>Auto-Identify</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
Reference in New Issue
Block a user