diff --git a/README.md b/README.md index 6151955..99e2105 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,9 @@ The configuration file is divided into several main sections: - `default_audio_workser`: Number of threads for audio download - `cleanup_tmp_folder`: Remove temporary .ts files after download +> [!IMPORTANT] +> Set `tqdm_use_large_bar` to `false` when using Termux or terminals with limited width to prevent display issues +
diff --git a/StreamingCommunity/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Lib/Downloader/HLS/segments.py index a469a39..ed4aab7 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Lib/Downloader/HLS/segments.py @@ -545,15 +545,14 @@ class M3U8_Segments: if file_size == 0: raise Exception("Output file is empty") - # Display additional info only if there is failed segments - if self.info_nFailed > 0: + # Display additional + if self.info_nRetry >= len(self.segments) * (1/3.33): # Get expected time ex_hours, ex_minutes, ex_seconds = format_duration(self.expected_real_time_s) ex_formatted_duration = f"[yellow]{int(ex_hours)}[red]h [yellow]{int(ex_minutes)}[red]m [yellow]{int(ex_seconds)}[red]s" console.print(f"[cyan]Max retry per URL[white]: [green]{self.info_maxRetry}[green] [white]| [cyan]Total retry done[white]: [green]{self.info_nRetry}[green] [white]| [cyan]Missing TS: [red]{self.info_nFailed} [white]| [cyan]Duration: {print_duration_table(self.tmp_file_path, None, True)} [white]| [cyan]Expected duation: {ex_formatted_duration} \n") - if self.info_nRetry >= len(self.segments) * (1/3.33): console.print("[yellow]⚠ Warning:[/yellow] Too many retries detected! Consider reducing the number of [cyan]workers[/cyan] in the [magenta]config.json[/magenta] file. This will impact [bold]performance[/bold]. \n") # Info to return diff --git a/StreamingCommunity/Lib/M3U8/estimator.py b/StreamingCommunity/Lib/M3U8/estimator.py index fc6b2fd..de063ae 100644 --- a/StreamingCommunity/Lib/M3U8/estimator.py +++ b/StreamingCommunity/Lib/M3U8/estimator.py @@ -1,3 +1,5 @@ +# 21.04.25 + import os import time import logging @@ -24,7 +26,7 @@ class M3U8_Ts_Estimator: def __init__(self, total_segments: int): """ Initialize the M3U8_Ts_Estimator object. - + Parameters: - total_segments (int): Length of total segments to download. """ @@ -33,103 +35,119 @@ class M3U8_Ts_Estimator: self.total_segments = total_segments self.lock = threading.Lock() self.speed = {"upload": "N/A", "download": "N/A"} + self.process_pid = os.getpid() # Get current process PID + logging.debug(f"Initializing M3U8_Ts_Estimator with PID: {self.process_pid}") - # Only start the speed capture thread if TQDM_USE_LARGE_BAR is True - if not TQDM_USE_LARGE_BAR: - self.speed_thread = threading.Thread(target=self.capture_speed) + # Start the speed capture thread if TQDM_USE_LARGE_BAR is True + if TQDM_USE_LARGE_BAR: + logging.debug("TQDM_USE_LARGE_BAR is True, starting speed capture thread") + self.speed_thread = threading.Thread(target=self.capture_speed, args=(1, self.process_pid)) self.speed_thread.daemon = True self.speed_thread.start() - def add_ts_file(self, size: int, size_download: int, duration: float): - """ - Add a file size to the list of file sizes. + else: + logging.debug("TQDM_USE_LARGE_BAR is False, speed capture thread not started") - Parameters: - - size (int): The size of the ts file to be added. - - size_download (int): Single size of the ts file. - - duration (float): Time to download segment file. - """ + def add_ts_file(self, size: int, size_download: int, duration: float): + """Add a file size to the list of file sizes.""" + logging.debug(f"Adding ts file - size: {size}, download size: {size_download}, duration: {duration}") + if size <= 0 or size_download <= 0 or duration <= 0: - logging.error("Invalid input values: size=%d, size_download=%d, duration=%f", size, size_download, duration) + logging.error(f"Invalid input values: size={size}, size_download={size_download}, duration={duration}") return - # Add total size bytes self.ts_file_sizes.append(size) self.now_downloaded_size += size_download + logging.debug(f"Current total downloaded size: {self.now_downloaded_size}") def capture_speed(self, interval: float = 1, pid: int = None): - """ - Capture the internet speed periodically for a specific process (identified by PID) - or the entire system if no PID is provided. - """ + """Capture the internet speed periodically.""" + logging.debug(f"Starting speed capture with interval {interval}s for PID: {pid}") def get_network_io(process=None): - """ - Get network I/O counters for a specific process or system-wide if no process is specified. - """ try: if process: - io_counters = process.io_counters() - return io_counters + + # For process-specific monitoring + connections = process.connections(kind='inet') + if connections: + io_counters = process.io_counters() + logging.debug(f"Process IO counters: {io_counters}") + return io_counters + + else: + logging.debug("No active internet connections found for process") + return None else: + + # For system-wide monitoring io_counters = psutil.net_io_counters() + logging.debug(f"System IO counters: {io_counters}") return io_counters + except Exception as e: - logging.warning(f"Unable to access network I/O counters: {e}") + logging.error(f"Error getting network IO: {str(e)}") return None - # If a PID is provided, attempt to attach to the corresponding process - process = None - if pid is not None: - try: - process = psutil.Process(pid) - except psutil.NoSuchProcess: - logging.error(f"Process with PID {pid} does not exist.") - return - except Exception as e: - logging.error(f"Failed to attach to process with PID {pid}: {e}") - return + try: + process = psutil.Process(pid) if pid else None + logging.debug(f"Monitoring process: {process}") + + except Exception as e: + logging.error(f"Failed to get process with PID {pid}: {str(e)}") + process = None + + last_upload = None + last_download = None + first_run = True + + # Buffer circolare per le ultime N misurazioni + speed_buffer_size = 3 + speed_buffer = deque(maxlen=speed_buffer_size) while True: - old_value = get_network_io(process) - - if old_value is None: # If psutil fails, continue with the next interval - time.sleep(interval) - continue - - time.sleep(interval) - new_value = get_network_io(process) - - if new_value is None: # Handle again if psutil fails in the next call - time.sleep(interval) - continue - - with self.lock: - - # Calculate speed based on process-specific counters if process is specified - if process: - upload_speed = (new_value.write_bytes - old_value.write_bytes) / interval - download_speed = (new_value.read_bytes - old_value.read_bytes) / interval + try: + io_counters = get_network_io() + + if io_counters: + current_upload = io_counters.bytes_sent + current_download = io_counters.bytes_recv - else: - # System-wide counters - upload_speed = (new_value.bytes_sent - old_value.bytes_sent) / interval - download_speed = (new_value.bytes_recv - old_value.bytes_recv) / interval + if not first_run and last_upload is not None and last_download is not None: - self.speed = { - "upload": internet_manager.format_transfer_speed(upload_speed), - "download": internet_manager.format_transfer_speed(download_speed) - } + # Calcola la velocità istantanea + upload_speed = max(0, (current_upload - last_upload) / interval) + download_speed = max(0, (current_download - last_download) / interval) + + # Aggiungi al buffer + speed_buffer.append(download_speed) + + # Calcola la media mobile delle velocità + if len(speed_buffer) > 0: + avg_download_speed = sum(speed_buffer) / len(speed_buffer) + + if avg_download_speed > 0: + with self.lock: + self.speed = { + "upload": internet_manager.format_transfer_speed(upload_speed), + "download": internet_manager.format_transfer_speed(avg_download_speed) + } + logging.debug(f"Updated speeds - Upload: {self.speed['upload']}, Download: {self.speed['download']}") + + last_upload = current_upload + last_download = current_download + first_run = False + + time.sleep(interval) + except Exception as e: + logging.error(f"Error in speed capture loop: {str(e)}") + logging.exception("Full traceback:") + logging.sleep(interval) - - def get_average_speed(self) -> float: - """ - Calculate the average internet speed. - - Returns: - float: The average internet speed in Mbps. - """ + def get_average_speed(self) -> list: + """Calculate the average internet speed.""" with self.lock: + logging.debug(f"Current speed data: {self.speed}") return self.speed['download'].split(" ") def calculate_total_size(self) -> str: @@ -156,7 +174,7 @@ class M3U8_Ts_Estimator: except Exception as e: logging.error("An unexpected error occurred: %s", e) return "Error" - + def get_downloaded_size(self) -> str: """ Get the total downloaded size formatted as a human-readable string. @@ -165,40 +183,47 @@ class M3U8_Ts_Estimator: str: The total downloaded size as a human-readable string. """ return internet_manager.format_file_size(self.now_downloaded_size) - + def update_progress_bar(self, total_downloaded: int, duration: float, progress_counter: tqdm) -> None: - """ - Updates the progress bar with information about the TS segment download. + """Updates the progress bar with download information.""" + try: + self.add_ts_file(total_downloaded * self.total_segments, total_downloaded, duration) + + downloaded_file_size_str = self.get_downloaded_size() + file_total_size = self.calculate_total_size() + + number_file_downloaded = downloaded_file_size_str.split(' ')[0] + number_file_total_size = file_total_size.split(' ')[0] + units_file_downloaded = downloaded_file_size_str.split(' ')[1] + units_file_total_size = file_total_size.split(' ')[1] + + if TQDM_USE_LARGE_BAR: + speed_data = self.get_average_speed() + logging.debug(f"Speed data for progress bar: {speed_data}") + + if len(speed_data) >= 2: + average_internet_speed = speed_data[0] + average_internet_unit = speed_data[1] - Parameters: - total_downloaded (int): The length of the content of the downloaded TS segment. - duration (float): The duration of the segment download in seconds. - progress_counter (tqdm): The tqdm object representing the progress bar. - """ - # Add the size of the downloaded segment to the estimator - self.add_ts_file(total_downloaded * self.total_segments, total_downloaded, duration) - - # Get downloaded size and total estimated size - downloaded_file_size_str = self.get_downloaded_size() - file_total_size = self.calculate_total_size() - - # Fix parameter for prefix - number_file_downloaded = downloaded_file_size_str.split(' ')[0] - number_file_total_size = file_total_size.split(' ')[0] - units_file_downloaded = downloaded_file_size_str.split(' ')[1] - units_file_total_size = file_total_size.split(' ')[1] - - # Update the progress bar's postfix - if TQDM_USE_LARGE_BAR: - average_internet_speed = self.get_average_speed()[0] - average_internet_unit = self.get_average_speed()[1] - progress_counter.set_postfix_str( - f"{Colors.WHITE}[ {Colors.GREEN}{number_file_downloaded} {Colors.WHITE}< {Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size} " - f"{Colors.WHITE}| {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit}" - ) - - else: - progress_counter.set_postfix_str( - f"{Colors.WHITE}[ {Colors.GREEN}{number_file_downloaded}{Colors.RED} {units_file_downloaded} " - f"{Colors.WHITE}| {Colors.CYAN}N/A{Colors.RED} N/A" - ) \ No newline at end of file + else: + logging.warning(f"Invalid speed data format: {speed_data}") + average_internet_speed = "N/A" + average_internet_unit = "" + + progress_str = ( + f"{Colors.WHITE}[ {Colors.GREEN}{number_file_downloaded} {Colors.WHITE}< " + f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size} " + f"{Colors.WHITE}| {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit}" + ) + + else: + progress_str = ( + f"{Colors.WHITE}[ {Colors.GREEN}{number_file_downloaded} {Colors.WHITE}< " + f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size}" + ) + + progress_counter.set_postfix_str(progress_str) + logging.debug(f"Updated progress bar: {progress_str}") + + except Exception as e: + logging.error(f"Error updating progress bar: {str(e)}") \ No newline at end of file diff --git a/StreamingCommunity/Util/ffmpeg_installer.py b/StreamingCommunity/Util/ffmpeg_installer.py index 4566af0..dc27683 100644 --- a/StreamingCommunity/Util/ffmpeg_installer.py +++ b/StreamingCommunity/Util/ffmpeg_installer.py @@ -140,6 +140,7 @@ class FFMPEGDownloader: break found_executables.append(found) else: + # Original behavior for other operating systems for executable in executables: exe_paths = glob.glob(os.path.join(self.base_dir, executable)) @@ -298,13 +299,14 @@ def check_ffmpeg() -> Tuple[Optional[str], Optional[str], Optional[str]]: # Special handling for macOS if system_platform == 'darwin': + # Common installation paths on macOS potential_paths = [ - '/usr/local/bin', # Homebrew default - '/opt/homebrew/bin', # Apple Silicon Homebrew - '/usr/bin', # System default - os.path.expanduser('~/Applications/binary'), # Custom installation - '/Applications/binary' # Custom installation + '/usr/local/bin', # Homebrew default + '/opt/homebrew/bin', # Apple Silicon Homebrew + '/usr/bin', # System default + os.path.expanduser('~/Applications/binary'), # Custom installation + '/Applications/binary' # Custom installation ] for path in potential_paths: @@ -314,6 +316,7 @@ def check_ffmpeg() -> Tuple[Optional[str], Optional[str], Optional[str]]: if (os.path.exists(ffmpeg_path) and os.path.exists(ffprobe_path) and os.access(ffmpeg_path, os.X_OK) and os.access(ffprobe_path, os.X_OK)): + # Return found executables, with ffplay being optional ffplay_path = ffplay_path if os.path.exists(ffplay_path) else None return ffmpeg_path, ffprobe_path, ffplay_path