diff --git a/scripts/dupe.ui b/scripts/dupe.ui
new file mode 100644
index 0000000..68defcd
--- /dev/null
+++ b/scripts/dupe.ui
@@ -0,0 +1,158 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 729
+ 406
+
+
+
+ Form
+
+
+
+ QLayout::SetMinAndMaxSize
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::ExtendedSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ -
+
+
+ true
+
+
+ Qt::Horizontal
+
+
+ false
+
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ true
+
+
+ 3
+
+
+
+ name
+
+
+
+
+ score
+
+
+
+
+ dupe name
+
+
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+
+ -
+
+
+ Delete Comic 1
+
+
+ Delete
+
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+
+ -
+
+
+ Delete Comic 2
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/find_dupes.py b/scripts/find_dupes.py
index 361b383..db3d35b 100755
--- a/scripts/find_dupes.py
+++ b/scripts/find_dupes.py
@@ -1,85 +1,687 @@
-#!/usr/bin/python
+#!/usr/bin/python3
"""Find all duplicate comics"""
-# import sys
+import argparse
+import hashlib
+import shutil
+import signal
+from pathlib import Path
+from operator import itemgetter
+from typing import Dict, List
+
+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 comictaggerlib.filerenamer import FileRenamer
-# from comictaggerlib.issuestring import *
-# import comictaggerlib.utils
+root = 1 << 31 - 1
+something = 1 << 31 - 1
+
+
+class ImageMeta:
+ def __init__(self, name, file_hash, image_hash, image_type, score=-1, score_file_hash=""):
+ self.name = name
+ self.file_hash = file_hash
+ self.image_hash = image_hash
+ self.type = image_type
+ self.score = score
+ self.score_file_hash = score_file_hash
+
+
+class Duplicate:
+ """docstring for Duplicate"""
+ imageHashes: Dict[str, ImageMeta]
+
+ def __init__(self, path, metadata: GenericMetadata, ca: ComicArchive, cover):
+ self.path = path
+ self.digest = ""
+ self.ca = ca
+ self.metadata = metadata
+ self.imageHashes = dict()
+ self.duplicateImages = set()
+ self.extras = set()
+ self.extractedPath = ""
+ self.deletable = False
+ self.keeping = False
+ self.fileCount = 0 # Excluding comicinfo.xml
+ self.imageCount = 0
+ self.cover = cover
+ blake2b = hashlib.blake2b(digest_size=16)
+ for f in open(self.path, "rb"):
+ blake2b.update(f)
+
+ self.digest = blake2b.hexdigest()
+
+ def extract(self, directory):
+ if self.ca.seemsToBeAComicArchive():
+ self.extractedPath = directory
+ for filepath in self.ca.archiver.getArchiveFilenameList():
+ filename = os.path.basename(filepath)
+ if filename.lower() in ["comicinfo.xml"]:
+ continue
+
+ self.fileCount += 1
+ archived_file = self.ca.archiver.readArchiveFile(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():
+ 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)
+ else:
+ self.extras.add(filename)
+
+ os.makedirs(self.extractedPath, 0o777, True)
+
+ unarchived_file = Path(os.path.join(self.extractedPath, filename))
+ unarchived_file.write_bytes(archived_file)
+
+ def clean(self):
+ shutil.rmtree(self.extractedPath, ignore_errors=True)
+
+ def delete(self):
+ if not self.keeping:
+ self.clean()
+ try:
+ os.remove(self.path)
+ except Exception:
+ pass
+ return not (os.path.exists(self.path) or os.path.exists(self.extractedPath))
+
+
+class Tree(QtCore.QAbstractListModel):
+ def __init__(self, item: List[List[Duplicate]]):
+ super(Tree, self).__init__()
+ self.rootItem = item
+
+ def rowCount(self, index: QtCore.QModelIndex = ...) -> int:
+ if not index.isValid():
+ return len(self.rootItem)
+
+ return 0
+
+ def columnCount(self, index: QtCore.QModelIndex = ...) -> int:
+ if index.isValid():
+ return 1
+
+ return 0
+
+ def data(self, index: QtCore.QModelIndex, role: int = ...) -> typing.Any:
+ if not index.isValid():
+ 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('')
+ return QtCore.QVariant()
+
+
+class MainWindow(QtWidgets.QMainWindow):
+ def __init__(self, file_list, style, work_path, parent=None):
+ super().__init__(parent)
+ uic.loadUi(ComicTaggerSettings.getUIFile("../../scripts/mainwindow.ui"), self)
+ self.dupes = []
+ self.firstRun = 0
+ self.dupe_set_list: List[List[Duplicate]] = list()
+ self.style = style
+ if work_path == "":
+ work_path = tempfile.mkdtemp()
+ self.work_path = work_path
+ self.initFiles = file_list
+ 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 update_dupes(self):
+ # print("updating duplicates")
+ new_set_list = list()
+ for dupe in self.dupe_set_list:
+ dupe_list = list()
+ for d in dupe:
+ QtCore.QCoreApplication.processEvents()
+ if os.path.exists(d.path):
+ dupe_list.append(d)
+ else:
+ d.clean()
+
+ if len(dupe_list) > 1:
+ new_set_list.append(dupe_list)
+ else:
+ dupe_list[0].clean()
+ self.dupe_set_list: List[List[Duplicate]] = new_set_list
+ self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
+
+ 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))
+
+ 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.show()
+
+ def compare_action(self, b):
+ selection = self.dupe_set_qlist.selectedIndexes()
+ if len(selection) > 0:
+ self.compare(selection[0].row())
+
+ def dupe_set_double_clicked(self, index: QtCore.QModelIndex):
+ self.compare(index.row())
+
+ def dupe_set_clicked(self, index: QtCore.QModelIndex):
+ 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:
+ 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)
+
+ def showEvent(self, event: QtGui.QShowEvent):
+ if self.firstRun == 0:
+ self.firstRun = 1
+
+ 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.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)
+ 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))
+
+ def load_files(self, file_list):
+ # Progress dialog on Linux flakes out for small range, so scale up
+ dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(file_list), parent=self)
+ dialog.setWindowTitle("Reading Comics")
+ dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ dialog.setMinimumDuration(300)
+ dialog.setMinimumWidth(400)
+ centerWindowOnParent(dialog)
+
+ comic_list = []
+ max_name_len = 2
+ for filename in file_list:
+ QtCore.QCoreApplication.processEvents()
+ if dialog.wasCanceled():
+ 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):
+ # 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)
+ 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()
+ prev_key = ""
+
+ dialog.setWindowTitle("Finding Duplicates")
+ dialog.setMaximum(len(comic_list))
+ dialog.setValue(dialog.minimum())
+
+ set_list = list()
+ for new_key, filename, ca, md, cover in comic_list:
+ dialog.setValue(dialog.value() + 1)
+ QtCore.QCoreApplication.processEvents()
+ if dialog.wasCanceled():
+ break
+ dialog.setLabelText(filename)
+
+ # if the new key same as the last, add to to dupe set
+ if new_key == prev_key:
+ dupe_set.append((filename, ca, md, cover))
+ # else we're on a new potential block
+ else:
+ # 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.append((filename, ca, md, cover))
+
+ prev_key = new_key
+
+ # Final dupe_set
+ if len(dupe_set) > 1:
+ set_list.append(dupe_set)
+
+ for d_set in set_list:
+ new_set = list()
+ for filename, ca, md, cover in d_set:
+ new_set.append(Duplicate(filename, md, ca, cover))
+ self.dupe_set_list.append(new_set)
+
+ self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
+ # print()
+ dialog.close()
+
+ # def delete_hashes(self):
+ # working_dir = os.path.join(self.tmp, "working")
+ # s = False
+ # # while working and len(dupe_set) > 1:
+ # remaining = list()
+ # for dupe_set in self.dupe_set_list:
+ # not_deleted = True
+ # if os.path.exists(working_dir):
+ # shutil.rmtree(working_dir, ignore_errors=True)
+ #
+ # os.mkdir(working_dir)
+ # extract(dupe_set, working_dir)
+ # if mark_hashes(dupe_set):
+ # if s: # Auto delete if s flag or if there are not any non image extras
+ # dupe_set.sort(key=attrgetter("fileCount"))
+ # dupe_set.sort(key=lambda x: len(x.duplicateImages))
+ # dupe_set[0].keeping = True
+ # else:
+ # dupe_set[select_archive("Select archive to keep: ", dupe_set)].keeping = True
+ # else:
+ # # app.exec_()
+ # compare_dupe(dupe_set[0], dupe_set[1])
+ # for i, dupe in enumerate(dupe_set):
+ # print("{0}. {1}: {2.series} #{2.issue:0>3} {2.year}; extras: {3}; deletable: {4}".format(
+ # i,
+ # dupe.path,
+ # dupe.metadata,
+ # ", ".join(sorted(dupe.extras)), dupe.deletable))
+ # dupe_set = delete(dupe_set)
+ # if not_deleted:
+ # remaining.append(dupe_set)
+ # self.dupe_set_list = remaining
+
+
+class DupeWindow(QtWidgets.QWidget):
+ closed = QtCore.pyqtSignal()
+
+ def __init__(self, duplicates: List[Duplicate], tmp, parent=None):
+ super().__init__(parent, QtCore.Qt.Window)
+ uic.loadUi(ComicTaggerSettings.getUIFile("../../scripts/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.dupe1 = -1
+ self.dupe2 = -1
+
+ self.tmp = tmp
+
+ self.setWindowTitle("ComicTagger Duplicate compare")
+
+ self.pageList.currentItemChanged.connect(self.current_item_changed)
+ self.comic1Delete.clicked.connect(self.delete_1)
+ self.comic2Delete.clicked.connect(self.delete_2)
+ self.dupeList.itemSelectionChanged.connect(self.show_dupe_list)
+ # self.dupeList = QtWidgets.QListWidget()
+ self.dupeList.setIconSize(QtCore.QSize(100, 50))
+
+ while self.pageList.rowCount() > 0:
+ self.pageList.removeRow(0)
+
+ self.pageList.setSortingEnabled(False)
+
+ if len(duplicates) < 2:
+ return
+ extract(duplicates, tmp)
+
+ tmp1 = DupeImage(self.duplicates[0])
+ tmp2 = DupeImage(self.duplicates[1])
+ self.comic1Data.layout().replaceWidget(self.comic1Image, tmp1)
+ self.comic2Data.layout().replaceWidget(self.comic2Image, tmp2)
+ self.comic1Image = tmp1
+ self.comic2Image = tmp2
+ self.comic1Image.deleted.connect(self.update_dupes)
+ self.comic2Image.deleted.connect(self.update_dupes)
+
+ def showEvent(self, event: QtGui.QShowEvent) -> None:
+ self.update_dupes()
+
+ def closeEvent(self, event: QtGui.QCloseEvent) -> None:
+ self.closed.emit()
+ event.accept()
+
+ def show_dupe_list(self):
+ dupes = self.dupeList.selectedItems()
+ if len(dupes) != 2:
+ return
+ self.dupe1 = int(dupes[0].data(QtCore.Qt.UserRole))
+ self.dupe2 = int(dupes[1].data(QtCore.Qt.UserRole))
+ if len(self.duplicates[self.dupe2].imageHashes) > len(self.duplicates[self.dupe1].imageHashes):
+ self.dupe1, self.dupe2 = self.dupe2, self.dupe1
+ compare_dupe(self.duplicates[self.dupe1].imageHashes, self.duplicates[self.dupe2].imageHashes)
+ self.display_dupe()
+
+ def update_dupes(self):
+ dupes: List[Duplicate] = list()
+ for f in self.duplicates:
+ if os.path.exists(f.path):
+ dupes.append(f)
+ else:
+ f.clean()
+ self.duplicates = dupes
+ if len(self.duplicates) < 2:
+ self.close()
+
+ for i, dupe in enumerate(self.duplicates):
+ item = QtWidgets.QListWidgetItem()
+ item.setText(dupe.path)
+ item.setToolTip(dupe.path)
+ pm = QtGui.QPixmap()
+ pm.loadFromData(dupe.cover)
+ item.setIcon(QtGui.QIcon(pm))
+ item.setData(QtCore.Qt.UserRole, i)
+ self.dupeList.addItem(item)
+ self.dupeList.setCurrentRow(0)
+ self.dupeList.setCurrentRow(1, QtCore.QItemSelectionModel.Select)
+
+ def delete_1(self):
+ self.duplicates[self.dupe1].delete()
+ self.update_dupes()
+
+ def delete_2(self):
+ self.duplicates[self.dupe2].delete()
+ self.update_dupes()
+
+ def display_dupe(self):
+ for f in range(self.pageList.rowCount()):
+ self.pageList.removeRow(0)
+ for h in self.duplicates[self.dupe1].imageHashes.values():
+ row = self.pageList.rowCount()
+ self.pageList.insertRow(row)
+ name = QtWidgets.QTableWidgetItem()
+ score = QtWidgets.QTableWidgetItem()
+ dupe_name = QtWidgets.QTableWidgetItem()
+
+ item_text = os.path.basename(h.name)
+ name.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ name.setText(item_text)
+ name.setData(QtCore.Qt.UserRole, h.file_hash)
+ name.setData(QtCore.Qt.ToolTipRole, item_text)
+ self.pageList.setItem(row, 0, name)
+
+ item_text = str(h.score)
+ score.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ score.setText(item_text)
+ score.setData(QtCore.Qt.UserRole, h.file_hash)
+ score.setData(QtCore.Qt.ToolTipRole, item_text)
+ self.pageList.setItem(row, 1, score)
+
+ item_text = os.path.basename(self.duplicates[self.dupe2].imageHashes[h.score_file_hash].name)
+ dupe_name.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ dupe_name.setText(item_text)
+ dupe_name.setData(QtCore.Qt.UserRole, h.file_hash)
+ dupe_name.setData(QtCore.Qt.ToolTipRole, item_text)
+ self.pageList.setItem(row, 2, dupe_name)
+
+ self.pageList.resizeColumnsToContents()
+ self.pageList.selectRow(0)
+
+ def current_item_changed(self, curr, prev):
+
+ if curr is None:
+ return
+ if prev is not None and prev.row() == curr.row():
+ return
+
+ file_hash = str(self.pageList.item(curr.row(), 0).data(QtCore.Qt.UserRole))
+ image_hash = self.duplicates[self.dupe1].imageHashes[file_hash]
+ score_hash = self.duplicates[self.dupe2].imageHashes[image_hash.score_file_hash]
+
+ image1 = QtGui.QPixmap(image_hash.name)
+ image2 = QtGui.QPixmap(score_hash.name)
+
+ page_color = "red"
+ size_color = "red"
+ type_color = "red"
+ file_color = "black"
+ image_color = "black"
+ if image1.width() == image2.width() and image2.height() == image1.height():
+ size_color = "green"
+ if len(self.duplicates[self.dupe1].imageHashes) == len(self.duplicates[self.dupe2].imageHashes):
+ page_color = "green"
+ if image_hash.type == score_hash.type:
+ type_color = "green"
+ if image_hash.image_hash == score_hash.image_hash:
+ image_color = "green"
+ 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}}
" \
+ "page count: {len}
" \
+ "size/type: {{width}}x{{height}}/{meta.type}
" \
+ "file_hash: {meta.file_hash}
" \
+ "image_hash: {meta.image_hash}" \
+ .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}}
" \
+ "page count: {len}
" \
+ "size/type: {{width}}x{{height}}/{score.type}
" \
+ "file_hash: {score.file_hash}
" \
+ "image_hash: {score.image_hash}" \
+ .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)
+ self.comic2Image.setLabelStyle(style)
+
+
+class QQlabel(QtWidgets.QLabel):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.image = None
+ self.setMinimumSize(1, 1)
+ self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+
+ def setPixmap(self, pixmap: QtGui.QPixmap) -> None:
+ self.image = pixmap
+ self.setMaximumWidth(pixmap.width())
+ self.setMaximumHeight(pixmap.height())
+ super().setPixmap(
+ 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))
+
+
+class DupeImage(QtWidgets.QWidget):
+ deleted = QtCore.pyqtSignal(str)
+
+ def __init__(self, duplicate: Duplicate, style=".path {color: black;}.hash {color: black;}",
+ text="path: {duplicate.path}
hash: {duplicate.digest}",
+ image="cover", parent=None):
+ super().__init__(parent)
+ self.setLayout(QtWidgets.QVBoxLayout())
+ self.image = QQlabel()
+ self.label = QtWidgets.QLabel()
+ self.duplicate = duplicate
+ self.text = text
+ self.labelStyle = style
+
+ self.iHeight = 0
+ self.iWidth = 0
+ self.setStyleSheet("color: black;")
+ self.label.setWordWrap(True)
+
+ self.setImage(image)
+ self.setLabelStyle(self.labelStyle)
+ self.setText(self.text)
+
+ # label.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
+ self.layout().addWidget(self.image)
+ self.layout().addWidget(self.label)
+
+ def contextMenuEvent(self, event: QtGui.QContextMenuEvent):
+ menu = QtWidgets.QMenu()
+ delete_action = menu.addAction("delete")
+ action = menu.exec(self.mapToGlobal(event.pos()))
+ if action == delete_action:
+ if self.duplicate.delete():
+ self.hide()
+ self.deleteLater()
+ # print("signal emitted")
+ self.deleted.emit(self.duplicate.path)
+
+ def setDuplicate(self, duplicate: Duplicate):
+ self.duplicate = duplicate
+ self.setImage("cover")
+ self.label.setText(
+ f"" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
+ height=self.iHeight))
+
+ def setText(self, text):
+ self.text = text
+ self.label.setText(
+ f"" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
+ height=self.iHeight))
+
+ def setImage(self, image):
+ if self.duplicate is not None:
+ pm = QtGui.QPixmap()
+ if image == "cover":
+ pm.loadFromData(self.duplicate.cover)
+ else:
+ pm.load(image)
+ self.iHeight = pm.height()
+ self.iWidth = pm.width()
+ self.image.setPixmap(pm)
+
+ def setLabelStyle(self, style):
+ self.labelStyle = style
+ self.label.setText(
+ f"" + self.text.format(duplicate=self.duplicate, width=self.iWidth,
+ height=self.iHeight))
+
+
+def extract(dupe_set, directory):
+ for dupe in dupe_set:
+ dupe.extract(unique_dir(os.path.join(directory, os.path.basename(dupe.path))))
+
+
+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():
+ tmp = ImageHasher.hamming_distance(image1.image_hash, image2.image_hash)
+ if tmp < score:
+ score = tmp
+ file_hash = image2.file_hash
+
+ dupe1[k].score = score
+ dupe1[k].score_file_hash = file_hash
+
+
+def make_key(x):
+ return "<" + str(x.series) + " #" + str(x.issue) + " - " + str(x.title) + " - " + str(x.year) + ">"
+
+
+def unique_dir(file_name):
+ counter = 1
+ file_name_parts = os.path.splitext(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]
+ counter += 1
+
+
+app = None
+settings = ComicTaggerSettings()
def main():
- utils.fix_output_encoding()
- settings = ComicTaggerSettings()
+ 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()
style = MetaDataStyle.CIX
+ global app
+ workdir = args.w
+ app = QtWidgets.QApplication(sys.argv)
+ file_list = utils.get_recursive_filelist(args.paths)
- if len(sys.argv) < 2:
- print >> sys.stderr, "Usage: {0} [comic_folder]".format(sys.argv[0])
- return
+ 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.
- filelist = utils.get_recursive_filelist(sys.argv[1:])
+ window = MainWindow(file_list, style, workdir)
+ window.show()
+ app.exec()
+ shutil.rmtree(workdir, True)
- # first find all comics with metadata
- print >> sys.stderr, "Reading in all comics..."
- comic_list = []
- fmt_str = ""
- max_name_len = 2
- for filename in filelist:
- ca = ComicArchive(filename, settings.rar_exe_path)
- if ca.seemsToBeAComicArchive() and ca.hasMetadata(style):
- max_name_len = max(max_name_len, len(filename))
- fmt_str = u"{{0:{0}}}".format(max_name_len)
- print >> sys.stderr, fmt_str.format(filename) + "\r",
- sys.stderr.flush()
- comic_list.append((filename, ca.readMetadata(style)))
- print >> sys.stderr, fmt_str.format("") + "\r",
- print "--------------------------------------------------------------------------"
- print "Found {0} comics with {1} tags".format(len(comic_list), MetaDataStyle.name[style])
- print "--------------------------------------------------------------------------"
+def sigint_handler(*args):
+ """Handler for the SIGINT signal."""
+ sys.stderr.write('\r')
+ QtWidgets.QApplication.quit()
- # sort the list by series+issue+year, to put all the dupes together
- def makeKey(x):
- return "<" + unicode(x[1].series) + u" #" + unicode(x[1].issue) + u" - " + unicode(x[1].year) + ">"
-
- comic_list.sort(key=makeKey, reverse=False)
-
- # look for duplicate blocks
- dupe_set_list = []
- dupe_set = []
- prev_key = ""
- for filename, md in comic_list:
- print >> sys.stderr, fmt_str.format(filename) + "\r",
- sys.stderr.flush()
-
- new_key = makeKey((filename, md))
-
- # if the new key same as the last, add to to dupe set
- if new_key == prev_key:
- dupe_set.append(filename)
-
- # else we're on a new potential block
- else:
- # only add if the dupe list has 2 or more
- if len(dupe_set) > 1:
- dupe_set_list.append(dupe_set)
- dupe_set = []
- dupe_set.append(filename)
-
- prev_key = new_key
-
- print >> sys.stderr, fmt_str.format("") + "\r",
- print "Found {0} duplicate sets".format(len(dupe_set_list))
-
- for dupe_set in dupe_set_list:
- ca = ComicArchive(dupe_set[0], settings.rar_exe_path)
- md = ca.readMetadata(style)
- print "{0} #{1} ({2})".format(md.series, md.issue, md.year)
- for filename in dupe_set:
- print "------>{0}".format(filename)
if __name__ == "__main__":
diff --git a/scripts/mainwindow.ui b/scripts/mainwindow.ui
new file mode 100644
index 0000000..a6ed1b5
--- /dev/null
+++ b/scripts/mainwindow.ui
@@ -0,0 +1,92 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 600
+
+
+
+ ComicTagger Duplicate finder
+
+
+
+
+ QLayout::SetMinAndMaxSize
+
+ -
+
+
+ true
+
+
+ true
+
+
+ Qt::Horizontal
+
+
+ false
+
+
+
+
+
+ 400
+ 0
+
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 396
+ 520
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+ Compare Comic
+
+
+
+
+
+