Compare commits

..

58 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
41f730a558 Version update for 0.9.5
git-svn-id: http://comictagger.googlecode.com/svn/trunk@305 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-17 00:05:09 +00:00
550b84361c Create a list of story arcs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@304 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:59:09 +00:00
fb4248fda2 Fixed some typos
git-svn-id: http://comictagger.googlecode.com/svn/trunk@303 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:58:54 +00:00
9626c3fd77 Use the CT version in JSON
Make sure certain fields are ints

git-svn-id: http://comictagger.googlecode.com/svn/trunk@302 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:29:32 +00:00
3f305c6788 Made sure to reset the cache on a tag block delete
git-svn-id: http://comictagger.googlecode.com/svn/trunk@301 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:20:59 +00:00
9e68516dac Work on multi-file processing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@300 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 22:46:22 +00:00
8f45994b9a Added a CLI option for searching by CV issue ID, that can be used when being called by Mylar
git-svn-id: http://comictagger.googlecode.com/svn/trunk@299 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-12 01:48:34 +00:00
4ea56c0bd0 Updated version and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@296 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 21:20:36 +00:00
5445417404 Tweaked setting window UI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@295 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 21:20:20 +00:00
db6423aea9 Tweaked CBL tranform to save notes and weblink to comments
git-svn-id: http://comictagger.googlecode.com/svn/trunk@293 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 20:15:17 +00:00
aa62a3e8ff gracefully handle no search results
git-svn-id: http://comictagger.googlecode.com/svn/trunk@292 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-31 00:43:22 +00:00
cd1733a975 Added a cache version file to manage clearing old one on upgrade
git-svn-id: http://comictagger.googlecode.com/svn/trunk@291 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 23:32:37 +00:00
c81319402d A few more unicode fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@290 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 05:31:00 +00:00
8a8e53d9c9 A lot of unicode related fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@289 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 05:06:12 +00:00
7614e95084 Handle case of no numeric portion of issue number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@288 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-22 01:19:14 +00:00
bd9f314496 Some tweaks to issue number finder
git-svn-id: http://comictagger.googlecode.com/svn/trunk@287 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-22 01:18:18 +00:00
bebd09d3f6 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@284 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 06:00:31 +00:00
8a5430c83e updated todo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@278 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:07:14 +00:00
93be1b42f4 Neatened up the new settings tabs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@277 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:06:44 +00:00
01be389fad New version
git-svn-id: http://comictagger.googlecode.com/svn/trunk@276 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:06:15 +00:00
ca9aaf9279 Don't always show full help
git-svn-id: http://comictagger.googlecode.com/svn/trunk@275 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:05:51 +00:00
ee9175087e Implemented file-renaming in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@273 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 01:37:55 +00:00
94c5882175 Fixed printing of primary flag on CLI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@272 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-18 23:21:56 +00:00
ff74b3e5bc Added menu options to rename and apply CBL transform
git-svn-id: http://comictagger.googlecode.com/svn/trunk@271 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-18 23:14:00 +00:00
0017903a4f Got CBL transformer working
git-svn-id: http://comictagger.googlecode.com/svn/trunk@270 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 21:19:21 +00:00
3d98118fa9 Added option set CV series start year as volume
git-svn-id: http://comictagger.googlecode.com/svn/trunk@269 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 18:44:33 +00:00
faf0b5d437 New settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@268 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 18:19:32 +00:00
e14c9dfe19 fixed encoding error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@267 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:23:48 +00:00
4343f3f08d Gracefully deal with bad image data
git-svn-id: http://comictagger.googlecode.com/svn/trunk@266 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:09:26 +00:00
4a94bf4d6f Ignore image files that begin with ".". They're probably cruft.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@265 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:08:59 +00:00
a602c42f0e new file renamer class
git-svn-id: http://comictagger.googlecode.com/svn/trunk@263 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-15 05:54:12 +00:00
1efdc0e623 Set ctrl+A for menu auto-select
git-svn-id: http://comictagger.googlecode.com/svn/trunk@262 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-14 23:18:44 +00:00
41 changed files with 4842 additions and 1641 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>

