diff --git a/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py index 899734b..78caa92 100644 --- a/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py @@ -55,7 +55,7 @@ max_timeout = config_manager.get_int("REQUESTS", "timeout") class M3U8_Segments: - def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True): + def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True, base_timeout=1.0, max_timeout=5.0): """ Initializes the M3U8_Segments object. @@ -81,6 +81,9 @@ class M3U8_Segments: self.queue = PriorityQueue() self.stop_event = threading.Event() self.downloaded_segments = set() + self.base_timeout = base_timeout + self.max_timeout = max_timeout + self.current_timeout = base_timeout # Stopping self.interrupt_flag = threading.Event() @@ -226,13 +229,7 @@ class M3U8_Segments: - progress_bar (tqdm): Progress counter for tracking download progress. - retries (int): The number of times to retry on failure (default is 3). - backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds). - """ - if self.interrupt_flag.is_set(): - return - - need_verify = REQUEST_VERIFY - min_segment_size = 100 # Minimum acceptable size for a TS segment in bytes - + """ for attempt in range(retries): if self.interrupt_flag.is_set(): return @@ -247,7 +244,7 @@ class M3U8_Segments: proxy = self.valid_proxy[index % len(self.valid_proxy)] logging.info(f"Use proxy: {proxy}") - with httpx.Client(proxies=proxy, verify=need_verify) as client: + with httpx.Client(proxies=proxy, verify=REQUEST_VERIFY) as client: if 'key_base_url' in self.__dict__: response = client.get( url=ts_url, @@ -265,7 +262,7 @@ class M3U8_Segments: ) else: - with httpx.Client(verify=need_verify) as client_2: + with httpx.Client(verify=REQUEST_VERIFY) as client_2: if 'key_base_url' in self.__dict__: response = client_2.get( url=ts_url, @@ -286,19 +283,12 @@ class M3U8_Segments: response.raise_for_status() segment_content = response.content content_size = len(segment_content) - - # Check if segment is too small (possibly corrupted or empty) - if content_size < min_segment_size: - raise httpx.RequestError(f"Segment {index} too small ({content_size} bytes)") - duration = time.time() - start_time # Decrypt if needed and verify decrypted content if self.decryption is not None: try: segment_content = self.decryption.decrypt(segment_content) - if len(segment_content) < min_segment_size: - raise Exception(f"Decrypted segment {index} too small ({len(segment_content)} bytes)") except Exception as e: logging.error(f"Decryption failed for segment {index}: {str(e)}") @@ -334,19 +324,20 @@ class M3U8_Segments: """ Writes segments to file with additional verification. """ + buffer = {} + expected_index = 0 + segments_written = set() + with open(self.tmp_file_path, 'wb') as f: - expected_index = 0 - buffer = {} - total_written = 0 - segments_written = set() - while not self.stop_event.is_set() or not self.queue.empty(): - if self.interrupt_flag.is_set(): break try: - index, segment_content = self.queue.get(timeout=1) + index, segment_content = self.queue.get(timeout=self.current_timeout) + + # Successful queue retrieval: reduce timeout + self.current_timeout = max(self.base_timeout, self.current_timeout / 2) # Handle failed segments if segment_content is None: @@ -357,7 +348,6 @@ class M3U8_Segments: # Write segment if it's the next expected one if index == expected_index: f.write(segment_content) - total_written += len(segment_content) segments_written.add(index) f.flush() expected_index += 1 @@ -365,26 +355,25 @@ class M3U8_Segments: # Write any buffered segments that are now in order while expected_index in buffer: next_segment = buffer.pop(expected_index) + if next_segment is not None: f.write(next_segment) - total_written += len(next_segment) segments_written.add(expected_index) f.flush() + expected_index += 1 + else: buffer[index] = segment_content except queue.Empty: + self.current_timeout = min(self.max_timeout, self.current_timeout * 1.5) + if self.stop_event.is_set(): break - continue + except Exception as e: logging.error(f"Error writing segment {index}: {str(e)}") - continue - - # Final verification - if total_written == 0: - raise Exception("No data written to file") def download_streams(self, add_desc): """ diff --git a/StreamingCommunity/Src/Lib/M3U8/estimator.py b/StreamingCommunity/Src/Lib/M3U8/estimator.py index 10c2e02..b5a5fde 100644 --- a/StreamingCommunity/Src/Lib/M3U8/estimator.py +++ b/StreamingCommunity/Src/Lib/M3U8/estimator.py @@ -65,18 +65,21 @@ class M3U8_Ts_Estimator: try: io_counters = psutil.net_io_counters() return io_counters + except Exception as e: logging.warning(f"Unable to access network I/O counters: {e}") return None while True: old_value = get_network_io() + if old_value is None: # If psutil is not available, continue with default values time.sleep(interval) continue time.sleep(interval) new_value = get_network_io() + if new_value is None: # Handle again if psutil fails in the next call time.sleep(interval) continue diff --git a/StreamingCommunity/Src/Util/os.py b/StreamingCommunity/Src/Util/os.py index b1e5707..fff6728 100644 --- a/StreamingCommunity/Src/Util/os.py +++ b/StreamingCommunity/Src/Util/os.py @@ -23,7 +23,7 @@ import httpx # Internal utilities -from StreamingCommunity.Src.Util.console import console +from StreamingCommunity.Src.Util.console import console, msg # Variable @@ -318,9 +318,6 @@ class OsSummary(): Returns: str: The version string of the executable. - - Raises: - SystemExit: If the command is not found or fails to execute. """ try: @@ -328,7 +325,7 @@ class OsSummary(): return version_output.split(" ")[2] except (FileNotFoundError, subprocess.CalledProcessError): - print(f"{command[0]} not found") + console.print(f"{command[0]} not found", style="bold red") sys.exit(0) def get_library_version(self, lib_name: str): @@ -341,7 +338,6 @@ class OsSummary(): Returns: str: The library name followed by its version, or `-not installed` if not found. """ - try: version = importlib.metadata.version(lib_name) return f"{lib_name}-{version}" @@ -349,7 +345,62 @@ class OsSummary(): except importlib.metadata.PackageNotFoundError: return f"{lib_name}-not installed" - def get_system_summary(self): + async def download_requirements(self, url: str, filename: str): + """ + Download the requirements.txt file from the specified URL if not found locally using httpx. + + Args: + url (str): The URL to download the requirements file from. + filename (str): The local filename to save the requirements file as. + """ + try: + async with httpx.AsyncClient() as client: + console.print(f"{filename} not found locally. Downloading from {url}...", style="bold yellow") + response = await client.get(url) + + if response.status_code == 200: + with open(filename, 'wb') as f: + f.write(response.content) + + console.print(f"{filename} successfully downloaded.", style="bold green") + + else: + console.print(f"Failed to download {filename}. HTTP Status code: {response.status_code}", style="bold red") + sys.exit(1) + + except Exception as e: + console.print(f"Failed to download {filename}: {e}", style="bold red") + sys.exit(1) + + def install_library(self, lib_name: str): + """ + Install a Python library using pip. + + Args: + lib_name (str): The name of the library to install. + """ + try: + console.print(f"Installing {lib_name}...", style="bold yellow") + subprocess.check_call([sys.executable, "-m", "pip", "install", lib_name]) + console.print(f"{lib_name} installed successfully!", style="bold green") + + except subprocess.CalledProcessError as e: + console.print(f"Failed to install {lib_name}: {e}", style="bold red") + sys.exit(1) + + def check_python_version(self): + """ + Check if the installed Python is the official CPython distribution. + Exits with a message if not the official version. + """ + python_implementation = platform.python_implementation() + + if python_implementation != "CPython": + console.print(f"[bold red]Warning: You are using a non-official Python distribution: {python_implementation}.[/bold red]") + console.print("Please install the official Python from [bold blue]https://www.python.org[/bold blue] and try again.", style="bold yellow") + sys.exit(0) + + async def get_system_summary(self): """ Generate a summary of the system environment. @@ -360,6 +411,9 @@ class OsSummary(): - Installed Python libraries as listed in `requirements.txt`. """ + # Check if Python is the official CPython + self.check_python_version() + # Check internet connectivity InternManager().check_internet() console.print("[bold blue]System Summary[/bold blue][white]:") @@ -381,12 +435,32 @@ class OsSummary(): console.print(f"[cyan]Exe versions[white]: [bold red]ffmpeg {ffmpeg_version}, ffprobe {ffprobe_version}[/bold red]") logging.info(f"Dependencies: ffmpeg {ffmpeg_version}, ffprobe {ffprobe_version}") - # Optional libraries versions - """optional_libraries = [line.strip() for line in open('requirements.txt', 'r', encoding='utf-8-sig')] - optional_libs_versions = [self.get_library_version(lib) for lib in optional_libraries] + # Check if requirements.txt exists, if not download it + requirements_file = 'requirements.txt' + if not os.path.exists(requirements_file): + await self.download_requirements( + 'https://raw.githubusercontent.com/Lovi-0/StreamingCommunity/refs/heads/main/requirements.txt', + requirements_file + ) + + # Read the optional libraries from the requirements file + optional_libraries = [line.strip() for line in open(requirements_file, 'r', encoding='utf-8-sig')] - console.print(f"[cyan]Libraries[white]: [bold red]{', '.join(optional_libs_versions)}[/bold red]\n") - logging.info(f"Libraries: {', '.join(optional_libs_versions)}")""" + # Check if libraries are installed and prompt to install missing ones + for lib in optional_libraries: + installed_version = self.get_library_version(lib) + if 'not installed' in installed_version: + # Prompt user to install missing library using Prompt.ask() + user_response = msg.ask(f"{lib} is not installed. Do you want to install it? (yes/no)", default="y") + + if user_response.lower().strip() in ["yes", "y"]: + self.install_library(lib) + else: + #console.print(f"[cyan]Library[white]: [bold red]{installed_version}[/bold red]") + logging.info(f"Library: {installed_version}") + + console.print(f"[cyan]Libraries[white]: [bold red]{', '.join([self.get_library_version(lib) for lib in optional_libraries])}[/bold red]\n") + logging.info(f"Libraries: {', '.join([self.get_library_version(lib) for lib in optional_libraries])}") diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 2487c2f..89a5b86 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -4,6 +4,7 @@ import os import sys import time import glob +import asyncio import logging import platform import argparse @@ -16,7 +17,7 @@ from StreamingCommunity.Src.Util.message import start_message from StreamingCommunity.Src.Util.console import console, msg from StreamingCommunity.Src.Util._jsonConfig import config_manager from StreamingCommunity.Src.Upload.update import update as git_update -from StreamingCommunity.Src.Util.os import os_summary +from StreamingCommunity.Src.Util.os import OsSummary from StreamingCommunity.Src.Lib.TMBD import tmdb from StreamingCommunity.Src.Util.logger import Logger @@ -103,7 +104,8 @@ def initialize(): start_message() # Get system info - os_summary.get_system_summary() + os_summary = OsSummary() + asyncio.run(os_summary.get_system_summary()) # Set terminal size for win 7 if platform.system() == "Windows" and "7" in platform.version():