Compare commits
107 Commits
0.9.0-beta
...
1.0.1-beta
Author | SHA1 | Date | |
---|---|---|---|
79a9cf1b40 | |||
6e7d7bcc47 | |||
d96690c351 | |||
c44c240eef | |||
ddc225c2be | |||
d9cdb14aa6 | |||
cab525675d | |||
e9321b741e | |||
4143ca3314 | |||
667c21bbed | |||
37048b99fc | |||
e839b008c6 | |||
ba3673a4c0 | |||
221923607a | |||
b712226b1e | |||
b8e8c6433a | |||
be3b0fe92c | |||
d26441306a | |||
f2b1db5479 | |||
0cd10f3f75 | |||
97dc36b8fb | |||
d58e033689 | |||
c3d5d44788 | |||
2bf9b9ed7c | |||
cfca394bcb | |||
7a7adc1c3f | |||
41f730a558 | |||
550b84361c | |||
fb4248fda2 | |||
9626c3fd77 | |||
3f305c6788 | |||
9e68516dac | |||
8f45994b9a | |||
4ea56c0bd0 | |||
5445417404 | |||
db6423aea9 | |||
aa62a3e8ff | |||
cd1733a975 | |||
c81319402d | |||
8a8e53d9c9 | |||
7614e95084 | |||
bd9f314496 | |||
bebd09d3f6 | |||
8a5430c83e | |||
93be1b42f4 | |||
01be389fad | |||
ca9aaf9279 | |||
ee9175087e | |||
94c5882175 | |||
ff74b3e5bc | |||
0017903a4f | |||
3d98118fa9 | |||
faf0b5d437 | |||
e14c9dfe19 | |||
4343f3f08d | |||
4a94bf4d6f | |||
a602c42f0e | |||
1efdc0e623 | |||
152040964e | |||
584f78bc3c | |||
3f1868222d | |||
45b94ce1fd | |||
7289f6915a | |||
a5d39a88c8 | |||
2acf2f60f3 | |||
f6ff6c3b73 | |||
6b88fb7e58 | |||
3364e437c6 | |||
1e5f40121c | |||
2a347522e4 | |||
7f1ce793e3 | |||
f7cb6e9d2b | |||
487c8a5bf4 | |||
5b8f73528b | |||
8af7651a50 | |||
1e3d8ccad3 | |||
c367b8806b | |||
d3ea8d1b2c | |||
c5f1542874 | |||
ab5d8599ac | |||
a2d0068522 | |||
c6c5728cb3 | |||
e6f63beee2 | |||
72af8f8564 | |||
5390a92b98 | |||
c814436899 | |||
dbec1999dc | |||
a970ed0e36 | |||
6d8d90d5b7 | |||
117d8d8998 | |||
3689317518 | |||
c845c786e4 | |||
9ccdc60c19 | |||
aec0477170 | |||
134dcbaba3 | |||
f040f8dc74 | |||
948acf9b23 | |||
3c2f4fa662 | |||
f99d466bae | |||
a773ab6539 | |||
ff2fca44f4 | |||
97fe437bb4 | |||
32aabb100b | |||
b385be4338 | |||
deeeef90a6 | |||
121889ed1b | |||
d300f51c7f |
6
Makefile
6
Makefile
@ -16,5 +16,9 @@ zip:
|
||||
rm -rf comictagger-src-$(VERSION_STR)
|
||||
|
||||
@echo When satisfied with release, do this:
|
||||
@echo svn fpoooo $(VERSION_STR)
|
||||
@echo make svn_tag
|
||||
|
||||
svn_tag:
|
||||
svn copy https://comictagger.googlecode.com/svn/trunk \
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
202
autotagmatchwindow.py
Normal file
202
autotagmatchwindow.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, match_set_list, style, fetch_func):
|
||||
super(AutoTagMatchWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip"))
|
||||
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Next")
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
self.fetch_func = fetch_func
|
||||
|
||||
self.current_match_set_idx = 0
|
||||
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.skipButton.clicked.connect(self.skipToNext)
|
||||
|
||||
self.updateData()
|
||||
|
||||
def updateData( self):
|
||||
|
||||
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.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.current_match_set_idx+1,
|
||||
len( self.match_set_list ),
|
||||
os.path.split(path)[1] ))
|
||||
|
||||
def populateTable( self ):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for match in self.current_match_set.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = ""
|
||||
if match['month'] is not None:
|
||||
item_text = u"{0}/".format(match['month'])
|
||||
if match['year'] is not None:
|
||||
item_text += u"{0}".format(match['year'])
|
||||
else:
|
||||
item_text += u"????"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
row += 1
|
||||
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.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))
|
||||
|
||||
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' )))
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.saveMatch()
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len( self.match_set_list ):
|
||||
# no more items
|
||||
QtGui.QDialog.accept(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def skipToNext( self ):
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len( self.match_set_list ):
|
||||
# no more items
|
||||
QtGui.QDialog.reject(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def reject(self):
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Cancel Matching"),
|
||||
self.tr("Are you sure you wish to cancel the matching process?"),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
QtGui.QDialog.reject(self)
|
||||
|
||||
def saveMatch( self ):
|
||||
|
||||
match = self.current_match_set.matches[self.current_row]
|
||||
ca = self.current_match_set.ca
|
||||
|
||||
md = ca.readMetadata( self.style )
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.fetch_func( match )
|
||||
if cv_md is None:
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!"))
|
||||
return
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay( cv_md )
|
||||
success = ca.writeMetadata( md, self.style )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))
|
161
autotagmatchwindow.ui
Normal file
161
autotagmatchwindow.ui
Normal file
@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogMatchSelect</class>
|
||||
<widget class="QDialog" name="dialogMatchSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>831</width>
|
||||
<height>506</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelCover">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</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>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</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>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
67
autotagprogresswindow.py
Normal file
67
autotagprogresswindow.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
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 AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
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 )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
|
||||
def setTestImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblTest )
|
||||
|
||||
def setCoverImage( self, img_data , label):
|
||||
if img_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( img_data )
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
label.setScaledContents(True)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
||||
|
150
autotagprogresswindow.ui
Normal file
150
autotagprogresswindow.ui
Normal file
@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogIssueSelect</class>
|
||||
<widget class="QDialog" name="dialogIssueSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>900</width>
|
||||
<height>413</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Issue Identification Progress</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="invertedAppearance">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="lblTest">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
55
autotagstartwindow.py
Normal file
55
autotagstartwindow.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxSaveOnLowConfidence.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAssumeIssueOne.setCheckState( QtCore.Qt.Unchecked )
|
||||
|
||||
self.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
|
||||
|
124
autotagstartwindow.ui
Normal file
124
autotagstartwindow.ui
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogExport</class>
|
||||
<widget class="QDialog" name="dialogExport">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>524</width>
|
||||
<height>248</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Auto-Tag</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">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<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">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save on low confidence match</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
99
cbltransformer.py
Normal file
99
cbltransformer.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""
|
||||
Class to manage modifying metadata specifically for CBL/CBI
|
||||
"""
|
||||
|
||||
"""
|
||||
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
|
||||
import utils
|
||||
|
||||
|
||||
class CBLTransformer:
|
||||
def __init__( self, metadata, settings ):
|
||||
self.metadata = metadata
|
||||
self.settings = settings
|
||||
|
||||
|
||||
def apply( self ):
|
||||
# helper funcs
|
||||
def append_to_tags_if_unique( item ):
|
||||
if item.lower() not in (tag.lower() for tag in self.metadata.tags):
|
||||
self.metadata.tags.append( item )
|
||||
|
||||
def add_string_list_to_tags( str_list ):
|
||||
if str_list is not None and str_list != "":
|
||||
items = [ s.strip() for s in str_list.split(',') ]
|
||||
for item in items:
|
||||
append_to_tags_if_unique( item )
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
|
||||
# helper
|
||||
def setLonePrimary( role_list ):
|
||||
lone_credit = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
if c['role'].lower() in role_list:
|
||||
count += 1
|
||||
lone_credit = c
|
||||
if count > 1:
|
||||
lone_credit = None
|
||||
break
|
||||
if lone_credit is not None:
|
||||
lone_credit['primary'] = True
|
||||
return lone_credit, count
|
||||
|
||||
#need to loop three times, once for 'writer', 'artist', and then 'penciler' if no artist
|
||||
setLonePrimary( ['writer'] )
|
||||
c, count = setLonePrimary( ['artist'] )
|
||||
if c is None and count == 0:
|
||||
c, count = setLonePrimary( ['penciler', 'penciller'] )
|
||||
if c is not None:
|
||||
c['primary'] = False
|
||||
self.metadata.addCredit( c['person'], 'Artist', True )
|
||||
|
||||
if self.settings.copy_characters_to_tags:
|
||||
add_string_list_to_tags( self.metadata.characters )
|
||||
|
||||
if self.settings.copy_teams_to_tags:
|
||||
add_string_list_to_tags( self.metadata.teams )
|
||||
|
||||
if self.settings.copy_locations_to_tags:
|
||||
add_string_list_to_tags( self.metadata.locations )
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.metadata.webLink is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.webLink
|
||||
|
||||
return self.metadata
|
||||
|
||||
|
||||
|
260
comet.py
Normal file
260
comet.py
Normal file
@ -0,0 +1,260 @@
|
||||
"""
|
||||
A python class to encapsulate CoMet data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import zipfile
|
||||
from pprint import pprint
|
||||
import xml.etree.ElementTree as ET
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
|
||||
class CoMet:
|
||||
|
||||
writer_synonyms = ['writer', 'plotter', 'scripter']
|
||||
penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ]
|
||||
inker_synonyms = [ 'inker', 'artist', 'finishes' ]
|
||||
colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ]
|
||||
letterer_synonyms = [ 'letterer']
|
||||
cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ]
|
||||
editor_synonyms = [ 'editor']
|
||||
|
||||
def metadataFromString( self, string ):
|
||||
|
||||
tree = ET.ElementTree(ET.fromstring( string ))
|
||||
return self.convertXMLToMetadata( tree )
|
||||
|
||||
def stringFromMetadata( self, metadata ):
|
||||
|
||||
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
|
||||
tree = self.convertMetadataToXML( self, metadata )
|
||||
return header + ET.tostring(tree.getroot())
|
||||
|
||||
def indent( self, elem, level=0 ):
|
||||
# for making the XML output readable
|
||||
i = "\n" + level*" "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
self.indent( elem, level+1 )
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def convertMetadataToXML( self, filename, metadata ):
|
||||
|
||||
#shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
# build a tree structure
|
||||
root = ET.Element("comet")
|
||||
root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/"
|
||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib['xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
|
||||
|
||||
#helper func
|
||||
def assign( comet_entry, md_entry):
|
||||
if md_entry is not None:
|
||||
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
|
||||
|
||||
# title is manditory
|
||||
if md.title is None:
|
||||
md.title = ""
|
||||
assign( 'title', md.title )
|
||||
assign( 'series', md.series )
|
||||
assign( 'issue', md.issue ) #must be int??
|
||||
assign( 'volume', md.volume )
|
||||
assign( 'description', md.comments )
|
||||
assign( 'publisher', md.publisher )
|
||||
assign( 'pages', md.pageCount )
|
||||
assign( 'format', md.format )
|
||||
assign( 'language', md.language )
|
||||
assign( 'rating', md.maturityRating )
|
||||
assign( 'price', md.price )
|
||||
assign( 'isVersionOf', md.isVersionOf )
|
||||
assign( 'rights', md.rights )
|
||||
assign( 'identifier', md.identifier )
|
||||
assign( 'lastMark', md.lastMark )
|
||||
assign( 'genre', md.genre ) # TODO repeatable
|
||||
|
||||
if md.characters is not None:
|
||||
char_list = [ c.strip() for c in md.characters.split(',') ]
|
||||
for c in char_list:
|
||||
assign( 'character', c )
|
||||
|
||||
if md.manga is not None and md.manga == "YesAndRightToLeft":
|
||||
assign( 'readingDirection', "rtl")
|
||||
|
||||
date_str = ""
|
||||
if md.year is not None:
|
||||
date_str = str(md.year).zfill(4)
|
||||
if md.month is not None:
|
||||
date_str += "-" + str(md.month).zfill(2)
|
||||
assign( 'date', date_str )
|
||||
|
||||
assign( 'coverImage', md.coverImage )
|
||||
|
||||
# need to specially process the credits, since they are structured differently than CIX
|
||||
credit_writer_list = list()
|
||||
credit_penciller_list = list()
|
||||
credit_inker_list = list()
|
||||
credit_colorist_list = list()
|
||||
credit_letterer_list = list()
|
||||
credit_cover_list = list()
|
||||
credit_editor_list = list()
|
||||
|
||||
# loop thru credits, and build a list for each role that CoMet supports
|
||||
for credit in metadata.credits:
|
||||
|
||||
if credit['role'].lower() in set( self.writer_synonyms ):
|
||||
ET.SubElement(root, 'writer').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.penciller_synonyms ):
|
||||
ET.SubElement(root, 'penciller').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.inker_synonyms ):
|
||||
ET.SubElement(root, 'inker').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.colorist_synonyms ):
|
||||
ET.SubElement(root, 'colorist').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.letterer_synonyms ):
|
||||
ET.SubElement(root, 'letterer').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.cover_synonyms ):
|
||||
ET.SubElement(root, 'coverDesigner').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.editor_synonyms ):
|
||||
ET.SubElement(root, 'editor').text = u"{0}".format(credit['person'])
|
||||
|
||||
|
||||
# self pretty-print
|
||||
self.indent(root)
|
||||
|
||||
# wrap it in an ElementTree instance, and save as XML
|
||||
tree = ET.ElementTree(root)
|
||||
return tree
|
||||
|
||||
|
||||
def convertXMLToMetadata( self, tree ):
|
||||
|
||||
root = tree.getroot()
|
||||
|
||||
if root.tag != 'comet':
|
||||
raise 1
|
||||
return None
|
||||
|
||||
metadata = GenericMetadata()
|
||||
md = metadata
|
||||
|
||||
# Helper function
|
||||
def xlate( tag ):
|
||||
node = root.find( tag )
|
||||
if node is not None:
|
||||
return node.text
|
||||
else:
|
||||
return None
|
||||
|
||||
md.series = xlate( 'series' )
|
||||
md.title = xlate( 'title' )
|
||||
md.issue = xlate( 'issue' )
|
||||
md.volume = xlate( 'volume' )
|
||||
md.comments = xlate( 'description' )
|
||||
md.publisher = xlate( 'publisher' )
|
||||
md.language = xlate( 'language' )
|
||||
md.format = xlate( 'format' )
|
||||
md.pageCount = xlate( 'pages' )
|
||||
md.maturityRating = xlate( 'rating' )
|
||||
md.price = xlate( 'price' )
|
||||
md.isVersionOf = xlate( 'isVersionOf' )
|
||||
md.rights = xlate( 'rights' )
|
||||
md.identifier = xlate( 'identifier' )
|
||||
md.lastMark = xlate( 'lastMark' )
|
||||
md.genre = xlate( 'genre' ) # TODO - repeatable field
|
||||
|
||||
date = xlate( 'date' )
|
||||
if date is not None:
|
||||
parts = date.split('-')
|
||||
if len( parts) > 0:
|
||||
md.year = parts[0]
|
||||
if len( parts) > 1:
|
||||
md.month = parts[1]
|
||||
|
||||
md.coverImage = xlate( 'coverImage' )
|
||||
|
||||
readingDirection = xlate( 'readingDirection' )
|
||||
if readingDirection is not None and readingDirection == "rtl":
|
||||
md.manga = "YesAndRightToLeft"
|
||||
|
||||
# loop for character tags
|
||||
char_list = []
|
||||
for n in root:
|
||||
if n.tag == 'character':
|
||||
char_list.append(n.text.strip())
|
||||
md.characters = utils.listToString( char_list )
|
||||
|
||||
# Now extract the credit info
|
||||
for n in root:
|
||||
if ( n.tag == 'writer' or
|
||||
n.tag == 'penciller' or
|
||||
n.tag == 'inker' or
|
||||
n.tag == 'colorist' or
|
||||
n.tag == 'letterer' or
|
||||
n.tag == 'editor'
|
||||
):
|
||||
metadata.addCredit( n.text.strip(), n.tag.title() )
|
||||
|
||||
if n.tag == 'coverDesigner':
|
||||
metadata.addCredit( n.text.strip(), "Cover" )
|
||||
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
|
||||
#verify that the string actually contains CoMet data in XML format
|
||||
def validateString( self, string ):
|
||||
try:
|
||||
tree = ET.ElementTree(ET.fromstring( string ))
|
||||
root = tree.getroot()
|
||||
if root.tag != 'comet':
|
||||
raise Exception
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def writeToExternalFile( self, filename, metadata ):
|
||||
|
||||
tree = self.convertMetadataToXML( self, metadata )
|
||||
#ET.dump(tree)
|
||||
tree.write(filename, encoding='utf-8')
|
||||
|
||||
def readFromExternalFile( self, filename ):
|
||||
|
||||
tree = ET.parse( filename )
|
||||
return self.convertXMLToMetadata( tree )
|
||||
|
460
comicarchive.py
460
comicarchive.py
@ -29,6 +29,13 @@ if platform.system() == "Windows":
|
||||
import _subprocess
|
||||
import time
|
||||
|
||||
import StringIO
|
||||
try:
|
||||
import Image
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".") )
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
@ -36,7 +43,8 @@ from UnRAR2.rar_exceptions import *
|
||||
from options import Options, MetaDataStyle
|
||||
from comicinfoxml import ComicInfoXml
|
||||
from comicbookinfo import ComicBookInfo
|
||||
from genericmetadata import GenericMetadata
|
||||
from comet import CoMet
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from filenameparser import FileNameParser
|
||||
|
||||
|
||||
@ -55,9 +63,16 @@ class ZipArchiver:
|
||||
return self.writeZipComment( self.path, comment )
|
||||
|
||||
def readArchiveFile( self, archive_file ):
|
||||
data = ""
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
data = zf.read( archive_file )
|
||||
zf.close()
|
||||
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)
|
||||
finally:
|
||||
zf.close()
|
||||
return data
|
||||
|
||||
def removeArchiveFile( self, archive_file ):
|
||||
@ -92,7 +107,6 @@ class ZipArchiver:
|
||||
# zip helper func
|
||||
def rebuildZipFile( self, exclude_list ):
|
||||
|
||||
# TODO: use tempfile.mkstemp
|
||||
# this recompresses the zip archive, without the files in the exclude_list
|
||||
#print "Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||
|
||||
@ -180,16 +194,41 @@ class ZipArchiver:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def copyFromArchive( self, otherArchive ):
|
||||
# Replace the current zip with one copied from another archive
|
||||
try:
|
||||
zout = zipfile.ZipFile (self.path, 'w')
|
||||
for fname in otherArchive.getArchiveFilenameList():
|
||||
data = otherArchive.readArchiveFile( fname )
|
||||
if data is not None:
|
||||
zout.writestr( fname, data )
|
||||
zout.close()
|
||||
|
||||
#preserve the old comment
|
||||
comment = otherArchive.getArchiveComment()
|
||||
if comment is not None:
|
||||
if not self.writeZipComment( self.path, comment ):
|
||||
return False
|
||||
except Exception as e:
|
||||
print "Error while copying to {0}: {1}".format(self.path, e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
#------------------------------------------
|
||||
# RAR implementation
|
||||
|
||||
class RarArchiver:
|
||||
|
||||
devnull = None
|
||||
def __init__( self, path ):
|
||||
self.path = path
|
||||
self.rar_exe_path = None
|
||||
self.devnull = open(os.devnull, "w")
|
||||
|
||||
if RarArchiver.devnull is None:
|
||||
RarArchiver.devnull = open(os.devnull, "w")
|
||||
|
||||
# windows only, keeps the cmd.exe from popping up
|
||||
if platform.system() == "Windows":
|
||||
@ -199,11 +238,12 @@ class RarArchiver:
|
||||
self.startupinfo = None
|
||||
|
||||
def __del__(self):
|
||||
self.devnull.close()
|
||||
|
||||
#RarArchiver.devnull.close()
|
||||
pass
|
||||
|
||||
def getArchiveComment( self ):
|
||||
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
rarc = self.getRARObj()
|
||||
return rarc.comment
|
||||
|
||||
def setArchiveComment( self, comment ):
|
||||
@ -216,10 +256,12 @@ class RarArchiver:
|
||||
f.write( comment )
|
||||
f.close()
|
||||
|
||||
working_dir = os.path.dirname( os.path.abspath( self.path ) )
|
||||
|
||||
# use external program to write comment to Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'c', '-c-', '-z' + tmp_name, self.path],
|
||||
subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=self.devnull)
|
||||
stdout=RarArchiver.devnull)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
time.sleep(1)
|
||||
@ -234,14 +276,38 @@ class RarArchiver:
|
||||
|
||||
def readArchiveFile( self, archive_file ):
|
||||
|
||||
entries = UnRAR2.RarFile( self.path ).read_files( archive_file )
|
||||
# Make sure to escape brackets, since some funky stuff is going on
|
||||
# underneath with "fnmatch"
|
||||
archive_file = archive_file.replace("[", '[[]')
|
||||
entries = []
|
||||
|
||||
#entries is a list of of tuples: ( rarinfo, filedata)
|
||||
if (len(entries) == 1):
|
||||
return entries[0][1]
|
||||
else:
|
||||
return ""
|
||||
rarc = self.getRARObj()
|
||||
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
try:
|
||||
tries = tries+1
|
||||
entries = rarc.read_files( archive_file )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print e, "in readArchiveFile! try %s" % tries
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print "Unexpected exception in readArchiveFile! {0}".format( e )
|
||||
break
|
||||
|
||||
else:
|
||||
#Success"
|
||||
#entries is a list of of tuples: ( rarinfo, filedata)
|
||||
if (len(entries) == 1):
|
||||
return entries[0][1]
|
||||
else:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def writeArchiveFile( self, archive_file, data ):
|
||||
|
||||
if self.rar_exe_path is not None:
|
||||
@ -249,15 +315,19 @@ class RarArchiver:
|
||||
tmp_folder = tempfile.mkdtemp()
|
||||
|
||||
tmp_file = os.path.join( tmp_folder, archive_file )
|
||||
|
||||
working_dir = os.path.dirname( os.path.abspath( self.path ) )
|
||||
|
||||
# TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt"
|
||||
# will need to create the subfolder above, I guess...
|
||||
f = open(tmp_file, 'w')
|
||||
f.write( data )
|
||||
f.close()
|
||||
|
||||
|
||||
# use external program to write file to Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'a', '-c-', '-ep', self.path, tmp_file],
|
||||
subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=self.devnull)
|
||||
stdout=RarArchiver.devnull)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
time.sleep(1)
|
||||
@ -276,7 +346,7 @@ class RarArchiver:
|
||||
# use external program to remove file from Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=self.devnull)
|
||||
stdout=RarArchiver.devnull)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
time.sleep(1)
|
||||
@ -289,10 +359,44 @@ class RarArchiver:
|
||||
|
||||
def getArchiveFilenameList( self ):
|
||||
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
rarc = self.getRARObj()
|
||||
#namelist = [ item.filename for item in rarc.infolist() ]
|
||||
#return namelist
|
||||
|
||||
return [ item.filename for item in rarc.infolist() ]
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
try:
|
||||
tries = tries+1
|
||||
namelist = [ item.filename for item in rarc.infolist() ]
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print e, "in getArchiveFilenameList! try %s" % tries
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
#Success"
|
||||
return namelist
|
||||
|
||||
raise e
|
||||
|
||||
|
||||
def getRARObj( self ):
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
try:
|
||||
tries = tries+1
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print e, "in getRARObj! try %s" % tries
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
#Success"
|
||||
return rarc
|
||||
|
||||
raise e
|
||||
|
||||
#------------------------------------------
|
||||
# Folder implementation
|
||||
class FolderArchiver:
|
||||
@ -385,7 +489,9 @@ class ComicArchive:
|
||||
def __init__( self, path ):
|
||||
self.path = path
|
||||
self.ci_xml_filename = 'ComicInfo.xml'
|
||||
|
||||
self.comet_default_filename = 'CoMet.xml'
|
||||
self.resetCache()
|
||||
|
||||
if self.zipTest():
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
self.archiver = ZipArchiver( self.path )
|
||||
@ -401,6 +507,22 @@ class ComicArchive:
|
||||
self.archive_type = self.ArchiveType.Unknown
|
||||
self.archiver = UnknownArchiver( self.path )
|
||||
|
||||
# Clears the cached data
|
||||
def resetCache( self ):
|
||||
self.has_cix = None
|
||||
self.has_cbi = None
|
||||
self.has_comet = None
|
||||
self.comet_filename = None
|
||||
self.page_count = None
|
||||
self.page_list = None
|
||||
self.cix_md = None
|
||||
self.cbi_md = None
|
||||
self.comet_md = None
|
||||
|
||||
def rename( self, path ):
|
||||
self.path = path
|
||||
self.archiver.path = path
|
||||
|
||||
def setExternalRarProgram( self, rar_exe_path ):
|
||||
if self.isRar():
|
||||
self.archiver.rar_exe_path = rar_exe_path
|
||||
@ -426,11 +548,11 @@ class ComicArchive:
|
||||
def isFolder( self ):
|
||||
return self.archive_type == self.ArchiveType.Folder
|
||||
|
||||
def isWritable( self ):
|
||||
def isWritable( self, check_rar_status=True ):
|
||||
if self.archive_type == self.ArchiveType.Unknown :
|
||||
return False
|
||||
|
||||
elif self.isRar() and self.archiver.rar_exe_path is None:
|
||||
elif check_rar_status and self.isRar() and self.archiver.rar_exe_path is None:
|
||||
return False
|
||||
|
||||
elif not os.access(self.path, os.W_OK):
|
||||
@ -470,15 +592,22 @@ class ComicArchive:
|
||||
return self.readCIX()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.readCBI()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
return self.readCoMet()
|
||||
else:
|
||||
return GenericMetadata()
|
||||
|
||||
def writeMetadata( self, metadata, style ):
|
||||
|
||||
retcode = None
|
||||
if style == MetaDataStyle.CIX:
|
||||
return self.writeCIX( metadata )
|
||||
retcode = self.writeCIX( metadata )
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.writeCBI( metadata )
|
||||
retcode = self.writeCBI( metadata )
|
||||
elif style == MetaDataStyle.COMET:
|
||||
retcode = self.writeCoMet( metadata )
|
||||
return retcode
|
||||
|
||||
|
||||
def hasMetadata( self, style ):
|
||||
|
||||
@ -486,20 +615,21 @@ class ComicArchive:
|
||||
return self.hasCIX()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.hasCBI()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
return self.hasCoMet()
|
||||
else:
|
||||
return False
|
||||
|
||||
def removeMetadata( self, style ):
|
||||
retcode = True
|
||||
if style == MetaDataStyle.CIX:
|
||||
return self.removeCIX()
|
||||
retcode = self.removeCIX()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.removeCBI()
|
||||
retcode = self.removeCBI()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
retcode = self.removeCoMet()
|
||||
return retcode
|
||||
|
||||
def getCoverPage(self):
|
||||
|
||||
# assume first page is the cover (for now)
|
||||
return self.getPage( 0 )
|
||||
|
||||
def getPage( self, index ):
|
||||
|
||||
image_data = None
|
||||
@ -523,31 +653,39 @@ class ComicArchive:
|
||||
|
||||
def getPageNameList( self , sort_list=True):
|
||||
|
||||
# get the list file names in the archive, and sort
|
||||
files = self.archiver.getArchiveFilenameList()
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files.sort(key=lambda x: x.lower())
|
||||
|
||||
# make a sub-list of image files
|
||||
page_list = []
|
||||
for name in files:
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
|
||||
page_list.append(name)
|
||||
if self.page_list is None:
|
||||
# get the list file names in the archive, and sort
|
||||
files = self.archiver.getArchiveFilenameList()
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files.sort(key=lambda x: x.lower())
|
||||
|
||||
# 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] != "." ):
|
||||
self.page_list.append(name)
|
||||
|
||||
return page_list
|
||||
return self.page_list
|
||||
|
||||
def getNumberOfPages( self ):
|
||||
|
||||
return len( self.getPageNameList( sort_list=False ) )
|
||||
if self.page_count is None:
|
||||
self.page_count = len( self.getPageNameList( ) )
|
||||
return self.page_count
|
||||
|
||||
def readCBI( self ):
|
||||
raw_cbi = self.readRawCBI()
|
||||
if raw_cbi is None:
|
||||
return GenericMetadata()
|
||||
|
||||
return ComicBookInfo().metadataFromString( raw_cbi )
|
||||
if self.cbi_md is None:
|
||||
raw_cbi = self.readRawCBI()
|
||||
if raw_cbi is None:
|
||||
self.cbi_md = GenericMetadata()
|
||||
else:
|
||||
self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi )
|
||||
|
||||
self.cbi_md.setDefaultPageList( self.getNumberOfPages() )
|
||||
|
||||
return self.cbi_md
|
||||
|
||||
def readRawCBI( self ):
|
||||
if ( not self.hasCBI() ):
|
||||
@ -555,24 +693,65 @@ class ComicArchive:
|
||||
|
||||
return self.archiver.getArchiveComment()
|
||||
|
||||
def hasCBI(self):
|
||||
if self.has_cbi is None:
|
||||
|
||||
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
self.has_cbi = False
|
||||
else:
|
||||
comment = self.archiver.getArchiveComment()
|
||||
self.has_cbi = ComicBookInfo().validateString( comment )
|
||||
|
||||
return self.has_cbi
|
||||
|
||||
def writeCBI( self, metadata ):
|
||||
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
|
||||
return self.archiver.setArchiveComment( cbi_string )
|
||||
if metadata is not None:
|
||||
self.applyArchiveInfoToMetadata( metadata )
|
||||
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
|
||||
write_success = self.archiver.setArchiveComment( cbi_string )
|
||||
if write_success:
|
||||
self.has_cbi = True
|
||||
self.cbi_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
|
||||
def removeCBI( self ):
|
||||
return self.archiver.setArchiveComment( "" )
|
||||
if self.hasCBI():
|
||||
write_success = self.archiver.setArchiveComment( "" )
|
||||
if write_success:
|
||||
self.has_cbi = False
|
||||
self.cbi_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
def readCIX( self ):
|
||||
raw_cix = self.readRawCIX()
|
||||
if raw_cix is None:
|
||||
return GenericMetadata()
|
||||
if self.cix_md is None:
|
||||
raw_cix = self.readRawCIX()
|
||||
if raw_cix is None:
|
||||
self.cix_md = GenericMetadata()
|
||||
else:
|
||||
self.cix_md = ComicInfoXml().metadataFromString( raw_cix )
|
||||
|
||||
#validate the existing page list (make sure count is correct)
|
||||
if len ( self.cix_md.pages ) != 0 :
|
||||
if len ( self.cix_md.pages ) != self.getNumberOfPages():
|
||||
# pages array doesn't match the actual number of images we're seeing
|
||||
# in the archive, so discard the data
|
||||
self.cix_md.pages = []
|
||||
|
||||
if len( self.cix_md.pages ) == 0:
|
||||
self.cix_md.setDefaultPageList( self.getNumberOfPages() )
|
||||
|
||||
return ComicInfoXml().metadataFromString( raw_cix )
|
||||
return self.cix_md
|
||||
|
||||
def readRawCIX( self ):
|
||||
if not self.hasCIX():
|
||||
print self.path, "doesn't has ComicInfo.xml data!"
|
||||
return None
|
||||
|
||||
return self.archiver.readArchiveFile( self.ci_xml_filename )
|
||||
@ -580,32 +759,156 @@ class ComicArchive:
|
||||
def writeCIX(self, metadata):
|
||||
|
||||
if metadata is not None:
|
||||
self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True )
|
||||
cix_string = ComicInfoXml().stringFromMetadata( metadata )
|
||||
return self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
|
||||
write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
|
||||
if write_success:
|
||||
self.has_cix = True
|
||||
self.cix_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
|
||||
def removeCIX( self ):
|
||||
|
||||
return self.archiver.removeArchiveFile( self.ci_xml_filename )
|
||||
if self.hasCIX():
|
||||
write_success = self.archiver.removeArchiveFile( self.ci_xml_filename )
|
||||
if write_success:
|
||||
self.has_cix = False
|
||||
self.cix_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
|
||||
def hasCIX(self):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
return False
|
||||
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
|
||||
return True
|
||||
if self.has_cix is None:
|
||||
|
||||
if not self.seemsToBeAComicArchive():
|
||||
self.has_cix = False
|
||||
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
|
||||
self.has_cix = True
|
||||
else:
|
||||
self.has_cix = False
|
||||
return self.has_cix
|
||||
|
||||
|
||||
def readCoMet( self ):
|
||||
if self.comet_md is None:
|
||||
raw_comet = self.readRawCoMet()
|
||||
if raw_comet is None:
|
||||
self.comet_md = GenericMetadata()
|
||||
else:
|
||||
self.comet_md = CoMet().metadataFromString( raw_comet )
|
||||
|
||||
self.comet_md.setDefaultPageList( self.getNumberOfPages() )
|
||||
#use the coverImage value from the comet_data to mark the cover in this struct
|
||||
# walk through list of images in file, and find the matching one for md.coverImage
|
||||
# need to remove the existing one in the default
|
||||
if self.comet_md.coverImage is not None:
|
||||
cover_idx = 0
|
||||
for idx,f in enumerate(self.getPageNameList()):
|
||||
if self.comet_md.coverImage == f:
|
||||
cover_idx = idx
|
||||
break
|
||||
if cover_idx != 0:
|
||||
del (self.comet_md.pages[0]['Type'] )
|
||||
self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover
|
||||
|
||||
return self.comet_md
|
||||
|
||||
def readRawCoMet( self ):
|
||||
if not self.hasCoMet():
|
||||
print self.path, "doesn't have CoMet data!"
|
||||
return None
|
||||
|
||||
return self.archiver.readArchiveFile( self.comet_filename )
|
||||
|
||||
def writeCoMet(self, metadata):
|
||||
|
||||
if metadata is not None:
|
||||
if not self.hasCoMet():
|
||||
self.comet_filename = self.comet_default_filename
|
||||
|
||||
self.applyArchiveInfoToMetadata( metadata )
|
||||
# Set the coverImage value, if it's not the first page
|
||||
cover_idx = int(metadata.getCoverPageIndexList()[0])
|
||||
if cover_idx != 0:
|
||||
metadata.coverImage = self.getPageName( cover_idx )
|
||||
|
||||
comet_string = CoMet().stringFromMetadata( metadata )
|
||||
write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string )
|
||||
if write_success:
|
||||
self.has_comet = True
|
||||
self.comet_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
|
||||
def removeCoMet( self ):
|
||||
if self.hasCoMet():
|
||||
write_success = self.archiver.removeArchiveFile( self.comet_filename )
|
||||
if write_success:
|
||||
self.has_comet = False
|
||||
self.comet_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
def hasCoMet(self):
|
||||
if self.has_comet is None:
|
||||
self.has_comet = False
|
||||
if not self.seemsToBeAComicArchive():
|
||||
return self.has_comet
|
||||
|
||||
#look at all xml files in root, and search for CoMet data, get first
|
||||
for n in self.archiver.getArchiveFilenameList():
|
||||
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 )
|
||||
if CoMet().validateString( data ):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
self.has_comet = True
|
||||
break
|
||||
|
||||
return self.has_comet
|
||||
|
||||
|
||||
def hasCBI(self):
|
||||
|
||||
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
return False
|
||||
def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False):
|
||||
md.pageCount = self.getNumberOfPages()
|
||||
|
||||
if calc_page_sizes:
|
||||
for p in md.pages:
|
||||
idx = int( p['Image'] )
|
||||
if pil_available:
|
||||
if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p:
|
||||
data = self.getPage( idx )
|
||||
if data is not None:
|
||||
try:
|
||||
im = Image.open(StringIO.StringIO(data))
|
||||
w,h = im.size
|
||||
|
||||
p['ImageSize'] = str(len(data))
|
||||
p['ImageHeight'] = str(h)
|
||||
p['ImageWidth'] = str(w)
|
||||
except IOError:
|
||||
p['ImageSize'] = str(len(data))
|
||||
|
||||
else:
|
||||
if 'ImageSize' not in p:
|
||||
data = self.getPage( idx )
|
||||
p['ImageSize'] = str(len(data))
|
||||
|
||||
comment = self.archiver.getArchiveComment()
|
||||
return ComicBookInfo().validateString( comment )
|
||||
|
||||
|
||||
def metadataFromFilename( self ):
|
||||
|
||||
metadata = GenericMetadata()
|
||||
@ -627,3 +930,12 @@ class ComicArchive:
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
|
||||
def exportAsZip( self, zipfilename ):
|
||||
if self.archive_type == self.ArchiveType.Zip:
|
||||
# nothing to do, we're already a zip
|
||||
return True
|
||||
|
||||
zip_archiver = ZipArchiver( zipfilename )
|
||||
return zip_archiver.copyFromArchive( self.archiver )
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
A python class to encapsulate the ComicBookInfo data and file handling
|
||||
A python class to encapsulate the ComicBookInfo data
|
||||
"""
|
||||
|
||||
"""
|
||||
@ -25,6 +25,7 @@ import zipfile
|
||||
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
class ComicBookInfo:
|
||||
|
||||
@ -62,6 +63,12 @@ class ComicBookInfo:
|
||||
metadata.criticalRating = xlate( 'rating' )
|
||||
metadata.tags = xlate( 'tags' )
|
||||
|
||||
# make sure credits and tags are at least empty lists and not None
|
||||
if metadata.credits is None:
|
||||
metadata.credits = []
|
||||
if metadata.tags is None:
|
||||
metadata.tags = []
|
||||
|
||||
#need to massage the language string to be ISO
|
||||
if metadata.language is not None:
|
||||
# reverse look-up
|
||||
@ -96,7 +103,7 @@ class ComicBookInfo:
|
||||
|
||||
# Create the dictionary that we will convert to JSON text
|
||||
cbi = dict()
|
||||
cbi_container = {'appID' : 'ComicTagger/0.1',
|
||||
cbi_container = {'appID' : 'ComicTagger/' + ctversion.version,
|
||||
'lastModified' : str(datetime.now()),
|
||||
'ComicBookInfo/1.0' : cbi }
|
||||
|
||||
@ -104,18 +111,28 @@ class ComicBookInfo:
|
||||
def assign( cbi_entry, md_entry):
|
||||
if md_entry is not None:
|
||||
cbi[cbi_entry] = md_entry
|
||||
|
||||
#helper func
|
||||
def toInt(s):
|
||||
i = None
|
||||
if type(s) == str or type(s) == int:
|
||||
try:
|
||||
i = int(s)
|
||||
except ValueError:
|
||||
pass
|
||||
return i
|
||||
|
||||
assign( 'series', metadata.series )
|
||||
assign( 'title', metadata.title )
|
||||
assign( 'issue', metadata.issue )
|
||||
assign( 'publisher', metadata.publisher )
|
||||
assign( 'publicationMonth', metadata.month )
|
||||
assign( 'publicationYear', metadata.year )
|
||||
assign( 'numberOfIssues', metadata.issueCount )
|
||||
assign( 'publicationMonth', toInt(metadata.month) )
|
||||
assign( 'publicationYear', toInt(metadata.year) )
|
||||
assign( 'numberOfIssues', toInt(metadata.issueCount) )
|
||||
assign( 'comments', metadata.comments )
|
||||
assign( 'genre', metadata.genre )
|
||||
assign( 'volume', metadata.volume )
|
||||
assign( 'numberOfVolumes', metadata.volumeCount )
|
||||
assign( 'volume', toInt(metadata.volume) )
|
||||
assign( 'numberOfVolumes', toInt(metadata.volumeCount) )
|
||||
assign( 'language', utils.getLanguageFromISO(metadata.language) )
|
||||
assign( 'country', metadata.country )
|
||||
assign( 'rating', metadata.criticalRating )
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
A python class to encapsulate ComicRack's ComicInfo.xml data and file handling
|
||||
A python class to encapsulate ComicRack's ComicInfo.xml data
|
||||
"""
|
||||
|
||||
"""
|
||||
@ -270,6 +270,7 @@ class ComicInfoXml:
|
||||
if pages_node is not None:
|
||||
for page in pages_node:
|
||||
metadata.pages.append( page.attrib )
|
||||
#print page.attrib
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
||||
|
513
comictagger.py
513
comictagger.py
@ -28,7 +28,10 @@ import time
|
||||
from pprint import pprint
|
||||
import json
|
||||
import platform
|
||||
import locale
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt4 import QtCore, QtGui
|
||||
@ -43,24 +46,151 @@ from comicarchive import ComicArchive
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
|
||||
import utils
|
||||
import codecs
|
||||
|
||||
class MultipleMatch():
|
||||
def __init__( self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
class OnlineMatchResults():
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.writeFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
|
||||
return cv_md
|
||||
|
||||
def actual_metadata_save( ca, opts, md ):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print "The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print "Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print "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 u"{0}".format(md)
|
||||
return True
|
||||
|
||||
|
||||
def post_process_matches( match_results, opts, settings ):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len( match_results.goodMatches ) > 0:
|
||||
print "\nSuccessful matches:"
|
||||
print "------------------"
|
||||
for f in match_results.goodMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.noMatches ) > 0:
|
||||
print "\nNo matches:"
|
||||
print "------------------"
|
||||
for f in match_results.noMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.writeFailures ) > 0:
|
||||
print "\nFile Write Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.writeFailures:
|
||||
print f
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
#jusr quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nMultiple 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
|
||||
|
||||
|
||||
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"
|
||||
return
|
||||
|
||||
for f in opts.file_list:
|
||||
if len( opts.file_list ) > 1:
|
||||
print "Processing: ", f
|
||||
process_file_cli( f, opts, settings )
|
||||
|
||||
def process_file_cli( filename, opts, settings ):
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli( f, opts, settings, match_results )
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches( match_results, opts, settings )
|
||||
|
||||
|
||||
def create_local_metadata( opts, ca, has_desired_tags ):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.setDefaultPageList( ca.getNumberOfPages() )
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata( opts.data_style )
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
|
||||
return md
|
||||
|
||||
def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
batch_mode = len( opts.file_list ) > 1
|
||||
|
||||
ca = ComicArchive(filename)
|
||||
if settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( settings.rar_exe_path )
|
||||
@ -70,246 +200,254 @@ def process_file_cli( filename, opts, settings ):
|
||||
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.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"
|
||||
return
|
||||
|
||||
|
||||
cix = False
|
||||
cbi = False
|
||||
if ca.hasCIX(): cix = True
|
||||
if ca.hasCBI(): cbi = True
|
||||
has = [ False, False, False ]
|
||||
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
|
||||
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
|
||||
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
if ca.isZip(): brief = "ZIP archive "
|
||||
elif ca.isRar(): brief = "RAR archive "
|
||||
elif ca.isFolder(): brief = "Folder archive "
|
||||
|
||||
if batch_mode:
|
||||
brief = "{0}: ".format(filename)
|
||||
|
||||
if ca.isZip(): brief += "ZIP archive "
|
||||
elif ca.isRar(): brief += "RAR archive "
|
||||
elif ca.isFolder(): brief += "Folder archive "
|
||||
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not (cbi or cix):
|
||||
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
|
||||
brief += "none "
|
||||
else:
|
||||
if cbi: brief += "CBL "
|
||||
if cix: brief += "CR "
|
||||
if has[ MetaDataStyle.CBI ]: brief += "CBL "
|
||||
if has[ MetaDataStyle.CIX ]: brief += "CR "
|
||||
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print brief
|
||||
print
|
||||
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if cix:
|
||||
if has[ MetaDataStyle.CIX ]:
|
||||
print "------ComicRack tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCIX())
|
||||
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
|
||||
else:
|
||||
print u"{0}".format(ca.readCIX())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if cbi:
|
||||
if has[ MetaDataStyle.CBI ]:
|
||||
print "------ComicBookLover tags--------"
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print u"{0}".format(ca.readCBI())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
if opts.data_style == MetaDataStyle.CIX:
|
||||
if cix:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeCIX():
|
||||
print "Tag removal seemed to fail!"
|
||||
else:
|
||||
print "Removed ComicRack tags."
|
||||
else:
|
||||
print "dry-run. ComicRack tags not removed"
|
||||
else:
|
||||
print "This archive doesn't have ComicRack tags."
|
||||
|
||||
if opts.data_style == MetaDataStyle.CBI:
|
||||
if cbi:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeCBI():
|
||||
print "Tag removal seemed to fail!"
|
||||
else:
|
||||
print "Removed ComicBookLover tags."
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[ MetaDataStyle.COMET ]:
|
||||
print "------CoMet tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCoMet())
|
||||
else:
|
||||
print "dry-run. ComicBookLover tags not removed"
|
||||
print u"{0}".format(ca.readCoMet())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if has[ opts.data_style ]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata( opts.data_style ):
|
||||
print "{0}: Tag removal seemed to fail!".format( filename )
|
||||
else:
|
||||
print "{0}: Removed {1} tags.".format( filename, style_name )
|
||||
else:
|
||||
print "This archive doesn't have ComicBookLover tags."
|
||||
print "{0}: dry-run. {1} tags not removed".format( filename, style_name )
|
||||
else:
|
||||
print "{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print "{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print "{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[ opts.copy_source ]
|
||||
if has[ opts.copy_source ]:
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print u"{0}: Tag copy seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
|
||||
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
# OK we're gonna do a save of some new data
|
||||
md = GenericMetadata()
|
||||
|
||||
# First read in existing data, if it's there
|
||||
if opts.data_style == MetaDataStyle.CIX and cix:
|
||||
md = ca.readCIX()
|
||||
elif opts.data_style == MetaDataStyle.CBI and cbi:
|
||||
md = ca.readCBI()
|
||||
|
||||
# now, overlay the new data onto the old, in order
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
|
||||
return
|
||||
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
if batch_mode:
|
||||
print u"Processing {0}: ".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
|
||||
# finally, search online
|
||||
# now, search online
|
||||
if opts.search_online:
|
||||
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "No metadata given to search online with!"
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple matches. Save aborted"
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print "Online search: Low confidence match. Save aborted"
|
||||
return
|
||||
if not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
return
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( matches[0]['volume_id'], matches[0]['issue_number'] )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return
|
||||
if cv_md is None:
|
||||
print "No match for ID {0} was found.".format(opts.issue_id)
|
||||
return None
|
||||
|
||||
md.overlay( cv_md )
|
||||
# ok, done building our metadata. time to save
|
||||
|
||||
#HACK
|
||||
#opts.dryrun = True
|
||||
#HACK
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print "The tag save seemed to fail!"
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
else:
|
||||
print "Save complete."
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "No metadata given to search online with!"
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple 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)
|
||||
return
|
||||
if not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings)
|
||||
if cv_md is None:
|
||||
return
|
||||
|
||||
md.overlay( cv_md )
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save( ca, opts, md ):
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
md = GenericMetadata()
|
||||
# First read in existing data, if it's there
|
||||
if opts.data_style == MetaDataStyle.CIX and cix:
|
||||
md = ca.readCIX()
|
||||
elif opts.data_style == MetaDataStyle.CBI and cbi:
|
||||
md = ca.readCBI()
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if md.isEmpty:
|
||||
print "Comic archive contains no tags!"
|
||||
|
||||
if opts.data_style == MetaDataStyle.CIX:
|
||||
if cix:
|
||||
md = ca.readCIX()
|
||||
else:
|
||||
print "Comic archive contains no ComicRack tags!"
|
||||
|
||||
if opts.data_style == MetaDataStyle.CBI:
|
||||
if cbi:
|
||||
md = ca.readCBI()
|
||||
else:
|
||||
print "Comic archive contains no ComicBookLover tags!"
|
||||
|
||||
# TODO move this to ComicArchive, or maybe another class???
|
||||
new_name = ""
|
||||
if md.series is not None:
|
||||
new_name += "{0}".format( md.series )
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[ opts.data_style ]
|
||||
else:
|
||||
print "Can't rename without series name"
|
||||
return
|
||||
use_tags = False
|
||||
|
||||
if md.volume is not None:
|
||||
new_name += " v{0}".format( md.volume )
|
||||
|
||||
if md.issue is not None:
|
||||
new_name += " #{:03d}".format( int(md.issue) )
|
||||
else:
|
||||
print "Can't rename without issue number"
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer( md )
|
||||
renamer.setTemplate( settings.rename_template )
|
||||
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
if md.issueCount is not None:
|
||||
new_name += " (of {0})".format( md.issueCount )
|
||||
|
||||
if md.year is not None:
|
||||
new_name += " ({0})".format( md.year )
|
||||
|
||||
if ca.isZip():
|
||||
new_name += ".cbz"
|
||||
elif ca.isRar():
|
||||
new_name += ".cbr"
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print "Filename is already good!"
|
||||
print msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
#HACK
|
||||
#opts.dryrun = True
|
||||
#HACK
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.rename( filename, new_abs_path )
|
||||
else:
|
||||
print "dry-run option was set, so nothing was changed, but here is the proposed filename:"
|
||||
print "'{0}'".format(new_abs_path)
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
|
||||
|
||||
|
||||
|
||||
@ -319,7 +457,7 @@ def process_file_cli( filename, opts, settings ):
|
||||
def main():
|
||||
|
||||
# try to make stdout encodings happy for unicode
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
|
||||
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
@ -348,8 +486,8 @@ def main():
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.filename, settings )
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.file_list, settings )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
@ -361,8 +499,7 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
@ -25,6 +25,7 @@ import sys
|
||||
import os
|
||||
import datetime
|
||||
|
||||
import ctversion
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class ComicVineCacher:
|
||||
@ -32,15 +33,38 @@ class ComicVineCacher:
|
||||
def __init__(self ):
|
||||
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
|
||||
self.db_file = os.path.join( self.settings_folder, "cv_cache.db")
|
||||
self.version_file = os.path.join( self.settings_folder, "cache_version.txt")
|
||||
|
||||
#verify that cache is from same version as this one
|
||||
data = ""
|
||||
try:
|
||||
with open( self.version_file, 'rb' ) as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
if data != ctversion.version:
|
||||
self.clearCache()
|
||||
|
||||
if not os.path.exists( self.db_file ):
|
||||
self.create_cache_db()
|
||||
|
||||
def clearCache( self ):
|
||||
os.unlink( self.db_file )
|
||||
try:
|
||||
os.unlink( self.db_file )
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.unlink( self.version_file )
|
||||
except:
|
||||
pass
|
||||
|
||||
def create_cache_db( self ):
|
||||
|
||||
#create the version file
|
||||
with open( self.version_file, 'w' ) as f:
|
||||
f.write( ctversion.version )
|
||||
|
||||
# this will wipe out any existing version
|
||||
open( self.db_file, 'w').close()
|
||||
|
||||
@ -68,6 +92,7 @@ class ComicVineCacher:
|
||||
"name TEXT," +
|
||||
"publisher TEXT," +
|
||||
"count_of_issues INT," +
|
||||
"start_year INT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id) )"
|
||||
)
|
||||
@ -92,7 +117,7 @@ class ComicVineCacher:
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
with con:
|
||||
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
@ -130,6 +155,7 @@ class ComicVineCacher:
|
||||
results = list()
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
|
||||
@ -178,6 +204,7 @@ class ComicVineCacher:
|
||||
"name": cv_volume_record['name'],
|
||||
"publisher": pub_name,
|
||||
"count_of_issues": cv_volume_record['count_of_issues'],
|
||||
"start_year": cv_volume_record['start_year'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
@ -202,6 +229,7 @@ class ComicVineCacher:
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale volume info
|
||||
a_week_ago = datetime.datetime.today()-datetime.timedelta(days=7)
|
||||
@ -212,7 +240,7 @@ class ComicVineCacher:
|
||||
cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
@ -227,6 +255,7 @@ class ComicVineCacher:
|
||||
result['publisher'] = dict()
|
||||
result['publisher']['name'] = row[2]
|
||||
result['count_of_issues'] = row[3]
|
||||
result['start_year'] = row[4]
|
||||
result['issues'] = list()
|
||||
|
||||
cur.execute("SELECT id,name,issue_number,image_url,image_hash FROM Issues WHERE volume_id = ?", [ volume_id ] )
|
||||
@ -252,6 +281,7 @@ class ComicVineCacher:
|
||||
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
@ -270,6 +300,7 @@ class ComicVineCacher:
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year FROM Issues WHERE id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
|
@ -24,6 +24,8 @@ from pprint import pprint
|
||||
import urllib2, urllib
|
||||
import math
|
||||
import re
|
||||
import datetime
|
||||
import ctversion
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
@ -43,6 +45,8 @@ import utils
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from genericmetadata import GenericMetadata
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class ComicVineTalkerException(Exception):
|
||||
pass
|
||||
@ -91,7 +95,8 @@ class ComicVineTalker(QObject):
|
||||
|
||||
original_series_name = series_name
|
||||
|
||||
series_name = urllib.quote_plus(str(series_name))
|
||||
series_name = urllib.quote_plus(series_name.encode("utf-8"))
|
||||
#series_name = urllib.quote_plus(unicode(series_name))
|
||||
search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
|
||||
|
||||
content = self.getUrlContent(search_url)
|
||||
@ -175,12 +180,14 @@ class ComicVineTalker(QObject):
|
||||
return volume_results
|
||||
|
||||
|
||||
def fetchIssueData( self, series_id, issue_number ):
|
||||
def fetchIssueData( self, series_id, issue_number, settings ):
|
||||
|
||||
volume_results = self.fetchVolumeData( series_id )
|
||||
|
||||
found = False
|
||||
for record in volume_results['issues']:
|
||||
for record in volume_results['issues']:
|
||||
if IssueString(issue_number).asFloat() is None:
|
||||
issue_number = 1
|
||||
if float(record['issue_number']) == float(issue_number):
|
||||
found = True
|
||||
break
|
||||
@ -198,16 +205,35 @@ class ComicVineTalker(QObject):
|
||||
else:
|
||||
return None
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
return self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
|
||||
def fetchIssueDataByIssueID( self, issue_id, settings ):
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
|
||||
issue_results = cv_response['results']
|
||||
|
||||
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
md = self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
md.isEmpty = False
|
||||
return md
|
||||
|
||||
def mapCVDataToMetadata(self, volume_results, issue_results, settings ):
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
metadata = GenericMetadata()
|
||||
|
||||
metadata.series = issue_results['volume']['name']
|
||||
|
||||
# format the issue number string nicely, since it's usually something like "2.00"
|
||||
num_f = float(issue_results['issue_number'])
|
||||
num_s = str( int(math.floor(num_f)) )
|
||||
if math.floor(num_f) != num_f:
|
||||
num_s = str( num_f )
|
||||
num_s = IssueString(issue_results['issue_number']).asString()
|
||||
|
||||
metadata.issue = num_s
|
||||
metadata.title = issue_results['name']
|
||||
@ -216,8 +242,13 @@ class ComicVineTalker(QObject):
|
||||
metadata.year = issue_results['publish_year']
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.comments = self.cleanup_html(issue_results['description'])
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger app using info from Comic Vine."
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
|
||||
ctversion.version,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
issue_results['id'])
|
||||
#metadata.notes += issue_results['site_detail_url']
|
||||
|
||||
metadata.webLink = issue_results['site_detail_url']
|
||||
@ -227,7 +258,7 @@ class ComicVineTalker(QObject):
|
||||
for role in person['roles']:
|
||||
# can we determine 'primary' from CV??
|
||||
role_name = role['role'].title()
|
||||
metadata.addCredit( person['name'], role['role'].title(), False )
|
||||
metadata.addCredit( person['name'], role['role'].title(), False )
|
||||
|
||||
character_credits = issue_results['character_credits']
|
||||
character_list = list()
|
||||
@ -248,11 +279,12 @@ class ComicVineTalker(QObject):
|
||||
metadata.locations = utils.listToString( location_list )
|
||||
|
||||
story_arc_credits = issue_results['story_arc_credits']
|
||||
for arc in story_arc_credits:
|
||||
metadata.storyArc = arc['name']
|
||||
#just use the first one, if at all
|
||||
break
|
||||
|
||||
arc_list = []
|
||||
for arc in story_arc_credits:
|
||||
arc_list.append(arc['name'])
|
||||
if len(arc_list) > 0:
|
||||
metadata.storyArc = utils.listToString(arc_list)
|
||||
|
||||
return metadata
|
||||
|
||||
def cleanup_html( self, string):
|
||||
|
@ -30,7 +30,7 @@ class CreditEditorWindow(QtGui.QDialog):
|
||||
ModeNew = 1
|
||||
|
||||
|
||||
def __init__(self, parent, mode, role, name ):
|
||||
def __init__(self, parent, mode, role, name, primary ):
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'crediteditorwindow.ui' ), self)
|
||||
@ -64,10 +64,33 @@ class CreditEditorWindow(QtGui.QDialog):
|
||||
self.cbRole.setEditText( role )
|
||||
else:
|
||||
self.cbRole.setCurrentIndex( i )
|
||||
|
||||
if primary:
|
||||
self.cbPrimary.setCheckState( QtCore.Qt.Checked )
|
||||
|
||||
self.cbRole.currentIndexChanged.connect(self.roleChanged)
|
||||
self.cbRole.editTextChanged.connect(self.roleChanged)
|
||||
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def updatePrimaryButton( self ):
|
||||
enabled =self.currentRoleCanBePrimary()
|
||||
self.cbPrimary.setEnabled( enabled )
|
||||
|
||||
def currentRoleCanBePrimary( self ):
|
||||
role = self.cbRole.currentText()
|
||||
if str(role).lower() == "writer" or str(role).lower() == "artist":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def roleChanged( self, s ):
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def getCredits( self ):
|
||||
return self.cbRole.currentText(), self.leName.text()
|
||||
|
||||
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
|
||||
return self.cbRole.currentText(), self.leName.text(), primary
|
||||
|
||||
|
||||
def accept( self ):
|
||||
if self.cbRole.currentText() == "" or self.leName.text() == "":
|
||||
|
@ -66,6 +66,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="cbPrimary">
|
||||
<property name="text">
|
||||
<string>Primary</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="0.9.0-beta"
|
||||
version="1.0.1-beta"
|
||||
|
62
exportwindow.py
Normal file
62
exportwindow.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for export to zip
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
class ExportConflictOpts:
|
||||
dontCreate = 1
|
||||
overwrite = 2
|
||||
createUnique = 3
|
||||
|
||||
class ExportWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAddToList.setCheckState( QtCore.Qt.Checked )
|
||||
self.radioDontCreate.setChecked( True )
|
||||
|
||||
self.deleteOriginal = False
|
||||
self.addToList = True
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
|
||||
self.addToList = self.cbxAddToList.isChecked()
|
||||
if self.radioDontCreate.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
elif self.radioCreateNew.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.createUnique
|
||||
#else:
|
||||
# self.fileConflictBehavior = ExportConflictOpts.overwrite
|
154
exportwindow.ui
Normal file
154
exportwindow.ui
Normal file
@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogExport</class>
|
||||
<widget class="QDialog" name="dialogExport">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>515</width>
|
||||
<height>307</height>
|
||||
</rect>
|
||||
</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">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxAddToList">
|
||||
<property name="text">
|
||||
<string>Add New Archive to ComicTagger list</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxDeleteOriginal">
|
||||
<property name="text">
|
||||
<string>Delete Original RAR (Not recommended)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>When Filename already exists:</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="radioDontCreate">
|
||||
<property name="text">
|
||||
<string>Don't Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="radioCreateNew">
|
||||
<property name="text">
|
||||
<string>Create New Archive With Unique Name (Number appended)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -80,8 +80,11 @@ class FileNameParser:
|
||||
|
||||
# first, look for multiple "--", this mean's it's formatted differently from most:
|
||||
if "--" in filename:
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name follow
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
|
||||
filename = filename.split("--")[0]
|
||||
elif "___" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
|
||||
filename = filename.split("__")[0]
|
||||
|
||||
# guess based on position
|
||||
|
||||
@ -89,8 +92,19 @@ class FileNameParser:
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
word_list = tmpstr.split(' ')
|
||||
|
||||
#before we search, remove any kind of likely "of X" phrase
|
||||
for i in range(0, len(word_list)-2):
|
||||
if ( word_list[i].isdigit() and
|
||||
word_list[i+1] == "of" and
|
||||
word_list[i+2].isdigit() ):
|
||||
word_list[i+1] ="XXX"
|
||||
word_list[i+2] ="XXX"
|
||||
|
||||
|
||||
# assume the last number in the filename that is under 4 digits is the issue number
|
||||
for word in reversed(word_list):
|
||||
for word in reversed(word_list):
|
||||
if word[0] == "#":
|
||||
word = word[1:]
|
||||
if (
|
||||
(word.isdigit() and len(word) < 4) or
|
||||
(self.isPointIssue(word))
|
||||
@ -126,14 +140,15 @@ class FileNameParser:
|
||||
tmpstr = tmpstr.replace("#", " ")
|
||||
|
||||
if issue != "":
|
||||
# assume that issue substr has at least on space before it
|
||||
# assume that issue substr has at least one space before it
|
||||
issue_str = " " + str(issue)
|
||||
series = tmpstr.split(issue_str)[0]
|
||||
else:
|
||||
# no issue to work off of
|
||||
#!!! TODO we should look for the year, and split from that
|
||||
# and if that doesn't exist, remove parenthetical words
|
||||
# and if that doesn't exist, remove parenthetical phrases
|
||||
series = tmpstr
|
||||
series = re.sub( "\(.*\)", "", tmpstr)
|
||||
|
||||
volume = ""
|
||||
|
||||
@ -161,7 +176,7 @@ class FileNameParser:
|
||||
return year
|
||||
|
||||
def parseFilename( self, filename ):
|
||||
|
||||
|
||||
# remove the path
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
@ -171,6 +186,13 @@ class FileNameParser:
|
||||
#url decode, just in case
|
||||
filename = unquote(filename)
|
||||
|
||||
# sometimes archives get messed up names from too many decodings
|
||||
# often url encodings will break and leave "_28" and "_29" in place
|
||||
# of "(" and ")" see if there are a number of these, and replace them
|
||||
if filename.count("_28") > 1 and filename.count("_29") > 1:
|
||||
filename = filename.replace("_28", "(")
|
||||
filename = filename.replace("_29", ")")
|
||||
|
||||
# ----HACK
|
||||
# remove the first word that word is a 3 digit number.
|
||||
# some story arcs collection packs do this, but it's ugly
|
||||
@ -190,3 +212,6 @@ class FileNameParser:
|
||||
self.issue = self.issue.lstrip("0")
|
||||
if self.issue == "":
|
||||
self.issue = "0"
|
||||
if self.issue[0] == ".":
|
||||
self.issue = "0" + self.issue
|
||||
|
||||
|
115
filerenamer.py
Normal file
115
filerenamer.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""
|
||||
Functions for renaming files based on metadata
|
||||
"""
|
||||
|
||||
"""
|
||||
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
|
||||
import re
|
||||
from issuestring import IssueString
|
||||
|
||||
class FileRenamer:
|
||||
def __init__( self, metadata ):
|
||||
self.setMetadata( metadata )
|
||||
self.setTemplate( "%series% v%volume% #%issue% (of %issuecount%) (%year%)" )
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
|
||||
def setMetadata( self, metadata ):
|
||||
self.metdata = metadata
|
||||
|
||||
def setIssueZeroPadding( self, count ):
|
||||
self.issue_zero_padding = count
|
||||
|
||||
def setSmartCleanup( self, on ):
|
||||
self.smart_cleanup = on
|
||||
|
||||
def setTemplate( self, template ):
|
||||
self.template = template
|
||||
|
||||
def replaceToken( self, text, value, token ):
|
||||
#helper func
|
||||
def isToken( word ):
|
||||
return (word[0] == "%" and word[-1:] == "%")
|
||||
|
||||
if value is not None:
|
||||
return text.replace( token, unicode(value) )
|
||||
else:
|
||||
if self.smart_cleanup:
|
||||
# smart cleanup means we want to remove anything appended to token if it's empty
|
||||
# (e.g "#%issue%" or "v%volume%" )
|
||||
# (TODO: This could fail if there is more than one token appended together, I guess)
|
||||
text_list = text.split()
|
||||
|
||||
#special case for issuecount, remove preceding non-token word, as in "...(of %issuecount%)..."
|
||||
if token == '%issuecount%':
|
||||
for idx,word in enumerate( text_list ):
|
||||
if token in word and not isToken(text_list[idx -1]) :
|
||||
text_list[idx -1] = ""
|
||||
|
||||
text_list = [ x for x in text_list if token not in x ]
|
||||
return " ".join( text_list )
|
||||
else:
|
||||
return text.replace( token, "" )
|
||||
|
||||
def determineName( self, filename, ext=None ):
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
|
||||
#print u"{0}".format(md)
|
||||
|
||||
new_name = self.replaceToken( new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken( new_name, md.volume, '%volume%')
|
||||
|
||||
if md.issue is not None:
|
||||
issue_str = u"{0}".format( IssueString(md.issue).asString(pad=self.issue_zero_padding) )
|
||||
else:
|
||||
issue_str = None
|
||||
new_name = self.replaceToken( new_name, issue_str, '%issue%')
|
||||
|
||||
new_name = self.replaceToken( new_name, md.issueCount, '%issuecount%')
|
||||
new_name = self.replaceToken( new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken( new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken( new_name, md.title, '%title%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
# remove empty braces,brackets, parentheses
|
||||
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name )
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name )
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name )
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]+\s+", "- ", new_name )
|
||||
new_name = re.sub("(\s-)+", " -", new_name )
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext( filename )[1]
|
||||
|
||||
new_name += ext
|
||||
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(":", "-")
|
||||
|
||||
return new_name
|
||||
|
||||
|
397
fileselectionlist.py
Normal file
397
fileselectionlist.py
Normal file
@ -0,0 +1,397 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
A PyQt4 widget for managing list of comic archive files
|
||||
"""
|
||||
|
||||
"""
|
||||
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 PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(FileTableWidget, self).__init__(parent)
|
||||
|
||||
|
||||
self.setColumnCount(5)
|
||||
self.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
|
||||
self.horizontalHeader().setStretchLastSection( True )
|
||||
|
||||
|
||||
class FileTableWidgetItem(QTableWidgetItem):
|
||||
def __lt__(self, other):
|
||||
return (self.data(Qt.UserRole).toBool() <
|
||||
other.data(Qt.UserRole).toBool())
|
||||
|
||||
|
||||
class FileInfo( ):
|
||||
def __init__(self, ca ):
|
||||
self.ca = ca
|
||||
|
||||
class FileSelectionList(QWidget):
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
listCleared = pyqtSignal()
|
||||
|
||||
fileColNum = 0
|
||||
CRFlagColNum = 1
|
||||
CBLFlagColNum = 2
|
||||
typeColNum = 3
|
||||
readonlyColNum = 4
|
||||
folderColNum = 5
|
||||
dataColNum = fileColNum
|
||||
|
||||
|
||||
def __init__(self, parent , settings ):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
#self.twList = FileTableWidget( self )
|
||||
#gridlayout = QGridLayout( self )
|
||||
#gridlayout.addWidget( self.twList )
|
||||
|
||||
#self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
self.currentItem = None
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.modifiedFlag = False
|
||||
|
||||
selectAllAction = QAction("Select All", self)
|
||||
removeAction = QAction("Remove Selected Items", self)
|
||||
self.separator = QAction("",self)
|
||||
self.separator.setSeparator(True)
|
||||
|
||||
selectAllAction.setShortcut( 'Ctrl+A' )
|
||||
removeAction.setShortcut( 'Ctrl+X' )
|
||||
|
||||
selectAllAction.triggered.connect(self.selectAll)
|
||||
removeAction.triggered.connect(self.removeSelection)
|
||||
|
||||
self.addAction(selectAllAction)
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
|
||||
def addAppAction( self, action ):
|
||||
self.insertAction( None , action )
|
||||
|
||||
def setModifiedFlag( self, modified ):
|
||||
self.modifiedFlag = modified
|
||||
|
||||
def selectAll( self ):
|
||||
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), True )
|
||||
|
||||
def deselectAll( self ):
|
||||
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), False )
|
||||
|
||||
def removeArchiveList( self, ca_list ):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for ca in ca_list:
|
||||
for row in range(self.twList.rowCount()):
|
||||
row_ca = self.getArchiveByRow( row )
|
||||
if row_ca == ca:
|
||||
self.twList.removeRow(row)
|
||||
break
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def getArchiveByRow( self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data( Qt.UserRole ).toPyObject()
|
||||
return fi.ca
|
||||
|
||||
def getCurrentArchive( self ):
|
||||
return self.getArchiveByRow( self.twList.currentRow() )
|
||||
|
||||
def removeSelection( self ):
|
||||
row_list = []
|
||||
for item in self.twList.selectedItems():
|
||||
if item.column() == 0:
|
||||
row_list.append(item.row())
|
||||
|
||||
if len(row_list) == 0:
|
||||
return
|
||||
|
||||
if self.twList.currentRow() in row_list:
|
||||
if not self.modifiedFlagVerification( "Remove Archive",
|
||||
"If you close this archive, data in the form will be lost. Are you sure?"):
|
||||
return
|
||||
|
||||
row_list.sort()
|
||||
row_list.reverse()
|
||||
|
||||
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for i in row_list:
|
||||
self.twList.removeRow(i)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
|
||||
def addPathList( self, pathlist ):
|
||||
filelist = []
|
||||
for p in pathlist:
|
||||
# if path is a folder, walk it recursivly, and all files underneath
|
||||
if os.path.isdir( unicode(p)):
|
||||
for root,dirs,files in os.walk( unicode(p) ):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root,unicode(f)))
|
||||
else:
|
||||
filelist.append(unicode(p))
|
||||
|
||||
# we now have a list of files to add
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle( "Adding Files" )
|
||||
progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
firstAdded = None
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx,f in enumerate(filelist):
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem( f )
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
progdialog.close()
|
||||
if firstAdded is not None:
|
||||
self.twList.selectRow(firstAdded)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
# Adjust column size
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setColumnWidth(FileSelectionList.CRFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.CBLFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.readonlyColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.typeColNum, 45)
|
||||
if self.twList.columnWidth(FileSelectionList.fileColNum) > 250:
|
||||
self.twList.setColumnWidth(FileSelectionList.fileColNum, 250)
|
||||
if self.twList.columnWidth(FileSelectionList.folderColNum ) > 200:
|
||||
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
|
||||
|
||||
def isListDupe( self, path ):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.getArchiveByRow( r )
|
||||
if ca.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def addPathItem( self, path):
|
||||
path = unicode( path )
|
||||
path = os.path.abspath( path )
|
||||
#print "processing", path
|
||||
|
||||
if self.isListDupe(path):
|
||||
return None
|
||||
|
||||
ca = ComicArchive( path )
|
||||
if self.settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( self.settings.rar_exe_path )
|
||||
|
||||
if ca.seemsToBeAComicArchive() :
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
|
||||
fi = FileInfo( ca )
|
||||
|
||||
filename_item = QTableWidgetItem()
|
||||
folder_item = QTableWidgetItem()
|
||||
cix_item = FileTableWidgetItem()
|
||||
cbi_item = FileTableWidgetItem()
|
||||
readonly_item = FileTableWidgetItem()
|
||||
type_item = QTableWidgetItem()
|
||||
|
||||
filename_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
filename_item.setData( Qt.UserRole , fi )
|
||||
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
|
||||
|
||||
folder_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
|
||||
|
||||
type_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
|
||||
|
||||
cix_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
|
||||
|
||||
cbi_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
|
||||
|
||||
readonly_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
readonly_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
|
||||
|
||||
self.updateRow( row )
|
||||
|
||||
return row
|
||||
|
||||
def updateRow( self, row ):
|
||||
fi = self.twList.item( row, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
|
||||
|
||||
filename_item = self.twList.item( row, FileSelectionList.fileColNum )
|
||||
folder_item = self.twList.item( row, FileSelectionList.folderColNum )
|
||||
cix_item = self.twList.item( row, FileSelectionList.CRFlagColNum )
|
||||
cbi_item = self.twList.item( row, FileSelectionList.CBLFlagColNum )
|
||||
type_item = self.twList.item( row, FileSelectionList.typeColNum )
|
||||
readonly_item = self.twList.item( row, FileSelectionList.readonlyColNum )
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[0]
|
||||
folder_item.setText( item_text )
|
||||
folder_item.setData( Qt.ToolTipRole, item_text )
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[1]
|
||||
filename_item.setText( item_text )
|
||||
filename_item.setData( Qt.ToolTipRole, item_text )
|
||||
|
||||
if fi.ca.isZip():
|
||||
item_text = "ZIP"
|
||||
elif fi.ca.isRar():
|
||||
item_text = "RAR"
|
||||
else:
|
||||
item_text = ""
|
||||
type_item.setText( item_text )
|
||||
type_item.setData( Qt.ToolTipRole, item_text )
|
||||
|
||||
|
||||
if fi.ca.hasCIX():
|
||||
cix_item.setCheckState(Qt.Checked)
|
||||
cix_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cix_item.setData(Qt.UserRole, False)
|
||||
cix_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if fi.ca.hasCBI():
|
||||
cbi_item.setCheckState(Qt.Checked)
|
||||
cbi_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cbi_item.setData(Qt.UserRole, False)
|
||||
cbi_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if not fi.ca.isWritable():
|
||||
readonly_item.setCheckState(Qt.Checked)
|
||||
readonly_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
readonly_item.setData(Qt.UserRole, False)
|
||||
readonly_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
|
||||
# Reading these will force them into the ComicArchive's cache
|
||||
fi.ca.readCIX()
|
||||
fi.ca.hasCBI()
|
||||
|
||||
def getSelectedArchiveList( self ):
|
||||
ca_list = []
|
||||
for r in range( self.twList.rowCount() ):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
fi = item.data( Qt.UserRole ).toPyObject()
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
return ca_list
|
||||
|
||||
def updateCurrentRow( self ):
|
||||
self.updateRow( self.twList.currentRow() )
|
||||
|
||||
def updateSelectedRows( self ):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for r in range( self.twList.rowCount() ):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
self.updateRow( r )
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def currentItemChangedCB( self, curr, prev ):
|
||||
|
||||
new_idx = curr.row()
|
||||
old_idx = -1
|
||||
if prev is not None:
|
||||
old_idx = prev.row()
|
||||
#print "old {0} new {1}".format(old_idx, new_idx)
|
||||
|
||||
if old_idx == new_idx:
|
||||
return
|
||||
|
||||
# don't allow change if modified
|
||||
if prev is not None and new_idx != old_idx:
|
||||
if not self.modifiedFlagVerification( "Change Archive",
|
||||
"If you change archives now, data in the form will be lost. Are you sure?"):
|
||||
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
|
||||
self.twList.setCurrentItem( prev )
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
# Need to defer this revert selection, for some reason
|
||||
QTimer.singleShot(1, self.revertSelection)
|
||||
return
|
||||
|
||||
fi = self.twList.item( new_idx, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
|
||||
self.selectionChanged.emit( QVariant(fi))
|
||||
|
||||
def revertSelection( self ):
|
||||
self.twList.selectRow( self.twList.currentRow() )
|
||||
|
||||
|
||||
def modifiedFlagVerification( self, title, desc):
|
||||
if self.modifiedFlag:
|
||||
reply = QMessageBox.question(self,
|
||||
self.tr(title),
|
||||
self.tr(desc),
|
||||
QMessageBox.Yes, QMessageBox.No )
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Attempt to use a special checkbox widget in the cell.
|
||||
# Couldn't figure out how to disable it with "enabled" colors
|
||||
#w = QWidget()
|
||||
#cb = QCheckBox(w)
|
||||
#cb.setCheckState(Qt.Checked)
|
||||
#layout = QHBoxLayout()
|
||||
#layout.addWidget( cb )
|
||||
#layout.setAlignment(Qt.AlignHCenter)
|
||||
#layout.setMargin(2)
|
||||
#w.setLayout(layout)
|
||||
#self.twList.setCellWidget( row, 2, w )
|
112
fileselectionlist.ui
Normal file
112
fileselectionlist.ui
Normal file
@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>pageListEditor</class>
|
||||
<widget class="QWidget" name="pageListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>527</width>
|
||||
<height>323</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideMiddle</enum>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>61</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>36</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File Name</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CR</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Has ComicRack Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CBL</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Has ComicBookLover Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Archive Type</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>R/O</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Read-Only</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Folder</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File Location</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -32,7 +32,6 @@ class PageType:
|
||||
Roundup = "Roundup"
|
||||
Story = "Story"
|
||||
Advertisment = "Advertisment"
|
||||
Story = "Story"
|
||||
Editorial = "Editorial"
|
||||
Letters = "Letters"
|
||||
Preview = "Preview"
|
||||
@ -93,11 +92,19 @@ class GenericMetadata:
|
||||
self.characters = None
|
||||
self.teams = None
|
||||
self.locations = None
|
||||
|
||||
|
||||
self.credits = list()
|
||||
self.tags = list()
|
||||
self.pages = list()
|
||||
|
||||
# Some CoMet-only items
|
||||
self.price = None
|
||||
self.isVersionOf = None
|
||||
self.rights = None
|
||||
self.identifier = None
|
||||
self.lastMark = None
|
||||
self.coverImage = None
|
||||
|
||||
def overlay( self, new_md ):
|
||||
# Overlay a metadata object on this one
|
||||
# that is, when the new object has non-None
|
||||
@ -125,24 +132,30 @@ class GenericMetadata:
|
||||
assign( "genre", new_md.genre )
|
||||
assign( "language", new_md.language )
|
||||
assign( "country", new_md.country )
|
||||
assign( "alternateSeries", new_md.criticalRating )
|
||||
assign( "criticalRating", new_md.criticalRating )
|
||||
assign( "alternateSeries", new_md.alternateSeries )
|
||||
assign( "alternateNumber", new_md.alternateNumber )
|
||||
assign( "alternateCount", new_md.alternateCount )
|
||||
assign( "imprint", new_md.imprint )
|
||||
assign( "webLink", new_md.webLink )
|
||||
assign( "webLink", new_md.webLink )
|
||||
assign( "format", new_md.format )
|
||||
assign( "manga", new_md.manga )
|
||||
assign( "blackAndWhite", new_md.blackAndWhite )
|
||||
assign( "maturityRating", new_md.maturityRating )
|
||||
assign( "scanInfo", new_md.scanInfo )
|
||||
assign( "scanInfo", new_md.scanInfo )
|
||||
assign( "storyArc", new_md.storyArc )
|
||||
assign( "seriesGroup", new_md.seriesGroup )
|
||||
assign( "scanInfo", new_md.scanInfo )
|
||||
assign( "characters", new_md.characters )
|
||||
assign( "teams", new_md.teams )
|
||||
assign( "locations", new_md.locations )
|
||||
assign( "comments", new_md.comments )
|
||||
assign( "notes", new_md.notes )
|
||||
|
||||
assign( "price", new_md.price )
|
||||
assign( "isVersionOf", new_md.isVersionOf )
|
||||
assign( "rights", new_md.rights )
|
||||
assign( "identifier", new_md.identifier )
|
||||
assign( "lastMark", new_md.lastMark )
|
||||
|
||||
self.overlayCredits( new_md.credits )
|
||||
# TODO
|
||||
@ -153,7 +166,8 @@ class GenericMetadata:
|
||||
# For now, go the easy route, where any overlay
|
||||
# value wipes out the whole list
|
||||
if len(new_md.tags) > 0:
|
||||
assign( "tags", new_md.tags )
|
||||
assign( "tags", new_md.tags )
|
||||
|
||||
if len(new_md.pages) > 0:
|
||||
assign( "pages", new_md.pages )
|
||||
|
||||
@ -173,7 +187,35 @@ class GenericMetadata:
|
||||
# otherwise, add it!
|
||||
else:
|
||||
self.addCredit( c['person'], c['role'], primary )
|
||||
|
||||
|
||||
def setDefaultPageList( self, count ):
|
||||
# generate a default page list, with the first page marked as the cover
|
||||
for i in range(count):
|
||||
page_dict = dict()
|
||||
page_dict['Image'] = str(i)
|
||||
if i == 0:
|
||||
page_dict['Type'] = PageType.FrontCover
|
||||
self.pages.append( page_dict )
|
||||
|
||||
def getArchivePageIndex( self, pagenum ):
|
||||
# convert the displayed page number to the page index of the file in the archive
|
||||
if pagenum < len( self.pages ):
|
||||
return int( self.pages[pagenum]['Image'] )
|
||||
else:
|
||||
return 0
|
||||
|
||||
def getCoverPageIndexList( self ):
|
||||
# return a list of archive page indices of cover pages
|
||||
coverlist = []
|
||||
for p in self.pages:
|
||||
if 'Type' in p and p['Type'] == PageType.FrontCover:
|
||||
coverlist.append( int(p['Image']))
|
||||
|
||||
if len(coverlist) == 0:
|
||||
coverlist.append( 0 )
|
||||
|
||||
return coverlist
|
||||
|
||||
def addCredit( self, person, role, primary = False ):
|
||||
|
||||
credit = dict()
|
||||
@ -229,6 +271,13 @@ class GenericMetadata:
|
||||
add_attr_string( "webLink" )
|
||||
add_attr_string( "format" )
|
||||
add_attr_string( "manga" )
|
||||
|
||||
add_attr_string( "price" )
|
||||
add_attr_string( "isVersionOf" )
|
||||
add_attr_string( "rights" )
|
||||
add_attr_string( "identifier" )
|
||||
add_attr_string( "lastMark" )
|
||||
|
||||
if self.blackAndWhite:
|
||||
add_attr_string( "blackAndWhite" )
|
||||
add_attr_string( "maturityRating" )
|
||||
@ -246,7 +295,7 @@ class GenericMetadata:
|
||||
for c in self.credits:
|
||||
primary = ""
|
||||
if c.has_key('primary') and c['primary']:
|
||||
primary == " [P]"
|
||||
primary = " [P]"
|
||||
add_string( "credit", c['role']+": "+c['person'] + primary)
|
||||
|
||||
# find the longest field name
|
||||
|
BIN
graphics/autotag.png
Normal file
BIN
graphics/autotag.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
graphics/longbox.png
Normal file
BIN
graphics/longbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -1,5 +1,6 @@
|
||||
|
||||
import StringIO
|
||||
import sys
|
||||
|
||||
try:
|
||||
import Image
|
||||
@ -17,14 +18,25 @@ class ImageHasher(object):
|
||||
|
||||
if path is None and data is None:
|
||||
raise IOError
|
||||
elif path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
|
||||
try:
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
except:
|
||||
print "Image data seems corrupted!"
|
||||
# just generate a bogus image
|
||||
self.image = Image.new( "L", (1,1))
|
||||
|
||||
def average_hash(self):
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
try:
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "average_hash error:", e
|
||||
return long(0)
|
||||
|
||||
pixels = list(image.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
|
||||
|
@ -34,6 +34,7 @@ from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagehasher import ImageHasher
|
||||
from imagefetcher import ImageFetcher, ImageFetcherException
|
||||
from issuestring import IssueString
|
||||
|
||||
import utils
|
||||
|
||||
@ -71,8 +72,10 @@ class IssueIdentifier:
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
self.callback = None
|
||||
self.coverUrlCallback = None
|
||||
self.search_result = self.ResultNoMatches
|
||||
|
||||
self.cover_page_index = 0
|
||||
|
||||
def setScoreMinThreshold( self, thresh ):
|
||||
self.min_score_thresh = thresh
|
||||
|
||||
@ -95,7 +98,7 @@ class IssueIdentifier:
|
||||
def setOutputFunction( self, func ):
|
||||
self.output_function = func
|
||||
pass
|
||||
|
||||
|
||||
def calculateHash( self, image_data ):
|
||||
if self.image_hasher == '3':
|
||||
return ImageHasher( data=image_data ).dct_average_hash()
|
||||
@ -105,11 +108,13 @@ class IssueIdentifier:
|
||||
return ImageHasher( data=image_data ).average_hash()
|
||||
|
||||
def getAspectRatio( self, image_data ):
|
||||
try:
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w,h = im.size
|
||||
return float(h)/float(w)
|
||||
except:
|
||||
return 1.5
|
||||
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w,h = im.size
|
||||
return float(h)/float(w)
|
||||
|
||||
def cropCover( self, image_data ):
|
||||
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
@ -126,6 +131,9 @@ class IssueIdentifier:
|
||||
|
||||
def setProgressCallback( self, cb_func ):
|
||||
self.callback = cb_func
|
||||
|
||||
def setCoverURLCallback( self, cb_func ):
|
||||
self.coverUrlCallback = cb_func
|
||||
|
||||
def getSearchKeys( self ):
|
||||
|
||||
@ -194,7 +202,7 @@ class IssueIdentifier:
|
||||
|
||||
@staticmethod
|
||||
def defaultWriteOutput( text ):
|
||||
sys.stdout.write(text)
|
||||
sys.stdout.write(text.encode( errors='replace') )
|
||||
sys.stdout.flush()
|
||||
|
||||
def log_msg( self, msg , newline=True ):
|
||||
@ -217,7 +225,7 @@ class IssueIdentifier:
|
||||
self.log_msg( "Sorry, but "+ opts.filename + " is not a comic archive!")
|
||||
return self.match_list
|
||||
|
||||
cover_image_data = ca.getCoverPage()
|
||||
cover_image_data = ca.getPage( self.cover_page_index )
|
||||
cover_hash = self.calculateHash( cover_image_data )
|
||||
|
||||
#check the apect ratio
|
||||
@ -248,12 +256,12 @@ class IssueIdentifier:
|
||||
if keys['month'] is not None:
|
||||
self.log_msg( "\tMonth : " + str(keys['month']) )
|
||||
|
||||
self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
|
||||
comicVine = ComicVineTalker( )
|
||||
|
||||
#self.log_msg( ( "Searching for " + keys['series'] + "...")
|
||||
self.log_msg( "Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
|
||||
self.log_msg( u"Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
|
||||
try:
|
||||
cv_search_results = comicVine.searchForSeries( keys['series'] )
|
||||
except ComicVineTalkerException:
|
||||
@ -273,7 +281,7 @@ class IssueIdentifier:
|
||||
date_approved = True
|
||||
|
||||
# remove any series that starts after the issue year
|
||||
if keys['year'] is not None and keys['year'].isdigit():
|
||||
if keys['year'] is not None and str(keys['year']).isdigit():
|
||||
if int(keys['year']) < item['start_year']:
|
||||
date_approved = False
|
||||
|
||||
@ -302,7 +310,6 @@ class IssueIdentifier:
|
||||
if self.callback is not None:
|
||||
self.callback( 0, len(series_shortlist))
|
||||
|
||||
|
||||
# now sort the list by name length
|
||||
series_shortlist.sort(key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
@ -315,7 +322,7 @@ class IssueIdentifier:
|
||||
counter += 1
|
||||
self.callback( counter, len(series_shortlist))
|
||||
|
||||
self.log_msg( "Fetching info for ID: {0} {1} ({2}) ...".format(
|
||||
self.log_msg( u"Fetching info for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=False )
|
||||
@ -328,13 +335,8 @@ class IssueIdentifier:
|
||||
|
||||
issue_list = cv_series_results['issues']
|
||||
for issue in issue_list:
|
||||
num_s = IssueString(issue['issue_number']).asString()
|
||||
|
||||
# format the issue number string nicely, since it's usually something like "2.00"
|
||||
num_f = float(issue['issue_number'])
|
||||
num_s = str( int(math.floor(num_f)) )
|
||||
if math.floor(num_f) != num_f:
|
||||
num_s = str( num_f )
|
||||
|
||||
# look for a matching issue number
|
||||
if num_s == keys['issue_number']:
|
||||
# found a matching issue number! now get the issue data
|
||||
@ -359,6 +361,9 @@ class IssueIdentifier:
|
||||
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)
|
||||
|
||||
@ -368,7 +373,7 @@ class IssueIdentifier:
|
||||
score = min( score, score2 )
|
||||
|
||||
match = dict()
|
||||
match['series'] = "{0} ({1})".format(series['name'], series['start_year'])
|
||||
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
|
||||
@ -388,7 +393,6 @@ class IssueIdentifier:
|
||||
|
||||
break
|
||||
self.log_msg( "" )
|
||||
|
||||
|
||||
if len(self.match_list) == 0:
|
||||
self.log_msg( ":-( no matches!" )
|
||||
@ -420,20 +424,20 @@ class IssueIdentifier:
|
||||
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( "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(5, ca.getNumberOfPages())):
|
||||
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 d={0} on page {1}!".format(distance, i+1) )
|
||||
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 d={0} on page {1}".format(distance, i) )
|
||||
self.log_msg( "Found a good match (distance = {0}) on page {1}".format(distance, i) )
|
||||
found = True
|
||||
self.log_msg( ".", newline=False )
|
||||
self.log_msg( "" )
|
||||
@ -441,7 +445,9 @@ class IssueIdentifier:
|
||||
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:
|
||||
@ -456,7 +462,9 @@ class IssueIdentifier:
|
||||
self.match_list.remove(item)
|
||||
|
||||
if len(self.match_list) == 1:
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
|
||||
elif len(self.match_list) == 0:
|
||||
@ -466,8 +474,10 @@ class IssueIdentifier:
|
||||
print
|
||||
self.log_msg( "More than one likley candiate." )
|
||||
self.search_result = self.ResultMultipleGoodMatches
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
for item in self.match_list:
|
||||
print_match(item)
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
|
||||
return self.match_list
|
||||
|
||||
|
@ -28,6 +28,7 @@ from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
@ -94,12 +95,12 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = u"{0}".format(record['name'])
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
if float(record['issue_number']) == float(self.issue_number):
|
||||
if IssueString(record['issue_number']).asString() == IssueString(self.issue_number).asString():
|
||||
self.initial_id = record['id']
|
||||
|
||||
row += 1
|
||||
|
101
issuestring.py
Normal file
101
issuestring.py
Normal file
@ -0,0 +1,101 @@
|
||||
"""
|
||||
Class for handling the odd permutations of an 'issue number' that the comics industry throws at us
|
||||
|
||||
e.g.:
|
||||
|
||||
"12"
|
||||
"12.1"
|
||||
"0"
|
||||
"-1"
|
||||
"5AU"
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import utils
|
||||
import math
|
||||
import re
|
||||
|
||||
class IssueString:
|
||||
def __init__(self, text):
|
||||
|
||||
if text is None:
|
||||
self.num = None
|
||||
self.suffix = ""
|
||||
return
|
||||
|
||||
self.text = str(text)
|
||||
#strip out non float-y stuff
|
||||
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
|
||||
|
||||
if tmp_num_str == "":
|
||||
self.num = None
|
||||
self.suffix = self.text
|
||||
|
||||
else:
|
||||
if tmp_num_str.count(".") > 1:
|
||||
#make sure it's a valid float or int.
|
||||
parts = tmp_num_str.split('.')
|
||||
self.num = float( parts[0] + '.' + parts[1] )
|
||||
else:
|
||||
self.num = float( tmp_num_str )
|
||||
|
||||
self.suffix = ""
|
||||
parts = self.text.split(tmp_num_str)
|
||||
if len( parts ) > 1 :
|
||||
self.suffix = parts[1]
|
||||
|
||||
def asString( self, pad = 0 ):
|
||||
#return the float, left side zero-padded, with suffix attached
|
||||
if self.num is None:
|
||||
return self.suffix
|
||||
|
||||
negative = self.num < 0
|
||||
|
||||
num_f = abs(self.num)
|
||||
|
||||
num_int = int( num_f )
|
||||
num_s = str( num_int )
|
||||
if float( num_int ) != num_f:
|
||||
num_s = str( num_f )
|
||||
|
||||
num_s += self.suffix
|
||||
|
||||
# create padding
|
||||
padding = ""
|
||||
l = len( str(num_int))
|
||||
if l < pad :
|
||||
padding = "0" * (pad - l)
|
||||
|
||||
num_s = padding + num_s
|
||||
if negative:
|
||||
num_s = "-" + num_s
|
||||
|
||||
return num_s
|
||||
|
||||
def asFloat( self ):
|
||||
#return the float, with no suffix
|
||||
return self.num
|
||||
|
||||
def asInt( self ):
|
||||
#return the int version of the float
|
||||
if self.num is None:
|
||||
return None
|
||||
return int( self.num )
|
||||
|
||||
|
@ -33,6 +33,7 @@ diskimage:
|
||||
rm -rf $(STAGING)
|
||||
mkdir $(STAGING)
|
||||
cp $(TAGGER_BASE)/release_notes.txt $(STAGING)
|
||||
ln -s /Applications $(STAGING)/Applications
|
||||
cp -a $(APP_BUNDLE) $(STAGING)
|
||||
cp $(MAC_BASE)/volume.icns $(STAGING)/.VolumeIcon.icns
|
||||
SetFile -c icnC $(STAGING)/.VolumeIcon.icns
|
||||
|
102
options.py
102
options.py
@ -23,6 +23,7 @@ import getopt
|
||||
import platform
|
||||
import os
|
||||
|
||||
import ctversion
|
||||
from genericmetadata import GenericMetadata
|
||||
|
||||
class Enum(set):
|
||||
@ -34,7 +35,8 @@ class Enum(set):
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
name = [ 'ComicBookLover', 'ComicRack' ]
|
||||
COMET = 2
|
||||
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
|
||||
|
||||
|
||||
class Options:
|
||||
@ -50,17 +52,24 @@ If no options are given, {0} will run in windowed mode
|
||||
--raw With -p, will print out the raw tag block(s)
|
||||
from the file
|
||||
-d, --delete Deletes the tag block of specified type (via -t)
|
||||
-c, --copy=SOURCE Copy the specified source tag block to destination style
|
||||
specified via via -t (potentially lossy operation)
|
||||
-s, --save Save out tags as specified type (via -t)
|
||||
Must specify also at least -o, -p, or -m
|
||||
--nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c )
|
||||
-n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r)
|
||||
-t, --type=TYPE Specify TYPE as either "CR" or "CBL", (as either
|
||||
ComicRack or ComicBookLover style tags, respectivly)
|
||||
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either
|
||||
ComicRack, ComicBookLover, or CoMet style tags, respectivly)
|
||||
-f, --parsefilename Parse the filename to get some info, specifically
|
||||
series name, issue number, volume, and publication
|
||||
year
|
||||
-i, --interactive Interactively query the user when there are multiple matches for
|
||||
an online search
|
||||
--nosummary Suppress the default summary after a save operation
|
||||
-o, --online Search online and attempt to identify file using
|
||||
existing metadata and images in archive. May be used
|
||||
in conjuntion with -f and -m
|
||||
--id=ID Use the issue ID when searching online. Overrides all other metadata
|
||||
-m, --metadata=LIST Explicity define, as a list, some tags to be used
|
||||
e.g. "series=Plastic Man , publisher=Quality Comics"
|
||||
"series=Kickers^, Inc., issue=1, year=1986"
|
||||
@ -71,6 +80,8 @@ If no options are given, {0} will run in windowed mode
|
||||
-r, --rename Rename the file based on specified tag style.
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
-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
|
||||
"""
|
||||
|
||||
@ -80,23 +91,32 @@ If no options are given, {0} will run in windowed mode
|
||||
self.no_gui = False
|
||||
self.filename = None
|
||||
self.verbose = False
|
||||
self.terse = False
|
||||
self.metadata = None
|
||||
self.print_tags = False
|
||||
self.copy_tags = False
|
||||
self.delete_tags = False
|
||||
self.search_online = False
|
||||
self.dryrun = False
|
||||
self.abortOnLowConfidence = True
|
||||
self.save_tags = False
|
||||
self.parse_filename = False
|
||||
self.show_save_summary = True
|
||||
self.raw = False
|
||||
self.rename_file = False
|
||||
self.no_overwrite = False
|
||||
self.interactive = False
|
||||
self.issue_id = None
|
||||
self.file_list = []
|
||||
|
||||
def display_help_and_quit( self, msg, code ):
|
||||
def display_msg_and_quit( self, msg, code, show_help=False ):
|
||||
appname = os.path.basename(sys.argv[0])
|
||||
if msg is not None:
|
||||
print( msg )
|
||||
print self.help_text.format(appname)
|
||||
if show_help:
|
||||
print self.help_text.format(appname)
|
||||
else:
|
||||
print "For more help, run with '--help'"
|
||||
sys.exit(code)
|
||||
|
||||
def parseMetadataFromString( self, mdstr ):
|
||||
@ -159,23 +179,36 @@ If no options are given, {0} will run in windowed mode
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt( input_args,
|
||||
"hpdt:fm:vonsr",
|
||||
[ "help", "print", "delete", "type=", "parsefilename", "metadata=", "verbose",
|
||||
"online", "dryrun", "save", "rename" , "raw", "noabort" ])
|
||||
"hpdt:fm:vonsrc:i",
|
||||
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
|
||||
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
|
||||
"interactive", "nosummary", "version", "id=" ])
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_help_and_quit( str(err), 2 )
|
||||
self.display_msg_and_quit( str(err), 2 )
|
||||
|
||||
# process options
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
self.display_help_and_quit( None, 0 )
|
||||
self.display_msg_and_quit( None, 0, show_help=True )
|
||||
if o in ("-v", "--verbose"):
|
||||
self.verbose = True
|
||||
if o in ("-p", "--print"):
|
||||
self.print_tags = True
|
||||
if o in ("-d", "--delete"):
|
||||
self.delete_tags = True
|
||||
if o in ("-i", "--interactive"):
|
||||
self.interactive = True
|
||||
if o in ("-c", "--copy"):
|
||||
self.copy_tags = True
|
||||
if a.lower() == "cr":
|
||||
self.copy_source = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.copy_source = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.copy_source = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit( "Invalid copy tag source type", 1 )
|
||||
if o in ("-o", "--online"):
|
||||
self.search_online = True
|
||||
if o in ("-n", "--dryrun"):
|
||||
@ -188,43 +221,68 @@ If no options are given, {0} will run in windowed mode
|
||||
self.rename_file = True
|
||||
if o in ("-f", "--parsefilename"):
|
||||
self.parse_filename = True
|
||||
if o in ("--raw"):
|
||||
if o == "--id":
|
||||
self.issue_id = a
|
||||
if o == "--raw":
|
||||
self.raw = True
|
||||
if o in ("--noabort"):
|
||||
if o == "--noabort":
|
||||
self.abortOnLowConfidence = False
|
||||
if o == "--terse":
|
||||
self.terse = True
|
||||
if o == "--nosummary":
|
||||
self.show_save_summary = False
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--version":
|
||||
print "ComicTagger version: ", ctversion.version
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
self.data_style = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.data_style = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.data_style = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_help_and_quit( "Invalid tag type", 1 )
|
||||
self.display_msg_and_quit( "Invalid tag type", 1 )
|
||||
|
||||
if self.print_tags or self.delete_tags or self.save_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:
|
||||
self.no_gui = True
|
||||
|
||||
count = 0
|
||||
if self.print_tags: count += 1
|
||||
if self.delete_tags: count += 1
|
||||
if self.save_tags: count += 1
|
||||
if self.copy_tags: count += 1
|
||||
if self.rename_file: count += 1
|
||||
|
||||
if count > 1:
|
||||
self.display_help_and_quit( "Must choose only one action of print, delete, save, or rename", 1 )
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
|
||||
|
||||
if len(args) > 0:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
if platform.system() == "Windows":
|
||||
# no globbing on windows shell, so do it for them
|
||||
import glob
|
||||
self.file_list = []
|
||||
for item in args:
|
||||
self.file_list.extend(glob.glob(item))
|
||||
self.filename = self.file_list[0]
|
||||
else:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
|
||||
if self.no_gui and self.filename is None:
|
||||
self.display_help_and_quit( "Command requires a filename!", 1 )
|
||||
self.display_msg_and_quit( "Command requires a filename!", 1 )
|
||||
|
||||
if self.delete_tags and self.data_style is None:
|
||||
self.display_help_and_quit( "Please specify the type to delete with -t", 1 )
|
||||
self.display_msg_and_quit( "Please specify the type to delete with -t", 1 )
|
||||
|
||||
if self.save_tags and self.data_style is None:
|
||||
self.display_help_and_quit( "Please specify the type to save with -t", 1 )
|
||||
self.display_msg_and_quit( "Please specify the type to save with -t", 1 )
|
||||
|
||||
if self.copy_tags and self.data_style is None:
|
||||
self.display_msg_and_quit( "Please specify the type to copy to with -t", 1 )
|
||||
|
||||
if self.rename_file and self.data_style is None:
|
||||
self.display_help_and_quit( "Please specify the type to use for renaming with -t", 1 )
|
||||
#if self.rename_file and self.data_style is None:
|
||||
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )
|
||||
|
||||
|
@ -26,7 +26,7 @@ from settings import ComicTaggerSettings
|
||||
|
||||
class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent, metadata):
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
|
||||
@ -37,6 +37,7 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.current_pixmap = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
self.btnNext.clicked.connect( self.nextPage )
|
||||
self.btnPrev.clicked.connect( self.prevPage )
|
||||
@ -66,7 +67,8 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.setPage()
|
||||
|
||||
def setPage( self ):
|
||||
image_data = self.comic_archive.getPage( self.current_page_num )
|
||||
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 )
|
||||
|
309
pagelisteditor.py
Normal file
309
pagelisteditor.py
Normal file
@ -0,0 +1,309 @@
|
||||
"""
|
||||
A PyQt4 widget for editing the page list info
|
||||
"""
|
||||
|
||||
"""
|
||||
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 pageloader import PageLoader
|
||||
|
||||
def itemMoveEvents( widget ):
|
||||
|
||||
class Filter(QObject):
|
||||
|
||||
mysignal = pyqtSignal( str )
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
#print event.type()
|
||||
if event.type() == QEvent.ChildRemoved:
|
||||
#print "ChildRemoved"
|
||||
self.mysignal.emit("finish")
|
||||
if event.type() == QEvent.ChildAdded:
|
||||
#print "ChildAdded"
|
||||
self.mysignal.emit("start")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter( widget )
|
||||
widget.installEventFilter( filter )
|
||||
return filter.mysignal
|
||||
|
||||
class PageListEditor(QWidget):
|
||||
|
||||
firstFrontCoverChanged = pyqtSignal( int )
|
||||
listOrderChanged = pyqtSignal( )
|
||||
modified = pyqtSignal( )
|
||||
|
||||
pageTypeNames = {
|
||||
PageType.FrontCover: "Front Cover",
|
||||
PageType.InnerCover: "Inner Cover",
|
||||
PageType.Advertisment: "Advertisment",
|
||||
PageType.Roundup: "Roundup",
|
||||
PageType.Story: "Story",
|
||||
PageType.Editorial: "Editorial",
|
||||
PageType.Letters: "Letters",
|
||||
PageType.Preview: "Preview",
|
||||
PageType.BackCover: "Back Cover",
|
||||
PageType.Other: "Other",
|
||||
PageType.Deleted: "Deleted",
|
||||
}
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
|
||||
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
self.page_loader = None
|
||||
|
||||
self.resetPage()
|
||||
|
||||
# Add the entries to the manga combobox
|
||||
self.comboBox.addItem( "", "" )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.FrontCover], PageType.FrontCover )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.InnerCover], PageType.InnerCover )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Advertisment], PageType.Advertisment )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Roundup], PageType.Roundup )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Story], PageType.Story )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Editorial], PageType.Editorial )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Letters], PageType.Letters )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Preview], PageType.Preview )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.BackCover], PageType.BackCover )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Other], PageType.Other )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Deleted], PageType.Deleted )
|
||||
|
||||
self.listWidget.itemSelectionChanged.connect( self.changePage )
|
||||
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
|
||||
self.comboBox.activated.connect( self.changePageType )
|
||||
self.btnUp.clicked.connect( self.moveCurrentUp )
|
||||
self.btnDown.clicked.connect( self.moveCurrentDown )
|
||||
self.pre_move_row = -1
|
||||
self.first_front_page = None
|
||||
|
||||
def resetPage( self ):
|
||||
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
|
||||
def moveCurrentUp( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem ( row )
|
||||
self.listWidget.insertItem( row-1, item )
|
||||
self.listWidget.setCurrentRow( row-1 )
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def moveCurrentDown( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
if row < self.listWidget.count()-1:
|
||||
item = self.listWidget.takeItem ( row )
|
||||
self.listWidget.insertItem( row+1, item )
|
||||
self.listWidget.setCurrentRow( row+1 )
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def itemMoveEvent(self, s):
|
||||
#print "move event: ", s, self.listWidget.currentRow()
|
||||
if s == "start":
|
||||
self.pre_move_row = self.listWidget.currentRow()
|
||||
if s == "finish":
|
||||
if self.pre_move_row != self.listWidget.currentRow():
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def changePageType( self , i):
|
||||
new_type = self.comboBox.itemData(i).toString()
|
||||
if self.getCurrentPageType() != new_type:
|
||||
self.setCurrentPageType( new_type )
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def changePage( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
pagetype = self.getCurrentPageType()
|
||||
|
||||
i = self.comboBox.findData( pagetype )
|
||||
self.comboBox.setCurrentIndex( i )
|
||||
|
||||
#idx = int(str (self.listWidget.item( row ).text()))
|
||||
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
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)
|
||||
|
||||
def getFirstFrontCover( self ):
|
||||
frontCover = 0
|
||||
for i in range( self.listWidget.count() ):
|
||||
item = self.listWidget.item( i )
|
||||
page_dict = item.data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict and page_dict['Type'] == PageType.FrontCover:
|
||||
frontCover = int(page_dict['Image'])
|
||||
break
|
||||
return frontCover
|
||||
|
||||
|
||||
def getCurrentPageType( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict:
|
||||
return page_dict['Type']
|
||||
else:
|
||||
return ""
|
||||
|
||||
def setCurrentPageType( self, t ):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
|
||||
|
||||
if t == "":
|
||||
if 'Type' in page_dict:
|
||||
del(page_dict['Type'])
|
||||
else:
|
||||
page_dict['Type'] = str(t)
|
||||
|
||||
item = self.listWidget.item( row )
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (page_dict,) )
|
||||
item.setText( self.listEntryText( page_dict ) )
|
||||
|
||||
|
||||
def 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
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect( self.changePage )
|
||||
|
||||
self.listWidget.clear()
|
||||
for p in pages_list:
|
||||
item = QListWidgetItem( self.listEntryText( p ) )
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (p, ))
|
||||
|
||||
self.listWidget.addItem( item )
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.listWidget.itemSelectionChanged.connect( self.changePage )
|
||||
self.listWidget.setCurrentRow ( 0 )
|
||||
|
||||
def listEntryText(self, page_dict):
|
||||
text = page_dict['Image']
|
||||
if 'Type' in page_dict:
|
||||
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
|
||||
return text
|
||||
|
||||
def getPageList( self ):
|
||||
page_list = []
|
||||
for i in range( self.listWidget.count() ):
|
||||
item = self.listWidget.item( i )
|
||||
page_list.append( item.data(Qt.UserRole).toPyObject()[0] )
|
||||
return page_list
|
||||
|
||||
def emitFrontCoverChange( self ):
|
||||
if self.first_front_page != self.getFirstFrontCover():
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.firstFrontCoverChanged.emit( self.first_front_page )
|
||||
|
||||
def setMetadataStyle( self, data_style ):
|
||||
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.label.palette()
|
||||
|
||||
inactive_palette3 = self.label.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
|
||||
|
||||
if data_style == MetaDataStyle.CIX:
|
||||
self.btnUp.setEnabled( True )
|
||||
self.btnDown.setEnabled( True )
|
||||
self.comboBox.setEnabled( True )
|
||||
self.listWidget.setEnabled( True )
|
||||
|
||||
self.listWidget.setPalette(active_palette)
|
||||
|
||||
elif data_style == MetaDataStyle.CBI:
|
||||
self.btnUp.setEnabled( False )
|
||||
self.btnDown.setEnabled( False )
|
||||
self.comboBox.setEnabled( False )
|
||||
self.listWidget.setEnabled( False )
|
||||
|
||||
self.listWidget.setPalette(inactive_palette3)
|
||||
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
def showEvent( self, event ):
|
||||
# make sure to adjust the size and pos of the pixmap based on frame size
|
||||
self.setDisplayPixmap( 0,0 )
|
||||
|
135
pagelisteditor.ui
Normal file
135
pagelisteditor.ui
Normal file
@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>pageListEditor</class>
|
||||
<widget class="QWidget" name="pageListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>527</width>
|
||||
<height>323</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="listWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::InternalMove</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnUp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>^</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnDown">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>v</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Page Type:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<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>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
77
pageloader.py
Normal file
77
pageloader.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""
|
||||
A PyQT4 class to load a page image from a ComicArchive in a background thread
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comicarchive import ComicArchive
|
||||
|
||||
"""
|
||||
This class holds onto a reference of each instance in a list
|
||||
since problems occur if the ref count goes to zero and the GC
|
||||
tries to reap the object while the thread is going.
|
||||
|
||||
If the client class wants to stop the thread, they should mark
|
||||
it as "abandoned", and no signals will be issued
|
||||
"""
|
||||
|
||||
class PageLoader( QtCore.QThread ):
|
||||
|
||||
loadComplete = pyqtSignal( QtGui.QImage )
|
||||
|
||||
instanceList = []
|
||||
mutex = QtCore.QMutex()
|
||||
|
||||
"""
|
||||
Remove all finished threads from the list
|
||||
"""
|
||||
@staticmethod
|
||||
def reapInstances():
|
||||
for obj in reversed(PageLoader.instanceList ):
|
||||
if obj.isFinished():
|
||||
PageLoader.instanceList.remove(obj)
|
||||
|
||||
def __init__(self, ca, page_num ):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.ca = ca
|
||||
self.page_num = page_num
|
||||
self.abandoned = False
|
||||
|
||||
# remove any old instances, and then add ourself
|
||||
PageLoader.mutex.lock()
|
||||
PageLoader.reapInstances()
|
||||
PageLoader.instanceList.append( self )
|
||||
PageLoader.mutex.unlock()
|
||||
|
||||
def run(self):
|
||||
image_data = self.ca.getPage( self.page_num )
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
self.loadComplete.emit( img )
|
||||
|
||||
|
@ -32,7 +32,12 @@ class IDProgressWindow(QtGui.QDialog):
|
||||
|
||||
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 )
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>556</width>
|
||||
<width>650</width>
|
||||
<height>287</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -28,7 +28,11 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="readOnly">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier</family>
|
||||
</font>
|
||||
</property> <property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
|
@ -1,4 +1,78 @@
|
||||
|
||||
30-Nov-2012
|
||||
0.9.0-beta
|
||||
---------------------------------
|
||||
1.0.1-beta - 24-Jan-2013
|
||||
---------------------------------
|
||||
|
||||
Bug Fix:
|
||||
Fixed an issue where unicode strings can't be printed to OS Console
|
||||
|
||||
---------------------------------
|
||||
1.0.0-beta - 23-Jan-2013
|
||||
---------------------------------
|
||||
Version 1! New multi-file processing in GUI!
|
||||
|
||||
GUI Changes:
|
||||
Open multiple files and/or folders via drag/drop or file dialog
|
||||
File management list for easy viewing and selection
|
||||
Batch tag remove
|
||||
Batch export as zip
|
||||
Batch rename
|
||||
Batch tag copy
|
||||
Batch auto-tag (automatic identification and save!)
|
||||
|
||||
---------------------------------
|
||||
0.9.5-beta - 16-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Added CLI option to search by comicvine issue ID
|
||||
Some image loading optimizations
|
||||
Bug Fix: Some CBL fields that should have been ints were written as strings
|
||||
|
||||
---------------------------------
|
||||
0.9.4-beta - 7-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Better handling of non-ascii characters in filenames and data
|
||||
Add CBL Transform to copy Web Link and Notes to comments
|
||||
Minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.3-beta - 19-Dec-2012
|
||||
---------------------------------
|
||||
Changes:
|
||||
File rename in GUI
|
||||
Setting for file rename
|
||||
Option to use series start year as volume
|
||||
Added "CBL Transform" to handle primary credits copying data into the generic tags field
|
||||
Bug Fix: unicode characters in credits caused crash
|
||||
Bug Fix: bad or non-image data in file caused crash
|
||||
|
||||
Note:
|
||||
The user should clear the cache and delete the existing settings when first running this version.
|
||||
|
||||
---------------------------------
|
||||
0.9.2-beta - 13-Dec-2012
|
||||
---------------------------------
|
||||
Page List/Type editing in GUI
|
||||
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
|
||||
Fixed RAR writing bug on windows
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.1-beta - 07-Dec-2012
|
||||
---------------------------------
|
||||
Export as ZIP Archive
|
||||
Added help menu option for websites
|
||||
Added Primary Credit Flag editing
|
||||
Menu enhancements
|
||||
CLI Enhancements:
|
||||
Interactive selection of matches
|
||||
Tag copy
|
||||
Better output
|
||||
CoMet support
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.0-beta - 30-Nov-2012
|
||||
---------------------------------
|
||||
Initial beta release
|
153
renamewindow.py
Normal file
153
renamewindow.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm rename
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from options import MetaDataStyle
|
||||
|
||||
import os
|
||||
import utils
|
||||
|
||||
class RenameWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, comic_archive_list, data_style, settings ):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
|
||||
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
|
||||
self.btnSettings.clicked.connect( self.modifySettings )
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def configRenamer( self ):
|
||||
self.renamer = FileRenamer( None )
|
||||
self.renamer.setTemplate( self.settings.rename_template )
|
||||
self.renamer.setIssueZeroPadding( self.settings.rename_issue_number_padding )
|
||||
self.renamer.setSmartCleanup( self.settings.rename_use_smart_string_cleanup )
|
||||
|
||||
def doPreview( self ):
|
||||
self.rename_list = []
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for ca in self.comic_archive_list:
|
||||
|
||||
new_ext = None # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
md = ca.readMetadata(self.data_style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
self.renamer.setMetadata( md )
|
||||
new_name = self.renamer.determineName( ca.path, ext=new_ext )
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
folder_item = QtGui.QTableWidgetItem()
|
||||
old_name_item = QtGui.QTableWidgetItem()
|
||||
new_name_item = QtGui.QTableWidgetItem()
|
||||
|
||||
item_text = os.path.split(ca.path)[0]
|
||||
folder_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, folder_item)
|
||||
folder_item.setText( item_text )
|
||||
folder_item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
|
||||
item_text = os.path.split(ca.path)[1]
|
||||
old_name_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, old_name_item)
|
||||
old_name_item.setText( item_text )
|
||||
old_name_item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
|
||||
new_name_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, new_name_item)
|
||||
new_name_item.setText( new_name )
|
||||
new_name_item.setData( QtCore.Qt.ToolTipRole, new_name )
|
||||
|
||||
dict_item = dict()
|
||||
dict_item['archive'] = ca
|
||||
dict_item['new_name'] = new_name
|
||||
self.rename_list.append( dict_item)
|
||||
|
||||
# Adjust column sizes
|
||||
self.twList.setVisible( False )
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setVisible( True )
|
||||
if self.twList.columnWidth(0) > 200:
|
||||
self.twList.setColumnWidth(0, 200)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modifySettings( self ):
|
||||
settingswin = SettingsWindow( self, self.settings )
|
||||
settingswin.setModal(True)
|
||||
settingswin.showRenameTab()
|
||||
settingswin.exec_()
|
||||
if settingswin.result():
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def accept( self ):
|
||||
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog.setWindowTitle( "Renaming Archives" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
for idx,item in enumerate(self.rename_list):
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
idx += 1
|
||||
progdialog.setLabelText( item['new_name'] )
|
||||
|
||||
if item['new_name'] == os.path.basename( item['archive'].path ):
|
||||
print item['new_name'] , "Filename is already good!"
|
||||
continue
|
||||
|
||||
if not item['archive'].isWritable(check_rar_status=False):
|
||||
continue
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( item['archive'].path ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, item['new_name'] ) )
|
||||
|
||||
os.rename( item['archive'].path, new_abs_path)
|
||||
|
||||
item['archive'].rename( new_abs_path )
|
||||
|
||||
progdialog.close()
|
||||
|
||||
QtGui.QDialog.accept(self)
|
116
renamewindow.ui
Normal file
116
renamewindow.ui
Normal file
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogRename</class>
|
||||
<widget class="QDialog" name="dialogRename">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>801</width>
|
||||
<height>360</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Archive Rename</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string> Preview:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideMiddle</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Folder</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Old Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>New Name</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="btnSettings">
|
||||
<property name="text">
|
||||
<string>Rename Settings</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogRename</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogRename</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
106
settings.py
106
settings.py
@ -51,12 +51,15 @@ class ComicTaggerSettings:
|
||||
self.allow_cbi_in_rar = True
|
||||
|
||||
# automatic settings
|
||||
self.last_selected_data_style = 0
|
||||
self.last_selected_save_data_style = 0
|
||||
self.last_selected_load_data_style = 0
|
||||
self.last_opened_folder = ""
|
||||
self.last_main_window_width = 0
|
||||
self.last_main_window_height = 0
|
||||
self.last_main_window_x = 0
|
||||
self.last_main_window_y = 0
|
||||
self.last_form_side_width = -1
|
||||
self.last_list_side_width = -1
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
@ -65,7 +68,27 @@ class ComicTaggerSettings:
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
|
||||
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
self.copy_characters_to_tags = False
|
||||
self.copy_teams_to_tags = False
|
||||
self.copy_locations_to_tags = False
|
||||
self.copy_notes_to_comments = False
|
||||
self.copy_weblink_to_comments = False
|
||||
self.apply_cbl_transform_on_cv_import = False
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
|
||||
# Rename settings
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.settings_file = ""
|
||||
@ -119,9 +142,11 @@ class ComicTaggerSettings:
|
||||
|
||||
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
|
||||
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
|
||||
|
||||
if self.config.has_option('auto', 'last_selected_data_style'):
|
||||
self.last_selected_data_style = self.config.getint( 'auto', 'last_selected_data_style' )
|
||||
|
||||
if self.config.has_option('auto', 'last_selected_load_data_style'):
|
||||
self.last_selected_load_data_style = self.config.getint( 'auto', 'last_selected_load_data_style' )
|
||||
if self.config.has_option('auto', 'last_selected_save_data_style'):
|
||||
self.last_selected_save_data_style = self.config.getint( 'auto', 'last_selected_save_data_style' )
|
||||
if self.config.has_option('auto', 'last_opened_folder'):
|
||||
self.last_opened_folder = self.config.get( 'auto', 'last_opened_folder' )
|
||||
if self.config.has_option('auto', 'last_main_window_width'):
|
||||
@ -132,17 +157,50 @@ class ComicTaggerSettings:
|
||||
self.last_main_window_x = self.config.getint( 'auto', 'last_main_window_x' )
|
||||
if self.config.has_option('auto', 'last_main_window_y'):
|
||||
self.last_main_window_y = self.config.getint( 'auto', 'last_main_window_y' )
|
||||
if self.config.has_option('auto', 'last_form_side_width'):
|
||||
self.last_form_side_width = self.config.getint( 'auto', 'last_form_side_width' )
|
||||
if self.config.has_option('auto', 'last_list_side_width'):
|
||||
self.last_list_side_width = self.config.getint( 'auto', 'last_list_side_width' )
|
||||
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint( 'identifier', 'id_length_delta_thresh' )
|
||||
if self.config.has_option('identifier', 'id_publisher_blacklist'):
|
||||
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
|
||||
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
|
||||
|
||||
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
|
||||
self.ask_about_cbi_in_rar = self.config.getboolean( 'dialogflags', 'ask_about_cbi_in_rar' )
|
||||
if self.config.has_option('dialogflags', 'show_disclaimer'):
|
||||
self.show_disclaimer = self.config.getboolean( 'dialogflags', 'show_disclaimer' )
|
||||
|
||||
|
||||
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
|
||||
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
|
||||
|
||||
if self.config.has_option('cbl_transform', 'assume_lone_credit_is_primary'):
|
||||
self.assume_lone_credit_is_primary = self.config.getboolean( 'cbl_transform', 'assume_lone_credit_is_primary' )
|
||||
if self.config.has_option('cbl_transform', 'copy_characters_to_tags'):
|
||||
self.copy_characters_to_tags = self.config.getboolean( 'cbl_transform', 'copy_characters_to_tags' )
|
||||
if self.config.has_option('cbl_transform', 'copy_teams_to_tags'):
|
||||
self.copy_teams_to_tags = self.config.getboolean( 'cbl_transform', 'copy_teams_to_tags' )
|
||||
if self.config.has_option('cbl_transform', 'copy_locations_to_tags'):
|
||||
self.copy_locations_to_tags = self.config.getboolean( 'cbl_transform', 'copy_locations_to_tags' )
|
||||
if self.config.has_option('cbl_transform', 'copy_notes_to_comments'):
|
||||
self.copy_notes_to_comments = self.config.getboolean( 'cbl_transform', 'copy_notes_to_comments' )
|
||||
if self.config.has_option('cbl_transform', 'copy_weblink_to_comments'):
|
||||
self.copy_weblink_to_comments = self.config.getboolean( 'cbl_transform', 'copy_weblink_to_comments' )
|
||||
if self.config.has_option('cbl_transform', 'apply_cbl_transform_on_cv_import'):
|
||||
self.apply_cbl_transform_on_cv_import = self.config.getboolean( 'cbl_transform', 'apply_cbl_transform_on_cv_import' )
|
||||
if self.config.has_option('cbl_transform', 'apply_cbl_transform_on_bulk_operation'):
|
||||
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean( 'cbl_transform', 'apply_cbl_transform_on_bulk_operation' )
|
||||
|
||||
if self.config.has_option('rename', 'rename_template'):
|
||||
self.rename_template = self.config.get( 'rename', 'rename_template' )
|
||||
if self.config.has_option('rename', 'rename_issue_number_padding'):
|
||||
self.rename_issue_number_padding = self.config.getint( 'rename', 'rename_issue_number_padding' )
|
||||
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
|
||||
self.rename_use_smart_string_cleanup = self.config.getboolean( 'rename', 'rename_use_smart_string_cleanup' )
|
||||
if self.config.has_option('rename', 'rename_extension_based_on_archive'):
|
||||
self.rename_extension_based_on_archive = self.config.getboolean( 'rename', 'rename_extension_based_on_archive' )
|
||||
|
||||
def save( self ):
|
||||
|
||||
if not self.config.has_section( 'settings' ):
|
||||
@ -154,12 +212,15 @@ class ComicTaggerSettings:
|
||||
if not self.config.has_section( 'auto' ):
|
||||
self.config.add_section( 'auto' )
|
||||
|
||||
self.config.set( 'auto', 'last_selected_data_style', self.last_selected_data_style )
|
||||
self.config.set( 'auto', 'last_selected_load_data_style', self.last_selected_load_data_style )
|
||||
self.config.set( 'auto', 'last_selected_save_data_style', self.last_selected_save_data_style )
|
||||
self.config.set( 'auto', 'last_opened_folder', self.last_opened_folder )
|
||||
self.config.set( 'auto', 'last_main_window_width', self.last_main_window_width )
|
||||
self.config.set( 'auto', 'last_main_window_height', self.last_main_window_height )
|
||||
self.config.set( 'auto', 'last_main_window_x', self.last_main_window_x )
|
||||
self.config.set( 'auto', 'last_main_window_y', self.last_main_window_y )
|
||||
self.config.set( 'auto', 'last_form_side_width', self.last_form_side_width )
|
||||
self.config.set( 'auto', 'last_list_side_width', self.last_list_side_width )
|
||||
|
||||
if not self.config.has_section( 'identifier' ):
|
||||
self.config.add_section( 'identifier' )
|
||||
@ -173,8 +234,31 @@ class ComicTaggerSettings:
|
||||
self.config.set( 'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar )
|
||||
self.config.set( 'dialogflags', 'show_disclaimer', self.show_disclaimer )
|
||||
|
||||
if not self.config.has_section( 'comicvine' ):
|
||||
self.config.add_section( 'comicvine' )
|
||||
|
||||
self.config.set( 'comicvine', 'use_series_start_as_volume', self.use_series_start_as_volume )
|
||||
|
||||
if not self.config.has_section( 'cbl_transform' ):
|
||||
self.config.add_section( 'cbl_transform' )
|
||||
|
||||
self.config.set( 'cbl_transform', 'assume_lone_credit_is_primary', self.assume_lone_credit_is_primary )
|
||||
self.config.set( 'cbl_transform', 'copy_characters_to_tags', self.copy_characters_to_tags )
|
||||
self.config.set( 'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags )
|
||||
self.config.set( 'cbl_transform', 'copy_locations_to_tags', self.copy_locations_to_tags )
|
||||
self.config.set( 'cbl_transform', 'copy_notes_to_comments', self.copy_notes_to_comments )
|
||||
self.config.set( 'cbl_transform', 'copy_weblink_to_comments', self.copy_weblink_to_comments )
|
||||
self.config.set( 'cbl_transform', 'apply_cbl_transform_on_cv_import', self.apply_cbl_transform_on_cv_import )
|
||||
self.config.set( 'cbl_transform', 'apply_cbl_transform_on_bulk_operation', self.apply_cbl_transform_on_bulk_operation )
|
||||
|
||||
if not self.config.has_section( 'rename' ):
|
||||
self.config.add_section( 'rename' )
|
||||
|
||||
self.config.set( 'rename', 'rename_template', self.rename_template )
|
||||
self.config.set( 'rename', 'rename_issue_number_padding', self.rename_issue_number_padding )
|
||||
self.config.set( 'rename', 'rename_use_smart_string_cleanup', self.rename_use_smart_string_cleanup )
|
||||
self.config.set( 'rename', 'rename_extension_based_on_archive', self.rename_extension_based_on_archive )
|
||||
|
||||
with open( self.settings_file, 'wb') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -96,6 +96,12 @@ class SettingsWindow(QtGui.QDialog):
|
||||
)
|
||||
self.tePublisherBlacklist.setToolTip(pblTip)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthDeltaThresh.setValidator(validator)
|
||||
|
||||
self.settingsToForm()
|
||||
|
||||
self.btnBrowseRar.clicked.connect(self.selectRar)
|
||||
@ -110,7 +116,35 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.leUnrarExePath.setText( self.settings.unrar_exe_path )
|
||||
self.leNameLengthDeltaThresh.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.tePublisherBlacklist.setPlainText( self.settings.id_publisher_blacklist )
|
||||
|
||||
if self.settings.use_series_start_as_volume:
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_characters_to_tags:
|
||||
self.cbxCopyCharactersToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_teams_to_tags:
|
||||
self.cbxCopyTeamsToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_locations_to_tags:
|
||||
self.cbxCopyLocationsToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_notes_to_comments:
|
||||
self.cbxCopyNotesToComments.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
self.cbxCopyWebLinkToComments.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
self.cbxApplyCBLTransformOnCVIMport.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
self.leRenameTemplate.setText( self.settings.rename_template )
|
||||
self.leIssueNumPadding.setText( str(self.settings.rename_issue_number_padding) )
|
||||
if self.settings.rename_use_smart_string_cleanup:
|
||||
self.cbxSmartCleanup.setCheckState( QtCore.Qt.Checked )
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
self.cbxChangeExtension.setCheckState( QtCore.Qt.Checked )
|
||||
|
||||
|
||||
def accept( self ):
|
||||
|
||||
# Copy values from form to settings and save
|
||||
@ -121,12 +155,30 @@ class SettingsWindow(QtGui.QDialog):
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
QtGui.QMessageBox.information(self,"Settings", "The Name Length Delta Threshold must be a number!")
|
||||
return
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
|
||||
self.settings.rename_template = str(self.leRenameTemplate.text())
|
||||
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
@ -170,5 +222,6 @@ class SettingsWindow(QtGui.QDialog):
|
||||
fileList = dialog.selectedFiles()
|
||||
control.setText( str(fileList[0]) )
|
||||
|
||||
|
||||
def showRenameTab( self ):
|
||||
self.tabWidget.setCurrentIndex(4)
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>640</width>
|
||||
<height>416</height>
|
||||
<width>674</width>
|
||||
<height>428</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -301,6 +301,199 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Comic Vine</string>
|
||||
</attribute>
|
||||
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>240</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Series Start Date as Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_4">
|
||||
<attribute name="title">
|
||||
<string>CBL</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_6">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxApplyCBLTransformOnCVIMport">
|
||||
<property name="text">
|
||||
<string>Apply CBL Transforms on ComicVine Import</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
|
||||
<property name="text">
|
||||
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>CBL Transforms</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>246</width>
|
||||
<height>182</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
|
||||
<property name="text">
|
||||
<string>Assume Lone Credit Is Primary</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyCharactersToTags">
|
||||
<property name="text">
|
||||
<string>Copy Characters to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyTeamsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Teams to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Locations to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyNotesToComments">
|
||||
<property name="text">
|
||||
<string>Copy Notes to Comments</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyWebLinkToComments">
|
||||
<property name="text">
|
||||
<string>Copy Web Link to Comments</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_5">
|
||||
<attribute name="title">
|
||||
<string>Rename</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0">
|
||||
<layout class="QFormLayout" name="formLayout_3">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Template:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Issue # Zero Padding</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="leIssueNumPadding">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">Issue # Zero Padding</span> dictates if the issue number should be padded on left with zeros. A value of 2, for example, means that the number will always be at least two digits.</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSmartCleanup">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Smart Text Cleanup (Experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxChangeExtension">
|
||||
<property name="text">
|
||||
<string>Change Extension Based On Archive Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
1100
taggerwindow.py
1100
taggerwindow.py
File diff suppressed because it is too large
Load Diff
2055
taggerwindow.ui
2055
taggerwindow.ui
File diff suppressed because it is too large
Load Diff
150
todo.txt
150
todo.txt
@ -1,5 +1,71 @@
|
||||
-----------------------------------------------------
|
||||
Config Mgmt
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
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?
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
spider-man 678 .... ascii print problem. grrrr
|
||||
-----------------------------------------------------
|
||||
RAR Password -- childrens crusade 3
|
||||
|
||||
-----------------------------------------------------
|
||||
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
|
||||
|
||||
-----------------------------------------------------
|
||||
Small(er) Future Feature
|
||||
-----------------------------------------------------
|
||||
Parse out the rest of the scan info from filename
|
||||
|
||||
Style sheets for windows/mac/linux
|
||||
|
||||
CLI
|
||||
explicit metadata settings option format
|
||||
-- figure out how to add CBI "tags"
|
||||
-- delete CBI "tags"
|
||||
-- set primary credit flags
|
||||
-- set frontcover and others?
|
||||
|
||||
Archive function to detect tag blocks out of sync
|
||||
|
||||
Settings
|
||||
Add setting to dis-allow writing CBI to RAR
|
||||
|
||||
Google App engine to store hashes
|
||||
Content Hashes, Image hashes, who knows?
|
||||
|
||||
Filename parsing:
|
||||
Rework how series name is separated from issue
|
||||
|
||||
Support marvel's "AU" issues...
|
||||
Mostly done, gotta wait and see what CV does
|
||||
|
||||
Internal GenericMetadata - Make Characters, Genre into lists?
|
||||
|
||||
-----------------------------------------------------
|
||||
Config Mgmt check list
|
||||
-----------------------------------------------------
|
||||
|
||||
Release Process
|
||||
@ -12,88 +78,8 @@ Release Process
|
||||
Make zip on Mac or Linux
|
||||
Tag the repository
|
||||
Upload packages
|
||||
Announce on Forum and Main Page
|
||||
|
||||
-----------------------------------------------------
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Future
|
||||
-----------------------------------------------------
|
||||
Add license info to About Dialog
|
||||
|
||||
Add CoMet Support
|
||||
|
||||
Support marvel's "AU" issues...
|
||||
|
||||
Style sheets for windows/mac/linux
|
||||
|
||||
File rename
|
||||
-Dialog??
|
||||
formatting with missing pieces.
|
||||
|
||||
TaggerWindow entry fields
|
||||
Special tabbed Dialog needed for:
|
||||
Pages Info - maybe a custom painted widget
|
||||
|
||||
CLI
|
||||
explicit metadata settings option format
|
||||
-- figure out how to add tags
|
||||
-- delete tags
|
||||
write a log for multiple file processing
|
||||
override abort on low confidence flag
|
||||
interactive for choices option?
|
||||
--- or defer choices to end, by keeping special log of match option for each file
|
||||
|
||||
Scrape alternate Covers from ComicVine issue pages
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
Overlay tags, maybe more, on online search
|
||||
|
||||
GUI to handle mutliple files or folders
|
||||
|
||||
Auto search:
|
||||
Searching w/o issue #?
|
||||
|
||||
Wizard for converting between tag styles
|
||||
|
||||
App option to covert RAR to ZIP
|
||||
|
||||
Archive function to detect tag blocks out of sync
|
||||
|
||||
app tour?
|
||||
|
||||
Settings
|
||||
Add setting to dis-allow writing CBI to RAR
|
||||
Overwrite or overlay
|
||||
|
||||
Google App engine to store hashes
|
||||
Content Hashes, Image hashes, who knows?
|
||||
|
||||
Support primary credit flag editing
|
||||
|
||||
Filename parsing:
|
||||
Rework how series name is separated from issue
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
COMIC RACK Questions
|
||||
|
||||
Missing from XML as enterable in ComicRack:
|
||||
Main Character or Team
|
||||
Review
|
||||
User Rating
|
||||
|
||||
Some that seem library only:
|
||||
"Series Complete"
|
||||
Tags
|
||||
Proposed Values
|
||||
Community Rating
|
||||
|
||||
|
@ -85,7 +85,7 @@ class IdentifyThread( QtCore.QThread):
|
||||
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, series_name, issue_number, year, comic_archive, settings, autoselect=False):
|
||||
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
|
||||
@ -97,7 +97,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.volume_id = 0
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
@ -105,9 +107,21 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def updateButtons( self ):
|
||||
if self.cv_search_results is not None and len(self.cv_search_results) > 0:
|
||||
enabled = True
|
||||
else:
|
||||
enabled = False
|
||||
|
||||
self.btnRequery.setEnabled( enabled )
|
||||
self.btnIssues.setEnabled( enabled )
|
||||
self.btnAutoSelect.setEnabled( enabled )
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled( enabled )
|
||||
|
||||
def requery( self, ):
|
||||
self.performQuery( refresh=True )
|
||||
self.twList.selectRow(0)
|
||||
@ -136,6 +150,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
self.ii.setAdditionalMetadata( md )
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
print self.cover_index_list
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
self.id_thread = IdentifyThread( self.ii )
|
||||
self.id_thread.identifyComplete.connect( self.identifyComplete )
|
||||
@ -147,7 +163,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.iddialog.exec_()
|
||||
|
||||
def logIDOutput( self, text ):
|
||||
print text,
|
||||
print unicode(text),
|
||||
self.iddialog.textEdit.ensureCursorVisible()
|
||||
self.iddialog.textEdit.insertPlainText(text)
|
||||
|
||||
@ -216,7 +232,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
title = record['name']
|
||||
title += " (" + str(record['start_year']) + ")"
|
||||
title += " (" + unicode(record['start_year']) + ")"
|
||||
title += " - "
|
||||
break
|
||||
|
||||
@ -274,8 +290,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to search for series!"))
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
self.updateButtons()
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
@ -285,8 +302,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
for record in self.cv_search_results:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem( item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
@ -316,7 +333,11 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
if self.immediate_autoselect:
|
||||
if len( self.cv_search_results ) == 0:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtGui.QMessageBox.information(self,"Search Result", "No matches found!")
|
||||
|
||||
if self.immediate_autoselect and len( self.cv_search_results ) > 0:
|
||||
# defer the immediate autoselect so this dialog has time to pop up
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
|
||||
|
Reference in New Issue
Block a user