Compare commits

...

12 Commits

Author SHA1 Message Date
Vxrtrauter f030795f10 fix: implement forced exit on shutdown and enhance shutdown handling 2026-03-07 22:10:45 +01:00
KeksNino eccdb2f068 fix file deletion when seeding 2026-03-07 21:56:07 +01:00
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
Vxrtrauter dcc12c0b28 fix: LibTorrent not detecting that file has been deleted until LibTorrent Session is reinitialized / enhance download management with file existence checks and cleanup 2026-03-07 00:45:27 +01:00
Vxrtrauter 925fe21d03 Add logging and download tracking to direct downloads 2026-03-07 14:00:00 +01:00
Vxrtrauter ec462afde6 Simplify boolean checks in network interface detection 2026-03-06 12:00:00 +01:00
Vxrtrauter e50ef32c66 refactor: clean up imports and remove unused code in scrapers and download modules 2026-03-05 23:19:08 +01:00
KeksNino d6e418d045 fix: crash when there are no results in search tab 2026-03-04 21:24:56 +01:00
15 changed files with 430 additions and 85 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"Version": "dev" "version": "dev"
} }
+1 -3
View File
@@ -2,7 +2,6 @@ import requests
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
def scrape_rutracker(query): def scrape_rutracker(query):
search = requests.get(f"{state.api_url}/search?q={query}") search = requests.get(f"{state.api_url}/search?q={query}")
consoleLog("Sent request to server") consoleLog("Sent request to server")
@@ -12,10 +11,9 @@ def scrape_rutracker(query):
except Exception: except Exception:
consoleLog("No results found / No response from server") consoleLog("No results found / No response from server")
return None return None
else: else:
return None return None
# This function utilizes the SoftwareManager server - source code can be found under the "server" branch. # This function utilizes the SoftwareManager server - source code can be found under the SoftwareManager-Server repository.
+1 -4
View File
@@ -32,7 +32,4 @@ def scrape_uztracker(query):
except requests.RequestException as e: except requests.RequestException as e:
consoleLog(f"Failed to fetch {search_url}: {e}") consoleLog(f"Failed to fetch {search_url}: {e}")
return None return None
+287 -32
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,25 +668,112 @@ 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()
from core.utils.general.shutdown import force_exit
force_exit()
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
state.tracker = self.tracker_list.currentText() state.tracker = self.tracker_list.currentText()
while self.horizontal_layout.count(): old_widget = state.tracker_list[old_tracker]
item = self.horizontal_layout.takeAt(0) self.horizontal_layout.removeWidget(old_widget)
if item.widget(): old_widget.setParent(None)
item.widget().setParent(None)
tracker_widget = state.tracker_list[state.tracker] tracker_widget = state.tracker_list[state.tracker]
self.horizontal_layout.addWidget(tracker_widget) self.horizontal_layout.addWidget(tracker_widget)
@@ -628,15 +883,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if download_path and os.path.exists(download_path): if download_path and os.path.exists(download_path):
try: try:
if os.path.isfile(download_path): if os.path.isfile(download_path):
remove_download_log(magnet_link)
os.remove(download_path) os.remove(download_path)
del state.active_downloads[magnet_link] del state.active_downloads[magnet_link]
remove_download_log(magnet_link)
consoleLog(f"Deleted files for: {magnetdl.status().name}", True) consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
else: else:
import shutil import shutil
shutil.rmtree(download_path)
del state.active_downloads[magnet_link] del state.active_downloads[magnet_link]
remove_download_log(magnet_link) remove_download_log(magnet_link)
shutil.rmtree(download_path)
consoleLog(f"Deleted files for: {magnetdl.status().name}", True) consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
except Exception as e: except Exception as e:
consoleLog(f"Error deleting files: {e}", True) consoleLog(f"Error deleting files: {e}", True)
+1 -1
View File
@@ -55,7 +55,7 @@ def return_pressed(self):
state.tracker_list[state.tracker].clear() state.tracker_list[state.tracker].clear()
self.show_empty_results(True) self.show_empty_results(True)
else: else:
state.post_author, state.post_titles, state.post_urls = format_data_minimal(state.posts) state.post_author, state.post_titles, state.post_urls = format_data_minimal(state.posts)
self.show_empty_results(False) self.show_empty_results(False)
state.tracker_list[state.tracker].clear() state.tracker_list[state.tracker].clear()
state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author"]) state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author"])
+2 -2
View File
@@ -19,7 +19,7 @@ def get_active_interfaces():
for addr in addr_list: for addr in addr_list:
if addr.family == 2: # ipv4 if addr.family == 2: # ipv4
ipv4 = addr.address ipv4 = addr.address
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True: if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up:
active.append(interface) active.append(interface)
consoleLog(f"Found Active: {interface}") consoleLog(f"Found Active: {interface}")
@@ -33,7 +33,7 @@ def list_interfaces() -> None:
for addr in addr_list: for addr in addr_list:
if addr.family == 2: # ipv4 if addr.family == 2: # ipv4
ipv4 = addr.address ipv4 = addr.address
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True: if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up:
status = "ACTIVE" status = "ACTIVE"
else: else:
status = "INACTIVE" status = "INACTIVE"
+46 -4
View File
@@ -1,10 +1,12 @@
import time import time
import os
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.network.interface import get_interface_ip from core.network.interface import get_interface_ip
import threading import threading
import libtorrent as lt import libtorrent as lt
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.general.shutdown import shutdown_event
global loop_running global loop_running
@@ -51,8 +53,26 @@ def add_download(magnet_uri, dl_path=state.download_path):
init_session() init_session()
if magnet_uri in state.active_downloads: if magnet_uri in state.active_downloads:
consoleLog("Skipping Downloading, download already running...") try:
return handle = state.active_downloads[magnet_uri]
status = handle.status()
except RuntimeError as e:
consoleLog(f"Error in LibTorrent Handle: {e}")
if status.has_metadata:
filepath = os.path.join(status.save_path, status.name)
if not os.path.exists(filepath):
consoleLog(f"File Deleted, redownloading: {status.name}")
state.dl_session.remove_torrent(handle)
del state.active_downloads[magnet_uri]
else:
consoleLog("Skipping Downloading, download already running...")
return False
else:
consoleLog("Skipping Downloading, download already running... ")
return False
magnetdl = lt.parse_magnet_uri(magnet_uri) magnetdl = lt.parse_magnet_uri(magnet_uri)
magnetdl.save_path = dl_path magnetdl.save_path = dl_path
@@ -62,8 +82,27 @@ def add_download(magnet_uri, dl_path=state.download_path):
consoleLog(f"Added {magnet_uri} to downloads") consoleLog(f"Added {magnet_uri} to downloads")
run_thread(threading.Thread(target=dl_status_loop)) run_thread(threading.Thread(target=dl_status_loop))
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
@@ -78,9 +117,12 @@ def dl_status_loop():
loop_running = False loop_running = False
return return
while state.active_downloads: while state.active_downloads and not shutdown_event.is_set():
for magnet_uri, magnetdl in list(state.active_downloads.items()): for magnet_uri, magnetdl in list(state.active_downloads.items()):
status = magnetdl.status() try:
status = magnetdl.status()
except RuntimeError:
continue
if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set: if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set:
consoleLog(f"Download completed: {status.name}") consoleLog(f"Download completed: {status.name}")
+22 -1
View File
@@ -3,6 +3,7 @@ from core.utils.logging.logs import update_download_completed_by_hash
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from plyer import notification from plyer import notification
import libtorrent as lt import libtorrent as lt
import os
import time import time
@@ -50,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)
@@ -58,3 +59,23 @@ def update_log(shutdown_event):
except Exception: except Exception:
pass pass
time.sleep(5) time.sleep(5)
def check_deleted_files(shutdown_event):
while not shutdown_event.is_set():
try:
for magnet_uri, magnetdl in list(state.active_downloads.items()):
if isinstance(magnetdl, dict):
continue
status = magnetdl.status()
if status.state == lt.torrent_status.seeding and status.has_metadata:
file_path = os.path.join(status.save_path, status.name)
if not os.path.exists(file_path):
consoleLog(f"Registered File Deletion: {status.name}")
state.dl_session.remove_torrent(magnetdl)
del state.active_downloads[magnet_uri]
except Exception:
pass
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 = []
+5 -4
View File
@@ -1,9 +1,10 @@
import threading import threading
from core.network.libtorrent_misc import cleanup_session import os
from core.utils.general.wrappers import run_thread
shutdown_event = threading.Event() shutdown_event = threading.Event()
def closehelper(): def closehelper():
run_thread(threading.Thread(target=cleanup_session)) shutdown_event.set()
shutdown_event.set()
def force_exit():
os._exit(0)
+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:
+21 -13
View File
@@ -1,32 +1,40 @@
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.data.tracker import get_item_url from core.utils.data.tracker import get_item_url
from core.utils.data.tracker import get_magnet_link 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
import time 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)
consoleLog(f"Selected URL: {post_url}") consoleLog(f"Selected URL: {post_url}")
magnet_uri = get_magnet_link(post_url) magnet_uri = get_magnet_link(post_url)
add_download(magnet_uri) if add_download(magnet_uri):
add_download_log(item, post_url, magnet_uri, False) add_download_log(item, post_url, magnet_uri, False)
def run_download_direct(magnet_uri): def run_download_direct(magnet_uri):
add_download(magnet_uri) consoleLog(f"Direct download: {magnet_uri[:60]}")
add_download(magnet_uri)
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:")
+5 -2
View File
@@ -1,7 +1,7 @@
from core.interface.gui import MainWindow, windowCloseHelper from core.interface.gui import MainWindow, windowCloseHelper
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.network.libtorrent_misc import send_notification, update_log from core.network.libtorrent_misc import send_notification, update_log, check_deleted_files
from core.utils.logging.logs import get_download_logs from core.utils.logging.logs import get_download_logs
from core.utils.general.shutdown import closehelper, shutdown_event from core.utils.general.shutdown import closehelper, shutdown_event
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
@@ -33,7 +33,8 @@ def run_gui():
def keyboardinterrupthandler(signum, frame): def keyboardinterrupthandler(signum, frame):
closehelper() closehelper()
windowCloseHelper() from core.utils.general.shutdown import force_exit
force_exit()
def main(): def main():
start_time = time.perf_counter() start_time = time.perf_counter()
@@ -53,6 +54,8 @@ def main():
consoleLog("Started Thread: update_log") consoleLog("Started Thread: update_log")
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume))) run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
consoleLog("Started Thread: check_completed") consoleLog("Started Thread: check_completed")
run_thread(threading.Thread(target=check_deleted_files, args=(shutdown_event,), daemon=True))
consoleLog("Started Thread: check_deleted_files")
check_downloads(downloads) check_downloads(downloads)
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI") consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")