mirror of
https://github.com/Arrowar/StreamingCommunity.git
synced 2025-07-19 00:20:00 +00:00
Merge a35421ccfcd39b352cc6a3cd09d878d6540e0d82 into b60e1e296c66e13395f3271117711df714cc2513
This commit is contained in:
commit
b4f7bbc556
5
.gitignore
vendored
5
.gitignore
vendored
@ -52,4 +52,7 @@ cmd.txt
|
|||||||
bot_config.json
|
bot_config.json
|
||||||
scripts.json
|
scripts.json
|
||||||
active_requests.json
|
active_requests.json
|
||||||
working_proxies.json
|
domains.json
|
||||||
|
working_proxies.json
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
10
README.md
10
README.md
@ -112,17 +112,17 @@ pip install --upgrade StreamingCommunity
|
|||||||
|
|
||||||
Create a simple script (`run_streaming.py`) to launch the main application:
|
Create a simple script (`run_streaming.py`) to launch the main application:
|
||||||
|
|
||||||
```python
|
Install requirements:
|
||||||
from StreamingCommunity.run import main
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
```bash
|
||||||
main()
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the script:
|
Run the script:
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python run_streaming.py
|
python streaming_gui.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
@ -241,6 +241,8 @@ def main(script_id = 0):
|
|||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("script_id", nargs="?", default="unknown", help="ID dello script")
|
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
|
# Add arguments for the main configuration parameters
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -271,13 +273,11 @@ def main(script_id = 0):
|
|||||||
'--global', action='store_true', help='Perform a global search across multiple sites.'
|
'--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
|
# Parse command-line arguments
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
search_terms = args.search
|
search_terms = args.search
|
||||||
|
specified_site = args.site
|
||||||
|
|
||||||
# Map command-line arguments to the config values
|
# Map command-line arguments to the config values
|
||||||
config_updates = {}
|
config_updates = {}
|
||||||
|
|
||||||
@ -306,6 +306,25 @@ def main(script_id = 0):
|
|||||||
global_search(search_terms)
|
global_search(search_terms)
|
||||||
return
|
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
|
# Create mappings using module indice
|
||||||
input_to_function = {}
|
input_to_function = {}
|
||||||
choice_labels = {}
|
choice_labels = {}
|
||||||
|
92
Test/GUI/README.md
Normal file
92
Test/GUI/README.md
Normal file
@ -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
|
61
Test/GUI/run_tests.py
Executable file
61
Test/GUI/run_tests.py
Executable file
@ -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)
|
156
Test/GUI/test_integration.py
Normal file
156
Test/GUI/test_integration.py
Normal file
@ -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()
|
93
Test/GUI/test_main_window.py
Normal file
93
Test/GUI/test_main_window.py
Normal file
@ -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()
|
116
Test/GUI/test_results_table.py
Normal file
116
Test/GUI/test_results_table.py
Normal file
@ -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()
|
185
Test/GUI/test_run_tab.py
Normal file
185
Test/GUI/test_run_tab.py
Normal file
@ -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()
|
67
Test/GUI/test_site_manager.py
Normal file
67
Test/GUI/test_site_manager.py
Normal file
@ -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()
|
60
Test/GUI/test_stream_redirect.py
Normal file
60
Test/GUI/test_stream_redirect.py
Normal file
@ -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()
|
0
gui/__init__.py
Normal file
0
gui/__init__.py
Normal file
39
gui/main_window.py
Normal file
39
gui/main_window.py
Normal file
@ -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()
|
0
gui/tabs/__init__.py
Normal file
0
gui/tabs/__init__.py
Normal file
308
gui/tabs/run_tab.py
Normal file
308
gui/tabs/run_tab.py
Normal file
@ -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)
|
0
gui/utils/__init__.py
Normal file
0
gui/utils/__init__.py
Normal file
18
gui/utils/site_manager.py
Normal file
18
gui/utils/site_manager.py
Normal file
@ -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()
|
13
gui/utils/stream_redirect.py
Normal file
13
gui/utils/stream_redirect.py
Normal file
@ -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
|
0
gui/widgets/__init__.py
Normal file
0
gui/widgets/__init__.py
Normal file
62
gui/widgets/results_table.py
Normal file
62
gui/widgets/results_table.py
Normal file
@ -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)
|
@ -14,4 +14,5 @@ pycryptodomex
|
|||||||
ua-generator
|
ua-generator
|
||||||
qbittorrent-api
|
qbittorrent-api
|
||||||
pyTelegramBotAPI
|
pyTelegramBotAPI
|
||||||
beautifulsoup4
|
PyQt5
|
||||||
|
beautifulsoup4
|
||||||
|
4
run_streaming.py
Normal file
4
run_streaming.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from StreamingCommunity.run import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
14
streaming_gui.py
Normal file
14
streaming_gui.py
Normal file
@ -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()
|
Loading…
x
Reference in New Issue
Block a user