Merge pull request #40 from noschXL/main

This commit is contained in:
shayaa
2026-03-09 17:18:40 +01:00
committed by GitHub
17 changed files with 635 additions and 439 deletions
+3 -220
View File
@@ -1,220 +1,3 @@
# Byte-compiled / optimized / DLL files # Created by venv; see https://docs.python.org/3/library/venv.html
__pycache__/ __pycache__/
*.py[codz] *.exe
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# IDE's and editors
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
*.code-workspace
*.swp
# Local build files
build-local.ps1
build-local.bat
+1
View File
@@ -9,3 +9,4 @@ plyer==2.1.0
psutil==7.1.0 psutil==7.1.0
libtorrent==2.0.11 libtorrent==2.0.11
libtorrent-windows-dll==0.0.3 libtorrent-windows-dll==0.0.3
fuzzyfinder==2.3.0
+22 -3
View File
@@ -1,6 +1,10 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import requests import requests
import time import time
from core.utils.data.state import state
from core.utils.network.jsonhandler import format_data
from core.utils.data.tracker import get_magnet_link
from typing import Dict
cache = { cache = {
"data": [], "data": [],
@@ -31,7 +35,7 @@ def _get_telegram_posts():
for bubble in bubbles: for bubble in bubbles:
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text") post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
if not post_txt or not post_txt.b: if not post_txt is None or not post_txt.b is None:
continue continue
title = post_txt.b.text title = post_txt.b.text
@@ -42,9 +46,9 @@ def _get_telegram_posts():
if post_url not in added: if post_url not in added:
added.add(post_url) added.add(post_url)
posts.append(dict( posts.append(dict(
title=title,
author="m0nkrus", author="m0nkrus",
id=len(posts) + 1, id=len(posts) + 1,
title=title,
url=post_url url=post_url
)) ))
@@ -67,4 +71,19 @@ def scrape_m0nkrus(query):
filtered_post["id"] = len(filtered_posts) + 1 filtered_post["id"] = len(filtered_posts) + 1
filtered_posts.append(filtered_post) filtered_posts.append(filtered_post)
return filtered_posts return filtered_posts
def get_magnet(post: Dict):
_, post_links, _, _, _ = format_data([post])
return get_magnet_link(post_links[0])
Metadata = {
"name" : "m0nkrus",
"headers" : ["Post Title", "Author"],
"scrapeFunc" : scrape_m0nkrus,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_m0nkrus():
state.trackers.update({Metadata["name"] : Metadata})
@@ -0,0 +1,22 @@
from bs4 import BeautifulSoup
import requests
def scrape_buzzheavier(url):
response = requests.get(url)
response.raise_for_status()
download_url = url + '/download'
headers = {
'hx-current-url': url,
'hx-request': 'true',
'referer': url
}
head_response = requests.head(download_url, headers=headers, allow_redirects=False)
hx_redirect = head_response.headers.get('hx-redirect')
return hx_redirect
if __name__ == "__main__":
example = "https://buzzheavier.com/irzewxdjt5ah"
download = scrape_buzzheavier("https://buzzheavier.com/irzewxdjt5ah")
print(download)
@@ -0,0 +1,21 @@
import requests
from bs4 import BeautifulSoup
def scrape_fichier(url: str):
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
btn = soup.find_all("a", class_="ok")
print(response.text)
link = btn[0].get("href")
return link
if __name__ == "__main__":
print(scrape_fichier("https://1fichier.com/?ai6vzcfi1r79q9rebc3a"))
+30
View File
@@ -0,0 +1,30 @@
import requests
import re
from core.utils.logging.loghandler import consoleLog
def scrape_gofile(url):
filetoken = re.findall(r"(?<=https...gofile.io\/d\/).*", url)[0]
session = requests.Session()
acc = session.post("https://api.gofile.io/accounts")
token = acc.json()["data"]["token"]
headers = {
"Authorization": f"Bearer {token}",
"X-Website-Token": "4fd6sg89d7s6",
}
r = session.get(
f"https://api.gofile.io/contents/{filetoken}",
headers=headers,
)
try:
temp: dict = r.json()["data"]["children"]
child = [key for key in temp.keys()]
return temp[child[0]]["link"]
except:
consoleLog("gofile didnt auth you, either the api has changed\n or you sent to many requests, please try again later.\n If it still doesnt work please open an issue on Github.")
+33
View File
@@ -0,0 +1,33 @@
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
options = uc.ChromeOptions()
options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
driver = uc.Chrome(options=options, version_main=145)
driver.get("https://megadb.net/qp0yi1v1p6nl")
WebDriverWait(driver, 20).until(
EC.frame_to_be_available_and_switch_to_it(
(By.XPATH, "//iframe[@title='reCAPTCHA']")
)
)
checkboxui = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.ID, "recaptcha-anchor"))
)
checkboxelement = driver.find_element(By.ID, "recaptcha-anchor")
while checkboxelement.get_attribute("aria-checked") == "false":
time.sleep(1)
print("captcha is solved")
driver.quit()
@@ -0,0 +1,25 @@
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
import requests
def scrape_vikingfile(url):
options = uc.ChromeOptions()
driver = uc.Chrome(options=options, version_main=145)
driver.get(url)
while driver.find_element(By.ID, "download-link").get_attribute("href") is None:
continue
link = driver.find_element(By.ID, "download-link").get_attribute("href")
driver.quit()
return link
if __name__ == "__main__":
example = "https://vik1ngfile.site/f/4txMf0RghN"
downloadlink = scrape_vikingfile(example)
print(downloadlink)
+38 -9
View File
@@ -1,19 +1,48 @@
import requests 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
from core.utils.data.tracker import get_magnet_link
from core.utils.network.jsonhandler import split_data, format_data
from typing import Dict
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("[core.data.scrapers.rutracker] Sent request to server")
if search: if search:
try: _, data, _, _, _ = split_data(search.text)
return search.text
except Exception:
consoleLog("No results found / No response from server")
return None
else:
return None
sorteddata = []
for entry in data:
sorteddata.append(
{
"title" : entry["title"],
"author" : entry["author"],
"seeders" : entry["seeders"],
"leechers" : entry["leechers"],
"url" : entry["url"],
"id" : entry["id"],
}
)
return sorteddata
else:
consoleLog("[core.data.scrapers.rutracker] No search Text, returning nothing")
return []
# This function utilizes the SoftwareManager server - source code can be found under the SoftwareManager-Server repository. def get_magnet(post: Dict):
_, post_links, _, _, _ = format_data([post])
return get_magnet_link(post_links[0])
Metadata = {
"name" : "rutracker",
"headers" : ["Post Title", "Author", "Seeders", "Leechers"],
"scrapeFunc" : scrape_rutracker,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_rutracker():
state.trackers.update({Metadata["name"] : Metadata})
+132
View File
@@ -0,0 +1,132 @@
from bs4 import BeautifulSoup
from fuzzyfinder.main import fuzzyfinder
import requests
import re
import time
import webbrowser
from core.data.scrapers.provider.buzzheavier import scrape_buzzheavier
from core.data.scrapers.provider.gofile import scrape_gofile
from typing import Generator, Dict
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
cache = {
"data": [],
"last_fetched": 0
}
cache_expiry = 300
def get_Metadata():
return Metadata
def scrape_steamrip_links():
current_time = time.time()
if cache["data"] != [] and (current_time - cache["last_fetched"] < cache_expiry):
return cache["data"]
url = f"https://steamrip.com/games-list-page/"
response = requests.get(url)
text = response.text
soup = BeautifulSoup(text, "html.parser")
games = soup.find_all("li", class_="az-list-item")
if len(games) == 0:
return []
links = []
names = []
for gamehtml in games:
link = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
links += re.findall(r'(?<=href=")[^"]*', link.__str__())
name = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
names += re.findall(r'(?<=\/">)[^<]*', name.__str__())
# construct the list[dict[str,str]]
ret = []
for i in range(len(names)-1):
ret.append({"name" : names[i], "link" : links[i]})
return ret
def scrape_steamrip_game_downloads(gamelink):
url = "https://steamrip.com" + gamelink
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
download_link_elements = soup.find_all("a",class_="shortc-button")
download_links = ["buzzheavier", "gofile", "vikingfile", "megadb"]
for download_link in download_link_elements:
pure = download_link.attrs.get("href")
if pure[2] == "b":
download_links[0] = "https:" + pure
if pure[2] == "g":
download_links[1] = "https:" + pure
if pure[2] == "v":
download_links[2] = "https:" + pure
if pure[2] == "m":
download_links[3] = "https:" + pure
ret = []
for link in download_links:
if len(link) != 1:
ret.append(link)
cache["data"] = ret
return ret
def get_download_link(post: Dict):
url = post["link"]
links = scrape_steamrip_game_downloads(url)
best = ""
for link in links:
if link[0] == "h":
best = link
break
if links.index(best) == 0:
return scrape_buzzheavier(best)
elif links.index(best) == 1:
return scrape_gofile(best)
else:
consoleLog("cant scrape this cuz of captcha... opening in the browser")
webbrowser.open(best)
return None
def filter_steamrip(query: str):
options: Generator = fuzzyfinder(query, scrape_steamrip_links(), accessor=lambda x: x["name"])
return list(options)
Metadata = {
"name" : "steamrip",
"headers" : ["Game"],
"scrapeFunc" : filter_steamrip,
"linkFunc" : get_download_link,
"isMagnet" : False,
}
def init_steamrip():
state.trackers.update({Metadata["name"] : Metadata})
if __name__ == "__main__":
print(scrape_steamrip_game_downloads("/r-e-p-o-free-download/"))
+21 -2
View File
@@ -2,6 +2,10 @@ import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from urllib.parse import urljoin from urllib.parse import urljoin
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from core.utils.data.tracker import get_magnet_link
from typing import Dict
def scrape_uztracker(query): def scrape_uztracker(query):
base_url="https://uztracker.net/" base_url="https://uztracker.net/"
@@ -26,12 +30,27 @@ def scrape_uztracker(query):
posts.append(dict( posts.append(dict(
title=title, title=title,
author=author,
url=url, url=url,
author=author
)) ))
return posts return posts
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
def get_magnet(post: Dict):
item = "https://uztracker.net/" + post["url"].lstrip("./")
return get_magnet_link(item)
Metadata = {
"name" : "uztracker",
"headers" : ["Post Title", "Author"],
"scrapeFunc" : scrape_uztracker,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_uztracker():
state.trackers.update({Metadata["name"] : Metadata})
+92 -102
View File
@@ -13,6 +13,7 @@ from PySide6.QtWidgets import (
QHeaderView, QHeaderView,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
QTableWidgetItem,
QTextEdit, QTextEdit,
QStyledItemDelegate, QStyledItemDelegate,
QStatusBar QStatusBar
@@ -189,15 +190,24 @@ def download_update(latest_version):
def windowCloseHelper(): def windowCloseHelper():
QGuiApplication.quit() QGuiApplication.quit()
class TrackerHoverDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
if index.row() == MainWindow._instance._tracker_hovered_row:
painter.save()
painter.fillRect(option.rect, _theme_colors()["hover"])
painter.restore()
super().paint(painter, option, index)
class MainWindow(QtWidgets.QMainWindow, QWidget): class MainWindow(QtWidgets.QMainWindow, QWidget):
log_signal = Signal(str) # Thread-safe signal for logging log_signal = Signal(str) # Thread-safe signal for logging
search_finished_signal = Signal() # Thread-safe signal for search completion search_results_signal = Signal(list) # and searching
def __init__(self): def __init__(self):
super().__init__() super().__init__()
MainWindow._instance = self MainWindow._instance = self
self.log_signal.connect(self._on_log_signal) self.log_signal.connect(self._on_log_signal)
self.search_finished_signal.connect(self._on_search_finished) self.search_results_signal.connect(self._on_search_results)
pixmap = QPixmap() pixmap = QPixmap()
image_data = base64.b64decode(logo_base64) image_data = base64.b64decode(logo_base64)
@@ -273,68 +283,30 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
flush_log_buffer() flush_log_buffer()
class TrackerHoverDelegate(QStyledItemDelegate): # create initial tracker table
def paint(self, painter, option, index): state.trackertable = QTableWidget()
if index.row() == MainWindow._instance._tracker_hovered_row: state.trackertable.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
painter.save() state.trackertable.verticalHeader().setVisible(False)
painter.fillRect(option.rect, _theme_colors()["hover"]) state.trackertable.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
painter.restore() state.trackertable.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
super().paint(painter, option, index) model = state.trackertable.model()
model.dataChanged.disconnect()
state.trackertable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self._tracker_hover_delegate = TrackerHoverDelegate(self) state.trackertable.setStyleSheet(_table_stylesheet("QTableWidget"))
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() state.trackertable.setItemDelegate(TrackerHoverDelegate(state.trackertable))
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) state.trackertable.viewport().installEventFilter(self)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) state.trackertable.setMouseTracking(True)
header.resizeSection(1, 500) state.trackertable.viewport().setMouseTracking(True)
header.setStretchLastSection(False)
header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
header.setHighlightSections(False)
table.setShowGrid(False)
table.setStyleSheet(_table_stylesheet("QTableWidget"))
table.setMouseTracking(True)
table.viewport().setMouseTracking(True)
table.setItemDelegate(self._tracker_hover_delegate)
return table
self.rutrackerlist = create_tracker_table(["Post Title", "Author", "Seeders", "Leechers"])
self.uztrackerlist = create_tracker_table(["Post Title", "Author"])
self.monkruslist = create_tracker_table(["Post Title", "Author"])
state.tracker_list.update({"rutracker": self.rutrackerlist, "uztracker": self.uztrackerlist, "m0nkrus": self.monkruslist})
self.rutrackerlist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.rutrackerlist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
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() container = QWidget()
containerLayout = QVBoxLayout() containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(state.trackertable)
search_row = QHBoxLayout() search_row = QHBoxLayout()
search_row.addWidget(self.searchbar) search_row.addWidget(self.searchbar)
containerLayout.addLayout(search_row)
containerLayout.addWidget(state.tracker_list[state.tracker])
class DownloadModel(QAbstractTableModel): class DownloadModel(QAbstractTableModel):
def __init__(self): def __init__(self):
@@ -568,8 +540,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
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 self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.trackertable.selectedItems(),))))
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)
@@ -589,10 +560,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.horizontal_layout = QHBoxLayout() self.horizontal_layout = QHBoxLayout()
self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.tracker_widget = state.tracker_list[state.tracker] self.horizontal_layout.addWidget(state.trackertable)
self.horizontal_layout.addWidget(self.tracker_widget)
self.tab1 = create_tab("Search", self.searchbar, state.tracker_list[state.tracker], self.tabs, self.dlbutton, self.horizontal_layout) self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout)
# self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None) # self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
@@ -621,7 +591,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tracker_list = QComboBox() self.tracker_list = QComboBox()
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"]) self.tracker_list.addItems(list(state.trackers.keys()))
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor) self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
self.tracker_list.activated.connect(self.set_tracker) self.tracker_list.activated.connect(self.set_tracker)
self.corner_layout.addWidget(self.tracker_list) self.corner_layout.addWidget(self.tracker_list)
@@ -705,16 +675,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def _start_search(self): def _start_search(self):
self.searchbar.setEnabled(False) self.searchbar.setEnabled(False)
def _search_thread(): def _search_thread():
try: return_pressed(self)
return_pressed(self)
finally:
self.search_finished_signal.emit()
run_thread(threading.Thread(target=_search_thread)) run_thread(threading.Thread(target=_search_thread))
def _on_search_finished(self):
self.searchbar.setEnabled(True)
self.searchbar.setFocus()
@staticmethod @staticmethod
def add_log(text): def add_log(text):
if MainWindow._instance: if MainWindow._instance:
@@ -727,6 +690,28 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.consoleLog.verticalScrollBar().maximum() self.consoleLog.verticalScrollBar().maximum()
) )
def _on_search_results(self, headers):
if state.posts is None or state.posts == []:
self.show_empty_results(True)
return
state.trackertable.clearContents()
state.trackertable.setColumnCount(len(headers))
state.trackertable.setHorizontalHeaderLabels(headers)
state.trackertable.setRowCount(len(state.posts))
for x, rowdata in enumerate(state.posts):
for y, data in enumerate(rowdata.values()):
state.trackertable.setItem(x, y, QTableWidgetItem(str(data)))
self._resize_tracker_columns()
self.show_empty_results(False)
self.searchbar.setEnabled(True)
self.searchbar.setFocus()
state.trackertable.setItemDelegate(TrackerHoverDelegate(state.trackertable))
state.trackertable.viewport().installEventFilter(self)
def update_image_overlay(self, new_image_path): def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path) self.image = QImage(new_image_path)
self.pixmap = QPixmap.fromImage(self.image) self.pixmap = QPixmap.fromImage(self.image)
@@ -784,9 +769,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def mousePressEvent(self, event): def mousePressEvent(self, event):
self.rutrackerlist.clearSelection() state.trackertable.clearSelection()
self.uztrackerlist.clearSelection()
self.monkruslist.clearSelection()
self.downloadList.clearSelection() self.downloadList.clearSelection()
super().mousePressEvent(event) super().mousePressEvent(event)
@@ -801,21 +784,19 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
try: try:
for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist): if obj == state.trackertable.viewport():
if obj == tbl.viewport(): if event.type() == QEvent.Type.MouseMove:
if event.type() == QEvent.Type.MouseMove: pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos() idx = state.trackertable.indexAt(pos)
idx = tbl.indexAt(pos) new_row = idx.row() if idx.isValid() else -1
new_row = idx.row() if idx.isValid() else -1 if new_row != self._tracker_hovered_row or state.trackertable is not self._tracker_hovered_table:
if new_row != self._tracker_hovered_row or tbl is not self._tracker_hovered_table: self._tracker_hovered_row = new_row
self._tracker_hovered_row = new_row self._tracker_hovered_table = state.trackertable
self._tracker_hovered_table = tbl state.trackertable.viewport().update()
tbl.viewport().update() elif event.type() == QEvent.Type.Leave:
elif event.type() == QEvent.Type.Leave: self._tracker_hovered_row = -1
self._tracker_hovered_row = -1 self._tracker_hovered_table = None
self._tracker_hovered_table = None state.trackertable.viewport().update()
tbl.viewport().update()
break
if obj == self.downloadList.viewport(): if obj == self.downloadList.viewport():
if event.type() == QEvent.Type.MouseMove: if event.type() == QEvent.Type.MouseMove:
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos() pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
@@ -839,22 +820,14 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.downloadList.viewport().update() self.downloadList.viewport().update()
def set_tracker(self, _): def set_tracker(self, _):
old_tracker = state.tracker state.currenttracker = self.tracker_list.currentText()
state.tracker = self.tracker_list.currentText()
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)
def show_empty_results(self, show: bool): def show_empty_results(self, show: bool):
if show: if show:
state.tracker_list[state.tracker].hide() state.trackertable.hide()
self.emptyResults.show() self.emptyResults.show()
else: else:
state.tracker_list[state.tracker].show() state.trackertable.show()
self.emptyResults.hide() self.emptyResults.hide()
def show_empty_downloads(self): def show_empty_downloads(self):
@@ -865,11 +838,28 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.emptyDownload.show() self.emptyDownload.show()
self.downloadList.hide() self.downloadList.hide()
# thank you claude # thanks claude
def _resize_tracker_columns(self):
header = state.trackertable.horizontalHeader()
col_count = header.count()
if col_count == 0:
return
for i in range(col_count):
state.trackertable.resizeColumnToContents(i)
content_widths = [header.sectionSize(i) for i in range(col_count)]
total_content_width = sum(content_widths)
remaining = state.trackertable.viewport().width() - total_content_width
extra = max(0, remaining) // col_count
header.setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
for i in range(col_count):
header.resizeSection(i, content_widths[i] + extra)
def resizeEvent(self, event): def resizeEvent(self, event):
super().resizeEvent(event) super().resizeEvent(event)
table_width = state.tracker_list[state.tracker].viewport().width() self._resize_tracker_columns()
state.tracker_list[state.tracker].setColumnWidth(1, int(table_width * 0.3))
def contextMenuEvent(self, event: QContextMenuEvent): def contextMenuEvent(self, event: QContextMenuEvent):
if self.downloadList.underMouse(): if self.downloadList.underMouse():
+112
View File
@@ -0,0 +1,112 @@
from typing import Generator
import fuzzyfinder
from PySide6 import QtCore, QtWidgets, QtGui
from sys import exit
from copy import deepcopy
class FuzzySearchWindow(QtWidgets.QWidget): # should work everywhere
"""
The dict should contain a:
$(keytosortby) : str
THIS IS REQUIRED
all other things will be displayed alphabetically after it.
All elements should have the same Keys, eg when one element has "sources" : "buzzheavier" all other elements should as well have a key called "sources", though the value can be "MegaDB"
All elements are strings, e.g. "count" : "1"
The key will be at the Top of the Table
The value will be listed for each element
keys in ignorekeys will be ignored and not displayed, e.g. if oyu have links in the database which arent relevant
"""
def __init__(self, options: list[dict[str, str]], ignorekeys: list[str] = [], columnClicklShouldReturn = 1, keytosortby = "name"):
super().__init__()
self.opts: list[dict[str, str]] = options
self.filtered_opts = deepcopy(self.opts)
self.ignorekeys = ignorekeys
self.returncolumn = columnClicklShouldReturn
self.sortkey = keytosortby
self.searchbox = QtWidgets.QLineEdit()
self.searchbox.setPlaceholderText("Search for a game on Steamrip")
self.table = QtWidgets.QTableWidget()
self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # disable editing
self.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) # no selection highlight
self.table.setFocusPolicy(QtCore.Qt.NoFocus)
self.searchbox.setFocus()
headers = []
for key in self.opts[1].keys():
if key not in self.ignorekeys:
headers.append(key)
print(headers)
self.table.setColumnCount(len(headers))
self.table.setHorizontalHeaderLabels(headers)
self.table.setRowCount(len(self.opts))
for row_index, row_data in enumerate(self.opts):
for col_index, key in enumerate(headers):
if key in self.ignorekeys:
continue
value = row_data.get(key, "")
item = QtWidgets.QTableWidgetItem(value)
self.table.setItem(row_index, col_index, item)
self.table.resizeColumnsToContents()
self.searchbox.textChanged.connect(self.filter_table)
self.table.cellClicked.connect(self.cellClick)
layout = QtWidgets.QGridLayout(self)
layout.addWidget(self.searchbox, 0, 0)
layout.addWidget(self.table, 1, 0)
layout.setRowStretch(0, 0)
layout.setRowStretch(1, 1)
def filter_table(self):
searchquery = self.searchbox.text()
options: Generator = fuzzyfinder.main.fuzzyfinder(searchquery, self.opts, accessor=lambda x: x[self.sortkey])
self.filtered_opts = []
for row_index, row_data in enumerate(options):
self.filtered_opts.append(row_data)
for col_index, key in enumerate(row_data.keys()):
if key in self.ignorekeys:
continue
value = row_data.get(key, "")
self.table.setItem(
row_index,
col_index,
QtWidgets.QTableWidgetItem(value)
)
def cellClick(self, row: int, _: int):
item = self.table.item(row, self.returncolumn)
if item is not None:
value = item.text()
print(value)
if __name__ == "__main__":
import core.data.scrapers.steamrip as sr
app = QtWidgets.QApplication([])
widget = FuzzySearchWindow(sr.scrape_steamrip_links())
widget.resize(600,400)
widget.show()
exit(app.exec())
+17 -54
View File
@@ -1,66 +1,29 @@
from PySide6.QtWidgets import QTableWidgetItem from PySide6.QtWidgets import QTableWidgetItem, QHeaderView
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.data.scrapers.uztracker import scrape_uztracker from core.data.scrapers.rutracker import init_rutracker
from core.data.scrapers.rutracker import scrape_rutracker from core.data.scrapers.uztracker import init_uztracker
from core.data.scrapers.monkrus import scrape_m0nkrus from core.data.scrapers.monkrus import init_m0nkrus
from core.utils.network.jsonhandler import split_data, format_data, format_data_minimal from core.data.scrapers.steamrip import init_steamrip
from core.utils.data.state import state from core.utils.data.state import state
scrapers = { init_rutracker()
"uztracker": scrape_uztracker, init_uztracker()
"rutracker": scrape_rutracker, init_m0nkrus()
"m0nkrus": scrape_m0nkrus init_steamrip()
}
def return_pressed(self): def return_pressed(self):
self.show_empty_results(False)
search_text = self.searchbar.text() search_text = self.searchbar.text()
if search_text == "": if search_text == "":
consoleLog("Error: Can't search for nothing") consoleLog("Error: Can't search for nothing")
return return
consoleLog(f"User searched for: {search_text}") consoleLog(f"User searched for: {search_text}")
if state.tracker == "rutracker": tracker = state.trackers[state.currenttracker]
response = scrape_rutracker(search_text)
if response:
_, state.posts, _, _, cached = split_data(response)
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, state.post_seeders, state.post_leechers = format_data(state.posts)
self.show_empty_results(False)
state.tracker_list[state.tracker].clear()
state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
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))
for i, seeders in enumerate(state.post_seeders):
state.tracker_list[state.tracker].setItem(i, 2, QTableWidgetItem(seeders))
for i, leechers in enumerate(state.post_leechers):
state.tracker_list[state.tracker].setItem(i, 3, QTableWidgetItem(leechers))
if state.debug == True:
consoleLog(f"Response Cached: {cached}")
else:
consoleLog(f"No response from rutracker")
state.tracker_list[state.tracker].clear()
self.show_empty_results(True)
elif state.tracker is not None: scrapefunc: function = tracker["scrapeFunc"]
state.posts = scrapers[state.tracker](search_text)
if not state.posts: state.posts = scrapefunc(search_text)
consoleLog(f"No Results for {search_text}")
state.tracker_list[state.tracker].clear() from core.interface.gui import MainWindow
self.show_empty_results(True) MainWindow._instance.search_results_signal.emit(tracker["headers"])
else:
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"])
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))
+15 -10
View File
@@ -1,6 +1,6 @@
from PySide6.QtCore import QObject, Signal from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QTableWidget from PySide6.QtWidgets import QTableWidget
from typing import Optional, Any, List, Dict from typing import Any, List, Dict
from pathlib import Path from pathlib import Path
class AppState(QObject): class AppState(QObject):
@@ -8,20 +8,26 @@ class AppState(QObject):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.posts: list[Any] | None = None self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seederm leecher
self.post_titles: Optional[List] = None
self.post_urls: Optional[List] = None
self.post_author: List[str] = []
self.post_seeders: List[str] = []
self.post_leechers: List[str] = []
self.version: str = "dev" self.version: str = "dev"
self._image_path: str = "" self._image_path: str = ""
self.ignore_updates: bool = False self.ignore_updates: bool = False
self.debug: bool = False self.debug: bool = False
self.autoresume: bool = True self.autoresume: bool = True
self.tracker: str = "rutracker"
self.tracker_list: dict[str, QTableWidget] = {} self.currenttracker: str = "rutracker"
self.trackertable: QTableWidget
self.trackers: Dict[str,Dict[str,Any]] = {}# each tracker should add itself here
'''
an example:
"rutracker" : {
"name" : "rutracker", # name of the tracker
"headers" : ["author", "title"], # keys shown in the table
"scrapeFunc" : function,
}
'''
self.api_url: str = "https://api.michijackson.xyz" self.api_url: str = "https://api.michijackson.xyz"
self.seeded_magnets: set = set()
self.download_path: str = str(Path.home() / "Downloads") self.download_path: str = str(Path.home() / "Downloads")
self.up_speed_limit: int = 0 self.up_speed_limit: int = 0
self.down_speed_limit: int = 0 self.down_speed_limit: int = 0
@@ -30,7 +36,6 @@ 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 = []
-25
View File
@@ -1,31 +1,6 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.network.jsonhandler import format_data
def get_item_url(item, posts, post_titles): # softwarelist currentitem, post list (dict), post titles list
post_index = post_titles.index(item)
if 0 <= post_index < len(post_titles):
if state.tracker == "uztracker":
item = "https://uztracker.net/" + state.post_urls[post_index].lstrip("./")
consoleLog(f"Found post URL: {item}")
return item
if state.tracker == "rutracker":
item_dict = posts[post_index]
_, post_links, _, _, _ = format_data([item_dict])
consoleLog(f"Found post URL: {post_links[0]}")
return post_links[0]
if state.tracker == "m0nkrus":
item_dict = posts[post_index]
_, post_links, _, _, _,= format_data([item_dict])
consoleLog(f"Found post URL: {post_links[0]}")
return post_links[0]
return None
def get_magnet_link(post_url): def get_magnet_link(post_url):
+51 -14
View File
@@ -1,14 +1,18 @@
from PySide6.QtWidgets import QTableWidgetItem
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.network.libtorrent_wrapper import add_magnet
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.general.wrappers import run_thread
from core.utils.logging.logs import add_download_log from core.utils.logging.logs import add_download_log
from core.network.libtorrent_int import add_seed from core.network.libtorrent_int import add_seed
from core.utils.data.state import state
from urllib.parse import urlparse, unquote
import threading import threading
import re
import requests
import random
def download_selected(items, posts, post_titles): def download_selected(items: list[QTableWidgetItem]):
if not items: if not items:
consoleLog("No item selected for download.") consoleLog("No item selected for download.")
return return
@@ -17,24 +21,57 @@ def download_selected(items, posts, post_titles):
if item.column() != 0: if item.column() != 0:
continue continue
text = item.text() text = item.text()
if text and text not in seen: if text != "" and text not in seen:
seen.add(text) seen.add(text)
consoleLog(f"Downloading {text}") consoleLog(f"Downloading {text}")
run_thread(threading.Thread(target=run_download, args=(text, posts, post_titles))) run_thread(threading.Thread(target=run_download, args=(state.posts[item.row()],)))
def run_download(item, posts, post_titles): def get_direct_filename(url: str, headers) -> str:
post_url = get_item_url(item, posts, post_titles) cd = headers.get('content-disposition', '')
consoleLog(f"Selected URL: {post_url}") if cd:
magnet_uri = get_magnet_link(post_url) match = re.search(r'filename\*=UTF-8\'\'(.+)', cd) # RFC 5987 encoded
if match:
return unquote(match.group(1))
match = re.search(r'filename="?([^";\n]+)"?', cd)
if match:
return match.group(1).strip()
if add_download(magnet_uri): path = urlparse(url).path
add_download_log(item, post_url, magnet_uri, False) name = path.split('/')[-1]
if name:
return unquote(name)
return f"download{random.randint(1,9999)}" #avoid collision
def filedownload(url):
try:
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
name = get_direct_filename(url, r.headers)
with open(state.download_path + f"/{name}", 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
consoleLog(f"Finished downloading {name}")
except Exception as e:
consoleLog(f"Error downloading: {e}")
def run_download(post):
linkfunc = state.trackers[state.currenttracker]["linkFunc"]
ismagnet = state.trackers[state.currenttracker]["isMagnet"]
link = linkfunc(post)
if ismagnet:
add_magnet(link)
else:
filedownload(link)
def run_download_direct(magnet_uri): def run_download_direct(magnet_uri):
consoleLog(f"Direct download: {magnet_uri[:60]}") consoleLog(f"Direct download: {magnet_uri[:60]}")
add_download(magnet_uri) add_magnet(magnet_uri)
add_download_log("Direct Download", "", magnet_uri, False) add_download_log("Direct Download", "", magnet_uri, False)
def seed_magnet(magnet_uri, file_path): def seed_magnet(magnet_uri, file_path):
consoleLog(f"Seeding: {magnet_uri[:60]}") consoleLog(f"Seeding: {magnet_uri[:60]}")
add_seed(magnet_uri, file_path) add_seed(magnet_uri, file_path)