Compare commits

...

5 Commits

Author SHA1 Message Date
Vxrtrauter bd5ccc7688 fix: improve download status handling and enhance thread safety in logging functions 2026-03-07 21:36:11 +01:00
Vxrtrauter b2a30af5cd fix: prevent processing of non-downloadable items in download_selected function 2026-03-07 20:46:41 +01:00
Vxrtrauter fbc2225878 refactor: standardize version key in build_info.json and enhance download selection logic in download.py
feat: add SVG icons for play, pause, and folder actions in gui.py; implement status bar for download speed display
fix: remove unnecessary newline in libtorrent_misc.py and streamline update checker logic in update_checker.py
2026-03-07 20:41:03 +01:00
KeksNino 4d1a639651 remove unused QProgressBar widget 2026-03-07 17:55:52 +01:00
Vxrtrauter 8d1b3ba68f feat: implement seeding functionality and enhance download management with seeded magnets tracking 2026-03-08 12:00:00 +01:00
9 changed files with 355 additions and 53 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"Version": "dev" "version": "dev"
} }
+279 -26
View File
@@ -1,5 +1,5 @@
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtCore import QPersistentModelIndex, Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize from PySide6.QtCore import QPersistentModelIndex, Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize, QByteArray
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QTableView, QTableView,
@@ -10,15 +10,16 @@ from PySide6.QtWidgets import (
QHBoxLayout, QHBoxLayout,
QComboBox, QComboBox,
QTabWidget, QTabWidget,
QProgressBar,
QHeaderView, QHeaderView,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
QTextEdit, QTextEdit,
QStyledItemDelegate, QStyledItemDelegate,
QStatusBar
) )
from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication, QColor, QBrush
from PySide6.QtSvg import QSvgRenderer
import darkdetect import darkdetect
import threading import threading
import platform import platform
@@ -44,6 +45,22 @@ from core.interface.assets.base64_icons import logo_base64
from core.utils.general.shutdown import closehelper from core.utils.general.shutdown import closehelper
def _svg_icon(svg_str, size=20):
renderer = QSvgRenderer(QByteArray(svg_str.encode()))
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
from PySide6.QtGui import QPainter
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return QIcon(pixmap)
SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" fill="white"/></svg>'
SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="5" y="3" width="4" height="18" rx="1" fill="white"/><rect x="15" y="3" width="4" height="18" rx="1" fill="white"/></svg>'
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="white"/></svg>'
def download_update(latest_version): def download_update(latest_version):
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
@@ -138,27 +155,82 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_model = None self.download_model = None
self.downloadList = QTableView() self.downloadList = QTableView()
self.downloadList.setMouseTracking(True)
self._hovered_row = -1
self._tracker_hovered_row = -1
self._tracker_hovered_table = None
self.downloadList.viewport().installEventFilter(self)
self.emptyLibrary = QLabel("No items in library.") self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.") self.emptyDownload = QLabel("No items in downloads.")
self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter) self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.consoleLog = QTextEdit() self.consoleLog = QTextEdit()
self.progressbar = QProgressBar() self.statusbar = QStatusBar()
flush_log_buffer() flush_log_buffer()
class TrackerHoverDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
if index.row() == MainWindow._instance._tracker_hovered_row:
painter.save()
painter.fillRect(option.rect, QColor(255, 255, 255, 15))
painter.restore()
super().paint(painter, option, index)
self._tracker_hover_delegate = TrackerHoverDelegate(self)
def create_tracker_table(headers): def create_tracker_table(headers):
table = QTableWidget() table = QTableWidget()
table.setColumnCount(len(headers)) table.setColumnCount(len(headers))
table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
table.verticalHeader().setVisible(False) table.verticalHeader().setVisible(False)
table.setHorizontalHeaderLabels(headers) table.setHorizontalHeaderLabels(headers)
table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
table.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
header = table.horizontalHeader() header = table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
header.resizeSection(1, 500) header.resizeSection(1, 500)
header.setStretchLastSection(False) header.setStretchLastSection(False)
header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
header.setHighlightSections(False)
table.setShowGrid(False)
table.setStyleSheet("""
QTableWidget {
border: none;
outline: 0;
font-size: 13px;
}
QTableWidget::item {
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
padding: 6px 14px;
}
QTableWidget::item:selected {
background: rgba(255, 255, 255, 0.04);
}
QHeaderView {
background: transparent;
}
QHeaderView::section {
background: transparent;
color: rgba(255, 255, 255, 0.5);
font-weight: normal;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 6px 14px;
}
QHeaderView::section:checked {
background: transparent;
color: rgba(255, 255, 255, 0.5);
font-weight: normal;
}
""")
table.setMouseTracking(True)
table.viewport().setMouseTracking(True)
table.setItemDelegate(self._tracker_hover_delegate)
return table return table
@@ -175,6 +247,12 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.uztrackerlist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.uztrackerlist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.uztrackerlist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.uztrackerlist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.monkruslist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.monkruslist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist):
tbl.viewport().installEventFilter(self)
container = QWidget() container = QWidget()
containerLayout = QVBoxLayout() containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar) containerLayout.addWidget(self.searchbar)
@@ -218,17 +296,18 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
elif status.state == lt.torrent_status.seeding: elif status.state == lt.torrent_status.seeding:
return "Seeding" return "Seeding"
elif status.paused: elif status.paused:
return "Paused" is_auto = bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
return "Queued" if is_auto else "Paused"
else: else:
return "Queued" return "Queued"
elif col == 3: elif col == 3:
return f"{status.progress * 100:.1f}%" return f"{status.progress * 100:.1f}%"
elif col == 4: elif col == 4:
speed_kbs = status.download_rate / 1024 down_kb = status.download_rate / 1024
if speed_kbs > 1024: up_kb = status.upload_rate / 1024
return f"{speed_kbs / 1024:.1f} MB/s" down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s"
else: up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s"
return f"{speed_kbs:.1f} kB/s" return f"{down_text}{up_text}"
elif col == 5: elif col == 5:
downloaded_mb = status.total_wanted_done / (1024 * 1024) downloaded_mb = status.total_wanted_done / (1024 * 1024)
if downloaded_mb > 1024: if downloaded_mb > 1024:
@@ -299,9 +378,24 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
idx = self.index(row, 0) idx = self.index(row, 0)
self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole]) self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
class HoverRowDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
if index.row() == MainWindow._instance._hovered_row:
painter.save()
painter.fillRect(option.rect, QColor(255, 255, 255, 15))
painter.restore()
super().paint(painter, option, index)
class PauseResumeDelegate(QStyledItemDelegate): class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int) clicked = Signal(int)
def paint(self, painter, option, index):
if index.row() == MainWindow._instance._hovered_row:
painter.save()
painter.fillRect(option.rect, QColor(255, 255, 255, 15))
painter.restore()
super().paint(painter, option, index)
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
button = editor.findChild(QtWidgets.QPushButton) button = editor.findChild(QtWidgets.QPushButton)
@@ -311,9 +405,12 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if button: if button:
if status.state == lt.torrent_status.seeding: if status.state == lt.torrent_status.seeding:
button.setText("📁") button.setIcon(_svg_icon(SVG_FOLDER, 18))
button.setText("")
else: else:
button.setText("▶︎" if status.paused else "⏸︎") is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
button.setIcon(_svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
button.setText("")
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
magnet_link = list(state.active_downloads.keys())[index.row()] magnet_link = list(state.active_downloads.keys())[index.row()]
@@ -321,14 +418,26 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
status = magnetdl.status() status = magnetdl.status()
widget = QWidget(parent) widget = QWidget(parent)
widget.setStyleSheet("border: none;") widget.setStyleSheet("border: none; background: transparent;")
layout = QHBoxLayout(widget) layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
if status.state == lt.torrent_status.seeding: if status.state == lt.torrent_status.seeding:
btnPause = QtWidgets.QPushButton("📁") btnPause = QtWidgets.QPushButton()
btnPause.setIcon(_svg_icon(SVG_FOLDER, 18))
else: else:
btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎") btnPause = QtWidgets.QPushButton()
btnPause.setFixedSize(40, 30) is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
btnPause.setIcon(_svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
btnPause.setIconSize(QSize(18, 18))
btnPause.setFixedSize(30, 30)
btnPause.setCursor(Qt.CursorShape.PointingHandCursor)
btnPause.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
btnPause.clicked.connect(lambda: self.clicked.emit(index.row())) btnPause.clicked.connect(lambda: self.clicked.emit(index.row()))
layout.addStretch() layout.addStretch()
layout.addWidget(btnPause) layout.addWidget(btnPause)
@@ -336,16 +445,58 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
def editorEvent(self, event, model, option, index): def editorEvent(self, event, model, option, index):
if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress:
self.clicked.emit(index.row())
return True
return False return False
self.download_model = DownloadModel() self.download_model = DownloadModel()
self.downloadList.setModel(self.download_model) self.downloadList.setModel(self.download_model)
self.downloadList.horizontalHeader().setStretchLastSection(False) self.downloadList.horizontalHeader().setStretchLastSection(False)
self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.downloadList.horizontalHeader().resizeSection(0, 70)
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.downloadList.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
self.downloadList.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
self.downloadList.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
self.downloadList.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
self.downloadList.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
self.downloadList.horizontalHeader().setMinimumSectionSize(60)
self.downloadList.verticalHeader().setDefaultSectionSize(40)
self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
self.downloadList.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
self.downloadList.verticalHeader().setVisible(False)
self.downloadList.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.downloadList.horizontalHeader().setHighlightSections(False)
self.downloadList.setMouseTracking(True)
self.downloadList.setShowGrid(False)
self.downloadList.setStyleSheet("""
QTableView {
border: none;
outline: 0;
font-size: 13px;
}
QTableView::item {
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
padding: 6px 14px;
}
QTableView::item:selected {
background: rgba(255, 255, 255, 0.04);
}
QHeaderView {
background: transparent;
}
QHeaderView::section {
background: transparent;
color: rgba(255, 255, 255, 0.5);
font-weight: normal;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 6px 14px;
}
QHeaderView::section:checked {
background: transparent;
color: rgba(255, 255, 255, 0.5);
font-weight: normal;
}
""")
self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
@@ -354,12 +505,16 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def on_pause_resume_clicked(row): def on_pause_resume_clicked(row):
download_model.toggle_pause_resume(row) download_model.toggle_pause_resume(row)
self.downloadList.viewport().setMouseTracking(True)
hover_delegate = HoverRowDelegate(self.downloadList)
self.downloadList.setItemDelegate(hover_delegate)
delegate = PauseResumeDelegate(self.downloadList) delegate = PauseResumeDelegate(self.downloadList)
self.downloadList.setItemDelegateForColumn(0, delegate) self.downloadList.setItemDelegateForColumn(0, delegate)
delegate.clicked.connect(on_pause_resume_clicked) delegate.clicked.connect(on_pause_resume_clicked)
# download button triggers # download button triggers
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.tracker_list[state.tracker].currentItem(), state.posts, state.post_titles)))) self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.tracker_list[state.tracker].selectedItems(), state.posts, state.post_titles))))
container.setLayout(containerLayout) container.setLayout(containerLayout)
self.setCentralWidget(container) self.setCentralWidget(container)
@@ -463,8 +618,21 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.consoleLog.setReadOnly(True) self.consoleLog.setReadOnly(True)
self.consoleLog.setFixedHeight(150) self.consoleLog.setFixedHeight(150)
self.consoleLog.setStyleSheet("margin-bottom: 0px; background: transparent;")
containerLayout.addWidget(self.consoleLog) containerLayout.addWidget(self.consoleLog)
self.statusbar.show()
self.statusbar.setStyleSheet("background: transparent; border: none; padding-right: 1px; padding-left: 6px; padding-bottom: 1px; margin-top: -8px")
self.version = QLabel(f"Version: {state.version}")
self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
self.speed_label = QLabel("↓ 0.0 kB/s ↑ 0.0 kB/s")
self.speed_label.setStyleSheet("padding-right: 6px;")
self.statusbar.addPermanentWidget(self.speed_label)
self.setStatusBar(self.statusbar)
self.download_timer = QTimer() self.download_timer = QTimer()
self.download_timer.timeout.connect(self.download_list_update) self.download_timer.timeout.connect(self.download_list_update)
self.download_timer.start(500) self.download_timer.start(500)
@@ -500,18 +668,103 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def download_list_update(self): def download_list_update(self):
if self.download_model: if self.download_model:
self.download_model.layoutAboutToBeChanged.emit() row_count = self.download_model.rowCount()
self.download_model.layoutChanged.emit() if row_count > 0:
for row in range(self.download_model.rowCount()): top_left = self.download_model.index(0, 0)
idx = self.download_model.index(row, 0) bottom_right = self.download_model.index(row_count - 1, self.download_model.columnCount() - 1)
self.downloadList.closePersistentEditor(idx) self.download_model.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
self.downloadList.openPersistentEditor(idx)
if not hasattr(self, '_last_dl_row_count'):
self._last_dl_row_count = 0
if row_count != self._last_dl_row_count:
for row in range(row_count, self._last_dl_row_count):
idx = self.download_model.index(row, 0)
self.downloadList.closePersistentEditor(idx)
self._last_dl_row_count = row_count
self.download_model.layoutAboutToBeChanged.emit()
self.download_model.layoutChanged.emit()
for row in range(row_count):
idx = self.download_model.index(row, 0)
self.downloadList.closePersistentEditor(idx)
self.downloadList.openPersistentEditor(idx)
else:
delegate = self.downloadList.itemDelegateForColumn(0)
for row in range(row_count):
idx = self.download_model.index(row, 0)
editor = self.downloadList.indexWidget(idx)
if editor and delegate:
delegate.setEditorData(editor, idx)
self._update_speed_label()
def _update_speed_label(self):
total_down = 0
total_up = 0
for handle in state.active_downloads.values():
try:
s = handle.status()
total_down += s.download_rate
total_up += s.upload_rate
except Exception:
pass
down_kb = total_down / 1024
up_kb = total_up / 1024
down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s"
up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s"
self.speed_label.setText(f"{down_text}{up_text}")
def mousePressEvent(self, event):
self.rutrackerlist.clearSelection()
self.uztrackerlist.clearSelection()
self.monkruslist.clearSelection()
self.downloadList.clearSelection()
super().mousePressEvent(event)
def closeEvent(self, event: QCloseEvent): def closeEvent(self, event: QCloseEvent):
closehelper() closehelper()
event.accept() event.accept()
def eventFilter(self, obj, event):
try:
for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist):
if obj == tbl.viewport():
if event.type() == QEvent.Type.MouseMove:
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
idx = tbl.indexAt(pos)
new_row = idx.row() if idx.isValid() else -1
if new_row != self._tracker_hovered_row or tbl is not self._tracker_hovered_table:
self._tracker_hovered_row = new_row
self._tracker_hovered_table = tbl
tbl.viewport().update()
elif event.type() == QEvent.Type.Leave:
self._tracker_hovered_row = -1
self._tracker_hovered_table = None
tbl.viewport().update()
break
if obj == self.downloadList.viewport():
if event.type() == QEvent.Type.MouseMove:
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
idx = self.downloadList.indexAt(pos)
new_row = idx.row() if idx.isValid() else -1
if new_row != self._hovered_row:
old_row = self._hovered_row
self._hovered_row = new_row
self._invalidate_hover_row(old_row)
self._invalidate_hover_row(new_row)
elif event.type() == QEvent.Type.Leave:
old_row = self._hovered_row
self._hovered_row = -1
self._invalidate_hover_row(old_row)
except RuntimeError:
pass
return super().eventFilter(obj, event)
def _invalidate_hover_row(self, row):
if row >= 0 and self.download_model:
self.downloadList.viewport().update()
def set_tracker(self, _): def set_tracker(self, _):
old_tracker = state.tracker old_tracker = state.tracker
state.tracker = self.tracker_list.currentText() state.tracker = self.tracker_list.currentText()
+18
View File
@@ -84,6 +84,24 @@ def add_download(magnet_uri, dl_path=state.download_path):
return True return True
def add_seed(magnet_uri, file_path):
if state.active_downloads is None:
state.active_downloads = {}
init_session()
if magnet_uri in state.active_downloads:
consoleLog("Already seeding this torrent")
return False
magnetdl = lt.parse_magnet_uri(magnet_uri)
magnetdl.save_path = os.path.dirname(file_path)
handle = state.dl_session.add_torrent(magnetdl)
state.active_downloads[magnet_uri] = handle
state.seeded_magnets.add(magnet_uri)
return True
def dl_status_loop(): def dl_status_loop():
global loop_running global loop_running
+1 -2
View File
@@ -51,7 +51,7 @@ def update_log(shutdown_event):
status = magnetdl.status() status = magnetdl.status()
if status.state == lt.torrent_status.seeding and magnet_uri not in updated: if status.state == lt.torrent_status.seeding and magnet_uri not in updated and magnet_uri not in state.seeded_magnets:
consoleLog(f"Marking {status.name} as completed") consoleLog(f"Marking {status.name} as completed")
info_hash = str(status.info_hash) info_hash = str(status.info_hash)
update_download_completed_by_hash(info_hash, True) update_download_completed_by_hash(info_hash, True)
@@ -76,7 +76,6 @@ def check_deleted_files(shutdown_event):
consoleLog(f"Registered File Deletion: {status.name}") consoleLog(f"Registered File Deletion: {status.name}")
state.dl_session.remove_torrent(magnetdl) state.dl_session.remove_torrent(magnetdl)
del state.active_downloads[magnet_uri] del state.active_downloads[magnet_uri]
except Exception: except Exception:
pass pass
time.sleep(5) time.sleep(5)
+1
View File
@@ -30,6 +30,7 @@ class AppState(QObject):
self.settings_path: str = "" self.settings_path: str = ""
self.dl_session: Any = None self.dl_session: Any = None
self.active_downloads: Dict = {} self.active_downloads: Dict = {}
self.seeded_magnets: set = set()
self.window_transparency: bool = False self.window_transparency: bool = False
self.interfaces: List = [] self.interfaces: List = []
self.active_interfaces: List = [] self.active_interfaces: List = []
+2 -1
View File
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog, remove_download_log from core.utils.logging.logs import consoleLog, remove_download_log
from core.utils.network.download import run_download_direct from core.utils.network.download import run_download_direct, seed_magnet
import os import os
@@ -22,6 +22,7 @@ def check_downloads(downloads):
for download in downloads: for download in downloads:
if os.path.exists(download.path): if os.path.exists(download.path):
consoleLog(f"Existing Download: {download.title}") consoleLog(f"Existing Download: {download.title}")
seed_magnet(download.magnet_uri, download.path)
else: else:
consoleLog(f"Inexistent Download: {download.title}") consoleLog(f"Inexistent Download: {download.title}")
remove_download_log(download.magnet_uri) remove_download_log(download.magnet_uri)
+34 -12
View File
@@ -6,23 +6,14 @@ import json
import os import os
import re import re
import time import time
import threading
_log_buffer = [] _log_buffer = []
_downloads_lock = threading.RLock()
def add_download_log(title, url, magnet_uri, completed) -> DownloadList: def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") # wait for metadata outside the lock to avoid blocking other threads
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except json.JSONDecodeError:
downloads = []
else:
downloads = []
magnetdl = state.active_downloads.get(magnet_uri) magnetdl = state.active_downloads.get(magnet_uri)
if magnetdl: if magnetdl:
status = magnetdl.status() status = magnetdl.status()
@@ -37,6 +28,21 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
path = os.path.join(save_path, torrent_name) path = os.path.join(save_path, torrent_name)
else: else:
path = os.path.join(state.download_path, title) path = os.path.join(state.download_path, title)
with _downloads_lock:
return _add_download_log_inner(title, url, magnet_uri, completed, path)
def _add_download_log_inner(title, url, magnet_uri, completed, path) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except json.JSONDecodeError:
downloads = []
else:
downloads = []
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads): if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
if magnet_uri in state.active_downloads: if magnet_uri in state.active_downloads:
@@ -65,6 +71,10 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
return download_list return download_list
def remove_download_log(magnet_uri) -> DownloadList: def remove_download_log(magnet_uri) -> DownloadList:
with _downloads_lock:
return _remove_download_log_inner(magnet_uri)
def _remove_download_log_inner(magnet_uri) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
@@ -91,6 +101,10 @@ def remove_download_log(magnet_uri) -> DownloadList:
return download_list return download_list
def update_download_completed(magnet_uri, completed) -> DownloadList: def update_download_completed(magnet_uri, completed) -> DownloadList:
with _downloads_lock:
return _update_download_completed_inner(magnet_uri, completed)
def _update_download_completed_inner(magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
@@ -143,6 +157,10 @@ def update_download_completed(magnet_uri, completed) -> DownloadList:
def get_download_logs() -> DownloadList: def get_download_logs() -> DownloadList:
with _downloads_lock:
return _get_download_logs_inner()
def _get_download_logs_inner() -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
@@ -166,6 +184,10 @@ def extract_hash_from_magnet(magnet_uri): # full credits to claude for this
def update_download_completed_by_hash(info_hash, completed) -> DownloadList: def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
with _downloads_lock:
return _update_download_completed_by_hash_inner(info_hash, completed)
def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
+18 -6
View File
@@ -4,15 +4,23 @@ from core.utils.data.tracker import get_magnet_link
from core.network.libtorrent_wrapper import add_download from core.network.libtorrent_wrapper import add_download
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.logging.logs import add_download_log from core.utils.logging.logs import add_download_log
from core.network.libtorrent_int import add_seed
import threading import threading
def download_selected(item, posts, post_titles): def download_selected(items, posts, post_titles):
if item is not None: if not items:
consoleLog(f"Downloading {item.text()}")
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
else:
consoleLog("No item selected for download.") consoleLog("No item selected for download.")
return
seen = set()
for item in items:
if item.column() != 0:
continue
text = item.text()
if text and text not in seen:
seen.add(text)
consoleLog(f"Downloading {text}")
run_thread(threading.Thread(target=run_download, args=(text, posts, post_titles)))
def run_download(item, posts, post_titles): def run_download(item, posts, post_titles):
post_url = get_item_url(item, posts, post_titles) post_url = get_item_url(item, posts, post_titles)
@@ -25,4 +33,8 @@ def run_download(item, posts, post_titles):
def run_download_direct(magnet_uri): def run_download_direct(magnet_uri):
consoleLog(f"Direct download: {magnet_uri[:60]}") consoleLog(f"Direct download: {magnet_uri[:60]}")
add_download(magnet_uri) add_download(magnet_uri)
add_download_log("Direct Download", "", magnet_uri, False) add_download_log("Direct Download", "", magnet_uri, False)
def seed_magnet(magnet_uri, file_path):
consoleLog(f"Seeding: {magnet_uri[:60]}")
add_seed(magnet_uri, file_path)
+1 -5
View File
@@ -17,11 +17,7 @@ def check_for_updates():
latest_version = latest_release["name"] latest_version = latest_release["name"]
assets = latest_release["assets"] assets = latest_release["assets"]
if latest_version != state.version:
if state.version == "dev":
consoleLog("Dev release detected, skipping version check")
return None, None
elif latest_version != state.version:
consoleLog(f"New release available: {latest_version}") consoleLog(f"New release available: {latest_version}")
if assets: if assets:
consoleLog("Assets:") consoleLog("Assets:")