99
cbltransformer.py Normal file
View 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

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:
@ -419,8 +490,8 @@ class ComicArchive:
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
self.comet_default_filename = 'CoMet.xml'
self.comet_filename = None
self.resetCache()
if self.zipTest():
self.archive_type = self.ArchiveType.Zip
self.archiver = ZipArchiver( self.path )
@ -436,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
@ -461,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):
@ -512,12 +599,15 @@ class ComicArchive:
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:
return self.writeCoMet( metadata )
retcode = self.writeCoMet( metadata )
return retcode
def hasMetadata( self, style ):
@ -531,13 +621,15 @@ class ComicArchive:
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:
return self.removeCoMet()
retcode = self.removeCoMet()
return retcode
def getPage( self, index ):
image_data = None
@ -561,35 +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:
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() ):
@ -598,42 +694,64 @@ class ComicArchive:
return self.archiver.getArchiveComment()
def hasCBI(self):
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
return False
if self.has_cbi is None:
comment = self.archiver.getArchiveComment()
return ComicBookInfo().validateString( comment )
#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 ):
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():
print self.path, "doesn't have ComicInfo.xml data!"
return None
return self.archiver.readArchiveFile( self.ci_xml_filename )
@ -643,46 +761,63 @@ 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 not self.seemsToBeAComicArchive():
return False
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
return True
else:
return False
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 ):
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():
@ -704,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'):
@ -730,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()
@ -746,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

@ -25,6 +25,7 @@ import zipfile
from genericmetadata import GenericMetadata
import utils
import ctversion
class ComicBookInfo:
@ -102,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 }
@ -110,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 )

View File

@ -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,7 +46,9 @@ from comicarchive import ComicArchive
from issueidentifier import IssueIdentifier
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from issuestring import IssueString
from filerenamer import FileRenamer
from cbltransformer import CBLTransformer
import utils
import codecs
@ -65,10 +70,14 @@ 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.assume_lone_credit_is_primary )
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 ):
@ -120,7 +129,7 @@ def post_process_matches( match_results, opts, settings ):
for mm in match_results.multipleMatches:
print mm.filename
for (counter,m) in enumerate(mm.matches):
print " {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
m['series'],
m['issue_number'],
m['publisher'],
@ -153,6 +162,7 @@ def cli_mode( 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()
@ -283,88 +293,106 @@ def process_file_cli( filename, opts, settings, match_results ):
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 "{0}: Tag copy seemed to fail!".format( filename )
print u"{0}: Tag copy seemed to fail!".format( filename )
else:
print "{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
else:
print "{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
else:
print "{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
elif opts.save_tags:
if opts.no_overwrite and has[ opts.data_style ]:
print "{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
return
if batch_mode:
print "Processing {0}: ".format(filename)
print u"Processing {0}: ".format(filename)
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
# 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 )
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
if cv_md is None:
print "No match for ID {0} was found.".format(opts.issue_id)
return None
if settings.apply_cbl_transform_on_cv_import:
cv_md = CBLTransformer( cv_md, settings ).apply()
else:
ii = IssueIdentifier( ca, settings )
if md is None or md.isEmpty:
print "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 )
@ -378,7 +406,7 @@ def process_file_cli( filename, opts, settings, match_results ):
msg_hdr = ""
if batch_mode:
msg_hdr = "{0}: ".format(filename)
msg_hdr = u"{0}: ".format(filename)
if opts.data_style is not None:
use_tags = has[ opts.data_style ]
@ -386,34 +414,24 @@ def process_file_cli( filename, opts, settings, match_results ):
use_tags = False
md = create_local_metadata( opts, ca, use_tags )
# TODO move this to ComicArchive, or maybe another class???
new_name = ""
if md.series is not None:
new_name += "{0}".format( md.series )
else:
if md.series is None:
print msg_hdr + "Can't rename without series name"
return
if md.volume is not None:
new_name += " v{0}".format( md.volume )
if md.issue is not None:
new_name += " #{0}".format( IssueString(md.issue).asString(pad=3) )
#else:
# print msg_hdr + "Can't rename without issue number"
# 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 msg_hdr + "Filename is already good!"
@ -429,7 +447,7 @@ def process_file_cli( filename, opts, settings, match_results ):
else:
suffix = " (dry-run, no change)"
print "renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
@ -439,7 +457,7 @@ def process_file_cli( filename, opts, settings, match_results ):
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()
@ -468,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":
@ -481,8 +499,7 @@ def main():
if __name__ == "__main__":
main()
main()

View File

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

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
@ -45,6 +47,7 @@ from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from issuestring import IssueString
class ComicVineTalkerException(Exception):
pass
@ -92,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)
@ -176,12 +180,14 @@ class ComicVineTalker(QObject):
return volume_results
def fetchIssueData( self, series_id, issue_number, assumeLoneCreditIsPrimary = False ):
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
@ -199,6 +205,29 @@ 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()
@ -213,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']
@ -224,32 +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 )
if assumeLoneCreditIsPrimary:
def setLonePrimary( role ):
lone_credit = None
count = 0
for c in metadata.credits:
if c['role'].lower() == role:
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' )
if c is not None:
c['primary'] = False
metadata.addCredit( c['person'], 'Artist', True )
metadata.addCredit( person['name'], role['role'].title(), False )
character_credits = issue_results['character_credits']
character_list = list()
@ -270,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):

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.2-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

