diff --git a/README.md b/README.md index 296f247..d59edaa 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,6 @@ Make sure you have the following prerequisites installed on your system: * [ffmpeg](https://www.gyan.dev/ffmpeg/builds/) * [opnessl](https://www.openssl.org) or [pycryptodome](https://pypi.org/project/pycryptodome/) -* [nodejs](https://nodejs.org/) - ## Installation Install the required Python libraries using the following command: diff --git a/Src/Api/Template/Util/manage_ep.py b/Src/Api/Template/Util/manage_ep.py index 09ca8cf..7d0f049 100644 --- a/Src/Api/Template/Util/manage_ep.py +++ b/Src/Api/Template/Util/manage_ep.py @@ -14,6 +14,27 @@ from Src.Util.os import remove_special_characters MAP_EPISODE = config_manager.get('DEFAULT', 'map_episode_name') +def dynamic_format_number(n: int) -> str: + """ + Formats a number by adding a leading zero. + The width of the resulting string is dynamic, calculated as the number of digits in the number plus one. + + Args: + - n (int): The number to format. + + Returns: + - str: The formatted number as a string with a leading zero. + + Examples: + >>> dynamic_format_number(1) + '01' + >>> dynamic_format_number(20) + '020' + """ + width = len(str(n)) + 1 + return str(n).zfill(width) + + def manage_selection(cmd_insert: str, max_count: int) -> List[int]: """ Manage user selection for seasons to download. @@ -45,6 +66,7 @@ def manage_selection(cmd_insert: str, max_count: int) -> List[int]: logging.info(f"List return: {list_season_select}") return list_season_select + def map_episode_title(tv_name: str, number_season: int, episode_number: int, episode_name: str) -> str: """ Maps the episode title to a specific format. @@ -60,8 +82,8 @@ def map_episode_title(tv_name: str, number_season: int, episode_number: int, epi """ map_episode_temp = MAP_EPISODE map_episode_temp = map_episode_temp.replace("%(tv_name)", remove_special_characters(tv_name)) - map_episode_temp = map_episode_temp.replace("%(season)", str(number_season)) - map_episode_temp = map_episode_temp.replace("%(episode)", str(episode_number)) + map_episode_temp = map_episode_temp.replace("%(season)", dynamic_format_number(number_season)) + map_episode_temp = map_episode_temp.replace("%(episode)", dynamic_format_number(episode_number)) map_episode_temp = map_episode_temp.replace("%(episode_name)", remove_special_characters(episode_name)) # Additional fix diff --git a/Src/Api/altadefinizione/Core/Player/supervideo.py b/Src/Api/altadefinizione/Core/Player/supervideo.py index dfe5138..f851924 100644 --- a/Src/Api/altadefinizione/Core/Player/supervideo.py +++ b/Src/Api/altadefinizione/Core/Player/supervideo.py @@ -12,7 +12,7 @@ from bs4 import BeautifulSoup # Internal utilities from Src.Util.headers import get_headers -from Src.Util.os import run_node_script +from Src.Util.os import run_node_script, run_node_script_api class VideoSource: @@ -118,9 +118,14 @@ class VideoSource: """ for script in soup.find_all("script"): if "eval" in str(script): - new_script = str(script.text).replace("eval", "var a = ") - new_script = new_script.replace(")))", ")));console.log(a);") - return run_node_script(new_script) + + # WITH INSTALL NODE JS + #new_script = str(script.text).replace("eval", "var a = ") + #new_script = new_script.replace(")))", ")));console.log(a);") + #return run_node_script(new_script) + + # WITH API + return run_node_script_api(script.text) return None @@ -155,7 +160,7 @@ class VideoSource: pattern = r'data-link="(//supervideo[^"]+)"' match = re.search(pattern, str(down_page_soup)) if not match: - logging.error("No match found for supervideo URL.") + logging.error("No player available for download.") return None supervideo_url = "https:" + match.group(1) diff --git a/Src/Api/altadefinizione/film.py b/Src/Api/altadefinizione/film.py index d0cc4b7..31c1f1b 100644 --- a/Src/Api/altadefinizione/film.py +++ b/Src/Api/altadefinizione/film.py @@ -7,7 +7,7 @@ import logging # Internal utilities from Src.Util.console import console -from Src.Lib.Hls.downloader import Downloader +from Src.Lib.Downloader import HLS_Downloader from Src.Util.message import start_message @@ -49,7 +49,7 @@ def download_film(title_name: str, url: str): master_playlist = video_source.get_playlist() # Download the film using the m3u8 playlist, and output filename - Downloader( + HLS_Downloader( m3u8_playlist = master_playlist, output_filename = os.path.join(mp4_path, mp4_name) ).start() \ No newline at end of file diff --git a/Src/Api/animeunity/anime.py b/Src/Api/animeunity/anime.py index ca1a12c..01e9d47 100644 --- a/Src/Api/animeunity/anime.py +++ b/Src/Api/animeunity/anime.py @@ -6,7 +6,7 @@ import logging # Internal utilities from Src.Util.console import console, msg -from Src.Lib.Hls.downloader import Downloader +from Src.Lib.Downloader import HLS_Downloader from Src.Util.message import start_message from ..Template import manage_selection @@ -50,7 +50,7 @@ def download_episode(index_select: int): mp4_path = os.path.join(ROOT_PATH, SITE_NAME, MOVIE_FOLDER, video_source.series_name) # Start downloading - Downloader( + HLS_Downloader( m3u8_playlist = video_source.get_playlist(), output_filename = os.path.join(mp4_path, mp4_name) ).start() diff --git a/Src/Api/ddlstreamitaly/Core/Class/ScrapeSerie.py b/Src/Api/ddlstreamitaly/Core/Class/ScrapeSerie.py index c5dd366..2060987 100644 --- a/Src/Api/ddlstreamitaly/Core/Class/ScrapeSerie.py +++ b/Src/Api/ddlstreamitaly/Core/Class/ScrapeSerie.py @@ -13,13 +13,16 @@ from bs4 import BeautifulSoup # Internal utilities from Src.Util.headers import get_headers -from Src.Util._jsonConfig import config_manager # Logic class from .SearchType import MediaItem +# Variable +from ...costant import COOKIE + + class GetSerieInfo: @@ -28,10 +31,10 @@ class GetSerieInfo: Initializes the GetSerieInfo object with default values. Args: - dict_serie (MediaItem): Dictionary containing series information (optional). + - dict_serie (MediaItem): Dictionary containing series information (optional). """ self.headers = {'user-agent': get_headers()} - self.cookies = config_manager.get_dict('REQUESTS', 'index') + self.cookies = COOKIE self.url = dict_serie.url self.tv_name = None self.list_episodes = None @@ -49,7 +52,7 @@ class GetSerieInfo: # Make an HTTP request to the series URL try: - response = httpx.get(self.url + "?area=online", cookies=self.cookies, headers=self.headers) + response = httpx.get(self.url + "?area=online", cookies=self.cookies, headers=self.headers, timeout=10) response.raise_for_status() except Exception as e: diff --git a/Src/Api/ddlstreamitaly/Core/Player/ddl.py b/Src/Api/ddlstreamitaly/Core/Player/ddl.py index 64857b7..2969f15 100644 --- a/Src/Api/ddlstreamitaly/Core/Player/ddl.py +++ b/Src/Api/ddlstreamitaly/Core/Player/ddl.py @@ -14,6 +14,10 @@ from Src.Util.headers import get_headers from Src.Util._jsonConfig import config_manager +# Variable +from ...costant import COOKIE + + class VideoSource: def __init__(self) -> None: @@ -25,7 +29,7 @@ class VideoSource: cookie (dict): A dictionary to store cookies. """ self.headers = {'user-agent': get_headers()} - self.cookie = config_manager.get_dict('REQUESTS', 'index') + self.cookie = COOKIE def setup(self, url: str) -> None: """ diff --git a/Src/Api/ddlstreamitaly/costant.py b/Src/Api/ddlstreamitaly/costant.py index 9c7e339..2ff35c8 100644 --- a/Src/Api/ddlstreamitaly/costant.py +++ b/Src/Api/ddlstreamitaly/costant.py @@ -10,6 +10,7 @@ from Src.Util._jsonConfig import config_manager SITE_NAME = os.path.basename(os.path.dirname(os.path.abspath(__file__))) ROOT_PATH = config_manager.get('DEFAULT', 'root_path') DOMAIN_NOW = config_manager.get('SITE', SITE_NAME) +COOKIE = config_manager.get_dict('SITE', SITE_NAME)['cookie'] MOVIE_FOLDER = "Movie" SERIES_FOLDER = "Serie" diff --git a/Src/Api/ddlstreamitaly/series.py b/Src/Api/ddlstreamitaly/series.py index 6b13077..e778806 100644 --- a/Src/Api/ddlstreamitaly/series.py +++ b/Src/Api/ddlstreamitaly/series.py @@ -7,12 +7,11 @@ from urllib.parse import urlparse # Internal utilities -from Src.Util.color import Colors -from Src.Util.console import console, msg +from Src.Util.console import console from Src.Util.message import start_message from Src.Util.os import create_folder, can_create_file from Src.Util.table import TVShowManager -from Src.Lib.Hls.download_mp4 import MP4_downloader +from Src.Lib.Downloader import MP4_downloader from ..Template import manage_selection, map_episode_title @@ -70,7 +69,6 @@ def donwload_video(scape_info_serie: GetSerieInfo, index_episode_selected: int) url = master_playlist, path = os.path.join(mp4_path, mp4_name), referer = f"{parsed_url.scheme}://{parsed_url.netloc}/", - add_desc=f"{Colors.MAGENTA}video" ) diff --git a/Src/Api/ddlstreamitaly/site.py b/Src/Api/ddlstreamitaly/site.py index 8175fa8..6331c15 100644 --- a/Src/Api/ddlstreamitaly/site.py +++ b/Src/Api/ddlstreamitaly/site.py @@ -1,5 +1,6 @@ # 09.06.24 +import sys import logging @@ -21,7 +22,6 @@ from .Core.Class.SearchType import MediaManager # Variable from .costant import SITE_NAME -cookie_index = config_manager.get_dict('REQUESTS', 'index') media_search_manager = MediaManager() table_show_manager = TVShowManager() diff --git a/Src/Api/guardaserie/Core/Player/supervideo.py b/Src/Api/guardaserie/Core/Player/supervideo.py index e97e856..02810d0 100644 --- a/Src/Api/guardaserie/Core/Player/supervideo.py +++ b/Src/Api/guardaserie/Core/Player/supervideo.py @@ -11,7 +11,7 @@ from bs4 import BeautifulSoup # Internal utilities from Src.Util.headers import get_headers -from Src.Util.os import run_node_script +from Src.Util.os import run_node_script, run_node_script_api class VideoSource: @@ -46,7 +46,7 @@ class VideoSource: """ try: - response = httpx.get(url, headers=self.headers, follow_redirects=True) + response = httpx.get(url, headers=self.headers, follow_redirects=True, timeout=10) response.raise_for_status() return response.text @@ -85,9 +85,14 @@ class VideoSource: """ for script in soup.find_all("script"): if "eval" in str(script): - new_script = str(script.text).replace("eval", "var a = ") - new_script = new_script.replace(")))", ")));console.log(a);") - return run_node_script(new_script) + + # WITH INSTALL NODE JS + #new_script = str(script.text).replace("eval", "var a = ") + #new_script = new_script.replace(")))", ")));console.log(a);") + #return run_node_script(new_script) + + # WITH API + return run_node_script_api(script.text) return None diff --git a/Src/Api/guardaserie/series.py b/Src/Api/guardaserie/series.py index ccfd1f6..ed020b5 100644 --- a/Src/Api/guardaserie/series.py +++ b/Src/Api/guardaserie/series.py @@ -9,7 +9,7 @@ import logging from Src.Util.console import console, msg from Src.Util.table import TVShowManager from Src.Util.message import start_message -from Src.Lib.Hls.downloader import Downloader +from Src.Lib.Downloader import HLS_Downloader from ..Template import manage_selection, map_episode_title @@ -53,7 +53,7 @@ def donwload_video(scape_info_serie: GetSerieInfo, index_season_selected: int, i # Get m3u8 master playlist master_playlist = video_source.get_playlist() - Downloader( + HLS_Downloader( m3u8_playlist = master_playlist, output_filename = os.path.join(mp4_path, mp4_name) ).start() diff --git a/Src/Api/streamingcommunity/film.py b/Src/Api/streamingcommunity/film.py index 9f0e804..6b5431c 100644 --- a/Src/Api/streamingcommunity/film.py +++ b/Src/Api/streamingcommunity/film.py @@ -6,7 +6,7 @@ import logging # Internal utilities from Src.Util.console import console -from Src.Lib.Hls.downloader import Downloader +from Src.Lib.Downloader import HLS_Downloader from Src.Util.message import start_message @@ -50,7 +50,7 @@ def download_film(id_film: str, title_name: str, domain: str): mp4_path = os.path.join(ROOT_PATH, SITE_NAME, MOVIE_FOLDER, title_name) # Download the film using the m3u8 playlist, and output filename - Downloader( + HLS_Downloader( m3u8_playlist = master_playlist, output_filename = os.path.join(mp4_path, mp4_format) ).start() diff --git a/Src/Api/streamingcommunity/series.py b/Src/Api/streamingcommunity/series.py index a9f2365..7f63cba 100644 --- a/Src/Api/streamingcommunity/series.py +++ b/Src/Api/streamingcommunity/series.py @@ -9,7 +9,7 @@ import logging from Src.Util.console import console, msg from Src.Util.message import start_message from Src.Util.table import TVShowManager -from Src.Lib.Hls.downloader import Downloader +from Src.Lib.Downloader import HLS_Downloader from ..Template import manage_selection, map_episode_title @@ -51,7 +51,7 @@ def donwload_video(tv_name: str, index_season_selected: int, index_episode_selec master_playlist = video_source.get_playlist() # Download the episode - Downloader( + HLS_Downloader( m3u8_playlist = master_playlist, output_filename = os.path.join(mp4_path, mp4_name) ).start() diff --git a/Src/Lib/Downloader/HLS/__init__.py b/Src/Lib/Downloader/HLS/__init__.py new file mode 100644 index 0000000..331013c --- /dev/null +++ b/Src/Lib/Downloader/HLS/__init__.py @@ -0,0 +1,3 @@ +# 20.02.24 + +from .downloader import HLS_Downloader \ No newline at end of file diff --git a/Src/Lib/Hls/downloader.py b/Src/Lib/Downloader/HLS/downloader.py similarity index 89% rename from Src/Lib/Hls/downloader.py rename to Src/Lib/Downloader/HLS/downloader.py index 973cc7d..b52832f 100644 --- a/Src/Lib/Hls/downloader.py +++ b/Src/Lib/Downloader/HLS/downloader.py @@ -11,8 +11,6 @@ import httpx from unidecode import unidecode - - # Internal utilities from Src.Util.headers import get_headers from Src.Util._jsonConfig import config_manager @@ -22,7 +20,7 @@ from Src.Util.os import ( remove_folder, delete_files_except_one, compute_sha1_hash, - format_size, + format_file_size, create_folder, reduce_base_name, remove_special_characters, @@ -31,19 +29,18 @@ from Src.Util.os import ( # Logic class -from ..FFmpeg import ( +from ...FFmpeg import ( print_duration_table, join_video, join_audios, join_subtitle ) -from ..M3U8 import ( +from ...M3U8 import ( M3U8_Parser, M3U8_Codec, M3U8_UrlFix ) from .segments import M3U8_Segments -from ..E_Table import report_table # Config @@ -59,11 +56,11 @@ FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolutio # Variable -headers_index = config_manager.get_dict('REQUESTS', 'index') +headers_index = config_manager.get_dict('REQUESTS', 'user-agent') -class Downloader(): +class HLS_Downloader(): def __init__(self, output_filename: str = None, m3u8_playlist:str = None, m3u8_index:str = None): """ @@ -77,7 +74,8 @@ class Downloader(): self.m3u8_playlist = m3u8_playlist self.m3u8_index = m3u8_index - self.output_filename = output_filename.replace(" ", "_") + self.output_filename = output_filename + self.expected_real_time = None # Auto generate out file name if not present if output_filename == None: @@ -87,6 +85,8 @@ class Downloader(): self.output_filename = os.path.join("missing", compute_sha1_hash(m3u8_index)) else: + + # For missing output_filename folder, base_name = os.path.split(self.output_filename) # Split file_folder output base_name = reduce_base_name(remove_special_characters(base_name)) # Remove special char create_folder(folder) # Create folder and check if exist @@ -94,6 +94,7 @@ class Downloader(): logging.error("Invalid mp4 name.") sys.exit(0) + # Parse to only ascii for win, linux, mac and termux self.output_filename = os.path.join(folder, base_name) self.output_filename = unidecode(self.output_filename) @@ -134,23 +135,17 @@ class Downloader(): str: The text content of the response. """ + # Send a GET request to the provided URL + logging.info(f"Test url: {url}") + headers_index = {'user-agent': get_headers()} + response = httpx.get(url, headers=headers_index) + try: - - # Send a GET request to the provided URL - logging.info(f"Test url: {url}") - headers_index['user-agent'] = get_headers() - response = httpx.get(url, headers=headers_index) response.raise_for_status() - - if response.status_code == 200: - return response.text + return response.text - else: - logging.error(f"Test request to {url} failed with status code: {response.status_code}") - return None - except Exception as e: - logging.error(f"An unexpected error occurred with test request: {e}") + logging.error(f"Test request to {url} failed with error: {e}") return None def __manage_playlist__(self, m3u8_playlist_text): @@ -179,7 +174,7 @@ class Downloader(): # Check if there is some audios, else disable download if self.list_available_audio != None: - console.print(f"[cyan]Find audios [white]=> [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}") + console.print(f"[cyan]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") @@ -209,7 +204,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.print(f"[cyan]Find resolution [white]=> [red]{sorted(list_available_resolution, reverse=True)}") + console.print(f"[cyan]Resolutions [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: @@ -230,7 +225,7 @@ class Downloader(): logging.info(f"Find codec: {self.codec}") if self.codec is not None: - 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]))") + console.print(f"[cyan]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): @@ -260,6 +255,7 @@ class Downloader(): # Download the video segments video_m3u8.download_streams(f"{Colors.MAGENTA}video") + self.expected_real_time = video_m3u8.expected_real_time # Get time of output file print_duration_table(os.path.join(full_path_video, "0.ts")) @@ -459,6 +455,10 @@ class Downloader(): - out_path (str): Path of the output file. """ + def dict_to_seconds(d): + return d['h'] * 3600 + d['m'] * 60 + d['s'] + + # Check if file to rename exist logging.info(f"Check if end file converted exist: {out_path}") if out_path is None or not os.path.isfile(out_path): @@ -471,12 +471,29 @@ class Downloader(): # Rename file converted to original set in init os.rename(out_path, self.output_filename) - # Print size of the file - console.print(Panel( + # Get dict with h m s for ouput file name + end_output_time = print_duration_table(self.output_filename, description=False, return_string=False) + + # Calculate info for panel + formatted_size = 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.expected_real_time) + end_output_seconds = dict_to_seconds(end_output_time) + missing_ts = not (expected_real_seconds - 3 <= end_output_seconds <= expected_real_seconds + 3) + + panel_content = ( 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")) + 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]" + ) + + console.print(Panel( + panel_content, + 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)) @@ -501,8 +518,6 @@ class Downloader(): if self.m3u8_playlist: logging.info("Download from PLAYLIST") - - m3u8_playlist_text = self.__df_make_req__(self.m3u8_playlist) # Add full URL of the M3U8 playlist to fix next .ts without https if necessary diff --git a/Src/Lib/Hls/proxyes.py b/Src/Lib/Downloader/HLS/proxyes.py similarity index 100% rename from Src/Lib/Hls/proxyes.py rename to Src/Lib/Downloader/HLS/proxyes.py diff --git a/Src/Lib/Hls/segments.py b/Src/Lib/Downloader/HLS/segments.py similarity index 89% rename from Src/Lib/Hls/segments.py rename to Src/Lib/Downloader/HLS/segments.py index e9432b4..3e6c45c 100644 --- a/Src/Lib/Hls/segments.py +++ b/Src/Lib/Downloader/HLS/segments.py @@ -27,7 +27,7 @@ from Src.Util.call_stack import get_call_stack # Logic class -from ..M3U8 import ( +from ...M3U8 import ( M3U8_Decryption, M3U8_Ts_Estimator, M3U8_Parser, @@ -53,7 +53,7 @@ PROXY_START_MAX = config_manager.get_float('REQUESTS', 'proxy_start_max') # Variable -headers_index = config_manager.get_dict('REQUESTS', 'index') +headers_index = config_manager.get_dict('REQUESTS', 'user-agent') @@ -68,6 +68,7 @@ class M3U8_Segments: """ self.url = url self.tmp_folder = tmp_folder + self.expected_real_time = None self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts") os.makedirs(self.tmp_folder, exist_ok=True) @@ -90,7 +91,7 @@ class M3U8_Segments: Returns: bytes: The encryption key in bytes. """ - headers_index['user-agent'] = get_headers() + headers_index = {'user-agent': get_headers()} # Construct the full URL of the key key_uri = urljoin(self.url, m3u8_parser.keys.get('uri')) @@ -123,8 +124,9 @@ class M3U8_Segments: m3u8_parser = M3U8_Parser() m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content) - console.log(f"[red]Expected duration after download: {m3u8_parser.get_duration()}") - console.log(f"[red]There is key: [yellow]{m3u8_parser.keys is not None}") + #console.log(f"[red]Expected duration after download: {m3u8_parser.get_duration()}") + #console.log(f"[red]There is key: [yellow]{m3u8_parser.keys is not None}") + self.expected_real_time = m3u8_parser.get_duration(return_string=False) # Check if there is an encryption key in the playlis if m3u8_parser.keys is not None: @@ -170,7 +172,7 @@ class M3U8_Segments: """ Makes a request to the index M3U8 file to get information about segments. """ - headers_index['user-agent'] = get_headers() + headers_index = {'user-agent': get_headers()} # Send a GET request to retrieve the index M3U8 file response = httpx.get(self.url, headers=headers_index) @@ -206,14 +208,14 @@ class M3U8_Segments: proxy = self.valid_proxy[index % len(self.valid_proxy)] logging.info(f"Use proxy: {proxy}") - with httpx.Client(transport=httpx.HTTPTransport(retries=3), proxies=proxy, verify=REQUEST_VERIFY) as client: + with httpx.Client(proxies=proxy, verify=REQUEST_VERIFY) as client: if 'key_base_url' in self.__dict__: response = client.get(ts_url, headers=random_headers(self.key_base_url), timeout=REQUEST_TIMEOUT) else: response = client.get(ts_url, headers={'user-agent': get_headers()}, timeout=REQUEST_TIMEOUT) else: - with httpx.Client(transport=httpx.HTTPTransport(retries=3), verify=REQUEST_VERIFY) as client_2: + with httpx.Client(verify=REQUEST_VERIFY) as client_2: if 'key_base_url' in self.__dict__: response = client_2.get(ts_url, headers=random_headers(self.key_base_url), timeout=REQUEST_TIMEOUT) else: @@ -298,16 +300,28 @@ class M3U8_Segments: # Custom bar for mobile and pc if TQDM_USE_LARGE_BAR: - bar_format=f"{Colors.YELLOW}Downloading {Colors.WHITE}({add_desc}{Colors.WHITE}): {Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ {Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] {Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + bar_format = ( + f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{add_desc}{Colors.WHITE}): " + f"{Colors.RED}{{percentage:.2f}}% " + f"{Colors.MAGENTA}{{bar}} " + f"{Colors.WHITE}[ {Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] " + f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + ) else: - bar_format=f"{Colors.YELLOW}Proc{Colors.WHITE}: {Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + bar_format = ( + f"{Colors.YELLOW}Proc{Colors.WHITE}: " + f"{Colors.RED}{{percentage:.2f}}% " + f"{Colors.WHITE}| " + f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + ) # Create progress bar progress_bar = tqdm( total=len(self.segments), unit='s', ascii='░▒█', - bar_format=bar_format + bar_format=bar_format, + mininterval=0.05 ) # Start a separate thread to write segments to the file diff --git a/Src/Lib/Downloader/MP4/__init__.py b/Src/Lib/Downloader/MP4/__init__.py new file mode 100644 index 0000000..3480381 --- /dev/null +++ b/Src/Lib/Downloader/MP4/__init__.py @@ -0,0 +1,3 @@ +# 23.06.24 + +from .downloader import MP4_downloader \ No newline at end of file diff --git a/Src/Lib/Hls/download_mp4.py b/Src/Lib/Downloader/MP4/downloader.py similarity index 68% rename from Src/Lib/Hls/download_mp4.py rename to Src/Lib/Downloader/MP4/downloader.py index 6a8d3b6..22bb146 100644 --- a/Src/Lib/Hls/download_mp4.py +++ b/Src/Lib/Downloader/MP4/downloader.py @@ -15,11 +15,11 @@ from Src.Util.headers import get_headers from Src.Util.color import Colors from Src.Util.console import console, Panel from Src.Util._jsonConfig import config_manager -from Src.Util.os import format_size +from Src.Util.os import format_file_size # Logic class -from ..FFmpeg import print_duration_table +from ...FFmpeg import print_duration_table # Config @@ -29,19 +29,32 @@ REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') -def MP4_downloader(url: str, path: str, referer: str, add_desc: str): +def MP4_downloader(url: str, path: str, referer: str): + """ + Downloads an MP4 video from a given URL using the specified referer header. + + Parameter: + - url (str): The URL of the MP4 video to download. + - path (str): The local path where the downloaded MP4 file will be saved. + - referer (str): The referer header value to include in the HTTP request headers. + """ + # Make request to get content of video logging.info(f"Make request to fetch mp4 from: {url}") - headers = {'Referer': referer, 'user-agent': get_headers()} + + if referer != None: + headers = {'Referer': referer, 'user-agent': get_headers()} + else: + headers = {'user-agent': get_headers()} with httpx.Client(verify=REQUEST_VERIFY, timeout=REQUEST_TIMEOUT) as client: - with client.stream("GET", url, headers=headers, timeout=99) as response: + with client.stream("GET", url, headers=headers, timeout=10) as response: total = int(response.headers.get('content-length', 0)) # Create bar format if TQDM_USE_LARGE_BAR: - bar_format = (f"{Colors.YELLOW}Downloading {Colors.WHITE}({add_desc}{Colors.WHITE}): " + bar_format = (f"{Colors.YELLOW}[MP4] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] " f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}} {Colors.WHITE}| " @@ -53,11 +66,11 @@ def MP4_downloader(url: str, path: str, referer: str, add_desc: str): # Create progress bar progress_bar = tqdm( total=total, - unit='iB', ascii='░▒█', bar_format=bar_format, unit_scale=True, - unit_divisor=1024 + unit_divisor=1024, + mininterval=0.05 ) # Download file @@ -70,8 +83,8 @@ def MP4_downloader(url: str, path: str, referer: str, add_desc: str): # Get summary console.print(Panel( f"[bold green]Download completed![/bold green]\n" - f"File size: [bold red]{format_size(os.path.getsize(path))}[/bold red]\n" - f"Duration: [bold]{print_duration_table(path, show=False)}[/bold]", + f"[cyan]File size: [bold red]{format_file_size(os.path.getsize(path))}[/bold red]\n" + f"[cyan]Duration: [bold]{print_duration_table(path, description=False, return_string=True)}[/bold]", title=f"{os.path.basename(path.replace('.mp4', ''))}", border_style="green" )) \ No newline at end of file diff --git a/Src/Lib/Downloader/TOR/__init__.py b/Src/Lib/Downloader/TOR/__init__.py new file mode 100644 index 0000000..a7a28e7 --- /dev/null +++ b/Src/Lib/Downloader/TOR/__init__.py @@ -0,0 +1,3 @@ +# 23.06.24 + +from .downloader import TOR_downloader \ No newline at end of file diff --git a/Src/Lib/Downloader/TOR/downloader.py b/Src/Lib/Downloader/TOR/downloader.py new file mode 100644 index 0000000..d878b85 --- /dev/null +++ b/Src/Lib/Downloader/TOR/downloader.py @@ -0,0 +1,209 @@ +# 23.06.24 + +import os +import time +import shutil +import logging + + +# Internal utilities +from Src.Util.color import Colors +from Src.Util.os import format_file_size, format_transfer_speed +from Src.Util._jsonConfig import config_manager + + +# External libraries +from tqdm import tqdm +from qbittorrent import Client + + +# Tor config +HOST = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['host']) +PORT = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['port']) +USERNAME = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['user']) +PASSWORD = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['pass']) + +# Config +TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') +REQUEST_VERIFY = config_manager.get_float('REQUESTS', 'verify_ssl') +REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') + + + + +class TOR_downloader: + def __init__(self): + """ + Initializes the TorrentManager instance. + + Parameters: + - host (str): IP address or hostname of the qBittorrent Web UI. + - port (int): Port number of the qBittorrent Web UI. + - username (str): Username for logging into qBittorrent. + - password (str): Password for logging into qBittorrent. + """ + self.qb = Client(f'http://{HOST}:{PORT}/') + self.username = USERNAME + self.password = PASSWORD + self.logged_in = False + self.save_path = None + self.torrent_name = None + + self.login() + + def login(self): + """ + Logs into the qBittorrent Web UI. + """ + try: + self.qb.login(self.username, self.password) + self.logged_in = True + logging.info("Successfully logged in to qBittorrent.") + + except Exception as e: + logging.error(f"Failed to log in: {str(e)}") + self.logged_in = False + + def add_magnet_link(self, magnet_link): + """ + Adds a torrent via magnet link to qBittorrent. + + Parameters: + - magnet_link (str): Magnet link of the torrent to be added. + """ + try: + self.qb.download_from_link(magnet_link) + logging.info("Added magnet link to qBittorrent.") + + # Get the hash of the latest added torrent + torrents = self.qb.torrents() + if torrents: + self.latest_torrent_hash = torrents[-1]['hash'] + logging.info(f"Latest torrent hash: {self.latest_torrent_hash}") + + except Exception as e: + logging.error(f"Failed to add magnet link: {str(e)}") + + + def start_download(self): + """ + Starts downloading the latest added torrent and monitors progress. + """ + try: + + torrents = self.qb.torrents() + if not torrents: + logging.error("No torrents found.") + return + + # Sleep to load magnet to qbit app + time.sleep(5) + latest_torrent = torrents[-1] + torrent_hash = latest_torrent['hash'] + + # Custom bar for mobile and pc + if TQDM_USE_LARGE_BAR: + bar_format = ( + f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " + f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " + f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + ) + else: + bar_format = ( + f"{Colors.YELLOW}Proc{Colors.WHITE}: " + f"{Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| " + f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + ) + + progress_bar = tqdm( + total=100, + ascii='░▒█', + bar_format=bar_format, + unit_scale=True, + unit_divisor=1024, + mininterval=0.05 + ) + + with progress_bar as pbar: + while True: + + # Get variable from qtorrent + torrent_info = self.qb.get_torrent(torrent_hash) + self.save_path = torrent_info['save_path'] + self.torrent_name = torrent_info['name'] + + # Fetch important variable + pieces_have = torrent_info['pieces_have'] + pieces_num = torrent_info['pieces_num'] + progress = (pieces_have / pieces_num) * 100 if pieces_num else 0 + pbar.n = progress + + download_speed = torrent_info['dl_speed'] + total_size = torrent_info['total_size'] + downloaded_size = torrent_info['total_downloaded'] + + # Format variable + downloaded_size_str = format_file_size(downloaded_size) + downloaded_size = downloaded_size_str.split(' ')[0] + + total_size_str = format_file_size(total_size) + total_size = total_size_str.split(' ')[0] + total_size_unit = total_size_str.split(' ')[1] + + average_internet_str = format_transfer_speed(download_speed) + average_internet = average_internet_str.split(' ')[0] + average_internet_unit = average_internet_str.split(' ')[1] + + # Update the progress bar's postfix + if TQDM_USE_LARGE_BAR: + pbar.set_postfix_str( + f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size} {Colors.WHITE}< {Colors.GREEN}{total_size} {Colors.RED}{total_size_unit} " + f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}" + ) + else: + pbar.set_postfix_str( + f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size}{Colors.RED} {total_size} " + f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}" + ) + + pbar.refresh() + time.sleep(0.2) + + # Break at the end + if int(progress) == 100: + break + + except KeyboardInterrupt: + logging.info("Download process interrupted.") + + except Exception as e: + logging.error(f"Download error: {str(e)}") + + def move_downloaded_files(self, destination=None): + """ + Moves downloaded files of the latest torrent to another location. + + Parameters: + - save_path (str): Current save path (output directory) of the torrent. + - destination (str, optional): Destination directory to move files. If None, moves to current directory. + + Returns: + - bool: True if files are moved successfully, False otherwise. + """ + + # List directories in the save path + dirs = [d for d in os.listdir(self.save_path) if os.path.isdir(os.path.join(self.save_path, d))] + + for dir_name in dirs: + if dir_name in self.torrent_name : + dir_path = os.path.join(self.save_path, dir_name) + if destination: + destination_path = os.path.join(destination, dir_name) + else: + destination_path = os.path.join(os.getcwd(), dir_name) + + shutil.move(dir_path, destination_path) + logging.info(f"Moved directory {dir_name} to {destination_path}") + break + + return True \ No newline at end of file diff --git a/Src/Lib/Downloader/__init__.py b/Src/Lib/Downloader/__init__.py new file mode 100644 index 0000000..d01f8a5 --- /dev/null +++ b/Src/Lib/Downloader/__init__.py @@ -0,0 +1,5 @@ +# 23.06.24 + +from .HLS import HLS_Downloader +from .MP4 import MP4_downloader +from .TOR import TOR_downloader \ No newline at end of file diff --git a/Src/Lib/E_Table/__init__.py b/Src/Lib/E_Table/__init__.py deleted file mode 100644 index e61ffb3..0000000 --- a/Src/Lib/E_Table/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# 20.05.24 - -from .sql_table import SimpleDBManager, report_table -report_table: SimpleDBManager = report_table diff --git a/Src/Lib/E_Table/sql_table.py b/Src/Lib/E_Table/sql_table.py deleted file mode 100644 index 6a10393..0000000 --- a/Src/Lib/E_Table/sql_table.py +++ /dev/null @@ -1,212 +0,0 @@ -# 20.05.24 - -import csv -import logging - - -# Internal utilities -from Src.Util._jsonConfig import config_manager - - -# Variable -CREATE_REPORT = config_manager.get_bool('M3U8_DOWNLOAD', 'create_report') - - -class SimpleDBManager: - def __init__(self, filename, columns): - """ - Initialize a new database manager. - - Args: - - filename (str): The name of the CSV file containing the database. - - columns (list): List of database columns. - """ - self.filename = filename - self.db = [] - self.columns = columns - logging.info("Database manager initialized.") - - def load_database(self): - """ - Load the database from the specified CSV file. - If the file doesn't exist, initialize a new database. - """ - try: - with open(self.filename, 'r', newline='') as file: - reader = csv.reader(file) - self.db = list(reader) - logging.info(f"Database {self.filename} loaded successfully.") - - except FileNotFoundError: - logging.warning(f"File {self.filename} not found, creating a new database...") - self.initialize_database() - - def initialize_database(self, columns=None, rows=None): - """ - Initialize a new database with specified columns and rows. - - Args: - - columns (list, optional): List of database columns. If not specified, uses the columns provided in the constructor. - - rows (list, optional): List of database rows. Each row should be a list of values. If not specified, the database will be empty. - """ - self.db = [self.columns] - if rows: - for row_data in rows: - self.add_row_to_database(row_data) - - logging.info("Database initialized successfully.") - - def add_row_to_database(self, *row_data): - """ - Add a new row to the database. - - Args: - - row_data (list): List of values for the new row. - """ - self.db.append(list(row_data)) - logging.info("New row added to the database.") - - def add_column_to_database(self, column_name, default_value=''): - """ - Add a new column to the database. - - Args: - - column_name (str): Name of the new column. - - default_value (str, optional): Default value to be inserted in cells of the new column. Default is an empty string. - """ - for row in self.db: - row.append(default_value) - self.db[0][-1] = column_name - logging.info(f"New column '{column_name}' added to the database.") - - def update_row_in_database(self, row_index, new_row_data): - """ - Update an existing row in the database. - - Args: - - row_index (int): Index of the row to update. - - new_row_data (list): List of the new values for the updated row. - """ - self.db[row_index] = new_row_data - logging.info(f"Row {row_index} of the database updated.") - - def remove_row_from_database(self, column_index: int, search_value) -> list: - """ - Remove a row from the database based on a specific column value. - - Args: - - column_index (int): Index of the column to search in. - - search_value: The value to search for in the specified column. - - Returns: - list: The removed row from the database, if found; otherwise, an empty list. - """ - - # Find the index of the row with the specified value in the specified column - row_index = None - for i, row in enumerate(self.db): - if row[column_index] == search_value: - row_index = i - break - - # If the row with the specified value is found, remove it - remove_row = [] - if row_index is not None: - remove_row = self.db[row_index] - del self.db[row_index] - logging.info(f"Row at index {row_index} with value {search_value} in column {column_index} removed from the database.") - else: - logging.warning(f"No row found with value {search_value} in column {column_index}. Nothing was removed from the database.") - - return remove_row - - def remove_column_from_database(self, col_index): - """ - Remove a column from the database. - - Args: - - col_index (int): Index of the column to remove. - """ - for row in self.db: - del row[col_index] - logging.info(f"Column {col_index} of the database removed.") - - def save_database(self): - """ - Save the database to the CSV file specified in the constructor. - """ - with open(self.filename, 'w', newline='') as file: - writer = csv.writer(file) - writer.writerows(self.db) - logging.info("Database saved to file.") - - def print_database_as_sql(self): - """ - Print the database in SQL format to the console. - """ - max_lengths = [max(len(str(cell)) for cell in col) for col in zip(*self.db)] - line = "+-" + "-+-".join("-" * length for length in max_lengths) + "-+" - print(line) - print("| " + " | ".join(f"{cell:<{length}}" for cell, length in zip(self.db[0], max_lengths)) + " |") - print(line) - for row in self.db[1:]: - print("| " + " | ".join(f"{cell:<{length}}" for cell, length in zip(row, max_lengths)) + " |") - print(line) - logging.info("Database printed as SQL.") - - def search_database(self, column_index, value): - """ - Search the database for rows matching the specified value in the given column. - - Args: - - column_index (int): Index of the column to search on. - - value (str): Value to search for. - - Returns: - list: List of rows matching the search. - """ - results = [] - for row in self.db[1:]: - if row[column_index] == value: - results.append(row) - logging.info(f"Database searched for value '{value}' in column {column_index}. Found {len(results)} matches.") - return results - - def sort_database(self, column_index): - """ - Sort the database based on values in the specified column. - - Args: - - column_index (int): Index of the column to sort on. - """ - self.db[1:] = sorted(self.db[1:], key=lambda x: x[column_index]) - logging.info(f"Database sorted based on column {column_index}.") - - def filter_database(self, column_index, condition): - """ - Filter the database based on a condition on the specified column. - - Args: - - column_index (int): Index of the column to apply the condition on. - - condition (function): Condition function to apply on the values of the column. Should return True or False. - - Returns: - list: List of rows satisfying the condition. - """ - results = [self.db[0]] # Keep the header row - for row in self.db[1:]: - if condition(row[column_index]): - results.append(row) - logging.info(f"Filter applied on column {column_index}. Found {len(results) - 1} rows satisfying the condition.") - return results - - - - -# Output -if CREATE_REPORT: - report_table = SimpleDBManager("riepilogo.csv", ["Date", "Name", "Size"]) - report_table.load_database() - report_table.save_database() -else: - report_table = None \ No newline at end of file diff --git a/Src/Lib/FFmpeg/capture.py b/Src/Lib/FFmpeg/capture.py index 561820e..50727c8 100644 --- a/Src/Lib/FFmpeg/capture.py +++ b/Src/Lib/FFmpeg/capture.py @@ -12,7 +12,7 @@ from typing import Tuple # Internal utilities from Src.Util.console import console -from Src.Util.os import format_size +from Src.Util.os import format_file_size # Variable @@ -61,12 +61,11 @@ def capture_output(process: subprocess.Popen, description: str) -> None: else: byte_size = int(re.findall(r'\d+', data.get('size', '0'))[0]) * 1000 - time_now = datetime.now().strftime('%H:%M:%S') # Construct the progress string with formatted output information - progress_string = (f"[blue][{time_now}][purple] FFmpeg [white][{description}[white]]: " + progress_string = (f"[yellow][FFmpeg] [white][{description}[white]]: " f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], " - f"[green]'size': [yellow]{format_size(byte_size)}[white])") + f"[green]'size': [yellow]{format_file_size(byte_size)}[white])") max_length = max(max_length, len(progress_string)) # Print the progress string to the console, overwriting the previous line diff --git a/Src/Lib/FFmpeg/util.py b/Src/Lib/FFmpeg/util.py index 1f46cf4..b2b3693 100644 --- a/Src/Lib/FFmpeg/util.py +++ b/Src/Lib/FFmpeg/util.py @@ -92,22 +92,44 @@ def format_duration(seconds: float) -> Tuple[int, int, int]: return int(hours), int(minutes), int(seconds) -def print_duration_table(file_path: str, show = True) -> None: +def print_duration_table(file_path: str, description: str = "Duration", return_string: bool = False): """ - Print duration of a video file in hours, minutes, and seconds. + Print the duration of a video file in hours, minutes, and seconds, or return it as a formatted string. Args: - file_path (str): The path to the video file. + - description (str): Optional description to be included in the output. Defaults to "Duration". If not provided, the duration will not be printed. + - return_string (bool): If True, returns the formatted duration string. If False, returns a dictionary with hours, minutes, and seconds. + + Returns: + - 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. + + Example usage: + >>> print_duration_table("path/to/video.mp4") + [cyan]Duration for [white]([green]video.mp4[white]): [yellow]1[red]h [yellow]1[red]m [yellow]1[red]s + + >>> print_duration_table("path/to/video.mp4", description=None) + '[yellow]1[red]h [yellow]1[red]m [yellow]1[red]s' + + >>> print_duration_table("path/to/video.mp4", description=None, return_string=False) + {'h': 1, 'm': 1, 's': 1} """ video_duration = get_video_duration(file_path) if video_duration is not None: hours, minutes, seconds = format_duration(video_duration) - 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") + formatted_duration = f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s" + duration_dict = {'h': hours, 'm': minutes, 's': seconds} + + if description: + console.print(f"[cyan]{description} for [white]([green]{os.path.basename(file_path)}[white]): {formatted_duration}") else: - return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s" + if return_string: + return formatted_duration + else: + return duration_dict def get_ffprobe_info(file_path): diff --git a/Src/Lib/Hls/__init__.py b/Src/Lib/Hls/__init__.py deleted file mode 100644 index 181935e..0000000 --- a/Src/Lib/Hls/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# 20.02.24 - -from .downloader import Downloader \ No newline at end of file diff --git a/Src/Lib/M3U8/estimator.py b/Src/Lib/M3U8/estimator.py index 6af05bd..808c52b 100644 --- a/Src/Lib/M3U8/estimator.py +++ b/Src/Lib/M3U8/estimator.py @@ -14,7 +14,7 @@ from tqdm import tqdm # Internal utilities from Src.Util.color import Colors -from Src.Util.os import format_size +from Src.Util.os import format_file_size, format_transfer_speed from Src.Util._jsonConfig import config_manager @@ -64,15 +64,6 @@ class M3U8_Ts_Estimator: io_counters = psutil.net_io_counters() return io_counters - def format_bytes(bytes): - if bytes < 1024: - return f"{bytes:.2f} Bytes/s" - elif bytes < 1024 * 1024: - return f"{bytes / 1024:.2f} KB/s" - else: - return f"{bytes / (1024 * 1024):.2f} MB/s" - - # Get proc id pid = os.getpid() @@ -88,8 +79,8 @@ class M3U8_Ts_Estimator: download_speed = (new_value.bytes_recv - old_value.bytes_recv) / interval self.speed = ({ - "upload": format_bytes(upload_speed), - "download": format_bytes(download_speed) + "upload": format_transfer_speed(upload_speed), + "download": format_transfer_speed(download_speed) }) old_value = new_value @@ -103,7 +94,7 @@ class M3U8_Ts_Estimator: float: The average internet speed in Mbps. """ with self.lock: - return self.speed['download'].split(" ") + return self.speed['download'].split(" ") def calculate_total_size(self) -> str: """ @@ -120,7 +111,7 @@ class M3U8_Ts_Estimator: mean_size = total_size / len(self.ts_file_sizes) # Return formatted mean size - return format_size(mean_size) + return format_file_size(mean_size) except ZeroDivisionError as e: logging.error("Division by zero error occurred: %s", e) @@ -137,7 +128,7 @@ class M3U8_Ts_Estimator: Returns: str: The total downloaded size as a human-readable string. """ - return format_size(self.now_downloaded_size) + return format_file_size(self.now_downloaded_size) def update_progress_bar(self, total_downloaded: int, duration: float, progress_counter: tqdm) -> None: """ diff --git a/Src/Lib/M3U8/parser.py b/Src/Lib/M3U8/parser.py index 94e2bbf..4d13e95 100644 --- a/Src/Lib/M3U8/parser.py +++ b/Src/Lib/M3U8/parser.py @@ -584,20 +584,34 @@ class M3U8_Parser: self._audio = M3U8_Audio(self.audio_playlist) self._subtitle = M3U8_Subtitle(self.subtitle_playlist) - def get_duration(self): + def get_duration(self, return_string:bool = True): """ Convert duration from seconds to hours, minutes, and remaining seconds. Parameters: - - seconds (float): Duration in seconds. + - return_string (bool): If True, returns the formatted duration string. + If False, returns a dictionary with hours, minutes, and seconds. Returns: - - formatted_duration (str): Formatted duration string with hours, minutes, and seconds. + - formatted_duration (str): Formatted duration string with hours, minutes, and seconds if return_string is True. + - duration_dict (dict): Dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds respectively if return_string is False. + + Example usage: + >>> obj = YourClass(duration=3661) + >>> obj.get_duration() + '[yellow]1[red]h [yellow]1[red]m [yellow]1[red]s' + >>> obj.get_duration(return_string=False) + {'h': 1, 'm': 1, 's': 1} """ + # Calculate hours, minutes, and remaining seconds - hours = int(self.duration / 3600) - minutes = int((self.duration % 3600) / 60) - remaining_seconds = int(self.duration % 60) + hours, remainder = divmod(self.duration, 3600) + minutes, seconds = divmod(remainder, 60) + + # Format the duration string with colors - return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(remaining_seconds)}[red]s" + if return_string: + return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s" + else: + return {'h': int(hours), 'm': int(minutes), 's': int(seconds)} diff --git a/Src/Util/os.py b/Src/Util/os.py index 23a188d..3ab458c 100644 --- a/Src/Util/os.py +++ b/Src/Util/os.py @@ -23,11 +23,13 @@ from typing import List # External library +import httpx import unicodedata # Internal utilities from .console import console +from .headers import get_headers @@ -316,32 +318,45 @@ def clean_json(path: str) -> None: -# --> OS MANAGE SIZE FILE -def format_size(size_bytes: float) -> str: +# --> OS MANAGE SIZE FILE AND INTERNET SPEED +def format_file_size(size_bytes: float) -> str: """ - Format the size in bytes into a human-readable format. + Formats a file size from bytes into a human-readable string representation. Args: - - size_bytes (float): The size in bytes to be formatted. + size_bytes (float): Size in bytes to be formatted. Returns: - str: The formatted size. + str: Formatted string representing the file size with appropriate unit (B, KB, MB, GB, TB). """ - if size_bytes <= 0: return "0B" units = ['B', 'KB', 'MB', 'GB', 'TB'] unit_index = 0 - # Convert bytes to appropriate unit while size_bytes >= 1024 and unit_index < len(units) - 1: size_bytes /= 1024 unit_index += 1 - # Round the size to two decimal places and return with the appropriate unit return f"{size_bytes:.2f} {units[unit_index]}" +def format_transfer_speed(bytes: float) -> str: + """ + Formats a transfer speed from bytes per second into a human-readable string representation. + + Args: + bytes (float): Speed in bytes per second to be formatted. + + Returns: + str: Formatted string representing the transfer speed with appropriate unit (Bytes/s, KB/s, MB/s). + """ + if bytes < 1024: + return f"{bytes:.2f} Bytes/s" + elif bytes < 1024 * 1024: + return f"{bytes / 1024:.2f} KB/s" + else: + return f"{bytes / (1024 * 1024):.2f} MB/s" @@ -452,7 +467,6 @@ def get_system_summary(): console.print(f"[cyan]Python[white]: [bold red]{python_version} ({python_implementation} {arch}) - {os_info} ({openssl_version}, {glibc_version})[/bold red]") logging.info(f"Python: {python_version} ({python_implementation} {arch}) - {os_info} ({openssl_version}, {glibc_version})") - # ffmpeg and ffprobe versions ffmpeg_version = get_executable_version(['ffmpeg', '-version']) @@ -524,6 +538,79 @@ def run_node_script(script_content: str) -> str: import os os.remove('script.js') +def run_node_script_api(script_content: str) -> str: + """ + Runs a Node.js script and returns its output. + + Args: + script_content (str): The content of the Node.js script to run. + + Returns: + str: The output of the Node.js script. + """ + + headers = { + 'accept': '*/*', + 'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7', + 'dnt': '1', + 'origin': 'https://onecompiler.com', + 'priority': 'u=1, i', + 'referer': 'https://onecompiler.com/javascript', + 'user-agent': get_headers() + } + + json_data = { + 'name': 'JavaScript', + 'title': '42gyum6qn', + 'version': 'ES6', + 'mode': 'javascript', + 'description': None, + 'extension': 'js', + 'languageType': 'programming', + 'active': False, + 'properties': { + 'language': 'javascript', + 'docs': False, + 'tutorials': False, + 'cheatsheets': False, + 'filesEditable': False, + 'filesDeletable': False, + 'files': [ + { + 'name': 'index.js', + 'content': script_content + }, + ], + 'newFileOptions': [ + { + 'helpText': 'New JS file', + 'name': 'script${i}.js', + 'content': "/**\n * In main file\n * let script${i} = require('./script${i}');\n * console.log(script${i}.sum(1, 2));\n */\n\nfunction sum(a, b) {\n return a + b;\n}\n\nmodule.exports = { sum };", + }, + { + 'helpText': 'Add Dependencies', + 'name': 'package.json', + 'content': '{\n "name": "main_app",\n "version": "1.0.0",\n "description": "",\n "main": "HelloWorld.js",\n "dependencies": {\n "lodash": "^4.17.21"\n }\n}', + }, + ], + }, + '_id': '42gcvpkbg_42gyuud7m', + 'user': None, + 'visibility': 'public', + } + + # Return error + response = httpx.post('https://onecompiler.com/api/code/exec', headers=headers, json=json_data) + response.raise_for_status() + + if response.status_code == 200: + return str(response.json()['stderr']).split("\n")[1] + + else: + logging.error("Cant connect to site: onecompiler.com") + sys.exit(0) + + # --> OS FILE VALIDATOR diff --git a/Test/bandwidth_gui.py b/Test/bandwidth_gui.py index 15bd0b9..ddd1363 100644 --- a/Test/bandwidth_gui.py +++ b/Test/bandwidth_gui.py @@ -1,8 +1,14 @@ -import tkinter as tk -from threading import Thread, Lock -from collections import deque -import psutil +# 23.06.24 + import time +from collections import deque +from threading import Thread, Lock + + +# External library +import psutil +import tkinter as tk + class NetworkMonitor: def __init__(self, maxlen=10): @@ -38,6 +44,7 @@ class NetworkMonitor: old_value = new_value + class NetworkMonitorApp: def __init__(self, root): self.monitor = NetworkMonitor() @@ -77,7 +84,8 @@ class NetworkMonitorApp: self.monitor_thread = Thread(target=self.monitor.capture_speed, args=(0.5,), daemon=True) self.monitor_thread.start() -if __name__ == "__main__": - root = tk.Tk() - app = NetworkMonitorApp(root) - root.mainloop() + + +root = tk.Tk() +app = NetworkMonitorApp(root) +root.mainloop() diff --git a/Test/t_down_hls.py b/Test/t_down_hls.py new file mode 100644 index 0000000..eedeef5 --- /dev/null +++ b/Test/t_down_hls.py @@ -0,0 +1,9 @@ +# 23.06.24 + +from Src.Lib.Downloader import HLS_Downloader + + +HLS_Downloader( + output_filename="EP_1.mp4", + m3u8_playlist="" +) \ No newline at end of file diff --git a/Test/t_down_mp4.py b/Test/t_down_mp4.py new file mode 100644 index 0000000..d443dea --- /dev/null +++ b/Test/t_down_mp4.py @@ -0,0 +1,8 @@ +# 23.06.24 + +from Src.Lib.Downloader import MP4_downloader + +MP4_downloader( + "", + "EP_1.mp4" +) diff --git a/Test/t_down_tor.py b/Test/t_down_tor.py new file mode 100644 index 0000000..a7ef491 --- /dev/null +++ b/Test/t_down_tor.py @@ -0,0 +1,10 @@ +# 23.06.24 + +from Src.Lib.Downloader import TOR_downloader + +manager = TOR_downloader() + +magnet_link = "magnet:?x" +manager.add_magnet_link(magnet_link) +manager.start_download() +manager.move_downloaded_files() diff --git a/config.json b/config.json index c9c7489..3f6c331 100644 --- a/config.json +++ b/config.json @@ -8,15 +8,19 @@ "root_path": "Video", "map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)", "auto_update_domain": true, + "config_qbit_tor": { + "host": "192.168.1.1", + "port": "8080", + "user": "admin", + "pass": "admin" + }, "not_close": false }, "REQUESTS": { "timeout": 10, "max_retry": 3, "verify_ssl": false, - "index": { - "user-agent": "" - }, + "user-agent": "", "proxy_start_min": 0.1, "proxy_start_max": 0.5, "proxy": [] @@ -75,7 +79,12 @@ "ddlstreamitaly": { "video_workers": -1, "audio_workers": -1, - "domain": "co" + "domain": "co", + "cookie": { + "ips4_device_key": "", + "ips4_member_id": "", + "ips4_login_key": "" + } } } } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9782604..35a5e13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ tqdm m3u8 psutil unidecode -fake-useragent \ No newline at end of file +fake-useragent +qbittorrent-api \ No newline at end of file