mirror of
https://github.com/ciromattia/kcc
synced 2026-05-27 01:42:09 +00:00
Add bulk metadata editing support
* Enable multi-file selection in Metadata Editor using getOpenFileNames()
* Disable Volume, Number, and Title fields in bulk mode
* Pre-fill Series and author fields from first selected file in bulk mode
* Show status "Editing Y files" in bulk mode
* Add progress display during bulk save ("Processing X/Y: filename")
* Disable buttons during processing
* Skip read-only CBR files with error collection
* Show error summary dialog if any files fail to save
* Keep single file metadata editing the same
This commit is contained in:
@@ -617,27 +617,30 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.jobList.scrollToBottom()
|
GUI.jobList.scrollToBottom()
|
||||||
|
|
||||||
def selectFileMetaEditor(self, sname):
|
def selectFileMetaEditor(self, sname):
|
||||||
|
files = []
|
||||||
if not sname:
|
if not sname:
|
||||||
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
||||||
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
||||||
if dname != '':
|
if dname != '':
|
||||||
sname = os.path.join(dname, 'ComicInfo.xml')
|
files = [os.path.join(dname, 'ComicInfo.xml')]
|
||||||
self.lastPath = os.path.dirname(sname)
|
self.lastPath = os.path.dirname(files[0])
|
||||||
else:
|
else:
|
||||||
if self.sevenzip:
|
if self.sevenzip:
|
||||||
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
|
fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
|
||||||
'Comic (*.cbz *.cbr *.cb7)')
|
'Comic (*.cbz *.cbr *.cb7)')
|
||||||
|
files = fnames[0]
|
||||||
|
if files:
|
||||||
|
self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir))
|
||||||
else:
|
else:
|
||||||
fname = ['']
|
|
||||||
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
|
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
|
||||||
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
||||||
' to enable metadata editing.', 'warning')
|
' to enable metadata editing.', 'warning')
|
||||||
if fname[0] != '':
|
else:
|
||||||
sname = fname[0]
|
files = [sname]
|
||||||
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
|
|
||||||
if sname:
|
if files:
|
||||||
try:
|
try:
|
||||||
self.editor.loadData(sname)
|
self.editor.loadData(files)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_, _, traceback = sys.exc_info()
|
_, _, traceback = sys.exc_info()
|
||||||
GUI.sentry.captureException()
|
GUI.sentry.captureException()
|
||||||
@@ -1415,53 +1418,132 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
|
|
||||||
|
|
||||||
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
||||||
def loadData(self, file):
|
def loadData(self, files):
|
||||||
self.parser = metadata.MetadataParser(file)
|
self.files = files if isinstance(files, list) else [files]
|
||||||
if self.parser.format in ['RAR', 'RAR5']:
|
self.bulkMode = len(self.files) > 1
|
||||||
self.editorWidget.setEnabled(False)
|
|
||||||
self.okButton.setEnabled(False)
|
if self.bulkMode:
|
||||||
self.statusLabel.setText('CBR metadata are read-only.')
|
firstFile = self.files[0]
|
||||||
else:
|
self.parser = metadata.MetadataParser(firstFile)
|
||||||
self.editorWidget.setEnabled(True)
|
self.editorWidget.setEnabled(True)
|
||||||
self.okButton.setEnabled(True)
|
self.okButton.setEnabled(True)
|
||||||
self.statusLabel.setText('Separate authors with a comma.')
|
self.statusLabel.setText(f'Editing {len(self.files)} files.')
|
||||||
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
|
|
||||||
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
for field in (self.volumeLine, self.numberLine, self.titleLine):
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
field.setEnabled(False)
|
||||||
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
field.setText('')
|
||||||
for field in (self.seriesLine, self.titleLine):
|
field.setPlaceholderText('(multiple files)')
|
||||||
if field.text() == '':
|
|
||||||
path = Path(file)
|
for field in (self.seriesLine,):
|
||||||
if file.endswith('.xml'):
|
field.setEnabled(True)
|
||||||
field.setText(path.parent.name)
|
field.setPlaceholderText('')
|
||||||
else:
|
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
||||||
field.setText(path.stem)
|
|
||||||
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
field.setEnabled(True)
|
||||||
|
field.setPlaceholderText('')
|
||||||
|
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
||||||
|
else:
|
||||||
|
file = self.files[0]
|
||||||
|
self.parser = metadata.MetadataParser(file)
|
||||||
|
|
||||||
|
for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine,
|
||||||
|
self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
field.setEnabled(True)
|
||||||
|
field.setPlaceholderText('')
|
||||||
|
|
||||||
|
if self.parser.format in ['RAR', 'RAR5']:
|
||||||
|
self.editorWidget.setEnabled(False)
|
||||||
|
self.okButton.setEnabled(False)
|
||||||
|
self.statusLabel.setText('CBR metadata are read-only.')
|
||||||
|
else:
|
||||||
|
self.editorWidget.setEnabled(True)
|
||||||
|
self.okButton.setEnabled(True)
|
||||||
|
self.statusLabel.setText('Separate authors with a comma.')
|
||||||
|
|
||||||
|
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
|
||||||
|
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
||||||
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
||||||
|
for field in (self.seriesLine, self.titleLine):
|
||||||
|
if field.text() == '':
|
||||||
|
path = Path(file)
|
||||||
|
if file.endswith('.xml'):
|
||||||
|
field.setText(path.parent.name)
|
||||||
|
else:
|
||||||
|
field.setText(path.stem)
|
||||||
|
|
||||||
def saveData(self):
|
def saveData(self):
|
||||||
for field in (self.volumeLine, self.numberLine):
|
if self.bulkMode:
|
||||||
if field.text().isnumeric() or self.cleanData(field.text()) == '':
|
bulkData = {}
|
||||||
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
if self.cleanData(self.seriesLine.text()):
|
||||||
else:
|
bulkData['Series'] = self.cleanData(self.seriesLine.text())
|
||||||
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for field in (self.seriesLine, self.titleLine):
|
|
||||||
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
fieldName = field.objectName().capitalize()[:-4] + 's'
|
||||||
values = self.cleanData(field.text()).split(',')
|
values = self.cleanData(field.text()).split(',')
|
||||||
tmpData = []
|
tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
|
||||||
for value in values:
|
if tmpData:
|
||||||
if self.cleanData(value) != '':
|
bulkData[fieldName] = tmpData
|
||||||
tmpData.append(self.cleanData(value))
|
|
||||||
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
|
if not bulkData:
|
||||||
try:
|
self.statusLabel.setText('No changes to apply.')
|
||||||
self.parser.saveXML()
|
return
|
||||||
except Exception as err:
|
|
||||||
_, _, traceback = sys.exc_info()
|
errors = []
|
||||||
GUI.sentry.captureException()
|
total = len(self.files)
|
||||||
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
self.okButton.setEnabled(False)
|
||||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
self.cancelButton.setEnabled(False)
|
||||||
|
|
||||||
|
for i, file in enumerate(self.files, 1):
|
||||||
|
self.statusLabel.setText(f'Processing {i}/{total}: {os.path.basename(file)}')
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = metadata.MetadataParser(file)
|
||||||
|
if parser.format in ['RAR', 'RAR5']:
|
||||||
|
errors.append(f'{os.path.basename(file)}: CBR is read-only')
|
||||||
|
continue
|
||||||
|
for key, value in bulkData.items():
|
||||||
|
parser.data[key] = value
|
||||||
|
parser.saveXML()
|
||||||
|
except Exception as err:
|
||||||
|
errors.append(f'{os.path.basename(file)}: {str(err)}')
|
||||||
|
|
||||||
|
self.okButton.setEnabled(True)
|
||||||
|
self.cancelButton.setEnabled(True)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
GUI.showDialog("Some files failed to save:\n\n" + "\n".join(errors[:10]) +
|
||||||
|
(f"\n...and {len(errors) - 10} more" if len(errors) > 10 else ""), 'error')
|
||||||
|
else:
|
||||||
|
self.statusLabel.setText(f'Successfully updated {total} files.')
|
||||||
self.ui.close()
|
self.ui.close()
|
||||||
|
else:
|
||||||
|
for field in (self.volumeLine, self.numberLine):
|
||||||
|
if field.text().isnumeric() or self.cleanData(field.text()) == '':
|
||||||
|
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
||||||
|
else:
|
||||||
|
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for field in (self.seriesLine, self.titleLine):
|
||||||
|
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
||||||
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
values = self.cleanData(field.text()).split(',')
|
||||||
|
tmpData = []
|
||||||
|
for value in values:
|
||||||
|
if self.cleanData(value) != '':
|
||||||
|
tmpData.append(self.cleanData(value))
|
||||||
|
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
|
||||||
|
try:
|
||||||
|
self.parser.saveXML()
|
||||||
|
except Exception as err:
|
||||||
|
_, _, traceback = sys.exc_info()
|
||||||
|
GUI.sentry.captureException()
|
||||||
|
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
||||||
|
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||||
|
self.ui.close()
|
||||||
|
|
||||||
def cleanData(self, s):
|
def cleanData(self, s):
|
||||||
return escape(s.strip())
|
return escape(s.strip())
|
||||||
@@ -1469,6 +1551,8 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ui = QDialog()
|
self.ui = QDialog()
|
||||||
self.parser = None
|
self.parser = None
|
||||||
|
self.files = []
|
||||||
|
self.bulkMode = False
|
||||||
self.setupUi(self.ui)
|
self.setupUi(self.ui)
|
||||||
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
||||||
self.okButton.clicked.connect(self.saveData)
|
self.okButton.clicked.connect(self.saveData)
|
||||||
|
|||||||
Reference in New Issue
Block a user