mirror of
https://github.com/ciromattia/kcc
synced 2026-06-10 08:30:31 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e68ce380c | |||
| b95cf6e179 | |||
| 08070cdd97 | |||
| df3d174437 | |||
| dc4475bcb0 | |||
| b2e7fd3f5a | |||
| 4102643110 | |||
| ca6b3b7611 | |||
| 6d5db71b5b | |||
| 87c6b7143a | |||
| a5bd995a6b | |||
| bcc69b0f05 | |||
| f96adc5dc3 | |||
| f54b06e058 |
@@ -365,6 +365,7 @@ One time setup and running for the first time:
|
||||
```
|
||||
python -m venv venv
|
||||
venv\Scripts\activate.bat
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python kcc.py
|
||||
```
|
||||
@@ -390,6 +391,7 @@ One time setup and running for the first time:
|
||||
```
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python kcc.py
|
||||
```
|
||||
|
||||
@@ -62,6 +62,19 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="volumeLine"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="bulkVolumeCheck">
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> → sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> → range from 1 to 10<br><code>1, 3, 5</code> → specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
@@ -195,6 +208,7 @@
|
||||
<tabstops>
|
||||
<tabstop>seriesLine</tabstop>
|
||||
<tabstop>volumeLine</tabstop>
|
||||
<tabstop>bulkVolumeCheck</tabstop>
|
||||
<tabstop>titleLine</tabstop>
|
||||
<tabstop>numberLine</tabstop>
|
||||
<tabstop>writerLine</tabstop>
|
||||
|
||||
+310
-58
@@ -22,7 +22,7 @@ import itertools
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
|
||||
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
|
||||
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
||||
|
||||
import os
|
||||
@@ -393,7 +393,10 @@ class WorkerThread(QThread):
|
||||
for job in currentJobs:
|
||||
bookDir.append(job)
|
||||
try:
|
||||
fusion_source_parent = str(Path(bookDir[0]).parent)
|
||||
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
||||
if options.output is None:
|
||||
options.output = fusion_source_parent
|
||||
currentJobs.clear()
|
||||
currentJobs.append(comic2ebook.makeFusion(bookDir))
|
||||
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
|
||||
@@ -557,13 +560,6 @@ class WorkerThread(QThread):
|
||||
move(item, GUI.targetDirectory)
|
||||
except Exception:
|
||||
pass
|
||||
if options.filefusion:
|
||||
for path in currentJobs:
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
comic2ebook.checkPre('LLL-')
|
||||
GUI.progress.content = ''
|
||||
GUI.progress.stop()
|
||||
MW.hideProgressBar.emit()
|
||||
@@ -658,27 +654,45 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
|
||||
|
||||
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)
|
||||
# Multi-directory selection for bulk editing ComicInfo.xml
|
||||
dialog = QFileDialog(MW, 'Select volume directories', self.lastPath)
|
||||
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
||||
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
|
||||
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
|
||||
|
||||
# Enable multi-selection in the dialog (may not work with native dialog on all platforms)
|
||||
file_view = dialog.findChild(QListView, 'listView')
|
||||
if file_view:
|
||||
file_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
file_tree = dialog.findChild(QTreeView)
|
||||
if file_tree:
|
||||
file_tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
if dialog.exec():
|
||||
selected_dirs = dialog.selectedFiles()
|
||||
if selected_dirs:
|
||||
files = [os.path.join(d, 'ComicInfo.xml') for d in selected_dirs]
|
||||
self.lastPath = os.path.dirname(selected_dirs[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()
|
||||
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
|
||||
@@ -1536,61 +1550,299 @@ 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.')
|
||||
def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile):
|
||||
note = '<p><em>Note: Changing this field will overwrite all values in all selected files.</em></p>'
|
||||
|
||||
if len(valuesByFile) <= 20:
|
||||
rows = ''.join(
|
||||
'<tr>'
|
||||
f'<td style="padding:2px 6px; white-space:nowrap;">{escape(os.path.basename(f))}</td>'
|
||||
f'<td style="padding:2px 6px;">{escape(v)}</td>'
|
||||
'</tr>'
|
||||
for f, v in valuesByFile
|
||||
)
|
||||
|
||||
table = (
|
||||
'<table border="1" cellspacing="0" cellpadding="0">'
|
||||
'<tr>'
|
||||
'<th style="padding:2px 6px; text-align:left;">File</th>'
|
||||
'<th style="padding:2px 6px; text-align:left;">Value</th>'
|
||||
'</tr>'
|
||||
f'{rows}'
|
||||
'</table>'
|
||||
)
|
||||
else:
|
||||
counts = {}
|
||||
for _, v in valuesByFile:
|
||||
counts[v] = counts.get(v, 0) + 1
|
||||
|
||||
rows = ''.join(
|
||||
'<tr>'
|
||||
f'<td style="padding:2px 6px;">{escape(v)}</td>'
|
||||
f'<td style="padding:2px 6px; text-align:right;">{c}</td>'
|
||||
'</tr>'
|
||||
for v, c in sorted(counts.items(), key=lambda t: (-t[1], t[0]))
|
||||
)
|
||||
|
||||
table = (
|
||||
'<table border="1" cellspacing="0" cellpadding="0">'
|
||||
'<tr>'
|
||||
'<th style="padding:2px 6px; text-align:left;">Value</th>'
|
||||
'<th style="padding:2px 6px; text-align:right;">Count</th>'
|
||||
'</tr>'
|
||||
f'{rows}'
|
||||
'</table>'
|
||||
)
|
||||
|
||||
tooltipHTML = f'\
|
||||
<b>{escape(fieldLabel)}</b>\
|
||||
{note}\
|
||||
{table}\
|
||||
'
|
||||
|
||||
return tooltipHTML
|
||||
|
||||
def loadData(self, files):
|
||||
self.files = files if isinstance(files, list) else [files]
|
||||
self.bulkMode = len(self.files) > 1
|
||||
|
||||
# Sort files by name for consistent volume assignment
|
||||
self.files.sort()
|
||||
|
||||
# Unified CBR check for all files (both single and bulk mode)
|
||||
for file in self.files:
|
||||
parser = metadata.MetadataParser(file)
|
||||
if parser.format in ['RAR', 'RAR5']:
|
||||
self.editorWidget.setEnabled(False)
|
||||
self.okButton.setEnabled(False)
|
||||
self.statusLabel.setText('CBR files in selection are read-only.')
|
||||
return
|
||||
|
||||
if self.bulkMode:
|
||||
firstFile = self.files[0]
|
||||
self.parser = metadata.MetadataParser(firstFile)
|
||||
self.editorWidget.setEnabled(True)
|
||||
self.okButton.setEnabled(True)
|
||||
self.statusLabel.setText(f'Editing {len(self.files)} files.')
|
||||
|
||||
# Show bulk volume checkbox
|
||||
self.bulkVolumeCheck.setVisible(True)
|
||||
self.bulkVolumeCheck.setChecked(False)
|
||||
|
||||
for field in (self.volumeLine, self.numberLine, self.titleLine):
|
||||
field.setEnabled(False)
|
||||
field.setText('')
|
||||
field.setPlaceholderText('(multiple files)')
|
||||
field.setToolTip('')
|
||||
|
||||
# Load metadata for all files and show common values, or “(multiple values)” + tooltip.
|
||||
parsed = []
|
||||
for file in self.files:
|
||||
parsed.append((file, metadata.MetadataParser(file)))
|
||||
|
||||
field_specs = [
|
||||
(self.seriesLine, 'Series', lambda p: (p.data.get('Series', '') or '')),
|
||||
(self.writerLine, 'Writer', lambda p: ', '.join(p.data.get('Writers', []) or [])),
|
||||
(self.pencillerLine, 'Penciller', lambda p: ', '.join(p.data.get('Pencillers', []) or [])),
|
||||
(self.inkerLine, 'Inker', lambda p: ', '.join(p.data.get('Inkers', []) or [])),
|
||||
(self.coloristLine, 'Colorist', lambda p: ', '.join(p.data.get('Colorists', []) or [])),
|
||||
]
|
||||
|
||||
for line, label, extractor in field_specs:
|
||||
line.setEnabled(True)
|
||||
valuesByFile = [(f, extractor(p)) for f, p in parsed]
|
||||
uniqueValues = {v for _, v in valuesByFile}
|
||||
|
||||
if len(uniqueValues) == 1:
|
||||
common_value = valuesByFile[0][1] if valuesByFile else ''
|
||||
line.setPlaceholderText('')
|
||||
line.setToolTip('')
|
||||
line.setText(common_value)
|
||||
else:
|
||||
line.setText('')
|
||||
line.setPlaceholderText('(multiple values)')
|
||||
line.setToolTip(self._buildBulkFieldToolTip(label, valuesByFile))
|
||||
else:
|
||||
file = self.files[0]
|
||||
self.parser = metadata.MetadataParser(file)
|
||||
|
||||
# Hide bulk volume checkbox in single file mode
|
||||
self.bulkVolumeCheck.setVisible(False)
|
||||
|
||||
for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine,
|
||||
self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||
field.setEnabled(True)
|
||||
field.setPlaceholderText('')
|
||||
|
||||
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)
|
||||
|
||||
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.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||
self.ui.close()
|
||||
tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
|
||||
if tmpData:
|
||||
bulkData[fieldName] = tmpData
|
||||
# Handle bulk volume editing
|
||||
volumes = None
|
||||
if self.bulkVolumeCheck.isChecked():
|
||||
volumeText = self.volumeLine.text()
|
||||
volumes, error = self.parseVolumeInput(volumeText, len(self.files))
|
||||
if error:
|
||||
self.statusLabel.setText(error)
|
||||
return
|
||||
|
||||
if not bulkData and volumes is None:
|
||||
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
|
||||
# Set volume if bulk volume editing is enabled
|
||||
if volumes is not None:
|
||||
parser.data['Volume'] = str(volumes[i - 1])
|
||||
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')
|
||||
self.statusLabel.setText('Errors occurred.')
|
||||
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.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())
|
||||
|
||||
def parseVolumeInput(self, text, fileCount):
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return None, None
|
||||
|
||||
volumes = []
|
||||
|
||||
# Check if it's a range (e.g., "5-10")
|
||||
if '-' in text and ',' not in text:
|
||||
parts = text.split('-')
|
||||
if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
|
||||
return None, 'Invalid range format (use start-end)'
|
||||
try:
|
||||
start = int(parts[0].strip())
|
||||
end = int(parts[1].strip())
|
||||
if start < 0 or end < 0:
|
||||
return None, 'Volume numbers must be positive'
|
||||
if start > end:
|
||||
return None, 'Invalid range: start > end'
|
||||
volumes = list(range(start, end + 1))
|
||||
except ValueError:
|
||||
return None, 'Invalid range format'
|
||||
# Check if it's a comma-separated list (e.g., "1,3,5")
|
||||
elif ',' in text:
|
||||
try:
|
||||
volumes = [int(v.strip()) for v in text.split(',') if v.strip()]
|
||||
if any(v < 0 for v in volumes):
|
||||
return None, 'Volume numbers must be positive'
|
||||
except ValueError:
|
||||
return None, 'Invalid list format'
|
||||
# Single number - generate sequence starting from that number
|
||||
else:
|
||||
try:
|
||||
start = int(text)
|
||||
if start < 0:
|
||||
return None, 'Volume number must be positive'
|
||||
volumes = list(range(start, start + fileCount))
|
||||
except ValueError:
|
||||
return None, 'Invalid number'
|
||||
|
||||
# Validate count
|
||||
if not volumes:
|
||||
return None, 'No valid volume numbers parsed'
|
||||
if len(volumes) != fileCount:
|
||||
return None, f'Volume count ({len(volumes)}) != file count ({fileCount})'
|
||||
|
||||
return volumes, None
|
||||
|
||||
def toggleBulkVolume(self, checked):
|
||||
self.volumeLine.setEnabled(checked)
|
||||
if checked:
|
||||
self.volumeLine.setText('')
|
||||
self.volumeLine.setPlaceholderText('e.g., 5 or 1-10 or 1,3,5')
|
||||
else:
|
||||
self.volumeLine.setText('')
|
||||
self.volumeLine.setPlaceholderText('(multiple files)')
|
||||
|
||||
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.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume)
|
||||
|
||||
self.okButton.clicked.connect(self.saveData)
|
||||
self.cancelButton.clicked.connect(self.ui.close)
|
||||
if sys.platform.startswith('linux'):
|
||||
|
||||
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
||||
QVBoxLayout, QWidget)
|
||||
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
|
||||
QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
||||
QSizePolicy, QVBoxLayout, QWidget)
|
||||
from . import KCC_rc
|
||||
|
||||
class Ui_editorDialog(object):
|
||||
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
|
||||
|
||||
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
|
||||
|
||||
self.bulkVolumeCheck = QCheckBox(self.editorWidget)
|
||||
self.bulkVolumeCheck.setObjectName(u"bulkVolumeCheck")
|
||||
self.bulkVolumeCheck.setVisible(False)
|
||||
|
||||
self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1)
|
||||
|
||||
self.label_3 = QLabel(self.editorWidget)
|
||||
self.label_3.setObjectName(u"label_3")
|
||||
|
||||
@@ -157,7 +163,8 @@ class Ui_editorDialog(object):
|
||||
self.verticalLayout.addWidget(self.optionWidget)
|
||||
|
||||
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.titleLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck)
|
||||
QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine)
|
||||
QWidget.setTabOrder(self.titleLine, self.numberLine)
|
||||
QWidget.setTabOrder(self.numberLine, self.writerLine)
|
||||
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
|
||||
@@ -175,6 +182,10 @@ class Ui_editorDialog(object):
|
||||
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
|
||||
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
|
||||
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.bulkVolumeCheck.setToolTip(QCoreApplication.translate("editorDialog", u"<b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> \u2192 sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> \u2192 range from 1 to 10<br><code>1, 3, 5</code> \u2192 specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.bulkVolumeCheck.setText("")
|
||||
self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
|
||||
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
|
||||
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '10.1.3'
|
||||
__version__ = '10.2.0'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -76,23 +76,19 @@ def main(argv=None):
|
||||
print('No matching files found.')
|
||||
return 1
|
||||
if options.filefusion:
|
||||
fusion_source_parent = str(Path(sources[0]).parent)
|
||||
fusion_path = makeFusion(list(sources))
|
||||
sources.clear()
|
||||
sources.append(fusion_path)
|
||||
for source in sources:
|
||||
source = source.rstrip('\\').rstrip('/')
|
||||
options = copy(args)
|
||||
if options.filefusion and options.output is None:
|
||||
options.output = fusion_source_parent
|
||||
options = checkOptions(options)
|
||||
print('Working on ' + source + '...')
|
||||
makeBook(source)
|
||||
|
||||
if options.filefusion:
|
||||
for path in sources:
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
checkPre('LLL-')
|
||||
return 0
|
||||
|
||||
|
||||
@@ -903,9 +899,11 @@ def getWorkFolder(afile, workdir=None):
|
||||
else:
|
||||
check_path = gettempdir()
|
||||
|
||||
DISK_WARNING = "Not enough disk space to perform conversion. Try Temp Directory option."
|
||||
|
||||
if os.path.isdir(afile):
|
||||
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
raise UserWarning(DISK_WARNING)
|
||||
try:
|
||||
copytree(afile, fullPath)
|
||||
sanitizePermissions(fullPath)
|
||||
@@ -915,7 +913,7 @@ def getWorkFolder(afile, workdir=None):
|
||||
raise UserWarning("Failed to prepare a workspace.")
|
||||
elif os.path.isfile(afile):
|
||||
if disk_usage(check_path)[2]< os.path.getsize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
raise UserWarning(DISK_WARNING)
|
||||
if afile.lower().endswith('.pdf'):
|
||||
if not os.path.exists(fullPath):
|
||||
os.makedirs(fullPath)
|
||||
@@ -977,10 +975,11 @@ def getWorkFolder(afile, workdir=None):
|
||||
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
|
||||
ordered_image_paths = []
|
||||
for i, spine_item in enumerate(spine):
|
||||
if spine_item not in manifest_dict:
|
||||
try:
|
||||
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
|
||||
page = ET.parse(page_path)
|
||||
except Exception:
|
||||
continue
|
||||
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
|
||||
page = ET.parse(page_path)
|
||||
imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
|
||||
|
||||
largest_size = 0
|
||||
@@ -1688,7 +1687,7 @@ def makeFusion(sources: List[str]):
|
||||
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
|
||||
start = perf_counter()
|
||||
first_path = Path(sources[0])
|
||||
|
||||
|
||||
if options.tempdir:
|
||||
fusion_parent = first_path.parent
|
||||
else:
|
||||
@@ -1897,11 +1896,11 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
|
||||
end = perf_counter()
|
||||
print(f"{job_progress}makeBook: {end - start} seconds")
|
||||
# Clean up temporary workspace
|
||||
try:
|
||||
rmtree(path, True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if options.filefusion:
|
||||
rmtree(source, True)
|
||||
checkPre('LLL-')
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def mergeDirectory(work):
|
||||
imagesValid.append(i[0])
|
||||
# Silently drop directories that contain too many images
|
||||
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
|
||||
if targetHeight > 131072 * 4:
|
||||
if targetHeight > 131072 * 8:
|
||||
raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
|
||||
result = Image.new('RGB', (targetWidth, targetHeight))
|
||||
y = 0
|
||||
|
||||
@@ -165,7 +165,10 @@ class ComicPageParser:
|
||||
with Image.open(srcImgPath) as im:
|
||||
self.image = im.copy()
|
||||
|
||||
self.fill = self.fillCheck()
|
||||
self.page_background_color = self.fillCheck()
|
||||
self.fill = self.page_background_color
|
||||
if self.opt.bordersColor:
|
||||
self.fill = self.opt.bordersColor
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
@@ -195,9 +198,9 @@ class ComicPageParser:
|
||||
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
||||
new_image.paste(pageone, (0, 0))
|
||||
new_image.paste(pagetwo, (0, height))
|
||||
self.payload.append(['N', self.source, new_image, self.fill])
|
||||
self.payload.append(['N', self.source, new_image, self.page_background_color, self.fill])
|
||||
elif self.opt.webtoon:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
||||
# rotate only TODO dead code?
|
||||
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
|
||||
spread = self.image
|
||||
@@ -206,11 +209,12 @@ class ComicPageParser:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
else:
|
||||
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
|
||||
# elif wide enough to split
|
||||
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
|
||||
# if (split) or (split and rotate)
|
||||
if self.opt.splitter != 1 and width / height < 1.75:
|
||||
BISECT_THRESHOLD = 1.8
|
||||
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
|
||||
if width > height:
|
||||
leftbox = (0, 0, int(width / 2), height)
|
||||
rightbox = (int(width / 2), 0, width, height)
|
||||
@@ -223,23 +227,23 @@ class ComicPageParser:
|
||||
else:
|
||||
pageone = self.image.crop(leftbox)
|
||||
pagetwo = self.image.crop(rightbox)
|
||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||
self.payload.append(['S1', self.source, pageone, self.page_background_color, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
|
||||
|
||||
# if (rotate) or (split and rotate)
|
||||
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
|
||||
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
if not self.opt.rotateright:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
else:
|
||||
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
|
||||
else:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
||||
|
||||
def fillCheck(self):
|
||||
if self.opt.bordersColor:
|
||||
if False:
|
||||
return self.opt.bordersColor
|
||||
else:
|
||||
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
||||
@@ -278,7 +282,7 @@ class ComicPageParser:
|
||||
|
||||
|
||||
class ComicPage:
|
||||
def __init__(self, options, mode, path, image, fill):
|
||||
def __init__(self, options, mode, path, image, page_background_color, fill):
|
||||
self.opt = options
|
||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||
if self.opt.hq:
|
||||
@@ -288,6 +292,7 @@ class ComicPage:
|
||||
self.image = image.convert("RGB")
|
||||
self.color = self.colorCheck()
|
||||
self.colorOutput = self.color and self.opt.forcecolor
|
||||
self.page_background_color = page_background_color
|
||||
self.fill = fill
|
||||
self.rotated = False
|
||||
self.orgPath = os.path.join(path[0], path[1])
|
||||
@@ -561,7 +566,7 @@ class ComicPage:
|
||||
self.image = self.image.crop(box)
|
||||
|
||||
def cropPageNumber(self, power, minimum):
|
||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.page_background_color)
|
||||
|
||||
if bbox:
|
||||
w, h = self.image.size
|
||||
@@ -571,7 +576,7 @@ class ComicPage:
|
||||
self.maybeCrop(bbox, minimum)
|
||||
|
||||
def cropMargin(self, power, minimum):
|
||||
bbox = get_bbox_crop_margin(self.image, power, self.fill)
|
||||
bbox = get_bbox_crop_margin(self.image, power, self.page_background_color)
|
||||
|
||||
if bbox:
|
||||
w, h = self.image.size
|
||||
@@ -581,7 +586,7 @@ class ComicPage:
|
||||
self.maybeCrop(bbox, minimum)
|
||||
|
||||
def cropInterPanelEmptySections(self, direction):
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.page_background_color)
|
||||
|
||||
class Cover:
|
||||
def __init__(self, source, opt):
|
||||
|
||||
@@ -8,4 +8,4 @@ mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy<2
|
||||
PyMuPDF>=1.26.1
|
||||
PyMuPDF==1.25.5
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PySide6==6.1.3
|
||||
Pillow>=9
|
||||
psutil>=5.9.5
|
||||
requests>=2.34.2
|
||||
requests>=2.32.4
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
|
||||
Reference in New Issue
Block a user