From eb95a4e5ac71e05d4829dfad2948e18a0265f5e1 Mon Sep 17 00:00:00 2001 From: Lovi <62809003+Lovi-0@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:14:09 +0100 Subject: [PATCH] v1.9.2 --- StreamingCommunity/Api/Player/vixcloud.py | 3 +- .../Api/Template/Util/manage_ep.py | 54 ++++++++++++++----- .../Lib/Downloader/HLS/downloader.py | 35 ++++++------ .../Lib/Downloader/HLS/segments.py | 27 ++++++---- StreamingCommunity/Lib/FFmpeg/__init__.py | 2 +- StreamingCommunity/Lib/FFmpeg/util.py | 3 +- StreamingCommunity/Upload/version.py | 2 +- StreamingCommunity/Util/os.py | 12 +++-- StreamingCommunity/run.py | 5 +- setup.py | 2 +- 10 files changed, 85 insertions(+), 60 deletions(-) diff --git a/StreamingCommunity/Api/Player/vixcloud.py b/StreamingCommunity/Api/Player/vixcloud.py index 4351467..73e6da6 100644 --- a/StreamingCommunity/Api/Player/vixcloud.py +++ b/StreamingCommunity/Api/Player/vixcloud.py @@ -120,8 +120,7 @@ class VideoSource: response.raise_for_status() except Exception as e: - print("\n") - console.print(Panel("[red bold]Coming soon", title="Notification", title_align="left", border_style="yellow")) + logging.error(f"Failed to get vixcloud contente with error: {e}") sys.exit(0) # Parse response with BeautifulSoup to get content diff --git a/StreamingCommunity/Api/Template/Util/manage_ep.py b/StreamingCommunity/Api/Template/Util/manage_ep.py index 5fc3fb3..b138b04 100644 --- a/StreamingCommunity/Api/Template/Util/manage_ep.py +++ b/StreamingCommunity/Api/Template/Util/manage_ep.py @@ -117,16 +117,29 @@ def validate_selection(list_season_select: List[int], seasons_count: int) -> Lis Returns: - List[int]: Adjusted list of valid season numbers. """ + while True: + try: + + # Remove any seasons greater than the available seasons + valid_seasons = [season for season in list_season_select if 1 <= season <= seasons_count] - # Remove any seasons greater than the available seasons - valid_seasons = [season for season in list_season_select if 1 <= season <= seasons_count] + # If the list is empty, the input was completely invalid + if not valid_seasons: + logging.error(f"Invalid selection: The selected seasons are outside the available range (1-{seasons_count}). Please try again.") - # If the list is empty, the input was completely invalid - if not valid_seasons: - print() - raise ValueError(f"Invalid selection: The selected seasons are outside the available range (1-{seasons_count}).") + # Re-prompt for valid input + input_seasons = input(f"Enter valid season numbers (1-{seasons_count}): ") + list_season_select = list(map(int, input_seasons.split(','))) + continue # Re-prompt the user if the selection is invalid + + return valid_seasons # Return the valid seasons if the input is correct + + except ValueError: + logging.error("Error: Please enter valid integers separated by commas.") - return valid_seasons + # Prompt the user for valid input again + input_seasons = input(f"Enter valid season numbers (1-{seasons_count}): ") + list_season_select = list(map(int, input_seasons.split(','))) # --> for episode @@ -141,13 +154,26 @@ def validate_episode_selection(list_episode_select: List[int], episodes_count: i Returns: - List[int]: Adjusted list of valid episode numbers. """ + while True: + try: - # Remove any episodes greater than the available episodes - valid_episodes = [episode for episode in list_episode_select if 1 <= episode <= episodes_count] + # Remove any episodes greater than the available episodes + valid_episodes = [episode for episode in list_episode_select if 1 <= episode <= episodes_count] - # If the list is empty, the input was completely invalid - if not valid_episodes: - print() - raise ValueError(f"Invalid selection: The selected episodes are outside the available range (1-{episodes_count}).") + # If the list is empty, the input was completely invalid + if not valid_episodes: + logging.error(f"Invalid selection: The selected episodes are outside the available range (1-{episodes_count}). Please try again.") - return valid_episodes + # Re-prompt for valid input + input_episodes = input(f"Enter valid episode numbers (1-{episodes_count}): ") + list_episode_select = list(map(int, input_episodes.split(','))) + continue # Re-prompt the user if the selection is invalid + + return valid_episodes + + except ValueError: + logging.error("Error: Please enter valid integers separated by commas.") + + # Prompt the user for valid input again + input_episodes = input(f"Enter valid episode numbers (1-{episodes_count}): ") + list_episode_select = list(map(int, input_episodes.split(','))) \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/HLS/downloader.py b/StreamingCommunity/Lib/Downloader/HLS/downloader.py index b666057..2dd03de 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/downloader.py +++ b/StreamingCommunity/Lib/Downloader/HLS/downloader.py @@ -22,7 +22,6 @@ from StreamingCommunity.Util.os import ( # Logic class from ...FFmpeg import ( print_duration_table, - get_video_duration_s, join_video, join_audios, join_subtitle @@ -52,6 +51,7 @@ GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link') max_timeout = config_manager.get_int("REQUESTS", "timeout") headers_index = config_manager.get_dict('REQUESTS', 'user-agent') m3u8_url_fixer = M3U8_UrlFix() +list_MissingTs = [] @@ -397,7 +397,8 @@ class ContentDownloader: self.expected_real_time = video_m3u8.expected_real_time # Download the video streams and print status - video_m3u8.download_streams(f"{Colors.MAGENTA}video", "video") + info_dw = video_m3u8.download_streams(f"{Colors.MAGENTA}video", "video") + list_MissingTs.append(info_dw) # Print duration information of the downloaded video #print_duration_table(downloaded_video[0].get('path')) @@ -427,7 +428,8 @@ class ContentDownloader: audio_m3u8.get_info() # Download the audio segments and print status - audio_m3u8.download_streams(f"{Colors.MAGENTA}audio {Colors.RED}{obj_audio.get('language')}", "audio") + info_dw = audio_m3u8.download_streams(f"{Colors.MAGENTA}audio {Colors.RED}{obj_audio.get('language')}", f"audio_{obj_audio.get('language')}") + list_MissingTs.append(info_dw) # Print duration information of the downloaded audio #print_duration_table(obj_audio.get('path')) @@ -838,40 +840,33 @@ class HLS_Downloader: # Rename the output file to the desired output filename if it does not already exist if not os.path.exists(self.output_filename): + missing_ts = False + missing_info = "" # Rename the converted file to the specified output filename os.rename(out_path, self.output_filename) - # Get duration information for the output file - end_output_time = print_duration_table(self.output_filename, description=False, return_string=False) - # Calculate file size and duration for reporting formatted_size = internet_manager.format_file_size(os.path.getsize(self.output_filename)) formatted_duration = print_duration_table(self.output_filename, description=False, return_string=True) - - expected_real_seconds = dict_to_seconds(self.content_downloader.expected_real_time) - end_output_seconds = dict_to_seconds(end_output_time) - # Check if the downloaded content is complete based on expected duration - if expected_real_seconds is not None: - missing_ts = not (expected_real_seconds - 3 <= end_output_seconds <= expected_real_seconds + 3) - else: - missing_ts = "Undefined" - - # Second check for missing segments - if not missing_ts: - if get_video_duration_s(self.output_filename) < int(expected_real_seconds) - 5: + # Collect info about type missing + for item in list_MissingTs: + if int(item['nFailed']) >= 1: missing_ts = True + missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n" # Prepare the report panel content print("") panel_content = ( f"[bold green]Download completed![/bold green]\n" f"[cyan]File size: [bold red]{formatted_size}[/bold red]\n" - f"[cyan]Duration: [bold]{formatted_duration}[/bold]\n" - f"[cyan]Missing TS: [bold red]{missing_ts}[/bold red]" + f"[cyan]Duration: [bold]{formatted_duration}[/bold]" ) + if missing_ts: + panel_content += f"\n{missing_info}" + # Display the download completion message console.print(Panel( panel_content, diff --git a/StreamingCommunity/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Lib/Downloader/HLS/segments.py index 9f31644..338f4dc 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Lib/Downloader/HLS/segments.py @@ -35,7 +35,7 @@ from ...M3U8 import ( M3U8_Parser, M3U8_UrlFix ) -from ...FFmpeg.util import print_duration_table +from ...FFmpeg.util import print_duration_table, format_duration from .proxyes import main_test_proxy # Config @@ -98,6 +98,7 @@ class M3U8_Segments: # OTHER INFO self.info_maxRetry = 0 self.info_nRetry = 0 + self.info_nFailed = 0 def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes: """ @@ -133,6 +134,7 @@ class M3U8_Segments: hex_content = binascii.hexlify(response.content).decode('utf-8') byte_content = bytes.fromhex(hex_content) + #console.print(f"[cyan]Find key: [red]{hex_content}") return byte_content def parse_data(self, m3u8_content: str) -> None: @@ -332,6 +334,8 @@ class M3U8_Segments: console.log(f"[red]Final retry failed for segment: {index}") self.queue.put((index, None)) # Marker for failed segment progress_bar.update(1) + self.info_nFailed += 1 + #break sleep_time = backoff_factor * (2 ** attempt) @@ -426,10 +430,10 @@ class M3U8_Segments: AUDIO_WORKERS = DEFAULT_AUDIO_WORKERS # Differnt workers for audio and video - if "video" == str(type): + if "video" in str(type): TQDM_MAX_WORKER = VIDEO_WORKERS - if "audio" == str(type): + if "audio" in str(type): TQDM_MAX_WORKER = AUDIO_WORKERS console.print(f"[cyan]Video workers[white]: [green]{VIDEO_WORKERS} [white]| [cyan]Audio workers[white]: [green]{AUDIO_WORKERS}") @@ -526,11 +530,6 @@ class M3U8_Segments: if self.download_interrupted: console.log("[red] Download was manually stopped.") - # Optional: Delete partial download - if os.path.exists(self.tmp_file_path): - os.remove(self.tmp_file_path) - sys.exit(0) - # Clean up self.stop_event.set() writer_thread.join(timeout=30) @@ -549,12 +548,18 @@ class M3U8_Segments: file_size = os.path.getsize(self.tmp_file_path) if file_size == 0: raise Exception("Output file is empty") - - 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]Duration: {print_duration_table(self.tmp_file_path, None, True)} \n") + + # 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]." - ) \ No newline at end of file + ) + + # Info to return + return {'type': type, 'nFailed': self.info_nFailed} \ No newline at end of file diff --git a/StreamingCommunity/Lib/FFmpeg/__init__.py b/StreamingCommunity/Lib/FFmpeg/__init__.py index 463766c..8a00219 100644 --- a/StreamingCommunity/Lib/FFmpeg/__init__.py +++ b/StreamingCommunity/Lib/FFmpeg/__init__.py @@ -1,4 +1,4 @@ # 18.04.24 from .command import join_video, join_audios, join_subtitle -from .util import print_duration_table, get_video_duration_s +from .util import print_duration_table, get_video_duration diff --git a/StreamingCommunity/Lib/FFmpeg/util.py b/StreamingCommunity/Lib/FFmpeg/util.py index 354f02d..7c7828e 100644 --- a/StreamingCommunity/Lib/FFmpeg/util.py +++ b/StreamingCommunity/Lib/FFmpeg/util.py @@ -79,7 +79,7 @@ def get_video_duration(file_path: str) -> float: return 1 except Exception as e: - logging.error(f"Error get video duration: {e}") + logging.error(f"Get video duration error: {e}") sys.exit(0) @@ -144,7 +144,6 @@ def print_duration_table(file_path: str, description: str = "Duration", return_s - str: The formatted duration string if return_string is True. - dict: A dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds if return_string is False. """ - video_duration = get_video_duration(file_path) if video_duration is not None: diff --git a/StreamingCommunity/Upload/version.py b/StreamingCommunity/Upload/version.py index 54a54d2..073a917 100644 --- a/StreamingCommunity/Upload/version.py +++ b/StreamingCommunity/Upload/version.py @@ -1,5 +1,5 @@ __title__ = 'StreamingCommunity' -__version__ = '1.9.0' +__version__ = '1.9.2' __author__ = 'Lovi-0' __description__ = 'A command-line program to download film' __copyright__ = 'Copyright 2024' diff --git a/StreamingCommunity/Util/os.py b/StreamingCommunity/Util/os.py index 0077ed8..d795acc 100644 --- a/StreamingCommunity/Util/os.py +++ b/StreamingCommunity/Util/os.py @@ -309,9 +309,8 @@ class InternManager(): class OsSummary: def __init__(self): - ffmpeg_path, ffprobe_path = check_ffmpeg() - self.ffmpeg_path = ffmpeg_path - self.ffprobe_path = ffprobe_path + self.ffmpeg_path = None + self.ffprobe_path = None def get_executable_version(self, command: list): """ @@ -454,13 +453,17 @@ class OsSummary: else: command = 'which' - # Locate ffmpeg and ffprobe + # Locate ffmpeg and ffprobe from path enviroment if self.ffmpeg_path != None and "binary" not in self.ffmpeg_path: self.ffmpeg_path = self.check_ffmpeg_location([command, 'ffmpeg']) if self.ffprobe_path != None and "binary" not in self.ffprobe_path: self.ffprobe_path = self.check_ffmpeg_location([command, 'ffprobe']) + # Locate ffmpeg from bin installation + if self.ffmpeg_path is None or self.ffprobe_path is None: + self.ffmpeg_path, self.ffprobe_path = check_ffmpeg() + if self.ffmpeg_path is None or self.ffprobe_path is None: console.log("[red]Cant locate ffmpeg or ffprobe") sys.exit(0) @@ -501,7 +504,6 @@ class OsSummary: logging.info(f"Libraries: {', '.join([self.get_library_version(lib) for lib in optional_libraries])}") - # OTHER os_manager = OsManager() internet_manager = InternManager() diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 0b2c0ad..e58a47f 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -16,7 +16,7 @@ from StreamingCommunity.Util.message import start_message from StreamingCommunity.Util.console import console, msg from StreamingCommunity.Util._jsonConfig import config_manager from StreamingCommunity.Upload.update import update as git_update -from StreamingCommunity.Util.os import OsSummary +from StreamingCommunity.Util.os import os_summary from StreamingCommunity.Lib.TMBD import tmdb from StreamingCommunity.Util.logger import Logger @@ -108,7 +108,6 @@ def initialize(): start_message() # Get system info - os_summary = OsSummary() os_summary.get_system_summary() # Set terminal size for win 7 @@ -133,7 +132,7 @@ def initialize(): print() tmdb.display_trending_tv_shows() print() - + def main(): diff --git a/setup.py b/setup.py index 7029c54..1381c0d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open("requirements.txt", "r", encoding="utf-8-sig") as f: setup( name="StreamingCommunity", - version="1.9.1", + version="1.9.2", long_description=read_readme(), long_description_content_type="text/markdown", author="Lovi-0",