diff --git a/.gitignore b/.gitignore index 9bd9aa6..6f64177 100644 --- a/.gitignore +++ b/.gitignore @@ -1,220 +1,3 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$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 \ No newline at end of file +# Created by venv; see https://docs.python.org/3/library/venv.html +__pycache__/ +*.exe diff --git a/requirements.txt b/requirements.txt index 0fecfdd..dec79d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ plyer==2.1.0 psutil==7.1.0 libtorrent==2.0.11 libtorrent-windows-dll==0.0.3 +fuzzyfinder==2.3.0 diff --git a/src/core/data/scrapers/monkrus.py b/src/core/data/scrapers/monkrus.py index 67e64f3..e48a5df 100644 --- a/src/core/data/scrapers/monkrus.py +++ b/src/core/data/scrapers/monkrus.py @@ -1,6 +1,10 @@ from bs4 import BeautifulSoup import requests 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 = { "data": [], @@ -31,7 +35,7 @@ def _get_telegram_posts(): for bubble in bubbles: 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 title = post_txt.b.text @@ -42,9 +46,9 @@ def _get_telegram_posts(): if post_url not in added: added.add(post_url) posts.append(dict( + title=title, author="m0nkrus", id=len(posts) + 1, - title=title, url=post_url )) @@ -67,4 +71,19 @@ def scrape_m0nkrus(query): filtered_post["id"] = len(filtered_posts) + 1 filtered_posts.append(filtered_post) - return filtered_posts \ No newline at end of file + 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}) diff --git a/src/core/data/scrapers/provider/buzzheavier.py b/src/core/data/scrapers/provider/buzzheavier.py new file mode 100644 index 0000000..b7d36bd --- /dev/null +++ b/src/core/data/scrapers/provider/buzzheavier.py @@ -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) \ No newline at end of file diff --git a/src/core/data/scrapers/provider/fichier.py b/src/core/data/scrapers/provider/fichier.py new file mode 100644 index 0000000..7a54af5 --- /dev/null +++ b/src/core/data/scrapers/provider/fichier.py @@ -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")) + + diff --git a/src/core/data/scrapers/provider/gofile.py b/src/core/data/scrapers/provider/gofile.py new file mode 100644 index 0000000..42574af --- /dev/null +++ b/src/core/data/scrapers/provider/gofile.py @@ -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.") + diff --git a/src/core/data/scrapers/provider/megadb.py b/src/core/data/scrapers/provider/megadb.py new file mode 100644 index 0000000..4e9c6de --- /dev/null +++ b/src/core/data/scrapers/provider/megadb.py @@ -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() diff --git a/src/core/data/scrapers/provider/vikingfile.py b/src/core/data/scrapers/provider/vikingfile.py new file mode 100644 index 0000000..1a3145a --- /dev/null +++ b/src/core/data/scrapers/provider/vikingfile.py @@ -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) \ No newline at end of file diff --git a/src/core/data/scrapers/rutracker.py b/src/core/data/scrapers/rutracker.py index bf67bb5..ab235ef 100644 --- a/src/core/data/scrapers/rutracker.py +++ b/src/core/data/scrapers/rutracker.py @@ -1,19 +1,48 @@ import requests from core.utils.data.state import state 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): 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: - try: - return search.text - except Exception: - consoleLog("No results found / No response from server") - return None - else: - return None + _, data, _, _, _ = split_data(search.text) + 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}) diff --git a/src/core/data/scrapers/steamrip.py b/src/core/data/scrapers/steamrip.py new file mode 100644 index 0000000..65c29dc --- /dev/null +++ b/src/core/data/scrapers/steamrip.py @@ -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/")) diff --git a/src/core/data/scrapers/uztracker.py b/src/core/data/scrapers/uztracker.py index ac2aa23..f6395ef 100644 --- a/src/core/data/scrapers/uztracker.py +++ b/src/core/data/scrapers/uztracker.py @@ -2,6 +2,10 @@ import requests from bs4 import BeautifulSoup from urllib.parse import urljoin 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): base_url="https://uztracker.net/" @@ -26,12 +30,27 @@ def scrape_uztracker(query): posts.append(dict( title=title, + author=author, url=url, - author=author )) return posts except requests.RequestException as e: consoleLog(f"Failed to fetch {search_url}: {e}") - return None \ No newline at end of file + 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}) diff --git a/src/core/interface/gui.py b/src/core/interface/gui.py index 539f9af..6b7f127 100644 --- a/src/core/interface/gui.py +++ b/src/core/interface/gui.py @@ -13,6 +13,7 @@ from PySide6.QtWidgets import ( QHeaderView, QMessageBox, QTableWidget, + QTableWidgetItem, QTextEdit, QStyledItemDelegate, QStatusBar @@ -189,15 +190,24 @@ def download_update(latest_version): def windowCloseHelper(): 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): 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): super().__init__() MainWindow._instance = self 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() image_data = base64.b64decode(logo_base64) @@ -273,68 +283,30 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): 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, _theme_colors()["hover"]) - painter.restore() - super().paint(painter, option, index) + # create initial tracker table + state.trackertable = QTableWidget() + state.trackertable.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + state.trackertable.verticalHeader().setVisible(False) + state.trackertable.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + state.trackertable.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + 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() - 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(_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) + state.trackertable.setItemDelegate(TrackerHoverDelegate(state.trackertable)) + state.trackertable.viewport().installEventFilter(self) + state.trackertable.setMouseTracking(True) + state.trackertable.viewport().setMouseTracking(True) container = QWidget() containerLayout = QVBoxLayout() - + containerLayout.addWidget(self.searchbar) + containerLayout.addWidget(state.trackertable) search_row = QHBoxLayout() search_row.addWidget(self.searchbar) - containerLayout.addLayout(search_row) - containerLayout.addWidget(state.tracker_list[state.tracker]) class DownloadModel(QAbstractTableModel): def __init__(self): @@ -568,8 +540,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): 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].selectedItems(), state.posts, state.post_titles)))) + self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.trackertable.selectedItems(),)))) container.setLayout(containerLayout) self.setCentralWidget(container) @@ -589,10 +560,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.horizontal_layout = QHBoxLayout() self.horizontal_layout.addWidget(self.emptyResults, stretch=3) - self.tracker_widget = state.tracker_list[state.tracker] - self.horizontal_layout.addWidget(self.tracker_widget) + self.horizontal_layout.addWidget(state.trackertable) - 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.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.addItems(["rutracker", "uztracker", "m0nkrus"]) + self.tracker_list.addItems(list(state.trackers.keys())) self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor) self.tracker_list.activated.connect(self.set_tracker) self.corner_layout.addWidget(self.tracker_list) @@ -705,16 +675,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): def _start_search(self): self.searchbar.setEnabled(False) def _search_thread(): - try: - return_pressed(self) - finally: - self.search_finished_signal.emit() + return_pressed(self) run_thread(threading.Thread(target=_search_thread)) - def _on_search_finished(self): - self.searchbar.setEnabled(True) - self.searchbar.setFocus() - @staticmethod def add_log(text): if MainWindow._instance: @@ -727,6 +690,28 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): 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): self.image = QImage(new_image_path) self.pixmap = QPixmap.fromImage(self.image) @@ -784,9 +769,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): def mousePressEvent(self, event): - self.rutrackerlist.clearSelection() - self.uztrackerlist.clearSelection() - self.monkruslist.clearSelection() + state.trackertable.clearSelection() self.downloadList.clearSelection() super().mousePressEvent(event) @@ -801,21 +784,19 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): 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 == state.trackertable.viewport(): + if event.type() == QEvent.Type.MouseMove: + pos = event.position().toPoint() if hasattr(event, 'position') else event.pos() + idx = state.trackertable.indexAt(pos) + 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: + self._tracker_hovered_row = new_row + self._tracker_hovered_table = state.trackertable + state.trackertable.viewport().update() + elif event.type() == QEvent.Type.Leave: + self._tracker_hovered_row = -1 + self._tracker_hovered_table = None + state.trackertable.viewport().update() if obj == self.downloadList.viewport(): if event.type() == QEvent.Type.MouseMove: pos = event.position().toPoint() if hasattr(event, 'position') else event.pos() @@ -839,22 +820,14 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.downloadList.viewport().update() def set_tracker(self, _): - old_tracker = state.tracker - 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) + state.currenttracker = self.tracker_list.currentText() def show_empty_results(self, show: bool): if show: - state.tracker_list[state.tracker].hide() + state.trackertable.hide() self.emptyResults.show() else: - state.tracker_list[state.tracker].show() + state.trackertable.show() self.emptyResults.hide() def show_empty_downloads(self): @@ -865,11 +838,28 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.emptyDownload.show() 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): super().resizeEvent(event) - table_width = state.tracker_list[state.tracker].viewport().width() - state.tracker_list[state.tracker].setColumnWidth(1, int(table_width * 0.3)) + self._resize_tracker_columns() def contextMenuEvent(self, event: QContextMenuEvent): if self.downloadList.underMouse(): diff --git a/src/core/interface/utils/fzhelper.py b/src/core/interface/utils/fzhelper.py new file mode 100644 index 0000000..9aba5d8 --- /dev/null +++ b/src/core/interface/utils/fzhelper.py @@ -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()) diff --git a/src/core/interface/utils/searchhelper.py b/src/core/interface/utils/searchhelper.py index eda868a..967872e 100644 --- a/src/core/interface/utils/searchhelper.py +++ b/src/core/interface/utils/searchhelper.py @@ -1,66 +1,29 @@ -from PySide6.QtWidgets import QTableWidgetItem +from PySide6.QtWidgets import QTableWidgetItem, QHeaderView 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_m0nkrus -from core.utils.network.jsonhandler import split_data, format_data, format_data_minimal +from core.data.scrapers.rutracker import init_rutracker +from core.data.scrapers.uztracker import init_uztracker +from core.data.scrapers.monkrus import init_m0nkrus +from core.data.scrapers.steamrip import init_steamrip from core.utils.data.state import state -scrapers = { - "uztracker": scrape_uztracker, - "rutracker": scrape_rutracker, - "m0nkrus": scrape_m0nkrus -} +init_rutracker() +init_uztracker() +init_m0nkrus() +init_steamrip() def return_pressed(self): + self.show_empty_results(False) search_text = self.searchbar.text() if search_text == "": consoleLog("Error: Can't search for nothing") return consoleLog(f"User searched for: {search_text}") - if state.tracker == "rutracker": - 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) + tracker = state.trackers[state.currenttracker] - elif state.tracker is not None: - state.posts = scrapers[state.tracker](search_text) - if not state.posts: - consoleLog(f"No Results for {search_text}") - 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) - 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)) + scrapefunc: function = tracker["scrapeFunc"] + + state.posts = scrapefunc(search_text) + + from core.interface.gui import MainWindow + MainWindow._instance.search_results_signal.emit(tracker["headers"]) diff --git a/src/core/utils/data/state.py b/src/core/utils/data/state.py index 1e33a70..6d135c8 100644 --- a/src/core/utils/data/state.py +++ b/src/core/utils/data/state.py @@ -1,6 +1,6 @@ from PySide6.QtCore import QObject, Signal from PySide6.QtWidgets import QTableWidget -from typing import Optional, Any, List, Dict +from typing import Any, List, Dict from pathlib import Path class AppState(QObject): @@ -8,20 +8,26 @@ class AppState(QObject): def __init__(self): super().__init__() - self.posts: list[Any] | None = None - 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.posts: list[Dict[str,str]] | None = None # titles, urls, author, seederm leecher self.version: str = "dev" self._image_path: str = "" self.ignore_updates: bool = False self.debug: bool = False 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.seeded_magnets: set = set() self.download_path: str = str(Path.home() / "Downloads") self.up_speed_limit: int = 0 self.down_speed_limit: int = 0 @@ -30,7 +36,6 @@ 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 = [] diff --git a/src/core/utils/data/tracker.py b/src/core/utils/data/tracker.py index 79ed98f..f70756b 100644 --- a/src/core/utils/data/tracker.py +++ b/src/core/utils/data/tracker.py @@ -1,31 +1,6 @@ import requests from bs4 import BeautifulSoup -from core.utils.data.state import state 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): diff --git a/src/core/utils/network/download.py b/src/core/utils/network/download.py index 69082b0..fcba5a2 100644 --- a/src/core/utils/network/download.py +++ b/src/core/utils/network/download.py @@ -1,14 +1,18 @@ +from PySide6.QtWidgets import QTableWidgetItem 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.network.libtorrent_wrapper import add_magnet from core.utils.general.wrappers import run_thread from core.utils.logging.logs import add_download_log from core.network.libtorrent_int import add_seed +from core.utils.data.state import state +from urllib.parse import urlparse, unquote import threading +import re +import requests +import random -def download_selected(items, posts, post_titles): +def download_selected(items: list[QTableWidgetItem]): if not items: consoleLog("No item selected for download.") return @@ -17,24 +21,57 @@ def download_selected(items, posts, post_titles): if item.column() != 0: continue text = item.text() - if text and text not in seen: + 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))) + run_thread(threading.Thread(target=run_download, args=(state.posts[item.row()],))) -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) +def get_direct_filename(url: str, headers) -> str: + cd = headers.get('content-disposition', '') + if cd: + 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): - add_download_log(item, post_url, magnet_uri, False) + path = urlparse(url).path + 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): consoleLog(f"Direct download: {magnet_uri[:60]}") - add_download(magnet_uri) + add_magnet(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) \ No newline at end of file + add_seed(magnet_uri, file_path)