1
0
mirror of https://github.com/ciromattia/kcc synced 2026-05-19 22:11:59 +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:
jaroslawjanas
2025-12-03 15:06:02 +01:00
parent 61d6972e22
commit abd1adb22f

View File

@@ -617,27 +617,30 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.scrollToBottom()
def selectFileMetaEditor(self, sname):
files = []
if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml')
self.lastPath = os.path.dirname(sname)
files = [os.path.join(dname, 'ComicInfo.xml')]
self.lastPath = os.path.dirname(files[0])
else:
if self.sevenzip:
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
files = fnames[0]
if files:
self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir))
else:
fname = ['']
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>'
' to enable metadata editing.', 'warning')
if fname[0] != '':
sname = fname[0]
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
if sname:
else:
files = [sname]
if files:
try:
self.editor.loadData(sname)
self.editor.loadData(files)
except Exception as err:
_, _, traceback = sys.exc_info()
GUI.sentry.captureException()
@@ -1415,53 +1418,132 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
def loadData(self, file):
self.parser = metadata.MetadataParser(file)
if self.parser.format in ['RAR', 'RAR5']:
self.editorWidget.setEnabled(False)
self.okButton.setEnabled(False)
self.statusLabel.setText('CBR metadata are read-only.')
else:
def loadData(self, files):
self.files = files if isinstance(files, list) else [files]
self.bulkMode = len(self.files) > 1
if self.bulkMode:
firstFile = self.files[0]
self.parser = metadata.MetadataParser(firstFile)
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)
self.statusLabel.setText(f'Editing {len(self.files)} files.')
for field in (self.volumeLine, self.numberLine, self.titleLine):
field.setEnabled(False)
field.setText('')
field.setPlaceholderText('(multiple files)')
for field in (self.seriesLine,):
field.setEnabled(True)
field.setPlaceholderText('')
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
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):
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())
if self.bulkMode:
bulkData = {}
if self.cleanData(self.seriesLine.text()):
bulkData['Series'] = self.cleanData(self.seriesLine.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
fieldName = field.objectName().capitalize()[:-4] + 's'
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')
tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
if tmpData:
bulkData[fieldName] = tmpData
if not bulkData:
self.statusLabel.setText('No changes to apply.')
return
errors = []
total = len(self.files)
self.okButton.setEnabled(False)
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()
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):
return escape(s.strip())
@@ -1469,6 +1551,8 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
def __init__(self):
self.ui = QDialog()
self.parser = None
self.files = []
self.bulkMode = False
self.setupUi(self.ui)
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.okButton.clicked.connect(self.saveData)