Compare commits

..

26 Commits

Author SHA1 Message Date
79a9cf1b40 Release 1.0.1
Fixed stupid bug where unicode can't be printed to OS X console

git-svn-id: http://comictagger.googlecode.com/svn/trunk@341 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-24 04:22:37 +00:00
6e7d7bcc47 Fixed an issue with export
git-svn-id: http://comictagger.googlecode.com/svn/trunk@334 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-24 01:33:21 +00:00
d96690c351 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@333 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 22:15:00 +00:00
c44c240eef UI tweaks
New icons
bumped version number

git-svn-id: http://comictagger.googlecode.com/svn/trunk@332 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 22:05:38 +00:00
ddc225c2be New auto-tag icon
git-svn-id: http://comictagger.googlecode.com/svn/trunk@331 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 19:43:05 +00:00
d9cdb14aa6 Form re-worked
Preserve splitter location in settings

git-svn-id: http://comictagger.googlecode.com/svn/trunk@330 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 18:55:10 +00:00
cab525675d Fixed a rename window bug where window was going away too soon
git-svn-id: http://comictagger.googlecode.com/svn/trunk@329 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 18:54:44 +00:00
e9321b741e Added more cursor feedback when saving
git-svn-id: http://comictagger.googlecode.com/svn/trunk@328 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 06:00:59 +00:00
4143ca3314 Added a dialog for manually matching after auto-tag
git-svn-id: http://comictagger.googlecode.com/svn/trunk@327 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 05:25:50 +00:00
667c21bbed More exception handling for corrupt archives
git-svn-id: http://comictagger.googlecode.com/svn/trunk@326 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 03:04:55 +00:00
37048b99fc Assorted fixes and enhancements
git-svn-id: http://comictagger.googlecode.com/svn/trunk@325 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 01:25:17 +00:00
e839b008c6 Handle exception when resizing corrupt image data
git-svn-id: http://comictagger.googlecode.com/svn/trunk@324 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 01:24:33 +00:00
ba3673a4c0 Better exception handling
Fixed a horrible memory leak

git-svn-id: http://comictagger.googlecode.com/svn/trunk@323 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 01:23:57 +00:00
221923607a Auto-Tag progress window added
More auto-tag and other stuff

git-svn-id: http://comictagger.googlecode.com/svn/trunk@322 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-22 04:09:08 +00:00
b712226b1e Export confirm window
AutoTag confirm window
Other fixes and such

git-svn-id: http://comictagger.googlecode.com/svn/trunk@321 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-22 00:19:59 +00:00
b8e8c6433a Options dialog for export to zip
Fixed progress dialogs

git-svn-id: http://comictagger.googlecode.com/svn/trunk@320 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-21 06:39:44 +00:00
be3b0fe92c Tweaked the comictagger note text
Fixed a transformer bug

git-svn-id: http://comictagger.googlecode.com/svn/trunk@319 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-21 01:45:51 +00:00
d26441306a Added setting for rename to use the archive type
git-svn-id: http://comictagger.googlecode.com/svn/trunk@318 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-21 01:07:45 +00:00
f2b1db5479 Added batch tag copy
Other fixed for batch stuff

git-svn-id: http://comictagger.googlecode.com/svn/trunk@317 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-20 20:43:48 +00:00
0cd10f3f75 Handle case of non-exisiting issue string
git-svn-id: http://comictagger.googlecode.com/svn/trunk@316 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-20 20:42:06 +00:00
97dc36b8fb Implemented batch export to zip
more multi-file/batch enhancements

git-svn-id: http://comictagger.googlecode.com/svn/trunk@315 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-20 08:36:21 +00:00
d58e033689 Implemented batch tag remove
more multi-file/batch enhancements

git-svn-id: http://comictagger.googlecode.com/svn/trunk@314 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-19 06:15:33 +00:00
c3d5d44788 Implemented batch rename in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@312 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-19 03:12:25 +00:00
2bf9b9ed7c Completed initial multi-file management, before implementing batch features
git-svn-id: http://comictagger.googlecode.com/svn/trunk@311 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-18 21:12:23 +00:00
cfca394bcb More work on managing mutiple files in the GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@310 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-18 00:52:42 +00:00
7a7adc1c3f Implemented context menu for file list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@309 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-17 06:19:06 +00:00
34 changed files with 2937 additions and 1055 deletions

