diff --git a/.gitignore b/.gitignore index 9322c75..40c5e32 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,7 @@ cmd.txt bot_config.json scripts.json active_requests.json -working_proxies.json \ No newline at end of file +domains.json +working_proxies.json +.vscode/ +.idea/ diff --git a/README.md b/README.md index 78a0022..b28af68 100644 --- a/README.md +++ b/README.md @@ -112,17 +112,17 @@ pip install --upgrade StreamingCommunity Create a simple script (`run_streaming.py`) to launch the main application: -```python -from StreamingCommunity.run import main +Install requirements: -if __name__ == "__main__": - main() +```bash +pip install -r requirements.txt ``` Run the script: + ```bash -python run_streaming.py +python streaming_gui.py ``` ## Modules 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/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() 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..662fbb0 --- /dev/null +++ b/gui/tabs/run_tab.py @@ -0,0 +1,308 @@ +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.buffer = "" + 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) + # 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 = "" + 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: + # 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)}") + + 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) + + 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 "Episodes find:" in self.buffer: + # 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) + + if "Insert" in self.buffer: + 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 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 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: + 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(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:]: + if line.strip() and "│" in line: + 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): + 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 + # 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 == "*": + 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() + # Reset selected_season when the process finishes + self.selected_season = None + 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/requirements.txt b/requirements.txt index 95706c7..1f0c11c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ pycryptodomex ua-generator qbittorrent-api pyTelegramBotAPI -beautifulsoup4 \ No newline at end of file +PyQt5 +beautifulsoup4 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 diff --git a/streaming_gui.py b/streaming_gui.py new file mode 100644 index 0000000..e14270d --- /dev/null +++ b/streaming_gui.py @@ -0,0 +1,14 @@ +import sys +from PyQt5.QtWidgets import QApplication +from gui.main_window import StreamingGUI + + +def main(): + app = QApplication(sys.argv) + gui = StreamingGUI() + gui.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main()