Compare commits

...

15 Commits

Author SHA1 Message Date
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
Vxrtrauter a7c71b37f5 refactor: remove duplicate stripping 2026-03-04 21:00:37 +01:00
Vxrtrauter 8fb1bb9e8d Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-03-04 20:58:58 +01:00
Vxrtrauter 89fd8b4ebb fix: fix scraping for uztracker posts to support displaying authors properly 2026-03-04 20:58:56 +01:00
KeksNino 3915e114a3 fix: logging in uztracker scraper and search for non-ascii letters and remove unused imports 2026-03-04 20:28:55 +01:00
15 changed files with 431 additions and 99 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.logging.logs import consoleLog
def scrape_rutracker(query):
search = requests.get(f"{state.api_url}/search?q={query}")
consoleLog("Sent request to server")
@@ -12,10 +11,9 @@ def scrape_rutracker(query):
except Exception:
consoleLog("No results found / No response from server")
return None
else:
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.
+13 -21
View File
@@ -1,10 +1,7 @@
import requests
import threading
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
from core.utils.general.wrappers import run_thread
def scrape_uztracker(query):
base_url="https://uztracker.net/"
@@ -16,28 +13,23 @@ def scrape_uztracker(query):
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
for link in links:
if link.b:
title = link.b.text
url = urljoin(base_url, link['href'])
author_link = link.find_parent('td').find('a', class_="med ts-text")
author = author_link.text.strip() if author_link else "Unknown"
theme_link = link.find('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
url = urljoin(base_url, theme_link['href'])
title = theme_link.b.text
author_link = link.find('a', class_="med")
author = author_link.text.strip() if author_link else "Unknown"
posts.append(dict(
title=title,
url=url,
author=author
))
posts.append(dict(
title=title,
url=url,
author=author
))
return posts
except requests.RequestException as e:
if posts:
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
+286 -33
View File
@@ -1,5 +1,5 @@
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 (
QLineEdit,
QTableView,
@@ -10,15 +10,16 @@ from PySide6.QtWidgets import (
QHBoxLayout,
QComboBox,
QTabWidget,
QProgressBar,
QHeaderView,
QMessageBox,
QTableWidget,
QTextEdit,
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 threading
import platform
@@ -44,6 +45,22 @@ from core.interface.assets.base64_icons import logo_base64
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):
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"
@@ -138,27 +155,82 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_model = None
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.emptyDownload = QLabel("No items in downloads.")
self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.consoleLog = QTextEdit()
self.progressbar = QProgressBar()
self.statusbar = QStatusBar()
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):
table = QTableWidget()
table.setColumnCount(len(headers))
table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
table.verticalHeader().setVisible(False)
table.setHorizontalHeaderLabels(headers)
table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
table.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
header = table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
header.resizeSection(1, 200)
header.resizeSection(1, 500)
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
@@ -175,6 +247,12 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.uztrackerlist.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()
containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar)
@@ -218,17 +296,18 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
elif status.state == lt.torrent_status.seeding:
return "Seeding"
elif status.paused:
return "Paused"
is_auto = bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
return "Queued" if is_auto else "Paused"
else:
return "Queued"
elif col == 3:
return f"{status.progress * 100:.1f}%"
elif col == 4:
speed_kbs = status.download_rate / 1024
if speed_kbs > 1024:
return f"{speed_kbs / 1024:.1f} MB/s"
else:
return f"{speed_kbs:.1f} kB/s"
down_kb = status.download_rate / 1024
up_kb = status.upload_rate / 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"
return f"{down_text}{up_text}"
elif col == 5:
downloaded_mb = status.total_wanted_done / (1024 * 1024)
if downloaded_mb > 1024:
@@ -299,9 +378,24 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
idx = self.index(row, 0)
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):
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):
button = editor.findChild(QtWidgets.QPushButton)
@@ -311,9 +405,12 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if button:
if status.state == lt.torrent_status.seeding:
button.setText("📁")
button.setIcon(_svg_icon(SVG_FOLDER, 18))
button.setText("")
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):
magnet_link = list(state.active_downloads.keys())[index.row()]
@@ -321,14 +418,26 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
status = magnetdl.status()
widget = QWidget(parent)
widget.setStyleSheet("border: none;")
widget.setStyleSheet("border: none; background: transparent;")
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
if status.state == lt.torrent_status.seeding:
btnPause = QtWidgets.QPushButton("📁")
btnPause = QtWidgets.QPushButton()
btnPause.setIcon(_svg_icon(SVG_FOLDER, 18))
else:
btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎")
btnPause.setFixedSize(40, 30)
btnPause = QtWidgets.QPushButton()
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()))
layout.addStretch()
layout.addWidget(btnPause)
@@ -336,16 +445,58 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
widget.setLayout(layout)
return widget
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
self.download_model = DownloadModel()
self.downloadList.setModel(self.download_model)
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(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.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.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
@@ -354,12 +505,16 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def on_pause_resume_clicked(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)
self.downloadList.setItemDelegateForColumn(0, delegate)
delegate.clicked.connect(on_pause_resume_clicked)
# 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)
self.setCentralWidget(container)
@@ -463,8 +618,21 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.consoleLog.setReadOnly(True)
self.consoleLog.setFixedHeight(150)
self.consoleLog.setStyleSheet("margin-bottom: 0px; background: transparent;")
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.timeout.connect(self.download_list_update)
self.download_timer.start(500)
@@ -500,25 +668,110 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def download_list_update(self):
if self.download_model:
self.download_model.layoutAboutToBeChanged.emit()
self.download_model.layoutChanged.emit()
for row in range(self.download_model.rowCount()):
idx = self.download_model.index(row, 0)
self.downloadList.closePersistentEditor(idx)
self.downloadList.openPersistentEditor(idx)
row_count = self.download_model.rowCount()
if row_count > 0:
top_left = self.download_model.index(0, 0)
bottom_right = self.download_model.index(row_count - 1, self.download_model.columnCount() - 1)
self.download_model.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
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):
closehelper()
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, _):
old_tracker = state.tracker
state.tracker = self.tracker_list.currentText()
while self.horizontal_layout.count():
item = self.horizontal_layout.takeAt(0)
if item.widget():
item.widget().setParent(None)
old_widget = state.tracker_list[old_tracker]
self.horizontal_layout.removeWidget(old_widget)
old_widget.setParent(None)
tracker_widget = state.tracker_list[state.tracker]
self.horizontal_layout.addWidget(tracker_widget)
@@ -628,15 +881,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if download_path and os.path.exists(download_path):
try:
if os.path.isfile(download_path):
remove_download_log(magnet_link)
os.remove(download_path)
del state.active_downloads[magnet_link]
remove_download_log(magnet_link)
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
else:
import shutil
shutil.rmtree(download_path)
del state.active_downloads[magnet_link]
remove_download_log(magnet_link)
shutil.rmtree(download_path)
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
except Exception as e:
consoleLog(f"Error deleting files: {e}", True)
+2 -2
View File
@@ -55,7 +55,7 @@ def return_pressed(self):
state.tracker_list[state.tracker].clear()
self.show_empty_results(True)
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)
state.tracker_list[state.tracker].clear()
state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author"])
@@ -63,4 +63,4 @@ def return_pressed(self):
for i, author in enumerate(state.post_author):
state.tracker_list[state.tracker].setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
state.tracker_list[state.tracker].setItem(i, 0, QTableWidgetItem(title))
state.tracker_list[state.tracker].setItem(i, 0, QTableWidgetItem(title))
+2 -2
View File
@@ -19,7 +19,7 @@ def get_active_interfaces():
for addr in addr_list:
if addr.family == 2: # ipv4
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)
consoleLog(f"Found Active: {interface}")
@@ -33,7 +33,7 @@ def list_interfaces() -> None:
for addr in addr_list:
if addr.family == 2: # ipv4
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"
else:
status = "INACTIVE"
+40 -2
View File
@@ -1,4 +1,5 @@
import time
import os
from core.utils.general.wrappers import run_thread
from core.network.interface import get_interface_ip
import threading
@@ -51,8 +52,26 @@ def add_download(magnet_uri, dl_path=state.download_path):
init_session()
if magnet_uri in state.active_downloads:
consoleLog("Skipping Downloading, download already running...")
return
try:
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.save_path = dl_path
@@ -62,8 +81,27 @@ def add_download(magnet_uri, dl_path=state.download_path):
consoleLog(f"Added {magnet_uri} to downloads")
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():
global loop_running
+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 plyer import notification
import libtorrent as lt
import os
import time
@@ -50,7 +51,7 @@ def update_log(shutdown_event):
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")
info_hash = str(status.info_hash)
update_download_completed_by_hash(info_hash, True)
@@ -58,3 +59,23 @@ def update_log(shutdown_event):
except Exception:
pass
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)
+2 -1
View File
@@ -8,7 +8,7 @@ class AppState(QObject):
def __init__(self):
super().__init__()
self.posts: List[Dict[str, Any]] = []
self.posts: list[Any] | None = None
self.post_titles: Optional[List] = None
self.post_urls: Optional[List] = None
self.post_author: List[str] = []
@@ -30,6 +30,7 @@ class AppState(QObject):
self.settings_path: str = ""
self.dl_session: Any = None
self.active_downloads: Dict = {}
self.seeded_magnets: set = set()
self.window_transparency: bool = False
self.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.network.download import run_download_direct
from core.utils.network.download import run_download_direct, seed_magnet
import os
@@ -22,6 +22,7 @@ def check_downloads(downloads):
for download in downloads:
if os.path.exists(download.path):
consoleLog(f"Existing Download: {download.title}")
seed_magnet(download.magnet_uri, download.path)
else:
consoleLog(f"Inexistent Download: {download.title}")
remove_download_log(download.magnet_uri)
+34 -12
View File
@@ -6,23 +6,14 @@ import json
import os
import re
import time
import threading
_log_buffer = []
_downloads_lock = threading.RLock()
def add_download_log(title, url, magnet_uri, completed) -> 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 = []
# wait for metadata outside the lock to avoid blocking other threads
magnetdl = state.active_downloads.get(magnet_uri)
if magnetdl:
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)
else:
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 magnet_uri in state.active_downloads:
@@ -65,6 +71,10 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
return download_list
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")
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
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")
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:
with _downloads_lock:
return _get_download_logs_inner()
def _get_download_logs_inner() -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
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:
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")
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.data.tracker import get_item_url
from core.utils.data.tracker import get_magnet_link
from core.network.libtorrent_wrapper import add_download
from core.utils.general.wrappers import run_thread
from core.utils.logging.logs import add_download_log
import time
from core.network.libtorrent_int import add_seed
import threading
def download_selected(item, posts, post_titles):
if item is not None:
consoleLog(f"Downloading {item.text()}")
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
else:
def download_selected(items, posts, post_titles):
if not items:
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):
post_url = get_item_url(item, posts, post_titles)
consoleLog(f"Selected URL: {post_url}")
magnet_uri = get_magnet_link(post_url)
add_download(magnet_uri)
add_download_log(item, post_url, magnet_uri, False)
if add_download(magnet_uri):
add_download_log(item, post_url, magnet_uri, False)
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 -1
View File
@@ -25,7 +25,7 @@ def format_data_minimal(data):
post_titles = [post["title"] for post in data]
post_links = [post["url"] for post in data]
return post_author, post_titles, post_links
return post_author, post_titles, post_links
+1 -5
View File
@@ -17,11 +17,7 @@ def check_for_updates():
latest_version = latest_release["name"]
assets = latest_release["assets"]
if state.version == "dev":
consoleLog("Dev release detected, skipping version check")
return None, None
elif latest_version != state.version:
if latest_version != state.version:
consoleLog(f"New release available: {latest_version}")
if assets:
consoleLog("Assets:")
+3 -1
View File
@@ -1,7 +1,7 @@
from core.interface.gui import MainWindow, windowCloseHelper
from core.utils.data.state import state
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.general.shutdown import closehelper, shutdown_event
from core.utils.general.wrappers import run_thread
@@ -53,6 +53,8 @@ def main():
consoleLog("Started Thread: update_log")
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
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)
elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")