From 2960b810cd9089978fa72a6cf0c9069f14fac784 Mon Sep 17 00:00:00 2001 From: Ghost <62809003+Ghost6446@users.noreply.github.com> Date: Sat, 1 Jun 2024 14:20:50 +0200 Subject: [PATCH] Fix shorter video and small bar, remove ctrl+c. --- Src/Api/Animeunity/Core/Vix_player/player.py | 4 +- .../Core/Vix_player/player.py | 3 +- Src/Api/Streamingcommunity/costant.py | 5 +- Src/Lib/FFmpeg/capture.py | 4 + Src/Lib/FFmpeg/command.py | 94 ++++++++++++++----- Src/Lib/FFmpeg/util.py | 28 +++++- Src/Lib/Hls/downloader.py | 37 +++++--- Src/Lib/Hls/segments.py | 34 ++----- Src/Lib/M3U8/parser.py | 48 ++++++---- Src/Lib/Request/my_requests.py | 6 +- Src/Util/os.py | 17 +++- config.json | 5 +- run.py | 64 ++++++------- 13 files changed, 222 insertions(+), 127 deletions(-) diff --git a/Src/Api/Animeunity/Core/Vix_player/player.py b/Src/Api/Animeunity/Core/Vix_player/player.py index 9fad807..bcac59a 100644 --- a/Src/Api/Animeunity/Core/Vix_player/player.py +++ b/Src/Api/Animeunity/Core/Vix_player/player.py @@ -1,5 +1,6 @@ # 01.03.24 +import sys import logging from urllib.parse import urljoin, urlparse, parse_qs, urlencode, urlunparse @@ -11,6 +12,7 @@ from bs4 import BeautifulSoup # Internal utilities from Src.Util.headers import get_headers +from Src.Util.console import console from Src.Util._jsonConfig import config_manager @@ -204,4 +206,4 @@ class VideoSource: new_url = m._replace(query=new_query) # Replace the old query string with the new one final_url = urlunparse(new_url) # Construct the final URL from the modified parts - return final_url \ No newline at end of file + return final_url diff --git a/Src/Api/Streamingcommunity/Core/Vix_player/player.py b/Src/Api/Streamingcommunity/Core/Vix_player/player.py index 831e216..e72e7e0 100644 --- a/Src/Api/Streamingcommunity/Core/Vix_player/player.py +++ b/Src/Api/Streamingcommunity/Core/Vix_player/player.py @@ -202,7 +202,6 @@ class VideoSource: logging.error(f"Error getting content: {e}") raise - def get_playlist(self) -> str: """ Get playlist. @@ -240,4 +239,4 @@ class VideoSource: new_url = m._replace(query=new_query) # Replace the old query string with the new one final_url = urlunparse(new_url) # Construct the final URL from the modified parts - return final_url \ No newline at end of file + return final_url diff --git a/Src/Api/Streamingcommunity/costant.py b/Src/Api/Streamingcommunity/costant.py index e1e8653..8ac2f97 100644 --- a/Src/Api/Streamingcommunity/costant.py +++ b/Src/Api/Streamingcommunity/costant.py @@ -4,4 +4,7 @@ STREAMING_FOLDER = "streamingcommunity" MOVIE_FOLDER = "Movie" SERIES_FOLDER = "Serie" -SERVER_IP = ['162.19.231.20', '162.19.255.224', '162.19.254.232', '162.19.254.230', '51.195.107.230', '162.19.255.36', '162.19.228.128', '51.195.107.7', '162.19.253.242', '141.95.0.248', '57.129.4.77', '57.129.7.85'] \ No newline at end of file +SERVER_IP = ['162.19.255.224', '162.19.255.223', '162.19.254.244', '162.19.254.232', '162.19.254.230', + '162.19.253.242', '162.19.249.48', '162.19.245.142', '162.19.231.20', '162.19.229.177', + '162.19.228.128', '162.19.228.127', '162.19.228.105', '141.95.1.32', '141.95.1.196', + '141.95.1.102', '141.95.0.50', '141.95.0.248', '135.125.237.84', '135.125.233.236'] \ No newline at end of file diff --git a/Src/Lib/FFmpeg/capture.py b/Src/Lib/FFmpeg/capture.py index fe4ddbd..561820e 100644 --- a/Src/Lib/FFmpeg/capture.py +++ b/Src/Lib/FFmpeg/capture.py @@ -40,6 +40,10 @@ def capture_output(process: subprocess.Popen, description: str) -> None: logging.info(f"FFMPEG line: {line}") + # Capture only error + if "rror" in str(line): + console.log(f"[red]FFMPEG: {str(line).strip()}") + # Check if termination is requested if terminate_flag.is_set(): break diff --git a/Src/Lib/FFmpeg/command.py b/Src/Lib/FFmpeg/command.py index af2f57b..f3b85c1 100644 --- a/Src/Lib/FFmpeg/command.py +++ b/Src/Lib/FFmpeg/command.py @@ -19,14 +19,18 @@ except: pass from Src.Util._jsonConfig import config_manager from Src.Util.os import check_file_existence, suppress_output from Src.Util.console import console -from .util import has_audio_stream, need_to_force_to_ts, check_ffmpeg_input +from .util import has_audio_stream, need_to_force_to_ts, check_ffmpeg_input, check_duration_v_a from .capture import capture_ffmpeg_real_time +from ..M3U8.parser import M3U8_Codec # Config DEBUG_MODE = config_manager.get_bool("DEFAULT", "debug") DEBUG_FFMPEG = "debug" if DEBUG_MODE else "error" -USE_CODECS = config_manager.get_bool("M3U8_CONVERSION", "use_codec") +USE_CODEC = config_manager.get_bool("M3U8_CONVERSION", "use_codec") +USE_VCODEC = config_manager.get_bool("M3U8_CONVERSION", "use_vcodec") +USE_ACODEC = config_manager.get_bool("M3U8_CONVERSION", "use_acodec") +USE_BITRATE = config_manager.get_bool("M3U8_CONVERSION", "use_bitrate") USE_GPU = config_manager.get_bool("M3U8_CONVERSION", "use_gpu") FFMPEG_DEFAULT_PRESET = config_manager.get("M3U8_CONVERSION", "default_preset") CHECK_OUTPUT_CONVERSION = config_manager.get_bool("M3U8_CONVERSION", "check_output_after_ffmpeg") @@ -263,7 +267,7 @@ def __transcode_with_subtitles(video: str, subtitles_list: List[Dict[str, str]], # --> v 1.1 (new) -def join_video(video_path: str, out_path: str, vcodec: str = None, acodec: str = None, bitrate: str = None): +def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None): """ Joins single ts video file to mp4 @@ -281,12 +285,12 @@ def join_video(video_path: str, out_path: str, vcodec: str = None, acodec: str = logging.error("Missing input video for ffmpeg conversion.") sys.exit(0) - # Start command ffmpeg_cmd = ['ffmpeg'] # Enabled the use of gpu - ffmpeg_cmd.extend(['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda']) + if USE_GPU: + ffmpeg_cmd.extend(['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda']) # Add mpegts to force to detect input file as ts file if need_to_force_to_ts(video_path): @@ -294,15 +298,30 @@ def join_video(video_path: str, out_path: str, vcodec: str = None, acodec: str = ffmpeg_cmd.extend(['-f', 'mpegts']) vcodec = "libx264" - # Insert input video path ffmpeg_cmd.extend(['-i', video_path]) # Add output args - if USE_CODECS: - if vcodec: ffmpeg_cmd.extend(['-c:v', vcodec]) - if acodec: ffmpeg_cmd.extend(['-c:a', acodec]) - if bitrate: ffmpeg_cmd.extend(['-b:a', str(bitrate)]) + if USE_CODEC: + if USE_VCODEC: + if codec.video_codec_name: + if not USE_GPU: + ffmpeg_cmd.extend(['-c:v', codec.video_codec_name]) + else: + ffmpeg_cmd.extend(['-c:v', 'h264_nvenc']) + else: + console.log("[red]Cant find vcodec for 'join_audios'") + + if USE_ACODEC: + if codec.audio_codec_name: + ffmpeg_cmd.extend(['-c:a', codec.audio_codec_name]) + else: + console.log("[red]Cant find acodec for 'join_audios'") + + if USE_BITRATE: + ffmpeg_cmd.extend(['-b:v', f'{codec.video_bitrate // 1000}k']) + ffmpeg_cmd.extend(['-b:a', f'{codec.audio_bitrate // 1000}k']) + else: ffmpeg_cmd.extend(['-c', 'copy']) @@ -312,12 +331,10 @@ def join_video(video_path: str, out_path: str, vcodec: str = None, acodec: str = else: ffmpeg_cmd.extend(['-preset', 'fast']) - # Overwrite ffmpeg_cmd += [out_path, "-y"] logging.info(f"FFmpeg command: {ffmpeg_cmd}") - # Run join if DEBUG_MODE: subprocess.run(ffmpeg_cmd, check=True) @@ -333,8 +350,6 @@ def join_video(video_path: str, out_path: str, vcodec: str = None, acodec: str = capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video") print() - - # Check file output if CHECK_OUTPUT_CONVERSION: console.log("[red]Check output ffmpeg") @@ -347,7 +362,7 @@ def join_video(video_path: str, out_path: str, vcodec: str = None, acodec: str = sys.exit(0) -def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: str): +def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: str, codec: M3U8_Codec = None): """ Joins audio tracks with a video file using FFmpeg. @@ -362,9 +377,17 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s logging.error("Missing input video for ffmpeg conversion.") sys.exit(0) + video_audio_same_duration = check_duration_v_a(video_path, audio_tracks[0].get('path')) # Start command - ffmpeg_cmd = ['ffmpeg', '-i', video_path] + ffmpeg_cmd = ['ffmpeg'] + + # Enabled the use of gpu + if USE_GPU: + ffmpeg_cmd.extend(['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda']) + + # Insert input video path + ffmpeg_cmd.extend(['-i', video_path]) # Add audio tracks as input for i, audio_track in enumerate(audio_tracks): @@ -373,7 +396,6 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s else: logging.error(f"Skip audio join: {audio_track.get('path')} dont exist") - # Map the video and audio streams ffmpeg_cmd.append('-map') ffmpeg_cmd.append('0:v') # Map video stream from the first input (video_path) @@ -382,18 +404,45 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s ffmpeg_cmd.append('-map') ffmpeg_cmd.append(f'{i}:a') # Map audio streams from subsequent inputs - # Add output args - if USE_CODECS: - ffmpeg_cmd.extend(['-c:v', 'copy', '-c:a', 'copy']) + if USE_CODEC: + if USE_VCODEC: + if codec.video_codec_name: + if not USE_GPU: + ffmpeg_cmd.extend(['-c:v', codec.video_codec_name]) + else: + ffmpeg_cmd.extend(['-c:v', 'h264_nvenc']) + else: + console.log("[red]Cant find vcodec for 'join_audios'") + + if USE_ACODEC: + if codec.audio_codec_name: + ffmpeg_cmd.extend(['-c:a', codec.audio_codec_name]) + else: + console.log("[red]Cant find acodec for 'join_audios'") + + if USE_BITRATE: + ffmpeg_cmd.extend(['-b:v', f'{codec.video_bitrate // 1000}k']) + ffmpeg_cmd.extend(['-b:a', f'{codec.audio_bitrate // 1000}k']) + else: ffmpeg_cmd.extend(['-c', 'copy']) + # Ultrafast preset always or fast for gpu + if not USE_GPU: + ffmpeg_cmd.extend(['-preset', FFMPEG_DEFAULT_PRESET]) + else: + ffmpeg_cmd.extend(['-preset', 'fast']) + + # Use shortest input path for video and audios + if not video_audio_same_duration: + console.log("[red]Use shortest input.") + ffmpeg_cmd.extend(['-shortest', '-strict', 'experimental']) + # Overwrite ffmpeg_cmd += [out_path, "-y"] logging.info(f"FFmpeg command: {ffmpeg_cmd}") - # Run join if DEBUG_MODE: subprocess.run(ffmpeg_cmd, check=True) @@ -409,7 +458,6 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio") print() - # Check file output if CHECK_OUTPUT_CONVERSION: console.log("[red]Check output ffmpeg") @@ -456,7 +504,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat ffmpeg_cmd += ["-metadata:s:s:{}".format(idx), "title={}".format(subtitle['name'])] # Add output args - if USE_CODECS: + if USE_CODEC: ffmpeg_cmd.extend(['-c:v', 'copy', '-c:a', 'copy', '-c:s', 'mov_text']) else: ffmpeg_cmd.extend(['-c', 'copy', '-c:s', 'mov_text']) diff --git a/Src/Lib/FFmpeg/util.py b/Src/Lib/FFmpeg/util.py index 4e03244..10ef22c 100644 --- a/Src/Lib/FFmpeg/util.py +++ b/Src/Lib/FFmpeg/util.py @@ -92,7 +92,7 @@ def format_duration(seconds: float) -> Tuple[int, int, int]: return int(hours), int(minutes), int(seconds) -def print_duration_table(file_path: str) -> None: +def print_duration_table(file_path: str, show = True) -> None: """ Print duration of a video file in hours, minutes, and seconds. @@ -104,7 +104,10 @@ def print_duration_table(file_path: str) -> None: if video_duration is not None: hours, minutes, seconds = format_duration(video_duration) - console.log(f"[cyan]Duration for [white]([green]{os.path.basename(file_path)}[white]): [yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s") + if show: + console.print(f"[cyan]Duration for [white]([green]{os.path.basename(file_path)}[white]): [yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s") + else: + return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s" def get_ffprobe_info(file_path): @@ -210,3 +213,24 @@ def check_ffmpeg_input(input_file): except Exception as e: logging.error(f"An unexpected error occurred: {e}") return False + +def check_duration_v_a(video_path, audio_path): + """ + Check if the duration of the video and audio matches. + + Args: + - video_path (str): Path to the video file. + - audio_path (str): Path to the audio file. + + Returns: + - bool: True if the duration of the video and audio matches, False otherwise. + """ + + # Ottieni la durata del video + video_duration = get_video_duration(video_path) + + # Ottieni la durata dell'audio + audio_duration = get_video_duration(audio_path) + + # Verifica se le durate corrispondono + return video_duration == audio_duration \ No newline at end of file diff --git a/Src/Lib/Hls/downloader.py b/Src/Lib/Hls/downloader.py index a3fea2c..9ef3ff3 100644 --- a/Src/Lib/Hls/downloader.py +++ b/Src/Lib/Hls/downloader.py @@ -178,7 +178,7 @@ class Downloader(): # Check if there is some audios, else disable download if self.list_available_audio != None: - console.log(f"[cyan]Find audios [white]=> [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}") + console.print(f"[cyan]Find audios [white]=> [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}") else: console.log("[red]Cant find a list of audios") @@ -191,7 +191,7 @@ class Downloader(): # Check if there is some subtitles, else disable download if self.list_available_subtitles != None: - console.log(f"[cyan]Find subtitles [white]=> [red]{[obj_sub.get('language') for obj_sub in self.list_available_subtitles]}") + console.print(f"[cyan]Find subtitles [white]=> [red]{[obj_sub.get('language') for obj_sub in self.list_available_subtitles]}") else: console.log("[red]Cant find a list of audios") @@ -208,7 +208,7 @@ class Downloader(): logging.info(f"M3U8 index select: {self.m3u8_index}, with resolution: {video_res}") # Get URI of the best quality and codecs parameters - console.log(f"[cyan]Find resolution [white]=> [red]{sorted(list_available_resolution, reverse=True)}") + console.print(f"[cyan]Find resolution [white]=> [red]{sorted(list_available_resolution, reverse=True)}") # Fix URL if it is not complete with http:\\site_name.domain\... if "http" not in self.m3u8_index: @@ -219,7 +219,7 @@ class Downloader(): # Check if a valid HTTPS URL is obtained if self.m3u8_index is not None and "https" in self.m3u8_index: - console.log(f"[cyan]Found m3u8 index [white]=> [red]{self.m3u8_index}") + console.print(f"[cyan]Found m3u8 index [white]=> [red]{self.m3u8_index}") else: logging.error("[download_m3u8] Can't find a valid m3u8 index") raise @@ -229,7 +229,8 @@ class Downloader(): logging.info(f"Find codec: {self.codec}") if self.codec is not None: - console.log(f"[cyan]Find codec [white]=> ([green]'v'[white]: [yellow]{self.codec.video_codec_name}[white], [green]'a'[white]: [yellow]{self.codec.audio_codec_name}[white], [green]'b'[white]: [yellow]{self.codec.bandwidth})") + console.print(f"[cyan]Find codec [white]=> ([green]'v'[white]: [yellow]{self.codec.video_codec_name}[white] ([green]b[white]: [yellow]{self.codec.video_bitrate // 1000}k[white]), [green]'a'[white]: [yellow]{self.codec.audio_codec_name}[white] ([green]b[white]: [yellow]{self.codec.audio_bitrate // 1000}k[white]))") + def __donwload_video__(self, server_ip: list = None): """ @@ -263,6 +264,9 @@ class Downloader(): # Download the video segments video_m3u8.download_streams(f"{Colors.MAGENTA}video") + # Get time of output file + print_duration_table(os.path.join(full_path_video, "0.ts")) + else: console.log("[cyan]Video [red]already exists.") @@ -309,6 +313,9 @@ class Downloader(): # Download the audio segments audio_m3u8.download_streams(f"{Colors.MAGENTA}audio {Colors.RED}{obj_audio.get('language')}") + # Get time of output file + print_duration_table(os.path.join(full_path_audio, "0.ts")) + else: console.log(f"[cyan]Audio [white]([green]{obj_audio.get('language')}[white]) [red]already exists.") @@ -373,14 +380,14 @@ class Downloader(): ) # Initiate the download of the subtitle content - console.log(f"[cyan]Downloading subtitle: [red]{sub_language.lower()}") + console.print(f"[cyan]Downloading subtitle: [red]{sub_language.lower()}") futures.append(executor.submit(self.__save_subtitle_content, m3u8_sub_parser.subtitle[-1], sub_full_path)) # Wait for all downloads to finish for future in futures: future.result() - def __join_video__(self, vcodec = 'copy') -> str: + def __join_video__(self) -> str: """ Join downloaded video segments into a single video file. @@ -397,10 +404,9 @@ class Downloader(): join_video( video_path = self.downloaded_video[0].get('path'), out_path = path_join_video, - vcodec = vcodec + codec = self.codec ) - print_duration_table(path_join_video) return path_join_video def __join_video_audio__(self) -> str: @@ -420,10 +426,10 @@ class Downloader(): join_audios( video_path = self.downloaded_video[0].get('path'), audio_tracks = self.downloaded_audio, - out_path = path_join_video_audio + out_path = path_join_video_audio, + codec = self.codec ) - print_duration_table(path_join_video_audio) return path_join_video_audio def __join_video_subtitles__(self, input_path: str) -> str: @@ -449,7 +455,6 @@ class Downloader(): path_join_video_subtitle ) - print_duration_table(path_join_video_subtitle) return path_join_video_subtitle def __clean__(self, out_path: str) -> None: @@ -473,7 +478,11 @@ class Downloader(): os.rename(out_path, self.output_filename) # Print size of the file - console.print(Panel(f"[bold green]Download completed![/bold green]\nFile size: [bold red]{format_size(os.path.getsize(self.output_filename))}[/bold red]", title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}", border_style="green")) + console.print(Panel( + f"[bold green]Download completed![/bold green]\n" + f"File size: [bold red]{format_size(os.path.getsize(self.output_filename))}[/bold red]\n" + f"Duration: [bold]{print_duration_table(self.output_filename, show=False)}[/bold]", + title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}", border_style="green")) # Delete all files except the output file delete_files_except_one(self.base_path, os.path.basename(self.output_filename)) @@ -535,7 +544,7 @@ class Downloader(): there_is_video: bool = (len(self.downloaded_video) > 0) there_is_audio: bool = (len(self.downloaded_audio) > 0) there_is_subtitle: bool = (len(self.downloaded_subtitle) > 0) - console.log(f"[cyan]Conversion [white]=> ([green]Audio: [yellow]{there_is_audio}[white], [green]Subtitle: [yellow]{there_is_subtitle}[white])") + console.print(f"[cyan]Conversion [white]=> ([green]Audio: [yellow]{there_is_audio}[white], [green]Subtitle: [yellow]{there_is_subtitle}[white])") # Join audio and video diff --git a/Src/Lib/Hls/segments.py b/Src/Lib/Hls/segments.py index beeb831..c52d82d 100644 --- a/Src/Lib/Hls/segments.py +++ b/Src/Lib/Hls/segments.py @@ -31,7 +31,6 @@ from ..M3U8 import ( M3U8_UrlFix ) - # Config TQDM_MAX_WORKER = config_manager.get_int('M3U8_DOWNLOAD', 'tdqm_workers') TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') @@ -122,7 +121,7 @@ class M3U8_Segments: m3u8_parser = M3U8_Parser() m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content) # Parse the content of the M3U8 playlist - console.log(f"[cyan]There is key: [yellow]{m3u8_parser.keys is not None}") + console.print(f"[red]There is key: [yellow]{m3u8_parser.keys is not None}") # Check if there is an encryption key in the playlis if m3u8_parser.keys is not None: @@ -166,6 +165,7 @@ class M3U8_Segments: # Update segments for estimator self.class_ts_estimator.total_segments = len(self.segments) + logging.info(f"fSegmnets to donwload: [{len(self.segments)}]") def get_info(self) -> None: """ @@ -220,12 +220,13 @@ class M3U8_Segments: # Generate new user agent headers_segments['user-agent'] = get_headers() + logging.info(f"Make request to get segmenet: [{index} - {len(self.segments)}]") try: # Make request and calculate time duration start_time = time.time() - response = requests.get(ts_url, headers=headers_segments, verify_ssl=REQUEST_VERIFY_SSL) + response = requests.get(ts_url, headers=headers_segments, verify=REQUEST_VERIFY_SSL, timeout=30) duration = time.time() - start_time if response.ok: @@ -267,7 +268,7 @@ class M3U8_Segments: while not stop_event.is_set() or not self.segment_queue.empty(): with self.condition: while self.segment_queue.empty() and not stop_event.is_set(): - self.condition.wait(timeout=1) # Wait until a new segment is available or stop_event is set + self.condition.wait() # Wait until a new segment is available or stop_event is set if stop_event.is_set(): break @@ -311,16 +312,6 @@ class M3U8_Segments: mininterval=0.01 ) - def signal_handler(sig, frame): - self.ctrl_c_detected = True # Set global variable to indicate Ctrl+C detection - - stop_event.set() - with self.condition: - self.condition.notify_all() # Wake up the writer thread if it's waiting - - # Register the signal handler for Ctrl+C - signal.signal(signal.SIGINT, signal_handler) - with ThreadPoolExecutor(max_workers=TQDM_MAX_WORKER) as executor: # Start a separate thread to write segments to the file @@ -330,24 +321,13 @@ class M3U8_Segments: # Delay the start of each worker for index, segment_url in enumerate(self.segments): - # Check for Ctrl+C before starting each download task - time.sleep(0.03) - - if self.ctrl_c_detected: - console.log("[red]Ctrl+C detected. Stopping further downloads.") - - stop_event.set() - with self.condition: - self.condition.notify_all() # Wake up the writer thread if it's waiting - - break - # Submit the download task to the executor executor.submit(self.make_requests_stream, segment_url, index, stop_event, progress_bar) # Wait for all segments to be downloaded - executor.shutdown(wait=True) + executor.shutdown() stop_event.set() # Set the stop event to halt the writer thread with self.condition: self.condition.notify_all() # Wake up the writer thread if it's waiting writer_thread.join() # Wait for the writer thread to finish + diff --git a/Src/Lib/M3U8/parser.py b/Src/Lib/M3U8/parser.py index 2e3b647..9514dae 100644 --- a/Src/Lib/M3U8/parser.py +++ b/Src/Lib/M3U8/parser.py @@ -48,10 +48,6 @@ RESOLUTIONS = [ class M3U8_Codec: - """ - Represents codec information for an M3U8 playlist. - """ - def __init__(self, bandwidth, codecs): """ Initializes the M3U8Codec object with the provided parameters. @@ -64,20 +60,23 @@ class M3U8_Codec: self.codecs = codecs self.audio_codec = None self.video_codec = None + self.video_codec_name = None + self.audio_codec_name = None self.extract_codecs() self.parse_codecs() + self.calculate_bitrates() def extract_codecs(self): """ Parses the codecs information to extract audio and video codecs. Extracted codecs are set as attributes: audio_codec and video_codec. """ - - # Split the codecs string by comma try: + # Split the codecs string by comma codecs_list = self.codecs.split(',') except Exception as e: - logging.error(f"Cant split codec list: {self.codecs} with error {e}") + logging.error(f"Can't split codec list: {self.codecs} with error {e}") + return # Separate audio and video codecs for codec in codecs_list: @@ -87,7 +86,6 @@ class M3U8_Codec: self.audio_codec = codec def convert_video_codec(self, video_codec_identifier) -> str: - """ Convert video codec identifier to codec name. @@ -97,6 +95,9 @@ class M3U8_Codec: Returns: str: Codec name corresponding to the identifier. """ + if not video_codec_identifier: + logging.warning("No video codec identifier provided. Using default codec libx264.") + return "libx264" # Default # Extract codec type from the identifier codec_type = video_codec_identifier.split('.')[0] @@ -107,13 +108,11 @@ class M3U8_Codec: if codec_name: return codec_name - else: logging.warning(f"No corresponding video codec found for {video_codec_identifier}. Using default codec libx264.") - return "libx264" # Default - - def convert_audio_codec(self, audio_codec_identifier) -> str: + return "libx264" # Default + def convert_audio_codec(self, audio_codec_identifier) -> str: """ Convert audio codec identifier to codec name. @@ -123,6 +122,9 @@ class M3U8_Codec: Returns: str: Codec name corresponding to the identifier. """ + if not audio_codec_identifier: + logging.warning("No audio codec identifier provided. Using default codec aac.") + return "aac" # Default # Extract codec type from the identifier codec_type = audio_codec_identifier.split('.')[0] @@ -133,25 +135,33 @@ class M3U8_Codec: if codec_name: return codec_name - else: logging.warning(f"No corresponding audio codec found for {audio_codec_identifier}. Using default codec aac.") - return "aac" # Default - + return "aac" # Default + def parse_codecs(self): """ Parse video and audio codecs. This method updates `video_codec_name` and `audio_codec_name` attributes. """ - self.video_codec_name = self.convert_video_codec(self.video_codec) self.audio_codec_name = self.convert_audio_codec(self.audio_codec) - def __str__(self): + def calculate_bitrates(self): """ - Returns a string representation of the M3U8Codec object. + Calculate video and audio bitrates based on the available bandwidth. """ - return f"BANDWIDTH={self.bandwidth},RESOLUTION={self.resolution},CODECS=\"{self.codecs}\"" + if self.bandwidth: + + # Define the video and audio bitrates + video_bitrate = int(self.bandwidth * 0.8) # Using 80% of bandwidth for video + audio_bitrate = self.bandwidth - video_bitrate + + self.video_bitrate = video_bitrate + self.audio_bitrate = audio_bitrate + else: + logging.warning("No bandwidth provided. Bitrates cannot be calculated.") + class M3U8_Video: diff --git a/Src/Lib/Request/my_requests.py b/Src/Lib/Request/my_requests.py index 5c8c06c..45e774c 100644 --- a/Src/Lib/Request/my_requests.py +++ b/Src/Lib/Request/my_requests.py @@ -189,7 +189,7 @@ class ManageRequests: timeout: float = HTTP_TIMEOUT, retries: int = HTTP_RETRIES, params: Optional[Dict[str, str]] = None, - verify_ssl: bool = True, + verify: bool = True, auth: Optional[tuple] = None, proxy: Optional[str] = None, cookies: Optional[Dict[str, str]] = None, @@ -206,7 +206,7 @@ class ManageRequests: - timeout (float, optional): The request timeout. Defaults to HTTP_TIMEOUT. - retries (int, optional): The number of retries in case of request failure. Defaults to HTTP_RETRIES. - params (Optional[Dict[str, str]], optional): The query parameters for the request. Defaults to None. - - verify_ssl (bool, optional): Indicates whether SSL certificate verification should be performed. Defaults to True. + - verify (bool, optional): Indicates whether SSL certificate verification should be performed. Defaults to True. - auth (Optional[tuple], optional): Tuple containing the username and password for basic authentication. Defaults to None. - proxy (Optional[str], optional): The proxy URL. Defaults to None. - cookies (Optional[Dict[str, str]], optional): The cookies to be included in the request. Defaults to None. @@ -218,7 +218,7 @@ class ManageRequests: self.timeout = timeout self.retries = retries self.params = params - self.verify_ssl = verify_ssl + self.verify_ssl = verify self.auth = auth self.proxy = proxy self.cookies = cookies diff --git a/Src/Util/os.py b/Src/Util/os.py index f2ab1de..a9617f5 100644 --- a/Src/Util/os.py +++ b/Src/Util/os.py @@ -16,6 +16,7 @@ import platform import importlib import subprocess import contextlib +import urllib.request import importlib.metadata from typing import List @@ -403,6 +404,19 @@ def convert_to_hex(bytes_data: bytes) -> str: # --> OS GET SUMMARY +def check_internet(): + while True: + try: + # Attempt to open a connection to a website to check for internet connection + urllib.request.urlopen("http://www.google.com", timeout=1) + console.log("[bold green]Internet is available![/bold green]") + break + + except urllib.error.URLError: + console.log("[bold red]Internet is not available. Waiting...[/bold red]") + time.sleep(5) + print() + def get_executable_version(command): try: version_output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode().split('\n')[0] @@ -419,7 +433,8 @@ def get_library_version(lib_name): return f"{lib_name}-not installed" def get_system_summary(): - + + check_internet() console.print("[bold blue]System Summary[/bold blue][white]:") # Python version and platform diff --git a/config.json b/config.json index 2603713..d769c1b 100644 --- a/config.json +++ b/config.json @@ -32,7 +32,10 @@ }, "M3U8_CONVERSION": { "use_codec": false, - "use_gpu": false, + "use_vcodec": true, + "use_acodec": true, + "use_bitrate": true, + "use_gpu": true, "default_preset": "ultrafast", "check_output_after_ffmpeg": false }, diff --git a/run.py b/run.py index 762982a..e99a551 100644 --- a/run.py +++ b/run.py @@ -27,34 +27,6 @@ from Src.Api.Altadefinizione import main_film as altadefinizione_film CLOSE_CONSOLE = config_manager.get_bool('DEFAULT', 'not_close') -def initialize(): - """ - Initialize the application. - Checks Python version, removes temporary folder, and displays start message. - """ - - # Set terminal size for win 7 - if platform.system() == "Windows" and "7" in platform.version(): - os.system('mode 120, 40') - - - # Check python version - if sys.version_info < (3, 7): - console.log("[red]Install python version > 3.7.16") - sys.exit(0) - - - # Removing temporary folder - start_message() - - - # Attempting GitHub update - """try: - git_update() - except Exception as e: - console.print(f"[blue]Req github [white]=> [red]Failed: {e}")""" - - def run_function(func: Callable[..., None], close_console: bool = False) -> None: """ Run a given function indefinitely or once, depending on the value of close_console. @@ -63,9 +35,6 @@ def run_function(func: Callable[..., None], close_console: bool = False) -> None func (Callable[..., None]): The function to run. close_console (bool, optional): Whether to close the console after running the function once. Defaults to False. """ - - initialize() - if close_console: while 1: func() @@ -73,11 +42,40 @@ def run_function(func: Callable[..., None], close_console: bool = False) -> None func() -def main(): - +def initialize(): + """ + Initialize the application. + Checks Python version, removes temporary folder, and displays start message. + """ + + start_message() + + # Create logger log_not = Logger() + + # Get system info get_system_summary() + # Set terminal size for win 7 + if platform.system() == "Windows" and "7" in platform.version(): + os.system('mode 120, 40') + + # Check python version + if sys.version_info < (3, 7): + console.log("[red]Install python version > 3.7.16") + sys.exit(0) + + # Attempting GitHub update + try: + git_update() + except Exception as e: + console.print(f"[blue]Req github [white]=> [red]Failed: {e}") + + +def main(): + + initialize() + # Parse command line arguments parser = argparse.ArgumentParser(description='Script to download film and series from the internet.') parser.add_argument('-sa', '--streaming_anime', action='store_true', help='Check into anime category')