Compare commits
33 Commits
0.9.4-beta
...
1.0.1-beta
Author | SHA1 | Date | |
---|---|---|---|
79a9cf1b40 | |||
6e7d7bcc47 | |||
d96690c351 | |||
c44c240eef | |||
ddc225c2be | |||
d9cdb14aa6 | |||
cab525675d | |||
e9321b741e | |||
4143ca3314 | |||
667c21bbed | |||
37048b99fc | |||
e839b008c6 | |||
ba3673a4c0 | |||
221923607a | |||
b712226b1e | |||
b8e8c6433a | |||
be3b0fe92c | |||
d26441306a | |||
f2b1db5479 | |||
0cd10f3f75 | |||
97dc36b8fb | |||
d58e033689 | |||
c3d5d44788 | |||
2bf9b9ed7c | |||
cfca394bcb | |||
7a7adc1c3f | |||
41f730a558 | |||
550b84361c | |||
fb4248fda2 | |||
9626c3fd77 | |||
3f305c6788 | |||
9e68516dac | |||
8f45994b9a |
202
autotagmatchwindow.py
Normal file
202
autotagmatchwindow.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, match_set_list, style, fetch_func):
|
||||
super(AutoTagMatchWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip"))
|
||||
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Next")
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
self.fetch_func = fetch_func
|
||||
|
||||
self.current_match_set_idx = 0
|
||||
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
self.skipButton.clicked.connect(self.skipToNext)
|
||||
|
||||
self.updateData()
|
||||
|
||||
def updateData( self):
|
||||
|
||||
self.current_match_set = self.match_set_list[ self.current_match_set_idx ]
|
||||
|
||||
|
||||
if self.current_match_set_idx + 1 == len( self.match_set_list ):
|
||||
self.skipButton.setDisabled(True)
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.current_row = 0
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle( "Select correct match ({0} of {1}): {2}".format(
|
||||
self.current_match_set_idx+1,
|
||||
len( self.match_set_list ),
|
||||
os.path.split(path)[1] ))
|
||||
|
||||
def populateTable( self ):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for match in self.current_match_set.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = ""
|
||||
if match['month'] is not None:
|
||||
item_text = u"{0}/".format(match['month'])
|
||||
if match['year'] is not None:
|
||||
item_text += u"{0}".format(match['year'])
|
||||
else:
|
||||
item_text += u"????"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
row += 1
|
||||
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.current_row = curr.row()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
|
||||
self.cover_fetcher.fetch( self.current_match_set.matches[self.current_row]['img_url'] )
|
||||
|
||||
# called when the image is done loading
|
||||
def coverFetchComplete( self, image_data, issue_id ):
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
|
||||
def setCoverImage( self ):
|
||||
ca = self.current_match_set.ca
|
||||
cover_idx = ca.readMetadata(self.style).getCoverPageIndexList()[0]
|
||||
image_data = ca.getPage( cover_idx )
|
||||
self.labelCover.setScaledContents(True)
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelCover.setPixmap(QtGui.QPixmap(img))
|
||||
else:
|
||||
self.labelCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.saveMatch()
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len( self.match_set_list ):
|
||||
# no more items
|
||||
QtGui.QDialog.accept(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def skipToNext( self ):
|
||||
self.current_match_set_idx += 1
|
||||
|
||||
if self.current_match_set_idx == len( self.match_set_list ):
|
||||
# no more items
|
||||
QtGui.QDialog.reject(self)
|
||||
else:
|
||||
self.updateData()
|
||||
|
||||
def reject(self):
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Cancel Matching"),
|
||||
self.tr("Are you sure you wish to cancel the matching process?"),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
QtGui.QDialog.reject(self)
|
||||
|
||||
def saveMatch( self ):
|
||||
|
||||
match = self.current_match_set.matches[self.current_row]
|
||||
ca = self.current_match_set.ca
|
||||
|
||||
md = ca.readMetadata( self.style )
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.fetch_func( match )
|
||||
if cv_md is None:
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!"))
|
||||
return
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay( cv_md )
|
||||
success = ca.writeMetadata( md, self.style )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))
|
161
autotagmatchwindow.ui
Normal file
161
autotagmatchwindow.ui
Normal file
@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogMatchSelect</class>
|
||||
<widget class="QDialog" name="dialogMatchSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>831</width>
|
||||
<height>506</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelCover">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
67
autotagprogresswindow.py
Normal file
67
autotagprogresswindow.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.isdone = False
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# make font for scroll window a smidge smaller
|
||||
f = self.textEdit.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
self.textEdit.setFont( f )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
|
||||
def setTestImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblTest )
|
||||
|
||||
def setCoverImage( self, img_data , label):
|
||||
if img_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( img_data )
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
label.setScaledContents(True)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
||||
|
150
autotagprogresswindow.ui
Normal file
150
autotagprogresswindow.ui
Normal file
@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogIssueSelect</class>
|
||||
<widget class="QDialog" name="dialogIssueSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>900</width>
|
||||
<height>413</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Issue Identification Progress</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="invertedAppearance">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="lblTest">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
55
autotagstartwindow.py
Normal file
55
autotagstartwindow.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxSaveOnLowConfidence.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAssumeIssueOne.setCheckState( QtCore.Qt.Unchecked )
|
||||
|
||||
self.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
|
||||
|
124
autotagstartwindow.ui
Normal file
124
autotagstartwindow.ui
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogExport</class>
|
||||
<widget class="QDialog" name="dialogExport">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>524</width>
|
||||
<height>248</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Auto-Tag</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save on low confidence match</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -76,12 +76,22 @@ class CBLTransformer:
|
||||
add_string_list_to_tags( self.metadata.locations )
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.metadata.notes is not None and self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += "\n\n" + self.metadata.notes
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.metadata.webLink is not None and self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += "\n\n" + self.metadata.webLink
|
||||
if self.metadata.webLink is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.webLink
|
||||
|
||||
return self.metadata
|
||||
|
||||
|
409
comicarchive.py
409
comicarchive.py
@ -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" ] and os.path.basename(name)[0] != "." ):
|
||||
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 )
|
||||
|
@ -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 )
|
||||
|
151
comictagger.py
151
comictagger.py
@ -294,7 +294,7 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation:
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
@ -320,65 +320,79 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
# 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 )
|
||||
|
||||
@ -405,12 +419,12 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
print msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
else:
|
||||
new_ext = None # default
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer( md )
|
||||
renamer.setTemplate( settings.rename_template )
|
||||
@ -472,11 +486,8 @@ def main():
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
fname = None
|
||||
if opts.filename is not None:
|
||||
fname = opts.filename.decode(filename_encoding, 'replace')
|
||||
tagger_window = TaggerWindow( fname, settings )
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.file_list, settings )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
|
@ -24,6 +24,8 @@ from pprint import pprint
|
||||
import urllib2, urllib
|
||||
import math
|
||||
import re
|
||||
import datetime
|
||||
import ctversion
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
@ -183,7 +185,9 @@ class ComicVineTalker(QObject):
|
||||
volume_results = self.fetchVolumeData( series_id )
|
||||
|
||||
found = False
|
||||
for record in volume_results['issues']:
|
||||
for record in volume_results['issues']:
|
||||
if IssueString(issue_number).asFloat() is None:
|
||||
issue_number = 1
|
||||
if float(record['issue_number']) == float(issue_number):
|
||||
found = True
|
||||
break
|
||||
@ -201,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()
|
||||
|
||||
@ -218,7 +245,10 @@ class ComicVineTalker(QObject):
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger app using info from Comic Vine."
|
||||
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
|
||||
ctversion.version,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
issue_results['id'])
|
||||
#metadata.notes += issue_results['site_detail_url']
|
||||
|
||||
metadata.webLink = issue_results['site_detail_url']
|
||||
@ -249,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):
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="0.9.4-beta"
|
||||
version="1.0.1-beta"
|
||||
|
62
exportwindow.py
Normal file
62
exportwindow.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for export to zip
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
class ExportConflictOpts:
|
||||
dontCreate = 1
|
||||
overwrite = 2
|
||||
createUnique = 3
|
||||
|
||||
class ExportWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAddToList.setCheckState( QtCore.Qt.Checked )
|
||||
self.radioDontCreate.setChecked( True )
|
||||
|
||||
self.deleteOriginal = False
|
||||
self.addToList = True
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
|
||||
self.addToList = self.cbxAddToList.isChecked()
|
||||
if self.radioDontCreate.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.dontCreate
|
||||
elif self.radioCreateNew.isChecked():
|
||||
self.fileConflictBehavior = ExportConflictOpts.createUnique
|
||||
#else:
|
||||
# self.fileConflictBehavior = ExportConflictOpts.overwrite
|
154
exportwindow.ui
Normal file
154
exportwindow.ui
Normal file
@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogExport</class>
|
||||
<widget class="QDialog" name="dialogExport">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>515</width>
|
||||
<height>307</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Export to Zip Archive</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxAddToList">
|
||||
<property name="text">
|
||||
<string>Add New Archive to ComicTagger list</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxDeleteOriginal">
|
||||
<property name="text">
|
||||
<string>Delete Original RAR (Not recommended)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>When Filename already exists:</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="radioDontCreate">
|
||||
<property name="text">
|
||||
<string>Don't Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="radioCreateNew">
|
||||
<property name="text">
|
||||
<string>Create New Archive With Unique Name (Number appended)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -212,3 +212,6 @@ class FileNameParser:
|
||||
self.issue = self.issue.lstrip("0")
|
||||
if self.issue == "":
|
||||
self.issue = "0"
|
||||
if self.issue[0] == ".":
|
||||
self.issue = "0" + self.issue
|
||||
|
||||
|
@ -106,6 +106,10 @@ class FileRenamer:
|
||||
|
||||
new_name += ext
|
||||
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(":", "-")
|
||||
|
||||
return new_name
|
||||
|
||||
|
||||
|
397
fileselectionlist.py
Normal file
397
fileselectionlist.py
Normal file
@ -0,0 +1,397 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
A PyQt4 widget for managing list of comic archive files
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(FileTableWidget, self).__init__(parent)
|
||||
|
||||
|
||||
self.setColumnCount(5)
|
||||
self.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
|
||||
self.horizontalHeader().setStretchLastSection( True )
|
||||
|
||||
|
||||
class FileTableWidgetItem(QTableWidgetItem):
|
||||
def __lt__(self, other):
|
||||
return (self.data(Qt.UserRole).toBool() <
|
||||
other.data(Qt.UserRole).toBool())
|
||||
|
||||
|
||||
class FileInfo( ):
|
||||
def __init__(self, ca ):
|
||||
self.ca = ca
|
||||
|
||||
class FileSelectionList(QWidget):
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
listCleared = pyqtSignal()
|
||||
|
||||
fileColNum = 0
|
||||
CRFlagColNum = 1
|
||||
CBLFlagColNum = 2
|
||||
typeColNum = 3
|
||||
readonlyColNum = 4
|
||||
folderColNum = 5
|
||||
dataColNum = fileColNum
|
||||
|
||||
|
||||
def __init__(self, parent , settings ):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
#self.twList = FileTableWidget( self )
|
||||
#gridlayout = QGridLayout( self )
|
||||
#gridlayout.addWidget( self.twList )
|
||||
|
||||
#self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
self.currentItem = None
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.modifiedFlag = False
|
||||
|
||||
selectAllAction = QAction("Select All", self)
|
||||
removeAction = QAction("Remove Selected Items", self)
|
||||
self.separator = QAction("",self)
|
||||
self.separator.setSeparator(True)
|
||||
|
||||
selectAllAction.setShortcut( 'Ctrl+A' )
|
||||
removeAction.setShortcut( 'Ctrl+X' )
|
||||
|
||||
selectAllAction.triggered.connect(self.selectAll)
|
||||
removeAction.triggered.connect(self.removeSelection)
|
||||
|
||||
self.addAction(selectAllAction)
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
|
||||
def addAppAction( self, action ):
|
||||
self.insertAction( None , action )
|
||||
|
||||
def setModifiedFlag( self, modified ):
|
||||
self.modifiedFlag = modified
|
||||
|
||||
def selectAll( self ):
|
||||
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), True )
|
||||
|
||||
def deselectAll( self ):
|
||||
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), False )
|
||||
|
||||
def removeArchiveList( self, ca_list ):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for ca in ca_list:
|
||||
for row in range(self.twList.rowCount()):
|
||||
row_ca = self.getArchiveByRow( row )
|
||||
if row_ca == ca:
|
||||
self.twList.removeRow(row)
|
||||
break
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def getArchiveByRow( self, row):
|
||||
fi = self.twList.item(row, FileSelectionList.dataColNum).data( Qt.UserRole ).toPyObject()
|
||||
return fi.ca
|
||||
|
||||
def getCurrentArchive( self ):
|
||||
return self.getArchiveByRow( self.twList.currentRow() )
|
||||
|
||||
def removeSelection( self ):
|
||||
row_list = []
|
||||
for item in self.twList.selectedItems():
|
||||
if item.column() == 0:
|
||||
row_list.append(item.row())
|
||||
|
||||
if len(row_list) == 0:
|
||||
return
|
||||
|
||||
if self.twList.currentRow() in row_list:
|
||||
if not self.modifiedFlagVerification( "Remove Archive",
|
||||
"If you close this archive, data in the form will be lost. Are you sure?"):
|
||||
return
|
||||
|
||||
row_list.sort()
|
||||
row_list.reverse()
|
||||
|
||||
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for i in row_list:
|
||||
self.twList.removeRow(i)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
|
||||
def addPathList( self, pathlist ):
|
||||
filelist = []
|
||||
for p in pathlist:
|
||||
# if path is a folder, walk it recursivly, and all files underneath
|
||||
if os.path.isdir( unicode(p)):
|
||||
for root,dirs,files in os.walk( unicode(p) ):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root,unicode(f)))
|
||||
else:
|
||||
filelist.append(unicode(p))
|
||||
|
||||
# we now have a list of files to add
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle( "Adding Files" )
|
||||
progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
firstAdded = None
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx,f in enumerate(filelist):
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem( f )
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
progdialog.close()
|
||||
if firstAdded is not None:
|
||||
self.twList.selectRow(firstAdded)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
# Adjust column size
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setColumnWidth(FileSelectionList.CRFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.CBLFlagColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.readonlyColNum, 35)
|
||||
self.twList.setColumnWidth(FileSelectionList.typeColNum, 45)
|
||||
if self.twList.columnWidth(FileSelectionList.fileColNum) > 250:
|
||||
self.twList.setColumnWidth(FileSelectionList.fileColNum, 250)
|
||||
if self.twList.columnWidth(FileSelectionList.folderColNum ) > 200:
|
||||
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
|
||||
|
||||
def isListDupe( self, path ):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
ca = self.getArchiveByRow( r )
|
||||
if ca.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def addPathItem( self, path):
|
||||
path = unicode( path )
|
||||
path = os.path.abspath( path )
|
||||
#print "processing", path
|
||||
|
||||
if self.isListDupe(path):
|
||||
return None
|
||||
|
||||
ca = ComicArchive( path )
|
||||
if self.settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( self.settings.rar_exe_path )
|
||||
|
||||
if ca.seemsToBeAComicArchive() :
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
|
||||
fi = FileInfo( ca )
|
||||
|
||||
filename_item = QTableWidgetItem()
|
||||
folder_item = QTableWidgetItem()
|
||||
cix_item = FileTableWidgetItem()
|
||||
cbi_item = FileTableWidgetItem()
|
||||
readonly_item = FileTableWidgetItem()
|
||||
type_item = QTableWidgetItem()
|
||||
|
||||
filename_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
filename_item.setData( Qt.UserRole , fi )
|
||||
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
|
||||
|
||||
folder_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
|
||||
|
||||
type_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
|
||||
|
||||
cix_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
|
||||
|
||||
cbi_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
|
||||
|
||||
readonly_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
readonly_item.setTextAlignment(Qt.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
|
||||
|
||||
self.updateRow( row )
|
||||
|
||||
return row
|
||||
|
||||
def updateRow( self, row ):
|
||||
fi = self.twList.item( row, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
|
||||
|
||||
filename_item = self.twList.item( row, FileSelectionList.fileColNum )
|
||||
folder_item = self.twList.item( row, FileSelectionList.folderColNum )
|
||||
cix_item = self.twList.item( row, FileSelectionList.CRFlagColNum )
|
||||
cbi_item = self.twList.item( row, FileSelectionList.CBLFlagColNum )
|
||||
type_item = self.twList.item( row, FileSelectionList.typeColNum )
|
||||
readonly_item = self.twList.item( row, FileSelectionList.readonlyColNum )
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[0]
|
||||
folder_item.setText( item_text )
|
||||
folder_item.setData( Qt.ToolTipRole, item_text )
|
||||
|
||||
item_text = os.path.split(fi.ca.path)[1]
|
||||
filename_item.setText( item_text )
|
||||
filename_item.setData( Qt.ToolTipRole, item_text )
|
||||
|
||||
if fi.ca.isZip():
|
||||
item_text = "ZIP"
|
||||
elif fi.ca.isRar():
|
||||
item_text = "RAR"
|
||||
else:
|
||||
item_text = ""
|
||||
type_item.setText( item_text )
|
||||
type_item.setData( Qt.ToolTipRole, item_text )
|
||||
|
||||
|
||||
if fi.ca.hasCIX():
|
||||
cix_item.setCheckState(Qt.Checked)
|
||||
cix_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cix_item.setData(Qt.UserRole, False)
|
||||
cix_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if fi.ca.hasCBI():
|
||||
cbi_item.setCheckState(Qt.Checked)
|
||||
cbi_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
cbi_item.setData(Qt.UserRole, False)
|
||||
cbi_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
if not fi.ca.isWritable():
|
||||
readonly_item.setCheckState(Qt.Checked)
|
||||
readonly_item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
readonly_item.setData(Qt.UserRole, False)
|
||||
readonly_item.setCheckState(Qt.Unchecked)
|
||||
|
||||
|
||||
# Reading these will force them into the ComicArchive's cache
|
||||
fi.ca.readCIX()
|
||||
fi.ca.hasCBI()
|
||||
|
||||
def getSelectedArchiveList( self ):
|
||||
ca_list = []
|
||||
for r in range( self.twList.rowCount() ):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
fi = item.data( Qt.UserRole ).toPyObject()
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
return ca_list
|
||||
|
||||
def updateCurrentRow( self ):
|
||||
self.updateRow( self.twList.currentRow() )
|
||||
|
||||
def updateSelectedRows( self ):
|
||||
self.twList.setSortingEnabled(False)
|
||||
for r in range( self.twList.rowCount() ):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if self.twList.isItemSelected(item):
|
||||
self.updateRow( r )
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def currentItemChangedCB( self, curr, prev ):
|
||||
|
||||
new_idx = curr.row()
|
||||
old_idx = -1
|
||||
if prev is not None:
|
||||
old_idx = prev.row()
|
||||
#print "old {0} new {1}".format(old_idx, new_idx)
|
||||
|
||||
if old_idx == new_idx:
|
||||
return
|
||||
|
||||
# don't allow change if modified
|
||||
if prev is not None and new_idx != old_idx:
|
||||
if not self.modifiedFlagVerification( "Change Archive",
|
||||
"If you change archives now, data in the form will be lost. Are you sure?"):
|
||||
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
|
||||
self.twList.setCurrentItem( prev )
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
# Need to defer this revert selection, for some reason
|
||||
QTimer.singleShot(1, self.revertSelection)
|
||||
return
|
||||
|
||||
fi = self.twList.item( new_idx, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
|
||||
self.selectionChanged.emit( QVariant(fi))
|
||||
|
||||
def revertSelection( self ):
|
||||
self.twList.selectRow( self.twList.currentRow() )
|
||||
|
||||
|
||||
def modifiedFlagVerification( self, title, desc):
|
||||
if self.modifiedFlag:
|
||||
reply = QMessageBox.question(self,
|
||||
self.tr(title),
|
||||
self.tr(desc),
|
||||
QMessageBox.Yes, QMessageBox.No )
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Attempt to use a special checkbox widget in the cell.
|
||||
# Couldn't figure out how to disable it with "enabled" colors
|
||||
#w = QWidget()
|
||||
#cb = QCheckBox(w)
|
||||
#cb.setCheckState(Qt.Checked)
|
||||
#layout = QHBoxLayout()
|
||||
#layout.addWidget( cb )
|
||||
#layout.setAlignment(Qt.AlignHCenter)
|
||||
#layout.setMargin(2)
|
||||
#w.setLayout(layout)
|
||||
#self.twList.setCellWidget( row, 2, w )
|
112
fileselectionlist.ui
Normal file
112
fileselectionlist.ui
Normal file
@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>pageListEditor</class>
|
||||
<widget class="QWidget" name="pageListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>527</width>
|
||||
<height>323</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideMiddle</enum>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>61</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>36</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File Name</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CR</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Has ComicRack Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CBL</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Has ComicBookLover Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Archive Type</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>R/O</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Read-Only</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Folder</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File Location</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -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 )
|
||||
|
BIN
graphics/autotag.png
Normal file
BIN
graphics/autotag.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
graphics/longbox.png
Normal file
BIN
graphics/longbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -1,5 +1,6 @@
|
||||
|
||||
import StringIO
|
||||
import sys
|
||||
|
||||
try:
|
||||
import Image
|
||||
@ -29,7 +30,13 @@ class ImageHasher(object):
|
||||
self.image = Image.new( "L", (1,1))
|
||||
|
||||
def average_hash(self):
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
try:
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
except Exception as e:
|
||||
sys.exc_clear()
|
||||
print "average_hash error:", e
|
||||
return long(0)
|
||||
|
||||
pixels = list(image.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
|
||||
|
@ -72,6 +72,7 @@ class IssueIdentifier:
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
self.callback = None
|
||||
self.coverUrlCallback = None
|
||||
self.search_result = self.ResultNoMatches
|
||||
self.cover_page_index = 0
|
||||
|
||||
@ -97,7 +98,7 @@ class IssueIdentifier:
|
||||
def setOutputFunction( self, func ):
|
||||
self.output_function = func
|
||||
pass
|
||||
|
||||
|
||||
def calculateHash( self, image_data ):
|
||||
if self.image_hasher == '3':
|
||||
return ImageHasher( data=image_data ).dct_average_hash()
|
||||
@ -130,6 +131,9 @@ class IssueIdentifier:
|
||||
|
||||
def setProgressCallback( self, cb_func ):
|
||||
self.callback = cb_func
|
||||
|
||||
def setCoverURLCallback( self, cb_func ):
|
||||
self.coverUrlCallback = cb_func
|
||||
|
||||
def getSearchKeys( self ):
|
||||
|
||||
@ -198,7 +202,7 @@ class IssueIdentifier:
|
||||
|
||||
@staticmethod
|
||||
def defaultWriteOutput( text ):
|
||||
sys.stdout.write(text)
|
||||
sys.stdout.write(text.encode( errors='replace') )
|
||||
sys.stdout.flush()
|
||||
|
||||
def log_msg( self, msg , newline=True ):
|
||||
@ -252,7 +256,7 @@ class IssueIdentifier:
|
||||
if keys['month'] is not None:
|
||||
self.log_msg( "\tMonth : " + str(keys['month']) )
|
||||
|
||||
self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
|
||||
comicVine = ComicVineTalker( )
|
||||
|
||||
@ -306,7 +310,6 @@ class IssueIdentifier:
|
||||
if self.callback is not None:
|
||||
self.callback( 0, len(series_shortlist))
|
||||
|
||||
|
||||
# now sort the list by name length
|
||||
series_shortlist.sort(key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
@ -358,6 +361,9 @@ class IssueIdentifier:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback( url_image_data )
|
||||
|
||||
url_image_hash = self.calculateHash( url_image_data )
|
||||
score = ImageHasher.hamming_distance(cover_hash, url_image_hash)
|
||||
|
||||
@ -387,7 +393,6 @@ class IssueIdentifier:
|
||||
|
||||
break
|
||||
self.log_msg( "" )
|
||||
|
||||
|
||||
if len(self.match_list) == 0:
|
||||
self.log_msg( ":-( no matches!" )
|
||||
@ -419,7 +424,7 @@ class IssueIdentifier:
|
||||
if len(self.match_list) == 1:
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
if best_score > self.min_score_thresh:
|
||||
self.log_msg( "!!!! Very weak score for the cover. Maybe it's not the cover?" )
|
||||
self.log_msg( "Very weak score for the cover. Maybe it's not the cover..." )
|
||||
|
||||
self.log_msg( "Comparing to some other archive pages now..." )
|
||||
found = False
|
||||
@ -428,11 +433,11 @@ class IssueIdentifier:
|
||||
page_hash = self.calculateHash( image_data )
|
||||
distance = ImageHasher.hamming_distance(page_hash, self.match_list[0]['url_image_hash'])
|
||||
if distance <= self.strong_score_thresh:
|
||||
self.log_msg( "Found a great match d={0} on page {1}!".format(distance, i+1) )
|
||||
self.log_msg( "Found a great match (distance = {0}) on page {1}!".format(distance, i+1) )
|
||||
found = True
|
||||
break
|
||||
elif distance < self.min_score_thresh:
|
||||
self.log_msg( "Found a good match d={0} on page {1}".format(distance, i) )
|
||||
self.log_msg( "Found a good match (distance = {0}) on page {1}".format(distance, i) )
|
||||
found = True
|
||||
self.log_msg( ".", newline=False )
|
||||
self.log_msg( "" )
|
||||
@ -440,7 +445,9 @@ class IssueIdentifier:
|
||||
self.log_msg( "No matching pages in the issue. Bummer" )
|
||||
self.search_result = self.ResultFoundMatchButBadCoverScore
|
||||
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
return self.match_list
|
||||
|
||||
elif best_score > self.min_score_thresh and len(self.match_list) > 1:
|
||||
@ -455,7 +462,9 @@ class IssueIdentifier:
|
||||
self.match_list.remove(item)
|
||||
|
||||
if len(self.match_list) == 1:
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.search_result = self.ResultOneGoodMatch
|
||||
|
||||
elif len(self.match_list) == 0:
|
||||
@ -465,8 +474,10 @@ class IssueIdentifier:
|
||||
print
|
||||
self.log_msg( "More than one likley candiate." )
|
||||
self.search_result = self.ResultMultipleGoodMatches
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
for item in self.match_list:
|
||||
print_match(item)
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
|
||||
return self.match_list
|
||||
|
||||
|
@ -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 )
|
||||
|
@ -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,6 +106,7 @@ 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_msg_and_quit( self, msg, code, show_help=False ):
|
||||
@ -180,7 +182,7 @@ 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_msg_and_quit( str(err), 2 )
|
||||
@ -219,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":
|
||||
|
@ -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,17 +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'])
|
||||
|
||||
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
if self.comic_archive is not None:
|
||||
image_data = self.comic_archive.getPage( idx )
|
||||
else:
|
||||
image_data = None
|
||||
|
||||
if image_data is not None:
|
||||
img = QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.current_pixmap = QPixmap(QPixmap(img))
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
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
|
||||
@ -234,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 ) )
|
||||
@ -242,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
77
pageloader.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""
|
||||
A PyQT4 class to load a page image from a ComicArchive in a background thread
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comicarchive import ComicArchive
|
||||
|
||||
"""
|
||||
This class holds onto a reference of each instance in a list
|
||||
since problems occur if the ref count goes to zero and the GC
|
||||
tries to reap the object while the thread is going.
|
||||
|
||||
If the client class wants to stop the thread, they should mark
|
||||
it as "abandoned", and no signals will be issued
|
||||
"""
|
||||
|
||||
class PageLoader( QtCore.QThread ):
|
||||
|
||||
loadComplete = pyqtSignal( QtGui.QImage )
|
||||
|
||||
instanceList = []
|
||||
mutex = QtCore.QMutex()
|
||||
|
||||
"""
|
||||
Remove all finished threads from the list
|
||||
"""
|
||||
@staticmethod
|
||||
def reapInstances():
|
||||
for obj in reversed(PageLoader.instanceList ):
|
||||
if obj.isFinished():
|
||||
PageLoader.instanceList.remove(obj)
|
||||
|
||||
def __init__(self, ca, page_num ):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.ca = ca
|
||||
self.page_num = page_num
|
||||
self.abandoned = False
|
||||
|
||||
# remove any old instances, and then add ourself
|
||||
PageLoader.mutex.lock()
|
||||
PageLoader.reapInstances()
|
||||
PageLoader.instanceList.append( self )
|
||||
PageLoader.mutex.unlock()
|
||||
|
||||
def run(self):
|
||||
image_data = self.ca.getPage( self.page_num )
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
self.loadComplete.emit( img )
|
||||
|
||||
|
@ -32,7 +32,12 @@ class IDProgressWindow(QtGui.QDialog):
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
|
||||
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# make font for scroll window a smidge smaller
|
||||
f = self.textEdit.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
self.textEdit.setFont( f )
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>556</width>
|
||||
<width>650</width>
|
||||
<height>287</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -28,7 +28,11 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="readOnly">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier</family>
|
||||
</font>
|
||||
</property> <property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
|
@ -1,4 +1,33 @@
|
||||
|
||||
---------------------------------
|
||||
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
|
||||
---------------------------------
|
||||
|
116
renamewindow.py
116
renamewindow.py
@ -23,35 +23,91 @@ from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from options import MetaDataStyle
|
||||
|
||||
import os
|
||||
import utils
|
||||
|
||||
class RenameWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, comic_archive, metadata, settings ):
|
||||
def __init__( self, parent, comic_archive_list, data_style, settings ):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
|
||||
|
||||
self.settings = settings
|
||||
self.metadata = metadata
|
||||
self.comic_archive = comic_archive
|
||||
self.new_name = None
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
|
||||
self.btnSettings.clicked.connect( self.modifySettings )
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def configRenamer( self ):
|
||||
self.renamer = FileRenamer( self.metadata )
|
||||
self.renamer = FileRenamer( None )
|
||||
self.renamer.setTemplate( self.settings.rename_template )
|
||||
self.renamer.setIssueZeroPadding( self.settings.rename_issue_number_padding )
|
||||
self.renamer.setSmartCleanup( self.settings.rename_use_smart_string_cleanup )
|
||||
|
||||
def doPreview( self ):
|
||||
self.new_name = self.renamer.determineName( self.comic_archive.path )
|
||||
preview = u"\"{0}\" ==> \"{1}\"".format( self.comic_archive.path, self.new_name )
|
||||
self.textEdit.setPlainText( preview )
|
||||
self.rename_list = []
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for ca in self.comic_archive_list:
|
||||
|
||||
new_ext = None # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
md = ca.readMetadata(self.data_style)
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
self.renamer.setMetadata( md )
|
||||
new_name = self.renamer.determineName( ca.path, ext=new_ext )
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
folder_item = QtGui.QTableWidgetItem()
|
||||
old_name_item = QtGui.QTableWidgetItem()
|
||||
new_name_item = QtGui.QTableWidgetItem()
|
||||
|
||||
item_text = os.path.split(ca.path)[0]
|
||||
folder_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, folder_item)
|
||||
folder_item.setText( item_text )
|
||||
folder_item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
|
||||
item_text = os.path.split(ca.path)[1]
|
||||
old_name_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, old_name_item)
|
||||
old_name_item.setText( item_text )
|
||||
old_name_item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
|
||||
new_name_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, new_name_item)
|
||||
new_name_item.setText( new_name )
|
||||
new_name_item.setData( QtCore.Qt.ToolTipRole, new_name )
|
||||
|
||||
dict_item = dict()
|
||||
dict_item['archive'] = ca
|
||||
dict_item['new_name'] = new_name
|
||||
self.rename_list.append( dict_item)
|
||||
|
||||
# Adjust column sizes
|
||||
self.twList.setVisible( False )
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setVisible( True )
|
||||
if self.twList.columnWidth(0) > 200:
|
||||
self.twList.setColumnWidth(0, 200)
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modifySettings( self ):
|
||||
settingswin = SettingsWindow( self, self.settings )
|
||||
@ -63,17 +119,35 @@ class RenameWindow(QtGui.QDialog):
|
||||
self.doPreview()
|
||||
|
||||
def accept( self ):
|
||||
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
|
||||
progdialog.setWindowTitle( "Renaming Archives" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
for idx,item in enumerate(self.rename_list):
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
idx += 1
|
||||
progdialog.setLabelText( item['new_name'] )
|
||||
|
||||
if item['new_name'] == os.path.basename( item['archive'].path ):
|
||||
print item['new_name'] , "Filename is already good!"
|
||||
continue
|
||||
|
||||
if not item['archive'].isWritable(check_rar_status=False):
|
||||
continue
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( item['archive'].path ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, item['new_name'] ) )
|
||||
|
||||
os.rename( item['archive'].path, new_abs_path)
|
||||
|
||||
item['archive'].rename( new_abs_path )
|
||||
|
||||
progdialog.close()
|
||||
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
if self.new_name == os.path.basename( self.comic_archive.path ):
|
||||
#print msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( self.comic_archive.path ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, self.new_name ) )
|
||||
|
||||
os.rename( self.comic_archive.path, new_abs_path )
|
||||
|
||||
self.new_name = new_abs_path
|
||||
|
||||
|
@ -9,8 +9,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>556</width>
|
||||
<height>210</height>
|
||||
<width>801</width>
|
||||
<height>360</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -30,10 +30,31 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideMiddle</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Folder</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Old Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>New Name</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -56,17 +77,6 @@
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>140</x>
|
||||
<y>10</y>
|
||||
<width>133</width>
|
||||
<height>29</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout"/>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
|
28
settings.py
28
settings.py
@ -51,12 +51,15 @@ class ComicTaggerSettings:
|
||||
self.allow_cbi_in_rar = True
|
||||
|
||||
# automatic settings
|
||||
self.last_selected_data_style = 0
|
||||
self.last_selected_save_data_style = 0
|
||||
self.last_selected_load_data_style = 0
|
||||
self.last_opened_folder = ""
|
||||
self.last_main_window_width = 0
|
||||
self.last_main_window_height = 0
|
||||
self.last_main_window_x = 0
|
||||
self.last_main_window_y = 0
|
||||
self.last_form_side_width = -1
|
||||
self.last_list_side_width = -1
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
@ -84,7 +87,7 @@ class ComicTaggerSettings:
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
|
||||
self.rename_extension_based_on_archive = True
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -139,9 +142,11 @@ class ComicTaggerSettings:
|
||||
|
||||
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
|
||||
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
|
||||
|
||||
if self.config.has_option('auto', 'last_selected_data_style'):
|
||||
self.last_selected_data_style = self.config.getint( 'auto', 'last_selected_data_style' )
|
||||
|
||||
if self.config.has_option('auto', 'last_selected_load_data_style'):
|
||||
self.last_selected_load_data_style = self.config.getint( 'auto', 'last_selected_load_data_style' )
|
||||
if self.config.has_option('auto', 'last_selected_save_data_style'):
|
||||
self.last_selected_save_data_style = self.config.getint( 'auto', 'last_selected_save_data_style' )
|
||||
if self.config.has_option('auto', 'last_opened_folder'):
|
||||
self.last_opened_folder = self.config.get( 'auto', 'last_opened_folder' )
|
||||
if self.config.has_option('auto', 'last_main_window_width'):
|
||||
@ -152,6 +157,10 @@ class ComicTaggerSettings:
|
||||
self.last_main_window_x = self.config.getint( 'auto', 'last_main_window_x' )
|
||||
if self.config.has_option('auto', 'last_main_window_y'):
|
||||
self.last_main_window_y = self.config.getint( 'auto', 'last_main_window_y' )
|
||||
if self.config.has_option('auto', 'last_form_side_width'):
|
||||
self.last_form_side_width = self.config.getint( 'auto', 'last_form_side_width' )
|
||||
if self.config.has_option('auto', 'last_list_side_width'):
|
||||
self.last_list_side_width = self.config.getint( 'auto', 'last_list_side_width' )
|
||||
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint( 'identifier', 'id_length_delta_thresh' )
|
||||
@ -189,8 +198,9 @@ class ComicTaggerSettings:
|
||||
self.rename_issue_number_padding = self.config.getint( 'rename', 'rename_issue_number_padding' )
|
||||
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
|
||||
self.rename_use_smart_string_cleanup = self.config.getboolean( 'rename', 'rename_use_smart_string_cleanup' )
|
||||
if self.config.has_option('rename', 'rename_extension_based_on_archive'):
|
||||
self.rename_extension_based_on_archive = self.config.getboolean( 'rename', 'rename_extension_based_on_archive' )
|
||||
|
||||
|
||||
def save( self ):
|
||||
|
||||
if not self.config.has_section( 'settings' ):
|
||||
@ -202,12 +212,15 @@ class ComicTaggerSettings:
|
||||
if not self.config.has_section( 'auto' ):
|
||||
self.config.add_section( 'auto' )
|
||||
|
||||
self.config.set( 'auto', 'last_selected_data_style', self.last_selected_data_style )
|
||||
self.config.set( 'auto', 'last_selected_load_data_style', self.last_selected_load_data_style )
|
||||
self.config.set( 'auto', 'last_selected_save_data_style', self.last_selected_save_data_style )
|
||||
self.config.set( 'auto', 'last_opened_folder', self.last_opened_folder )
|
||||
self.config.set( 'auto', 'last_main_window_width', self.last_main_window_width )
|
||||
self.config.set( 'auto', 'last_main_window_height', self.last_main_window_height )
|
||||
self.config.set( 'auto', 'last_main_window_x', self.last_main_window_x )
|
||||
self.config.set( 'auto', 'last_main_window_y', self.last_main_window_y )
|
||||
self.config.set( 'auto', 'last_form_side_width', self.last_form_side_width )
|
||||
self.config.set( 'auto', 'last_list_side_width', self.last_list_side_width )
|
||||
|
||||
if not self.config.has_section( 'identifier' ):
|
||||
self.config.add_section( 'identifier' )
|
||||
@ -244,6 +257,7 @@ class ComicTaggerSettings:
|
||||
self.config.set( 'rename', 'rename_template', self.rename_template )
|
||||
self.config.set( 'rename', 'rename_issue_number_padding', self.rename_issue_number_padding )
|
||||
self.config.set( 'rename', 'rename_use_smart_string_cleanup', self.rename_use_smart_string_cleanup )
|
||||
self.config.set( 'rename', 'rename_extension_based_on_archive', self.rename_extension_based_on_archive )
|
||||
|
||||
with open( self.settings_file, 'wb') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
@ -141,6 +141,9 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.leIssueNumPadding.setText( str(self.settings.rename_issue_number_padding) )
|
||||
if self.settings.rename_use_smart_string_cleanup:
|
||||
self.cbxSmartCleanup.setCheckState( QtCore.Qt.Checked )
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
self.cbxChangeExtension.setCheckState( QtCore.Qt.Checked )
|
||||
|
||||
|
||||
def accept( self ):
|
||||
|
||||
@ -174,6 +177,7 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.settings.rename_template = str(self.leRenameTemplate.text())
|
||||
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
@ -334,7 +334,7 @@
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
|
||||
<property name="text">
|
||||
<string>Apply CBL Transforms on Batch/CLI Operations</string>
|
||||
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -354,7 +354,7 @@
|
||||
<rect>
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>242</width>
|
||||
<width>246</width>
|
||||
<height>182</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -483,6 +483,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxChangeExtension">
|
||||
<property name="text">
|
||||
<string>Change Extension Based On Archive Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
921
taggerwindow.py
921
taggerwindow.py
File diff suppressed because it is too large
Load Diff
2031
taggerwindow.ui
2031
taggerwindow.ui
File diff suppressed because it is too large
Load Diff
42
todo.txt
42
todo.txt
@ -1,33 +1,39 @@
|
||||
-----------------------------------------------------
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
|
||||
Multi-file:
|
||||
Does the main UI need to have "View/Read Tag Style" and "Write Tag style" concept?
|
||||
Create ComicArchiveListWidget
|
||||
as item is added, new metadata object is attached (for each block??)
|
||||
|
||||
How to add items to list?
|
||||
recursive
|
||||
drag and drop
|
||||
Batch Functions:
|
||||
Auto-Tag
|
||||
Interactive dialog at end
|
||||
Manually change cover image on left??
|
||||
|
||||
Rename
|
||||
check-box for rows?
|
||||
manual edit the preview?
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
spider-man 678 .... ascii print problem. grrrr
|
||||
-----------------------------------------------------
|
||||
RAR Password -- childrens crusade 3
|
||||
|
||||
-----------------------------------------------------
|
||||
Big Future Features
|
||||
-----------------------------------------------------
|
||||
GUI to handle mutliple files or folders
|
||||
|
||||
Scrape alternate Covers from ComicVine issue pages
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
pyComicMetaThis CBI features
|
||||
|
||||
Auto search:
|
||||
Searching w/o issue #
|
||||
|
||||
Form Mode: Single vs Batch
|
||||
|
||||
-----------------------------------------------------
|
||||
Small(er) Future Feature
|
||||
-----------------------------------------------------
|
||||
@ -46,7 +52,6 @@ Archive function to detect tag blocks out of sync
|
||||
|
||||
Settings
|
||||
Add setting to dis-allow writing CBI to RAR
|
||||
Overwrite or overlay
|
||||
|
||||
Google App engine to store hashes
|
||||
Content Hashes, Image hashes, who knows?
|
||||
@ -77,17 +82,4 @@ Release Process
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
COMIC RACK Questions
|
||||
|
||||
Missing from XML as enterable in ComicRack:
|
||||
Main Character or Team
|
||||
Review
|
||||
User Rating
|
||||
|
||||
Some that seem library only:
|
||||
"Series Complete"
|
||||
Tags
|
||||
Proposed Values
|
||||
Community Rating
|
||||
|
||||
|
Reference in New Issue
Block a user