202
autotagmatchwindow.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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 &quot;1&quot;</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>

View File

@ -76,12 +76,22 @@ class CBLTransformer:
add_string_list_to_tags( self.metadata.locations )
if self.settings.copy_notes_to_comments:
if self.metadata.notes is not None and self.metadata.notes not in self.metadata.comments:
self.metadata.comments += "\n\n" + self.metadata.notes
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 and self.metadata.webLink not in self.metadata.comments:
self.metadata.comments += "\n\n" + self.metadata.webLink
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

View File

@ -63,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 ):
@ -194,7 +201,8 @@ class ZipArchiver:
zout = zipfile.ZipFile (self.path, 'w')
for fname in otherArchive.getArchiveFilenameList():
data = otherArchive.readArchiveFile( fname )
zout.writestr( fname, data )
if data is not None:
zout.writestr( fname, data )
zout.close()
#preserve the old comment
@ -202,7 +210,8 @@ class ZipArchiver:
if comment is not None:
if not self.writeZipComment( self.path, comment ):
return False
except:
except Exception as e:
print "Error while copying to {0}: {1}".format(self.path, e)
return False
else:
return True
@ -213,10 +222,13 @@ class ZipArchiver:
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":
@ -226,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 ):
@ -248,7 +261,7 @@ class RarArchiver:
# use external program to write comment to Rar archive
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)
@ -263,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:
@ -290,7 +327,7 @@ class RarArchiver:
# use external program to write file to Rar archive
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)
@ -309,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)
@ -322,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:
@ -440,9 +511,17 @@ class ComicArchive:
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():
@ -469,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):
@ -527,7 +606,6 @@ class ComicArchive:
retcode = self.writeCBI( metadata )
elif style == MetaDataStyle.COMET:
retcode = self.writeCoMet( metadata )
self.resetCache()
return retcode
@ -550,7 +628,6 @@ class ComicArchive:
retcode = self.removeCBI()
elif style == MetaDataStyle.COMET:
retcode = self.removeCoMet()
self.resetCache()
return retcode
def getPage( self, index ):
@ -599,15 +676,16 @@ class ComicArchive:
return self.page_count
def readCBI( self ):
raw_cbi = self.readRawCBI()
if raw_cbi is None:
md = GenericMetadata()
else:
md = ComicBookInfo().metadataFromString( raw_cbi )
md.setDefaultPageList( self.getNumberOfPages() )
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 md
return self.cbi_md
def readRawCBI( self ):
if ( not self.hasCBI() ):
@ -628,30 +706,49 @@ class ComicArchive:
return self.has_cbi
def writeCBI( self, metadata ):
self.applyArchiveInfoToMetadata( 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:
md = GenericMetadata()
else:
md = ComicInfoXml().metadataFromString( raw_cix )
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 ( md.pages ) != 0 :
if len ( md.pages ) != self.getNumberOfPages():
# pages array doesn't match the actual number of images we're seeing
# in the archive, so discard the data
md.pages = []
#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() )
if len( md.pages ) == 0:
md.setDefaultPageList( self.getNumberOfPages() )
return md
return self.cix_md
def readRawCIX( self ):
if not self.hasCIX():
@ -664,13 +761,27 @@ class ComicArchive:
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 self.has_cix is None:
@ -685,28 +796,28 @@ class ComicArchive:
def readCoMet( self ):
raw_comet = self.readRawCoMet()
if raw_comet is None:
md = GenericMetadata()
else:
md = CoMet().metadataFromString( raw_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 md.coverImage is not None:
cover_idx = 0
for idx,f in enumerate(self.getPageNameList()):
if md.coverImage == f:
cover_idx = idx
break
if cover_idx != 0:
del (md.pages[0]['Type'] )
md.pages[ cover_idx ]['Type'] = PageType.FrontCover
return md
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():
@ -728,24 +839,34 @@ class ComicArchive:
metadata.coverImage = self.getPageName( cover_idx )
comet_string = CoMet().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.comet_filename, comet_string )
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():
retcode = self.archiver.removeArchiveFile( self.comet_filename )
self.comet_filename = None
return retcode
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 not self.seemsToBeAComicArchive():
return False
#Use the existence of self.comet_filename as a cue that the tag block exists
if self.comet_filename is None:
#TODO look at all xml files in root, and search for CoMet data, get first
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'):
@ -754,12 +875,12 @@ class ComicArchive:
if CoMet().validateString( data ):
# since we found it, save it!
self.comet_filename = n
return True
# if we made it through the loop, no CoMet here...
return False
else:
return True
self.has_comet = True
break
return self.has_comet
def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False):
md.pageCount = self.getNumberOfPages()
@ -770,13 +891,17 @@ class ComicArchive:
if pil_available:
if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p:
data = self.getPage( idx )
im = Image.open(StringIO.StringIO(data))
w,h = im.size
p['ImageSize'] = str(len(data))
p['ImageHeight'] = str(h)
p['ImageWidth'] = str(w)
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 )

