mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2a30af5cd | |||
| fbc2225878 | |||
| 4d1a639651 | |||
| 8d1b3ba68f | |||
| dcc12c0b28 | |||
| 925fe21d03 | |||
| ec462afde6 | |||
| e50ef32c66 | |||
| d6e418d045 | |||
| a7c71b37f5 | |||
| 8fb1bb9e8d | |||
| 89fd8b4ebb | |||
| 3915e114a3 | |||
| 34a50e1a1b | |||
| 90124fd9c8 | |||
| 4175f82967 | |||
| 4bf2e9af81 |
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"Version": "dev"
|
||||
"version": "dev"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ def _get_telegram_posts():
|
||||
|
||||
return(posts)
|
||||
|
||||
def scrape_monkrus_telegram(query):
|
||||
def scrape_m0nkrus(query):
|
||||
posts = _get_telegram_posts()
|
||||
filtered_posts = []
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import requests
|
||||
from core.utils.data.state import state
|
||||
from core.utils.logging.logs import consoleLog
|
||||
|
||||
|
||||
def scrape_rutracker(search_text):
|
||||
search = requests.get(f"{state.api_url}/search?q={search_text}")
|
||||
def scrape_rutracker(query):
|
||||
search = requests.get(f"{state.api_url}/search?q={query}")
|
||||
consoleLog("Sent request to server")
|
||||
if search:
|
||||
try:
|
||||
@@ -12,10 +11,9 @@ def scrape_rutracker(search_text):
|
||||
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.
|
||||
|
||||
@@ -1,83 +1,35 @@
|
||||
import requests
|
||||
import threading
|
||||
from bs4 import BeautifulSoup
|
||||
from core.utils.data.state import state
|
||||
from urllib.parse import urljoin
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.utils.general.wrappers import run_thread
|
||||
|
||||
def scrape_uztracker(query):
|
||||
base_url="https://uztracker.net/"
|
||||
search_url = f"{base_url.rstrip('/')}/tracker.php?nm={query}"
|
||||
consoleLog(search_url)
|
||||
posts = []
|
||||
|
||||
global url_uztracker
|
||||
url_uztracker = "https://uztracker.net/tracker.php?nm="
|
||||
|
||||
def init_uztracker():
|
||||
global up
|
||||
global soup
|
||||
try:
|
||||
response = requests.get(url_uztracker, timeout=10)
|
||||
if response.status_code == 200:
|
||||
up = True
|
||||
else:
|
||||
up = False
|
||||
consoleLog(f"Uztracker seems down, status code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
consoleLog(f"Request Exception on {url_uztracker}:")
|
||||
consoleLog(str(e))
|
||||
consoleLog("Is the Site down?")
|
||||
up = False
|
||||
|
||||
run_thread(threading.Thread(target=init_uztracker))
|
||||
|
||||
def scrape_uztracker(search):
|
||||
if up:
|
||||
search_url = url_uztracker + search
|
||||
consoleLog(search_url)
|
||||
result = False
|
||||
global results
|
||||
global resulttitles
|
||||
results = []
|
||||
resulttitles = []
|
||||
|
||||
try:
|
||||
resultCount = 0
|
||||
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'))
|
||||
for link in links:
|
||||
if link.b:
|
||||
resultCount += 1
|
||||
results.append(link['href'])
|
||||
resulttitles.append(link.b.text)
|
||||
result = True
|
||||
|
||||
if result:
|
||||
return resulttitles, results
|
||||
|
||||
if not result:
|
||||
# add popup in gui for no result
|
||||
return None
|
||||
|
||||
except requests.RequestException as e:
|
||||
if result:
|
||||
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||
return None
|
||||
else:
|
||||
consoleLog("Error: Uztracker down")
|
||||
return None, None
|
||||
|
||||
def get_post_title(post_url):
|
||||
if up:
|
||||
response = requests.get(post_url)
|
||||
response = requests.get(search_url)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
maintitle = soup.find(class_='tt-text')
|
||||
if maintitle:
|
||||
return maintitle.text
|
||||
else:
|
||||
consoleLog("Program not found")
|
||||
else:
|
||||
consoleLog("Error: Uztracker down")
|
||||
return None
|
||||
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
|
||||
for link in links:
|
||||
|
||||
|
||||
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
|
||||
))
|
||||
|
||||
return posts
|
||||
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||
return None
|
||||
+249
-20
@@ -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
|
||||
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"
|
||||
@@ -67,6 +84,9 @@ def download_update(latest_version):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def windowCloseHelper():
|
||||
QGuiApplication.quit()
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
log_signal = Signal(str) # Thread-safe signal for logging
|
||||
|
||||
@@ -135,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, 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
|
||||
|
||||
@@ -172,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)
|
||||
@@ -221,11 +302,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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:
|
||||
@@ -296,9 +377,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)
|
||||
|
||||
@@ -308,9 +404,11 @@ 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 "⏸︎")
|
||||
button.setIcon(_svg_icon(SVG_PLAY if status.paused else SVG_PAUSE, 18))
|
||||
button.setText("")
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
@@ -318,14 +416,25 @@ 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()
|
||||
btnPause.setIcon(_svg_icon(SVG_PLAY if status.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)
|
||||
@@ -341,8 +450,51 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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(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)
|
||||
|
||||
@@ -351,12 +503,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)
|
||||
@@ -460,8 +616,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)
|
||||
@@ -503,19 +672,79 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
idx = self.download_model.index(row, 0)
|
||||
self.downloadList.closePersistentEditor(idx)
|
||||
self.downloadList.openPersistentEditor(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):
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -2,10 +2,16 @@ from PySide6.QtWidgets import QTableWidgetItem
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.data.scrapers.uztracker import scrape_uztracker
|
||||
from core.data.scrapers.rutracker import scrape_rutracker
|
||||
from core.data.scrapers.monkrus import scrape_monkrus_telegram
|
||||
from core.utils.network.jsonhandler import split_data, format_data, format_data_m0nkrus
|
||||
from core.data.scrapers.monkrus import scrape_m0nkrus
|
||||
from core.utils.network.jsonhandler import split_data, format_data, format_data_minimal
|
||||
from core.utils.data.state import state
|
||||
|
||||
scrapers = {
|
||||
"uztracker": scrape_uztracker,
|
||||
"rutracker": scrape_rutracker,
|
||||
"m0nkrus": scrape_m0nkrus
|
||||
}
|
||||
|
||||
def return_pressed(self):
|
||||
search_text = self.searchbar.text()
|
||||
if search_text == "":
|
||||
@@ -13,29 +19,7 @@ def return_pressed(self):
|
||||
return
|
||||
consoleLog(f"User searched for: {search_text}")
|
||||
|
||||
if state.tracker == "uztracker":
|
||||
response = scrape_uztracker(search_text)
|
||||
if response:
|
||||
state.post_titles, state.post_urls = response
|
||||
state.tracker_list[state.tracker].clear()
|
||||
if state.post_titles and len(state.post_titles) > 0:
|
||||
state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
state.tracker_list[state.tracker].setRowCount(len(state.post_titles))
|
||||
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))
|
||||
self.show_empty_results(False)
|
||||
else:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
consoleLog(f"No response from uztracker")
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker == "rutracker":
|
||||
if state.tracker == "rutracker":
|
||||
response = scrape_rutracker(search_text)
|
||||
if response:
|
||||
_, state.posts, _, _, cached = split_data(response)
|
||||
@@ -64,15 +48,14 @@ def return_pressed(self):
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker == "m0nkrus":
|
||||
state.posts = scrape_monkrus_telegram(search_text)
|
||||
|
||||
elif state.tracker is not None:
|
||||
state.posts = scrapers[state.tracker](search_text)
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author = format_data_m0nkrus(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"])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 = []
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.utils.network.download import run_download_direct
|
||||
from core.utils.logging.logs import consoleLog, remove_download_log
|
||||
from core.utils.network.download import run_download_direct, seed_magnet
|
||||
|
||||
import os
|
||||
|
||||
def split_data(data):
|
||||
|
||||
@@ -16,6 +18,12 @@ def check_completed(downloads, resume):
|
||||
run_download_direct(download.magnet_uri)
|
||||
consoleLog(f"Resuming {download.title}")
|
||||
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -77,10 +77,12 @@ def remove_download_log(magnet_uri) -> DownloadList:
|
||||
else:
|
||||
downloads = []
|
||||
|
||||
|
||||
magnet_link = (magnet_uri or "").strip()
|
||||
title = next((getattr(d, 'title', 'Unknown') for d in downloads if (getattr(d, 'magnet_uri', None) or '').strip() == magnet_link or (getattr(d, 'url', None) or '').strip() == magnet_link))
|
||||
downloads = [d for d in downloads if (getattr(d, 'magnet_uri', None) or "").strip() != magnet_link and (getattr(d, 'url', None) or "").strip() != magnet_link]
|
||||
|
||||
consoleLog(f"Removed {magnet_link} from Log File")
|
||||
consoleLog(f"Removed {title} from Log File")
|
||||
|
||||
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||
with open(downloads_file, "w") as 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)
|
||||
@@ -20,12 +20,12 @@ def format_data(data):
|
||||
|
||||
return post_titles, post_links, post_author, post_seeders, post_leechers
|
||||
|
||||
def format_data_m0nkrus(data):
|
||||
def format_data_minimal(data):
|
||||
post_author = [post["author"] for post in data]
|
||||
post_titles = [post["title"] for post in data]
|
||||
post_links = [post["url"] for post in data]
|
||||
|
||||
return post_titles, post_links, post_author
|
||||
return post_author, post_titles, post_links
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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:")
|
||||
|
||||
+10
-12
@@ -1,11 +1,11 @@
|
||||
from core.interface.gui import MainWindow
|
||||
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
|
||||
from core.utils.logging.loghandler import split_data, check_completed
|
||||
from core.utils.logging.loghandler import split_data, check_completed, check_downloads
|
||||
from core.network.interface import list_interfaces, init_interfaces
|
||||
from core.utils.config.config import read_config
|
||||
from PySide6 import QtWidgets
|
||||
@@ -14,15 +14,9 @@ import qdarktheme
|
||||
import threading
|
||||
import platform
|
||||
import signal
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
def run_gui():
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("auto")
|
||||
@@ -39,14 +33,13 @@ def run_gui():
|
||||
|
||||
def keyboardinterrupthandler(signum, frame):
|
||||
closehelper()
|
||||
windowCloseHelper()
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
start_time = time.perf_counter()
|
||||
read_config()
|
||||
logs = get_download_logs()
|
||||
_, downloads = split_data(logs)
|
||||
if args.debug:
|
||||
state.debug = args.debug # override of read_config
|
||||
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
||||
consoleLog("Starting SoftwareManager...")
|
||||
consoleLog("Fetching Network Interfaces...")
|
||||
@@ -60,7 +53,12 @@ if __name__ == "__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")
|
||||
run_gui()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user