diff --git a/Src/Lib/FFmpeg/my_m3u8.py b/Src/Lib/FFmpeg/my_m3u8.py index 734f803..2e4f731 100644 --- a/Src/Lib/FFmpeg/my_m3u8.py +++ b/Src/Lib/FFmpeg/my_m3u8.py @@ -44,13 +44,14 @@ from .util import ( M3U8_Decryption, M3U8_Ts_Files, M3U8_Parser, + M3U8_Codec, M3U8_UrlFix ) # Config -Download_audio = config_manager.get_bool('M3U8_OPTIONS', 'download_audio') -Donwload_subtitles = config_manager.get_bool('M3U8_OPTIONS', 'download_subtitles') +DOWNLOAD_AUDIO = config_manager.get_bool('M3U8_OPTIONS', 'download_audio') +DOWNLOAD_SUBTITLES = config_manager.get_bool('M3U8_OPTIONS', 'download_subtitles') DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_OPTIONS', 'specific_list_audio') DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_OPTIONS', 'specific_list_subtitles') TQDM_MAX_WORKER = config_manager.get_int('M3U8', 'tdqm_workers') @@ -394,10 +395,10 @@ class M3U8_Segments: # Refresh progress bar progress_counter.refresh() - def join(self, output_filename: str, video_decoding: str = None, audio_decoding: str = None): + def join(self, output_filename: str): """ Join all segments file to a mp4 file name - !! NOT USED + !! NOT USED IN THIS VERSION Parameters: - video_decoding(str): video decoding to use with ffmpeg for only video @@ -437,9 +438,7 @@ class M3U8_Segments: # ADD IF concatenate_and_save( file_list_path = file_list_path, - output_filename = output_filename, - video_decoding = video_decoding, - audio_decoding = audio_decoding + output_filename = output_filename ) @@ -455,7 +454,6 @@ class Downloader(): - key (str, optional): Hexadecimal representation of the encryption key. """ - self.m3u8_playlist = m3u8_playlist self.m3u8_index = m3u8_index self.key = bytes.fromhex(key) if key is not None else key @@ -471,15 +469,16 @@ class Downloader(): if self.key != None: hex_data = convert_to_hex(self.key) console.log(f"[cyan]Key use [white]=> [red]{hex_data}") + logging.info(f"Key use: {self.key}") # Initialize temp base path self.base_path = os.path.join(str(self.output_filename).replace(".mp4", "")) self.video_segments_path = os.path.join(self.base_path, "tmp", "video") self.audio_segments_path = os.path.join(self.base_path, "tmp", "audio") self.subtitle_segments_path = os.path.join(self.base_path, "tmp", "subtitle") + logging.info(f"Output base path: {self.base_path}") # Create temp folder - logging.info("Create temp folder") os.makedirs(self.video_segments_path, exist_ok=True) os.makedirs(self.audio_segments_path, exist_ok=True) os.makedirs(self.subtitle_segments_path, exist_ok=True) @@ -488,14 +487,10 @@ class Downloader(): self.downloaded_audio = [] self.downloaded_subtitle = [] self.downloaded_video = [] - - # Default decoding - self.video_decoding = "avc1.640028" - self.audio_decoding = "mp4a.40.2" def __df_make_req__(self, url: str) -> str: """ - Make a request to get text from the provided URL. + Make a request to get text from the provided URL to test if index or m3u8 work correcly. Parameters: - url (str): The URL to make the request to. @@ -503,23 +498,30 @@ class Downloader(): Returns: - str: The text content of the response. """ + try: + # Send a GET request to the provided URL config_headers.get('index')['user-agent'] = get_headers() response = requests.get(url, headers=config_headers.get('index')) + # Check status response of request + logging.info(f"Test url: {url}") + response.raise_for_status() + if response.ok: return response.text + else: - logging.error(f"[df_make_req] Request to {url} failed with status code: {response.status_code}") + logging.error(f"Request to {url} failed with status code: {response.status_code}") return None except requests.RequestException as req_err: - logging.error(f"[df_make_req] Error occurred during request: {req_err}") + logging.error(f"Error occurred during request: {req_err}") return None except Exception as e: - logging.error(f"[df_make_req] An unexpected error occurred: {e}") + logging.error(f"An unexpected error occurred: {e}") return None def manage_playlist(self, m3u8_playlist_text): @@ -530,7 +532,7 @@ class Downloader(): m3u8_playlist_text (str): The text content of the M3U8 playlist. """ - global Download_audio, Donwload_subtitles + global DOWNLOAD_AUDIO, DOWNLOAD_SUBTITLES # Create an instance of the M3U8_Parser class parse_class_m3u8 = M3U8_Parser(DOWNLOAD_SPECIFIC_SUBTITLE) @@ -547,7 +549,7 @@ class Downloader(): console.log(f"[cyan]Find audios language: [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}") else: console.log("[red]Cant find a list of audios") - Download_audio = False + DOWNLOAD_AUDIO = False # Collect available subtitles and default subtitle self.list_available_subtitles = parse_class_m3u8.get_subtitles() @@ -558,7 +560,7 @@ class Downloader(): console.log(f"[cyan]Find subtitles language: [red]{[obj_sub.get('language') for obj_sub in self.list_available_subtitles]}") else: console.log("[red]Cant find a list of audios") - Donwload_subtitles = False + DOWNLOAD_SUBTITLES = False # Collect best quality video m3u8_index_obj = parse_class_m3u8.get_best_quality() @@ -566,7 +568,6 @@ class Downloader(): # Get URI of the best quality and codecs parameters console.log(f"[cyan]Select resolution: [red]{m3u8_index_obj.get('width')}") m3u8_index = m3u8_index_obj.get('uri') - m3u8_index_decoding = m3u8_index_obj.get('codecs') # Fix URL if it is not complete with http:\\site_name.domain\... if "http" not in m3u8_index: @@ -581,13 +582,12 @@ class Downloader(): logging.warning("[download_m3u8] Can't find a valid m3u8 index") sys.exit(0) - # Collect best index, video decoding, and audio decoding + # Set m3u8_index self.m3u8_index = m3u8_index - # if is present in playlist - if m3u8_index_decoding != None: - self.video_decoding = m3u8_index_decoding.split(",")[0] - self.audio_decoding = m3u8_index_decoding.split(",")[1] + # Get obj codec + self.codec: M3U8_Codec = parse_class_m3u8.codec + logging.info(f"Get coded: {self.codec}") def manage_subtitle(self): """ @@ -622,8 +622,8 @@ class Downloader(): 'path': os.path.abspath(sub_full_path) }) - # If the subtitle file doesn't exist, download it + logging.info(f"Download uri subtitles: {obj_subtitle.get('uri')} => {sub_full_path}") response = requests.get(obj_subtitle.get('uri')) open(sub_full_path, "wb").write(response.content) @@ -658,6 +658,7 @@ class Downloader(): if not os.path.exists(full_path_audio): # If the audio segment directory doesn't exist, download audio segments + logging.info(f"Download uri audio: {obj_audio.get('uri')} => {full_path_audio}") audio_m3u8 = M3U8_Segments(obj_audio.get('uri'), full_path_audio, self.key) console.log(f"[purple]Download audio segments [white]=> [red]{obj_audio.get('language')}.") @@ -716,7 +717,6 @@ class Downloader(): ts_files = [f for f in os.listdir(full_path) if f.endswith(".ts")] ts_files.sort(key=Downloader.extract_number) logging.info(f"Find {len(ts_files)} stream files to join") - logging.info(f"Using parameter: \n-c:v = {self.video_decoding} -c:a = {self.audio_decoding}])") # Check if there are enough .ts files to join (at least 10) if len(ts_files) < 10: @@ -735,8 +735,9 @@ class Downloader(): return concatenate_and_save( file_list_path=file_list_path, output_filename=out_file_name, - video_decoding=self.video_decoding, - audio_decoding=self.audio_decoding + v_codec=self.codec.video_codec, + a_codec=self.codec.audio_codec, + bandwidth=self.codec.bandwidth ) def download_audios(self): @@ -903,12 +904,12 @@ class Downloader(): self.manage_playlist(m3u8_playlist_text) # Download subtitles - if Donwload_subtitles: + if DOWNLOAD_SUBTITLES: logging.info("Download subtitles ...") self.manage_subtitle() # Download segmenets of audio tracks - if Download_audio: + if DOWNLOAD_AUDIO: logging.info("Download audios ...") self.manage_audio() diff --git a/Src/Lib/FFmpeg/util/__init__.py b/Src/Lib/FFmpeg/util/__init__.py index 72b4f05..eabf225 100644 --- a/Src/Lib/FFmpeg/util/__init__.py +++ b/Src/Lib/FFmpeg/util/__init__.py @@ -13,5 +13,5 @@ from .helper import ( from .decryption import M3U8_Decryption from .installer import check_ffmpeg from .math_calc import M3U8_Ts_Files -from .parser import M3U8_Parser +from .parser import M3U8_Parser, M3U8_Codec from .url_fix import M3U8_UrlFix \ No newline at end of file diff --git a/Src/Lib/FFmpeg/util/helper.py b/Src/Lib/FFmpeg/util/helper.py index e7f3554..367b0f3 100644 --- a/Src/Lib/FFmpeg/util/helper.py +++ b/Src/Lib/FFmpeg/util/helper.py @@ -5,6 +5,7 @@ import os import json import logging import shutil +import sys # External libraries @@ -182,17 +183,19 @@ def add_subtitle(input_video_path: str, input_subtitle_path: str, output_video_p return output_video_path -def concatenate_and_save(file_list_path: str, output_filename: str, video_decoding: str = None, audio_decoding: str = None, prefix: str = "segments", output_directory: str = None) -> str: +def concatenate_and_save(file_list_path: str, output_filename: str, v_codec: str = None, a_codec: str = None, bandwidth: int = None, prefix: str = "segments", output_directory: str = None) -> str: """ Concatenate input files and save the output with specified decoding parameters. Parameters: - file_list_path (str): Path to the file list containing the segments. - output_filename (str): Output filename for the concatenated video. - - video_decoding (str): Video decoding parameter (optional). - - audio_decoding (str): Audio decoding parameter (optional). + - v_codec (str): Video decoding parameter (optional). + - a_codec (str): Audio decoding parameter (optional). + - bandwidth (int): Bitrate for the output video (optional). - prefix (str): Prefix to add at the end of output file name (default is "segments"). - output_directory (str): Directory to save the output file. If not provided, defaults to the current directory. + - codecs (str): Codecs for video and audio (optional). Returns: - output_file_path (str): Path to the saved output file. @@ -208,9 +211,17 @@ def concatenate_and_save(file_list_path: str, output_filename: str, video_decodi output_args = { 'c': 'copy', 'loglevel': DEBUG_FFMPEG, - 'y': None + 'y': None, } + # Add BANDWIDTH and CODECS if provided + if bandwidth is not None: + output_args['b:v'] = str(bandwidth) + """if v_codec is not None: + output_args['vcodec'] = v_codec + if a_codec is not None: + output_args['acodec'] = a_codec""" + # Set up the output file name by modifying the video file name output_file_name = os.path.splitext(output_filename)[0] + f"_{prefix}.mp4" diff --git a/Src/Lib/FFmpeg/util/parser.py b/Src/Lib/FFmpeg/util/parser.py index df3759e..3b2912e 100644 --- a/Src/Lib/FFmpeg/util/parser.py +++ b/Src/Lib/FFmpeg/util/parser.py @@ -12,6 +12,58 @@ import requests from m3u8 import M3U8 + +class M3U8_Codec(): + """ + Represents codec information for an M3U8 playlist. + + Attributes: + - bandwidth (int): Bandwidth of the codec. + - resolution (str): Resolution of the codec. + - codecs (str): Codecs information in the format "avc1.xxxxxx,mp4a.xx". + - audio_codec (str): Audio codec extracted from the codecs information. + - video_codec (str): Video codec extracted from the codecs information. + """ + + def __init__(self, bandwidth, resolution, codecs): + """ + Initializes the M3U8Codec object with the provided parameters. + + Parameters: + - bandwidth (int): Bandwidth of the codec. + - resolution (str): Resolution of the codec. + - codecs (str): Codecs information in the format "avc1.xxxxxx,mp4a.xx". + """ + self.bandwidth = bandwidth + self.resolution = resolution + self.codecs = codecs + self.audio_codec = None + self.video_codec = None + self.parse_codecs() + + def parse_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 + codecs_list = self.codecs.split(',') + + # Separate audio and video codecs + for codec in codecs_list: + if codec.startswith('avc'): + self.video_codec = codec + elif codec.startswith('mp4a'): + self.audio_codec = codec + + def __str__(self): + """ + Returns a string representation of the M3U8Codec object. + """ + return f"BANDWIDTH={self.bandwidth},RESOLUTION={self.resolution},CODECS=\"{self.codecs}\"" + + class M3U8_Parser: def __init__(self, DOWNLOAD_SPECIFIC_SUBTITLE = None): """ @@ -24,6 +76,7 @@ class M3U8_Parser: self.subtitle_playlist = [] # No vvt ma url a vvt self.subtitle = [] # Url a vvt self.audio_ts = [] + self.codec: M3U8_Codec = None self.DOWNLOAD_SPECIFIC_SUBTITLE = DOWNLOAD_SPECIFIC_SUBTITLE def parse_data(self, m3u8_content: str) -> (None): @@ -41,11 +94,19 @@ class M3U8_Parser: # Collect video info with url, resolution and codecs for playlist in m3u8_obj.playlists: + self.video_playlist.append({ "uri": playlist.uri, "width": playlist.stream_info.resolution, - "codecs": playlist.stream_info.codecs - }) + }) + + self.codec = M3U8_Codec( + playlist.stream_info.bandwidth, + playlist.stream_info.resolution, + playlist.stream_info.codecs + ) + logging.info(f"Parse: {playlist.stream_info}") + logging.info(f"Coded test: {self.codec.bandwidth}") # Collect info of encryption if present, method, uri and iv for key in m3u8_obj.keys: @@ -92,7 +153,7 @@ class M3U8_Parser: self.subtitle.append(segment.uri) except Exception as e: - logging.error(f"[M3U8_Parser] Error parsing M3U8 content: {e}") + logging.error(f"Error parsing M3U8 content: {e}") def get_resolution(self, uri: str) -> (int): """ @@ -134,8 +195,8 @@ class M3U8_Parser: return sorted_uris[0] except: - logging.error("[M3U8_Parser] Error: Can't find M3U8 resolution by width...") - logging.info("[M3U8_Parser] Try searching in URI") + logging.error("Error: Can't find M3U8 resolution by width...") + logging.info("Try searching in URI") # Sort the list of video playlist items based on the 'width' attribute if present, # otherwise, use the resolution obtained from the 'uri' attribute as a fallback. @@ -146,7 +207,7 @@ class M3U8_Parser: return sorted_uris[0] else: - logging.info("[M3U8_Parser] No video playlists found.") + logging.info("No video playlists found.") return None def get_subtitles(self): @@ -168,7 +229,7 @@ class M3U8_Parser: # Get language name name_language = sub_info.get("language") - logging.info(f"[M3U8_Parser] Find subtitle: {name_language}") + logging.info(f"Find subtitle: {name_language}") # Check if there is custom subtitles to download if len(self.DOWNLOAD_SPECIFIC_SUBTITLE) > 0: @@ -178,7 +239,7 @@ class M3U8_Parser: continue # Make request to m3u8 subtitle to extract vtt - logging.info(f"[M3U8_Parser] Download subtitle: {name_language}") + logging.info(f"Download subtitle: {name_language}") req_sub_content = requests.get(sub_info.get("uri"), headers={'user-agent': get_headers()}) try: @@ -196,13 +257,13 @@ class M3U8_Parser: }) except Exception as e: - logging.error(f"[M3U8_Parser] Cant donwload: {name_language}, error: {e}") + logging.error(f"Cant donwload: {name_language}, error: {e}") # Return return output else: - logging.info("[M3U8_Parser] No subtitle find") + logging.info("No subtitle find") return None def get_track_audios(self) -> list: @@ -213,10 +274,10 @@ class M3U8_Parser: list: A list of dictionaries containing language and URI information for audio tracks, or None if no audio tracks are found. """ - logging.info(f"[M3U8_Parser] Finding {len(self.audio_ts)} playlist(s) with audio.") + logging.info(f"Finding {len(self.audio_ts)} playlist(s) with audio.") if self.audio_ts: - logging.info("[M3U8_Parser] Getting list of available audio names") + logging.info("Getting list of available audio names") list_output = [] # For all languages present in m3u8 @@ -232,7 +293,7 @@ class M3U8_Parser: return list_output else: - logging.info("[M3U8_Parser] No audio tracks found") + logging.info("No audio tracks found") return None def get_default_subtitle(self): @@ -289,4 +350,5 @@ class M3U8_Parser: } # Return the default audio track dictionary - return dict_default_audio \ No newline at end of file + return dict_default_audio + \ No newline at end of file diff --git a/Src/Util/_win32.py b/Src/Util/_win32.py index 77f4736..bf01902 100644 --- a/Src/Util/_win32.py +++ b/Src/Util/_win32.py @@ -1,6 +1,6 @@ # 07.04.24 - +# to do # run somehwere backup # add config to trace if ffmpeg is install, using config in local or temp @@ -11,6 +11,7 @@ import logging # Winreg only work for windows if platform.system() == "Windows": + # Winreg only work for windows import winreg # Define Windows registry key for user environment variables