@ -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
@ -99,7 +102,9 @@ class FileNameParser:
# 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))
@ -135,7 +140,7 @@ 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:
@ -171,7 +176,7 @@ class FileNameParser:
return year
def parseFilename( self, filename ):
# remove the path
filename = os.path.basename(filename)
@ -207,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
View 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
View 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
View 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>

View File

@ -132,7 +132,7 @@ 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 )
@ -142,8 +142,8 @@ class GenericMetadata:
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 )
@ -295,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

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

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()
@ -107,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))
@ -128,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 ):
@ -196,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 ):
@ -250,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:
@ -275,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
@ -304,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)
@ -317,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 )
@ -356,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)
@ -365,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
@ -385,7 +393,6 @@ class IssueIdentifier:
break
self.log_msg( "" )
if len(self.match_list) == 0:
self.log_msg( ":-( no matches!" )
@ -417,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
@ -426,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( "" )
@ -438,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:
@ -453,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:
@ -463,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

@ -95,7 +95,7 @@ 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)

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 )
@ -56,6 +62,9 @@ class IssueString:
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)
@ -85,6 +94,8 @@ class IssueString:
def asInt( self ):
#return the int version of the float
if self.num is None:
return None
return int( self.num )

View File

@ -69,6 +69,7 @@ If no options are given, {0} will run in windowed mode
-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"
@ -105,13 +106,17 @@ If no options are given, {0} will run in windowed mode
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 ):
@ -177,15 +182,15 @@ If no options are given, {0} will run in windowed mode
"hpdt:fm:vonsrc:i",
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
"interactive", "nosummary", "version" ])
"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"):
@ -203,7 +208,7 @@ If no options are given, {0} will run in windowed mode
elif a.lower() == "comet":
self.copy_source = MetaDataStyle.COMET
else:
self.display_help_and_quit( "Invalid copy tag source type", 1 )
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"):
@ -216,6 +221,8 @@ 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 == "--id":
self.issue_id = a
if o == "--raw":
self.raw = True
if o == "--noabort":
@ -237,7 +244,7 @@ If no options are given, {0} will run in windowed mode
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.copy_tags or self.rename_file:
self.no_gui = True
@ -250,7 +257,7 @@ If no options are given, {0} will run in windowed mode
if self.rename_file: count += 1
if count > 1:
self.display_help_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
if len(args) > 0:
if platform.system() == "Windows":
@ -265,17 +272,17 @@ If no options are given, {0} will run in windowed mode
self.file_list = args
if self.no_gui and self.filename is None:
self.display_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_help_and_quit( "Please specify the type to copy to with -t", 1 )
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 )
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )

View File

@ -27,6 +27,7 @@ from PyQt4 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
from pageloader import PageLoader
def itemMoveEvents( widget ):
@ -79,9 +80,9 @@ class PageListEditor(QWidget):
self.comic_archive = None
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( "", "" )
@ -105,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:
@ -151,14 +156,19 @@ class PageListEditor(QWidget):
#idx = int(str (self.listWidget.item( row ).text()))
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
image_data = self.comic_archive.getPage( idx )
if image_data is not None:
img = QImage()
img.loadFromData( image_data )
self.current_pixmap = QPixmap(QPixmap(img))
self.setDisplayPixmap( 0, 0)
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
@ -231,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 ) )
@ -239,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']

77
pageloader.py Normal file
View 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 )

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,3 +1,55 @@
---------------------------------
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
---------------------------------

153
renamewindow.py Normal file
View 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
View 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>

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
@ -67,8 +70,25 @@ class ComicTaggerSettings:
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 = ""
@ -122,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'):
@ -135,21 +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', 'assume_lone_credit_is_primary'):
self.assume_lone_credit_is_primary = self.config.getboolean( 'comicvine', 'assume_lone_credit_is_primary' )
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' ):
@ -161,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' )
@ -183,11 +237,28 @@ class ComicTaggerSettings:
if not self.config.has_section( 'comicvine' ):
self.config.add_section( 'comicvine' )
self.config.set( 'comicvine', 'assume_lone_credit_is_primary', self.assume_lone_credit_is_primary )
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)

View File

@ -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,10 +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
@ -124,12 +155,29 @@ 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)
@ -174,5 +222,6 @@ class SettingsWindow(QtGui.QDialog):
fileList = dialog.selectedFiles()
control.setText( str(fileList[0]) )
def showRenameTab( self ):
self.tabWidget.setCurrentIndex(4)

View File

@ -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">
@ -305,20 +305,195 @@
<attribute name="title">
<string>Comic Vine</string>
</attribute>
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
<property name="geometry">
<rect>
<x>50</x>
<x>30</x>
<y>30</y>
<width>241</width>
<height>20</height>
<width>240</width>
<height>25</height>
</rect>
</property>
<property name="text">
<string>Assume Lone Credit Is Primary</string>
<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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Issue # Zero Padding&lt;/span&gt; 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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="cbxSmartCleanup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +1,57 @@
-----------------------------------------------------
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?
File rename
renamer class
-Dialog
formatting with missing pieces. "Smart Clean-up"
-----------------------------------------------------
Bugs
spider-man 678 .... ascii print problem. grrrr
-----------------------------------------------------
Auto-select failure when year is off by one. Maybe check with a wider radius??
RAR Password -- childrens crusade 3
-----------------------------------------------------
Big Future Features
-----------------------------------------------------
GUI to handle mutliple files or folders
Scrape alternate Covers from ComicVine issue pages
Auto search:
Searching w/o issue #?
GCD scraper or DB reader
pyComicMetaThis CBI features
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
Setting/Option to copy Characters and others to CBL "Tags"
CLI "--cbl_opts"
GUI settings, or buttons??
Maybe CV setting
CLI
explicit metadata settings option format
-- figure out how to add CBI "tags"
-- delete CBI "tags"
-- set primary credit flags
-- set frontcover and others?
write a log for multiple file processing
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?
@ -61,7 +62,7 @@ Filename parsing:
Support marvel's "AU" issues...
Mostly done, gotta wait and see what CV does
Internal GenericMetadata - Make Characters, Genre lists?
Internal GenericMetadata - Make Characters, Genre into lists?
-----------------------------------------------------
Config Mgmt check list
@ -77,20 +78,8 @@ Release Process
Make zip on Mac or Linux
Tag the repository
Upload packages
Announce on Forum and Main Page
----------------------------------------------
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

View File

@ -98,6 +98,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
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)
@ -106,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)
@ -150,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)
@ -219,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
@ -277,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:
@ -288,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)
@ -319,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)