Compare commits
7 Commits
0.9.4-beta
...
0.9.5-beta
Author | SHA1 | Date | |
---|---|---|---|
41f730a558 | |||
550b84361c | |||
fb4248fda2 | |||
9626c3fd77 | |||
3f305c6788 | |||
9e68516dac | |||
8f45994b9a |
@ -419,8 +419,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 +436,14 @@ 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.comet_filename = None
|
||||
self.page_count = None
|
||||
self.page_list = None
|
||||
|
||||
def setExternalRarProgram( self, rar_exe_path ):
|
||||
if self.isRar():
|
||||
self.archiver.rar_exe_path = rar_exe_path
|
||||
@ -512,12 +520,16 @@ 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 )
|
||||
self.resetCache()
|
||||
return retcode
|
||||
|
||||
|
||||
def hasMetadata( self, style ):
|
||||
|
||||
@ -531,13 +543,16 @@ 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()
|
||||
self.resetCache()
|
||||
return retcode
|
||||
|
||||
def getPage( self, index ):
|
||||
|
||||
image_data = None
|
||||
@ -561,29 +576,32 @@ 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()
|
||||
md = GenericMetadata()
|
||||
else:
|
||||
md = ComicBookInfo().metadataFromString( raw_cbi )
|
||||
|
||||
@ -598,12 +616,16 @@ 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 )
|
||||
@ -633,7 +655,6 @@ class ComicArchive:
|
||||
|
||||
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 )
|
||||
@ -652,12 +673,15 @@ class ComicArchive:
|
||||
return self.archiver.removeArchiveFile( self.ci_xml_filename )
|
||||
|
||||
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 ):
|
||||
|
@ -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 )
|
||||
|
130
comictagger.py
130
comictagger.py
@ -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 )
|
||||
|
||||
|
@ -201,6 +201,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()
|
||||
|
||||
@ -249,11 +272,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="0.9.5-beta"
|
217
fileselectionlist.py
Normal file
217
fileselectionlist.py
Normal file
@ -0,0 +1,217 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
A PyQt4 widget for managing list of 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, path, ca, cix_md, cbi_md ):
|
||||
self.path = path
|
||||
self.cix_md = cix_md
|
||||
self.cbi_md = cbi_md
|
||||
self.ca = ca
|
||||
|
||||
class FileSelectionList(QWidget):
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
|
||||
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.setAcceptDrops(True)
|
||||
|
||||
self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
self.droppedFiles = None
|
||||
if event.mimeData().hasUrls():
|
||||
|
||||
# walk through the URL list and build a file list
|
||||
for url in event.mimeData().urls():
|
||||
if url.isValid() and url.scheme() == "file":
|
||||
if self.droppedFiles is None:
|
||||
self.droppedFiles = []
|
||||
self.droppedFiles.append(url.toLocalFile())
|
||||
|
||||
if self.droppedFiles is not None:
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
self.addPathList( self.droppedFiles)
|
||||
event.accept()
|
||||
|
||||
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)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx,f in enumerate(filelist):
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
self.addPathItem( f )
|
||||
|
||||
progdialog.close()
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
#Maybe set a max size??
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
|
||||
def isListDupe( self, path ):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
fi = self.twList.item(r, 0).data( Qt.UserRole ).toPyObject()
|
||||
if fi.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def addPathItem( self, path):
|
||||
path = unicode( path )
|
||||
#print "processing", path
|
||||
|
||||
if self.isListDupe(path):
|
||||
return
|
||||
|
||||
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 )
|
||||
|
||||
cix_md = None
|
||||
cbi_md = None
|
||||
|
||||
has_cix = ca.hasCIX()
|
||||
if has_cix:
|
||||
cix_md = ca.readCIX()
|
||||
|
||||
has_cbi = ca.hasCBI()
|
||||
if has_cbi:
|
||||
cbi_md = ca.readCBI()
|
||||
|
||||
fi = FileInfo( path, ca, cix_md, cbi_md )
|
||||
|
||||
item_text = os.path.split(path)[1]
|
||||
item = QTableWidgetItem(item_text)
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setData( Qt.UserRole , fi )
|
||||
item.setData( Qt.ToolTipRole ,item_text)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = os.path.split(path)[0]
|
||||
item = QTableWidgetItem(item_text)
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setData( Qt.ToolTipRole ,item_text)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
# 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 )
|
||||
|
||||
item = FileTableWidgetItem()
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setTextAlignment(Qt.AlignHCenter)
|
||||
if has_cix:
|
||||
item.setCheckState(Qt.Checked)
|
||||
item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
item.setData(Qt.UserRole, False)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item = FileTableWidgetItem()
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setTextAlignment(Qt.AlignHCenter)
|
||||
if has_cbi:
|
||||
item.setCheckState(Qt.Checked)
|
||||
item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
item.setData(Qt.UserRole, False)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
def itemSelectionChangedCB( self ):
|
||||
idx = self.twList.currentRow()
|
||||
|
||||
fi = self.twList.item(idx, 0).data( Qt.UserRole ).toPyObject()
|
||||
|
||||
#if fi.cix_md is not None:
|
||||
# print u"{0}".format(fi.cix_md)
|
||||
|
||||
self.selectionChanged.emit( QVariant(fi))
|
69
fileselectionlist.ui
Normal file
69
fileselectionlist.ui
Normal file
@ -0,0 +1,69 @@
|
||||
<?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="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</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>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CR</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CBL</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 )
|
||||
|
@ -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,6 +80,7 @@ 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)
|
||||
@ -151,17 +153,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
|
||||
|
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 )
|
||||
|
||||
|
@ -1,4 +1,12 @@
|
||||
|
||||
---------------------------------
|
||||
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
|
||||
---------------------------------
|
||||
|
@ -44,8 +44,10 @@ from filenameparser import FileNameParser
|
||||
from logwindow import LogWindow
|
||||
from optionalmsgdialog import OptionalMessageDialog
|
||||
from pagelisteditor import PageListEditor
|
||||
from fileselectionlist import FileSelectionList
|
||||
from cbltransformer import CBLTransformer
|
||||
from renamewindow import RenameWindow
|
||||
from pageloader import PageLoader
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
@ -91,18 +93,32 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
#signal.signal(signal.SIGINT, self.sigint_handler)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
|
||||
self.settings = settings
|
||||
|
||||
self.pageListEditor = PageListEditor( self.tabPages )
|
||||
gridlayout = QtGui.QGridLayout( self.tabPages )
|
||||
gridlayout.addWidget( self.pageListEditor )
|
||||
|
||||
#---------------------------
|
||||
self.fileSelectionList = FileSelectionList( self.widgetListHolder, self.settings )
|
||||
gridlayout = QtGui.QGridLayout( self.widgetListHolder )
|
||||
gridlayout.addWidget( self.fileSelectionList )
|
||||
|
||||
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
|
||||
# ATB: Disable the list for now...
|
||||
self.splitter.setSizes([100,0])
|
||||
self.splitter.setHandleWidth(0)
|
||||
self.splitter.handle(1).setDisabled(True)
|
||||
|
||||
#---------------------------
|
||||
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
|
||||
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
#print platform.system(), platform.release()
|
||||
self.dirtyFlag = False
|
||||
self.settings = settings
|
||||
self.data_style = settings.last_selected_data_style
|
||||
|
||||
#set up a default metadata object
|
||||
@ -117,6 +133,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.droppedFile = None
|
||||
|
||||
self.page_browser = None
|
||||
self.page_loader = None
|
||||
|
||||
self.populateComboBoxes()
|
||||
|
||||
@ -362,7 +379,6 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
msgBox.setStandardButtons( QtGui.QMessageBox.Ok )
|
||||
msgBox.exec_()
|
||||
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
self.droppedFile=None
|
||||
if event.mimeData().hasUrls():
|
||||
@ -371,12 +387,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if url.scheme()=="file":
|
||||
self.droppedFile=url.toLocalFile()
|
||||
event.accept()
|
||||
|
||||
|
||||
def dropEvent(self, event):
|
||||
if self.dirtyFlagVerification( "Open Archive",
|
||||
"If you open a new archive now, data in the form will be lost. Are you sure?"):
|
||||
self.openArchive( unicode(self.droppedFile))
|
||||
|
||||
|
||||
def openArchive( self, path, explicit_style=None, clear_form=True ):
|
||||
|
||||
if path is None or path == "":
|
||||
@ -431,35 +447,44 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
else:
|
||||
return
|
||||
|
||||
if self.metadata.isEmpty:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
self.updateCoverImage()
|
||||
|
||||
if self.page_browser is not None:
|
||||
self.page_browser.setComicArchive( self.comic_archive )
|
||||
self.page_browser.metadata = self.metadata
|
||||
|
||||
self.metadataToForm()
|
||||
self.pageListEditor.setData( self.comic_archive, self.metadata.pages )
|
||||
self.clearDirtyFlag() # also updates the app title
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
#self.updatePagesInfo()
|
||||
self.loadCurrentArchive()
|
||||
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
|
||||
|
||||
def updateCoverImage( self ):
|
||||
cover_idx = self.metadata.getCoverPageIndexList()[0]
|
||||
image_data = self.comic_archive.getPage( cover_idx )
|
||||
if not image_data is None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(img))
|
||||
self.lblCover.setScaledContents(True)
|
||||
def loadCurrentArchive( self ):
|
||||
if self.metadata.isEmpty:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
self.updateCoverImage()
|
||||
|
||||
if self.page_browser is not None:
|
||||
self.page_browser.setComicArchive( self.comic_archive )
|
||||
self.page_browser.metadata = self.metadata
|
||||
|
||||
self.metadataToForm()
|
||||
self.pageListEditor.setData( self.comic_archive, self.metadata.pages )
|
||||
self.clearDirtyFlag() # also updates the app title
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
def updateCoverImage( self ):
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
cover_idx = self.metadata.getCoverPageIndexList()[0]
|
||||
|
||||
self.page_loader = PageLoader( self.comic_archive, cover_idx )
|
||||
self.page_loader.loadComplete.connect( self.actualUpdateCoverImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualUpdateCoverImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(img))
|
||||
self.lblCover.setScaledContents(True)
|
||||
|
||||
|
||||
def updateMenus( self ):
|
||||
|
||||
# First just disable all the questionable items
|
||||
@ -1389,5 +1414,19 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.comic_archive = None
|
||||
self.openArchive( dlg.new_name )
|
||||
|
||||
def fileListSelectionChanged( self, qvarFI ):
|
||||
fi = qvarFI.toPyObject()
|
||||
#if fi.cix_md is not None:
|
||||
# print u"{0}".format(fi.cix_md)
|
||||
self.comic_archive = None
|
||||
self.clearForm()
|
||||
|
||||
|
||||
self.comic_archive = fi.ca
|
||||
if self.data_style == MetaDataStyle.CIX:
|
||||
self.metadata = fi.cix_md
|
||||
else:
|
||||
self.metadata = fi.cbi_md
|
||||
if self.metadata is None:
|
||||
self.metadata = GenericMetadata()
|
||||
|
||||
self.loadCurrentArchive()
|
||||
|
1813
taggerwindow.ui
1813
taggerwindow.ui
File diff suppressed because it is too large
Load Diff
27
todo.txt
27
todo.txt
@ -4,13 +4,28 @@ 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
|
||||
|
||||
Edit functions on list: select, select all, delete,
|
||||
|
||||
Batch Functions:
|
||||
Auto-Select
|
||||
Start/Options Dialog
|
||||
Progress Dialog - maybe reuse
|
||||
Interactive dialog at end
|
||||
Rename
|
||||
Start dialog with preview
|
||||
maybe table with checkboxes?
|
||||
|
||||
Copy Block
|
||||
Verify overwrites
|
||||
|
||||
Turn off drop accept for edit lines/boxes
|
||||
Drop on app goes to list and selects it
|
||||
Accept multiple files on file open dialog
|
||||
Warn on moving selection list away from modified form
|
||||
|
||||
|
||||
ComicArchive: cache each metadata block? Need to make sure cache is cleared on file modify
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
|
Reference in New Issue
Block a user