From 404b6d55e4c1d9eb5a40dc4908ad79b2e9275911 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 18:37:09 +0200
Subject: [PATCH 01/15] basic Linux support (fix notifications and config)
---
main.pyw | 6 ++++--
util/gui.py | 44 +++++++++++++++++++++++++++++++-------------
2 files changed, 35 insertions(+), 15 deletions(-)
diff --git a/main.pyw b/main.pyw
index ce9f60f..c79aaed 100644
--- a/main.pyw
+++ b/main.pyw
@@ -4,6 +4,7 @@
from util import gui
from PyQt5 import QtWidgets
+from PyQt5.QtCore import QStandardPaths
import sys
import configparser
@@ -48,8 +49,9 @@ def main() -> None:
ui.setupUi(MainWindow)
MainWindow.show()
- config_dir = os.path.join(os.environ["APPDATA"], "NyaaDownloader")
- config_path = os.path.join(config_dir, "config.ini")
+ config_filename = "config.ini"
+ config_dir = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ config_path = os.path.join(config_dir, config_filename)
config = configparser.ConfigParser()
config.read(config_path)
diff --git a/util/gui.py b/util/gui.py
index beb519a..830894a 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -5,21 +5,22 @@
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMessageBox, QDialog, QInputDialog, QLineEdit
-from PyQt5.QtCore import Qt, QThread, pyqtSignal
-from winotify import Notification, audio
+from PyQt5.QtCore import Qt, QThread, QStandardPaths, pyqtSignal
from shutil import move
+import platform
import configparser
import textwrap
import os
import webbrowser as wb
+import urllib
# ------------------------------GLOBAL VARIABLES------------------------------
unhandled_characters = ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]
-ICON_PATH = "ico\\nyaa.ico"
+ICON_PATH = os.path.join("ico", "nyaa.ico")
# ------------------------------CLASSES AND METHODS------------------------------
@@ -31,9 +32,10 @@ def update_config(key: str, value: str) -> None:
key (str): Key to update
value (str): Value to set for the key
"""
- config_dir = os.path.join(os.environ["APPDATA"], "NyaaDownloader")
+ config_filename = "config.ini"
+ config_dir = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
os.makedirs(config_dir, exist_ok=True)
- config_path = os.path.join(config_dir, "config.ini")
+ config_path = os.path.join(config_dir, config_filename)
config = configparser.ConfigParser()
config.read(config_path)
@@ -48,7 +50,7 @@ def update_config(key: str, value: str) -> None:
# Generated with Qt Designer (first time using this one)
class Ui_MainWindow(QDialog):
- def setupUi(self, MainWindow) -> None:
+ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
"""Build skeleton of the GUI
Args:
@@ -398,13 +400,29 @@ def notify(self, message: str) -> None:
message (str): Message to be displayed
"""
- toast = Notification(
- app_id="NyaaDownloader",
- title="NyaaDownloader",
- msg=message,
- )
- toast.set_audio(audio.Default, loop=False)
- toast.build().show()
+ system = platform.system()
+ if system == "Linux":
+ import gi
+ gi.require_version("Notify", "0.7")
+ from gi.repository import Notify
+ Notify.init("NyaaDownloader")
+ Notify.Notification.new("NyaaDownloader", message).show()
+ elif system == "Windows":
+ from winotify import Notification, audio
+ toast = Notification(
+ app_id="NyaaDownloader",
+ title="NyaaDownloader",
+ msg=message,
+ )
+ toast.set_audio(audio.Default, loop=False)
+ toast.build().show()
+ elif system == "Darwin":
+ from Foundation import NSUserNotification, NSUserNotificationCenter
+ notification = NSUserNotification.alloc().init()
+ notification.setTitle("NyaaDownloader")
+ notification.setInformativeText(message)
+ NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(notification)
+
def save_logs(self) -> None:
"""Saves the logs to a .txt file."""
From 01d121aea82391ec9e252adb406c0d0ba76823a1 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 18:39:02 +0200
Subject: [PATCH 02/15] fix for current NyaaPy
---
util/gui.py | 6 +++---
util/nyaa.py | 33 +++++++++++++++++----------------
2 files changed, 20 insertions(+), 19 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index 830894a..67ca634 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -566,7 +566,7 @@ def run(self) -> None:
)
else:
- self.error_popup.emit("No Internet connection available")
+ self.error_popup.emit(f"Failed downloading: {torrent.name}")
unexpected_end = True
break
@@ -581,7 +581,7 @@ def run(self) -> None:
self.update_logs.emit(f"Found: {anime_name} - Episode {episode}")
# Erai-raws add "END" in the torrent name when an anime has finished airing
- if uploader == "Erai-raws" and torrent["name"].find(" END [") != -1:
+ if uploader == "Erai-raws" and torrent.name.find(" END [") != -1:
self.update_logs.emit(
f"Note: {anime_name} has no more than {episode} episodes"
)
@@ -593,7 +593,7 @@ def run(self) -> None:
else:
if uploader == uploaders[-1]:
self.update_logs.emit(
- f"Failed: {anime_name} - Episode {episode}"
+ f"Failed finding: {anime_name} - Episode {episode}"
)
fails_in_a_row += 1
diff --git a/util/nyaa.py b/util/nyaa.py
index 3f8e520..d92c387 100644
--- a/util/nyaa.py
+++ b/util/nyaa.py
@@ -1,7 +1,7 @@
# ------------------------------IMPORTS------------------------------
-import NyaaPy
+import nyaapy.nyaasi.nyaa as NyaaPy
import requests
import webbrowser as wb
@@ -15,7 +15,7 @@ def is_in_database(anime_name: str) -> bool:
anime_name (str): Name of the anime to check.
Raises:
- Exception: If the anime is not found in the database, the only possible reason is that user has no Internet connection.
+ Exception: If the underlying module throws one.
Returns:
bool: True if check was successful, False otherwise.
@@ -30,8 +30,8 @@ def is_in_database(anime_name: str) -> bool:
== 0
):
return False
- except:
- raise Exception("No Internet connection available.")
+ except Exception as e:
+ raise e
return True
@@ -45,8 +45,8 @@ def download(torrent: dict) -> bool:
bool: True if the transfer was successful, False otherwise.
"""
try:
- with requests.get(torrent["download_url"]) as response, open(
- torrent["name"] + ".torrent", "wb"
+ with requests.get(torrent.download_url) as response, open(
+ torrent.name + ".torrent", "wb"
) as out_file:
out_file.write(response.content)
@@ -65,7 +65,7 @@ def transfer(torrent: dict) -> bool:
bool: True if the transfer was successful, False otherwise.
"""
try:
- wb.open(torrent["magnet"])
+ wb.open(torrent.magnet)
except:
return False
@@ -102,14 +102,17 @@ def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, unt
subcategory=2,
filters=0 if untrusted_option else 2,
)
+
+ if not isinstance(found_torrents, list) or len(found_torrents) == 0:
+ return {}
try:
# We take the very closest title to what we are looking for.
torrent = None
for t in found_torrents: # (break if found, so we get the most recent one)
if (
- t["name"].lower().find(f"{anime_name} - {episode}".lower()) != -1
- and t["name"].lower().find("~") == -1
+ t.name.lower().find(f"{anime_name} - {episode}".lower()) != -1
+ and t.name.lower().find("~") == -1
): # we want to avoid ~ because Erai-Raws use it for already packed episodes
torrent = t
break
@@ -118,16 +121,14 @@ def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, unt
if torrent is None:
for t in found_torrents:
if (
- t["name"].lower().find(f"{anime_name}".lower()) != -1
- and t["name"].lower().find(f" {episode} ") != -1
- and t["name"].lower().find("~") == -1
+ t.name.lower().find(f"{anime_name}".lower()) != -1
+ and t.name.lower().find(f" {episode} ") != -1
+ and t.name.lower().find("~") == -1
): # we want to avoid ~ because Erai-Raws use it for already packed episodes
torrent = t
break
- # The only exception possible is that no torrent have been found when NyaaPy.Nyaa.search()
- # (we are doing dict operations on a None object => raise an exception)
- except:
- return {}
+ except Exception as e:
+ raise e
return torrent
From 74f6662410c65b38c22d69645f87f515a0c03b1f Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 18:48:52 +0200
Subject: [PATCH 03/15] fix saving .torrent files
(btw can't use signal to create a folder and use move immediately, this creates a race condition, on my system the folder would simply always not exist, maybe on Windows trying to move a file is so slow that a directory would have existed by that time LOL)
---
util/gui.py | 42 +++++++++++++++++++++++++-----------------
1 file changed, 25 insertions(+), 17 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index 67ca634..d6969d9 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -369,18 +369,25 @@ def set_widget_after_check(self) -> None:
self.pushButton_4.setVisible(False) # Disabling Stop button
self.pushButton_3.setEnabled(True) # Enabling Save logs button
- def generate_download_folder(self, anime_name: str) -> None:
+ @staticmethod
+ def get_download_folder(anime_name: str) -> str:
+ anime_name = " ".join(anime_name.strip().split())
+
+ for s in unhandled_characters:
+ anime_name = anime_name.replace(s, "")
+
+ downloads = QStandardPaths.writableLocation(QStandardPaths.DownloadLocation)
+ return os.path.join(downloads, "DownloadedTorrents", anime_name)
+
+ @classmethod
+ def generate_download_folder(cls, anime_name: str) -> None:
"""Generates a folder name for the .torrents download.
Args:
anime_name (str): Name of the anime.
"""
-
- for s in unhandled_characters:
- anime_name = anime_name.replace(s, "")
-
try:
- os.makedirs(f"DownloadedTorrents\\{anime_name}")
+ os.makedirs(cls.get_download_folder(anime_name), exist_ok=True)
except FileExistsError:
pass
@@ -388,7 +395,14 @@ def generate_download_folder(self, anime_name: str) -> None:
def open_download_folder(self) -> None:
"""Open the DownloadedTorrents folder"""
try:
- os.startfile(f"{os.getcwd()}\DownloadedTorrents")
+ global anime_name
+ path = None
+ if anime_name is not None and len(anime_name) > 0:
+ path = self.get_download_folder(anime_name)
+ if path is None or not os.path.exists(path):
+ path = self.get_download_folder("")
+ if not QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(path)):
+ self.show_error_popup("Failed opening: " + path)
except Exception as e:
self.show_error_popup("DownloadedTorrents folder not found because: " + str(e))
@@ -485,11 +499,7 @@ def is_everything_good(self) -> None:
option = 1 if self.radioButton.isChecked() else 2
untrusted_option = True if self.radioButton_2.isChecked() else False
- folder_name = anime_name
- for c in unhandled_characters:
- folder_name = folder_name.replace(c, "")
-
- path = f"DownloadedTorrents\\{folder_name}"
+ path = self.get_download_folder(anime_name)
self.start_checking()
else:
@@ -508,7 +518,6 @@ def start_checking(self) -> None:
self.worker.finished.connect(lambda: self.worker_finished())
self.worker.update_logs.connect(self.append_to_logs)
self.worker.error_popup.connect(self.show_error_popup)
- self.worker.gen_folder.connect(self.generate_download_folder)
def worker_finished(self) -> None:
"""When the thread has finished processing, enable all widgets again and notify the user
@@ -536,7 +545,6 @@ class WorkerThread(QThread):
update_logs = pyqtSignal(str)
error_popup = pyqtSignal(str)
- gen_folder = pyqtSignal(str)
def run(self) -> None:
"""The "almost main" function of that program. Will download/transfer every found torrent. Will also handle logs update, etc."""
@@ -559,10 +567,10 @@ def run(self) -> None:
if option == 1:
if nyaa.download(torrent):
- self.gen_folder.emit(anime_name)
+ Ui_MainWindow.generate_download_folder(anime_name)
move(
- f"{torrent['name']}.torrent",
- f"{path}\\{torrent['name']}.torrent",
+ f"{torrent.name}.torrent",
+ os.path.join(path, f"{torrent.name}.torrent"),
)
else:
From f3370783834c77805de3b3713bb6782a246dcf69 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 18:49:00 +0200
Subject: [PATCH 04/15] cosmetics
---
main.pyw | 2 +-
util/gui.py | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/main.pyw b/main.pyw
index c79aaed..465e6ef 100644
--- a/main.pyw
+++ b/main.pyw
@@ -40,7 +40,7 @@ def main() -> None:
Only depends if the uploaders uploaded them episode by episode or not.
If you find any bug, please let me know on my GitHub:
- https://github.com/marcpinet.
"""
+ https://github.com/marcpinet/nyaadownloader."""
app = QtWidgets.QApplication([])
app.setApplicationName("NyaaDownloader")
diff --git a/util/gui.py b/util/gui.py
index d6969d9..2e96178 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -273,9 +273,10 @@ def retranslateUi(self, MainWindow) -> None:
def ask_anime_to_translate(self) -> None:
"""Asking anime title to translate by opening a link to MyAnimeList"""
text, okPressed = QInputDialog.getText(
- self, "Title Translator", "Anime Title:", QLineEdit.Normal, ""
+ self, "Search in MyAnimeList", "Anime title to find in MyAnimeList:", QLineEdit.Normal
)
if okPressed and text != "":
+ text = urllib.quote(text)
wb.open(f"https://myanimelist.net/anime.php?q={text}&cat=anime")
def cancel_process(self) -> None:
@@ -408,7 +409,7 @@ def open_download_folder(self) -> None:
self.show_error_popup("DownloadedTorrents folder not found because: " + str(e))
def notify(self, message: str) -> None:
- """Generate a windows 10 notifcation with a message
+ """Generate a notifcation with a message
Args:
message (str): Message to be displayed
From fe0e22d8b5bb46e8dad823d743675a1a9d77203f Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 18:58:24 +0200
Subject: [PATCH 05/15] update to Qt6 cause we're in 2025 (also that makes icon
setting work wow)
---
main.pyw | 8 ++++----
util/gui.py | 26 +++++++++++++-------------
2 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/main.pyw b/main.pyw
index 465e6ef..da29acc 100644
--- a/main.pyw
+++ b/main.pyw
@@ -3,8 +3,8 @@
from util import gui
-from PyQt5 import QtWidgets
-from PyQt5.QtCore import QStandardPaths
+from PyQt6 import QtWidgets
+from PyQt6.QtCore import QStandardPaths
import sys
import configparser
@@ -50,7 +50,7 @@ def main() -> None:
MainWindow.show()
config_filename = "config.ini"
- config_dir = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ config_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
config_path = os.path.join(config_dir, config_filename)
config = configparser.ConfigParser()
@@ -59,7 +59,7 @@ def main() -> None:
if not config.has_option("Settings", "ShowPopup") or config.getboolean("Settings", "ShowPopup"):
gui.Ui_MainWindow.show_info_popup(MainWindow, message, never_show_again=True)
- sys.exit(app.exec_())
+ sys.exit(app.exec())
# ------------------------------MAIN CALL------------------------------
diff --git a/util/gui.py b/util/gui.py
index 2e96178..d702d96 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -3,9 +3,9 @@
from . import nyaa
-from PyQt5 import QtCore, QtGui, QtWidgets
-from PyQt5.QtWidgets import QMessageBox, QDialog, QInputDialog, QLineEdit
-from PyQt5.QtCore import Qt, QThread, QStandardPaths, pyqtSignal
+from PyQt6 import QtCore, QtGui, QtWidgets
+from PyQt6.QtWidgets import QMessageBox, QDialog, QInputDialog, QLineEdit
+from PyQt6.QtCore import Qt, QThread, QStandardPaths, pyqtSignal
from shutil import move
import platform
@@ -33,7 +33,7 @@ def update_config(key: str, value: str) -> None:
value (str): Value to set for the key
"""
config_filename = "config.ini"
- config_dir = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ config_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
os.makedirs(config_dir, exist_ok=True)
config_path = os.path.join(config_dir, config_filename)
@@ -59,9 +59,9 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
MainWindow.setObjectName("NyaaDownloader")
MainWindow.setWindowIcon(QtGui.QIcon(ICON_PATH))
- MainWindow.resize(800, 341)
- MainWindow.setMinimumSize(QtCore.QSize(800, 341))
- MainWindow.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
+ MainWindow.resize(800, 450)
+ MainWindow.setMinimumSize(QtCore.QSize(800, 450))
+ MainWindow.setLocale(QtCore.QLocale(QtCore.QLocale.Language.English, QtCore.QLocale.Country.UnitedStates))
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
@@ -195,7 +195,7 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
- self.actionGet_translation_of_an_anime_title = QtWidgets.QAction(MainWindow)
+ self.actionGet_translation_of_an_anime_title = QtWidgets.QWidgetAction(MainWindow)
self.actionGet_translation_of_an_anime_title.setObjectName("actionGet_translation_of_an_anime_title")
self.menuTranslator.addAction(self.actionGet_translation_of_an_anime_title)
self.menubar.addAction(self.menuTranslator.menuAction())
@@ -273,10 +273,10 @@ def retranslateUi(self, MainWindow) -> None:
def ask_anime_to_translate(self) -> None:
"""Asking anime title to translate by opening a link to MyAnimeList"""
text, okPressed = QInputDialog.getText(
- self, "Search in MyAnimeList", "Anime title to find in MyAnimeList:", QLineEdit.Normal
+ self, "Search in MyAnimeList", "Anime title to find in MyAnimeList:", QLineEdit.EchoMode.Normal
)
if okPressed and text != "":
- text = urllib.quote(text)
+ text = urllib.parse.quote(text)
wb.open(f"https://myanimelist.net/anime.php?q={text}&cat=anime")
def cancel_process(self) -> None:
@@ -377,7 +377,7 @@ def get_download_folder(anime_name: str) -> str:
for s in unhandled_characters:
anime_name = anime_name.replace(s, "")
- downloads = QStandardPaths.writableLocation(QStandardPaths.DownloadLocation)
+ downloads = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation)
return os.path.join(downloads, "DownloadedTorrents", anime_name)
@classmethod
@@ -450,8 +450,8 @@ def save_logs(self) -> None:
if name != "":
with open(f"{name}.txt", "w") as f:
f.write(self.textBrowser.toPlainText())
- except Exception:
- pass
+ except Exception as e:
+ self.show_error_popup(e)
def is_everything_good(self) -> None:
"""Check if every input values are correct and if yes, will call the start_checking method"""
From 70ddddc5f56d17b4580671b66f57a9ae61dfafbd Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 19:05:01 +0200
Subject: [PATCH 06/15] Someone forgot disabling this checkbox during
operations
---
util/gui.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/util/gui.py b/util/gui.py
index d702d96..44cf21e 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -348,6 +348,7 @@ def set_widget_while_check(self) -> None:
self.radioButton.setEnabled(False)
self.radioButton_2.setEnabled(False)
self.pushButton_3.setEnabled(False)
+ self.checkBox_2.setEnabled(False)
self.pushButton_4.setVisible(True)
@@ -364,6 +365,7 @@ def set_widget_after_check(self) -> None:
self.checkBox.setEnabled(True)
self.radioButton.setEnabled(True)
self.radioButton_2.setEnabled(True)
+ self.checkBox_2.setEnabled(True)
self.checkBox.setChecked(False) # Reseting checkbox
self.pushButton_2.setEnabled(True) # Enabling Open Folder button
From 7c25417de727ab4cbab966e993ecd54136f5c61a Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 19:10:29 +0200
Subject: [PATCH 07/15] enable checkbox "Until last released one" by default
and don't reset it after completion, change fails_in_a_row to 3
---
util/gui.py | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index 44cf21e..5ca5afb 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -105,10 +105,12 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.spinBox_2.setMinimum(1)
self.spinBox_2.setMaximum(10000)
self.spinBox_2.setObjectName("spinBox_2")
+ self.spinBox_2.setEnabled(False)
mid_layout.addWidget(self.spinBox_2)
self.checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.checkBox.setObjectName("checkBox")
+ self.checkBox.setObjectName("checkBox")
+ self.checkBox.setChecked(True)
mid_layout.addWidget(self.checkBox)
left_layout.addLayout(mid_layout)
@@ -361,13 +363,13 @@ def set_widget_after_check(self) -> None:
self.lineEdit_2.setEnabled(True)
self.comboBox.setEnabled(True)
self.spinBox.setEnabled(True)
- self.spinBox_2.setEnabled(True)
+ if not self.checkBox.isChecked():
+ self.spinBox_2.setEnabled(True)
self.checkBox.setEnabled(True)
self.radioButton.setEnabled(True)
self.radioButton_2.setEnabled(True)
self.checkBox_2.setEnabled(True)
- self.checkBox.setChecked(False) # Reseting checkbox
self.pushButton_2.setEnabled(True) # Enabling Open Folder button
self.pushButton_4.setVisible(False) # Disabling Stop button
self.pushButton_3.setEnabled(True) # Enabling Save logs button
@@ -560,7 +562,7 @@ def run(self) -> None:
unexpected_end = False
# Will break if "END" found in title (Erai-raws)
- while not unexpected_end and episode <= start_end[1] and fails_in_a_row < 10:
+ while not unexpected_end and episode <= start_end[1] and fails_in_a_row < 3:
for uploader in uploaders:
torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, untrusted_option)
@@ -611,7 +613,7 @@ def run(self) -> None:
episode += 1
- if fails_in_a_row >= 10:
+ if fails_in_a_row >= 3:
self.update_logs.emit(
- f"Note: {anime_name} seems to only have {episode - 11} episodes"
+ f"Note: {anime_name} seems to only have {episode - 4} episodes"
)
From cba9931b298d0ddbf3e582e1726b573151b3237c Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 19:27:01 +0200
Subject: [PATCH 08/15] fix untrusted_option
---
util/gui.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/util/gui.py b/util/gui.py
index 5ca5afb..ef6401a 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -502,7 +502,7 @@ def is_everything_good(self) -> None:
)
quality = int(self.comboBox.currentText()[:-1])
option = 1 if self.radioButton.isChecked() else 2
- untrusted_option = True if self.radioButton_2.isChecked() else False
+ untrusted_option = True if self.checkBox_2.isChecked() else False
path = self.get_download_folder(anime_name)
self.start_checking()
From 0297dd5686303651043a29de20c15601a43c34df Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 20:04:02 +0200
Subject: [PATCH 09/15] allow saving magnets to a text file
---
util/gui.py | 35 +++++++++++++++++++++++++++--------
util/nyaa.py | 4 +---
2 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index ef6401a..02989d0 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -59,9 +59,9 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
MainWindow.setObjectName("NyaaDownloader")
MainWindow.setWindowIcon(QtGui.QIcon(ICON_PATH))
- MainWindow.resize(800, 450)
- MainWindow.setMinimumSize(QtCore.QSize(800, 450))
- MainWindow.setLocale(QtCore.QLocale(QtCore.QLocale.Language.English, QtCore.QLocale.Country.UnitedStates))
+ MainWindow.resize(800, 470)
+ MainWindow.setMinimumSize(QtCore.QSize(800, 470))
+ MainWindow.setLocale(QtCore.QLocale(QtCore.QLocale.Language.English, QtCore.QLocale.Country.World))
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
@@ -141,6 +141,10 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.radioButton_2.setObjectName("radioButton_2")
bottom_left_layout.addWidget(self.radioButton_2)
+ self.radioButton_3 = QtWidgets.QRadioButton(self.centralwidget)
+ self.radioButton_3.setObjectName("radioButton_3")
+ bottom_left_layout.addWidget(self.radioButton_3)
+
btn_layout = QtWidgets.QHBoxLayout()
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setMinimumSize(QtCore.QSize(70, 30))
@@ -241,15 +245,16 @@ def retranslateUi(self, MainWindow) -> None:
"Download .torrent files or open magnet links directly in your torrent client?",
)
)
- self.radioButton.setText(_translate("MainWindow", "Torrent"))
- self.radioButton_2.setText(_translate("MainWindow", "Magnet"))
+ self.radioButton.setText(_translate("MainWindow", "Download .torrent files"))
+ self.radioButton_2.setText(_translate("MainWindow", "Magnet (launch torrent client)"))
+ self.radioButton_3.setText(_translate("MainWindow", "Magnet (write to a text file)"))
self.checkBox.setText(_translate("MainWindow", "Until last released one"))
self.pushButton.setText(_translate("MainWindow", "Check"))
self.label_7.setText(_translate("MainWindow", "Logs:"))
self.pushButton_2.setText(_translate("MainWindow", "Open folder"))
self.pushButton_3.setText(_translate("MainWindow", "Save logs as .txt"))
self.pushButton_4.setText(_translate("MainWindow", "Stop"))
- self.checkBox_2.setText(_translate("MainWindow", "Allow untrusted"))
+ self.checkBox_2.setText(_translate("MainWindow", "Allow untrusted (torrents not uploaded by trusted users)"))
self.menuTranslator.setTitle(_translate("MainWindow", "Translator"))
self.actionGet_translation_of_an_anime_title.setText(
_translate("MainWindow", "Get translation of an anime title")
@@ -349,6 +354,7 @@ def set_widget_while_check(self) -> None:
self.checkBox.setEnabled(False)
self.radioButton.setEnabled(False)
self.radioButton_2.setEnabled(False)
+ self.radioButton_3.setEnabled(False)
self.pushButton_3.setEnabled(False)
self.checkBox_2.setEnabled(False)
@@ -368,6 +374,7 @@ def set_widget_after_check(self) -> None:
self.checkBox.setEnabled(True)
self.radioButton.setEnabled(True)
self.radioButton_2.setEnabled(True)
+ self.radioButton_3.setEnabled(True)
self.checkBox_2.setEnabled(True)
self.pushButton_2.setEnabled(True) # Enabling Open Folder button
@@ -501,7 +508,12 @@ def is_everything_good(self) -> None:
else (int(self.spinBox.text()), int(self.spinBox_2.text()))
)
quality = int(self.comboBox.currentText()[:-1])
- option = 1 if self.radioButton.isChecked() else 2
+ if self.radioButton_3.isChecked():
+ option = 3
+ elif self.radioButton_2.isChecked():
+ option = 2
+ else: #self.radioButton.isChecked():
+ option = 1
untrusted_option = True if self.checkBox_2.isChecked() else False
path = self.get_download_folder(anime_name)
@@ -583,7 +595,6 @@ def run(self) -> None:
unexpected_end = True
break
- # I prefer this rather than a simple else because it's cleaner
elif option == 2 and not nyaa.transfer(torrent):
self.error_popup.emit(
"No bittorrent client or web browser (that supports magnet links) found."
@@ -591,6 +602,14 @@ def run(self) -> None:
unexpected_end = True
break
+ elif option == 3:
+ Ui_MainWindow.generate_download_folder(anime_name)
+ with open(os.path.join(path, "magnets.txt"), "a") as magnetsfile:
+ magnetsfile.write(torrent.magnet)
+ magnetsfile.write("\n")
+ magnetsfile.close()
+
+
self.update_logs.emit(f"Found: {anime_name} - Episode {episode}")
# Erai-raws add "END" in the torrent name when an anime has finished airing
diff --git a/util/nyaa.py b/util/nyaa.py
index d92c387..3064190 100644
--- a/util/nyaa.py
+++ b/util/nyaa.py
@@ -65,13 +65,11 @@ def transfer(torrent: dict) -> bool:
bool: True if the transfer was successful, False otherwise.
"""
try:
- wb.open(torrent.magnet)
+ return wb.open(torrent.magnet)
except:
return False
- return True
-
def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, untrusted_option: bool) -> dict:
"""Find if the torrent is already in the database. If not, download it.
From c6354c228c0ed33098622387a7b77d5289082312 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 21:35:22 +0200
Subject: [PATCH 10/15] add HEVC and make torrent matching code more readable
---
util/gui.py | 40 ++++++++++++++++++++++++++++-----------
util/nyaa.py | 53 ++++++++++++++++++++++++++++++++--------------------
2 files changed, 62 insertions(+), 31 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index 02989d0..dbc04c3 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -14,6 +14,7 @@
import os
import webbrowser as wb
import urllib
+import re
# ------------------------------GLOBAL VARIABLES------------------------------
@@ -123,8 +124,17 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.comboBox = QtWidgets.QComboBox(self.centralwidget)
self.comboBox.setObjectName("comboBox")
- self.comboBox.addItems(["2160p", "1080p", "720p", "480p"])
- self.comboBox.setCurrentIndex(1)
+ self.comboBox.addItems([
+ "2160p",
+ "2160p HEVC",
+ "1080p",
+ "1080p HEVC",
+ "720p",
+ "720p HEVC",
+ "480p",
+ "480p HEVC",
+ ])
+ self.comboBox.setCurrentIndex(2)
bottom_left_layout.addWidget(self.comboBox)
self.label_6 = QtWidgets.QLabel(self.centralwidget)
@@ -224,7 +234,7 @@ def retranslateUi(self, MainWindow) -> None:
self.label.setText(_translate("MainWindow", "Uploaders:"))
self.lineEdit.setPlaceholderText(
_translate(
- "MainWindow", "Empty=all, else use semicolon like Erai-raws;SubsPlease"
+ "MainWindow", "Empty = all, otherwise separate with semicolon like Erai-raws;SubsPlease"
)
)
self.label_2.setText(_translate("MainWindow", "Anime Title:"))
@@ -235,10 +245,14 @@ def retranslateUi(self, MainWindow) -> None:
self.label_4.setText(_translate("MainWindow", "Until episode:"))
self.label_5.setText(_translate("MainWindow", "Quality:"))
self.comboBox.setItemText(0, _translate("MainWindow", "2160p"))
- self.comboBox.setItemText(1, _translate("MainWindow", "1080p"))
- self.comboBox.setItemText(2, _translate("MainWindow", "720p"))
- self.comboBox.setItemText(3, _translate("MainWindow", "480p"))
- self.comboBox.setCurrentIndex(1)
+ self.comboBox.setItemText(1, _translate("MainWindow", "2160p HEVC"))
+ self.comboBox.setItemText(2, _translate("MainWindow", "1080p"))
+ self.comboBox.setItemText(3, _translate("MainWindow", "1080p HEVC"))
+ self.comboBox.setItemText(4, _translate("MainWindow", "720p"))
+ self.comboBox.setItemText(5, _translate("MainWindow", "720p HEVC"))
+ self.comboBox.setItemText(6, _translate("MainWindow", "480p"))
+ self.comboBox.setItemText(7, _translate("MainWindow", "480p HEVC"))
+ self.comboBox.setCurrentIndex(2)
self.label_6.setText(
_translate(
"MainWindow",
@@ -492,7 +506,7 @@ def is_everything_good(self) -> None:
# Setting proper values
if everything_good:
- global uploaders, anime_name, start_end, quality, option, untrusted_option, path
+ global uploaders, anime_name, start_end, quality, hevc, option, untrusted_option, path
uploaders = [
u.strip() for u in self.lineEdit.text().strip().split(";") if u != ""
@@ -507,7 +521,8 @@ def is_everything_good(self) -> None:
if self.checkBox.isChecked()
else (int(self.spinBox.text()), int(self.spinBox_2.text()))
)
- quality = int(self.comboBox.currentText()[:-1])
+ quality = int(re.search(r'\d+', self.comboBox.currentText()).group())
+ hevc = "HEVC" in self.comboBox.currentText()
if self.radioButton_3.isChecked():
option = 3
elif self.radioButton_2.isChecked():
@@ -576,7 +591,7 @@ def run(self) -> None:
# Will break if "END" found in title (Erai-raws)
while not unexpected_end and episode <= start_end[1] and fails_in_a_row < 3:
for uploader in uploaders:
- torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, untrusted_option)
+ torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, hevc, untrusted_option)
if torrent:
fails_in_a_row = 0
@@ -611,9 +626,12 @@ def run(self) -> None:
self.update_logs.emit(f"Found: {anime_name} - Episode {episode}")
+ self.update_logs.emit("")
+ self.update_logs.emit(torrent.name)
+ self.update_logs.emit("")
# Erai-raws add "END" in the torrent name when an anime has finished airing
- if uploader == "Erai-raws" and torrent.name.find(" END [") != -1:
+ if uploader == "Erai-raws" and " END [" in torrent.name:
self.update_logs.emit(
f"Note: {anime_name} has no more than {episode} episodes"
)
diff --git a/util/nyaa.py b/util/nyaa.py
index 3064190..86c91b1 100644
--- a/util/nyaa.py
+++ b/util/nyaa.py
@@ -71,7 +71,7 @@ def transfer(torrent: dict) -> bool:
return False
-def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, untrusted_option: bool) -> dict:
+def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, hevc: bool, untrusted_option: bool) -> dict:
"""Find if the torrent is already in the database. If not, download it.
Args:
uploader (str): The name of the uploader.
@@ -89,28 +89,40 @@ def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, unt
else:
episode = "0" + str(episode)
- found_torrents = NyaaPy.Nyaa.search(
- keyword=f"[{uploader}] {anime_name} - {episode} [{quality}p]",
- category=1,
- subcategory=2,
- filters=0 if untrusted_option else 2,
- ) + NyaaPy.Nyaa.search(
- keyword=f"[{uploader}] {anime_name} - {episode} ({quality}p)",
- category=1,
- subcategory=2,
- filters=0 if untrusted_option else 2,
- )
-
- if not isinstance(found_torrents, list) or len(found_torrents) == 0:
+ hevc_keywords = ["HEVC", "x265", "H.265"]
+
+ queries = []
+
+ # Explicitly either HEVC or not
+ if hevc:
+ queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + "|".join(map(lambda k: f'"{k}"', hevc_keywords)))
+ else:
+ queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + " ".join(map(lambda k: f'-"{k}"', hevc_keywords)))
+
+ found_torrents = []
+ for query in queries:
+ chunk = NyaaPy.Nyaa.search(
+ keyword=query,
+ category=1,
+ subcategory=2,
+ filters=0 if untrusted_option else 2,
+ )
+ print(query, found_torrents)
+ if not isinstance(found_torrents, list) or len(found_torrents) == 0:
+ found_torrents = found_torrents + chunk
+
+ if len(found_torrents) == 0:
return {}
try:
# We take the very closest title to what we are looking for.
torrent = None
- for t in found_torrents: # (break if found, so we get the most recent one)
+ a_name = anime_name.lower()
+ for t in found_torrents: # (break if found, so we get the most recent one)
+ t_name = t.name.lower()
if (
- t.name.lower().find(f"{anime_name} - {episode}".lower()) != -1
- and t.name.lower().find("~") == -1
+ f"{a_name} - {episode}" in t_name
+ and "~" not in t_name
): # we want to avoid ~ because Erai-Raws use it for already packed episodes
torrent = t
break
@@ -118,10 +130,11 @@ def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, unt
# Else, we take try to get a close title to the one we are looking for.
if torrent is None:
for t in found_torrents:
+ t_name = t.name.lower()
if (
- t.name.lower().find(f"{anime_name}".lower()) != -1
- and t.name.lower().find(f" {episode} ") != -1
- and t.name.lower().find("~") == -1
+ a_name in t_name
+ and f" {episode} " in t_name
+ and "~" not in t_name
): # we want to avoid ~ because Erai-Raws use it for already packed episodes
torrent = t
break
From d24e82d7dc1725ee4c9d82ae51580e3caff96eb5 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 22:01:41 +0200
Subject: [PATCH 11/15] add AV1 and Best quality settings
---
util/gui.py | 47 +++++++++++++++++++++++++++++++----------------
util/nyaa.py | 37 +++++++++++++++++++++++++++----------
2 files changed, 58 insertions(+), 26 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index dbc04c3..256aa59 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -125,13 +125,18 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.comboBox = QtWidgets.QComboBox(self.centralwidget)
self.comboBox.setObjectName("comboBox")
self.comboBox.addItems([
+ "Best",
"2160p",
+ "2160p AV1",
"2160p HEVC",
"1080p",
+ "1080p AV1",
"1080p HEVC",
"720p",
+ "720p AV1",
"720p HEVC",
"480p",
+ "480p AV1",
"480p HEVC",
])
self.comboBox.setCurrentIndex(2)
@@ -244,15 +249,20 @@ def retranslateUi(self, MainWindow) -> None:
self.label_3.setText(_translate("MainWindow", "Starting from:"))
self.label_4.setText(_translate("MainWindow", "Until episode:"))
self.label_5.setText(_translate("MainWindow", "Quality:"))
- self.comboBox.setItemText(0, _translate("MainWindow", "2160p"))
- self.comboBox.setItemText(1, _translate("MainWindow", "2160p HEVC"))
- self.comboBox.setItemText(2, _translate("MainWindow", "1080p"))
- self.comboBox.setItemText(3, _translate("MainWindow", "1080p HEVC"))
- self.comboBox.setItemText(4, _translate("MainWindow", "720p"))
- self.comboBox.setItemText(5, _translate("MainWindow", "720p HEVC"))
- self.comboBox.setItemText(6, _translate("MainWindow", "480p"))
- self.comboBox.setItemText(7, _translate("MainWindow", "480p HEVC"))
- self.comboBox.setCurrentIndex(2)
+ self.comboBox.setItemText(0, _translate("MainWindow", "Best"))
+ self.comboBox.setItemText(1, _translate("MainWindow", "2160p"))
+ self.comboBox.setItemText(2, _translate("MainWindow", "2160p AV1"))
+ self.comboBox.setItemText(3, _translate("MainWindow", "2160p HEVC"))
+ self.comboBox.setItemText(4, _translate("MainWindow", "1080p"))
+ self.comboBox.setItemText(5, _translate("MainWindow", "1080p AV1"))
+ self.comboBox.setItemText(6, _translate("MainWindow", "1080p HEVC"))
+ self.comboBox.setItemText(7, _translate("MainWindow", "720p"))
+ self.comboBox.setItemText(8, _translate("MainWindow", "720p AV1"))
+ self.comboBox.setItemText(9, _translate("MainWindow", "720p HEVC"))
+ self.comboBox.setItemText(10, _translate("MainWindow", "480p"))
+ self.comboBox.setItemText(11, _translate("MainWindow", "480p AV1"))
+ self.comboBox.setItemText(12, _translate("MainWindow", "480p HEVC"))
+ self.comboBox.setCurrentIndex(0)
self.label_6.setText(
_translate(
"MainWindow",
@@ -506,7 +516,7 @@ def is_everything_good(self) -> None:
# Setting proper values
if everything_good:
- global uploaders, anime_name, start_end, quality, hevc, option, untrusted_option, path
+ global uploaders, anime_name, start_end, quality, codec, option, untrusted_option, path
uploaders = [
u.strip() for u in self.lineEdit.text().strip().split(";") if u != ""
@@ -521,8 +531,13 @@ def is_everything_good(self) -> None:
if self.checkBox.isChecked()
else (int(self.spinBox.text()), int(self.spinBox_2.text()))
)
- quality = int(re.search(r'\d+', self.comboBox.currentText()).group())
- hevc = "HEVC" in self.comboBox.currentText()
+ quality_text = self.comboBox.currentText()
+ quality = None if quality_text == "Best" else int(re.search(r'\d+', quality_text).group())
+ codec = None
+ if "AV1" in quality_text:
+ codec = "AV1"
+ elif "HEVC" in quality_text:
+ codec = "HEVC"
if self.radioButton_3.isChecked():
option = 3
elif self.radioButton_2.isChecked():
@@ -589,9 +604,9 @@ def run(self) -> None:
unexpected_end = False
# Will break if "END" found in title (Erai-raws)
- while not unexpected_end and episode <= start_end[1] and fails_in_a_row < 3:
+ while not unexpected_end and episode <= start_end[1] and fails_in_a_row < 2:
for uploader in uploaders:
- torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, hevc, untrusted_option)
+ torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, codec, untrusted_option)
if torrent:
fails_in_a_row = 0
@@ -650,7 +665,7 @@ def run(self) -> None:
episode += 1
- if fails_in_a_row >= 3:
+ if fails_in_a_row >= 2:
self.update_logs.emit(
- f"Note: {anime_name} seems to only have {episode - 4} episodes"
+ f"\nNote: {anime_name} seems to only have {episode - 3} episodes"
)
diff --git a/util/nyaa.py b/util/nyaa.py
index 86c91b1..73d3f4d 100644
--- a/util/nyaa.py
+++ b/util/nyaa.py
@@ -2,6 +2,7 @@
import nyaapy.nyaasi.nyaa as NyaaPy
+from nyaapy.torrent import Torrent
import requests
import webbrowser as wb
@@ -70,8 +71,7 @@ def transfer(torrent: dict) -> bool:
except:
return False
-
-def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, hevc: bool, untrusted_option: bool) -> dict:
+def find_torrent(uploader: str, anime_name: str, episode_num: int, quality: int, codec: str | None, untrusted_option: bool) -> Torrent | None:
"""Find if the torrent is already in the database. If not, download it.
Args:
uploader (str): The name of the uploader.
@@ -83,21 +83,39 @@ def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, hev
dict: Returns torrent if found, else None.
"""
+ if quality is None:
+ resolutions = [2160, 1080, 720, 480]
+ codecs = ["AV1", "HEVC", None]
+ qualities = [(res, codec) for res in resolutions for codec in codecs]
+ for quality in qualities:
+ resolution = quality[0]
+ codec = quality[1]
+ result = find_torrent(uploader, anime_name, episode_num, resolution, codec, untrusted_option)
+ if result is not None and isinstance(result, Torrent):
+ return result
+ return None
+
# Because anime title usually have their episode number like '0X' when X < 10, we need to add a 0 to the episode number.
- if episode >= 10:
- episode = str(episode)
+ if episode_num >= 10:
+ episode = str(episode_num)
else:
- episode = "0" + str(episode)
+ episode = "0" + str(episode_num)
hevc_keywords = ["HEVC", "x265", "H.265"]
+ hevc_keyword_positive = "|".join(map(lambda k: f'"{k}"', hevc_keywords))
+ hevc_keyword_negative = " ".join(map(lambda k: f'-"{k}"', hevc_keywords))
+ av1_keyword_positive = "AV1"
+ av1_keyword_negative = "-AV1"
queries = []
# Explicitly either HEVC or not
- if hevc:
- queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + "|".join(map(lambda k: f'"{k}"', hevc_keywords)))
+ if codec is "AV1":
+ queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + av1_keyword_positive + " " + hevc_keyword_negative)
+ elif codec is "HEVC":
+ queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + av1_keyword_negative + " " + hevc_keyword_positive)
else:
- queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + " ".join(map(lambda k: f'-"{k}"', hevc_keywords)))
+ queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + av1_keyword_negative + " " + hevc_keyword_negative)
found_torrents = []
for query in queries:
@@ -107,12 +125,11 @@ def find_torrent(uploader: str, anime_name: str, episode: int, quality: int, hev
subcategory=2,
filters=0 if untrusted_option else 2,
)
- print(query, found_torrents)
if not isinstance(found_torrents, list) or len(found_torrents) == 0:
found_torrents = found_torrents + chunk
if len(found_torrents) == 0:
- return {}
+ return None
try:
# We take the very closest title to what we are looking for.
From 33f9f3d41df93fa4aad95531466c80b26075c308 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 22:38:02 +0200
Subject: [PATCH 12/15] allow batches + statusbar text
---
util/gui.py | 31 +++++++++++----
util/nyaa.py | 104 ++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 103 insertions(+), 32 deletions(-)
diff --git a/util/gui.py b/util/gui.py
index 256aa59..05bc544 100644
--- a/util/gui.py
+++ b/util/gui.py
@@ -51,6 +51,7 @@ def update_config(key: str, value: str) -> None:
# Generated with Qt Designer (first time using this one)
class Ui_MainWindow(QDialog):
+
def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
"""Build skeleton of the GUI
@@ -60,8 +61,8 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
MainWindow.setObjectName("NyaaDownloader")
MainWindow.setWindowIcon(QtGui.QIcon(ICON_PATH))
- MainWindow.resize(800, 470)
- MainWindow.setMinimumSize(QtCore.QSize(800, 470))
+ MainWindow.resize(800, 490)
+ MainWindow.setMinimumSize(QtCore.QSize(800, 490))
MainWindow.setLocale(QtCore.QLocale(QtCore.QLocale.Language.English, QtCore.QLocale.Country.World))
self.centralwidget = QtWidgets.QWidget(MainWindow)
@@ -178,6 +179,11 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.checkBox_2.setObjectName("checkBox_2")
bottom_left_layout.addWidget(self.checkBox_2)
+ self.checkBox_3 = QtWidgets.QCheckBox(self.centralwidget)
+ self.checkBox_3.setObjectName("checkBox_3")
+ self.checkBox_3.setChecked(True)
+ bottom_left_layout.addWidget(self.checkBox_3)
+
left_layout.addLayout(bottom_left_layout)
main_layout.addLayout(left_layout)
@@ -219,7 +225,7 @@ def setupUi(self, MainWindow: QtWidgets.QMainWindow) -> None:
self.actionGet_translation_of_an_anime_title = QtWidgets.QWidgetAction(MainWindow)
self.actionGet_translation_of_an_anime_title.setObjectName("actionGet_translation_of_an_anime_title")
self.menuTranslator.addAction(self.actionGet_translation_of_an_anime_title)
- self.menubar.addAction(self.menuTranslator.menuAction())
+ self.menubar.addAction(self.menuTranslator.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
@@ -279,6 +285,7 @@ def retranslateUi(self, MainWindow) -> None:
self.pushButton_3.setText(_translate("MainWindow", "Save logs as .txt"))
self.pushButton_4.setText(_translate("MainWindow", "Stop"))
self.checkBox_2.setText(_translate("MainWindow", "Allow untrusted (torrents not uploaded by trusted users)"))
+ self.checkBox_3.setText(_translate("MainWindow", "Allow batches (multiple episodes in a single torrent)"))
self.menuTranslator.setTitle(_translate("MainWindow", "Translator"))
self.actionGet_translation_of_an_anime_title.setText(
_translate("MainWindow", "Get translation of an anime title")
@@ -381,6 +388,7 @@ def set_widget_while_check(self) -> None:
self.radioButton_3.setEnabled(False)
self.pushButton_3.setEnabled(False)
self.checkBox_2.setEnabled(False)
+ self.checkBox_3.setEnabled(False)
self.pushButton_4.setVisible(True)
@@ -400,6 +408,7 @@ def set_widget_after_check(self) -> None:
self.radioButton_2.setEnabled(True)
self.radioButton_3.setEnabled(True)
self.checkBox_2.setEnabled(True)
+ self.checkBox_3.setEnabled(True)
self.pushButton_2.setEnabled(True) # Enabling Open Folder button
self.pushButton_4.setVisible(False) # Disabling Stop button
@@ -516,7 +525,7 @@ def is_everything_good(self) -> None:
# Setting proper values
if everything_good:
- global uploaders, anime_name, start_end, quality, codec, option, untrusted_option, path
+ global uploaders, anime_name, start_end, quality, codec, option, untrusted_option, allow_batch, path
uploaders = [
u.strip() for u in self.lineEdit.text().strip().split(";") if u != ""
@@ -544,7 +553,8 @@ def is_everything_good(self) -> None:
option = 2
else: #self.radioButton.isChecked():
option = 1
- untrusted_option = True if self.checkBox_2.isChecked() else False
+ untrusted_option = self.checkBox_2.isChecked()
+ allow_batch = self.checkBox_3.isChecked()
path = self.get_download_folder(anime_name)
self.start_checking()
@@ -565,6 +575,7 @@ def start_checking(self) -> None:
self.worker.finished.connect(lambda: self.worker_finished())
self.worker.update_logs.connect(self.append_to_logs)
self.worker.error_popup.connect(self.show_error_popup)
+ self.worker.statusbar_signal.connect(lambda text: self.statusbar.showMessage(text))
def worker_finished(self) -> None:
"""When the thread has finished processing, enable all widgets again and notify the user
@@ -592,6 +603,7 @@ class WorkerThread(QThread):
update_logs = pyqtSignal(str)
error_popup = pyqtSignal(str)
+ statusbar_signal = pyqtSignal(str)
def run(self) -> None:
"""The "almost main" function of that program. Will download/transfer every found torrent. Will also handle logs update, etc."""
@@ -606,7 +618,7 @@ def run(self) -> None:
# Will break if "END" found in title (Erai-raws)
while not unexpected_end and episode <= start_end[1] and fails_in_a_row < 2:
for uploader in uploaders:
- torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, codec, untrusted_option)
+ torrent = nyaa.find_torrent(uploader, anime_name, episode, quality, codec, untrusted_option, allow_batch, start_end, self.statusbar_signal)
if torrent:
fails_in_a_row = 0
@@ -639,8 +651,13 @@ def run(self) -> None:
magnetsfile.write("\n")
magnetsfile.close()
+ torrent_batch_info = nyaa.parse_batch_info(torrent.name)
- self.update_logs.emit(f"Found: {anime_name} - Episode {episode}")
+ if torrent_batch_info is not None:
+ self.update_logs.emit(f"Found: {anime_name} - Episode {torrent_batch_info[0]} ~ {torrent_batch_info[1]}")
+ episode = torrent_batch_info[1]
+ else:
+ self.update_logs.emit(f"Found: {anime_name} - Episode {episode}")
self.update_logs.emit("")
self.update_logs.emit(torrent.name)
self.update_logs.emit("")
diff --git a/util/nyaa.py b/util/nyaa.py
index 73d3f4d..a347b52 100644
--- a/util/nyaa.py
+++ b/util/nyaa.py
@@ -3,6 +3,8 @@
import nyaapy.nyaasi.nyaa as NyaaPy
from nyaapy.torrent import Torrent
+
+import re
import requests
import webbrowser as wb
@@ -71,13 +73,16 @@ def transfer(torrent: dict) -> bool:
except:
return False
-def find_torrent(uploader: str, anime_name: str, episode_num: int, quality: int, codec: str | None, untrusted_option: bool) -> Torrent | None:
+def parse_batch_info(torrent_name: str) -> None | tuple:
+ #if "batch" not in anime_name.lower(): # oh it will not always have the word "batch" in name
+ # return None
+ m = re.search(r"[\s(\[]([0-9]+)\s*[-~]\s*([0-9]+)[\s)\]]", torrent_name)
+ if m is None:
+ return None
+ return (int(m[1]), int(m[2]))
+
+def find_torrent(uploader: str, anime_name: str, episode_num: int, quality: int, codec: str | None, untrusted_option: bool, allow_batch: bool, start_end: tuple[int, int], statusbar_signal) -> Torrent | None:
"""Find if the torrent is already in the database. If not, download it.
- Args:
- uploader (str): The name of the uploader.
- anime_name (str): The name of the anime.
- episode (int): The episode number.
- quality (str): The quality of the torrent.
Returns:
dict: Returns torrent if found, else None.
@@ -90,16 +95,19 @@ def find_torrent(uploader: str, anime_name: str, episode_num: int, quality: int,
for quality in qualities:
resolution = quality[0]
codec = quality[1]
- result = find_torrent(uploader, anime_name, episode_num, resolution, codec, untrusted_option)
+ result = find_torrent(uploader, anime_name, episode_num, resolution, codec, untrusted_option, allow_batch, start_end, statusbar_signal)
if result is not None and isinstance(result, Torrent):
return result
return None
# Because anime title usually have their episode number like '0X' when X < 10, we need to add a 0 to the episode number.
- if episode_num >= 10:
- episode = str(episode_num)
- else:
- episode = "0" + str(episode_num)
+ def get_episode_str(episode_num: int):
+ if episode_num >= 10:
+ episode = str(episode_num)
+ else:
+ episode = "0" + str(episode_num)
+ return episode
+ episode = get_episode_str(episode_num)
hevc_keywords = ["HEVC", "x265", "H.265"]
hevc_keyword_positive = "|".join(map(lambda k: f'"{k}"', hevc_keywords))
@@ -109,22 +117,26 @@ def find_torrent(uploader: str, anime_name: str, episode_num: int, quality: int,
queries = []
- # Explicitly either HEVC or not
- if codec is "AV1":
+ # Explicitly match only the exact codec we're looking for and nothing else (or none of supported codecs as fallback)
+ if codec == "AV1":
queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + av1_keyword_positive + " " + hevc_keyword_negative)
- elif codec is "HEVC":
+ elif codec == "HEVC":
queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + av1_keyword_negative + " " + hevc_keyword_positive)
else:
queries.append(f"[{uploader}] ({anime_name}) {episode} [{quality}p] " + av1_keyword_negative + " " + hevc_keyword_negative)
found_torrents = []
for query in queries:
+ if statusbar_signal:
+ statusbar_signal.emit(query)
chunk = NyaaPy.Nyaa.search(
keyword=query,
category=1,
subcategory=2,
filters=0 if untrusted_option else 2,
)
+ if statusbar_signal:
+ statusbar_signal.emit("")
if not isinstance(found_torrents, list) or len(found_torrents) == 0:
found_torrents = found_torrents + chunk
@@ -135,24 +147,66 @@ def find_torrent(uploader: str, anime_name: str, episode_num: int, quality: int,
# We take the very closest title to what we are looking for.
torrent = None
a_name = anime_name.lower()
- for t in found_torrents: # (break if found, so we get the most recent one)
- t_name = t.name.lower()
- if (
- f"{a_name} - {episode}" in t_name
- and "~" not in t_name
- ): # we want to avoid ~ because Erai-Raws use it for already packed episodes
- torrent = t
- break
+
+ ep = episode_num
+ ep_start = start_end[0]
+ ep_end = start_end[1]
+ #ep_start_str = get_episode_str(ep_start)
+ #ep_end_str = get_episode_str(ep_end)
+
+ if allow_batch:
+ for t in found_torrents:
+ t_name = t.name.lower()
+ batch_start_end = parse_batch_info(t_name)
+ if (
+ batch_start_end is not None
+ and batch_start_end[0] == ep # the batch starts with the episode we're currently trying to download
+ and batch_start_end[1] <= ep_end # the batch ends before or exactly where we want to end
+ and f"{a_name} - " in t_name # (prefer this name form first, so that we hopefully avoid matching sequels)
+ ):
+ torrent = t
+ break
+
+ if allow_batch and torrent is None:
+ for t in found_torrents:
+ t_name = t.name.lower()
+ batch_start_end = parse_batch_info(t_name)
+ if (
+ batch_start_end is not None
+ and batch_start_end[0] == ep # the batch starts with the episode we're currently trying to download
+ and batch_start_end[1] <= ep_end # the batch ends before or exactly where we want to end
+ and a_name in t_name # of course should have our anime name in the title as well
+ ):
+ torrent = t
+ break
+
+ if torrent is None:
+ for t in found_torrents:
+ t_name = t.name.lower()
+ batch_start_end = parse_batch_info(t_name)
+ if (
+ f"{a_name} - {episode}" in t_name
+ and batch_start_end is None
+ ):
+ torrent = t
+ break
# Else, we take try to get a close title to the one we are looking for.
if torrent is None:
for t in found_torrents:
t_name = t.name.lower()
+ batch_start_end = parse_batch_info(t_name)
if (
a_name in t_name
- and f" {episode} " in t_name
- and "~" not in t_name
- ): # we want to avoid ~ because Erai-Raws use it for already packed episodes
+ and (
+ f" {episode} " in t_name
+ or f"({episode})" in t_name
+ or f"[{episode}]" in t_name
+ or f"E{episode} " in t_name
+ or f"E{episode}]" in t_name
+ )
+ and batch_start_end is None
+ ):
torrent = t
break
From e01de718e5855c07b2495dbc43f4db6cd5ccdd68 Mon Sep 17 00:00:00 2001
From: p0358
Date: Thu, 8 May 2025 23:38:25 +0200
Subject: [PATCH 13/15] change directory structure to allow packing
---
.gitignore | 1 +
main.pyw => NyaaDownloader/__main__.py | 5 ++-
{util => NyaaDownloader}/gui.py | 2 +-
.../icons/nyaadownloader.ico | Bin
NyaaDownloader/icons/nyaadownloader.png | Bin 0 -> 52278 bytes
{util => NyaaDownloader}/nyaa.py | 0
data/NyaaDownloader.desktop | 10 +++++
pyproject.toml | 41 ++++++++++++++++++
requirements.txt | 4 +-
9 files changed, 58 insertions(+), 5 deletions(-)
rename main.pyw => NyaaDownloader/__main__.py (95%)
rename {util => NyaaDownloader}/gui.py (99%)
rename ico/nyaa.ico => NyaaDownloader/icons/nyaadownloader.ico (100%)
create mode 100644 NyaaDownloader/icons/nyaadownloader.png
rename {util => NyaaDownloader}/nyaa.py (100%)
create mode 100644 data/NyaaDownloader.desktop
create mode 100644 pyproject.toml
diff --git a/.gitignore b/.gitignore
index 587a297..3d756a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ main.spec
# PyInstaller .bat script that allows me to create the .exe from the release
binary_gen.bat
+NyaaDownloader.egg-info
diff --git a/main.pyw b/NyaaDownloader/__main__.py
similarity index 95%
rename from main.pyw
rename to NyaaDownloader/__main__.py
index da29acc..db43d31 100644
--- a/main.pyw
+++ b/NyaaDownloader/__main__.py
@@ -1,7 +1,7 @@
# ------------------------------IMPORTS------------------------------
-from util import gui
+from . import gui
from PyQt6 import QtWidgets
from PyQt6.QtCore import QStandardPaths
@@ -42,8 +42,9 @@ def main() -> None:
If you find any bug, please let me know on my GitHub:
https://github.com/marcpinet/nyaadownloader."""
- app = QtWidgets.QApplication([])
+ app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("NyaaDownloader")
+ app.setDesktopFileName("NyaaDownloader")
MainWindow = QtWidgets.QMainWindow()
ui = gui.Ui_MainWindow()
ui.setupUi(MainWindow)
diff --git a/util/gui.py b/NyaaDownloader/gui.py
similarity index 99%
rename from util/gui.py
rename to NyaaDownloader/gui.py
index 05bc544..2e18175 100644
--- a/util/gui.py
+++ b/NyaaDownloader/gui.py
@@ -21,7 +21,7 @@
unhandled_characters = ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]
-ICON_PATH = os.path.join("ico", "nyaa.ico")
+ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "nyaadownloader.ico")
# ------------------------------CLASSES AND METHODS------------------------------
diff --git a/ico/nyaa.ico b/NyaaDownloader/icons/nyaadownloader.ico
similarity index 100%
rename from ico/nyaa.ico
rename to NyaaDownloader/icons/nyaadownloader.ico
diff --git a/NyaaDownloader/icons/nyaadownloader.png b/NyaaDownloader/icons/nyaadownloader.png
new file mode 100644
index 0000000000000000000000000000000000000000..38cb2d4e0e1c6cd21aebf3a48cfeea1c79ac7e7b
GIT binary patch
literal 52278
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