diff --git a/Makefile b/Makefile index c1b2b8c..4a0c177 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,10 @@ help: clean: clean-build ## remove all build, test, coverage and Python artifacts +upgrade: clean ## upgrade all dependencies + python -m pip install --upgrade pip + pip install --upgrade --pre -rrequirements.txt + environment: .\env\Scripts\activate.bat diff --git a/assets/percentage.png b/assets/percentage.png deleted file mode 100644 index e30ef80..0000000 Binary files a/assets/percentage.png and /dev/null differ diff --git a/assets/ruler.png b/assets/ruler.png deleted file mode 100644 index 3231781..0000000 Binary files a/assets/ruler.png and /dev/null differ diff --git a/assets/scale.png b/assets/scale.png new file mode 100644 index 0000000..1643b41 Binary files /dev/null and b/assets/scale.png differ diff --git a/requirements.txt b/requirements.txt index e69de29..12a14ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,3 @@ +torrentfile +pyben +PyQt6 diff --git a/torrentfileQt/checkTab.py b/torrentfileQt/checkTab.py index fa86d80..8f8199e 100644 --- a/torrentfileQt/checkTab.py +++ b/torrentfileQt/checkTab.py @@ -135,7 +135,6 @@ def submit(self): CheckerClass.register_callback(textEdit.callback) logging.debug("Registering Callback, setting root") piece_hasher(metafile, content, tree) - # tree.start_thread(metafile, content) class BrowseTorrents(QToolButton): @@ -214,67 +213,92 @@ def browse(self, path=None): self.parent().searchInput.setText(path) +class LineEdit(QLineEdit): + def __init__(self, parent=None): + super().__init__(parent=parent) + self._parent = parent + self.setStyleSheet(lineEditSheet) + + +class LogTextEdit(QPlainTextEdit): + def __init__(self, parent=None): + super().__init__(parent=parent) + self._parent = parent + self.setBackgroundVisible(True) + font = self.font() + font.setFamily("Consolas") + font.setBold(True) + font.setPointSize(8) + self.setFont(font) + self.setStyleSheet(logTextEditSheet) + + def clear_data(self): + self.clear() + + def callback(self, msg): + self.insertPlainText(msg) + self.insertPlainText("\n") + + +class Label(QLabel): + """Label Identifier for Window Widgets. + + Subclass: QLabel + """ + + def __init__(self, text, parent=None): + super().__init__(text, parent=parent) + self.setStyleSheet(labelSheet) + font = self.font() + font.setBold(True) + font.setPointSize(12) + self.setFont(font) + + class TreePieceItem(QTreeWidgetItem): def __init__(self, type=0, tree=None): super().__init__(type=type) policy = self.ChildIndicatorPolicy.DontShowIndicatorWhenChildless self.setChildIndicatorPolicy(policy) self.tree = tree + self.window = tree.window + self.counted = self.value = 0 self.progbar = None - self.total = self.value = self.counted = 0 - def setProgressBar(self, progressBar, size): - self.total = size - self.progbar = progressBar - self.progbar.setRange(0, size) + @property + def total(self): + return self.progbar.total def addValue(self, value): - left = self.total - self.counted - if left > value: - self.progbar.addValue(value) - self.value += value - self.counted += value - return 0 - self.progbar.addValue(left) - self.counted += left - self.value += left - remainder = value - left - return remainder - - def counted(self, value): - left = self.total - self.counted - if left < value: - self.counted += left - remainder = value - left - return remainder + if self.counted + value > self.total: + consumed = self.total - self.value + else: + consumed = value + self.value += consumed + self.counted += consumed + self.progbar.valueChanged.emit(consumed) + self.window.app.processEvents() + return consumed + + def cont(self, value): + if self.counted + value > self.total: + consumed = self.total - self.value + self.counted += consumed + return consumed self.counted += value - return 0 + return value +class ProgressBar(QProgressBar): - def __repr__(self): - return f"" - - -class Filler: - def __init__(self, **kwargs): - self.tree = kwargs["tree"] - self.value = None - self.expected = None - - def set_val(self, val, expected): - self.expected = expected - self.value = val - - def text(self): - if self.expected: - return self.expected - + valueChanged = pyqtSignal([int]) -class ProgressBar(QProgressBar): - def __init__(self, parent=None): + def __init__(self, parent=None, size=0): super().__init__(parent=parent) - self.setRange(0, 100) + self.total = size + self.setValue(0) + self.setRange(0, size) + self.valueChanged.connect(self.addValue) def addValue(self, value): currentvalue = self.value() @@ -311,17 +335,8 @@ def __init__(self, parent=None): self.total = 0 self.piece_length = None self.item_tree = {"widget": self.item} - self.valueUpdate.connect(self.updateValue) self.addPathChild.connect(self.add_path_child) - def updateValue(self, args): - actual, expected, path, size = args - widget = self.itemWidgets[path]["widget"] - # print(actual, expected) - if actual == expected: - widget.add_piece(size) - self.window.repaint() - def clear(self): super().clear() self.item_tree = {"widget": self.invisibleRootItem()} @@ -344,16 +359,16 @@ def add_path_child(self, path, size): item_tree[partial] = {"widget": item} if i == len(partials) - 1: fileicon = QIcon("./assets/file.png") - progressBar = ProgressBar() + progressBar = ProgressBar(parent=None, size=size) self.setItemWidget(item, 2, progressBar) - item.setProgressBar(progressBar, size) + item.progbar = progressBar self.itemWidgets[str(path)] = item else: fileicon = QIcon("./assets/folder.png") item.setIcon(0, fileicon) item.setText(1, partial) item_tree = item_tree[partial] - self.window.repaint() + self.window.app.processEvents() self.paths.append(path) def remainder(self, path): @@ -362,81 +377,32 @@ def remainder(self, path): def piece_hasher(metafile, content, tree): - mapping = {} checker = CheckerClass(metafile, content) - parent = os.path.dirname(content) + fileinfo = checker.fileinfo pathlist = checker.paths for path in pathlist: - relpath = path.lstrip(parent) - length = checker.fileinfo[path]["length"] + relpath = path.lstrip(tree.root) + length = fileinfo[path]["length"] tree.addPathChild.emit(relpath, length) - index, current = 0, None + if checker.meta_version == 1: + current = 0 + for actual, expected, path, size in checker.iter_hashes(): + value = size + while True: + path = pathlist[current].lstrip(tree.root) + widget = tree.itemWidgets[path] + if actual == expected: + consumed = widget.addValue(value) + else: + consumed = widget.count(value) + value -= consumed + if not value: + break + current += 1 + return for actual, expected, path, size in checker.iter_hashes(): - print(mapping) - if not current: current = path - elif path != current: - current = path - index = pathlist.index(path) - relpath = path.lstrip(parent) - widget = tree.itemWidgets[relpath] if actual == expected: - methodname = "addValue" + leaf = tree.itemWidgets[path.lstrip(tree.root)] + leaf.addValue(size) else: - methodname = "counted" - method = widget.__getattribute__(methodname) - remainder = method(size) - mapping[path] = {"size": size, "remiander": remainder} - while remainder > 0: - index += 1 - nextpath = pathlist[index] - current = nextpath - rpath = nextpath.lstrip(parent) - nwidget = tree.itemWidgets[rpath] - size = remainder - method = nwidget.__getattribute__(methodname) - remainder = method(size) - mapping[nextpath] = {"size": size, "remiander": remainder} - - -class LogTextEdit(QPlainTextEdit): - def __init__(self, parent=None): - super().__init__(parent=parent) - self._parent = parent - self.setBackgroundVisible(True) - font = self.font() - font.setFamily("Consolas") - font.setBold(True) - font.setPointSize(8) - self.setFont(font) - self.setStyleSheet(logTextEditSheet) - - def clear_data(self): - self.clear() - - def callback(self, msg): - self.insertPlainText(msg) - self.insertPlainText("\n") - - - - -class LineEdit(QLineEdit): - def __init__(self, parent=None): - super().__init__(parent=parent) - self._parent = parent - self.setStyleSheet(lineEditSheet) - - -class Label(QLabel): - """Label Identifier for Window Widgets. - - Subclass: QLabel - """ - - def __init__(self, text, parent=None): - super().__init__(text, parent=parent) - self.setStyleSheet(labelSheet) - font = self.font() - font.setBold(True) - font.setPointSize(12) - self.setFont(font) + leaf.count(size) diff --git a/torrentfileQt/infoTab.py b/torrentfileQt/infoTab.py index 98fdf52..16d4dc2 100644 --- a/torrentfileQt/infoTab.py +++ b/torrentfileQt/infoTab.py @@ -20,11 +20,12 @@ import math import os +from pathlib import Path from datetime import datetime from threading import Thread import pyben -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtWidgets import ( QFileDialog, QGridLayout, @@ -36,8 +37,7 @@ QTreeWidget, ) -from torrentfileQt.qss import (pushButtonSheet, infoLineEditSheet, - labelSheet, treeSheet) +from torrentfileQt.qss import pushButtonSheet, infoLineEditSheet, labelSheet, treeSheet from PyQt6.QtGui import QIcon @@ -49,88 +49,58 @@ class TreeWidget(QTreeWidget): parent (`widget`, default=`None`): The widget containing this widget. """ + itemReady = pyqtSignal([str, int]) + def __init__(self, parent=None): super().__init__(parent=parent) - self.tree = None + self.window = parent self.root = self.invisibleRootItem() + header = self.header() + header.setSectionResizeMode(0, header.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, header.ResizeMode.ResizeToContents) self.root.setChildIndicatorPolicy(self.root.ChildIndicatorPolicy.ShowIndicator) self.setIndentation(10) self.setEditTriggers(self.EditTrigger.NoEditTriggers) self.setHeaderHidden(True) self.setItemsExpandable(True) - self.setColumnCount(1) + self.setColumnCount(2) self.setStyleSheet(treeSheet) - - def apply_value(self, tree): - for key, value in tree.items(): - if key == "": - for item in self.apply_value(value): - yield item - elif isinstance(value, dict): - item = TreeItem(type=0) - item.setText(0, key) - for child in self.apply_value(value): - item.addChild(child) - yield item - else: + self.itemtree = {"widget": self.root} + self.itemReady.connect(self.apply_value) + + def apply_value(self, text, length): + partials = Path(text).parts + tree = self.itemtree + for i, partial in enumerate(partials): + if partial not in tree: item = TreeItem(type=0) - item.setText(0, key) - child = TreeItem(type=0) - child.setText(0, str(value)) - item.addChild(child) - yield item - - def set_tree(self, tree): - self.tree = tree - for key, value in tree.items(): - top_item = TreeItem(type=0) - top_item.setText(0, key) - for item in self.apply_value(value): - top_item.addChild(item) - self.addTopLevelItem(top_item) - - def assign_children(self, groups, parent): - for k, v in groups.items(): - item = TreeItem(type=0) - item.setText(0, k) - parent.addChild(item) - if not isinstance(v, dict): - length = TreeItem(type=0) - length.setText(0, f"Length: {v} bytes") - length.alt_icon() - item.addChild(length) - continue - self.assign_children(v, item) - - def set_files(self, filelist): - groups = {} - for item in filelist: - current = groups - partials = item["path"] - parts, start = len(partials), 0 - while start < parts: - partial = partials[start] - if start == parts - 1: - current[partial] = item["length"] - elif partial in current: - current = current[partial] + if i + 1 == len(partials): + iconpath = "./assets/file.png" + item.setLength(length) else: - current[partial] = {} - current = current[partial] - start += 1 - self.assign_children(groups, self.root) + iconpath = "./assets/folder.png" + icon = QIcon(iconpath) + item.setIcon(0, icon) + item.setText(1, partial) + tree["widget"].addChild(item) + item.setExpanded(True) + tree[partial] = {"widget": item} + tree = tree[partial] class TreeItem(QTreeWidgetItem): def __init__(self, type=0): super().__init__(type=type) - icon = QIcon("./assets/folder.png") - self.setIcon(0, icon) - self.setChildIndicatorPolicy(self.ChildIndicatorPolicy.ShowIndicator) + policy = self.ChildIndicatorPolicy.DontShowIndicatorWhenChildless + self.setChildIndicatorPolicy(policy) - def alt_icon(self): - icon = QIcon("./assets/ruler.png") - self.setIcon(0, icon) + def setLength(self, length): + child = TreeItem(type=0) + icon = QIcon("./assets/scale.png") + child.setIcon(0, icon) + child.setText(1, f"Size: {length} (bytes)") + self.setExpanded(True) + self.addChild(child) class InfoWidget(QWidget): @@ -152,11 +122,13 @@ def __init__(self, parent=None): self.privateLabel = Label("Private: ", parent=self) self.sourceLabel = Label("Source: ", parent=self) self.commentLabel = Label("Comment: ", parent=self) + self.metaVersionLabel = Label("Meta-Version:", parent=self) self.dateCreatedLabel = Label("Creation Date: ", parent=self) self.createdByLabel = Label("Created By: ", parent=self) self.contentsLabel = Label("Contents: ", parent=self) self.contentsTree = TreeWidget(parent=self) self.sourceEdit = InfoLineEdit(parent=self) + self.metaVersionEdit = InfoLineEdit(parent=self) self.pathEdit = InfoLineEdit(parent=self) self.nameEdit = InfoLineEdit(parent=self) self.pieceLengthEdit = InfoLineEdit(parent=self) @@ -179,20 +151,22 @@ def __init__(self, parent=None): self.layout.addWidget(self.totalPiecesEdit, 4, 1, 1, 1) self.layout.addWidget(self.trackerLabel, 5, 0, 1, 1) self.layout.addWidget(self.trackerEdit, 5, 1, 1, 1) - self.layout.addWidget(self.privateLabel, 6, 0, 1, 1) - self.layout.addWidget(self.privateEdit, 6, 1, 1, 1) - self.layout.addWidget(self.sourceLabel, 7, 0, 1, 1) - self.layout.addWidget(self.sourceEdit, 7, 1, 1, 1) - self.layout.addWidget(self.commentLabel, 8, 0, 1, 1) - self.layout.addWidget(self.commentEdit, 8, 1, 1, 1) - self.layout.addWidget(self.createdByLabel, 9, 0, 1, 1) - self.layout.addWidget(self.createdByEdit, 9, 1, 1, 1) - self.layout.addWidget(self.dateCreatedLabel, 10, 0, 1, 1) - self.layout.addWidget(self.dateCreatedEdit, 10, 1, 1, 1) - self.layout.addWidget(self.contentsLabel, 11, 0, 1, 1) - self.layout.addWidget(self.contentsTree, 11, 1, 5, 1) + self.layout.addWidget(self.metaVersionLabel, 6, 0, 1, 1) + self.layout.addWidget(self.metaVersionEdit, 6, 1, 1, 1) + self.layout.addWidget(self.privateLabel, 7, 0, 1, 1) + self.layout.addWidget(self.privateEdit, 7, 1, 1, 1) + self.layout.addWidget(self.sourceLabel, 8, 0, 1, 1) + self.layout.addWidget(self.sourceEdit, 8, 1, 1, 1) + self.layout.addWidget(self.commentLabel, 9, 0, 1, 1) + self.layout.addWidget(self.commentEdit, 9, 1, 1, 1) + self.layout.addWidget(self.createdByLabel, 10, 0, 1, 1) + self.layout.addWidget(self.createdByEdit, 10, 1, 1, 1) + self.layout.addWidget(self.dateCreatedLabel, 11, 0, 1, 1) + self.layout.addWidget(self.dateCreatedEdit, 11, 1, 1, 1) + self.layout.addWidget(self.contentsLabel, 12, 0, 1, 1) + self.layout.addWidget(self.contentsTree, 12, 1, 5, 1) self.selectButton = SelectButton("Select Torrent", parent=self) - self.layout.addWidget(self.selectButton, 16, 0, -1, -1) + self.layout.addWidget(self.selectButton, 17, 0, -1, -1) def fill(self, kws): """Fill all child widgets with collected information. @@ -208,25 +182,17 @@ def fill(self, kws): self.dateCreatedEdit.setText(str(kws["creation_date"])) self.createdByEdit.setText(kws["created_by"]) self.privateEdit.setText(kws["private"]) + self.metaVersionEdit.setText(str(kws["meta version"])) piece_length = kws["piece_length"] plength_str = denom(piece_length) + " / (" + pretty_int(piece_length) + ")" self.pieceLengthEdit.setText(plength_str) - size = denom(kws["length"]) + " / (" + pretty_int(kws["length"]) + ")" self.sizeEdit.setText(size) total_pieces = math.ceil(kws["length"] / kws["piece_length"]) self.totalPiecesEdit.setText(str(total_pieces)) - - if "file tree" in kws: - self.contentsTree.set_tree(kws["file tree"]) - elif "files" in kws: - self.contentsTree.set_files(kws["files"]) - else: - item = QTreeWidgetItem([kws["name"]]) - item.setText(0, kws["name"]) - self.contentsTree.addTopLevelItem(item) - + for path, size in kws["contents"].items(): + self.contentsTree.itemReady.emit(path, size) for widg in [ self.pathEdit, self.nameEdit, @@ -265,6 +231,12 @@ def selectTorrent(self, files=None): keywords = {} keywords["path"] = files[0] keywords["piece_length"] = info["piece length"] + if "meta version" not in info: + keywords["meta version"] = 1 + elif "pieces" in info: + keywords["meta version"] = 3 + else: + keywords["meta version"] = 2 if "created by" in meta: keywords["created_by"] = meta["created by"] else: @@ -278,22 +250,23 @@ def selectTorrent(self, files=None): keywords["announce"] = info["announce list"] + [meta["announce"]] else: keywords["announce"] = [meta["announce"]] - files, size = [], 0 + size = 0 if "files" in info: - keywords["files"] = info["files"] + contents = {} for entry in info["files"]: - files.append(os.path.join(*entry["path"])) + contents[os.path.join(info["name"], *entry["path"])] = entry["length"] size += entry["length"] - keywords["contents"] = files - keywords["length"] = size + keywords["contents"] = contents elif "file tree" in info: - paths = parse_filetree(info["file tree"]) - keywords["file tree"] = info["file tree"] - for k, v in paths.items(): - files.append(k) + contents = {} + for k, v in parse_filetree(info["file tree"]).items(): + contents[os.path.join(info["name"], k)] = v size += v - keywords["contents"] = files - keywords["length"] = size + keywords["contents"] = contents + else: + keywords["contents"] = {info["name"]: info["length"]} + size = info["length"] + keywords["length"] = size if "creation date" in meta: date = datetime.fromtimestamp(meta["creation date"]) text = date.strftime("%B %d, %Y %H:%M") @@ -304,16 +277,40 @@ def selectTorrent(self, files=None): keywords["private"] = "True" else: keywords["private"] = "False" - if "contents" not in keywords: - keywords["contents"] = [info["name"]] tab = self.parent() thread = Thread(group=None, target=tab.fill, args=(keywords,)) thread.run() +class Label(QLabel): + """Label Identifier for Window Widgets. + + Subclass: QLabel + """ + + def __init__(self, text, parent=None): + super().__init__(text, parent=parent) + self.setStyleSheet(labelSheet) + font = self.font() + font.setBold(True) + font.setPointSize(12) + self.setFont(font) + + +class InfoLineEdit(QLineEdit): + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setReadOnly(True) + self.setStyleSheet(infoLineEditSheet) + self.setDragEnabled(True) + font = self.font() + font.setBold(True) + self.setFont(font) + + def denom(num): txt = str(num) - if num < 1000: + if int(num) < 1000: return txt if 1000 <= num <= 999999: return "".join([txt[:-3], ".", txt[-3], "KB"]) @@ -338,37 +335,11 @@ def pretty_int(num): def parse_filetree(filetree): paths = {} - for key in filetree: - if key == "": - paths[key] = filetree[key]["length"] + for key, value in filetree: + if value == "": + return {key: filetree[key]["length"]} else: - out = parse_filetree(filetree[key]) + out = parse_filetree(value) for k, v in out.items(): paths[os.path.join(key, k)] = v return paths - - -class Label(QLabel): - """Label Identifier for Window Widgets. - - Subclass: QLabel - """ - - def __init__(self, text, parent=None): - super().__init__(text, parent=parent) - self.setStyleSheet(labelSheet) - font = self.font() - font.setBold(True) - font.setPointSize(12) - self.setFont(font) - - -class InfoLineEdit(QLineEdit): - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setReadOnly(True) - self.setStyleSheet(infoLineEditSheet) - self.setDragEnabled(True) - font = self.font() - font.setBold(True) - self.setFont(font)