From d6d8f4247a7370e13bf61308c318ce051f91628a Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Sat, 22 Feb 2025 14:48:10 +0100 Subject: [PATCH 01/17] add base run script and fix path for raspberry --- config.json | 2 +- run_streaming.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 run_streaming.py diff --git a/config.json b/config.json index 76cd172..9be6b77 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "show_message": true, "clean_console": true, "show_trending": true, - "root_path": "Video", + "root_path": "../media_plex", "movie_folder_name": "Movie", "serie_folder_name": "Serie", "anime_folder_name": "Anime", diff --git a/run_streaming.py b/run_streaming.py new file mode 100644 index 0000000..9488468 --- /dev/null +++ b/run_streaming.py @@ -0,0 +1,4 @@ +from StreamingCommunity.run import main + +if __name__ == "__main__": + main() \ No newline at end of file From 5d5204c4df21fb1c70700742da1b25b371a192e6 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Sat, 22 Feb 2025 15:46:31 +0100 Subject: [PATCH 02/17] Add support for passing search terms to functions Extend `run_function` to accept a `search_terms` parameter, allowing search filters to be dynamically passed. Update CLI argument parsing to include a `--search` option and propagate the value to the corresponding functions. --- StreamingCommunity/run.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 9cc5f42..d84c769 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -33,19 +33,20 @@ TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') -def run_function(func: Callable[..., None], close_console: bool = False) -> None: +def run_function(func: Callable[..., None], close_console: bool = False, search_terms: str = None) -> None: """ Run a given function indefinitely or once, depending on the value of close_console. Parameters: func (Callable[..., None]): The function to run. close_console (bool, optional): Whether to close the console after running the function once. Defaults to False. + search_terms (str, optional): Search terms to use for the function. Defaults to None. """ if close_console: while 1: - func() + func(search_terms) else: - func() + func(search_terms) def load_search_functions(): @@ -249,9 +250,11 @@ def main(script_id = 0): long_option = alias parser.add_argument(f'-{short_option}', f'--{long_option}', action='store_true', help=f'Search for {alias.split("_")[0]} on streaming platforms.') + parser.add_argument('-s', '--search', default=None, help='Search terms') # Parse command-line arguments args = parser.parse_args() + search_terms = args.search # Map command-line arguments to the config values config_updates = {} @@ -283,7 +286,7 @@ def main(script_id = 0): # Check which argument is provided and run the corresponding function for arg, func in arg_to_function.items(): if getattr(args, arg): - run_function(func) + run_function(func, search_terms=search_terms) return # Mapping user input to functions @@ -336,7 +339,7 @@ def main(script_id = 0): # Run the corresponding function based on user input if category in input_to_function: - run_function(input_to_function[category]) + run_function(input_to_function[category], search_terms = args.search) else: if TELEGRAM_BOT: From 4e2d6ca73f995f15601f908f29620b62204a38c1 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 11:57:17 +0100 Subject: [PATCH 03/17] Add GUI for script execution using PyQt5 and update dependencies This commit introduces a new graphical user interface (GUI) implemented in `streaming_gui.py`, enabling users to execute and interact with the script more intuitively. The GUI includes features for search inputs, site selection, script control, and real-time output display. Additionally, the `requirements.txt` file has been updated to include the PyQt5 dependency. --- requirements.txt | 3 +- streaming_gui.py | 200 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 streaming_gui.py diff --git a/requirements.txt b/requirements.txt index 0dca602..97ca848 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ pathvalidate pycryptodomex ua-generator qbittorrent-api -pyTelegramBotAPI \ No newline at end of file +pyTelegramBotAPI +PyQt5 \ No newline at end of file diff --git a/streaming_gui.py b/streaming_gui.py new file mode 100644 index 0000000..ee3c6ab --- /dev/null +++ b/streaming_gui.py @@ -0,0 +1,200 @@ +import sys +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLineEdit, QPushButton, + QComboBox, QTabWidget, QTextEdit, QGroupBox, + QFormLayout) +from PyQt5.QtCore import Qt, QProcess, pyqtSignal, QObject + +from StreamingCommunity.run import load_search_functions + +search_functions = load_search_functions() +sites = [] +for alias, (_, use_for) in search_functions.items(): + sites.append({"index": len(sites), "name": alias.split("_")[0], "flag": alias[:3].upper()}) + +class Stream(QObject): + """Reindirizza l'output dello script alla GUI""" + newText = pyqtSignal(str) + + def write(self, text): + self.newText.emit(str(text)) + + def flush(self): + pass + + +class StreamingGUI(QMainWindow): + def __init__(self): + super().__init__() + + # Processo per eseguire lo script + self.process = None + + self.init_ui() + + # Reindirizzamento dell'output + self.stdout_stream = Stream() + self.stdout_stream.newText.connect(self.update_output) + sys.stdout = self.stdout_stream + + def init_ui(self): + self.setWindowTitle("StreamingCommunity GUI") + self.setGeometry(100, 100, 1000, 700) + + # Widget e layout principale + central_widget = QWidget() + main_layout = QVBoxLayout() + + # Crea un widget a schede + tab_widget = QTabWidget() + + # Scheda 1: Esecuzione script + run_tab = QWidget() + run_layout = QVBoxLayout() + + # Gruppo per i parametri di ricerca + search_group = QGroupBox("Parametri di Ricerca") + search_layout = QFormLayout() + + self.search_terms = QLineEdit() + search_layout.addRow("Termini di ricerca:", self.search_terms) + + self.site_combo = QComboBox() + + for site in sites: + self.site_combo.addItem(f"{site['name']}", site['index']) + self.site_combo.setItemData(site['index'], site['flag'], Qt.ToolTipRole) + if self.site_combo.count() > 0: + self.site_combo.setCurrentIndex(0) + # Qui dovresti popolare il combobox con i siti disponibili + # Per ora lo lascio vuoto, ma andrebbe riempito dinamicamente + search_layout.addRow("Seleziona sito:", self.site_combo) + + search_group.setLayout(search_layout) + run_layout.addWidget(search_group) + + # Bottoni di controllo + control_layout = QHBoxLayout() + + self.run_button = QPushButton("Esegui Script") + self.run_button.clicked.connect(self.run_script) + control_layout.addWidget(self.run_button) + + self.stop_button = QPushButton("Ferma Script") + self.stop_button.clicked.connect(self.stop_script) + self.stop_button.setEnabled(False) + control_layout.addWidget(self.stop_button) + + run_layout.addLayout(control_layout) + + # Area di output + output_group = QGroupBox("Output") + output_layout = QVBoxLayout() + + self.output_text = QTextEdit() + self.output_text.setReadOnly(True) + output_layout.addWidget(self.output_text) + + output_group.setLayout(output_layout) + run_layout.addWidget(output_group) + + run_tab.setLayout(run_layout) + tab_widget.addTab(run_tab, "Esecuzione") + + main_layout.addWidget(tab_widget) + + central_widget.setLayout(main_layout) + self.setCentralWidget(central_widget) + + + def run_script(self): + if self.process is not None and self.process.state() == QProcess.Running: + print("Script già in esecuzione.") + return + + # Costruisci la command line con gli argomenti + args = [] + + # Aggiungi i termini di ricerca + search_terms = self.search_terms.text() + if search_terms: + args.extend(['-s', search_terms]) + + # Aggiungi il sito selezionato se presente + site_index = self.site_combo.currentIndex() + if site_index >= 0: + site_text = sites[site_index]['flag'] + # Assumo che il nome del sito sia il primo elemento del testo + site_name = site_text.split()[0].upper() + args.append(f'-{site_name}') + + self.output_text.clear() + print(f"Avvio script con argomenti: {' '.join(args)}") + + # Creiamo e avviamo il processo + self.process = QProcess() + self.process.readyReadStandardOutput.connect(self.handle_stdout) + self.process.readyReadStandardError.connect(self.handle_stderr) + self.process.finished.connect(self.process_finished) + + python_executable = sys.executable + script_path = "run_streaming.py" + + self.process.start(python_executable, [script_path] + args) + + # Aggiorna lo stato dei pulsanti + self.run_button.setEnabled(False) + self.stop_button.setEnabled(True) + + def stop_script(self): + if self.process is not None and self.process.state() == QProcess.Running: + self.process.terminate() + # Attendi che si chiuda (con timeout) + if not self.process.waitForFinished(3000): + self.process.kill() + print("Script terminato.") + + # Aggiorna lo stato dei pulsanti + self.run_button.setEnabled(True) + self.stop_button.setEnabled(False) + + def handle_stdout(self): + data = self.process.readAllStandardOutput() + stdout = bytes(data).decode('utf8', errors='replace') + self.update_output(stdout) + + def handle_stderr(self): + data = self.process.readAllStandardError() + stderr = bytes(data).decode('utf8', errors='replace') + self.update_output(stderr) + + def process_finished(self): + self.run_button.setEnabled(True) + self.stop_button.setEnabled(False) + print("Script terminato.") + + def update_output(self, text): + cursor = self.output_text.textCursor() + cursor.movePosition(cursor.End) + cursor.insertText(text) + self.output_text.setTextCursor(cursor) + self.output_text.ensureCursorVisible() + + def closeEvent(self, event): + # Fermae qualsiasi processo in esecuzione prima di chiudere + if self.process is not None and self.process.state() == QProcess.Running: + self.process.terminate() + if not self.process.waitForFinished(1000): + self.process.kill() + + # Ripristina stdout + sys.stdout = sys.__stdout__ + + event.accept() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + gui = StreamingGUI() + gui.show() + sys.exit(app.exec_()) \ No newline at end of file From 462aed698b8516fcf462fa15247b0c9e3bb8d753 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 11:57:57 +0100 Subject: [PATCH 04/17] Refactor code formatting and standardize string usage. Improved code readability by restructuring imports, using consistent string quotations (double quotes), and adjusting layout formatting. These changes aim to follow consistent style conventions and enhance maintainability. --- streaming_gui.py | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/streaming_gui.py b/streaming_gui.py index ee3c6ab..d602e78 100644 --- a/streaming_gui.py +++ b/streaming_gui.py @@ -1,8 +1,18 @@ import sys -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QHBoxLayout, QLineEdit, QPushButton, - QComboBox, QTabWidget, QTextEdit, QGroupBox, - QFormLayout) +from PyQt5.QtWidgets import ( + QApplication, + QMainWindow, + QWidget, + QVBoxLayout, + QHBoxLayout, + QLineEdit, + QPushButton, + QComboBox, + QTabWidget, + QTextEdit, + QGroupBox, + QFormLayout, +) from PyQt5.QtCore import Qt, QProcess, pyqtSignal, QObject from StreamingCommunity.run import load_search_functions @@ -10,10 +20,14 @@ from StreamingCommunity.run import load_search_functions search_functions = load_search_functions() sites = [] for alias, (_, use_for) in search_functions.items(): - sites.append({"index": len(sites), "name": alias.split("_")[0], "flag": alias[:3].upper()}) + sites.append( + {"index": len(sites), "name": alias.split("_")[0], "flag": alias[:3].upper()} + ) + class Stream(QObject): """Reindirizza l'output dello script alla GUI""" + newText = pyqtSignal(str) def write(self, text): @@ -62,8 +76,8 @@ class StreamingGUI(QMainWindow): self.site_combo = QComboBox() for site in sites: - self.site_combo.addItem(f"{site['name']}", site['index']) - self.site_combo.setItemData(site['index'], site['flag'], Qt.ToolTipRole) + self.site_combo.addItem(f"{site['name']}", site["index"]) + self.site_combo.setItemData(site["index"], site["flag"], Qt.ToolTipRole) if self.site_combo.count() > 0: self.site_combo.setCurrentIndex(0) # Qui dovresti popolare il combobox con i siti disponibili @@ -106,7 +120,6 @@ class StreamingGUI(QMainWindow): central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) - def run_script(self): if self.process is not None and self.process.state() == QProcess.Running: print("Script già in esecuzione.") @@ -118,15 +131,15 @@ class StreamingGUI(QMainWindow): # Aggiungi i termini di ricerca search_terms = self.search_terms.text() if search_terms: - args.extend(['-s', search_terms]) + args.extend(["-s", search_terms]) # Aggiungi il sito selezionato se presente site_index = self.site_combo.currentIndex() if site_index >= 0: - site_text = sites[site_index]['flag'] + site_text = sites[site_index]["flag"] # Assumo che il nome del sito sia il primo elemento del testo site_name = site_text.split()[0].upper() - args.append(f'-{site_name}') + args.append(f"-{site_name}") self.output_text.clear() print(f"Avvio script con argomenti: {' '.join(args)}") @@ -160,12 +173,12 @@ class StreamingGUI(QMainWindow): def handle_stdout(self): data = self.process.readAllStandardOutput() - stdout = bytes(data).decode('utf8', errors='replace') + stdout = bytes(data).decode("utf8", errors="replace") self.update_output(stdout) def handle_stderr(self): data = self.process.readAllStandardError() - stderr = bytes(data).decode('utf8', errors='replace') + stderr = bytes(data).decode("utf8", errors="replace") self.update_output(stderr) def process_finished(self): @@ -193,8 +206,8 @@ class StreamingGUI(QMainWindow): event.accept() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) gui = StreamingGUI() gui.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) From abd5753df9a725625202c397736aa1c8148b75ea Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 12:01:02 +0100 Subject: [PATCH 05/17] Refactor comments and docstrings for clarity and consistency --- streaming_gui.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/streaming_gui.py b/streaming_gui.py index d602e78..0456e96 100644 --- a/streaming_gui.py +++ b/streaming_gui.py @@ -26,7 +26,7 @@ for alias, (_, use_for) in search_functions.items(): class Stream(QObject): - """Reindirizza l'output dello script alla GUI""" + """Redirect script output to GUI""" newText = pyqtSignal(str) @@ -41,12 +41,12 @@ class StreamingGUI(QMainWindow): def __init__(self): super().__init__() - # Processo per eseguire lo script + # process to start script self.process = None self.init_ui() - # Reindirizzamento dell'output + # output redirect self.stdout_stream = Stream() self.stdout_stream.newText.connect(self.update_output) sys.stdout = self.stdout_stream @@ -55,18 +55,17 @@ class StreamingGUI(QMainWindow): self.setWindowTitle("StreamingCommunity GUI") self.setGeometry(100, 100, 1000, 700) - # Widget e layout principale + # widget and main layout central_widget = QWidget() main_layout = QVBoxLayout() - # Crea un widget a schede + # tabs widget tab_widget = QTabWidget() - # Scheda 1: Esecuzione script run_tab = QWidget() run_layout = QVBoxLayout() - # Gruppo per i parametri di ricerca + # search parameters group search_group = QGroupBox("Parametri di Ricerca") search_layout = QFormLayout() @@ -80,14 +79,13 @@ class StreamingGUI(QMainWindow): self.site_combo.setItemData(site["index"], site["flag"], Qt.ToolTipRole) if self.site_combo.count() > 0: self.site_combo.setCurrentIndex(0) - # Qui dovresti popolare il combobox con i siti disponibili - # Per ora lo lascio vuoto, ma andrebbe riempito dinamicamente + search_layout.addRow("Seleziona sito:", self.site_combo) search_group.setLayout(search_layout) run_layout.addWidget(search_group) - # Bottoni di controllo + # control buttons control_layout = QHBoxLayout() self.run_button = QPushButton("Esegui Script") @@ -101,7 +99,7 @@ class StreamingGUI(QMainWindow): run_layout.addLayout(control_layout) - # Area di output + # output area output_group = QGroupBox("Output") output_layout = QVBoxLayout() @@ -125,26 +123,25 @@ class StreamingGUI(QMainWindow): print("Script già in esecuzione.") return - # Costruisci la command line con gli argomenti + # build command line args args = [] - # Aggiungi i termini di ricerca + # add search terms search_terms = self.search_terms.text() if search_terms: args.extend(["-s", search_terms]) - # Aggiungi il sito selezionato se presente + # add site if present site_index = self.site_combo.currentIndex() if site_index >= 0: site_text = sites[site_index]["flag"] - # Assumo che il nome del sito sia il primo elemento del testo site_name = site_text.split()[0].upper() args.append(f"-{site_name}") self.output_text.clear() print(f"Avvio script con argomenti: {' '.join(args)}") - # Creiamo e avviamo il processo + # create and start process self.process = QProcess() self.process.readyReadStandardOutput.connect(self.handle_stdout) self.process.readyReadStandardError.connect(self.handle_stderr) @@ -155,19 +152,19 @@ class StreamingGUI(QMainWindow): self.process.start(python_executable, [script_path] + args) - # Aggiorna lo stato dei pulsanti + # Update button state self.run_button.setEnabled(False) self.stop_button.setEnabled(True) def stop_script(self): if self.process is not None and self.process.state() == QProcess.Running: self.process.terminate() - # Attendi che si chiuda (con timeout) + # Wait for close (with timeout) if not self.process.waitForFinished(3000): self.process.kill() print("Script terminato.") - # Aggiorna lo stato dei pulsanti + # Update button state self.run_button.setEnabled(True) self.stop_button.setEnabled(False) @@ -194,7 +191,7 @@ class StreamingGUI(QMainWindow): self.output_text.ensureCursorVisible() def closeEvent(self, event): - # Fermae qualsiasi processo in esecuzione prima di chiudere + # Stop all process if self.process is not None and self.process.state() == QProcess.Running: self.process.terminate() if not self.process.waitForFinished(1000): From 0d8cb8afe1a4bf38be874531c6ae8889ef9f7e96 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 12:01:20 +0100 Subject: [PATCH 06/17] Refactor comments and docstrings for clarity and consistency --- streaming_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/streaming_gui.py b/streaming_gui.py index 0456e96..e2d74f3 100644 --- a/streaming_gui.py +++ b/streaming_gui.py @@ -197,7 +197,7 @@ class StreamingGUI(QMainWindow): if not self.process.waitForFinished(1000): self.process.kill() - # Ripristina stdout + # Restore stdout sys.stdout = sys.__stdout__ event.accept() From 626946604d619392b822c0a8a8d4bc49478b9643 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 14:31:14 +0100 Subject: [PATCH 07/17] Enhance GUI with input controls for user interaction --- streaming_gui.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/streaming_gui.py b/streaming_gui.py index e2d74f3..8685e41 100644 --- a/streaming_gui.py +++ b/streaming_gui.py @@ -107,6 +107,22 @@ class StreamingGUI(QMainWindow): self.output_text.setReadOnly(True) output_layout.addWidget(self.output_text) + # add input layout + input_layout = QHBoxLayout() + self.input_field = QLineEdit() + self.input_field.setPlaceholderText("Inserisci l'indice del media...") + self.input_field.returnPressed.connect(self.send_input) + self.send_button = QPushButton("Invia") + self.send_button.clicked.connect(self.send_input) + + # initially hide input layout + self.input_field.hide() + self.send_button.hide() + + input_layout.addWidget(self.input_field) + input_layout.addWidget(self.send_button) + output_layout.addLayout(input_layout) + output_group.setLayout(output_layout) run_layout.addWidget(output_group) @@ -173,6 +189,25 @@ class StreamingGUI(QMainWindow): stdout = bytes(data).decode("utf8", errors="replace") self.update_output(stdout) + # show input controls when prompted an insert + if "Insert" in stdout: + self.input_field.show() + self.send_button.show() + self.input_field.setFocus() + + # check that output has scroll to bottom + self.output_text.verticalScrollBar().setValue( + self.output_text.verticalScrollBar().maximum() + ) + + def send_input(self): + if self.process and self.process.state() == QProcess.Running: + user_input = self.input_field.text() + "\n" + self.process.write(user_input.encode()) + self.input_field.clear() + self.input_field.hide() + self.send_button.hide() + def handle_stderr(self): data = self.process.readAllStandardError() stderr = bytes(data).decode("utf8", errors="replace") @@ -181,6 +216,8 @@ class StreamingGUI(QMainWindow): def process_finished(self): self.run_button.setEnabled(True) self.stop_button.setEnabled(False) + self.input_field.hide() + self.send_button.hide() print("Script terminato.") def update_output(self, text): From b933c34e77e204031737bb1d522a18b6c3f94a35 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 14:40:14 +0100 Subject: [PATCH 08/17] reset config --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index ef7c5c5..0c369ab 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "show_message": true, "clean_console": true, "show_trending": true, - "root_path": "../media_plex", + "root_path": "Video", "movie_folder_name": "Movie", "serie_folder_name": "Serie", "anime_folder_name": "Anime", From 7685b4e121fc8b97cdc6dab100ed323062f1f997 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 14:44:36 +0100 Subject: [PATCH 09/17] Update README with GUI setup and execution instructions --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 24638be..56628c7 100644 --- a/README.md +++ b/README.md @@ -78,22 +78,27 @@ Install directly from PyPI: pip install StreamingCommunity ``` -### Creating a Run Script - -Create `run_streaming.py`: - -```python -from StreamingCommunity.run import main - -if __name__ == "__main__": - main() -``` +### Running Script Run the script: ```bash python run_streaming.py ``` +### Running the gui + +Install requirements: + +```bash +pip install -r requirements.txt +``` + +Run the gui: + +```bash +python streaming_gui.py +``` + ### Updating via PyPI ```bash From 5d515e4e09a865c2486a300fada5da5bc5e337ac Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 15:22:02 +0100 Subject: [PATCH 10/17] Enhance GUI with results table and console toggle features --- streaming_gui.py | 223 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 180 insertions(+), 43 deletions(-) diff --git a/streaming_gui.py b/streaming_gui.py index 8685e41..59056ad 100644 --- a/streaming_gui.py +++ b/streaming_gui.py @@ -12,9 +12,13 @@ from PyQt5.QtWidgets import ( QTextEdit, QGroupBox, QFormLayout, + QTableWidget, + QTableWidgetItem, + QHeaderView, + QCheckBox, + QLabel, ) from PyQt5.QtCore import Qt, QProcess, pyqtSignal, QObject - from StreamingCommunity.run import load_search_functions search_functions = load_search_functions() @@ -40,10 +44,9 @@ class Stream(QObject): class StreamingGUI(QMainWindow): def __init__(self): super().__init__() - - # process to start script self.process = None - + self.current_context = None # 'seasons', 'episodes', or None + self.selected_season = None self.init_ui() # output redirect @@ -55,13 +58,10 @@ class StreamingGUI(QMainWindow): self.setWindowTitle("StreamingCommunity GUI") self.setGeometry(100, 100, 1000, 700) - # widget and main layout central_widget = QWidget() main_layout = QVBoxLayout() - # tabs widget tab_widget = QTabWidget() - run_tab = QWidget() run_layout = QVBoxLayout() @@ -73,7 +73,6 @@ class StreamingGUI(QMainWindow): search_layout.addRow("Termini di ricerca:", self.search_terms) self.site_combo = QComboBox() - for site in sites: self.site_combo.addItem(f"{site['name']}", site["index"]) self.site_combo.setItemData(site["index"], site["flag"], Qt.ToolTipRole) @@ -81,7 +80,6 @@ class StreamingGUI(QMainWindow): self.site_combo.setCurrentIndex(0) search_layout.addRow("Seleziona sito:", self.site_combo) - search_group.setLayout(search_layout) run_layout.addWidget(search_group) @@ -97,17 +95,39 @@ class StreamingGUI(QMainWindow): self.stop_button.setEnabled(False) control_layout.addWidget(self.stop_button) + # Console visibility checkbox + self.console_checkbox = QCheckBox("Mostra Console") + self.console_checkbox.setChecked(False) + self.console_checkbox.stateChanged.connect(self.toggle_console) + control_layout.addWidget(self.console_checkbox) + run_layout.addLayout(control_layout) + # Status label (replacing loader) + self.status_label = QLabel("Richiesta in corso...") + self.status_label.setAlignment(Qt.AlignCenter) + self.status_label.hide() + run_layout.addWidget(self.status_label) + # output area output_group = QGroupBox("Output") output_layout = QVBoxLayout() + # Table widget + self.results_table = QTableWidget() + self.results_table.setVisible(False) + self.results_table.setSelectionBehavior(QTableWidget.SelectRows) + self.results_table.setSelectionMode(QTableWidget.SingleSelection) + self.results_table.setEditTriggers(QTableWidget.NoEditTriggers) + output_layout.addWidget(self.results_table) + + # Console output self.output_text = QTextEdit() self.output_text.setReadOnly(True) + self.output_text.hide() output_layout.addWidget(self.output_text) - # add input layout + # Input controls input_layout = QHBoxLayout() self.input_field = QLineEdit() self.input_field.setPlaceholderText("Inserisci l'indice del media...") @@ -115,7 +135,6 @@ class StreamingGUI(QMainWindow): self.send_button = QPushButton("Invia") self.send_button.clicked.connect(self.send_input) - # initially hide input layout self.input_field.hide() self.send_button.hide() @@ -130,24 +149,107 @@ class StreamingGUI(QMainWindow): tab_widget.addTab(run_tab, "Esecuzione") main_layout.addWidget(tab_widget) - central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) + def toggle_console(self, state): + self.output_text.setVisible(state == Qt.Checked) + + def parse_and_show_results(self, text): + # Handle seasons list + if "Seasons found:" in text: + self.status_label.hide() + num_seasons = int(text.split("Seasons found:")[1].split()[0]) + + # Setup table for seasons + self.results_table.clear() + self.results_table.setColumnCount(2) + self.results_table.setHorizontalHeaderLabels(["Index", "Season"]) + + # Populate table with seasons + self.results_table.setRowCount(num_seasons) + for i in range(num_seasons): + index_item = QTableWidgetItem(str(i + 1)) + season_item = QTableWidgetItem(f"Stagione {i + 1}") + self.results_table.setItem(i, 0, index_item) + self.results_table.setItem(i, 1, season_item) + + # Adjust columns and show table + self.results_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeToContents + ) + self.results_table.setVisible(True) + self.results_table.itemSelectionChanged.connect(self.handle_selection) + return + + # Handle regular table output (episodes or search results) + if "┏━━━━━━━┳" in text and "└───────┴" in text: + self.status_label.hide() + + # Extract table content + table_lines = text[text.find("┏") : text.find("└")].split("\n") + + # Parse headers + headers = table_lines[1].split("┃")[1:-1] + headers = [h.strip() for h in headers] + + # Setup table + self.results_table.clear() + self.results_table.setColumnCount(len(headers)) + self.results_table.setHorizontalHeaderLabels(headers) + + # Parse rows + rows = [] + for line in table_lines[3:]: + if line.strip() and "│" in line: + cells = line.split("│")[1:-1] + cells = [cell.strip() for cell in cells] + rows.append(cells) + + # Populate table + self.results_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, cell in enumerate(row): + item = QTableWidgetItem(cell) + self.results_table.setItem(i, j, item) + + # Adjust columns + self.results_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeToContents + ) + + # Show table and connect selection + self.results_table.setVisible(True) + self.results_table.itemSelectionChanged.connect(self.handle_selection) + + def handle_selection(self): + if self.results_table.selectedItems(): + selected_row = self.results_table.currentRow() + if self.input_field.isVisible(): + if self.current_context == "seasons": + # For seasons, we add 1 because seasons are 1-based + self.input_field.setText(str(selected_row + 1)) + else: + # For episodes and search results, use the row number directly + self.input_field.setText(str(selected_row)) + def run_script(self): if self.process is not None and self.process.state() == QProcess.Running: print("Script già in esecuzione.") return + self.current_context = None + self.selected_season = None + self.results_table.setVisible(False) + self.status_label.setText("Richiesta in corso...") + self.status_label.show() + # build command line args args = [] - - # add search terms search_terms = self.search_terms.text() if search_terms: args.extend(["-s", search_terms]) - # add site if present site_index = self.site_combo.currentIndex() if site_index >= 0: site_text = sites[site_index]["flag"] @@ -157,7 +259,6 @@ class StreamingGUI(QMainWindow): self.output_text.clear() print(f"Avvio script con argomenti: {' '.join(args)}") - # create and start process self.process = QProcess() self.process.readyReadStandardOutput.connect(self.handle_stdout) self.process.readyReadStandardError.connect(self.handle_stderr) @@ -167,46 +268,76 @@ class StreamingGUI(QMainWindow): script_path = "run_streaming.py" self.process.start(python_executable, [script_path] + args) - - # Update button state self.run_button.setEnabled(False) self.stop_button.setEnabled(True) - def stop_script(self): - if self.process is not None and self.process.state() == QProcess.Running: - self.process.terminate() - # Wait for close (with timeout) - if not self.process.waitForFinished(3000): - self.process.kill() - print("Script terminato.") - - # Update button state - self.run_button.setEnabled(True) - self.stop_button.setEnabled(False) - def handle_stdout(self): data = self.process.readAllStandardOutput() stdout = bytes(data).decode("utf8", errors="replace") self.update_output(stdout) - # show input controls when prompted an insert + # Detect context + if "Seasons found:" in stdout: + self.current_context = "seasons" + self.input_field.setPlaceholderText( + "Inserisci il numero della stagione (es: 1, *, 1-2, 3-*)" + ) + elif "Episodes find:" in stdout: + self.current_context = "episodes" + self.input_field.setPlaceholderText( + "Inserisci l'indice dell'episodio (es: 1, *, 1-2, 3-*)" + ) + + # Parse and show results if available + if "┏━━━━━━━┳" in stdout or "Seasons found:" in stdout: + self.parse_and_show_results(stdout) + elif "Episodes find:" in stdout: + self.results_table.hide() + self.status_label.setText(stdout) + self.status_label.show() + + # Show input controls when needed if "Insert" in stdout: self.input_field.show() self.send_button.show() self.input_field.setFocus() - - # check that output has scroll to bottom self.output_text.verticalScrollBar().setValue( self.output_text.verticalScrollBar().maximum() ) def send_input(self): - if self.process and self.process.state() == QProcess.Running: - user_input = self.input_field.text() + "\n" - self.process.write(user_input.encode()) - self.input_field.clear() - self.input_field.hide() - self.send_button.hide() + if not self.process or self.process.state() != QProcess.Running: + return + + user_input = self.input_field.text().strip() + + # Handle season selection + if self.current_context == "seasons": + if "-" in user_input or user_input == "*": + # Multiple seasons selected, hide table + self.results_table.hide() + else: + # Single season selected, table will update with episodes + self.selected_season = user_input + + # Handle episode selection + elif self.current_context == "episodes": + if "-" in user_input or user_input == "*": + # Multiple episodes selected, hide table + self.results_table.hide() + + # Send input to process + self.process.write(f"{user_input}\n".encode()) + self.input_field.clear() + self.input_field.hide() + self.send_button.hide() + + # Show status label for next step + if self.current_context == "seasons" and not ( + "-" in user_input or user_input == "*" + ): + self.status_label.setText("Caricamento episodi...") + self.status_label.show() def handle_stderr(self): data = self.process.readAllStandardError() @@ -218,6 +349,7 @@ class StreamingGUI(QMainWindow): self.stop_button.setEnabled(False) self.input_field.hide() self.send_button.hide() + self.status_label.hide() print("Script terminato.") def update_output(self, text): @@ -227,16 +359,21 @@ class StreamingGUI(QMainWindow): self.output_text.setTextCursor(cursor) self.output_text.ensureCursorVisible() + def stop_script(self): + if self.process is not None and self.process.state() == QProcess.Running: + self.process.terminate() + if not self.process.waitForFinished(3000): + self.process.kill() + print("Script terminato.") + self.run_button.setEnabled(True) + self.stop_button.setEnabled(False) + def closeEvent(self, event): - # Stop all process if self.process is not None and self.process.state() == QProcess.Running: self.process.terminate() if not self.process.waitForFinished(1000): self.process.kill() - - # Restore stdout sys.stdout = sys.__stdout__ - event.accept() From 8b574f407fd494f2067f9e33742e2cbfecc747b9 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Feb 2025 15:42:14 +0100 Subject: [PATCH 11/17] Refactor GUI code and modularize components Splits the monolithic GUI logic into modular components, improving maintainability. Introduces `RunTab`, `ResultsTable`, and utility modules for better separation of concerns and reusability. Adjusts main entry point and refactors core functionalities to align with the new structure. --- gui/__init__.py | 0 gui/main_window.py | 39 ++++ gui/tabs/__init__.py | 0 gui/tabs/run_tab.py | 263 ++++++++++++++++++++++++ gui/utils/__init__.py | 0 gui/utils/site_manager.py | 18 ++ gui/utils/stream_redirect.py | 13 ++ gui/widgets/__init__.py | 0 gui/widgets/results_table.py | 62 ++++++ streaming_gui.py | 384 +---------------------------------- 10 files changed, 402 insertions(+), 377 deletions(-) create mode 100644 gui/__init__.py create mode 100644 gui/main_window.py create mode 100644 gui/tabs/__init__.py create mode 100644 gui/tabs/run_tab.py create mode 100644 gui/utils/__init__.py create mode 100644 gui/utils/site_manager.py create mode 100644 gui/utils/stream_redirect.py create mode 100644 gui/widgets/__init__.py create mode 100644 gui/widgets/results_table.py diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/main_window.py b/gui/main_window.py new file mode 100644 index 0000000..5fb4c13 --- /dev/null +++ b/gui/main_window.py @@ -0,0 +1,39 @@ +from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout +from PyQt5.QtCore import QProcess +import sys +from .tabs.run_tab import RunTab +from .utils.stream_redirect import Stream + + +class StreamingGUI(QMainWindow): + def __init__(self): + super().__init__() + self.process = None + self.init_ui() + self.setup_output_redirect() + + def init_ui(self): + self.setWindowTitle("StreamingCommunity GUI") + self.setGeometry(100, 100, 1000, 700) + + central_widget = QWidget() + main_layout = QVBoxLayout() + + self.run_tab = RunTab(self) + main_layout.addWidget(self.run_tab) + + central_widget.setLayout(main_layout) + self.setCentralWidget(central_widget) + + def setup_output_redirect(self): + self.stdout_stream = Stream() + self.stdout_stream.newText.connect(self.run_tab.update_output) + sys.stdout = self.stdout_stream + + def closeEvent(self, event): + if self.process and self.process.state() == QProcess.Running: + self.process.terminate() + if not self.process.waitForFinished(1000): + self.process.kill() + sys.stdout = sys.__stdout__ + event.accept() diff --git a/gui/tabs/__init__.py b/gui/tabs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py new file mode 100644 index 0000000..39ec4e5 --- /dev/null +++ b/gui/tabs/run_tab.py @@ -0,0 +1,263 @@ +from PyQt5.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QTabWidget, + QGroupBox, + QFormLayout, + QLineEdit, + QComboBox, + QPushButton, + QCheckBox, + QLabel, + QTextEdit, +) +from PyQt5.QtCore import Qt, QProcess +from ..widgets.results_table import ResultsTable +from ..utils.site_manager import sites +import sys + + +class RunTab(QTabWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.process = None + self.current_context = None + self.selected_season = None + self.init_ui() + + def init_ui(self): + run_tab = QWidget() + run_layout = QVBoxLayout() + + # Add search group + run_layout.addWidget(self.create_search_group()) + + # Add control buttons + run_layout.addLayout(self.create_control_layout()) + + # Add status label + self.status_label = QLabel("Richiesta in corso...") + self.status_label.setAlignment(Qt.AlignCenter) + self.status_label.hide() + run_layout.addWidget(self.status_label) + + # Add output group + run_layout.addWidget(self.create_output_group()) + + run_tab.setLayout(run_layout) + self.addTab(run_tab, "Esecuzione") + + def create_search_group(self): + search_group = QGroupBox("Parametri di Ricerca") + search_layout = QFormLayout() + + self.search_terms = QLineEdit() + search_layout.addRow("Termini di ricerca:", self.search_terms) + + self.site_combo = QComboBox() + for site in sites: + self.site_combo.addItem(f"{site['name']}", site["index"]) + self.site_combo.setItemData(site["index"], site["flag"], Qt.ToolTipRole) + if self.site_combo.count() > 0: + self.site_combo.setCurrentIndex(0) + + search_layout.addRow("Seleziona sito:", self.site_combo) + search_group.setLayout(search_layout) + return search_group + + def create_control_layout(self): + control_layout = QHBoxLayout() + + self.run_button = QPushButton("Esegui Script") + self.run_button.clicked.connect(self.run_script) + control_layout.addWidget(self.run_button) + + self.stop_button = QPushButton("Ferma Script") + self.stop_button.clicked.connect(self.stop_script) + self.stop_button.setEnabled(False) + control_layout.addWidget(self.stop_button) + + self.console_checkbox = QCheckBox("Mostra Console") + self.console_checkbox.setChecked(False) + self.console_checkbox.stateChanged.connect(self.toggle_console) + control_layout.addWidget(self.console_checkbox) + + return control_layout + + def create_output_group(self): + output_group = QGroupBox("Output") + output_layout = QVBoxLayout() + + self.results_table = ResultsTable() + output_layout.addWidget(self.results_table) + + self.output_text = QTextEdit() + self.output_text.setReadOnly(True) + self.output_text.hide() + output_layout.addWidget(self.output_text) + + input_layout = QHBoxLayout() + self.input_field = QLineEdit() + self.input_field.setPlaceholderText("Inserisci l'indice del media...") + self.input_field.returnPressed.connect(self.send_input) + self.send_button = QPushButton("Invia") + self.send_button.clicked.connect(self.send_input) + + self.input_field.hide() + self.send_button.hide() + + input_layout.addWidget(self.input_field) + input_layout.addWidget(self.send_button) + output_layout.addLayout(input_layout) + + output_group.setLayout(output_layout) + return output_group + + def toggle_console(self, state): + self.output_text.setVisible(state == Qt.Checked) + + def run_script(self): + if self.process is not None and self.process.state() == QProcess.Running: + print("Script già in esecuzione.") + return + + self.current_context = None + self.selected_season = None + self.results_table.setVisible(False) + self.status_label.setText("Richiesta in corso...") + self.status_label.show() + + args = [] + search_terms = self.search_terms.text() + if search_terms: + args.extend(["-s", search_terms]) + + site_index = self.site_combo.currentIndex() + if site_index >= 0: + site_text = sites[site_index]["flag"] + site_name = site_text.split()[0].upper() + args.append(f"-{site_name}") + + self.output_text.clear() + print(f"Avvio script con argomenti: {' '.join(args)}") + + self.process = QProcess() + self.process.readyReadStandardOutput.connect(self.handle_stdout) + self.process.readyReadStandardError.connect(self.handle_stderr) + self.process.finished.connect(self.process_finished) + + python_executable = sys.executable + script_path = "run_streaming.py" + + self.process.start(python_executable, [script_path] + args) + self.run_button.setEnabled(False) + self.stop_button.setEnabled(True) + + def handle_stdout(self): + data = self.process.readAllStandardOutput() + stdout = bytes(data).decode("utf8", errors="replace") + self.update_output(stdout) + + if "Seasons found:" in stdout: + self.current_context = "seasons" + self.input_field.setPlaceholderText( + "Inserisci il numero della stagione (es: 1, *, 1-2, 3-*)" + ) + elif "Episodes find:" in stdout: + self.current_context = "episodes" + self.input_field.setPlaceholderText( + "Inserisci l'indice dell'episodio (es: 1, *, 1-2, 3-*)" + ) + + if "┏━━━━━━━┳" in stdout or "Seasons found:" in stdout: + self.parse_and_show_results(stdout) + elif "Episodes find:" in stdout: + self.results_table.hide() + self.status_label.setText(stdout) + self.status_label.show() + + if "Insert" in stdout: + self.input_field.show() + self.send_button.show() + self.input_field.setFocus() + self.output_text.verticalScrollBar().setValue( + self.output_text.verticalScrollBar().maximum() + ) + + def parse_and_show_results(self, text): + if "Seasons found:" in text: + self.status_label.hide() + num_seasons = int(text.split("Seasons found:")[1].split()[0]) + self.results_table.update_with_seasons(num_seasons) + return + + if "┏━━━━━━━┳" in text and "└───────┴" in text: + self.status_label.hide() + table_lines = text[text.find("┏") : text.find("└")].split("\n") + headers = [h.strip() for h in table_lines[1].split("┃")[1:-1]] + + rows = [] + for line in table_lines[3:]: + if line.strip() and "│" in line: + cells = [cell.strip() for cell in line.split("│")[1:-1]] + rows.append(cells) + + self.results_table.update_with_results(headers, rows) + + def send_input(self): + if not self.process or self.process.state() != QProcess.Running: + return + + user_input = self.input_field.text().strip() + + if self.current_context == "seasons": + if "-" in user_input or user_input == "*": + self.results_table.hide() + else: + self.selected_season = user_input + + elif self.current_context == "episodes": + if "-" in user_input or user_input == "*": + self.results_table.hide() + + self.process.write(f"{user_input}\n".encode()) + self.input_field.clear() + self.input_field.hide() + self.send_button.hide() + + if self.current_context == "seasons" and not ( + "-" in user_input or user_input == "*" + ): + self.status_label.setText("Caricamento episodi...") + self.status_label.show() + + def handle_stderr(self): + data = self.process.readAllStandardError() + stderr = bytes(data).decode("utf8", errors="replace") + self.update_output(stderr) + + def process_finished(self): + self.run_button.setEnabled(True) + self.stop_button.setEnabled(False) + self.input_field.hide() + self.send_button.hide() + self.status_label.hide() + print("Script terminato.") + + def update_output(self, text): + cursor = self.output_text.textCursor() + cursor.movePosition(cursor.End) + cursor.insertText(text) + self.output_text.setTextCursor(cursor) + self.output_text.ensureCursorVisible() + + def stop_script(self): + if self.process is not None and self.process.state() == QProcess.Running: + self.process.terminate() + if not self.process.waitForFinished(3000): + self.process.kill() + print("Script terminato.") + self.run_button.setEnabled(True) + self.stop_button.setEnabled(False) diff --git a/gui/utils/__init__.py b/gui/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/utils/site_manager.py b/gui/utils/site_manager.py new file mode 100644 index 0000000..b1e2393 --- /dev/null +++ b/gui/utils/site_manager.py @@ -0,0 +1,18 @@ +from StreamingCommunity.run import load_search_functions + + +def get_sites(): + search_functions = load_search_functions() + sites = [] + for alias, (_, use_for) in search_functions.items(): + sites.append( + { + "index": len(sites), + "name": alias.split("_")[0], + "flag": alias[:3].upper(), + } + ) + return sites + + +sites = get_sites() diff --git a/gui/utils/stream_redirect.py b/gui/utils/stream_redirect.py new file mode 100644 index 0000000..69612c6 --- /dev/null +++ b/gui/utils/stream_redirect.py @@ -0,0 +1,13 @@ +from PyQt5.QtCore import QObject, pyqtSignal + + +class Stream(QObject): + """Redirect script output to GUI""" + + newText = pyqtSignal(str) + + def write(self, text): + self.newText.emit(str(text)) + + def flush(self): + pass diff --git a/gui/widgets/__init__.py b/gui/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/widgets/results_table.py b/gui/widgets/results_table.py new file mode 100644 index 0000000..bb059b5 --- /dev/null +++ b/gui/widgets/results_table.py @@ -0,0 +1,62 @@ +from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView +from PyQt5.QtCore import Qt + + +class ResultsTable(QTableWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setup_table() + + def setup_table(self): + self.setVisible(False) + self.setSelectionMode(QTableWidget.NoSelection) + self.setEditTriggers(QTableWidget.NoEditTriggers) + self.setFocusPolicy(Qt.NoFocus) + self.setDragDropMode(QTableWidget.NoDragDrop) + self.setContextMenuPolicy(Qt.NoContextMenu) + self.verticalHeader().setVisible(False) + + # set custom style for diabled table + self.setStyleSheet( + """ + QTableWidget:disabled { + color: white; + background-color: #323232; + } + """ + ) + self.setEnabled(False) + + def update_with_seasons(self, num_seasons): + self.clear() + self.setColumnCount(2) + self.setHorizontalHeaderLabels(["Index", "Season"]) + + self.setRowCount(num_seasons) + for i in range(num_seasons): + index_item = QTableWidgetItem(str(i + 1)) + season_item = QTableWidgetItem(f"Stagione {i + 1}") + index_item.setFlags(Qt.ItemIsEnabled) + season_item.setFlags(Qt.ItemIsEnabled) + self.setItem(i, 0, index_item) + self.setItem(i, 1, season_item) + + self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontalHeader().setEnabled(False) + self.setVisible(True) + + def update_with_results(self, headers, rows): + self.clear() + self.setColumnCount(len(headers)) + self.setHorizontalHeaderLabels(headers) + + self.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, cell in enumerate(row): + item = QTableWidgetItem(cell) + item.setFlags(Qt.ItemIsEnabled) + self.setItem(i, j, item) + + self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontalHeader().setEnabled(False) + self.setVisible(True) diff --git a/streaming_gui.py b/streaming_gui.py index 59056ad..e14270d 100644 --- a/streaming_gui.py +++ b/streaming_gui.py @@ -1,384 +1,14 @@ import sys -from PyQt5.QtWidgets import ( - QApplication, - QMainWindow, - QWidget, - QVBoxLayout, - QHBoxLayout, - QLineEdit, - QPushButton, - QComboBox, - QTabWidget, - QTextEdit, - QGroupBox, - QFormLayout, - QTableWidget, - QTableWidgetItem, - QHeaderView, - QCheckBox, - QLabel, -) -from PyQt5.QtCore import Qt, QProcess, pyqtSignal, QObject -from StreamingCommunity.run import load_search_functions +from PyQt5.QtWidgets import QApplication +from gui.main_window import StreamingGUI -search_functions = load_search_functions() -sites = [] -for alias, (_, use_for) in search_functions.items(): - sites.append( - {"index": len(sites), "name": alias.split("_")[0], "flag": alias[:3].upper()} - ) - -class Stream(QObject): - """Redirect script output to GUI""" - - newText = pyqtSignal(str) - - def write(self, text): - self.newText.emit(str(text)) - - def flush(self): - pass - - -class StreamingGUI(QMainWindow): - def __init__(self): - super().__init__() - self.process = None - self.current_context = None # 'seasons', 'episodes', or None - self.selected_season = None - self.init_ui() - - # output redirect - self.stdout_stream = Stream() - self.stdout_stream.newText.connect(self.update_output) - sys.stdout = self.stdout_stream - - def init_ui(self): - self.setWindowTitle("StreamingCommunity GUI") - self.setGeometry(100, 100, 1000, 700) - - central_widget = QWidget() - main_layout = QVBoxLayout() - - tab_widget = QTabWidget() - run_tab = QWidget() - run_layout = QVBoxLayout() - - # search parameters group - search_group = QGroupBox("Parametri di Ricerca") - search_layout = QFormLayout() - - self.search_terms = QLineEdit() - search_layout.addRow("Termini di ricerca:", self.search_terms) - - self.site_combo = QComboBox() - for site in sites: - self.site_combo.addItem(f"{site['name']}", site["index"]) - self.site_combo.setItemData(site["index"], site["flag"], Qt.ToolTipRole) - if self.site_combo.count() > 0: - self.site_combo.setCurrentIndex(0) - - search_layout.addRow("Seleziona sito:", self.site_combo) - search_group.setLayout(search_layout) - run_layout.addWidget(search_group) - - # control buttons - control_layout = QHBoxLayout() - - self.run_button = QPushButton("Esegui Script") - self.run_button.clicked.connect(self.run_script) - control_layout.addWidget(self.run_button) - - self.stop_button = QPushButton("Ferma Script") - self.stop_button.clicked.connect(self.stop_script) - self.stop_button.setEnabled(False) - control_layout.addWidget(self.stop_button) - - # Console visibility checkbox - self.console_checkbox = QCheckBox("Mostra Console") - self.console_checkbox.setChecked(False) - self.console_checkbox.stateChanged.connect(self.toggle_console) - control_layout.addWidget(self.console_checkbox) - - run_layout.addLayout(control_layout) - - # Status label (replacing loader) - self.status_label = QLabel("Richiesta in corso...") - self.status_label.setAlignment(Qt.AlignCenter) - self.status_label.hide() - run_layout.addWidget(self.status_label) - - # output area - output_group = QGroupBox("Output") - output_layout = QVBoxLayout() - - # Table widget - self.results_table = QTableWidget() - self.results_table.setVisible(False) - self.results_table.setSelectionBehavior(QTableWidget.SelectRows) - self.results_table.setSelectionMode(QTableWidget.SingleSelection) - self.results_table.setEditTriggers(QTableWidget.NoEditTriggers) - output_layout.addWidget(self.results_table) - - # Console output - self.output_text = QTextEdit() - self.output_text.setReadOnly(True) - self.output_text.hide() - output_layout.addWidget(self.output_text) - - # Input controls - input_layout = QHBoxLayout() - self.input_field = QLineEdit() - self.input_field.setPlaceholderText("Inserisci l'indice del media...") - self.input_field.returnPressed.connect(self.send_input) - self.send_button = QPushButton("Invia") - self.send_button.clicked.connect(self.send_input) - - self.input_field.hide() - self.send_button.hide() - - input_layout.addWidget(self.input_field) - input_layout.addWidget(self.send_button) - output_layout.addLayout(input_layout) - - output_group.setLayout(output_layout) - run_layout.addWidget(output_group) - - run_tab.setLayout(run_layout) - tab_widget.addTab(run_tab, "Esecuzione") - - main_layout.addWidget(tab_widget) - central_widget.setLayout(main_layout) - self.setCentralWidget(central_widget) - - def toggle_console(self, state): - self.output_text.setVisible(state == Qt.Checked) - - def parse_and_show_results(self, text): - # Handle seasons list - if "Seasons found:" in text: - self.status_label.hide() - num_seasons = int(text.split("Seasons found:")[1].split()[0]) - - # Setup table for seasons - self.results_table.clear() - self.results_table.setColumnCount(2) - self.results_table.setHorizontalHeaderLabels(["Index", "Season"]) - - # Populate table with seasons - self.results_table.setRowCount(num_seasons) - for i in range(num_seasons): - index_item = QTableWidgetItem(str(i + 1)) - season_item = QTableWidgetItem(f"Stagione {i + 1}") - self.results_table.setItem(i, 0, index_item) - self.results_table.setItem(i, 1, season_item) - - # Adjust columns and show table - self.results_table.horizontalHeader().setSectionResizeMode( - QHeaderView.ResizeToContents - ) - self.results_table.setVisible(True) - self.results_table.itemSelectionChanged.connect(self.handle_selection) - return - - # Handle regular table output (episodes or search results) - if "┏━━━━━━━┳" in text and "└───────┴" in text: - self.status_label.hide() - - # Extract table content - table_lines = text[text.find("┏") : text.find("└")].split("\n") - - # Parse headers - headers = table_lines[1].split("┃")[1:-1] - headers = [h.strip() for h in headers] - - # Setup table - self.results_table.clear() - self.results_table.setColumnCount(len(headers)) - self.results_table.setHorizontalHeaderLabels(headers) - - # Parse rows - rows = [] - for line in table_lines[3:]: - if line.strip() and "│" in line: - cells = line.split("│")[1:-1] - cells = [cell.strip() for cell in cells] - rows.append(cells) - - # Populate table - self.results_table.setRowCount(len(rows)) - for i, row in enumerate(rows): - for j, cell in enumerate(row): - item = QTableWidgetItem(cell) - self.results_table.setItem(i, j, item) - - # Adjust columns - self.results_table.horizontalHeader().setSectionResizeMode( - QHeaderView.ResizeToContents - ) - - # Show table and connect selection - self.results_table.setVisible(True) - self.results_table.itemSelectionChanged.connect(self.handle_selection) - - def handle_selection(self): - if self.results_table.selectedItems(): - selected_row = self.results_table.currentRow() - if self.input_field.isVisible(): - if self.current_context == "seasons": - # For seasons, we add 1 because seasons are 1-based - self.input_field.setText(str(selected_row + 1)) - else: - # For episodes and search results, use the row number directly - self.input_field.setText(str(selected_row)) - - def run_script(self): - if self.process is not None and self.process.state() == QProcess.Running: - print("Script già in esecuzione.") - return - - self.current_context = None - self.selected_season = None - self.results_table.setVisible(False) - self.status_label.setText("Richiesta in corso...") - self.status_label.show() - - # build command line args - args = [] - search_terms = self.search_terms.text() - if search_terms: - args.extend(["-s", search_terms]) - - site_index = self.site_combo.currentIndex() - if site_index >= 0: - site_text = sites[site_index]["flag"] - site_name = site_text.split()[0].upper() - args.append(f"-{site_name}") - - self.output_text.clear() - print(f"Avvio script con argomenti: {' '.join(args)}") - - self.process = QProcess() - self.process.readyReadStandardOutput.connect(self.handle_stdout) - self.process.readyReadStandardError.connect(self.handle_stderr) - self.process.finished.connect(self.process_finished) - - python_executable = sys.executable - script_path = "run_streaming.py" - - self.process.start(python_executable, [script_path] + args) - self.run_button.setEnabled(False) - self.stop_button.setEnabled(True) - - def handle_stdout(self): - data = self.process.readAllStandardOutput() - stdout = bytes(data).decode("utf8", errors="replace") - self.update_output(stdout) - - # Detect context - if "Seasons found:" in stdout: - self.current_context = "seasons" - self.input_field.setPlaceholderText( - "Inserisci il numero della stagione (es: 1, *, 1-2, 3-*)" - ) - elif "Episodes find:" in stdout: - self.current_context = "episodes" - self.input_field.setPlaceholderText( - "Inserisci l'indice dell'episodio (es: 1, *, 1-2, 3-*)" - ) - - # Parse and show results if available - if "┏━━━━━━━┳" in stdout or "Seasons found:" in stdout: - self.parse_and_show_results(stdout) - elif "Episodes find:" in stdout: - self.results_table.hide() - self.status_label.setText(stdout) - self.status_label.show() - - # Show input controls when needed - if "Insert" in stdout: - self.input_field.show() - self.send_button.show() - self.input_field.setFocus() - self.output_text.verticalScrollBar().setValue( - self.output_text.verticalScrollBar().maximum() - ) - - def send_input(self): - if not self.process or self.process.state() != QProcess.Running: - return - - user_input = self.input_field.text().strip() - - # Handle season selection - if self.current_context == "seasons": - if "-" in user_input or user_input == "*": - # Multiple seasons selected, hide table - self.results_table.hide() - else: - # Single season selected, table will update with episodes - self.selected_season = user_input - - # Handle episode selection - elif self.current_context == "episodes": - if "-" in user_input or user_input == "*": - # Multiple episodes selected, hide table - self.results_table.hide() - - # Send input to process - self.process.write(f"{user_input}\n".encode()) - self.input_field.clear() - self.input_field.hide() - self.send_button.hide() - - # Show status label for next step - if self.current_context == "seasons" and not ( - "-" in user_input or user_input == "*" - ): - self.status_label.setText("Caricamento episodi...") - self.status_label.show() - - def handle_stderr(self): - data = self.process.readAllStandardError() - stderr = bytes(data).decode("utf8", errors="replace") - self.update_output(stderr) - - def process_finished(self): - self.run_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.input_field.hide() - self.send_button.hide() - self.status_label.hide() - print("Script terminato.") - - def update_output(self, text): - cursor = self.output_text.textCursor() - cursor.movePosition(cursor.End) - cursor.insertText(text) - self.output_text.setTextCursor(cursor) - self.output_text.ensureCursorVisible() - - def stop_script(self): - if self.process is not None and self.process.state() == QProcess.Running: - self.process.terminate() - if not self.process.waitForFinished(3000): - self.process.kill() - print("Script terminato.") - self.run_button.setEnabled(True) - self.stop_button.setEnabled(False) - - def closeEvent(self, event): - if self.process is not None and self.process.state() == QProcess.Running: - self.process.terminate() - if not self.process.waitForFinished(1000): - self.process.kill() - sys.stdout = sys.__stdout__ - event.accept() - - -if __name__ == "__main__": +def main(): app = QApplication(sys.argv) gui = StreamingGUI() gui.show() sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() From 3053993f7a154bc95a233b6d5b57ad4fea43fd6e Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 26 Feb 2025 16:49:06 +0100 Subject: [PATCH 12/17] add windows compatibility --- gui/tabs/run_tab.py | 47 ++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py index 39ec4e5..5350f1e 100644 --- a/gui/tabs/run_tab.py +++ b/gui/tabs/run_tab.py @@ -25,6 +25,7 @@ class RunTab(QTabWidget): self.process = None self.current_context = None self.selected_season = None + self.buffer = "" self.init_ui() def init_ui(self): @@ -125,6 +126,7 @@ class RunTab(QTabWidget): self.current_context = None self.selected_season = None + self.buffer = "" self.results_table.setVisible(False) self.status_label.setText("Richiesta in corso...") self.status_label.show() @@ -160,25 +162,30 @@ class RunTab(QTabWidget): stdout = bytes(data).decode("utf8", errors="replace") self.update_output(stdout) - if "Seasons found:" in stdout: - self.current_context = "seasons" - self.input_field.setPlaceholderText( - "Inserisci il numero della stagione (es: 1, *, 1-2, 3-*)" - ) - elif "Episodes find:" in stdout: + self.buffer += stdout + + if "Episodes find:" in self.buffer: self.current_context = "episodes" self.input_field.setPlaceholderText( "Inserisci l'indice dell'episodio (es: 1, *, 1-2, 3-*)" ) + elif "Seasons found:" in self.buffer: + self.current_context = "seasons" + self.input_field.setPlaceholderText( + "Inserisci il numero della stagione (es: 1, *, 1-2, 3-*)" + ) - if "┏━━━━━━━┳" in stdout or "Seasons found:" in stdout: - self.parse_and_show_results(stdout) - elif "Episodes find:" in stdout: + if "Episodes find:" in self.buffer: self.results_table.hide() - self.status_label.setText(stdout) + self.current_context = "episodes" + text_to_show = f"Trovati {self.buffer.split("Episodes find:")[1].split()[0]} episodi!" + self.status_label.setText(text_to_show) self.status_label.show() + elif (("┏" in self.buffer or "┌" in self.buffer) and + ("┗" in self.buffer or "┛" in self.buffer or "└" in self.buffer)) or "Seasons found:" in self.buffer: + self.parse_and_show_results(self.buffer) - if "Insert" in stdout: + if "Insert" in self.buffer: self.input_field.show() self.send_button.show() self.input_field.setFocus() @@ -187,16 +194,26 @@ class RunTab(QTabWidget): ) def parse_and_show_results(self, text): - if "Seasons found:" in text: + if "Seasons found:" in text and not "Insert media index (e.g., 1)" in text: self.status_label.hide() num_seasons = int(text.split("Seasons found:")[1].split()[0]) self.results_table.update_with_seasons(num_seasons) return - if "┏━━━━━━━┳" in text and "└───────┴" in text: + if ("┏━━━━━━━┳" in text or "┌───────┬" in text) and "└───────┴" in text: + chars_to_find = [] + if "┏" in text: + chars_to_find.append("┏") + chars_to_find.append("┃") + elif "┌" in text: + chars_to_find.append("┌") + chars_to_find.append("│") + + if not chars_to_find or len(chars_to_find) == 0: + return self.status_label.hide() - table_lines = text[text.find("┏") : text.find("└")].split("\n") - headers = [h.strip() for h in table_lines[1].split("┃")[1:-1]] + table_lines = text[text.find(chars_to_find[0]) : text.find("└")].split("\n") + headers = [h.strip() for h in table_lines[1].split(chars_to_find[1])[1:-1]] rows = [] for line in table_lines[3:]: From afe913868cc89c926a3d512ed71641501f42fd11 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 26 Feb 2025 16:51:52 +0100 Subject: [PATCH 13/17] enhance console show --- gui/tabs/run_tab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py index 5350f1e..da7ee11 100644 --- a/gui/tabs/run_tab.py +++ b/gui/tabs/run_tab.py @@ -118,6 +118,7 @@ class RunTab(QTabWidget): def toggle_console(self, state): self.output_text.setVisible(state == Qt.Checked) + self.results_table.setVisible(state == Qt.Checked) def run_script(self): if self.process is not None and self.process.state() == QProcess.Running: From e47fb7162172b095d3a6873815d22006c9129e03 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Tue, 25 Mar 2025 18:28:21 +0100 Subject: [PATCH 14/17] fix --- .gitignore | 4 +++- gui/tabs/run_tab.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d7cc0b7..a856315 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ list_proxy.txt cmd.txt bot_config.json scripts.json -active_requests.json \ No newline at end of file +active_requests.json +.idea/ +.vscode/ \ No newline at end of file diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py index da7ee11..e855b7d 100644 --- a/gui/tabs/run_tab.py +++ b/gui/tabs/run_tab.py @@ -179,7 +179,7 @@ class RunTab(QTabWidget): if "Episodes find:" in self.buffer: self.results_table.hide() self.current_context = "episodes" - text_to_show = f"Trovati {self.buffer.split("Episodes find:")[1].split()[0]} episodi!" + text_to_show = f"Trovati {self.buffer.split('Episodes find:')[1].split()[0]} episodi!" self.status_label.setText(text_to_show) self.status_label.show() elif (("┏" in self.buffer or "┌" in self.buffer) and From c08de1b1a98f5f9147a20acf659b97773a622ff4 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Thu, 12 Jun 2025 16:58:14 +0200 Subject: [PATCH 15/17] Enhance command-line interface: add site specification and search terms support --- StreamingCommunity/run.py | 27 +++++++++++++++++++++++---- gui/tabs/run_tab.py | 6 +++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 82e4436..43e339d 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -241,6 +241,8 @@ def main(script_id = 0): ) parser.add_argument("script_id", nargs="?", default="unknown", help="ID dello script") + parser.add_argument('-s', '--search', default=None, help='Search terms') + parser.add_argument('--site', type=str, help='Specify site to search (e.g., streamingcommunity, eurostreaming)') # Add arguments for the main configuration parameters parser.add_argument( @@ -271,13 +273,11 @@ def main(script_id = 0): '--global', action='store_true', help='Perform a global search across multiple sites.' ) - # Add arguments for search functions - parser.add_argument('-s', '--search', default=None, help='Search terms') - # Parse command-line arguments args = parser.parse_args() - search_terms = args.search + specified_site = args.site + # Map command-line arguments to the config values config_updates = {} @@ -306,6 +306,25 @@ def main(script_id = 0): global_search(search_terms) return + # Modify the site selection logic: + if specified_site: + # Look for the specified site in the loaded functions + site_found = False + for alias, (func, use_for) in search_functions.items(): + module_name = alias.split("_")[0] + if module_name.lower() == specified_site.lower(): + run_function(func, search_terms=search_terms) + site_found = True + break + + if not site_found: + console.print(f"[red]Error: Site '{specified_site}' not found or not supported.") + if NOT_CLOSE_CONSOLE: + restart_script() + else: + force_exit() + return + # Create mappings using module indice input_to_function = {} choice_labels = {} diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py index e855b7d..3c245dd 100644 --- a/gui/tabs/run_tab.py +++ b/gui/tabs/run_tab.py @@ -139,9 +139,9 @@ class RunTab(QTabWidget): site_index = self.site_combo.currentIndex() if site_index >= 0: - site_text = sites[site_index]["flag"] - site_name = site_text.split()[0].upper() - args.append(f"-{site_name}") + # Usa il nome completo del sito invece della flag abbreviata + site_name = sites[site_index]["name"].lower() + args.extend(["--site", site_name]) self.output_text.clear() print(f"Avvio script con argomenti: {' '.join(args)}") From 1dae77893b5cc47b182856d4591d8ce71c62aecf Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Fri, 13 Jun 2025 16:37:09 +0200 Subject: [PATCH 16/17] Refactor GUI logic: improve episode handling and results table visibility --- gui/tabs/run_tab.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py index 3c245dd..662fbb0 100644 --- a/gui/tabs/run_tab.py +++ b/gui/tabs/run_tab.py @@ -118,13 +118,16 @@ class RunTab(QTabWidget): def toggle_console(self, state): self.output_text.setVisible(state == Qt.Checked) - self.results_table.setVisible(state == Qt.Checked) + # Don't hide the results table when toggling the console + if state == Qt.Checked: + self.results_table.setVisible(True) def run_script(self): if self.process is not None and self.process.state() == QProcess.Running: print("Script già in esecuzione.") return + # Reset all state variables when starting a new search self.current_context = None self.selected_season = None self.buffer = "" @@ -177,11 +180,23 @@ class RunTab(QTabWidget): ) if "Episodes find:" in self.buffer: - self.results_table.hide() - self.current_context = "episodes" - text_to_show = f"Trovati {self.buffer.split('Episodes find:')[1].split()[0]} episodi!" - self.status_label.setText(text_to_show) - self.status_label.show() + # If we've selected a season and we're now seeing episodes, update the table with episode data + if self.selected_season is not None: + # Check if we have a table to display + if (("┏" in self.buffer or "┌" in self.buffer) and + ("┗" in self.buffer or "┛" in self.buffer or "└" in self.buffer)): + self.parse_and_show_results(self.buffer) + self.status_label.hide() + else: + # We're still waiting for the table data + self.status_label.setText("Caricamento episodi...") + self.status_label.show() + else: + self.results_table.hide() + self.current_context = "episodes" + text_to_show = f"Trovati {self.buffer.split('Episodes find:')[1].split()[0]} episodi!" + self.status_label.setText(text_to_show) + self.status_label.show() elif (("┏" in self.buffer or "┌" in self.buffer) and ("┗" in self.buffer or "┛" in self.buffer or "└" in self.buffer)) or "Seasons found:" in self.buffer: self.parse_and_show_results(self.buffer) @@ -201,6 +216,12 @@ class RunTab(QTabWidget): self.results_table.update_with_seasons(num_seasons) return + # If we've selected a season and we're now seeing episodes, don't update the table with search results + # But only if we don't have a table to display yet + if self.selected_season is not None and "Episodes find:" in text and not (("┏" in text or "┌" in text) and + ("┗" in text or "┛" in text or "└" in text)): + return + if ("┏━━━━━━━┳" in text or "┌───────┬" in text) and "└───────┴" in text: chars_to_find = [] if "┏" in text: @@ -222,6 +243,8 @@ class RunTab(QTabWidget): cells = [cell.strip() for cell in line.split("│")[1:-1]] rows.append(cells) + # Make sure we're showing the table + self.results_table.setVisible(True) self.results_table.update_with_results(headers, rows) def send_input(self): @@ -235,6 +258,8 @@ class RunTab(QTabWidget): self.results_table.hide() else: self.selected_season = user_input + # Clear the buffer to ensure we don't mix old data with new episode data + self.buffer = "" elif self.current_context == "episodes": if "-" in user_input or user_input == "*": @@ -262,6 +287,8 @@ class RunTab(QTabWidget): self.input_field.hide() self.send_button.hide() self.status_label.hide() + # Reset selected_season when the process finishes + self.selected_season = None print("Script terminato.") def update_output(self, text): From a35421ccfcd39b352cc6a3cd09d878d6540e0d82 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Fri, 13 Jun 2025 17:11:45 +0200 Subject: [PATCH 17/17] Add GUI tests and test runner for StreamingCommunity application --- Test/GUI/README.md | 92 +++++++++++++++ Test/GUI/run_tests.py | 61 ++++++++++ Test/GUI/test_integration.py | 156 ++++++++++++++++++++++++++ Test/GUI/test_main_window.py | 93 ++++++++++++++++ Test/GUI/test_results_table.py | 116 +++++++++++++++++++ Test/GUI/test_run_tab.py | 185 +++++++++++++++++++++++++++++++ Test/GUI/test_site_manager.py | 67 +++++++++++ Test/GUI/test_stream_redirect.py | 60 ++++++++++ 8 files changed, 830 insertions(+) create mode 100644 Test/GUI/README.md create mode 100755 Test/GUI/run_tests.py create mode 100644 Test/GUI/test_integration.py create mode 100644 Test/GUI/test_main_window.py create mode 100644 Test/GUI/test_results_table.py create mode 100644 Test/GUI/test_run_tab.py create mode 100644 Test/GUI/test_site_manager.py create mode 100644 Test/GUI/test_stream_redirect.py diff --git a/Test/GUI/README.md b/Test/GUI/README.md new file mode 100644 index 0000000..b7aa7f5 --- /dev/null +++ b/Test/GUI/README.md @@ -0,0 +1,92 @@ +# GUI Tests + +This directory contains tests for the GUI components of the StreamingCommunity application. + +## Test Files + +- `test_main_window.py`: Tests for the main window class (`StreamingGUI`) +- `test_run_tab.py`: Tests for the run tab component (`RunTab`) +- `test_results_table.py`: Tests for the results table widget (`ResultsTable`) +- `test_stream_redirect.py`: Tests for the stdout redirection utility (`Stream`) +- `test_site_manager.py`: Tests for the site manager utility +- `test_integration.py`: Integration tests for all GUI components working together + +## Running the Tests + +### Using the Test Runner + +The easiest way to run the tests is to use the included test runner script: + +```bash +# Run all tests +cd Test/GUI +python run_tests.py + +# Run specific test files +python run_tests.py test_main_window.py test_run_tab.py + +# Run with different verbosity level (1-3) +python run_tests.py -v 3 +``` + +### Using unittest Directly + +You can also run the tests using the standard unittest module: + +```bash +# Run all GUI tests +python -m unittest discover -s Test/GUI + +# Run individual test files +python -m unittest Test/GUI/test_main_window.py +python -m unittest Test/GUI/test_run_tab.py +python -m unittest Test/GUI/test_results_table.py +python -m unittest Test/GUI/test_stream_redirect.py +python -m unittest Test/GUI/test_site_manager.py +python -m unittest Test/GUI/test_integration.py +``` + +## Test Coverage + +The tests cover the following aspects of the GUI: + +1. **Basic Initialization** + - Proper initialization of all GUI components + - Correct parent-child relationships + - Default states of widgets + +2. **UI Creation** + - Correct creation of all UI elements + - Proper layout of widgets + - Initial visibility states + +3. **Widget Interactions** + - Button clicks + - Checkbox toggles + - Table updates + +4. **Process Execution** + - Script execution + - Process termination + - Status updates + +5. **Integration** + - Components working together correctly + - Signal-slot connections + - Data flow between components + +## Adding New Tests + +When adding new GUI components, please add corresponding tests following the same pattern as the existing tests. Each test file should: + +1. Import the necessary modules +2. Create a test class that inherits from `unittest.TestCase` +3. Include setup and teardown methods +4. Test all aspects of the component's functionality +5. Include a main block to run the tests when the file is executed directly + +## Notes + +- The tests use `unittest.mock` to mock external dependencies like `QProcess` +- A `QApplication` instance is created in the `setUpClass` method to ensure that PyQt widgets can be created +- The tests clean up resources in the `tearDown` method to prevent memory leaks diff --git a/Test/GUI/run_tests.py b/Test/GUI/run_tests.py new file mode 100755 index 0000000..c563e61 --- /dev/null +++ b/Test/GUI/run_tests.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +import argparse + +def run_tests(verbosity=2, test_names=None): + """Run the GUI tests with the specified verbosity level.""" + # Create a test loader + loader = unittest.TestLoader() + + # If specific test names are provided, run only those tests + if test_names: + suite = unittest.TestSuite() + for test_name in test_names: + # Try to load the test module + try: + if test_name.endswith('.py'): + test_name = test_name[:-3] # Remove .py extension + + # If the test name is a module name, load all tests from that module + if test_name.startswith('test_'): + module = __import__(test_name) + suite.addTests(loader.loadTestsFromModule(module)) + else: + # Otherwise, assume it's a test class or method name + suite.addTests(loader.loadTestsFromName(test_name)) + except (ImportError, AttributeError) as e: + print(f"Error loading test {test_name}: {e}") + return False + else: + # Otherwise, discover all tests in the current directory + suite = loader.discover('.', pattern='test_*.py') + + # Run the tests + runner = unittest.TextTestRunner(verbosity=verbosity) + result = runner.run(suite) + + # Return True if all tests passed, False otherwise + return result.wasSuccessful() + +if __name__ == '__main__': + # Parse command line arguments + parser = argparse.ArgumentParser(description='Run GUI tests for StreamingCommunity') + parser.add_argument('-v', '--verbosity', type=int, default=2, + help='Verbosity level (1-3, default: 2)') + parser.add_argument('test_names', nargs='*', + help='Specific test modules, classes, or methods to run') + args = parser.parse_args() + + # Run the tests + success = run_tests(args.verbosity, args.test_names) + + # Exit with appropriate status code + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/Test/GUI/test_integration.py b/Test/GUI/test_integration.py new file mode 100644 index 0000000..a2c4954 --- /dev/null +++ b/Test/GUI/test_integration.py @@ -0,0 +1,156 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import patch, MagicMock +from PyQt5.QtWidgets import QApplication +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt, QProcess + +from gui.main_window import StreamingGUI +from gui.tabs.run_tab import RunTab +from gui.widgets.results_table import ResultsTable +from gui.utils.stream_redirect import Stream +from gui.utils.site_manager import sites + +class TestGUIIntegration(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of StreamingGUI for each test + self.gui = StreamingGUI() + + def tearDown(self): + # Clean up after each test + self.gui.close() + self.gui = None + + def test_main_window_has_run_tab(self): + """Test that the main window has a RunTab instance""" + self.assertIsInstance(self.gui.run_tab, RunTab) + + def test_run_tab_has_results_table(self): + """Test that the RunTab has a ResultsTable instance""" + self.assertIsInstance(self.gui.run_tab.results_table, ResultsTable) + + def test_stdout_redirection(self): + """Test that stdout is redirected to the RunTab's output_text""" + # Get the original stdout + original_stdout = sys.stdout + + # Check that stdout is now a Stream instance + self.assertIsInstance(sys.stdout, Stream) + + # Check that the Stream's newText signal is connected to the RunTab's update_output method + # We can't directly check the connections, but we can test the behavior + + # First, make the output_text visible + # Import Qt here to avoid circular import + from PyQt5.QtCore import Qt + self.gui.run_tab.toggle_console(Qt.Checked) + + # Then, print something + print("Test message") + + # Check that the message appears in the output_text + self.assertIn("Test message", self.gui.run_tab.output_text.toPlainText()) + + # Restore the original stdout for cleanup + sys.stdout = original_stdout + + @patch('gui.tabs.run_tab.QProcess') + def test_run_script_integration(self, mock_qprocess): + """Test that the run_script method integrates with other components""" + # Set up the mock QProcess + mock_process = MagicMock() + mock_qprocess.return_value = mock_process + + # Set some search terms + self.gui.run_tab.search_terms.setText("test search") + + # Call the run_script method + self.gui.run_tab.run_script() + + # Check that the process was created + self.assertIsNotNone(self.gui.run_tab.process) + + # Check that the run_button is disabled + self.assertFalse(self.gui.run_tab.run_button.isEnabled()) + + # Check that the stop_button is enabled + self.assertTrue(self.gui.run_tab.stop_button.isEnabled()) + + # Check that the process was started with the correct arguments + mock_process.start.assert_called_once() + args = mock_process.start.call_args[0][1] + self.assertIn("run_streaming.py", args[0]) + self.assertIn("-s", args[1:]) + self.assertIn("test search", args[1:]) + + def test_toggle_console_integration(self): + """Test that the toggle_console method integrates with other components""" + # Import Qt here to avoid circular import + from PyQt5.QtCore import Qt + + # Initially, the console_checkbox should be unchecked + self.assertFalse(self.gui.run_tab.console_checkbox.isChecked()) + + # Check the console_checkbox + self.gui.run_tab.console_checkbox.setChecked(True) + + # Call the toggle_console method directly with the checked state + self.gui.run_tab.toggle_console(Qt.Checked) + + # Uncheck the console_checkbox + self.gui.run_tab.console_checkbox.setChecked(False) + + # Call the toggle_console method directly with the unchecked state + self.gui.run_tab.toggle_console(Qt.Unchecked) + + def test_stop_script_integration(self): + """Test that the stop_script method integrates with other components""" + # Import QProcess here to avoid circular import + from PyQt5.QtCore import QProcess as ActualQProcess + + # Create a mock process + mock_process = MagicMock() + + # Set the process state to Running + mock_process.state.return_value = ActualQProcess.Running + + # Mock the waitForFinished method to return True + mock_process.waitForFinished.return_value = True + + # Set the process directly + self.gui.run_tab.process = mock_process + + # Enable the stop button + self.gui.run_tab.stop_button.setEnabled(True) + + # Disable the run button + self.gui.run_tab.run_button.setEnabled(False) + + # Stop the script + self.gui.run_tab.stop_script() + + # Check that the process was terminated + mock_process.terminate.assert_called_once() + + # Check that waitForFinished was called + mock_process.waitForFinished.assert_called_once_with(3000) + + # Check that the run_button is enabled + self.assertTrue(self.gui.run_tab.run_button.isEnabled()) + + # Check that the stop_button is disabled + self.assertFalse(self.gui.run_tab.stop_button.isEnabled()) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_main_window.py b/Test/GUI/test_main_window.py new file mode 100644 index 0000000..16701e9 --- /dev/null +++ b/Test/GUI/test_main_window.py @@ -0,0 +1,93 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import MagicMock +from PyQt5.QtWidgets import QApplication + +from gui.main_window import StreamingGUI + +class TestMainWindow(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of StreamingGUI for each test + self.gui = StreamingGUI() + + def tearDown(self): + # Clean up after each test + self.gui.close() + self.gui = None + + def test_init(self): + """Test that the main window initializes correctly""" + # Check that the window title is set correctly + self.assertEqual(self.gui.windowTitle(), "StreamingCommunity GUI") + + # Check that the window size is set correctly + self.assertEqual(self.gui.geometry().width(), 1000) + self.assertEqual(self.gui.geometry().height(), 700) + + # Check that the run_tab is created + self.assertIsNotNone(self.gui.run_tab) + + # Check that the stdout_stream is set up + self.assertIsNotNone(self.gui.stdout_stream) + + def test_close_event(self): + """Test that the close event handler works correctly""" + # Mock the process + self.gui.process = MagicMock() + self.gui.process.state.return_value = QApplication.instance().startingUp() # Not running + + # Create a mock event + event = MagicMock() + + # Call the close event handler + self.gui.closeEvent(event) + + # Check that the event was accepted + event.accept.assert_called_once() + + # Check that sys.stdout was restored + self.assertEqual(sys.stdout, sys.__stdout__) + + def test_close_event_with_running_process(self): + """Test that the close event handler terminates running processes""" + # Import QProcess here to avoid circular import + from PyQt5.QtCore import QProcess + + # Mock the process as running + self.gui.process = MagicMock() + self.gui.process.state.return_value = QProcess.Running + + # Mock the waitForFinished method to return True + self.gui.process.waitForFinished.return_value = True + + # Create a mock event + event = MagicMock() + + # Call the close event handler + self.gui.closeEvent(event) + + # Check that terminate was called + self.gui.process.terminate.assert_called_once() + + # Check that waitForFinished was called + self.gui.process.waitForFinished.assert_called_once_with(1000) + + # Check that the event was accepted + event.accept.assert_called_once() + + # Check that sys.stdout was restored + self.assertEqual(sys.stdout, sys.__stdout__) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_results_table.py b/Test/GUI/test_results_table.py new file mode 100644 index 0000000..3a7ac5a --- /dev/null +++ b/Test/GUI/test_results_table.py @@ -0,0 +1,116 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from PyQt5.QtWidgets import QApplication, QTableWidgetItem +from PyQt5.QtCore import Qt + +from gui.widgets.results_table import ResultsTable + +class TestResultsTable(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of ResultsTable for each test + self.results_table = ResultsTable() + + def tearDown(self): + # Clean up after each test + self.results_table.close() + self.results_table = None + + def test_init(self): + """Test that the ResultsTable initializes correctly""" + # Check that the table is hidden initially + self.assertFalse(self.results_table.isVisible()) + + # Check that the table is not editable + self.assertEqual(self.results_table.editTriggers(), ResultsTable.NoEditTriggers) + + # Check that the table has no selection + self.assertEqual(self.results_table.selectionMode(), ResultsTable.NoSelection) + + # Check that the table has no focus + self.assertEqual(self.results_table.focusPolicy(), Qt.NoFocus) + + # Check that the table has no drag and drop + self.assertEqual(self.results_table.dragDropMode(), ResultsTable.NoDragDrop) + + # Check that the table has no context menu + self.assertEqual(self.results_table.contextMenuPolicy(), Qt.NoContextMenu) + + # Check that the vertical header is hidden + self.assertFalse(self.results_table.verticalHeader().isVisible()) + + # Check that the table is disabled + self.assertFalse(self.results_table.isEnabled()) + + def test_update_with_seasons(self): + """Test that the update_with_seasons method works correctly""" + # Call the method with 3 seasons + self.results_table.update_with_seasons(3) + + # Check that the table has 2 columns + self.assertEqual(self.results_table.columnCount(), 2) + + # Check that the table has 3 rows + self.assertEqual(self.results_table.rowCount(), 3) + + # Check that the column headers are set correctly + self.assertEqual(self.results_table.horizontalHeaderItem(0).text(), "Index") + self.assertEqual(self.results_table.horizontalHeaderItem(1).text(), "Season") + + # Check that the table cells are set correctly + for i in range(3): + self.assertEqual(self.results_table.item(i, 0).text(), str(i + 1)) + self.assertEqual(self.results_table.item(i, 1).text(), f"Stagione {i + 1}") + + # Check that the items are not editable + self.assertEqual(self.results_table.item(i, 0).flags(), Qt.ItemIsEnabled) + self.assertEqual(self.results_table.item(i, 1).flags(), Qt.ItemIsEnabled) + + # Check that the table is visible + self.assertTrue(self.results_table.isVisible()) + + def test_update_with_results(self): + """Test that the update_with_results method works correctly""" + # Define headers and rows + headers = ["Column 1", "Column 2", "Column 3"] + rows = [ + ["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"], + ["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"], + ] + + # Call the method + self.results_table.update_with_results(headers, rows) + + # Check that the table has the correct number of columns + self.assertEqual(self.results_table.columnCount(), len(headers)) + + # Check that the table has the correct number of rows + self.assertEqual(self.results_table.rowCount(), len(rows)) + + # Check that the column headers are set correctly + for i, header in enumerate(headers): + self.assertEqual(self.results_table.horizontalHeaderItem(i).text(), header) + + # Check that the table cells are set correctly + for i, row in enumerate(rows): + for j, cell in enumerate(row): + self.assertEqual(self.results_table.item(i, j).text(), cell) + + # Check that the items are not editable + self.assertEqual(self.results_table.item(i, j).flags(), Qt.ItemIsEnabled) + + # Check that the table is visible + self.assertTrue(self.results_table.isVisible()) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_run_tab.py b/Test/GUI/test_run_tab.py new file mode 100644 index 0000000..aa720eb --- /dev/null +++ b/Test/GUI/test_run_tab.py @@ -0,0 +1,185 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import patch, MagicMock +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtCore import Qt, QProcess + +from gui.tabs.run_tab import RunTab +from gui.utils.site_manager import sites + +class TestRunTab(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a parent widget and a fresh instance of RunTab for each test + self.parent = QWidget() + self.run_tab = RunTab(self.parent) + + def tearDown(self): + # Clean up after each test + self.run_tab.close() + self.parent.close() + self.run_tab = None + self.parent = None + + def test_init(self): + """Test that the RunTab initializes correctly""" + # Check that the parent is set correctly + self.assertEqual(self.run_tab.parent, self.parent) + + # Check that the process is None initially + self.assertIsNone(self.run_tab.process) + + # Check that the current_context is None initially + self.assertIsNone(self.run_tab.current_context) + + # Check that the selected_season is None initially + self.assertIsNone(self.run_tab.selected_season) + + # Check that the buffer is empty initially + self.assertEqual(self.run_tab.buffer, "") + + def test_create_search_group(self): + """Test that the search group is created correctly""" + # Get the search group + search_group = self.run_tab.create_search_group() + + # Check that the search_terms QLineEdit is created + self.assertIsNotNone(self.run_tab.search_terms) + + # Check that the site_combo QComboBox is created and populated + self.assertIsNotNone(self.run_tab.site_combo) + self.assertEqual(self.run_tab.site_combo.count(), len(sites)) + + # Check that the first site is selected by default + if len(sites) > 0: + self.assertEqual(self.run_tab.site_combo.currentIndex(), 0) + + def test_create_control_layout(self): + """Test that the control layout is created correctly""" + # Get the control layout + control_layout = self.run_tab.create_control_layout() + + # Check that the run_button is created + self.assertIsNotNone(self.run_tab.run_button) + + # Check that the stop_button is created and disabled initially + self.assertIsNotNone(self.run_tab.stop_button) + self.assertFalse(self.run_tab.stop_button.isEnabled()) + + # Check that the console_checkbox is created and unchecked initially + self.assertIsNotNone(self.run_tab.console_checkbox) + self.assertFalse(self.run_tab.console_checkbox.isChecked()) + + def test_create_output_group(self): + """Test that the output group is created correctly""" + # Get the output group + output_group = self.run_tab.create_output_group() + + # Check that the results_table is created + self.assertIsNotNone(self.run_tab.results_table) + + # Check that the output_text is created and hidden initially + self.assertIsNotNone(self.run_tab.output_text) + self.assertFalse(self.run_tab.output_text.isVisible()) + + # Check that the input_field is created and hidden initially + self.assertIsNotNone(self.run_tab.input_field) + self.assertFalse(self.run_tab.input_field.isVisible()) + + # Check that the send_button is created and hidden initially + self.assertIsNotNone(self.run_tab.send_button) + self.assertFalse(self.run_tab.send_button.isVisible()) + + def test_toggle_console(self): + """Test that the toggle_console method works correctly""" + # Import Qt here to avoid circular import + from PyQt5.QtCore import Qt + + # Initially, the console_checkbox should be unchecked + self.assertFalse(self.run_tab.console_checkbox.isChecked()) + + # Check the console_checkbox + self.run_tab.console_checkbox.setChecked(True) + + # Call the toggle_console method directly with the checked state + self.run_tab.toggle_console(Qt.Checked) + + # Uncheck the console_checkbox + self.run_tab.console_checkbox.setChecked(False) + + # Call the toggle_console method directly with the unchecked state + self.run_tab.toggle_console(Qt.Unchecked) + + @patch('gui.tabs.run_tab.QProcess') + def test_run_script(self, mock_qprocess): + """Test that the run_script method works correctly""" + # Set up the mock QProcess + mock_process = MagicMock() + mock_qprocess.return_value = mock_process + + # Set some search terms + self.run_tab.search_terms.setText("test search") + + # Call the run_script method + self.run_tab.run_script() + + # Check that the process was created + self.assertIsNotNone(self.run_tab.process) + + # Check that the run_button is disabled + self.assertFalse(self.run_tab.run_button.isEnabled()) + + # Check that the stop_button is enabled + self.assertTrue(self.run_tab.stop_button.isEnabled()) + + # Check that the process was started with the correct arguments + mock_process.start.assert_called_once() + args = mock_process.start.call_args[0][1] + self.assertIn("run_streaming.py", args[0]) + self.assertIn("-s", args[1:]) + self.assertIn("test search", args[1:]) + + def test_stop_script(self): + """Test that the stop_script method works correctly""" + # Import QProcess here to avoid circular import + from PyQt5.QtCore import QProcess as ActualQProcess + + # Create a mock process + mock_process = MagicMock() + + # Set the process state to Running + mock_process.state.return_value = ActualQProcess.Running + + # Mock the waitForFinished method to return True + mock_process.waitForFinished.return_value = True + + # Set the process + self.run_tab.process = mock_process + + # Call the stop_script method + self.run_tab.stop_script() + + # Check that the process was terminated + mock_process.terminate.assert_called_once() + + # Check that waitForFinished was called + mock_process.waitForFinished.assert_called_once_with(3000) + + # Check that the run_button is enabled + self.assertTrue(self.run_tab.run_button.isEnabled()) + + # Check that the stop_button is disabled + self.assertFalse(self.run_tab.stop_button.isEnabled()) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_site_manager.py b/Test/GUI/test_site_manager.py new file mode 100644 index 0000000..1975e49 --- /dev/null +++ b/Test/GUI/test_site_manager.py @@ -0,0 +1,67 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import patch, MagicMock + +from gui.utils.site_manager import get_sites, sites + +class TestSiteManager(unittest.TestCase): + @patch('gui.utils.site_manager.load_search_functions') + def test_get_sites(self, mock_load_search_functions): + """Test that the get_sites function correctly processes search functions""" + # Set up the mock to return a dictionary of search functions + mock_load_search_functions.return_value = { + 'site1_search': (MagicMock(), 'movie'), + 'site2_search': (MagicMock(), 'tv'), + 'site3_search': (MagicMock(), 'anime'), + } + + # Call the function + result = get_sites() + + # Check that the mock was called + mock_load_search_functions.assert_called_once() + + # Check that the result is a list + self.assertIsInstance(result, list) + + # Check that the result has the correct length + self.assertEqual(len(result), 3) + + # Check that each item in the result is a dictionary with the correct keys + for i, site in enumerate(result): + self.assertIsInstance(site, dict) + self.assertIn('index', site) + self.assertIn('name', site) + self.assertIn('flag', site) + + # Check that the index is correct + self.assertEqual(site['index'], i) + + # Check that the name is correct (the part before '_' in the key) + expected_name = f'site{i+1}' + self.assertEqual(site['name'], expected_name) + + # Check that the flag is correct (first 3 characters of the key, uppercase) + expected_flag = f'SIT' + self.assertEqual(site['flag'], expected_flag) + + def test_sites_variable(self): + """Test that the sites variable is a list""" + # Check that sites is a list + self.assertIsInstance(sites, list) + + # Check that each item in sites is a dictionary with the correct keys + for site in sites: + self.assertIsInstance(site, dict) + self.assertIn('index', site) + self.assertIn('name', site) + self.assertIn('flag', site) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/Test/GUI/test_stream_redirect.py b/Test/GUI/test_stream_redirect.py new file mode 100644 index 0000000..70e1e37 --- /dev/null +++ b/Test/GUI/test_stream_redirect.py @@ -0,0 +1,60 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import MagicMock +from PyQt5.QtWidgets import QApplication + +from gui.utils.stream_redirect import Stream + +class TestStreamRedirect(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of Stream for each test + self.stream = Stream() + + # Create a mock slot to connect to the newText signal + self.mock_slot = MagicMock() + self.stream.newText.connect(self.mock_slot) + + def tearDown(self): + # Clean up after each test + self.stream.newText.disconnect(self.mock_slot) + self.stream = None + self.mock_slot = None + + def test_write(self): + """Test that the write method emits the newText signal with the correct text""" + # Call the write method + self.stream.write("Test message") + + # Check that the mock slot was called with the correct text + self.mock_slot.assert_called_once_with("Test message") + + # Call the write method again with a different message + self.stream.write("Another test") + + # Check that the mock slot was called again with the correct text + self.mock_slot.assert_called_with("Another test") + + # Check that the mock slot was called exactly twice + self.assertEqual(self.mock_slot.call_count, 2) + + def test_flush(self): + """Test that the flush method does nothing (as required by the io interface)""" + # Call the flush method + self.stream.flush() + + # Check that the mock slot was not called + self.mock_slot.assert_not_called() + +if __name__ == '__main__': + unittest.main()