View File

@ -294,7 +294,7 @@ def process_file_cli( filename, opts, settings, match_results ):
if not opts.dryrun:
md = ca.readMetadata( opts.copy_source )
if settings.apply_cbl_transform_on_bulk_operation:
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 ):
@ -419,12 +419,12 @@ def process_file_cli( filename, opts, settings, match_results ):
print msg_hdr + "Can't rename without series name"
return
if ca.isZip():
new_ext = ".cbz"
elif ca.isRar():
new_ext = ".cbr"
else:
new_ext = None # default
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 )
@ -486,11 +486,8 @@ def main():
splash.raise_()
app.processEvents()
try:
fname = None
if opts.filename is not None:
fname = opts.filename.decode(filename_encoding, 'replace')
tagger_window = TaggerWindow( fname, settings )
try:
tagger_window = TaggerWindow( opts.file_list, settings )
tagger_window.show()
if platform.system() != "Linux":

View File

@ -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
@ -183,7 +185,9 @@ class ComicVineTalker(QObject):
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
@ -241,7 +245,10 @@ class ComicVineTalker(QObject):
if settings.use_series_start_as_volume:
metadata.volume = volume_results['start_year']
metadata.notes = "Tagged with ComicTagger app using info from Comic Vine."
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']

View File

@ -1,3 +1,3 @@
# This file should contan only these comments, and the line below.
# Used by packaging makefiles and app
version="0.9.5-beta"
version="1.0.1-beta"

62
exportwindow.py Normal file
View 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
View 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>

View File

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

View File

@ -106,6 +106,10 @@ class FileRenamer:
new_name += ext
# some tweaks to keep various filesystems happy
new_name = new_name.replace("/", "-")
new_name = new_name.replace(":", "-")
return new_name

View File

