diff --git a/.github/.domain/domains.json b/.github/.domain/domains.json index 65ea82b0..99a31ca6 100644 --- a/.github/.domain/domains.json +++ b/.github/.domain/domains.json @@ -6,10 +6,10 @@ "time_change": "2025-03-19 12:20:19" }, "cb01new": { - "domain": "digital", - "full_url": "https://cb01net.digital/", - "old_domain": "life", - "time_change": "2025-06-07 07:18:34" + "domain": "live", + "full_url": "https://cb01net.live/", + "old_domain": "digital", + "time_change": "2025-06-11 07:20:30" }, "animeunity": { "domain": "so", @@ -25,9 +25,9 @@ }, "guardaserie": { "domain": "meme", - "full_url": "http://guardaserie.meme/", + "full_url": "https://guardaserie.meme/", "old_domain": "meme", - "time_change": "2025-06-10 10:23:05" + "time_change": "2025-06-11 07:20:36" }, "ddlstreamitaly": { "domain": "co", @@ -54,9 +54,9 @@ "time_change": "2025-06-10 10:23:11" }, "altadefinizionegratis": { - "domain": "cc", - "full_url": "https://altadefinizionegratis.cc/", - "old_domain": "icu", - "time_change": "2025-06-02 10:35:25" + "domain": "club", + "full_url": "https://altadefinizionegratis.club/", + "old_domain": "cc", + "time_change": "2025-06-11 07:20:42" } } \ No newline at end of file diff --git a/.github/workflows/update_domain.yml b/.github/workflows/update_domain.yml index 534956ba..70d11969 100644 --- a/.github/workflows/update_domain.yml +++ b/.github/workflows/update_domain.yml @@ -1,5 +1,4 @@ -name: Update domains - +name: Update domains (Amend Strategy) on: schedule: - cron: "0 7-21 * * *" @@ -8,22 +7,25 @@ on: jobs: update-domains: runs-on: ubuntu-latest + permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v4 - + with: + fetch-depth: 0 # Serve per l'amend + token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.12' - + python-version: '3.12' + - name: Install dependencies run: | pip install httpx tldextract ua-generator dnspython - pip install --upgrade pip setuptools wheel - name: Configure DNS @@ -33,18 +35,24 @@ jobs: - name: Execute domain update script run: python .github/.domain/domain_update.py - - - name: Commit and push changes (if any) + + - name: Always amend last commit run: | git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' - # Check if domains.json was modified if ! git diff --quiet .github/.domain/domains.json; then + echo "📝 Changes detected - amending last commit" git add .github/.domain/domains.json - git commit -m "Automatic domain update [skip ci]" - echo "Changes committed. Attempting to push..." - git push + git commit --amend --no-edit + git push --force-with-lease origin main else - echo "No changes to .github/.domain/domains.json to commit." + echo "✅ No changes to domains.json" fi + + - name: Verify repository state + if: failure() + run: | + echo "❌ Something went wrong. Repository state:" + git log --oneline -5 + git status \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9322c75a..40c5e325 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 1a712a4d..b28af68b 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 @@ -814,9 +814,6 @@ Addon per Stremio che consente lo streaming HTTPS di film, serie, anime e TV in ### 🧩 [streamingcommunity-unofficialapi](https://github.com/Blu-Tiger/streamingcommunity-unofficialapi) API non ufficiale per accedere ai contenuti del sito italiano StreamingCommunity. -### 🎥 [stream-buddy](https://github.com/Bbalduzz/stream-buddy) -Tool per guardare o scaricare film dalla piattaforma StreamingCommunity. - # Disclaimer This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 82e44366..43e339d3 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 00000000..b7aa7f59 --- /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 00000000..c563e61f --- /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 00000000..a2c4954b --- /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 00000000..16701e99 --- /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 00000000..3a7ac5af --- /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 00000000..aa720eb8 --- /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 00000000..1975e494 --- /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 00000000..70e1e37e --- /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 00000000..e69de29b diff --git a/gui/main_window.py b/gui/main_window.py new file mode 100644 index 00000000..5fb4c135 --- /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 00000000..e69de29b diff --git a/gui/tabs/run_tab.py b/gui/tabs/run_tab.py new file mode 100644 index 00000000..662fbb00 --- /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 00000000..e69de29b diff --git a/gui/utils/site_manager.py b/gui/utils/site_manager.py new file mode 100644 index 00000000..b1e2393d --- /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 00000000..69612c62 --- /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 00000000..e69de29b diff --git a/gui/widgets/results_table.py b/gui/widgets/results_table.py new file mode 100644 index 00000000..bb059b5b --- /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 95706c7f..1f0c11c1 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 00000000..94884684 --- /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 00000000..e14270d9 --- /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()