revamp scripts access
This commit is contained in:
parent
c870ed86e0
commit
4d02e88905
@ -108,6 +108,34 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
def deselect_all(self) -> None:
|
||||
self.twList.setRangeSelected(QtWidgets.QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), False)
|
||||
|
||||
def remove_paths(self, file_list: list[str]) -> None:
|
||||
flist = file_list
|
||||
current_removed = False
|
||||
self.twList.setSortingEnabled(False)
|
||||
for row in reversed(range(self.twList.rowCount())):
|
||||
print(row)
|
||||
ca = self.get_archive_by_row(row)
|
||||
print(flist, str(ca.path.absolute()))
|
||||
if ca and str(ca.path.absolute()) in flist:
|
||||
flist.remove(str(ca.path.absolute()))
|
||||
self.twList.removeRow(row)
|
||||
if row == self.twList.currentRow():
|
||||
current_removed = True
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
self.items_removed(current_removed)
|
||||
|
||||
def items_removed(self, current_removed: bool):
|
||||
if self.twList.rowCount() > 0 and current_removed:
|
||||
# since on a removal, we select row 0, make sure callback occurs if
|
||||
# we're already there
|
||||
if self.twList.currentRow() == 0:
|
||||
self.current_item_changed_cb(self.twList.currentItem(), None)
|
||||
self.twList.selectRow(0)
|
||||
elif self.twList.rowCount() <= 0:
|
||||
self.listCleared.emit()
|
||||
|
||||
def remove_archive_list(self, ca_list: list[ComicArchive]) -> None:
|
||||
self.twList.setSortingEnabled(False)
|
||||
current_removed = False
|
||||
@ -120,15 +148,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
self.twList.removeRow(row)
|
||||
break
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
if self.twList.rowCount() > 0 and current_removed:
|
||||
# since on a removal, we select row 0, make sure callback occurs if
|
||||
# we're already there
|
||||
if self.twList.currentRow() == 0:
|
||||
self.current_item_changed_cb(self.twList.currentItem(), None)
|
||||
self.twList.selectRow(0)
|
||||
elif self.twList.rowCount() <= 0:
|
||||
self.listCleared.emit()
|
||||
self.items_removed(current_removed)
|
||||
|
||||
def get_archive_by_row(self, row: int) -> Optional[ComicArchive]:
|
||||
if row >= 0:
|
||||
@ -166,14 +186,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
# since on a removal, we select row 0, make sure callback occurs if
|
||||
# we're already there
|
||||
if self.twList.currentRow() == 0:
|
||||
self.current_item_changed_cb(self.twList.currentItem(), None)
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
self.items_removed(True)
|
||||
|
||||
def add_path_list(self, pathlist: list[str]) -> None:
|
||||
|
||||
@ -345,11 +358,11 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
fi.ca.read_cix()
|
||||
fi.ca.has_cbi()
|
||||
|
||||
def get_selected_archive_list(self) -> List[ComicArchive]:
|
||||
def get_archive_list(self, all_comics=False) -> List[ComicArchive]:
|
||||
ca_list: List[ComicArchive] = []
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if item.isSelected():
|
||||
if item.isSelected() or all_comics:
|
||||
fi: FileInfo = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
ca_list.append(fi.ca)
|
||||
|
||||
|
@ -202,7 +202,7 @@ def ctmain() -> None:
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(opts.file_list, SETTINGS, opts=opts)
|
||||
tagger_window = TaggerWindow(opts.files, SETTINGS, opts=opts)
|
||||
tagger_window.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("app.png")))
|
||||
tagger_window.show()
|
||||
|
||||
|
@ -49,7 +49,6 @@ from comictaggerlib.fileselectionlist import FileInfo, FileSelectionList
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
from comictaggerlib.logwindow import LogWindow
|
||||
from comictaggerlib.optionalmsgdialog import OptionalMessageDialog
|
||||
from comictaggerlib.options import Options
|
||||
from comictaggerlib.pagebrowser import PageBrowserWindow
|
||||
from comictaggerlib.pagelisteditor import PageListEditor
|
||||
from comictaggerlib.renamewindow import RenameWindow
|
||||
@ -76,13 +75,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
file_list: list[str],
|
||||
settings: ComicTaggerSettings,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
opts: Optional[Options] = None,
|
||||
opts = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("taggerwindow.ui"), self)
|
||||
self.settings = settings
|
||||
|
||||
self.window_ref = {}
|
||||
|
||||
# prevent multiple instances
|
||||
socket = QtNetwork.QLocalSocket(self)
|
||||
socket.connectToServer(settings.install_id)
|
||||
@ -148,10 +149,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("app.png")))
|
||||
# TODO: this needs to be looked at
|
||||
if opts is not None and opts.data_style is not None:
|
||||
if opts is not None and opts.type is not None:
|
||||
# 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
|
||||
settings.last_selected_save_data_style = opts.type
|
||||
settings.last_selected_load_data_style = opts.type
|
||||
|
||||
self.save_data_style = settings.last_selected_save_data_style
|
||||
self.load_data_style = settings.last_selected_load_data_style
|
||||
@ -387,6 +388,8 @@ Have fun!
|
||||
self.actionPageBrowser.setStatusTip("Show the page browser")
|
||||
self.actionPageBrowser.triggered.connect(self.show_page_browser)
|
||||
|
||||
self.actionFind_Duplicate_Comics.triggered.connect(self.find_dupes)
|
||||
|
||||
# Help Menu
|
||||
self.actionAbout.setStatusTip("Show the " + self.appName + " info")
|
||||
self.actionAbout.triggered.connect(self.about_app)
|
||||
@ -420,8 +423,20 @@ Have fun!
|
||||
self.toolBar.addAction(self.actionPageBrowser)
|
||||
self.toolBar.addAction(self.actionAutoImprint)
|
||||
|
||||
def find_dupes(self):
|
||||
import comictaggerscripts.find_dupes
|
||||
window = comictaggerscripts.find_dupes.main((str(x.path.absolute()) for x in self.fileSelectionList.get_archive_list(True)), self.settings)
|
||||
window.closed.connect(self.finish_dupes)
|
||||
window.show()
|
||||
self.window_ref['comictaggerscripts.find_dupes'] = window
|
||||
|
||||
def finish_dupes(self, files: list[str]):
|
||||
self.fileSelectionList.remove_paths(files)
|
||||
if 'comictaggerscripts.find_dupes' in self.window_ref:
|
||||
del self.window_ref['comictaggerscripts.find_dupes']
|
||||
|
||||
def repackage_archive(self) -> None:
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
ca_list = self.fileSelectionList.get_archive_list()
|
||||
rar_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.is_rar():
|
||||
@ -1485,7 +1500,7 @@ Please choose options below, and select OK.
|
||||
|
||||
def remove_tags(self, style: int) -> None:
|
||||
# remove the indicated tags from the archive
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
ca_list = self.fileSelectionList.get_archive_list()
|
||||
has_md_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.has_metadata(style):
|
||||
@ -1560,7 +1575,7 @@ Please choose options below, and select OK.
|
||||
|
||||
def copy_tags(self) -> None:
|
||||
# copy the indicated tags in the archive
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
ca_list = self.fileSelectionList.get_archive_list()
|
||||
has_src_count = 0
|
||||
|
||||
src_style = self.load_data_style
|
||||
@ -1787,7 +1802,7 @@ Please choose options below, and select OK.
|
||||
return success, match_results
|
||||
|
||||
def auto_tag(self) -> None:
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
ca_list = self.fileSelectionList.get_archive_list()
|
||||
style = self.save_data_style
|
||||
|
||||
if len(ca_list) == 0:
|
||||
@ -2011,7 +2026,7 @@ Please choose options below, and select OK to Auto-Tag.
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
|
||||
def rename_archive(self) -> None:
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
ca_list = self.fileSelectionList.get_archive_list()
|
||||
|
||||
if len(ca_list) == 0:
|
||||
QtWidgets.QMessageBox.information(self, "Rename", "No archives selected!")
|
||||
|
@ -1182,7 +1182,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>21</height>
|
||||
<height>30</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
||||
@ -1245,6 +1245,7 @@
|
||||
<string>Window</string>
|
||||
</property>
|
||||
<addaction name="actionPageBrowser"/>
|
||||
<addaction name="actionFind_Duplicate_Comics"/>
|
||||
</widget>
|
||||
<addaction name="menuComicTagger"/>
|
||||
<addaction name="menuTags"/>
|
||||
@ -1455,6 +1456,11 @@
|
||||
<string>Parse Filename and split words</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionFind_Duplicate_Comics">
|
||||
<property name="text">
|
||||
<string>Find Duplicate Comics</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<resources/>
|
||||
|
@ -3,22 +3,26 @@
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import typing
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import filetype
|
||||
import typing
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.ui.qtutils import centerWindowOnParent
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.imagehasher import ImageHasher
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib.filerenamer import FileRenamer
|
||||
from comictaggerlib.imagehasher import ImageHasher
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent
|
||||
|
||||
root = 1 << 31 - 1
|
||||
something = 1 << 31 - 1
|
||||
@ -36,14 +40,15 @@ class ImageMeta:
|
||||
|
||||
class Duplicate:
|
||||
"""docstring for Duplicate"""
|
||||
imageHashes: Dict[str, ImageMeta]
|
||||
|
||||
imageHashes: dict[str, ImageMeta]
|
||||
|
||||
def __init__(self, path, metadata: GenericMetadata, ca: ComicArchive, cover):
|
||||
self.path = path
|
||||
self.digest = ""
|
||||
self.ca = ca
|
||||
self.ca: ComicArchive = ca
|
||||
self.metadata = metadata
|
||||
self.imageHashes = dict()
|
||||
self.imageHashes = {}
|
||||
self.duplicateImages = set()
|
||||
self.extras = set()
|
||||
self.extractedPath = ""
|
||||
@ -53,32 +58,35 @@ class Duplicate:
|
||||
self.imageCount = 0
|
||||
self.cover = cover
|
||||
blake2b = hashlib.blake2b(digest_size=16)
|
||||
for f in open(self.path, "rb"):
|
||||
blake2b.update(f)
|
||||
|
||||
with open(self.path, "rb") as f:
|
||||
for line in f:
|
||||
blake2b.update(line)
|
||||
|
||||
self.digest = blake2b.hexdigest()
|
||||
|
||||
def extract(self, directory):
|
||||
if self.ca.seemsToBeAComicArchive():
|
||||
if self.ca.seems_to_be_a_comic_archive():
|
||||
self.extractedPath = directory
|
||||
for filepath in self.ca.archiver.getArchiveFilenameList():
|
||||
for filepath in self.ca.archiver.get_filename_list():
|
||||
filename = os.path.basename(filepath)
|
||||
if filename.lower() in ["comicinfo.xml"]:
|
||||
continue
|
||||
|
||||
self.fileCount += 1
|
||||
archived_file = self.ca.archiver.readArchiveFile(filepath)
|
||||
archived_file = self.ca.archiver.read_file(filepath)
|
||||
|
||||
image_type = filetype.image_match(archived_file)
|
||||
if image_type is not None:
|
||||
self.imageCount += 1
|
||||
file_hash = hashlib.blake2b(archived_file, digest_size=16).hexdigest().upper()
|
||||
if file_hash in self.imageHashes.keys():
|
||||
if file_hash in self.imageHashes:
|
||||
self.duplicateImages.add(filename)
|
||||
else:
|
||||
image_hash = ImageHasher(data=archived_file, width=12, height=12).average_hash()
|
||||
self.imageHashes[file_hash] = ImageMeta(os.path.join(self.extractedPath, filename), file_hash,
|
||||
image_hash, image_type.extension)
|
||||
self.imageHashes[file_hash] = ImageMeta(
|
||||
os.path.join(self.extractedPath, filename), file_hash, image_hash, image_type.extension
|
||||
)
|
||||
else:
|
||||
self.extras.add(filename)
|
||||
|
||||
@ -101,8 +109,8 @@ class Duplicate:
|
||||
|
||||
|
||||
class Tree(QtCore.QAbstractListModel):
|
||||
def __init__(self, item: List[List[Duplicate]]):
|
||||
super(Tree, self).__init__()
|
||||
def __init__(self, item: list[list[Duplicate]]):
|
||||
super().__init__()
|
||||
self.rootItem = item
|
||||
|
||||
def rowCount(self, index: QtCore.QModelIndex = ...) -> int:
|
||||
@ -122,38 +130,49 @@ class Tree(QtCore.QAbstractListModel):
|
||||
return QtCore.QVariant()
|
||||
|
||||
f = FileRenamer(self.rootItem[index.row()][0].metadata)
|
||||
f.setTemplate("{series} #{issue} - {title} ({year})")
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
return f.determineName('')
|
||||
elif role == QtCore.Qt.UserRole:
|
||||
return f.determineName('')
|
||||
f.set_template("{series} #{issue} - {title} ({year})")
|
||||
if role in [QtCore.Qt.DisplayRole, QtCore.Qt.UserRole]:
|
||||
return f.determine_name("")
|
||||
return QtCore.QVariant()
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
def __init__(self, file_list, style, work_path, parent=None):
|
||||
closed = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, file_list, style, work_path, settings, parent=None):
|
||||
super().__init__(parent)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile("../../scripts/mainwindow.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("../../comictaggerscripts/mainwindow.ui"), self)
|
||||
self.dupes = []
|
||||
self.removed: list[str] = []
|
||||
self.firstRun = 0
|
||||
self.dupe_set_list: List[List[Duplicate]] = list()
|
||||
self.dupe_set_list: list[list[Duplicate]] = []
|
||||
self.style = style
|
||||
if work_path == "":
|
||||
work_path = tempfile.mkdtemp()
|
||||
self.work_path = work_path
|
||||
self.work_path = tempfile.mkdtemp()
|
||||
else:
|
||||
self.work_path = work_path
|
||||
self.initFiles = file_list
|
||||
self.settings = settings
|
||||
self.dupe_set_qlist.clicked.connect(self.dupe_set_clicked)
|
||||
self.dupe_set_qlist.doubleClicked.connect(self.dupe_set_double_clicked)
|
||||
self.actionCompare_Comic.triggered.connect(self.compare_action)
|
||||
|
||||
def comic_deleted(self, archive_path):
|
||||
self.update_dupes()
|
||||
def __del__(self):
|
||||
print("fuck this")
|
||||
shutil.rmtree(self.work_path, True)
|
||||
|
||||
def closeEvent(self, event: QtGui.QCloseEvent):
|
||||
self.closed.emit(self.removed)
|
||||
event.accept()
|
||||
|
||||
def comic_deleted(self, archive_path: str):
|
||||
self.removed.append(archive_path)
|
||||
|
||||
def update_dupes(self):
|
||||
# print("updating duplicates")
|
||||
new_set_list = list()
|
||||
new_set_list = []
|
||||
for dupe in self.dupe_set_list:
|
||||
dupe_list = list()
|
||||
dupe_list = []
|
||||
for d in dupe:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if os.path.exists(d.path):
|
||||
@ -165,16 +184,20 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
new_set_list.append(dupe_list)
|
||||
else:
|
||||
dupe_list[0].clean()
|
||||
self.dupe_set_list: List[List[Duplicate]] = new_set_list
|
||||
self.dupe_set_list: list[list[Duplicate]] = new_set_list
|
||||
self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
|
||||
if new_set_list:
|
||||
self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
|
||||
|
||||
self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
|
||||
self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
|
||||
self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
|
||||
else:
|
||||
self.clear_dupe_list()
|
||||
|
||||
def compare(self, i):
|
||||
if len(self.dupe_set_list) > i:
|
||||
dw = DupeWindow(self.dupe_set_list[i], self.work_path, self)
|
||||
dw.closed.connect(self.update_dupes)
|
||||
dw.comic_deleted.connect(self.comic_deleted)
|
||||
dw.show()
|
||||
|
||||
def compare_action(self, b):
|
||||
@ -185,25 +208,35 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
def dupe_set_double_clicked(self, index: QtCore.QModelIndex):
|
||||
self.compare(index.row())
|
||||
|
||||
def dupe_set_clicked(self, index: QtCore.QModelIndex):
|
||||
def clear_dupe_list(self):
|
||||
for f in self.dupe_list.children():
|
||||
f.deleteLater()
|
||||
self.dupe_set_list[index.row()].sort(key=lambda k: k.digest)
|
||||
for i, f in enumerate(self.dupe_set_list[index.row()]):
|
||||
color = "black"
|
||||
if i > 0:
|
||||
if self.dupe_set_list[index.row()][i - 1].digest == f.digest:
|
||||
color = "green"
|
||||
elif i == 0:
|
||||
if len(self.dupe_set_list[index.row()]) > 1:
|
||||
if self.dupe_set_list[index.row()][i + 1].digest == f.digest:
|
||||
if isinstance(f, DupeImage):
|
||||
f.deleteLater()
|
||||
|
||||
def dupe_set_clicked(self, index: QtCore.QModelIndex):
|
||||
self.clear_dupe_list()
|
||||
if self.dupe_set_list:
|
||||
self.dupe_set_list[index.row()].sort(key=lambda k: k.digest)
|
||||
for i, f in enumerate(self.dupe_set_list[index.row()]):
|
||||
color = "black"
|
||||
if i > 0:
|
||||
if self.dupe_set_list[index.row()][i - 1].digest == f.digest:
|
||||
color = "green"
|
||||
ql = DupeImage(duplicate=f, style=f".path {{color: black;}}.hash {{color: {color};}}",
|
||||
parent=self.dupe_list)
|
||||
ql.deleted.connect(self.update_dupes)
|
||||
ql.setMinimumWidth(300)
|
||||
ql.setMinimumHeight(500)
|
||||
self.dupe_list.layout().addWidget(ql)
|
||||
elif i == 0:
|
||||
if len(self.dupe_set_list[index.row()]) > 1:
|
||||
if self.dupe_set_list[index.row()][i + 1].digest == f.digest:
|
||||
color = "green"
|
||||
ql = DupeImage(
|
||||
duplicate=f, style=f".path {{color: black;}}.hash {{color: {color};}}", parent=self.dupe_list
|
||||
)
|
||||
ql.deleted.connect(self.update_dupes)
|
||||
ql.deleted.connect(self.comic_deleted)
|
||||
ql.setMinimumWidth(300)
|
||||
ql.setMinimumHeight(500)
|
||||
print(self.dupe_list.layout())
|
||||
|
||||
layout = self.dupe_list.layout()
|
||||
layout.addWidget(ql)
|
||||
|
||||
def showEvent(self, event: QtGui.QShowEvent):
|
||||
if self.firstRun == 0:
|
||||
@ -211,15 +244,20 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.load_files(self.initFiles)
|
||||
if len(self.dupe_set_list) < 1:
|
||||
dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.NoIcon, "ComicTagger Duplicate finder",
|
||||
"No duplicate comics found", QtWidgets.QMessageBox.Ok, parent=self)
|
||||
dialog = QtWidgets.QMessageBox(
|
||||
QtWidgets.QMessageBox.NoIcon,
|
||||
"ComicTagger Duplicate finder",
|
||||
"No duplicate comics found",
|
||||
QtWidgets.QMessageBox.Ok,
|
||||
parent=self,
|
||||
)
|
||||
dialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
qw = QtWidgets.QWidget()
|
||||
qw.setFixedWidth(90)
|
||||
dialog.layout().addWidget(qw, 3, 2, 1, 3)
|
||||
dialog.exec()
|
||||
QtWidgets.QApplication.quit()
|
||||
sys.exit(0)
|
||||
QtCore.QTimer.singleShot(1, self.close)
|
||||
self.close()
|
||||
self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
|
||||
self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
|
||||
|
||||
@ -230,7 +268,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
dialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
dialog.setMinimumDuration(300)
|
||||
dialog.setMinimumWidth(400)
|
||||
centerWindowOnParent(dialog)
|
||||
center_window_on_parent(dialog)
|
||||
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
@ -240,28 +278,31 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
break
|
||||
dialog.setValue(dialog.value() + 1)
|
||||
dialog.setLabelText(filename)
|
||||
ca = ComicArchive(path=filename, rar_exe_path=settings.rar_exe_path,
|
||||
default_image_path=ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
if ca.seemsToBeAComicArchive() and ca.hasMetadata(self.style):
|
||||
ca = ComicArchive(
|
||||
path=filename,
|
||||
rar_exe_path=self.settings.rar_exe_path,
|
||||
default_image_path=ComicTaggerSettings.get_graphic("nocover.png"),
|
||||
)
|
||||
if ca.seems_to_be_a_comic_archive() and ca.has_metadata(self.style):
|
||||
# fmt_str = "{{0:{0}}}".format(max_name_len)
|
||||
# print(fmt_str.format(filename) + "\r", end='', file=sys.stderr)
|
||||
# sys.stderr.flush()
|
||||
md = ca.readMetadata(self.style)
|
||||
cover = ca.getPage(0)
|
||||
md = ca.read_metadata(self.style)
|
||||
cover = ca.get_page(0)
|
||||
comic_list.append((make_key(md), filename, ca, md, cover))
|
||||
# max_name_len = len(filename)
|
||||
|
||||
comic_list.sort(key=itemgetter(0), reverse=False)
|
||||
|
||||
# look for duplicate blocks
|
||||
dupe_set = list()
|
||||
dupe_set = []
|
||||
prev_key = ""
|
||||
|
||||
dialog.setWindowTitle("Finding Duplicates")
|
||||
dialog.setMaximum(len(comic_list))
|
||||
dialog.setValue(dialog.minimum())
|
||||
|
||||
set_list = list()
|
||||
set_list = []
|
||||
for new_key, filename, ca, md, cover in comic_list:
|
||||
dialog.setValue(dialog.value() + 1)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -277,7 +318,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
# only add if the dupe list has 2 or more
|
||||
if len(dupe_set) > 1:
|
||||
set_list.append(dupe_set)
|
||||
dupe_set = list()
|
||||
dupe_set = []
|
||||
dupe_set.append((filename, ca, md, cover))
|
||||
|
||||
prev_key = new_key
|
||||
@ -287,7 +328,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
set_list.append(dupe_set)
|
||||
|
||||
for d_set in set_list:
|
||||
new_set = list()
|
||||
new_set = []
|
||||
for filename, ca, md, cover in d_set:
|
||||
new_set.append(Duplicate(filename, md, ca, cover))
|
||||
self.dupe_set_list.append(new_set)
|
||||
@ -300,7 +341,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
# working_dir = os.path.join(self.tmp, "working")
|
||||
# s = False
|
||||
# # while working and len(dupe_set) > 1:
|
||||
# remaining = list()
|
||||
# remaining = []
|
||||
# for dupe_set in self.dupe_set_list:
|
||||
# not_deleted = True
|
||||
# if os.path.exists(working_dir):
|
||||
@ -332,17 +373,18 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
class DupeWindow(QtWidgets.QWidget):
|
||||
closed = QtCore.pyqtSignal()
|
||||
comic_deleted = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, duplicates: List[Duplicate], tmp, parent=None):
|
||||
def __init__(self, duplicates: list[Duplicate], tmp, parent=None):
|
||||
super().__init__(parent, QtCore.Qt.Window)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile("../../scripts/dupe.ui"), self)
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("../../comictaggerscripts/dupe.ui"), self)
|
||||
|
||||
for f in self.comic1Image.children():
|
||||
f.deleteLater()
|
||||
for f in self.comic2Image.children():
|
||||
f.deleteLater()
|
||||
self.deleting = -1
|
||||
self.duplicates = duplicates
|
||||
self.duplicates: list[Duplicate] = duplicates
|
||||
self.dupe1 = -1
|
||||
self.dupe2 = -1
|
||||
|
||||
@ -394,7 +436,7 @@ class DupeWindow(QtWidgets.QWidget):
|
||||
self.display_dupe()
|
||||
|
||||
def update_dupes(self):
|
||||
dupes: List[Duplicate] = list()
|
||||
dupes: list[Duplicate] = []
|
||||
for f in self.duplicates:
|
||||
if os.path.exists(f.path):
|
||||
dupes.append(f)
|
||||
@ -403,6 +445,7 @@ class DupeWindow(QtWidgets.QWidget):
|
||||
self.duplicates = dupes
|
||||
if len(self.duplicates) < 2:
|
||||
self.close()
|
||||
self.dupeList.clear()
|
||||
|
||||
for i, dupe in enumerate(self.duplicates):
|
||||
item = QtWidgets.QListWidgetItem()
|
||||
@ -418,14 +461,16 @@ class DupeWindow(QtWidgets.QWidget):
|
||||
|
||||
def delete_1(self):
|
||||
self.duplicates[self.dupe1].delete()
|
||||
self.comic_deleted.emit(self.duplicates[self.dupe1].path)
|
||||
self.update_dupes()
|
||||
|
||||
def delete_2(self):
|
||||
self.duplicates[self.dupe2].delete()
|
||||
self.comic_deleted.emit(self.duplicates[self.dupe2].path)
|
||||
self.update_dupes()
|
||||
|
||||
def display_dupe(self):
|
||||
for f in range(self.pageList.rowCount()):
|
||||
for _ in range(self.pageList.rowCount()):
|
||||
self.pageList.removeRow(0)
|
||||
for h in self.duplicates[self.dupe1].imageHashes.values():
|
||||
row = self.pageList.rowCount()
|
||||
@ -488,39 +533,44 @@ class DupeWindow(QtWidgets.QWidget):
|
||||
if image_hash.file_hash == score_hash.file_hash:
|
||||
file_color = "green"
|
||||
style = f"""
|
||||
.page {{
|
||||
color: {page_color};
|
||||
}}
|
||||
.size {{
|
||||
color: {size_color};
|
||||
}}
|
||||
.type {{
|
||||
color: {type_color};
|
||||
}}
|
||||
.file {{
|
||||
color: {file_color};
|
||||
}}
|
||||
.image {{
|
||||
color: {image_color};
|
||||
}}
|
||||
"""
|
||||
text = "name: {{duplicate.path}}<br/>" \
|
||||
"page count: <span class='page'>{len}</span><br/>" \
|
||||
"size/type: <span class='size'>{{width}}x{{height}}</span>/<span class='type'>{meta.type}</span><br/>" \
|
||||
"file_hash: <span class='file'>{meta.file_hash}</span><br/>" \
|
||||
"image_hash: <span class='image'>{meta.image_hash}</span>" \
|
||||
.format(meta=image_hash, style=style, len=len(self.duplicates[self.dupe1].imageHashes))
|
||||
.page {{
|
||||
color: {page_color};
|
||||
}}
|
||||
.size {{
|
||||
color: {size_color};
|
||||
}}
|
||||
.type {{
|
||||
color: {type_color};
|
||||
}}
|
||||
.file {{
|
||||
color: {file_color};
|
||||
}}
|
||||
.image {{
|
||||
color: {image_color};
|
||||
}}"""
|
||||
text = ( # this is a format string that creates a format string
|
||||
"name: {{duplicate.path}}<br/>"
|
||||
"page count: <span class='page'>{len}</span><br/>"
|
||||
"size/type: <span class='size'>{{width}}x{{height}}</span>/<span class='type'>{meta.type}</span><br/>"
|
||||
"file_hash: <span class='file'>{meta.file_hash}</span><br/>"
|
||||
"image_hash: <span class='image'>{meta.image_hash}</span>".format(
|
||||
meta=image_hash, style=style, len=len(self.duplicates[self.dupe1].imageHashes)
|
||||
)
|
||||
)
|
||||
self.comic1Image.setDuplicate(self.duplicates[self.dupe1])
|
||||
self.comic1Image.setImage(image_hash.name)
|
||||
self.comic1Image.setText(text)
|
||||
self.comic1Image.setLabelStyle(style)
|
||||
|
||||
text = "name: {{duplicate.path}}<br/>" \
|
||||
"page count: <span class='page'>{len}</span><br/>" \
|
||||
"size/type: <span class='size'>{{width}}x{{height}}</span>/<span class='type'>{score.type}</span><br/>" \
|
||||
"file_hash: <span class='file'>{score.file_hash}</span><br/>" \
|
||||
"image_hash: <span class='image'>{score.image_hash}</span>" \
|
||||
.format(score=score_hash, style=style, len=len(self.duplicates[self.dupe2].imageHashes))
|
||||
text = (
|
||||
"name: {{duplicate.path}}<br/>"
|
||||
"page count: <span class='page'>{len}</span><br/>"
|
||||
"size/type: <span class='size'>{{width}}x{{height}}</span>/<span class='type'>{score.type}</span><br/>"
|
||||
"file_hash: <span class='file'>{score.file_hash}</span><br/>"
|
||||
"image_hash: <span class='image'>{score.image_hash}</span>".format(
|
||||
score=score_hash, style=style, len=len(self.duplicates[self.dupe2].imageHashes)
|
||||
)
|
||||
)
|
||||
self.comic2Image.setDuplicate(self.duplicates[self.dupe2])
|
||||
self.comic2Image.setImage(score_hash.name)
|
||||
self.comic2Image.setText(text)
|
||||
@ -539,20 +589,29 @@ class QQlabel(QtWidgets.QLabel):
|
||||
self.setMaximumWidth(pixmap.width())
|
||||
self.setMaximumHeight(pixmap.height())
|
||||
super().setPixmap(
|
||||
self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
|
||||
self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
)
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
if self.image is not None:
|
||||
super().setPixmap(self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation))
|
||||
super().setPixmap(
|
||||
self.image.scaled(
|
||||
self.width(), self.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DupeImage(QtWidgets.QWidget):
|
||||
deleted = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, duplicate: Duplicate, style=".path {color: black;}.hash {color: black;}",
|
||||
text="path: <span class='path'>{duplicate.path}</span><br/>hash: <span class='hash'>{duplicate.digest}</span>",
|
||||
image="cover", parent=None):
|
||||
def __init__(
|
||||
self,
|
||||
duplicate: Duplicate,
|
||||
style=".path {color: black;}.hash {color: black;}",
|
||||
text="path: <span class='path'>{duplicate.path}</span><br/>hash: <span class='hash'>{duplicate.digest}</span>",
|
||||
image="cover",
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
self.image = QQlabel()
|
||||
@ -589,14 +648,16 @@ class DupeImage(QtWidgets.QWidget):
|
||||
self.duplicate = duplicate
|
||||
self.setImage("cover")
|
||||
self.label.setText(
|
||||
f"<style>{self.labelStyle}</style>" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
|
||||
height=self.iHeight))
|
||||
f"<style>{self.labelStyle}</style>"
|
||||
+ self.text.format(duplicate=self.duplicate, width=self.iWidth, height=self.iHeight)
|
||||
)
|
||||
|
||||
def setText(self, text):
|
||||
self.text = text
|
||||
self.label.setText(
|
||||
f"<style>{self.labelStyle}</style>" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
|
||||
height=self.iHeight))
|
||||
f"<style>{self.labelStyle}</style>"
|
||||
+ self.text.format(duplicate=self.duplicate, width=self.iWidth, height=self.iHeight)
|
||||
)
|
||||
|
||||
def setImage(self, image):
|
||||
if self.duplicate is not None:
|
||||
@ -612,8 +673,9 @@ class DupeImage(QtWidgets.QWidget):
|
||||
def setLabelStyle(self, style):
|
||||
self.labelStyle = style
|
||||
self.label.setText(
|
||||
f"<style>{self.labelStyle}</style>" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
|
||||
height=self.iHeight))
|
||||
f"<style>{self.labelStyle}</style>"
|
||||
+ self.text.format(duplicate=self.duplicate, width=self.iWidth, height=self.iHeight)
|
||||
)
|
||||
|
||||
|
||||
def extract(dupe_set, directory):
|
||||
@ -621,11 +683,11 @@ def extract(dupe_set, directory):
|
||||
dupe.extract(unique_dir(os.path.join(directory, os.path.basename(dupe.path))))
|
||||
|
||||
|
||||
def compare_dupe(dupe1: Dict[str, ImageMeta], dupe2: Dict[str, ImageMeta]):
|
||||
def compare_dupe(dupe1: dict[str, ImageMeta], dupe2: dict[str, ImageMeta]):
|
||||
for k, image1 in dupe1.items():
|
||||
score = sys.maxsize
|
||||
file_hash = ""
|
||||
for k2, image2 in dupe2.items():
|
||||
for _, image2 in dupe2.items():
|
||||
tmp = ImageHasher.hamming_distance(image1.image_hash, image2.image_hash)
|
||||
if tmp < score:
|
||||
score = tmp
|
||||
@ -645,44 +707,49 @@ def unique_dir(file_name):
|
||||
while True:
|
||||
if not os.path.lexists(file_name):
|
||||
return file_name
|
||||
file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1]
|
||||
file_name = file_name_parts[0] + " (" + str(counter) + ")" + file_name_parts[1]
|
||||
counter += 1
|
||||
|
||||
|
||||
app = None
|
||||
settings = ComicTaggerSettings()
|
||||
def parse_args(args: list[str], config=True):
|
||||
parser = argparse.ArgumentParser(description="ComicTagger Duplicate comparison script")
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
metavar="workdir",
|
||||
type=str,
|
||||
nargs=1,
|
||||
default=tempfile.mkdtemp(),
|
||||
help="work directory, will be deleted on close",
|
||||
)
|
||||
parser.add_argument("paths", metavar="PATH", type=str, nargs="+", help="Path(s) to search for duplicates")
|
||||
if config:
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
help="""Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n""",
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
def main():
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
|
||||
parser = argparse.ArgumentParser(description='ComicTagger Duplicate comparison script')
|
||||
parser.add_argument('-w', metavar='workdir', type=str, nargs=1, default=tempfile.mkdtemp(), help='work directory')
|
||||
parser.add_argument('paths', metavar='PATH', type=str, nargs='+', help='Path(s) to search for duplicates')
|
||||
args = parser.parse_args()
|
||||
def main(args: Optional[list[str]] = None, settings: Optional[ComicTaggerSettings] = None):
|
||||
opts = parse_args(args)
|
||||
if not settings:
|
||||
settings = ComicTaggerSettings(opts.config)
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
global app
|
||||
workdir = args.w
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
file_list = utils.get_recursive_filelist(args.paths)
|
||||
file_list = utils.get_recursive_filelist(opts.paths)
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.start(50) # You may change this if you wish.
|
||||
timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms.
|
||||
|
||||
window = MainWindow(file_list, style, workdir)
|
||||
window.show()
|
||||
app.exec()
|
||||
shutil.rmtree(workdir, True)
|
||||
return MainWindow(file_list, style, opts.w, settings)
|
||||
|
||||
|
||||
def sigint_handler(*args):
|
||||
"""Handler for the SIGINT signal."""
|
||||
sys.stderr.write('\r')
|
||||
sys.stderr.write("\r")
|
||||
QtWidgets.QApplication.quit()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
app = QtWidgets.QApplication([])
|
||||
window = main()
|
||||
window.show()
|
||||
app.exec()
|
3
setup.py
3
setup.py
@ -53,7 +53,8 @@ setup(
|
||||
author="ComicTagger team",
|
||||
author_email="comictagger@gmail.com",
|
||||
url="https://github.com/comictagger/comictagger",
|
||||
packages=["comictaggerlib", "comicapi"],
|
||||
packages=["comictaggerlib", "comicapi", "comictaggerscripts"],
|
||||
package_dir={"comictaggerscripts": "scripts"},
|
||||
package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]},
|
||||
entry_points=dict(console_scripts=["comictagger=comictaggerlib.main:ctmain"]),
|
||||
classifiers=[
|
||||
|
Loading…
Reference in New Issue
Block a user