@ -1,6 +1,6 @@
# coding=utf-8
"""
A PyQt4 widget for managing list of files
A PyQt4 widget for managing list of comic archive files
"""
"""
@ -49,15 +49,22 @@ class FileTableWidgetItem(QTableWidgetItem):
class FileInfo( ):
def __init__(self, path, ca, cix_md, cbi_md ):
self.path = path
self.cix_md = cix_md
self.cbi_md = cbi_md
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)
@ -68,27 +75,88 @@ class FileSelectionList(QWidget):
#self.twList = FileTableWidget( self )
#gridlayout = QGridLayout( self )
#gridlayout.addWidget( self.twList )
self.setAcceptDrops(True)
self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
#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)
def dragEnterEvent(self, event):
self.droppedFiles = None
if event.mimeData().hasUrls():
# walk through the URL list and build a file list
for url in event.mimeData().urls():
if url.isValid() and url.scheme() == "file":
if self.droppedFiles is None:
self.droppedFiles = []
self.droppedFiles.append(url.toLocalFile())
if self.droppedFiles is not None:
event.accept()
self.addAction(selectAllAction)
self.addAction(removeAction)
self.addAction(self.separator)
def dropEvent(self, event):
self.addPathList( self.droppedFiles)
event.accept()
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 = []
@ -106,27 +174,43 @@ class FileSelectionList(QWidget):
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)
self.addPathItem( f )
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)
#Maybe set a max size??
# 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():
fi = self.twList.item(r, 0).data( Qt.UserRole ).toPyObject()
if fi.path == path:
ca = self.getArchiveByRow( r )
if ca.path == path:
return True
r = r + 1
@ -134,10 +218,11 @@ class FileSelectionList(QWidget):
def addPathItem( self, path):
path = unicode( path )
path = os.path.abspath( path )
#print "processing", path
if self.isListDupe(path):
return
return None
ca = ComicArchive( path )
if self.settings.rar_exe_path != "":
@ -148,70 +233,165 @@ class FileSelectionList(QWidget):
row = self.twList.rowCount()
self.twList.insertRow( row )
cix_md = None
cbi_md = None
fi = FileInfo( ca )
has_cix = ca.hasCIX()
if has_cix:
cix_md = ca.readCIX()
has_cbi = ca.hasCBI()
if has_cbi:
cbi_md = ca.readCBI()
fi = FileInfo( path, ca, cix_md, cbi_md )
item_text = os.path.split(path)[1]
item = QTableWidgetItem(item_text)
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setData( Qt.UserRole , fi )
item.setData( Qt.ToolTipRole ,item_text)
self.twList.setItem(row, 0, item)
item_text = os.path.split(path)[0]
item = QTableWidgetItem(item_text)
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setData( Qt.ToolTipRole ,item_text)
self.twList.setItem(row, 1, item)
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)
# 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 )
type_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
item = FileTableWidgetItem()
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setTextAlignment(Qt.AlignHCenter)
if has_cix:
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, True)
else:
item.setData(Qt.UserRole, False)
self.twList.setItem(row, 2, item)
cix_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
cix_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
item = FileTableWidgetItem()
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setTextAlignment(Qt.AlignHCenter)
if has_cbi:
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, True)
else:
item.setData(Qt.UserRole, False)
self.twList.setItem(row, 3, 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)
def itemSelectionChangedCB( self ):
idx = self.twList.currentRow()
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)
fi = self.twList.item(idx, 0).data( Qt.UserRole ).toPyObject()
#if fi.cix_md is not None:
# print u"{0}".format(fi.cix_md)
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 )

View File

@ -19,9 +19,15 @@
<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>
@ -35,10 +41,11 @@
<property name="text">
<string>File</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
<property name="toolTip">
<string>File Name</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
@ -46,7 +53,7 @@
<string>CR</string>
</property>
<property name="toolTip">
<string/>
<string>Has ComicRack Tags</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
@ -56,6 +63,42 @@
<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>

BIN
graphics/autotag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
graphics/longbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,5 +1,6 @@
import StringIO
import sys
try:
import Image
@ -29,7 +30,13 @@ class ImageHasher(object):
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)

View File

@ -72,6 +72,7 @@ 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
@ -97,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()
@ -130,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 ):
@ -198,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 ):
@ -252,7 +256,7 @@ 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( )
@ -306,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)
@ -358,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)
@ -387,7 +393,6 @@ class IssueIdentifier:
break
self.log_msg( "" )
if len(self.match_list) == 0:
self.log_msg( ":-( no matches!" )
@ -419,7 +424,7 @@ 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
@ -428,11 +433,11 @@ class IssueIdentifier:
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( "" )
@ -440,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:
@ -455,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:
@ -465,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

View File

@ -33,6 +33,12 @@ 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 )

View File

@ -82,8 +82,7 @@ class PageListEditor(QWidget):
self.pages_list = None
self.page_loader = None
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
self.setDisplayPixmap( 0, 0)
self.resetPage()
# Add the entries to the manga combobox
self.comboBox.addItem( "", "" )
@ -107,6 +106,10 @@ class PageListEditor(QWidget):
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:
@ -238,7 +241,9 @@ class PageListEditor(QWidget):
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 ) )
@ -246,8 +251,9 @@ class PageListEditor(QWidget):
item.setData(Qt.UserRole, (p, ))
self.listWidget.addItem( item )
self.listWidget.setCurrentRow ( 0 )
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']

View File

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

View File

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

View File

@ -1,4 +1,25 @@
---------------------------------
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
---------------------------------

View File

@ -23,35 +23,91 @@ 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, metadata, settings ):
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.metadata = metadata
self.comic_archive = comic_archive
self.new_name = None
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( self.metadata )
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.new_name = self.renamer.determineName( self.comic_archive.path )
preview = u"\"{0}\" ==> \"{1}\"".format( self.comic_archive.path, self.new_name )
self.textEdit.setPlainText( preview )
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 )
@ -63,17 +119,35 @@ class RenameWindow(QtGui.QDialog):
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)
if self.new_name == os.path.basename( self.comic_archive.path ):
#print msg_hdr + "Filename is already good!"
return
folder = os.path.dirname( os.path.abspath( self.comic_archive.path ) )
new_abs_path = utils.unique_file( os.path.join( folder, self.new_name ) )
os.rename( self.comic_archive.path, new_abs_path )
self.new_name = new_abs_path

View File

@ -9,8 +9,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>556</width>
<height>210</height>
<width>801</width>
<height>360</height>
</rect>
</property>
<property name="windowTitle">
@ -30,10 +30,31 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QTextEdit" name="textEdit">
<property name="readOnly">
<bool>true</bool>
<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>
@ -56,17 +77,6 @@
</widget>
</item>
</layout>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>140</x>
<y>10</y>
<width>133</width>
<height>29</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout"/>
</widget>
</widget>
<resources/>
<connections>

View File

@ -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
@ -84,7 +87,7 @@ class ComicTaggerSettings:
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):
@ -139,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'):
@ -152,6 +157,10 @@ 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' )
@ -189,8 +198,9 @@ class ComicTaggerSettings:
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' ):
@ -202,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' )
@ -244,6 +257,7 @@ class ComicTaggerSettings:
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)

View File

@ -141,6 +141,9 @@ class SettingsWindow(QtGui.QDialog):
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 ):
@ -174,6 +177,7 @@ class SettingsWindow(QtGui.QDialog):
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)

View File

@ -334,7 +334,7 @@
<item row="1" column="0">
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
<property name="text">
<string>Apply CBL Transforms on Batch/CLI Operations</string>
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
</property>
</widget>
</item>
@ -354,7 +354,7 @@
<rect>
<x>11</x>
<y>21</y>
<width>242</width>
<width>246</width>
<height>182</height>
</rect>
</property>
@ -483,6 +483,13 @@
</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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,39 @@
-----------------------------------------------------
Features
-----------------------------------------------------
Multi-file:
Does the main UI need to have "View/Read Tag Style" and "Write Tag style" concept?
Edit functions on list: select, select all, delete,
Batch Functions:
Auto-Select
Start/Options Dialog
Progress Dialog - maybe reuse
Interactive dialog at end
Rename
Start dialog with preview
maybe table with checkboxes?
Auto-Tag
Interactive dialog at end
Manually change cover image on left??
Copy Block
Verify overwrites
Turn off drop accept for edit lines/boxes
Drop on app goes to list and selects it
Accept multiple files on file open dialog
Warn on moving selection list away from modified form
Rename
check-box for rows?
manual edit the preview?
ComicArchive: cache each metadata block? Need to make sure cache is cleared on file modify
-----------------------------------------------------
Bugs
spider-man 678 .... ascii print problem. grrrr
-----------------------------------------------------
RAR Password -- childrens crusade 3
-----------------------------------------------------
Big Future Features
-----------------------------------------------------
GUI to handle mutliple files or folders
Scrape alternate Covers from ComicVine issue pages
GCD scraper or DB reader
pyComicMetaThis CBI features
Auto search:
Searching w/o issue #
Form Mode: Single vs Batch
-----------------------------------------------------
Small(er) Future Feature
-----------------------------------------------------
@ -61,7 +52,6 @@ Archive function to detect tag blocks out of sync
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?
@ -92,17 +82,4 @@ Release Process
----------------------------------------------
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