4608b97e23
Separate comicapi into it's own package Add support for tar files Insert standard gitignore Use suggested _version from setuptools-scm Cleanup setup.py Fix formatting in the rename template help
2043 lines
82 KiB
Python
2043 lines
82 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 datetime
|
|
import functools
|
|
import json
|
|
import locale
|
|
import os
|
|
import pickle
|
|
import platform
|
|
import pprint
|
|
import re
|
|
import sys
|
|
import webbrowser
|
|
|
|
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
|
|
from PyQt5.QtCore import QUrl
|
|
|
|
from comictaggerlib.ui.qtutils import centerWindowOnParent, reduceWidgetFontSize
|
|
|
|
from . import _version, utils
|
|
from .autotagmatchwindow import AutoTagMatchWindow
|
|
from .autotagprogresswindow import AutoTagProgressWindow
|
|
from .autotagstartwindow import AutoTagStartWindow
|
|
from .cbltransformer import CBLTransformer
|
|
from .comicarchive import MetaDataStyle
|
|
from .comicinfoxml import ComicInfoXml
|
|
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
|
from .coverimagewidget import CoverImageWidget
|
|
from .crediteditorwindow import CreditEditorWindow
|
|
from .exportwindow import ExportConflictOpts, ExportWindow
|
|
from .filenameparser import FileNameParser
|
|
from .fileselectionlist import FileSelectionList
|
|
from .genericmetadata import GenericMetadata
|
|
from .issueidentifier import IssueIdentifier
|
|
from .issuestring import IssueString
|
|
from .logwindow import LogWindow
|
|
from .optionalmsgdialog import OptionalMessageDialog
|
|
from .pagebrowser import PageBrowserWindow
|
|
from .pagelisteditor import PageListEditor
|
|
from .renamewindow import RenameWindow
|
|
from .settings import ComicTaggerSettings
|
|
from .settingswindow import SettingsWindow
|
|
from .versionchecker import VersionChecker
|
|
from .volumeselectionwindow import VolumeSelectionWindow
|
|
|
|
|
|
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 = _version.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)
|
|
|
|
self.leSeriesPubYear.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)
|
|
|
|
self.btnAutoImprint.clicked.connect(self.autoImprint)
|
|
|
|
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.actionLiteralSearch.triggered.connect(self.literalSearch)
|
|
|
|
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.actionLiteralSearch.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.actionLiteralSearch)
|
|
self.toolBar.addAction(self.actionClearEntryForm)
|
|
self.toolBar.addAction(self.actionPageBrowser)
|
|
|
|
self.actionMarkAd.setShortcut("A")
|
|
self.actionMarkAd.triggered.connect(self.toggleAd)
|
|
|
|
def toggleAd(self):
|
|
if self.tabWidget.tabText(self.tabWidget.currentIndex()) == "Pages":
|
|
self.pageListEditor.toggleAd()
|
|
|
|
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()
|
|
|
|
def getUrlFromLocalFileID(self, localFileID):
|
|
return localFileID.toLocalFile()
|
|
|
|
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")
|
|
elif ca.isTar():
|
|
self.lblArchiveType.setText("TAR 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.leSeriesPubYear, md.seriesYear)
|
|
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.seriesYear = utils.xlate(self.leSeriesPubYear.text(), "int")
|
|
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:
|
|
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 literalSearch(self):
|
|
self.queryOnline(autoselect=False, literal=True)
|
|
|
|
def queryOnline(self, autoselect=False, literal=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, literal
|
|
)
|
|
|
|
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=lang_dict.get):
|
|
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 self.settings.auto_imprint:
|
|
md.fixPublisher()
|
|
|
|
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()
|
|
|
|
def autoImprint(self):
|
|
self.formToMetadata()
|
|
self.metadata.fixPublisher()
|
|
self.metadataToForm()
|