0747a6b0ef
Moves to Python format strings for renaming, handles directory structures, moving of files to a destination directory, sanitizes file paths with pathvalidate and takes a different approach to smart filename cleanup using the Python string.Formatter class Moving to Python format strings means we can point to python documentation for syntax and all we have to do is document the properties and types that are attached to the GenericMetadata class. Switching to pathvalidate allows comictagger to more simply handle both directories and symbols in filenames. The only changes to the string.Formatter class is: 1. format_field returns an empty string if the value is none or an empty string regardless of the format specifier. 2. _vformat drops the previous literal text if the field value is an empty string and lstrips the following literal text of closing special characters.
2202 lines
85 KiB
Python
2202 lines
85 KiB
Python
# coding=utf-8
|
|
"""The main window of the ComicTagger app"""
|
|
|
|
# Copyright 2012-2014 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 locale
|
|
import functools
|
|
import platform
|
|
import os
|
|
import pprint
|
|
import json
|
|
import webbrowser
|
|
import re
|
|
import pickle
|
|
import datetime
|
|
#import signal
|
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
|
from PyQt5 import QtNetwork
|
|
from PyQt5.QtCore import QUrl
|
|
|
|
#from comicarchive import ComicArchive
|
|
#from pageloader import PageLoader
|
|
from .volumeselectionwindow import VolumeSelectionWindow
|
|
from .comicarchive import MetaDataStyle
|
|
from .comicinfoxml import ComicInfoXml
|
|
from .genericmetadata import GenericMetadata
|
|
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
|
from .crediteditorwindow import CreditEditorWindow
|
|
from .settingswindow import SettingsWindow
|
|
from .settings import ComicTaggerSettings
|
|
from .pagebrowser import PageBrowserWindow
|
|
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 .exportwindow import ExportWindow, ExportConflictOpts
|
|
from .issueidentifier import IssueIdentifier
|
|
from .issuestring import IssueString
|
|
from .autotagstartwindow import AutoTagStartWindow
|
|
from .autotagprogresswindow import AutoTagProgressWindow
|
|
from .autotagmatchwindow import AutoTagMatchWindow
|
|
from .coverimagewidget import CoverImageWidget
|
|
from .versionchecker import VersionChecker
|
|
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
|
|
from . import utils
|
|
from . import ctversion
|
|
|
|
|
|
class OnlineMatchResults():
|
|
|
|
def __init__(self):
|
|
self.goodMatches = []
|
|
self.noMatches = []
|
|
self.multipleMatches = []
|
|
self.lowConfidenceMatches = []
|
|
self.writeFailures = []
|
|
self.fetchDataFailures = []
|
|
|
|
|
|
class MultipleMatch():
|
|
|
|
def __init__(self, ca, match_list):
|
|
self.ca = ca
|
|
self.matches = match_list
|
|
|
|
|
|
class TaggerWindow(QtWidgets.QMainWindow):
|
|
|
|
appName = "ComicTagger"
|
|
version = ctversion.version
|
|
|
|
def __init__(self, file_list, settings, parent=None, opts=None):
|
|
super(TaggerWindow, self).__init__(parent)
|
|
|
|
uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui'), self)
|
|
self.settings = settings
|
|
|
|
#----------------------------------
|
|
# prevent multiple instances
|
|
socket = QtNetwork.QLocalSocket(self)
|
|
socket.connectToServer(settings.install_id)
|
|
alive = socket.waitForConnected(3000)
|
|
if alive:
|
|
print((
|
|
"Another application with key [{}] is already running".format(
|
|
settings.install_id)))
|
|
# send file list to other instance
|
|
if len(file_list) > 0:
|
|
socket.write(pickle.dumps(file_list))
|
|
if not socket.waitForBytesWritten(3000):
|
|
print((socket.errorString().toLatin1()))
|
|
socket.disconnectFromServer()
|
|
sys.exit()
|
|
else:
|
|
# listen on a socket to prevent multiple instances
|
|
self.socketServer = QtNetwork.QLocalServer(self)
|
|
self.socketServer.newConnection.connect(
|
|
self.onIncomingSocketConnection)
|
|
ok = self.socketServer.listen(settings.install_id)
|
|
if not ok:
|
|
if self.socketServer.serverError(
|
|
) == QtNetwork.QAbstractSocket.AddressInUseError:
|
|
#print("Resetting unresponsive socket with key [{}]".format(settings.install_id))
|
|
self.socketServer.removeServer(settings.install_id)
|
|
ok = self.socketServer.listen(settings.install_id)
|
|
if not ok:
|
|
print((
|
|
"Cannot start local socket with key [{}]. Reason: %s ".format(
|
|
settings.install_id,
|
|
self.socketServer.errorString())))
|
|
sys.exit()
|
|
#print("Registering as single instance with key [{}]".format(settings.install_id))
|
|
#----------------------------------
|
|
|
|
self.archiveCoverWidget = CoverImageWidget(
|
|
self.coverImageContainer, CoverImageWidget.ArchiveMode)
|
|
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
|
|
gridlayout.addWidget(self.archiveCoverWidget)
|
|
gridlayout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.pageListEditor = PageListEditor(self.tabPages)
|
|
gridlayout = QtWidgets.QGridLayout(self.tabPages)
|
|
gridlayout.addWidget(self.pageListEditor)
|
|
|
|
#---------------------------
|
|
self.fileSelectionList = FileSelectionList(
|
|
self.widgetListHolder, self.settings)
|
|
gridlayout = QtWidgets.QGridLayout(self.widgetListHolder)
|
|
gridlayout.addWidget(self.fileSelectionList)
|
|
|
|
self.fileSelectionList.selectionChanged.connect(
|
|
self.fileListSelectionChanged)
|
|
self.fileSelectionList.listCleared.connect(self.fileListCleared)
|
|
self.fileSelectionList.setSorting(
|
|
self.settings.last_filelist_sorted_column,
|
|
self.settings.last_filelist_sorted_order)
|
|
|
|
# we can't specify relative font sizes in the UI designer, so
|
|
# walk through all the lablels in the main form, and make them
|
|
# a smidge smaller
|
|
for child in self.scrollAreaWidgetContents.children():
|
|
if (isinstance(child, QtWidgets.QLabel)):
|
|
f = child.font()
|
|
if f.pointSize() > 10:
|
|
f.setPointSize(f.pointSize() - 2)
|
|
f.setItalic(True)
|
|
child.setFont(f)
|
|
|
|
self.scrollAreaWidgetContents.adjustSize()
|
|
|
|
self.setWindowIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
|
|
|
|
if opts is not None and opts.data_style is not None and opts.data_style != MetaDataStyle.COMET:
|
|
# respect the command line option tag type
|
|
settings.last_selected_save_data_style = opts.data_style
|
|
settings.last_selected_load_data_style = opts.data_style
|
|
|
|
self.save_data_style = settings.last_selected_save_data_style
|
|
self.load_data_style = settings.last_selected_load_data_style
|
|
|
|
self.setAcceptDrops(True)
|
|
self.configMenus()
|
|
self.statusBar()
|
|
self.populateComboBoxes()
|
|
|
|
self.page_browser = None
|
|
self.resetApp()
|
|
|
|
# set up some basic field validators
|
|
validator = QtGui.QIntValidator(1900, datetime.date.today().year + 15, self)
|
|
self.lePubYear.setValidator(validator)
|
|
|
|
validator = QtGui.QIntValidator(1, 12, self)
|
|
self.lePubMonth.setValidator(validator)
|
|
|
|
# TODO: for now keep it simple, ideally we should check the full date
|
|
validator = QtGui.QIntValidator(1, 31, self)
|
|
self.lePubDay.setValidator(validator)
|
|
|
|
validator = QtGui.QIntValidator(1, 99999, self)
|
|
self.leIssueCount.setValidator(validator)
|
|
self.leVolumeNum.setValidator(validator)
|
|
self.leVolumeCount.setValidator(validator)
|
|
self.leAltIssueNum.setValidator(validator)
|
|
self.leAltIssueCount.setValidator(validator)
|
|
|
|
# TODO set up an RE validator for issueNum that allows
|
|
# for all sorts of wacky things
|
|
|
|
# tweak some control fonts
|
|
reduceWidgetFontSize(self.lblFilename, 1)
|
|
reduceWidgetFontSize(self.lblArchiveType)
|
|
reduceWidgetFontSize(self.lblTagList)
|
|
reduceWidgetFontSize(self.lblPageCount)
|
|
|
|
# make sure some editable comboboxes don't take drop actions
|
|
self.cbFormat.lineEdit().setAcceptDrops(False)
|
|
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
|
|
|
|
# hook up the callbacks
|
|
self.cbLoadDataStyle.currentIndexChanged.connect(self.setLoadDataStyle)
|
|
self.cbSaveDataStyle.currentIndexChanged.connect(self.setSaveDataStyle)
|
|
self.btnEditCredit.clicked.connect(self.editCredit)
|
|
self.btnAddCredit.clicked.connect(self.addCredit)
|
|
self.btnRemoveCredit.clicked.connect(self.removeCredit)
|
|
self.twCredits.cellDoubleClicked.connect(self.editCredit)
|
|
self.connectDirtyFlagSignals()
|
|
self.pageListEditor.modified.connect(self.setDirtyFlag)
|
|
self.pageListEditor.firstFrontCoverChanged.connect(
|
|
self.frontCoverChanged)
|
|
self.pageListEditor.listOrderChanged.connect(self.pageListOrderChanged)
|
|
self.tabWidget.currentChanged.connect(self.tabChanged)
|
|
|
|
self.updateStyleTweaks()
|
|
|
|
self.show()
|
|
self.setAppPosition()
|
|
if self.settings.last_form_side_width != -1:
|
|
self.splitter.setSizes(
|
|
[self.settings.last_form_side_width, self.settings.last_list_side_width])
|
|
self.raise_()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.resizeEvent(None)
|
|
|
|
self.splitter.splitterMoved.connect(self.splitterMovedEvent)
|
|
|
|
self.fileSelectionList.addAppAction(self.actionAutoIdentify)
|
|
self.fileSelectionList.addAppAction(self.actionAutoTag)
|
|
self.fileSelectionList.addAppAction(self.actionCopyTags)
|
|
self.fileSelectionList.addAppAction(self.actionRename)
|
|
self.fileSelectionList.addAppAction(self.actionRemoveAuto)
|
|
self.fileSelectionList.addAppAction(self.actionRepackage)
|
|
|
|
if len(file_list) != 0:
|
|
self.fileSelectionList.addPathList(file_list)
|
|
|
|
if self.settings.show_disclaimer:
|
|
checked = OptionalMessageDialog.msg(self, "Welcome!",
|
|
"""
|
|
Thanks for trying ComicTagger!<br><br>
|
|
Be aware that this is beta-level software, and consider it experimental.
|
|
You should use it very carefully when modifying your data files. As the
|
|
license says, it's "AS IS!"<br><br>
|
|
Also, be aware that writing tags to comic archives will change their file hashes,
|
|
which has implications with respect to other software packages. It's best to
|
|
use ComicTagger on local copies of your comics.<br><br>
|
|
Have fun!
|
|
"""
|
|
)
|
|
self.settings.show_disclaimer = not checked
|
|
|
|
if self.settings.check_for_new_version:
|
|
#self.checkLatestVersionOnline()
|
|
pass
|
|
|
|
def sigint_handler(self, *args):
|
|
# defer the actual close in the app loop thread
|
|
QtCore.QTimer.singleShot(200, self.close)
|
|
|
|
def resetApp(self):
|
|
|
|
self.archiveCoverWidget.clear()
|
|
self.comic_archive = None
|
|
self.dirtyFlag = False
|
|
self.clearForm()
|
|
self.pageListEditor.resetPage()
|
|
if self.page_browser is not None:
|
|
self.page_browser.reset()
|
|
self.updateAppTitle()
|
|
self.updateMenus()
|
|
self.updateInfoBox()
|
|
|
|
self.droppedFile = None
|
|
self.page_loader = None
|
|
|
|
def updateAppTitle(self):
|
|
|
|
self.setWindowIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
|
|
|
|
if self.comic_archive is None:
|
|
self.setWindowTitle(self.appName)
|
|
else:
|
|
mod_str = ""
|
|
ro_str = ""
|
|
|
|
if self.dirtyFlag:
|
|
mod_str = " [modified]"
|
|
|
|
if not self.comic_archive.isWritable():
|
|
ro_str = " [read only]"
|
|
|
|
self.setWindowTitle(
|
|
self.appName +
|
|
" - " +
|
|
self.comic_archive.path +
|
|
mod_str +
|
|
ro_str)
|
|
|
|
def configMenus(self):
|
|
|
|
# File Menu
|
|
self.actionExit.setShortcut('Ctrl+Q')
|
|
self.actionExit.setStatusTip('Exit application')
|
|
self.actionExit.triggered.connect(self.close)
|
|
|
|
self.actionLoad.setShortcut('Ctrl+O')
|
|
self.actionLoad.setStatusTip('Load comic archive')
|
|
self.actionLoad.triggered.connect(self.selectFile)
|
|
|
|
self.actionLoadFolder.setShortcut('Ctrl+Shift+O')
|
|
self.actionLoadFolder.setStatusTip('Load folder with comic archives')
|
|
self.actionLoadFolder.triggered.connect(self.selectFolder)
|
|
|
|
self.actionWrite_Tags.setShortcut('Ctrl+S')
|
|
self.actionWrite_Tags.setStatusTip('Save tags to comic archive')
|
|
self.actionWrite_Tags.triggered.connect(self.commitMetadata)
|
|
|
|
self.actionAutoTag.setShortcut('Ctrl+T')
|
|
self.actionAutoTag.setStatusTip('Auto-tag multiple archives')
|
|
self.actionAutoTag.triggered.connect(self.autoTag)
|
|
|
|
self.actionCopyTags.setShortcut('Ctrl+C')
|
|
self.actionCopyTags.setStatusTip('Copy one tag style to another')
|
|
self.actionCopyTags.triggered.connect(self.copyTags)
|
|
|
|
self.actionRemoveAuto.setShortcut('Ctrl+D')
|
|
self.actionRemoveAuto.setStatusTip(
|
|
'Remove currently selected modify tag style from the archive')
|
|
self.actionRemoveAuto.triggered.connect(self.removeAuto)
|
|
|
|
self.actionRemoveCBLTags.setStatusTip(
|
|
'Remove ComicBookLover tags from comic archive')
|
|
self.actionRemoveCBLTags.triggered.connect(self.removeCBLTags)
|
|
|
|
self.actionRemoveCRTags.setStatusTip(
|
|
'Remove ComicRack tags from comic archive')
|
|
self.actionRemoveCRTags.triggered.connect(self.removeCRTags)
|
|
|
|
self.actionViewRawCRTags.setStatusTip(
|
|
'View raw ComicRack tag block from file')
|
|
self.actionViewRawCRTags.triggered.connect(self.viewRawCRTags)
|
|
|
|
self.actionViewRawCBLTags.setStatusTip(
|
|
'View raw ComicBookLover tag block from file')
|
|
self.actionViewRawCBLTags.triggered.connect(self.viewRawCBLTags)
|
|
|
|
self.actionRepackage.setShortcut('Ctrl+E')
|
|
self.actionRepackage.setStatusTip('Re-create archive as CBZ')
|
|
self.actionRepackage.triggered.connect(self.repackageArchive)
|
|
|
|
self.actionRename.setShortcut('Ctrl+N')
|
|
self.actionRename.setStatusTip('Rename archive based on tags')
|
|
self.actionRename.triggered.connect(self.renameArchive)
|
|
|
|
self.actionSettings.setShortcut('Ctrl+Shift+S')
|
|
self.actionSettings.setStatusTip('Configure ComicTagger')
|
|
self.actionSettings.triggered.connect(self.showSettings)
|
|
|
|
# Tag Menu
|
|
self.actionParse_Filename.setShortcut('Ctrl+F')
|
|
self.actionParse_Filename.setStatusTip(
|
|
'Try to extract tags from filename')
|
|
self.actionParse_Filename.triggered.connect(self.useFilename)
|
|
|
|
self.actionSearchOnline.setShortcut('Ctrl+W')
|
|
self.actionSearchOnline.setStatusTip('Search online for tags')
|
|
self.actionSearchOnline.triggered.connect(self.queryOnline)
|
|
|
|
self.actionAutoIdentify.setShortcut('Ctrl+I')
|
|
self.actionAutoIdentify.triggered.connect(self.autoIdentifySearch)
|
|
|
|
self.actionApplyCBLTransform.setShortcut('Ctrl+L')
|
|
self.actionApplyCBLTransform.setStatusTip(
|
|
'Modify tags specifically for CBL format')
|
|
self.actionApplyCBLTransform.triggered.connect(self.applyCBLTransform)
|
|
|
|
self.actionClearEntryForm.setShortcut('Ctrl+Shift+C')
|
|
self.actionClearEntryForm.setStatusTip(
|
|
'Clear all the data on the screen')
|
|
self.actionClearEntryForm.triggered.connect(self.clearForm)
|
|
|
|
# Window Menu
|
|
self.actionPageBrowser.setShortcut('Ctrl+P')
|
|
self.actionPageBrowser.setStatusTip('Show the page browser')
|
|
self.actionPageBrowser.triggered.connect(self.showPageBrowser)
|
|
|
|
# Help Menu
|
|
self.actionAbout.setStatusTip('Show the ' + self.appName + ' info')
|
|
self.actionAbout.triggered.connect(self.aboutApp)
|
|
self.actionWiki.triggered.connect(self.showWiki)
|
|
self.actionReportBug.triggered.connect(self.reportBug)
|
|
self.actionComicTaggerForum.triggered.connect(self.showForum)
|
|
|
|
# ToolBar
|
|
|
|
self.actionLoad.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('open.png')))
|
|
self.actionLoadFolder.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('longbox.png')))
|
|
self.actionWrite_Tags.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('save.png')))
|
|
self.actionParse_Filename.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('parse.png')))
|
|
self.actionSearchOnline.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('search.png')))
|
|
self.actionAutoIdentify.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('auto.png')))
|
|
self.actionAutoTag.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('autotag.png')))
|
|
self.actionClearEntryForm.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('clear.png')))
|
|
self.actionPageBrowser.setIcon(
|
|
QtGui.QIcon(ComicTaggerSettings.getGraphic('browse.png')))
|
|
|
|
self.toolBar.addAction(self.actionLoad)
|
|
self.toolBar.addAction(self.actionLoadFolder)
|
|
self.toolBar.addAction(self.actionWrite_Tags)
|
|
self.toolBar.addAction(self.actionSearchOnline)
|
|
self.toolBar.addAction(self.actionAutoIdentify)
|
|
self.toolBar.addAction(self.actionAutoTag)
|
|
self.toolBar.addAction(self.actionClearEntryForm)
|
|
self.toolBar.addAction(self.actionPageBrowser)
|
|
|
|
def repackageArchive(self):
|
|
ca_list = self.fileSelectionList.getSelectedArchiveList()
|
|
rar_count = 0
|
|
for ca in ca_list:
|
|
if ca.isRar():
|
|
rar_count += 1
|
|
|
|
if rar_count == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self,
|
|
self.tr("Export as Zip Archive"),
|
|
self.tr("No RAR archives selected!"))
|
|
return
|
|
|
|
if not self.dirtyFlagVerification(
|
|
"Export as Zip Archive",
|
|
"If you export archives as Zip now, unsaved data in the form may be lost. Are you sure?"):
|
|
return
|
|
|
|
if rar_count != 0:
|
|
dlg = ExportWindow(
|
|
self,
|
|
self.settings,
|
|
self.tr(
|
|
"You have selected {0} archive(s) to export to Zip format. New archives will be created in the same folder as the original.\n\nPlease choose options below, and select OK.\n".format(rar_count)))
|
|
dlg.adjustSize()
|
|
dlg.setModal(True)
|
|
if not dlg.exec_():
|
|
return
|
|
|
|
progdialog = QtWidgets.QProgressDialog(
|
|
"", "Cancel", 0, rar_count, self)
|
|
progdialog.setWindowTitle("Exporting as ZIP")
|
|
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
|
progdialog.setMinimumDuration(300)
|
|
centerWindowOnParent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
prog_idx = 0
|
|
|
|
new_archives_to_add = []
|
|
archives_to_remove = []
|
|
skipped_list = []
|
|
failed_list = []
|
|
success_count = 0
|
|
|
|
for ca in ca_list:
|
|
if ca.isRar():
|
|
QtCore.QCoreApplication.processEvents()
|
|
if progdialog.wasCanceled():
|
|
break
|
|
prog_idx += 1
|
|
progdialog.setValue(prog_idx)
|
|
progdialog.setLabelText(ca.path)
|
|
centerWindowOnParent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
original_path = os.path.abspath(ca.path)
|
|
export_name = os.path.splitext(original_path)[0] + ".cbz"
|
|
|
|
if os.path.lexists(export_name):
|
|
if dlg.fileConflictBehavior == ExportConflictOpts.dontCreate:
|
|
export_name = None
|
|
skipped_list.append(ca.path)
|
|
elif dlg.fileConflictBehavior == ExportConflictOpts.createUnique:
|
|
export_name = utils.unique_file(export_name)
|
|
|
|
if export_name is not None:
|
|
if ca.exportAsZip(export_name):
|
|
success_count += 1
|
|
if dlg.addToList:
|
|
new_archives_to_add.append(export_name)
|
|
if dlg.deleteOriginal:
|
|
archives_to_remove.append(ca)
|
|
os.unlink(ca.path)
|
|
|
|
else:
|
|
# last export failed, so remove the zip, if it
|
|
# exists
|
|
failed_list.append(ca.path)
|
|
if os.path.lexists(export_name):
|
|
os.remove(export_name)
|
|
|
|
progdialog.hide()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.fileSelectionList.addPathList(new_archives_to_add)
|
|
self.fileSelectionList.removeArchiveList(archives_to_remove)
|
|
|
|
summary = "Successfully created {0} Zip archive(s).".format(
|
|
success_count)
|
|
if len(skipped_list) > 0:
|
|
summary += "\n\nThe following {0} RAR archive(s) were skipped due to file name conflicts:\n".format(
|
|
len(skipped_list))
|
|
for f in skipped_list:
|
|
summary += "\t{0}\n".format(f)
|
|
if len(failed_list) > 0:
|
|
summary += "\n\nThe following {0} RAR archive(s) failed to export due to read/write errors:\n".format(
|
|
len(failed_list))
|
|
for f in failed_list:
|
|
summary += "\t{0}\n".format(f)
|
|
|
|
dlg = LogWindow(self)
|
|
dlg.setText(summary)
|
|
dlg.setWindowTitle("Archive Export to Zip Summary")
|
|
dlg.exec_()
|
|
|
|
def aboutApp(self):
|
|
|
|
website = "https://github.com/davide-romanini/comictagger"
|
|
email = "comictagger@gmail.com"
|
|
license_link = "http://www.apache.org/licenses/LICENSE-2.0"
|
|
license_name = "Apache License 2.0"
|
|
|
|
msgBox = QtWidgets.QMessageBox()
|
|
msgBox.setWindowTitle(self.tr("About " + self.appName))
|
|
msgBox.setTextFormat(QtCore.Qt.RichText)
|
|
msgBox.setIconPixmap(
|
|
QtGui.QPixmap(ComicTaggerSettings.getGraphic('about.png')))
|
|
msgBox.setText("<br><br><br>" +
|
|
self.appName +
|
|
" v" +
|
|
self.version +
|
|
"<br>" +
|
|
"©2014-2018 ComicTagger Devs<br><br>" +
|
|
"<a href='{0}'>{0}</a><br><br>".format(website) +
|
|
"<a href='mailto:{0}'>{0}</a><br><br>".format(email) +
|
|
"License: <a href='{0}'>{1}</a>".format(license_link, license_name))
|
|
|
|
msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok)
|
|
msgBox.exec_()
|
|
|
|
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(self.getUrlFromLocalFileID(url))
|
|
|
|
if self.droppedFiles is not None:
|
|
event.accept()
|
|
|
|
# http://stackoverflow.com/questions/34689562/pyqt-mimedata-filename
|
|
def getUrlFromLocalFileID(self, localFileID):
|
|
import sys
|
|
if not sys.platform == 'darwin':
|
|
return localFileID.toLocalFile()
|
|
|
|
import objc
|
|
import CoreFoundation as CF
|
|
localFileQString = QString(localFileID.toLocalFile())
|
|
relCFStringRef = CF.CFStringCreateWithCString(
|
|
CF.kCFAllocatorDefault,
|
|
localFileQString.toUtf8(),
|
|
CF.kCFStringEncodingUTF8
|
|
)
|
|
relCFURL = CF.CFURLCreateWithFileSystemPath(
|
|
CF.kCFAllocatorDefault,
|
|
relCFStringRef,
|
|
CF.kCFURLPOSIXPathStyle,
|
|
False # is directory
|
|
)
|
|
absCFURL = CF.CFURLCreateFilePathURL(
|
|
CF.kCFAllocatorDefault,
|
|
relCFURL,
|
|
objc.NULL
|
|
)
|
|
|
|
local = QUrl(str(absCFURL[0])).toLocalFile()
|
|
|
|
return local
|
|
|
|
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.fileSelectionList.addPathList(self.droppedFiles)
|
|
event.accept()
|
|
|
|
def actualLoadCurrentArchive(self):
|
|
if self.metadata.isEmpty:
|
|
self.metadata = self.comic_archive.metadataFromFilename(
|
|
self.settings.parse_scan_info)
|
|
if len(self.metadata.pages) == 0:
|
|
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.updateAppTitle()
|
|
|
|
def updateCoverImage(self):
|
|
cover_idx = self.metadata.getCoverPageIndexList()[0]
|
|
self.archiveCoverWidget.setArchive(self.comic_archive, cover_idx)
|
|
|
|
def updateMenus(self):
|
|
|
|
# First just disable all the questionable items
|
|
self.actionAutoTag.setEnabled(False)
|
|
self.actionCopyTags.setEnabled(False)
|
|
self.actionRemoveAuto.setEnabled(False)
|
|
self.actionRemoveCRTags.setEnabled(False)
|
|
self.actionRemoveCBLTags.setEnabled(False)
|
|
self.actionWrite_Tags.setEnabled(False)
|
|
self.actionRepackage.setEnabled(False)
|
|
self.actionViewRawCBLTags.setEnabled(False)
|
|
self.actionViewRawCRTags.setEnabled(False)
|
|
self.actionParse_Filename.setEnabled(False)
|
|
self.actionAutoIdentify.setEnabled(False)
|
|
self.actionRename.setEnabled(False)
|
|
self.actionApplyCBLTransform.setEnabled(False)
|
|
|
|
# now, selectively re-enable
|
|
if self.comic_archive is not None:
|
|
has_cix = self.comic_archive.hasCIX()
|
|
has_cbi = self.comic_archive.hasCBI()
|
|
|
|
self.actionParse_Filename.setEnabled(True)
|
|
self.actionAutoIdentify.setEnabled(True)
|
|
self.actionAutoTag.setEnabled(True)
|
|
self.actionRename.setEnabled(True)
|
|
self.actionApplyCBLTransform.setEnabled(True)
|
|
self.actionRepackage.setEnabled(True)
|
|
self.actionRemoveAuto.setEnabled(True)
|
|
self.actionRemoveCRTags.setEnabled(True)
|
|
self.actionRemoveCBLTags.setEnabled(True)
|
|
self.actionCopyTags.setEnabled(True)
|
|
|
|
if has_cix:
|
|
self.actionViewRawCRTags.setEnabled(True)
|
|
if has_cbi:
|
|
self.actionViewRawCBLTags.setEnabled(True)
|
|
|
|
if self.comic_archive.isWritable():
|
|
self.actionWrite_Tags.setEnabled(True)
|
|
|
|
def updateInfoBox(self):
|
|
|
|
ca = self.comic_archive
|
|
|
|
if ca is None:
|
|
self.lblFilename.setText("")
|
|
self.lblArchiveType.setText("")
|
|
self.lblTagList.setText("")
|
|
self.lblPageCount.setText("")
|
|
return
|
|
|
|
filename = os.path.basename(ca.path)
|
|
filename = os.path.splitext(filename)[0]
|
|
filename = FileNameParser().fixSpaces(filename, False)
|
|
|
|
self.lblFilename.setText(filename)
|
|
|
|
if ca.isZip():
|
|
self.lblArchiveType.setText("ZIP archive")
|
|
elif ca.isRar():
|
|
self.lblArchiveType.setText("RAR archive")
|
|
elif ca.isFolder():
|
|
self.lblArchiveType.setText("Folder archive")
|
|
else:
|
|
self.lblArchiveType.setText("")
|
|
|
|
page_count = " ({0} pages)".format(ca.getNumberOfPages())
|
|
self.lblPageCount.setText(page_count)
|
|
|
|
tag_info = ""
|
|
if ca.hasCIX():
|
|
tag_info = "• ComicRack tags"
|
|
if ca.hasCBI():
|
|
if tag_info != "":
|
|
tag_info += "\n"
|
|
tag_info += "• ComicBookLover tags"
|
|
|
|
self.lblTagList.setText(tag_info)
|
|
|
|
def setDirtyFlag(self, param1=None, param2=None, param3=None):
|
|
if not self.dirtyFlag:
|
|
self.dirtyFlag = True
|
|
self.fileSelectionList.setModifiedFlag(True)
|
|
self.updateAppTitle()
|
|
|
|
def clearDirtyFlag(self):
|
|
if self.dirtyFlag:
|
|
self.dirtyFlag = False
|
|
self.fileSelectionList.setModifiedFlag(False)
|
|
self.updateAppTitle()
|
|
|
|
def connectDirtyFlagSignals(self):
|
|
# recursively connect the tab form child slots
|
|
self.connectChildDirtyFlagSignals(self.tabWidget)
|
|
|
|
def connectChildDirtyFlagSignals(self, widget):
|
|
|
|
if (isinstance(widget, QtWidgets.QLineEdit)):
|
|
widget.textEdited.connect(self.setDirtyFlag)
|
|
if (isinstance(widget, QtWidgets.QTextEdit)):
|
|
widget.textChanged.connect(self.setDirtyFlag)
|
|
if (isinstance(widget, QtWidgets.QComboBox)):
|
|
widget.currentIndexChanged.connect(self.setDirtyFlag)
|
|
if (isinstance(widget, QtWidgets.QCheckBox)):
|
|
widget.stateChanged.connect(self.setDirtyFlag)
|
|
|
|
# recursive call on chillun
|
|
for child in widget.children():
|
|
if child != self.pageListEditor:
|
|
self.connectChildDirtyFlagSignals(child)
|
|
|
|
def clearForm(self):
|
|
|
|
# get a minty fresh metadata object
|
|
self.metadata = GenericMetadata()
|
|
if self.comic_archive is not None:
|
|
self.metadata.setDefaultPageList(
|
|
self.comic_archive.getNumberOfPages())
|
|
|
|
# recursively clear the tab form
|
|
self.clearChildren(self.tabWidget)
|
|
|
|
# clear the dirty flag, since there is nothing in there now to lose
|
|
self.clearDirtyFlag()
|
|
|
|
self.pageListEditor.setData(self.comic_archive, self.metadata.pages)
|
|
|
|
def clearChildren(self, widget):
|
|
|
|
if (isinstance(widget, QtWidgets.QLineEdit) or
|
|
isinstance(widget, QtWidgets.QTextEdit)):
|
|
widget.setText("")
|
|
if (isinstance(widget, QtWidgets.QComboBox)):
|
|
widget.setCurrentIndex(0)
|
|
if (isinstance(widget, QtWidgets.QCheckBox)):
|
|
widget.setChecked(False)
|
|
if (isinstance(widget, QtWidgets.QTableWidget)):
|
|
while widget.rowCount() > 0:
|
|
widget.removeRow(0)
|
|
|
|
# recursive call on chillun
|
|
for child in widget.children():
|
|
self.clearChildren(child)
|
|
|
|
# Copy all of the metadata object into to the form.
|
|
# Merging of metadata should be done via the overlay function
|
|
def metadataToForm(self):
|
|
def assignText(field, value):
|
|
if value is not None:
|
|
field.setText(str(value))
|
|
md = self.metadata
|
|
|
|
assignText(self.leSeries, md.series)
|
|
assignText(self.leIssueNum, md.issue)
|
|
assignText(self.leIssueCount, md.issueCount)
|
|
assignText(self.leVolumeNum, md.volume)
|
|
assignText(self.leVolumeCount, md.volumeCount)
|
|
assignText(self.leTitle, md.title)
|
|
assignText(self.lePublisher, md.publisher)
|
|
assignText(self.lePubMonth, md.month)
|
|
assignText(self.lePubYear, md.year)
|
|
assignText(self.lePubDay, md.day)
|
|
assignText(self.leGenre, md.genre)
|
|
assignText(self.leImprint, md.imprint)
|
|
assignText(self.teComments, md.comments)
|
|
assignText(self.teNotes, md.notes)
|
|
assignText(self.leCriticalRating, md.criticalRating)
|
|
assignText(self.leStoryArc, md.storyArc)
|
|
assignText(self.leScanInfo, md.scanInfo)
|
|
assignText(self.leSeriesGroup, md.seriesGroup)
|
|
assignText(self.leAltSeries, md.alternateSeries)
|
|
assignText(self.leAltIssueNum, md.alternateNumber)
|
|
assignText(self.leAltIssueCount, md.alternateCount)
|
|
assignText(self.leWebLink, md.webLink)
|
|
assignText(self.teCharacters, md.characters)
|
|
assignText(self.teTeams, md.teams)
|
|
assignText(self.teLocations, md.locations)
|
|
|
|
if md.format is not None and md.format != "":
|
|
i = self.cbFormat.findText(md.format)
|
|
if i == -1:
|
|
self.cbFormat.setEditText(md.format)
|
|
else:
|
|
self.cbFormat.setCurrentIndex(i)
|
|
|
|
if md.maturityRating is not None and md.maturityRating != "":
|
|
i = self.cbMaturityRating.findText(md.maturityRating)
|
|
if i == -1:
|
|
self.cbMaturityRating.setEditText(md.maturityRating)
|
|
else:
|
|
self.cbMaturityRating.setCurrentIndex(i)
|
|
else:
|
|
self.cbMaturityRating.setCurrentIndex(0)
|
|
|
|
if md.language is not None:
|
|
i = self.cbLanguage.findData(md.language)
|
|
self.cbLanguage.setCurrentIndex(i)
|
|
else:
|
|
self.cbLanguage.setCurrentIndex(0)
|
|
|
|
if md.country is not None:
|
|
i = self.cbCountry.findText(md.country)
|
|
self.cbCountry.setCurrentIndex(i)
|
|
else:
|
|
self.cbCountry.setCurrentIndex(0)
|
|
|
|
if md.manga is not None:
|
|
i = self.cbManga.findData(md.manga)
|
|
self.cbManga.setCurrentIndex(i)
|
|
else:
|
|
self.cbManga.setCurrentIndex(0)
|
|
|
|
if md.blackAndWhite != None and md.blackAndWhite:
|
|
self.cbBW.setChecked(True)
|
|
else:
|
|
self.cbBW.setChecked(False)
|
|
|
|
self.teTags.setText(utils.listToString(md.tags))
|
|
|
|
# !!! Should we clear the credits table or just avoid duplicates?
|
|
while self.twCredits.rowCount() > 0:
|
|
self.twCredits.removeRow(0)
|
|
|
|
if md.credits is not None and len(md.credits) != 0:
|
|
|
|
self.twCredits.setSortingEnabled(False)
|
|
|
|
row = 0
|
|
for credit in md.credits:
|
|
# if the role-person pair already exists, just skip adding it
|
|
# to the list
|
|
if self.isDupeCredit(credit['role'].title(), credit['person']):
|
|
continue
|
|
|
|
self.addNewCreditEntry(
|
|
row,
|
|
credit['role'].title(),
|
|
credit['person'],
|
|
(credit['primary'] if 'primary' in credit else False))
|
|
|
|
row += 1
|
|
|
|
self.twCredits.setSortingEnabled(True)
|
|
self.updateCreditColors()
|
|
|
|
def addNewCreditEntry(self, row, role, name, primary_flag=False):
|
|
self.twCredits.insertRow(row)
|
|
|
|
item_text = role
|
|
item = QtWidgets.QTableWidgetItem(item_text)
|
|
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
|
self.twCredits.setItem(row, 1, item)
|
|
|
|
item_text = name
|
|
item = QtWidgets.QTableWidgetItem(item_text)
|
|
item.setData(QtCore.Qt.ToolTipRole, item_text)
|
|
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
self.twCredits.setItem(row, 2, item)
|
|
|
|
item = QtWidgets.QTableWidgetItem("")
|
|
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
self.twCredits.setItem(row, 0, item)
|
|
self.updateCreditPrimaryFlag(row, primary_flag)
|
|
|
|
def isDupeCredit(self, role, name):
|
|
r = 0
|
|
while r < self.twCredits.rowCount():
|
|
if (self.twCredits.item(r, 1).text() == role and
|
|
self.twCredits.item(r, 2).text() == name):
|
|
return True
|
|
r = r + 1
|
|
|
|
return False
|
|
|
|
def formToMetadata(self):
|
|
# copy the data from the form into the metadata
|
|
md = GenericMetadata()
|
|
md.isEmpty = False
|
|
md.alternateNumber = IssueString(self.leAltIssueNum.text()).asString()
|
|
md.issue = IssueString(self.leIssueNum.text()).asString()
|
|
md.issueCount = utils.xlate(self.leIssueCount.text(), True)
|
|
md.volume = utils.xlate(self.leVolumeNum.text(), True)
|
|
md.volumeCount = utils.xlate(self.leVolumeCount.text(), True)
|
|
md.month = utils.xlate(self.lePubMonth.text(), True)
|
|
md.year = utils.xlate(self.lePubYear.text(), True)
|
|
md.day = utils.xlate(self.lePubDay.text(), True)
|
|
md.criticalRating = utils.xlate(self.leCriticalRating.text(), True)
|
|
md.alternateCount = utils.xlate(self.leAltIssueCount.text(), True)
|
|
|
|
md.series = self.leSeries.text()
|
|
md.title = self.leTitle.text()
|
|
md.publisher = self.lePublisher.text()
|
|
md.genre = self.leGenre.text()
|
|
md.imprint = self.leImprint.text()
|
|
md.comments = self.teComments.toPlainText()
|
|
md.notes = self.teNotes.toPlainText()
|
|
md.maturityRating = self.cbMaturityRating.currentText()
|
|
|
|
md.storyArc = self.leStoryArc.text()
|
|
md.scanInfo = self.leScanInfo.text()
|
|
md.seriesGroup = self.leSeriesGroup.text()
|
|
md.alternateSeries = self.leAltSeries.text()
|
|
md.webLink = self.leWebLink.text()
|
|
md.characters = self.teCharacters.toPlainText()
|
|
md.teams = self.teTeams.toPlainText()
|
|
md.locations = self.teLocations.toPlainText()
|
|
|
|
md.format = self.cbFormat.currentText()
|
|
md.country = self.cbCountry.currentText()
|
|
|
|
md.language = utils.xlate(self.cbLanguage.itemData(self.cbLanguage.currentIndex()))
|
|
|
|
md.manga = utils.xlate(self.cbManga.itemData(self.cbManga.currentIndex()))
|
|
|
|
# Make a list from the coma delimited tags string
|
|
tmp = self.teTags.toPlainText()
|
|
if tmp is not None:
|
|
def striplist(l):
|
|
return([x.strip() for x in l])
|
|
|
|
md.tags = striplist(tmp.split(","))
|
|
|
|
if (self.cbBW.isChecked()):
|
|
md.blackAndWhite = True
|
|
else:
|
|
md.blackAndWhite = False
|
|
|
|
# get the credits from the table
|
|
md.credits = list()
|
|
row = 0
|
|
while row < self.twCredits.rowCount():
|
|
role = "{0}".format(self.twCredits.item(row, 1).text())
|
|
name = "{0}".format(self.twCredits.item(row, 2).text())
|
|
primary_flag = self.twCredits.item(row, 0).text() != ""
|
|
|
|
md.addCredit(name, role, bool(primary_flag))
|
|
row += 1
|
|
|
|
md.pages = self.pageListEditor.getPageList()
|
|
self.metadata = md
|
|
|
|
def useFilename(self):
|
|
if self.comic_archive is not None:
|
|
# copy the form onto metadata object
|
|
self.formToMetadata()
|
|
new_metadata = self.comic_archive.metadataFromFilename(
|
|
self.settings.parse_scan_info)
|
|
if new_metadata is not None:
|
|
self.metadata.overlay(new_metadata)
|
|
self.metadataToForm()
|
|
|
|
def selectFolder(self):
|
|
self.selectFile(folder_mode=True)
|
|
|
|
def selectFile(self, folder_mode=False):
|
|
|
|
dialog = QtWidgets.QFileDialog(self)
|
|
if folder_mode:
|
|
dialog.setFileMode(QtWidgets.QFileDialog.Directory)
|
|
else:
|
|
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
|
|
|
|
if self.settings.last_opened_folder is not None:
|
|
dialog.setDirectory(self.settings.last_opened_folder)
|
|
# dialog.setFileMode(QtWidgets.QFileDialog.Directory)
|
|
|
|
if not folder_mode:
|
|
if platform.system() != "Windows" and utils.which("unrar") is None:
|
|
archive_filter = "Comic archive files (*.cbz *.zip)"
|
|
else:
|
|
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar)"
|
|
filters = [
|
|
archive_filter,
|
|
"Any files (*)"
|
|
]
|
|
dialog.setNameFilters(filters)
|
|
|
|
if (dialog.exec_()):
|
|
fileList = dialog.selectedFiles()
|
|
# if self.dirtyFlagVerification("Open Archive",
|
|
# "If you open a new archive now, data in the form will be lost. Are you sure?"):
|
|
self.fileSelectionList.addPathList(fileList)
|
|
|
|
def autoIdentifySearch(self):
|
|
if self.comic_archive is None:
|
|
QtWidgets.QMessageBox.warning(
|
|
self,
|
|
self.tr("Automatic Identify Search"),
|
|
self.tr("You need to load a comic first!"))
|
|
return
|
|
|
|
self.queryOnline(autoselect=True)
|
|
|
|
def queryOnline(self, autoselect=False):
|
|
|
|
issue_number = str(self.leIssueNum.text()).strip()
|
|
|
|
if autoselect and issue_number == "":
|
|
QtWidgets.QMessageBox.information(
|
|
self,
|
|
"Automatic Identify Search",
|
|
"Can't auto-identify without an issue number (yet!)")
|
|
return
|
|
|
|
if str(self.leSeries.text()).strip() != "":
|
|
series_name = str(self.leSeries.text()).strip()
|
|
else:
|
|
QtWidgets.QMessageBox.information(
|
|
self,
|
|
self.tr("Online Search"),
|
|
self.tr("Need to enter a series name to search."))
|
|
return
|
|
|
|
year = str(self.lePubYear.text()).strip()
|
|
if year == "":
|
|
year = None
|
|
|
|
issue_count = str(self.leIssueCount.text()).strip()
|
|
if issue_count == "":
|
|
issue_count = None
|
|
|
|
cover_index_list = self.metadata.getCoverPageIndexList()
|
|
selector = VolumeSelectionWindow(
|
|
self,
|
|
series_name,
|
|
issue_number,
|
|
year,
|
|
issue_count,
|
|
cover_index_list,
|
|
self.comic_archive,
|
|
self.settings,
|
|
autoselect)
|
|
|
|
title = "Search: '" + series_name + "' - "
|
|
selector.setWindowTitle(title + "Select Series")
|
|
|
|
selector.setModal(True)
|
|
selector.exec_()
|
|
|
|
if selector.result():
|
|
# we should now have a volume ID
|
|
QtWidgets.QApplication.setOverrideCursor(
|
|
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
|
|
|
# copy the form onto metadata object
|
|
self.formToMetadata()
|
|
|
|
try:
|
|
comicVine = ComicVineTalker()
|
|
new_metadata = comicVine.fetchIssueData(
|
|
selector.volume_id, selector.issue_number, self.settings)
|
|
except ComicVineTalkerException as e:
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
if e.code == ComicVineTalkerException.RateLimit:
|
|
QtWidgets.QMessageBox.critical(
|
|
self,
|
|
self.tr("Comic Vine Error"),
|
|
ComicVineTalker.getRateLimitMessage())
|
|
else:
|
|
QtWidgets.QMessageBox.critical(
|
|
self,
|
|
self.tr("Network Issue"),
|
|
self.tr("Could not connect to Comic Vine to get issue details.!"))
|
|
else:
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
if new_metadata is not None:
|
|
|
|
if self.settings.apply_cbl_transform_on_cv_import:
|
|
new_metadata = CBLTransformer(
|
|
new_metadata, self.settings).apply()
|
|
|
|
if self.settings.clear_form_before_populating_from_cv:
|
|
self.clearForm()
|
|
|
|
self.metadata.overlay(new_metadata)
|
|
# Now push the new combined data into the edit controls
|
|
self.metadataToForm()
|
|
else:
|
|
QtWidgets.QMessageBox.critical(
|
|
self, self.tr("Search"), self.tr(
|
|
"Could not find an issue {0} for that series".format(
|
|
selector.issue_number)))
|
|
|
|
def commitMetadata(self):
|
|
|
|
if (self.metadata is not None and self.comic_archive is not None):
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
self.tr("Save Tags"),
|
|
self.tr(
|
|
"Are you sure you wish to save " +
|
|
MetaDataStyle.name[
|
|
self.save_data_style] +
|
|
" tags to this archive?"),
|
|
QtWidgets.QMessageBox.Yes,
|
|
QtWidgets.QMessageBox.No)
|
|
|
|
if reply == QtWidgets.QMessageBox.Yes:
|
|
QtWidgets.QApplication.setOverrideCursor(
|
|
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
|
self.formToMetadata()
|
|
|
|
success = self.comic_archive.writeMetadata(
|
|
self.metadata, self.save_data_style)
|
|
self.comic_archive.loadCache(
|
|
[MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
|
|
if not success:
|
|
QtWidgets.QMessageBox.warning(
|
|
self,
|
|
self.tr("Save failed"),
|
|
self.tr("The tag save operation seemed to fail!"))
|
|
else:
|
|
self.clearDirtyFlag()
|
|
self.updateInfoBox()
|
|
self.updateMenus()
|
|
#QtWidgets.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
|
|
self.fileSelectionList.updateCurrentRow()
|
|
|
|
else:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Whoops!"), self.tr("No data to commit!"))
|
|
|
|
def setLoadDataStyle(self, s):
|
|
if self.dirtyFlagVerification(
|
|
"Change Tag Read Style",
|
|
"If you change read tag style now, data in the form will be lost. Are you sure?"):
|
|
self.load_data_style = self.cbLoadDataStyle.itemData(s)
|
|
self.settings.last_selected_load_data_style = self.load_data_style
|
|
self.updateMenus()
|
|
if self.comic_archive is not None:
|
|
self.loadArchive(self.comic_archive)
|
|
else:
|
|
self.cbLoadDataStyle.currentIndexChanged.disconnect(
|
|
self.setLoadDataStyle)
|
|
self.adjustLoadStyleCombo()
|
|
self.cbLoadDataStyle.currentIndexChanged.connect(
|
|
self.setLoadDataStyle)
|
|
|
|
def setSaveDataStyle(self, s):
|
|
self.save_data_style = self.cbSaveDataStyle.itemData(s)
|
|
|
|
self.settings.last_selected_save_data_style = self.save_data_style
|
|
self.updateStyleTweaks()
|
|
self.updateMenus()
|
|
|
|
def updateCreditColors(self):
|
|
#!!!ATB qt5 porting TODO
|
|
#return
|
|
inactive_color = QtGui.QColor(255, 170, 150)
|
|
active_palette = self.leSeries.palette()
|
|
active_color = active_palette.color(QtGui.QPalette.Base)
|
|
|
|
inactive_brush = QtGui.QBrush(inactive_color)
|
|
active_brush = QtGui.QBrush(active_color)
|
|
|
|
cix_credits = ComicInfoXml().getParseableCredits()
|
|
|
|
if self.save_data_style == MetaDataStyle.CIX:
|
|
# loop over credit table, mark selected rows
|
|
r = 0
|
|
while r < self.twCredits.rowCount():
|
|
if str(self.twCredits.item(r, 1).text()
|
|
).lower() not in cix_credits:
|
|
self.twCredits.item(
|
|
r, 1).setBackground(inactive_brush)
|
|
else:
|
|
self.twCredits.item(r, 1).setBackground(active_brush)
|
|
# turn off entire primary column
|
|
self.twCredits.item(r, 0).setBackground(inactive_brush)
|
|
r = r + 1
|
|
|
|
if self.save_data_style == MetaDataStyle.CBI:
|
|
# loop over credit table, make all active color
|
|
r = 0
|
|
while r < self.twCredits.rowCount():
|
|
self.twCredits.item(r, 0).setBackground(active_brush)
|
|
self.twCredits.item(r, 1).setBackground(active_brush)
|
|
r = r + 1
|
|
|
|
def updateStyleTweaks(self):
|
|
|
|
# depending on the current data style, certain fields are disabled
|
|
|
|
inactive_color = QtGui.QColor(255, 170, 150)
|
|
active_palette = self.leSeries.palette()
|
|
|
|
inactive_palette1 = self.leSeries.palette()
|
|
inactive_palette1.setColor(QtGui.QPalette.Base, inactive_color)
|
|
|
|
inactive_palette2 = self.leSeries.palette()
|
|
|
|
inactive_palette3 = self.leSeries.palette()
|
|
inactive_palette3.setColor(QtGui.QPalette.Base, inactive_color)
|
|
|
|
inactive_palette3.setColor(QtGui.QPalette.Base, inactive_color)
|
|
|
|
# helper func
|
|
def enableWidget(item, enable):
|
|
inactive_palette3.setColor(item.backgroundRole(), inactive_color)
|
|
inactive_palette2.setColor(item.backgroundRole(), inactive_color)
|
|
inactive_palette3.setColor(item.foregroundRole(), inactive_color)
|
|
|
|
if enable:
|
|
item.setPalette(active_palette)
|
|
item.setAutoFillBackground(False)
|
|
if isinstance(item, QtWidgets.QCheckBox):
|
|
item.setEnabled(True)
|
|
elif isinstance(item, QtWidgets.QComboBox):
|
|
item.setEnabled(True)
|
|
else:
|
|
item.setReadOnly(False)
|
|
else:
|
|
item.setAutoFillBackground(True)
|
|
if isinstance(item, QtWidgets.QCheckBox):
|
|
item.setPalette(inactive_palette2)
|
|
item.setEnabled(False)
|
|
elif isinstance(item, QtWidgets.QComboBox):
|
|
item.setPalette(inactive_palette3)
|
|
item.setEnabled(False)
|
|
else:
|
|
item.setReadOnly(True)
|
|
item.setPalette(inactive_palette1)
|
|
|
|
cbi_only = [self.leVolumeCount, self.cbCountry,
|
|
self.leCriticalRating, self.teTags]
|
|
cix_only = [
|
|
self.leImprint, self.teNotes, self.cbBW, self.cbManga,
|
|
self.leStoryArc, self.leScanInfo, self.leSeriesGroup,
|
|
self.leAltSeries, self.leAltIssueNum, self.leAltIssueCount,
|
|
self.leWebLink, self.teCharacters, self.teTeams,
|
|
self.teLocations, self.cbMaturityRating, self.cbFormat
|
|
]
|
|
|
|
if self.save_data_style == MetaDataStyle.CIX:
|
|
for item in cix_only:
|
|
enableWidget(item, True)
|
|
for item in cbi_only:
|
|
enableWidget(item, False)
|
|
|
|
if self.save_data_style == MetaDataStyle.CBI:
|
|
for item in cbi_only:
|
|
enableWidget(item, True)
|
|
for item in cix_only:
|
|
enableWidget(item, False)
|
|
|
|
self.updateCreditColors()
|
|
self.pageListEditor.setMetadataStyle(self.save_data_style)
|
|
|
|
def cellDoubleClicked(self, r, c):
|
|
self.editCredit()
|
|
|
|
def addCredit(self):
|
|
self.modifyCredits("add")
|
|
|
|
def editCredit(self):
|
|
if (self.twCredits.currentRow() > -1):
|
|
self.modifyCredits("edit")
|
|
|
|
def updateCreditPrimaryFlag(self, row, primary):
|
|
|
|
# if we're clearing a flagm do it and quit
|
|
if not primary:
|
|
self.twCredits.item(row, 0).setText("")
|
|
return
|
|
|
|
# otherwise, we need to check for, and clear, other primaries with same
|
|
# role
|
|
role = str(self.twCredits.item(row, 1).text())
|
|
r = 0
|
|
while r < self.twCredits.rowCount():
|
|
if (self.twCredits.item(r, 0).text() != "" and str(
|
|
self.twCredits.item(r, 1).text()).lower() == role.lower()):
|
|
self.twCredits.item(r, 0).setText("")
|
|
r = r + 1
|
|
|
|
# Now set our new primary
|
|
self.twCredits.item(row, 0).setText("Yes")
|
|
|
|
def modifyCredits(self, action):
|
|
|
|
if action == "edit":
|
|
row = self.twCredits.currentRow()
|
|
role = self.twCredits.item(row, 1).text()
|
|
name = self.twCredits.item(row, 2).text()
|
|
primary = self.twCredits.item(row, 0).text() != ""
|
|
else:
|
|
role = ""
|
|
name = ""
|
|
primary = False
|
|
|
|
editor = CreditEditorWindow(
|
|
self, CreditEditorWindow.ModeEdit, role, name, primary)
|
|
editor.setModal(True)
|
|
editor.exec_()
|
|
if editor.result():
|
|
new_role, new_name, new_primary = editor.getCredits()
|
|
|
|
if new_name == name and new_role == role and new_primary == primary:
|
|
# nothing has changed, just quit
|
|
return
|
|
|
|
# name and role is the same, but primary flag changed
|
|
if new_name == name and new_role == role:
|
|
self.updateCreditPrimaryFlag(row, new_primary)
|
|
return
|
|
|
|
# check for dupes
|
|
ok_to_mod = True
|
|
if self.isDupeCredit(new_role, new_name):
|
|
# delete the dupe credit from list
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
self.tr("Duplicate Credit!"),
|
|
self.tr(
|
|
"This will create a duplicate credit entry. Would you like to merge the entries, or create a duplicate?"),
|
|
self.tr("Merge"),
|
|
self.tr("Duplicate"))
|
|
|
|
if reply == 0:
|
|
# merge
|
|
if action == "edit":
|
|
# just remove the row that would be same
|
|
self.twCredits.removeRow(row)
|
|
# TODO -- need to find the row of the dupe, and
|
|
# possible change the primary flag
|
|
|
|
ok_to_mod = False
|
|
|
|
if ok_to_mod:
|
|
# modify it
|
|
if action == "edit":
|
|
self.twCredits.item(row, 1).setText(new_role)
|
|
self.twCredits.item(row, 2).setText(new_name)
|
|
self.updateCreditPrimaryFlag(row, new_primary)
|
|
else:
|
|
# add new entry
|
|
row = self.twCredits.rowCount()
|
|
self.addNewCreditEntry(
|
|
row, new_role, new_name, new_primary)
|
|
|
|
self.updateCreditColors()
|
|
self.setDirtyFlag()
|
|
|
|
def removeCredit(self):
|
|
row = self.twCredits.currentRow()
|
|
if row != -1:
|
|
self.twCredits.removeRow(row)
|
|
self.setDirtyFlag()
|
|
|
|
def showSettings(self):
|
|
|
|
settingswin = SettingsWindow(self, self.settings)
|
|
settingswin.setModal(True)
|
|
settingswin.exec_()
|
|
if settingswin.result():
|
|
pass
|
|
|
|
def setAppPosition(self):
|
|
if self.settings.last_main_window_width != 0:
|
|
self.move(
|
|
self.settings.last_main_window_x,
|
|
self.settings.last_main_window_y)
|
|
self.resize(
|
|
self.settings.last_main_window_width,
|
|
self.settings.last_main_window_height)
|
|
else:
|
|
screen = QtWidgets.QDesktopWidget().screenGeometry()
|
|
size = self.frameGeometry()
|
|
self.move((screen.width() - size.width()) / 2,
|
|
(screen.height() - size.height()) / 2)
|
|
|
|
def adjustLoadStyleCombo(self):
|
|
# select the current style
|
|
if (self.load_data_style == MetaDataStyle.CBI):
|
|
self.cbLoadDataStyle.setCurrentIndex(0)
|
|
elif (self.load_data_style == MetaDataStyle.CIX):
|
|
self.cbLoadDataStyle.setCurrentIndex(1)
|
|
|
|
def adjustSaveStyleCombo(self):
|
|
# select the current style
|
|
if (self.save_data_style == MetaDataStyle.CBI):
|
|
self.cbSaveDataStyle.setCurrentIndex(0)
|
|
elif (self.save_data_style == MetaDataStyle.CIX):
|
|
self.cbSaveDataStyle.setCurrentIndex(1)
|
|
self.updateStyleTweaks()
|
|
|
|
def populateComboBoxes(self):
|
|
|
|
# Add the entries to the tag style combobox
|
|
self.cbLoadDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI)
|
|
self.cbLoadDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
|
self.adjustLoadStyleCombo()
|
|
|
|
self.cbSaveDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI)
|
|
self.cbSaveDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
|
self.adjustSaveStyleCombo()
|
|
|
|
# Add the entries to the country combobox
|
|
self.cbCountry.addItem("", "")
|
|
for c in utils.countries:
|
|
self.cbCountry.addItem(c[1], c[0])
|
|
|
|
# Add the entries to the language combobox
|
|
self.cbLanguage.addItem("", "")
|
|
lang_dict = utils.getLanguageDict()
|
|
for key in sorted(lang_dict, key=functools.cmp_to_key(locale.strcoll)):
|
|
self.cbLanguage.addItem(lang_dict[key], key)
|
|
|
|
# Add the entries to the manga combobox
|
|
self.cbManga.addItem("", "")
|
|
self.cbManga.addItem("Yes", "Yes")
|
|
self.cbManga.addItem("Yes (Right to Left)", "YesAndRightToLeft")
|
|
self.cbManga.addItem("No", "No")
|
|
|
|
# Add the entries to the maturity combobox
|
|
self.cbMaturityRating.addItem("", "")
|
|
self.cbMaturityRating.addItem("Everyone", "")
|
|
self.cbMaturityRating.addItem("G", "")
|
|
self.cbMaturityRating.addItem("Early Childhood", "")
|
|
self.cbMaturityRating.addItem("Everyone 10+", "")
|
|
self.cbMaturityRating.addItem("PG", "")
|
|
self.cbMaturityRating.addItem("Kids to Adults", "")
|
|
self.cbMaturityRating.addItem("Teen", "")
|
|
self.cbMaturityRating.addItem("MA15+", "")
|
|
self.cbMaturityRating.addItem("Mature 17+", "")
|
|
self.cbMaturityRating.addItem("R18+", "")
|
|
self.cbMaturityRating.addItem("X18+", "")
|
|
self.cbMaturityRating.addItem("Adults Only 18+", "")
|
|
self.cbMaturityRating.addItem("Rating Pending", "")
|
|
|
|
# Add entries to the format combobox
|
|
self.cbFormat.addItem("")
|
|
self.cbFormat.addItem(".1")
|
|
self.cbFormat.addItem("-1")
|
|
self.cbFormat.addItem("1 Shot")
|
|
self.cbFormat.addItem("1/2")
|
|
self.cbFormat.addItem("1-Shot")
|
|
self.cbFormat.addItem("Annotation")
|
|
self.cbFormat.addItem("Annotations")
|
|
self.cbFormat.addItem("Annual")
|
|
self.cbFormat.addItem("Anthology")
|
|
self.cbFormat.addItem("B&W")
|
|
self.cbFormat.addItem("B/W")
|
|
self.cbFormat.addItem("B&&W")
|
|
self.cbFormat.addItem("Black & White")
|
|
self.cbFormat.addItem("Box Set")
|
|
self.cbFormat.addItem("Box-Set")
|
|
self.cbFormat.addItem("Crossover")
|
|
self.cbFormat.addItem("Director's Cut")
|
|
self.cbFormat.addItem("Epilogue")
|
|
self.cbFormat.addItem("Event")
|
|
self.cbFormat.addItem("FCBD")
|
|
self.cbFormat.addItem("Flyer")
|
|
self.cbFormat.addItem("Giant")
|
|
self.cbFormat.addItem("Giant Size")
|
|
self.cbFormat.addItem("Giant-Size")
|
|
self.cbFormat.addItem("Graphic Novel")
|
|
self.cbFormat.addItem("Hardcover")
|
|
self.cbFormat.addItem("Hard-Cover")
|
|
self.cbFormat.addItem("King")
|
|
self.cbFormat.addItem("King Size")
|
|
self.cbFormat.addItem("King-Size")
|
|
self.cbFormat.addItem("Limited Series")
|
|
self.cbFormat.addItem("Magazine")
|
|
self.cbFormat.addItem("-1")
|
|
self.cbFormat.addItem("NSFW")
|
|
self.cbFormat.addItem("One Shot")
|
|
self.cbFormat.addItem("One-Shot")
|
|
self.cbFormat.addItem("Point 1")
|
|
self.cbFormat.addItem("Preview")
|
|
self.cbFormat.addItem("Prologue")
|
|
self.cbFormat.addItem("Reference")
|
|
self.cbFormat.addItem("Review")
|
|
self.cbFormat.addItem("Reviewed")
|
|
self.cbFormat.addItem("Scanlation")
|
|
self.cbFormat.addItem("Script")
|
|
self.cbFormat.addItem("Series")
|
|
self.cbFormat.addItem("Sketch")
|
|
self.cbFormat.addItem("Special")
|
|
self.cbFormat.addItem("TPB")
|
|
self.cbFormat.addItem("Trade Paper Back")
|
|
self.cbFormat.addItem("WebComic")
|
|
self.cbFormat.addItem("Web Comic")
|
|
self.cbFormat.addItem("Year 1")
|
|
self.cbFormat.addItem("Year One")
|
|
|
|
def removeAuto(self):
|
|
self.removeTags(self.save_data_style)
|
|
|
|
def removeCBLTags(self):
|
|
self.removeTags(MetaDataStyle.CBI)
|
|
|
|
def removeCRTags(self):
|
|
self.removeTags(MetaDataStyle.CIX)
|
|
|
|
def removeTags(self, style):
|
|
# remove the indicated tags from the archive
|
|
ca_list = self.fileSelectionList.getSelectedArchiveList()
|
|
has_md_count = 0
|
|
for ca in ca_list:
|
|
if ca.hasMetadata(style):
|
|
has_md_count += 1
|
|
|
|
if has_md_count == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Remove Tags"), self.tr(
|
|
"No archives with {0} tags selected!".format(
|
|
MetaDataStyle.name[style])))
|
|
return
|
|
|
|
if has_md_count != 0 and not self.dirtyFlagVerification(
|
|
"Remove Tags",
|
|
"If you remove tags now, unsaved data in the form will be lost. Are you sure?"):
|
|
return
|
|
|
|
if has_md_count != 0:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
self.tr("Remove Tags"),
|
|
self.tr(
|
|
"Are you sure you wish to remove the {0} tags from {1} archive(s)?".format(
|
|
MetaDataStyle.name[style],
|
|
has_md_count)),
|
|
QtWidgets.QMessageBox.Yes,
|
|
QtWidgets.QMessageBox.No)
|
|
|
|
if reply == QtWidgets.QMessageBox.Yes:
|
|
progdialog = QtWidgets.QProgressDialog(
|
|
"", "Cancel", 0, has_md_count, self)
|
|
progdialog.setWindowTitle("Removing Tags")
|
|
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
|
progdialog.setMinimumDuration(300)
|
|
centerWindowOnParent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
#progdialog.show()
|
|
|
|
prog_idx = 0
|
|
|
|
failed_list = []
|
|
success_count = 0
|
|
for ca in ca_list:
|
|
if ca.hasMetadata(style):
|
|
QtCore.QCoreApplication.processEvents()
|
|
if progdialog.wasCanceled():
|
|
break
|
|
prog_idx += 1
|
|
progdialog.setValue(prog_idx)
|
|
progdialog.setLabelText(ca.path)
|
|
centerWindowOnParent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
if ca.hasMetadata(style) and ca.isWritable():
|
|
if not ca.removeMetadata(style):
|
|
failed_list.append(ca.path)
|
|
else:
|
|
success_count += 1
|
|
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
|
|
progdialog.hide()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.fileSelectionList.updateSelectedRows()
|
|
self.updateInfoBox()
|
|
self.updateMenus()
|
|
|
|
summary = "Successfully removed tags in {0} archive(s).".format(
|
|
success_count)
|
|
if len(failed_list) > 0:
|
|
summary += "\n\nThe remove operation failed in the following {0} archive(s):\n".format(
|
|
len(failed_list))
|
|
for f in failed_list:
|
|
summary += "\t{0}\n".format(f)
|
|
|
|
dlg = LogWindow(self)
|
|
dlg.setText(summary)
|
|
dlg.setWindowTitle("Tag Remove Summary")
|
|
# dlg.adjustSize()
|
|
dlg.exec_()
|
|
|
|
def copyTags(self):
|
|
# copy the indicated tags in the archive
|
|
ca_list = self.fileSelectionList.getSelectedArchiveList()
|
|
has_src_count = 0
|
|
|
|
src_style = self.load_data_style
|
|
dest_style = self.save_data_style
|
|
|
|
if src_style == dest_style:
|
|
QtWidgets.QMessageBox.information(
|
|
self,
|
|
self.tr("Copy Tags"),
|
|
self.tr(
|
|
"Can't copy tag style onto itself." +
|
|
" Read style and modify style must be different."))
|
|
return
|
|
|
|
for ca in ca_list:
|
|
if ca.hasMetadata(src_style):
|
|
has_src_count += 1
|
|
|
|
if has_src_count == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Copy Tags"), self.tr(
|
|
"No archives with {0} tags selected!".format(
|
|
MetaDataStyle.name[src_style])))
|
|
return
|
|
|
|
if has_src_count != 0 and not self.dirtyFlagVerification(
|
|
"Copy Tags",
|
|
"If you copy tags now, unsaved data in the form may be lost. Are you sure?"):
|
|
return
|
|
|
|
if has_src_count != 0:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
self.tr("Copy Tags"),
|
|
self.tr(
|
|
"Are you sure you wish to copy the {0} tags to {1} tags in {2} archive(s)?".format(
|
|
MetaDataStyle.name[src_style],
|
|
MetaDataStyle.name[dest_style],
|
|
has_src_count)),
|
|
QtWidgets.QMessageBox.Yes,
|
|
QtWidgets.QMessageBox.No)
|
|
|
|
if reply == QtWidgets.QMessageBox.Yes:
|
|
progdialog = QtWidgets.QProgressDialog(
|
|
"", "Cancel", 0, has_src_count, self)
|
|
progdialog.setWindowTitle("Copying Tags")
|
|
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
|
progdialog.setMinimumDuration(300)
|
|
centerWindowOnParent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
prog_idx = 0
|
|
|
|
failed_list = []
|
|
success_count = 0
|
|
for ca in ca_list:
|
|
if ca.hasMetadata(src_style):
|
|
QtCore.QCoreApplication.processEvents()
|
|
if progdialog.wasCanceled():
|
|
break
|
|
prog_idx += 1
|
|
progdialog.setValue(prog_idx)
|
|
progdialog.setLabelText(ca.path)
|
|
centerWindowOnParent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
if ca.hasMetadata(src_style) and ca.isWritable():
|
|
md = ca.readMetadata(src_style)
|
|
|
|
if dest_style == MetaDataStyle.CBI and self.settings.apply_cbl_transform_on_bulk_operation:
|
|
md = CBLTransformer(md, self.settings).apply()
|
|
|
|
if not ca.writeMetadata(md, dest_style):
|
|
failed_list.append(ca.path)
|
|
else:
|
|
success_count += 1
|
|
|
|
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
|
|
progdialog.hide()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.fileSelectionList.updateSelectedRows()
|
|
self.updateInfoBox()
|
|
self.updateMenus()
|
|
|
|
summary = "Successfully copied tags in {0} archive(s).".format(
|
|
success_count)
|
|
if len(failed_list) > 0:
|
|
summary += "\n\nThe copy operation failed in the following {0} archive(s):\n".format(
|
|
len(failed_list))
|
|
for f in failed_list:
|
|
summary += "\t{0}\n".format(f)
|
|
|
|
dlg = LogWindow(self)
|
|
dlg.setText(summary)
|
|
dlg.setWindowTitle("Tag Copy Summary")
|
|
dlg.exec_()
|
|
|
|
def actualIssueDataFetch(self, match):
|
|
|
|
# now get the particular issue data
|
|
cv_md = None
|
|
QtWidgets.QApplication.setOverrideCursor(
|
|
QtGui.QCursor(QtCore.Qt.WaitCursor))
|
|
|
|
try:
|
|
comicVine = ComicVineTalker()
|
|
comicVine.wait_for_rate_limit = self.settings.wait_and_retry_on_rate_limit
|
|
cv_md = comicVine.fetchIssueData(
|
|
match['volume_id'], match['issue_number'], self.settings)
|
|
except ComicVineTalkerException:
|
|
print("Network error while getting issue details. Save aborted")
|
|
|
|
if cv_md is not None:
|
|
if self.settings.apply_cbl_transform_on_cv_import:
|
|
cv_md = CBLTransformer(cv_md, self.settings).apply()
|
|
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
|
|
return cv_md
|
|
|
|
def autoTagLog(self, text):
|
|
IssueIdentifier.defaultWriteOutput(text)
|
|
if self.atprogdialog is not None:
|
|
self.atprogdialog.textEdit.insertPlainText(text)
|
|
self.atprogdialog.textEdit.ensureCursorVisible()
|
|
QtCore.QCoreApplication.processEvents()
|
|
QtCore.QCoreApplication.processEvents()
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
def identifyAndTagSingleArchive(self, ca, match_results, dlg):
|
|
success = False
|
|
ii = IssueIdentifier(ca, self.settings)
|
|
|
|
# read in metadata, and parse file name if not there
|
|
md = ca.readMetadata(self.save_data_style)
|
|
if md.isEmpty:
|
|
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
|
if dlg.ignoreLeadingDigitsInFilename and md.series is not None:
|
|
# remove all leading numbers
|
|
md.series = re.sub("([\d.]*)(.*)", "\\2", md.series)
|
|
|
|
# use the dialog specified search string
|
|
if dlg.searchString is not None:
|
|
md.series = dlg.searchString
|
|
|
|
if md is None or md.isEmpty:
|
|
print("No metadata given to search online with!")
|
|
return False, match_results
|
|
|
|
if dlg.dontUseYear:
|
|
md.year = None
|
|
if dlg.assumeIssueOne and (md.issue is None or md.issue == ""):
|
|
md.issue = "1"
|
|
ii.setAdditionalMetadata(md)
|
|
ii.onlyUseAdditionalMetaData = True
|
|
ii.waitAndRetryOnRateLimit = dlg.waitAndRetryOnRateLimit
|
|
ii.setOutputFunction(self.autoTagLog)
|
|
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
|
ii.setCoverURLCallback(self.atprogdialog.setTestImage)
|
|
ii.setNameLengthDeltaThreshold(dlg.nameLengthMatchTolerance)
|
|
|
|
matches = ii.search()
|
|
|
|
result = ii.search_result
|
|
|
|
found_match = False
|
|
choices = False
|
|
low_confidence = False
|
|
no_match = 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:
|
|
if low_confidence:
|
|
self.autoTagLog(
|
|
"Online search: Multiple low-confidence matches. Save aborted\n")
|
|
match_results.lowConfidenceMatches.append(
|
|
MultipleMatch(ca, matches))
|
|
else:
|
|
self.autoTagLog(
|
|
"Online search: Multiple matches. Save aborted\n")
|
|
match_results.multipleMatches.append(
|
|
MultipleMatch(ca, matches))
|
|
elif low_confidence and not dlg.autoSaveOnLow:
|
|
self.autoTagLog(
|
|
"Online search: Low confidence match. Save aborted\n")
|
|
match_results.lowConfidenceMatches.append(
|
|
MultipleMatch(ca, matches))
|
|
elif not found_match:
|
|
self.autoTagLog("Online search: No match found. Save aborted\n")
|
|
match_results.noMatches.append(ca.path)
|
|
else:
|
|
# a single match!
|
|
if low_confidence:
|
|
self.autoTagLog(
|
|
"Online search: Low confidence match, but saving anyways, as indicated...\n")
|
|
|
|
# now get the particular issue data
|
|
cv_md = self.actualIssueDataFetch(matches[0])
|
|
if cv_md is None:
|
|
match_results.fetchDataFailures.append(ca.path)
|
|
|
|
if cv_md is not None:
|
|
md.overlay(cv_md)
|
|
|
|
if not ca.writeMetadata(md, self.save_data_style):
|
|
match_results.writeFailures.append(ca.path)
|
|
self.autoTagLog("Save failed ;-(\n")
|
|
else:
|
|
match_results.goodMatches.append(ca.path)
|
|
success = True
|
|
self.autoTagLog("Save complete!\n")
|
|
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
|
|
return success, match_results
|
|
|
|
def autoTag(self):
|
|
ca_list = self.fileSelectionList.getSelectedArchiveList()
|
|
style = self.save_data_style
|
|
|
|
if len(ca_list) == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Auto-Tag"), self.tr("No archives selected!"))
|
|
return
|
|
|
|
if not self.dirtyFlagVerification(
|
|
"Auto-Tag",
|
|
"If you auto-tag now, unsaved data in the form will be lost. Are you sure?"):
|
|
return
|
|
|
|
atstartdlg = AutoTagStartWindow(
|
|
self,
|
|
self.settings,
|
|
self.tr(
|
|
"You have selected {0} archive(s) to automatically identify and write {1} tags to.\n\n".format(
|
|
len(ca_list),
|
|
MetaDataStyle.name[style]) +
|
|
"Please choose options below, and select OK to Auto-Tag.\n"))
|
|
|
|
atstartdlg.adjustSize()
|
|
atstartdlg.setModal(True)
|
|
if not atstartdlg.exec_():
|
|
return
|
|
|
|
self.atprogdialog = AutoTagProgressWindow(self)
|
|
self.atprogdialog.setModal(True)
|
|
self.atprogdialog.show()
|
|
self.atprogdialog.progressBar.setMaximum(len(ca_list))
|
|
self.atprogdialog.setWindowTitle("Auto-Tagging")
|
|
|
|
self.autoTagLog(
|
|
"==========================================================================\n")
|
|
self.autoTagLog(
|
|
"Auto-Tagging Started for {0} items\n".format(len(ca_list)))
|
|
|
|
prog_idx = 0
|
|
|
|
match_results = OnlineMatchResults()
|
|
archives_to_remove = []
|
|
for ca in ca_list:
|
|
self.autoTagLog(
|
|
"==========================================================================\n")
|
|
self.autoTagLog(
|
|
"Auto-Tagging {0} of {1}\n".format(prog_idx + 1, len(ca_list)))
|
|
self.autoTagLog("{0}\n".format(ca.path))
|
|
cover_idx = ca.readMetadata(style).getCoverPageIndexList()[0]
|
|
image_data = ca.getPage(cover_idx)
|
|
self.atprogdialog.setArchiveImage(image_data)
|
|
self.atprogdialog.setTestImage(None)
|
|
|
|
QtCore.QCoreApplication.processEvents()
|
|
if self.atprogdialog.isdone:
|
|
break
|
|
self.atprogdialog.progressBar.setValue(prog_idx)
|
|
prog_idx += 1
|
|
self.atprogdialog.label.setText(ca.path)
|
|
centerWindowOnParent(self.atprogdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
if ca.isWritable():
|
|
success, match_results = self.identifyAndTagSingleArchive(
|
|
ca, match_results, atstartdlg)
|
|
|
|
if success and atstartdlg.removeAfterSuccess:
|
|
archives_to_remove.append(ca)
|
|
|
|
self.atprogdialog.close()
|
|
|
|
if atstartdlg.removeAfterSuccess:
|
|
self.fileSelectionList.removeArchiveList(archives_to_remove)
|
|
self.fileSelectionList.updateSelectedRows()
|
|
|
|
self.loadArchive(self.fileSelectionList.getCurrentArchive())
|
|
self.atprogdialog = None
|
|
|
|
summary = ""
|
|
summary += "Successfully tagged archives: {0}\n".format(
|
|
len(match_results.goodMatches))
|
|
|
|
if len(match_results.multipleMatches) > 0:
|
|
summary += "Archives with multiple matches: {0}\n".format(
|
|
len(match_results.multipleMatches))
|
|
if len(match_results.lowConfidenceMatches) > 0:
|
|
summary += "Archives with one or more low-confidence matches: {0}\n".format(
|
|
len(match_results.lowConfidenceMatches))
|
|
if len(match_results.noMatches) > 0:
|
|
summary += "Archives with no matches: {0}\n".format(
|
|
len(match_results.noMatches))
|
|
if len(match_results.fetchDataFailures) > 0:
|
|
summary += "Archives that failed due to data fetch errors: {0}\n".format(
|
|
len(match_results.fetchDataFailures))
|
|
if len(match_results.writeFailures) > 0:
|
|
summary += "Archives that failed due to file writing errors: {0}\n".format(
|
|
len(match_results.writeFailures))
|
|
|
|
self.autoTagLog(summary)
|
|
|
|
sum_selectable = len(
|
|
match_results.multipleMatches) + len(match_results.lowConfidenceMatches)
|
|
if sum_selectable > 0:
|
|
summary += "\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
|
|
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
self.tr("Auto-Tag Summary"),
|
|
self.tr(summary),
|
|
QtWidgets.QMessageBox.Yes,
|
|
QtWidgets.QMessageBox.No)
|
|
|
|
match_results.multipleMatches.extend(
|
|
match_results.lowConfidenceMatches)
|
|
if reply == QtWidgets.QMessageBox.Yes:
|
|
matchdlg = AutoTagMatchWindow(
|
|
self,
|
|
match_results.multipleMatches,
|
|
style,
|
|
self.actualIssueDataFetch)
|
|
matchdlg.setModal(True)
|
|
matchdlg.exec_()
|
|
self.fileSelectionList.updateSelectedRows()
|
|
self.loadArchive(self.fileSelectionList.getCurrentArchive())
|
|
|
|
else:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Auto-Tag Summary"), self.tr(summary))
|
|
|
|
def dirtyFlagVerification(self, title, desc):
|
|
if self.dirtyFlag:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
self.tr(title),
|
|
self.tr(desc),
|
|
QtWidgets.QMessageBox.Yes,
|
|
QtWidgets.QMessageBox.No)
|
|
|
|
if reply != QtWidgets.QMessageBox.Yes:
|
|
return False
|
|
return True
|
|
|
|
def closeEvent(self, event):
|
|
|
|
if self.dirtyFlagVerification(
|
|
"Exit " +
|
|
self.appName,
|
|
"If you quit now, data in the form will be lost. Are you sure?"):
|
|
appsize = self.size()
|
|
self.settings.last_main_window_width = appsize.width()
|
|
self.settings.last_main_window_height = appsize.height()
|
|
self.settings.last_main_window_x = self.x()
|
|
self.settings.last_main_window_y = self.y()
|
|
self.settings.last_form_side_width = self.splitter.sizes()[0]
|
|
self.settings.last_list_side_width = self.splitter.sizes()[1]
|
|
self.settings.last_filelist_sorted_column, self.settings.last_filelist_sorted_order = self.fileSelectionList.getSorting()
|
|
self.settings.save()
|
|
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
|
|
def showPageBrowser(self):
|
|
if self.page_browser is None:
|
|
self.page_browser = PageBrowserWindow(self, self.metadata)
|
|
if self.comic_archive is not None:
|
|
self.page_browser.setComicArchive(self.comic_archive)
|
|
self.page_browser.finished.connect(self.pageBrowserClosed)
|
|
|
|
def pageBrowserClosed(self):
|
|
self.page_browser = None
|
|
|
|
def viewRawCRTags(self):
|
|
if self.comic_archive is not None and self.comic_archive.hasCIX():
|
|
dlg = LogWindow(self)
|
|
dlg.setText(self.comic_archive.readRawCIX())
|
|
dlg.setWindowTitle("Raw ComicRack Tag View")
|
|
dlg.exec_()
|
|
|
|
def viewRawCBLTags(self):
|
|
if self.comic_archive is not None and self.comic_archive.hasCBI():
|
|
dlg = LogWindow(self)
|
|
text = pprint.pformat(
|
|
json.loads(self.comic_archive.readRawCBI()), indent=4)
|
|
dlg.setText(text)
|
|
dlg.setWindowTitle("Raw ComicBookLover Tag View")
|
|
dlg.exec_()
|
|
|
|
def showWiki(self):
|
|
webbrowser.open("https://github.com/davide-romanini/comictagger/wiki")
|
|
|
|
def reportBug(self):
|
|
webbrowser.open("https://github.com/davide-romanini/comictagger/issues")
|
|
|
|
def showForum(self):
|
|
webbrowser.open("http://comictagger.forumotion.com/")
|
|
|
|
def frontCoverChanged(self, int):
|
|
self.metadata.pages = self.pageListEditor.getPageList()
|
|
self.updateCoverImage()
|
|
|
|
def pageListOrderChanged(self):
|
|
self.metadata.pages = self.pageListEditor.getPageList()
|
|
|
|
def applyCBLTransform(self):
|
|
self.formToMetadata()
|
|
self.metadata = CBLTransformer(self.metadata, self.settings).apply()
|
|
self.metadataToForm()
|
|
|
|
def renameArchive(self):
|
|
ca_list = self.fileSelectionList.getSelectedArchiveList()
|
|
|
|
if len(ca_list) == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Rename"), self.tr("No archives selected!"))
|
|
return
|
|
|
|
if self.dirtyFlagVerification(
|
|
"File Rename",
|
|
"If you rename files now, unsaved data in the form will be lost. Are you sure?"):
|
|
|
|
dlg = RenameWindow(
|
|
self, ca_list, self.load_data_style, self.settings)
|
|
dlg.setModal(True)
|
|
if dlg.exec_():
|
|
self.fileSelectionList.updateSelectedRows()
|
|
self.loadArchive(self.comic_archive)
|
|
|
|
def fileListSelectionChanged(self, qvarFI):
|
|
fi = qvarFI #.toPyObject()
|
|
self.loadArchive(fi.ca)
|
|
|
|
def loadArchive(self, comic_archive):
|
|
self.comic_archive = None
|
|
self.clearForm()
|
|
|
|
self.settings.last_opened_folder = os.path.abspath(
|
|
os.path.split(comic_archive.path)[0])
|
|
self.comic_archive = comic_archive
|
|
self.metadata = self.comic_archive.readMetadata(self.load_data_style)
|
|
if self.metadata is None:
|
|
self.metadata = GenericMetadata()
|
|
|
|
self.actualLoadCurrentArchive()
|
|
|
|
def fileListCleared(self):
|
|
self.resetApp()
|
|
|
|
def splitterMovedEvent(self, w1, w2):
|
|
scrollbar_w = 0
|
|
if self.scrollArea.verticalScrollBar().isVisible():
|
|
scrollbar_w = self.scrollArea.verticalScrollBar().width()
|
|
|
|
new_w = self.scrollArea.width() - scrollbar_w - 5
|
|
self.scrollAreaWidgetContents.resize(
|
|
new_w, self.scrollAreaWidgetContents.height())
|
|
|
|
def resizeEvent(self, ev):
|
|
self.splitterMovedEvent(0, 0)
|
|
|
|
def tabChanged(self, idx):
|
|
if idx == 0:
|
|
self.splitterMovedEvent(0, 0)
|
|
|
|
def checkLatestVersionOnline(self):
|
|
self.versionChecker = VersionChecker()
|
|
self.versionChecker.versionRequestComplete.connect(
|
|
self.versionCheckComplete)
|
|
self.versionChecker.asyncGetLatestVersion(
|
|
self.settings.install_id, self.settings.send_usage_stats)
|
|
|
|
def versionCheckComplete(self, new_version):
|
|
if (new_version != self.version and
|
|
new_version != self.settings.dont_notify_about_this_version):
|
|
website = "http://code.google.com/p/comictagger"
|
|
checked = OptionalMessageDialog.msg(
|
|
self,
|
|
"New version available!",
|
|
"New version ({0}) available!<br>(You are currently running {1})<br><br>".format(
|
|
new_version,
|
|
self.version) +
|
|
"Visit <a href='{0}'>{0}</a> for more info.<br><br>".format(website),
|
|
QtCore.Qt.Unchecked,
|
|
"Don't tell me about this version again")
|
|
if checked:
|
|
self.settings.dont_notify_about_this_version = new_version
|
|
|
|
def onIncomingSocketConnection(self):
|
|
# accept connection from other instance.
|
|
# read in the file list if they're giving it,
|
|
# and add to our own list
|
|
localSocket = self.socketServer.nextPendingConnection()
|
|
if localSocket.waitForReadyRead(3000):
|
|
byteArray = localSocket.readAll()
|
|
if len(byteArray) > 0:
|
|
obj = pickle.loads(byteArray)
|
|
localSocket.disconnectFromServer()
|
|
if isinstance(obj, list):
|
|
self.fileSelectionList.addPathList(obj)
|
|
else:
|
|
# print(localSocket.errorString().toLatin1())
|
|
pass
|
|
|
|
self.bringToTop()
|
|
|
|
def bringToTop(self):
|
|
if platform.system() == "Windows":
|
|
self.showNormal()
|
|
self.raise_()
|
|
self.activateWindow()
|
|
try:
|
|
import win32con
|
|
import win32gui
|
|
hwnd = self.effectiveWinId()
|
|
rect = win32gui.GetWindowRect(hwnd)
|
|
x = rect[0]
|
|
y = rect[1]
|
|
w = rect[2] - x
|
|
h = rect[3] - y
|
|
# mark it "always on top", just for a moment, to force it to
|
|
# the top
|
|
win32gui.SetWindowPos(
|
|
hwnd, win32con.HWND_TOPMOST, x, y, w, h, 0)
|
|
win32gui.SetWindowPos(
|
|
hwnd, win32con.HWND_NOTOPMOST, x, y, w, h, 0)
|
|
except Exception as e:
|
|
print("Whoops", e)
|
|
elif platform.system() == "Darwin":
|
|
self.raise_()
|
|
self.showNormal()
|
|
self.activateWindow()
|
|
else:
|
|
flags = self.windowFlags()
|
|
self.setWindowFlags(
|
|
flags | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.X11BypassWindowManagerHint)
|
|
QtCore.QCoreApplication.processEvents()
|
|
# self.show()
|
|
self.setWindowFlags(flags)
|
|
self.show()
|