diff --git a/.gitignore b/.gitignore index fb8905f..177124e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,7 @@ venv.bak/ Video note.txt list_proxy.txt -cmd.txt \ No newline at end of file +cmd.txt +bot_config.json +active_requests.json +scripts.json diff --git a/README.md b/README.md index 2a17245..4d419dc 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,6 @@ PyPI Downloads - - Lines of Code -

# 📋 Table of Contents @@ -39,6 +36,7 @@ - 📝 [Manual Installation](#3-manual-installation) - 💻 [Win 7](https://github.com/Ghost6446/StreamingCommunity_api/wiki/Installation#win-7) - 📱 [Termux](https://github.com/Ghost6446/StreamingCommunity_api/wiki/Termux) + - 📝 [Telegram Usage](#4-telegram-usage) - ⚙️ [Configuration](#configuration) - 🔧 [Default](#default-settings) - 📩 [Request](#requests-settings) @@ -258,6 +256,34 @@ python3 update.py
+## 4. Telegram Usage + +### Configuration + +You need to create an .env file and enter your Telegram token + +and user ID to authorize only one user to use it + +### .env Example: + +``` +TOKEN_TELEGRAM=IlTuo2131TOKEN$12D3Telegram +AUTHORIZED_USER_ID=12345678 +DEBUG=False +``` + +### Install Python Dependencies + +```bash +pip install -r requirements.txt +``` + +### On Linux/MacOS: + +```bash +python3 telegram_bot.py +``` + # Configuration You can change some behaviors by tweaking the configuration file. @@ -471,12 +497,12 @@ The `run-container` command mounts also the `config.json` file, so any change to | Website | Status | |:-------------------|:------:| | [1337xx](https://1337xx.to/) | ✅ | -| [AltadefinizioneGratis](https://altadefinizionegratis.site/) | ✅ | +| [AltadefinizioneGratis](https://altadefinizionegratis.info/) | ✅ | | [AnimeUnity](https://animeunity.so/) | ✅ | | [Ilcorsaronero](https://ilcorsaronero.link/) | ✅ | | [CB01New](https://cb01new.video/) | ✅ | | [DDLStreamItaly](https://ddlstreamitaly.co/) | ✅ | -| [GuardaSerie](https://guardaserie.meme/) | ✅ | +| [GuardaSerie](https://guardaserie.academy/) | ✅ | | [MostraGuarda](https://mostraguarda.stream/) | ✅ | | [StreamingCommunity](https://streamingcommunity.paris/) | ✅ | diff --git a/StreamingCommunity/Api/Site/1337xx/__init__.py b/StreamingCommunity/Api/Site/1337xx/__init__.py index 9a9ee35..4652162 100644 --- a/StreamingCommunity/Api/Site/1337xx/__init__.py +++ b/StreamingCommunity/Api/Site/1337xx/__init__.py @@ -11,6 +11,12 @@ from StreamingCommunity.Util.console import console, msg from .site import title_search, run_get_select_title, media_search_manager from .title import download_title +# Telegram bot instance +from telegram_bot import get_bot_instance +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') +import sys +import subprocess # Variable indice = 8 @@ -25,10 +31,25 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): """ Main function of the application for film and series. """ + if TELEGRAM_BOT: + bot = get_bot_instance() - if string_to_search is None: + # Chiedi la scelta all'utente con il bot Telegram + string_to_search = bot.ask( + "key_search", + f"Inserisci la parola da cercare\noppure 🔙 back per tornare alla scelta: ", + None + ) + + if string_to_search == 'back': + # Riavvia lo script + # Chiude il processo attuale e avvia una nuova istanza dello script + subprocess.Popen([sys.executable] + sys.argv) + sys.exit() + else: + if string_to_search is None: string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() - + # Search on database len_database = title_search(quote_plus(string_to_search)) diff --git a/StreamingCommunity/Api/Site/1337xx/site.py b/StreamingCommunity/Api/Site/1337xx/site.py index 4ad4590..6f50e23 100644 --- a/StreamingCommunity/Api/Site/1337xx/site.py +++ b/StreamingCommunity/Api/Site/1337xx/site.py @@ -17,6 +17,10 @@ from StreamingCommunity.Api.Template import get_select_title from StreamingCommunity.Api.Template.Util import search_domain from StreamingCommunity.Api.Template.Class.SearchType import MediaManager +# Telegram bot instance +from telegram_bot import get_bot_instance +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + # Variable from .costant import SITE_NAME, DOMAIN_NOW @@ -36,6 +40,10 @@ def title_search(word_to_search: str) -> int: Returns: - int: The number of titles found. """ + + if TELEGRAM_BOT: + bot = get_bot_instance() + media_search_manager.clear() table_show_manager.clear() @@ -48,8 +56,8 @@ def title_search(word_to_search: str) -> int: # Construct the full site URL and load the search page try: response = httpx.get( - url=f"https://{SITE_NAME}.{domain_to_use}/search/{word_to_search}/1/", - headers={'user-agent': get_headers()}, + url=f"https://{SITE_NAME}.{domain_to_use}/search/{word_to_search}/1/", + headers={'user-agent': get_headers()}, follow_redirects=True, timeout=max_timeout ) @@ -61,6 +69,10 @@ def title_search(word_to_search: str) -> int: # Create soup and find table soup = BeautifulSoup(response.text, "html.parser") + if TELEGRAM_BOT: + # Inizializza la lista delle scelte + choices = [] + for tr in soup.find_all('tr'): try: @@ -72,12 +84,23 @@ def title_search(word_to_search: str) -> int: 'date': tr.find_all("td")[-3].get_text(strip=True).replace("'", ""), 'size': tr.find_all("td")[-2].get_text(strip=True) } - - media_search_manager.add_media(title_info) + + if TELEGRAM_BOT: + # Crea una stringa formattata per ogni scelta con numero + choice_text = f"{len(choices)} - {title_info.get('name')} ({title_info.get('type')}) - {title_info.get('date')}" + choices.append(choice_text) + + media_search_manager.add_media(title_info) except Exception as e: print(f"Error parsing a film entry: {e}") + if TELEGRAM_BOT: + # Se ci sono scelte, inviale a Telegram + if choices: + # Invio a telegram la lista + bot.send_message(f"Lista dei risultati:", choices) + # Return the number of titles found return media_search_manager.get_length() diff --git a/StreamingCommunity/Api/Site/1337xx/title.py b/StreamingCommunity/Api/Site/1337xx/title.py index 06a5551..de8a4c8 100644 --- a/StreamingCommunity/Api/Site/1337xx/title.py +++ b/StreamingCommunity/Api/Site/1337xx/title.py @@ -23,6 +23,11 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem # Config from .costant import DOMAIN_NOW, SITE_NAME, MOVIE_FOLDER +# Telegram bot instance +from telegram_bot import get_bot_instance +from session import get_session, updateScriptId, deleteScriptId +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') def download_title(select_title: MediaItem): """ @@ -31,6 +36,12 @@ def download_title(select_title: MediaItem): Parameters: - select_title (MediaItem): The media item to be downloaded. This should be an instance of the MediaItem class, containing attributes like `name` and `url`. """ + if TELEGRAM_BOT: + bot = get_bot_instance() + bot.send_message(f"Download in corso:\n{select_title.name}", None) + script_id = get_session() + if script_id != "unknown": + updateScriptId(script_id, select_title.name) start_message() console.print(f"[yellow]Download: [red]{select_title.name} \n") @@ -38,7 +49,9 @@ def download_title(select_title: MediaItem): # Define output path title_name = os_manager.get_sanitize_file(select_title.name) - mp4_path = os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + mp4_path = os_manager.get_sanitize_path( + os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + ) # Create output folder os_manager.create_path(mp4_path) @@ -62,3 +75,9 @@ def download_title(select_title: MediaItem): manager.add_magnet_link(final_url) manager.start_download() manager.move_downloaded_files(mp4_path) + + if TELEGRAM_BOT: + # Delete script_id + script_id = get_session() + if script_id != "unknown": + deleteScriptId(script_id) diff --git a/StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py b/StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py index b5d540b..35123c1 100644 --- a/StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py +++ b/StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py @@ -11,6 +11,10 @@ from StreamingCommunity.Util.console import console, msg from .site import title_search, run_get_select_title, media_search_manager from .film import download_film +# Telegram bot instance +from telegram_bot import get_bot_instance +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') # Variable indice = 2 @@ -26,8 +30,25 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): Main function of the application for film and series. """ - if string_to_search is None: - string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() + if TELEGRAM_BOT: + bot = get_bot_instance() + + if string_to_search is None: + # Chiedi la scelta all'utente con il bot Telegram + string_to_search = bot.ask( + "key_search", + f"Inserisci la parola da cercare\noppure 🔙 back per tornare alla scelta: ", + None + ) + + if string_to_search == 'back': + # Riavvia lo script + # Chiude il processo attuale e avvia una nuova istanza dello script + subprocess.Popen([sys.executable] + sys.argv) + sys.exit() + else: + if string_to_search is None: + string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() # Search on database len_database = title_search(quote_plus(string_to_search)) @@ -45,6 +66,10 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): download_film(select_title) else: + + if TELEGRAM_BOT: + bot.send_message(f"Nessun risultato trovato riprova", None) + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") # Retry diff --git a/StreamingCommunity/Api/Site/altadefinizionegratis/film.py b/StreamingCommunity/Api/Site/altadefinizionegratis/film.py index ea83bef..8662b6f 100644 --- a/StreamingCommunity/Api/Site/altadefinizionegratis/film.py +++ b/StreamingCommunity/Api/Site/altadefinizionegratis/film.py @@ -24,6 +24,12 @@ from StreamingCommunity.Api.Player.supervideo import VideoSource # Config from .costant import MOVIE_FOLDER +# Telegram bot instance +from telegram_bot import get_bot_instance +from session import get_session, updateScriptId, deleteScriptId +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + def download_film(select_title: MediaItem) -> str: """ @@ -37,17 +43,29 @@ def download_film(select_title: MediaItem) -> str: - str: output path """ + if TELEGRAM_BOT: + bot = get_bot_instance() + + # Invio a telegram + bot.send_message(f"Download in corso:\n{select_title.name}", None) + + # Get script_id + script_id = get_session() + if script_id != "unknown": + updateScriptId(script_id, select_title.name) + # Start message and display film information start_message() console.print(f"[yellow]Download: [red]{select_title.name} \n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") - + # Set domain and media ID for the video source video_source = VideoSource(select_title.url) # Define output path title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4" - mp4_path = os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + mp4_path = os_manager.get_sanitize_path( + os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + ) # Get m3u8 master playlist master_playlist = video_source.get_playlist() @@ -65,7 +83,13 @@ def download_film(select_title: MediaItem) -> str: if msg.ask("[green]Do you want to continue [white]([red]y[white])[green] or return at home[white]([red]n[white]) ", choices=['y', 'n'], default='y', show_choices=True) == "n": frames = get_call_stack() execute_search(frames[-4])""" - + + if TELEGRAM_BOT: + # Delete script_id + script_id = get_session() + if script_id != "unknown": + deleteScriptId(script_id) + if r_proc != None: console.print("[green]Result: ") console.print(r_proc) diff --git a/StreamingCommunity/Api/Site/altadefinizionegratis/site.py b/StreamingCommunity/Api/Site/altadefinizionegratis/site.py index 20b89fd..36947a0 100644 --- a/StreamingCommunity/Api/Site/altadefinizionegratis/site.py +++ b/StreamingCommunity/Api/Site/altadefinizionegratis/site.py @@ -25,6 +25,11 @@ table_show_manager = TVShowManager() max_timeout = config_manager.get_int("REQUESTS", "timeout") disable_searchDomain = config_manager.get_bool("DEFAULT", "disable_searchDomain") +# Telegram bot instance +from telegram_bot import get_bot_instance +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + def title_search(title_search: str) -> int: """ @@ -36,6 +41,9 @@ def title_search(title_search: str) -> int: Returns: int: The number of titles found. """ + if TELEGRAM_BOT: + bot = get_bot_instance() + media_search_manager.clear() table_show_manager.clear() @@ -63,6 +71,10 @@ def title_search(title_search: str) -> int: # Create soup and find table soup = BeautifulSoup(response.text, "html.parser") + if TELEGRAM_BOT: + # Inizializza la lista delle scelte + choices = [] + for row in soup.find_all('div', class_='col-lg-3 col-md-3 col-xs-4'): try: @@ -81,8 +93,20 @@ def title_search(title_search: str) -> int: media_search_manager.add_media(film_info) + if TELEGRAM_BOT: + # Crea una stringa formattata per ogni scelta con numero + choice_text = f"{len(choices)} - {film_info.get('name')} ({film_info.get('url')}) {film_info.get('score')}" + choices.append(choice_text) + except AttributeError as e: print(f"Error parsing a film entry: {e}") + + + if TELEGRAM_BOT: + # Se ci sono scelte, inviale a Telegram + if choices: + # Invio a telegram la lista + bot.send_message(f"Lista dei risultati:", choices) # Return the number of titles found return media_search_manager.get_length() diff --git a/StreamingCommunity/Api/Site/animeunity/__init__.py b/StreamingCommunity/Api/Site/animeunity/__init__.py index 613c9aa..4107680 100644 --- a/StreamingCommunity/Api/Site/animeunity/__init__.py +++ b/StreamingCommunity/Api/Site/animeunity/__init__.py @@ -11,6 +11,13 @@ from StreamingCommunity.Util.console import console, msg from .site import title_search, run_get_select_title, media_search_manager from .film_serie import download_film, download_series +# Telegram bot instance +from telegram_bot import get_bot_instance +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') +import sys +import subprocess + # Variable indice = 1 @@ -23,8 +30,25 @@ from .costant import SITE_NAME def search(string_to_search: str = None, get_onylDatabase: bool = False): - if string_to_search is None: - string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() + if TELEGRAM_BOT: + bot = get_bot_instance() + + if string_to_search is None: + # Chiedi la scelta all'utente con il bot Telegram + string_to_search = bot.ask( + "key_search", + f"Inserisci la parola da cercare\noppure 🔙 back per tornare alla scelta: ", + None + ) + + if string_to_search == 'back': + # Riavvia lo script + # Chiude il processo attuale e avvia una nuova istanza dello script + subprocess.Popen([sys.executable] + sys.argv) + sys.exit() + else: + if string_to_search is None: + string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() # Search on database len_database = title_search(string_to_search) @@ -43,8 +67,12 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): else: download_series(select_title) - + else: + + if TELEGRAM_BOT: + bot.send_message(f"Nessun risultato trovato riprova", None) + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") # Retry diff --git a/StreamingCommunity/Api/Site/animeunity/film_serie.py b/StreamingCommunity/Api/Site/animeunity/film_serie.py index 575c9e9..82c7802 100644 --- a/StreamingCommunity/Api/Site/animeunity/film_serie.py +++ b/StreamingCommunity/Api/Site/animeunity/film_serie.py @@ -24,10 +24,16 @@ from StreamingCommunity.Api.Player.vixcloud import VideoSourceAnime # Variable from .costant import SITE_NAME, ANIME_FOLDER, MOVIE_FOLDER -KILL_HANDLER = bool(False) -def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_source: VideoSourceAnime) -> tuple[str,bool]: +# Telegram bot instance +from telegram_bot import get_bot_instance +from session import get_session, updateScriptId, deleteScriptId +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + + +def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_source: VideoSourceAnime, list_episode_select = None) -> str: """ Downloads the selected episode. @@ -36,8 +42,9 @@ def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_so Return: - str: output path - - bool: kill handler status """ + if TELEGRAM_BOT: + bot = get_bot_instance() # Get information about the selected episode obj_episode = scrape_serie.get_info_episode(index_select) @@ -46,13 +53,22 @@ def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_so start_message() console.print(f"[yellow]Download: [red]EP_{obj_episode.number} \n") - console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") - + + if TELEGRAM_BOT: + # Invio a telegram + bot.send_message(f"Download in corso:\nTitolo:{scrape_serie.series_name}\nEpisodio: {obj_episode.number}", None) + + # Get script_id + script_id = get_session() + if script_id != "unknown": + updateScriptId(script_id, f"{scrape_serie.series_name} - E{obj_episode.number}") + # Collect mp4 url video_source.get_embed(obj_episode.id) # Create output path - title_name = f"{obj_episode.number}.mp4" + #title_name = f"{obj_episode.number}.mp4" + title_name = f"{scrape_serie.series_name}_EP_{obj_episode.number}.mp4" if scrape_serie.is_series: mp4_path = os_manager.get_sanitize_path( @@ -64,15 +80,14 @@ def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_so ) # Create output folder - os_manager.create_path(mp4_path) + os_manager.create_path(mp4_path) # Start downloading - r_proc = MP4_downloader( url=str(video_source.src_mp4).strip(), path=os.path.join(mp4_path, title_name) ) - + if r_proc != None: console.print("[green]Result: ") console.print(r_proc) @@ -91,6 +106,9 @@ def download_series(select_title: MediaItem): - tv_id (int): The ID of the TV series. - tv_name (str): The name of the TV series. """ + if TELEGRAM_BOT: + bot = get_bot_instance() + scrape_serie = ScrapeSerieAnime(SITE_NAME) video_source = VideoSourceAnime(SITE_NAME) @@ -101,23 +119,40 @@ def download_series(select_title: MediaItem): episoded_count = scrape_serie.get_count_episodes() console.print(f"[cyan]Episodes find: [red]{episoded_count}") - # Prompt user to select an episode index - last_command = msg.ask("\n[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]or [red][3-*] [cyan]for a range of media") + if TELEGRAM_BOT: + console.print(f"\n[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]or [red][3-*] [cyan]for a range of media") + + # Invio a telegram + bot.send_message(f"Episodi trovati: {episoded_count}", None) + + last_command = bot.ask( + "select_title", + f"Inserisci l'indice del media o (*) per scaricare tutti i media, oppure [1-2] o [3-*] per un intervallo di media.", + None + ) + else: + # Prompt user to select an episode index + last_command = msg.ask("\n[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]or [red][3-*] [cyan]for a range of media") # Manage user selection list_episode_select = manage_selection(last_command, episoded_count) # Download selected episodes if len(list_episode_select) == 1 and last_command != "*": - download_episode(list_episode_select[0]-1, scrape_serie, video_source)[0] + download_episode(list_episode_select[0]-1, scrape_serie, video_source, list_episode_select=list_episode_select) # Download all other episodes selecter else: - kill_handler=bool(False) for i_episode in list_episode_select: - if kill_handler: - break - kill_handler= download_episode(i_episode-1, scrape_serie, video_source)[1] + download_episode(i_episode-1, scrape_serie, video_source, list_episode_select=list_episode_select) + + if TELEGRAM_BOT: + bot.send_message(f"Finito di scaricare tutte le serie e episodi", None) + + # Get script_id + script_id = get_session() + if script_id != "unknown": + deleteScriptId(script_id) def download_film(select_title: MediaItem): diff --git a/StreamingCommunity/Api/Site/animeunity/site.py b/StreamingCommunity/Api/Site/animeunity/site.py index daaa571..6984626 100644 --- a/StreamingCommunity/Api/Site/animeunity/site.py +++ b/StreamingCommunity/Api/Site/animeunity/site.py @@ -19,6 +19,11 @@ from StreamingCommunity.Api.Template import get_select_title from StreamingCommunity.Api.Template.Util import search_domain from StreamingCommunity.Api.Template.Class.SearchType import MediaManager +# Telegram bot instance +from telegram_bot import get_bot_instance +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + # Variable from .costant import SITE_NAME, DOMAIN_NOW @@ -103,6 +108,9 @@ def title_search(title: str) -> int: Returns: - int: A number containing the length of media search manager. """ + if TELEGRAM_BOT: + bot = get_bot_instance() + media_search_manager.clear() table_show_manager.clear() @@ -146,12 +154,16 @@ def title_search(title: str) -> int: except Exception as e: console.print(f"Site: {SITE_NAME}, request search error: {e}") + if TELEGRAM_BOT: + # Inizializza la lista delle scelte + choices = [] + for dict_title in response.json()['records']: try: - - # Rename keys for consistency + # Rinomina le chiavi per consistenza dict_title['name'] = get_real_title(dict_title) + # Aggiungi al media manager media_search_manager.add_media({ 'id': dict_title.get('id'), 'slug': dict_title.get('slug'), @@ -161,8 +173,18 @@ def title_search(title: str) -> int: 'episodes_count': dict_title.get('episodes_count') }) + if TELEGRAM_BOT: + # Crea una stringa formattata per ogni scelta con numero + choice_text = f"{len(choices)} - {dict_title.get('name')} ({dict_title.get('type')}) - Episodi: {dict_title.get('episodes_count')}" + choices.append(choice_text) + except Exception as e: print(f"Error parsing a film entry: {e}") + if TELEGRAM_BOT: + # Se ci sono scelte, inviale a Telegram + if choices: + # Invio a telegram la lista + bot.send_message(f"Lista dei risultati:", choices) # Return the length of media search manager return media_search_manager.get_length() diff --git a/StreamingCommunity/Api/Site/cb01new/film.py b/StreamingCommunity/Api/Site/cb01new/film.py index b886cac..11a1304 100644 --- a/StreamingCommunity/Api/Site/cb01new/film.py +++ b/StreamingCommunity/Api/Site/cb01new/film.py @@ -38,7 +38,6 @@ def download_film(select_title: MediaItem) -> str: # Start message and display film information start_message() console.print(f"[yellow]Download: [red]{select_title.name} \n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") # Setup api manger print(select_title.url) @@ -46,7 +45,9 @@ def download_film(select_title: MediaItem) -> str: # Define output path title_name = os_manager.get_sanitize_file(select_title.name) +".mp4" - mp4_path = os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + mp4_path = os_manager.get_sanitize_path( + os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + ) # Get m3u8 master playlist master_playlist = video_source.get_playlist() diff --git a/StreamingCommunity/Api/Site/ddlstreamitaly/series.py b/StreamingCommunity/Api/Site/ddlstreamitaly/series.py index 743f636..ec18bf1 100644 --- a/StreamingCommunity/Api/Site/ddlstreamitaly/series.py +++ b/StreamingCommunity/Api/Site/ddlstreamitaly/series.py @@ -28,7 +28,7 @@ from .costant import SERIES_FOLDER -def download_video(index_episode_selected: int, scape_info_serie: GetSerieInfo, video_source: VideoSource) -> tuple[str,bool]: +def download_video(index_episode_selected: int, scape_info_serie: GetSerieInfo, video_source: VideoSource) -> str: """ Download a single episode video. @@ -38,14 +38,13 @@ def download_video(index_episode_selected: int, scape_info_serie: GetSerieInfo, Return: - str: output path - - bool: kill handler status """ start_message() # Get info about episode obj_episode = scape_info_serie.list_episodes[index_episode_selected - 1] - console.print(f"[yellow]Download: [red]{obj_episode.get('name')}\n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") + console.print(f"[yellow]Download: [red]{obj_episode.get('name')}") + print() # Define filename and path for the downloaded video title_name = os_manager.get_sanitize_file( @@ -71,7 +70,7 @@ def download_video(index_episode_selected: int, scape_info_serie: GetSerieInfo, path=os.path.join(mp4_path, title_name), referer=f"{parsed_url.scheme}://{parsed_url.netloc}/", ) - + if r_proc != None: console.print("[green]Result: ") console.print(r_proc) @@ -106,11 +105,8 @@ def download_thread(dict_serie: MediaItem): return # Download selected episodes - kill_handler = bool(False) for i_episode in list_episode_select: - if kill_handler: - break - kill_handler = download_video(i_episode, scape_info_serie, video_source)[1] + download_video(i_episode, scape_info_serie, video_source) def display_episodes_list(obj_episode_manager) -> str: diff --git a/StreamingCommunity/Api/Site/guardaserie/series.py b/StreamingCommunity/Api/Site/guardaserie/series.py index 93a2658..70d8fa0 100644 --- a/StreamingCommunity/Api/Site/guardaserie/series.py +++ b/StreamingCommunity/Api/Site/guardaserie/series.py @@ -44,8 +44,9 @@ def download_video(index_season_selected: int, index_episode_selected: int, scap # Get info about episode obj_episode = scape_info_serie.list_episodes[index_episode_selected - 1] - console.print(f"[yellow]Download: [red]{index_season_selected}:{index_episode_selected} {obj_episode.get('name')}\n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") + console.print(f"[yellow]Download: [red]{index_season_selected}:{index_episode_selected} {obj_episode.get('name')}") + print() + # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scape_info_serie.tv_name, index_season_selected, index_episode_selected, obj_episode.get('name'))}.mp4" mp4_path = os.path.join(SERIES_FOLDER, scape_info_serie.tv_name, f"S{index_season_selected}") @@ -69,7 +70,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scap if msg.ask("[green]Do you want to continue [white]([red]y[white])[green] or return at home[white]([red]n[white]) ", choices=['y', 'n'], default='y', show_choices=True) == "n": frames = get_call_stack() execute_search(frames[-4])""" - + if r_proc != None: console.print("[green]Result: ") console.print(r_proc) @@ -112,10 +113,7 @@ def download_episode(scape_info_serie: GetSerieInfo, index_season_selected: int, return # Download selected episodes - stopped = bool(False) for i_episode in list_episode_select: - if stopped: - break download_video(index_season_selected, i_episode, scape_info_serie) diff --git a/StreamingCommunity/Api/Site/ilcorsaronero/title.py b/StreamingCommunity/Api/Site/ilcorsaronero/title.py index e32af4d..886de89 100644 --- a/StreamingCommunity/Api/Site/ilcorsaronero/title.py +++ b/StreamingCommunity/Api/Site/ilcorsaronero/title.py @@ -32,7 +32,9 @@ def download_title(select_title: MediaItem): # Define output path title_name = os_manager.get_sanitize_file(select_title.name) - mp4_path = os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + mp4_path = os_manager.get_sanitize_path( + os.path.join(MOVIE_FOLDER, title_name.replace(".mp4", "")) + ) # Create output folder os_manager.create_path(mp4_path) diff --git a/StreamingCommunity/Api/Site/mostraguarda/film.py b/StreamingCommunity/Api/Site/mostraguarda/film.py index 2e04228..b7889d9 100644 --- a/StreamingCommunity/Api/Site/mostraguarda/film.py +++ b/StreamingCommunity/Api/Site/mostraguarda/film.py @@ -50,7 +50,7 @@ def download_film(movie_details: Json_film) -> str: # Start message and display film information start_message() console.print(f"[yellow]Download: [red]{movie_details.title} \n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") + # Make request to main site try: url = f"https://{SITE_NAME}.{DOMAIN_NOW}/set-movie-a/{movie_details.imdb_id}" diff --git a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py index 7f93feb..16bc91f 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py @@ -1,7 +1,8 @@ # 21.05.24 from urllib.parse import quote_plus - +import subprocess +import sys # Internal utilities from StreamingCommunity.Util.console import console, msg @@ -12,6 +13,10 @@ from .site import get_version_and_domain, title_search, run_get_select_title, me from .film import download_film from .series import download_series +# Telegram bot instance +from StreamingCommunity.Util._jsonConfig import config_manager +from telegram_bot import get_bot_instance +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') # Variable indice = 0 @@ -27,8 +32,25 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): Main function of the application for film and series. """ - if string_to_search is None: - string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() + if TELEGRAM_BOT: + bot = get_bot_instance() + + if string_to_search is None: + # Chiedi la scelta all'utente con il bot Telegram + string_to_search = bot.ask( + "key_search", + f"Inserisci la parola da cercare\noppure 🔙 back per tornare alla scelta: ", + None + ) + + if string_to_search == 'back': + # Riavvia lo script + # Chiude il processo attuale e avvia una nuova istanza dello script + subprocess.Popen([sys.executable] + sys.argv) + sys.exit() + else: + if string_to_search is None: + string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip() # Get site domain and version and get result of the search site_version, domain = get_version_and_domain() @@ -37,20 +59,23 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): # Return list of elements if get_onylDatabase: return media_search_manager - + if len_database > 0: # Select title from list select_title = run_get_select_title() - + if select_title.type == 'tv': download_series(select_title, site_version) - + else: download_film(select_title) - + else: console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") + if TELEGRAM_BOT: + bot.send_message(f"Nessun risultato trovato riprova", None) + # Retry search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/film.py b/StreamingCommunity/Api/Site/streamingcommunity/film.py index eebfcdd..4ade5d6 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/film.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/film.py @@ -23,7 +23,13 @@ from StreamingCommunity.Api.Player.vixcloud import VideoSource # Variable from .costant import SITE_NAME, MOVIE_FOLDER - + +# Telegram bot instance +from telegram_bot import get_bot_instance +from session import get_session, updateScriptId, deleteScriptId +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + def download_film(select_title: MediaItem) -> str: """ @@ -36,11 +42,20 @@ def download_film(select_title: MediaItem) -> str: Return: - str: output path """ + if TELEGRAM_BOT: + bot = get_bot_instance() + + # Invio a telegram + bot.send_message(f"Download in corso:\n{select_title.name}", None) + + # Get script_id + script_id = get_session() + if script_id != "unknown": + updateScriptId(script_id, select_title.name) # Start message and display film information start_message() console.print(f"[yellow]Download: [red]{select_title.name} \n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") # Init class video_source = VideoSource(SITE_NAME, False) @@ -57,10 +72,16 @@ def download_film(select_title: MediaItem) -> str: # Download the film using the m3u8 playlist, and output filename r_proc = HLS_Downloader( - m3u8_playlist=master_playlist, + m3u8_playlist=master_playlist, output_filename=os.path.join(mp4_path, title_name) ).start() + if TELEGRAM_BOT: + # Delete script_id + script_id = get_session() + if script_id != "unknown": + deleteScriptId(script_id) + """if r_proc == 404: time.sleep(2) diff --git a/StreamingCommunity/Api/Site/streamingcommunity/series.py b/StreamingCommunity/Api/Site/streamingcommunity/series.py index 16b3d3b..8fe16fc 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/series.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/series.py @@ -26,9 +26,14 @@ from StreamingCommunity.Api.Player.vixcloud import VideoSource # Variable from .costant import SITE_NAME, SERIES_FOLDER +# Telegram bot instance +from telegram_bot import get_bot_instance +from session import get_session, updateScriptId, deleteScriptId +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') -def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: ScrapeSerie, video_source: VideoSource) -> tuple[str,bool]: +def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: ScrapeSerie, video_source: VideoSource) -> str: """ Download a single episode video. @@ -38,16 +43,31 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra Return: - str: output path - - bool: kill handler status """ + start_message() index_season_selected = dynamic_format_number(index_season_selected) # Get info about episode obj_episode = scrape_serie.episode_manager.get(index_episode_selected - 1) - console.print(f"[yellow]Download: [red]{index_season_selected}:{index_episode_selected} {obj_episode.name}\n") - console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n") - + console.print(f"[yellow]Download: [red]{index_season_selected}:{index_episode_selected} {obj_episode.name}") + print() + + if TELEGRAM_BOT: + bot = get_bot_instance() + + # Invio a telegram + bot.send_message( + f"Download in corso\nSerie: {scrape_serie.series_name}\nStagione: {index_season_selected}\nEpisodio: {index_episode_selected}\nTitolo: {obj_episode.name}", + None + ) + + # Get script_id + script_id = get_session() + if script_id != "unknown": + # Update script_id + updateScriptId(script_id, f"{scrape_serie.series_name} - S{index_season_selected} - E{index_episode_selected} - {obj_episode.name}") + # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.mp4" mp4_path = os.path.join(SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") @@ -56,13 +76,15 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra video_source.get_iframe(obj_episode.id) video_source.get_content() master_playlist = video_source.get_playlist() - + # Download the episode r_proc = HLS_Downloader( m3u8_playlist=master_playlist, output_filename=os.path.join(mp4_path, mp4_name) ).start() - + + #bot.send_message(f"Serie scaricata tutta", None) + """if r_proc == 404: time.sleep(2) @@ -74,6 +96,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra if r_proc != None: console.print("[green]Result: ") console.print(r_proc) + #bot.send_message(f"Episodio scaricato", None) return os.path.join(mp4_path, mp4_name) @@ -84,8 +107,11 @@ def download_episode(index_season_selected: int, scrape_serie: ScrapeSerie, vide Parameters: - index_season_selected (int): Index of the selected season. - download_all (bool): Download all episodes in the season. + - list_season_select (list): Lista delle stagioni selezionate per il download. """ + #bot = get_bot_instance() + # Clean memory of all episodes and get the number of the season scrape_serie.episode_manager.clear() @@ -101,6 +127,8 @@ def download_episode(index_season_selected: int, scrape_serie: ScrapeSerie, vide download_video(index_season_selected, i_episode, scrape_serie, video_source) console.print(f"\n[red]End downloaded [yellow]season: [red]{index_season_selected}.") + #bot.send_message(f"Finito di scaricare la stagione: {index_season_selected}", None) + else: # Display episodes list and manage user selection @@ -111,14 +139,12 @@ def download_episode(index_season_selected: int, scrape_serie: ScrapeSerie, vide list_episode_select = validate_episode_selection(list_episode_select, episodes_count) except ValueError as e: console.print(f"[red]{str(e)}") + #bot.send_message(f"{str(e)}", None) return - # Download selected episodes if not stopped - stopped = bool(False) + # Download selected episodes for i_episode in list_episode_select: - if stopped: - break - stopped=download_video(index_season_selected, i_episode, scrape_serie, video_source)[1] + download_video(index_season_selected, i_episode, scrape_serie, video_source) def download_series(select_season: MediaItem, version: str) -> None: """ @@ -129,6 +155,8 @@ def download_series(select_season: MediaItem, version: str) -> None: - domain (str): Domain from which to download. - version (str): Version of the site. """ + if TELEGRAM_BOT: + bot = get_bot_instance() # Start message and set up video source start_message() @@ -147,11 +175,24 @@ def download_series(select_season: MediaItem, version: str) -> None: # Prompt user for season selection and download episodes console.print(f"\n[green]Seasons found: [red]{seasons_count}") - index_season_selected = msg.ask( - "\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, " - "[yellow](e.g., 1-2) [cyan]for a range of seasons, or [yellow](e.g., 3-*) [cyan]to download from a specific season to the end" - ) - + + if TELEGRAM_BOT: + console.print("\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, " + "[yellow](e.g., 1-2) [cyan]for a range of seasons, or [yellow](e.g., 3-*) [cyan]to download from a specific season to the end") + + bot.send_message(f"Stagioni trovate: {seasons_count}", None) + + index_season_selected = bot.ask( + "select_title_episode", + "Inserisci il numero della stagione (es. 1), * per scaricare tutte le stagioni, (es. 1-2) per un intervallo di stagioni, o (es. 3-*) per scaricare dalla stagione specificata fino alla fine", + None + ) + else: + index_season_selected = msg.ask( + "\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, " + "[yellow](e.g., 1-2) [cyan]for a range of seasons, or [yellow](e.g., 3-*) [cyan]to download from a specific season to the end" + ) + # Manage and validate the selection list_season_select = manage_selection(index_season_selected, seasons_count) @@ -172,6 +213,13 @@ def download_series(select_season: MediaItem, version: str) -> None: # Otherwise, let the user select specific episodes for the single season download_episode(i_season, scrape_serie, video_source, download_all=False) + if TELEGRAM_BOT: + bot.send_message(f"Finito di scaricare tutte le serie e episodi", None) + # Get script_id + script_id = get_session() + if script_id != "unknown": + deleteScriptId(script_id) + def display_episodes_list(scrape_serie) -> str: """ @@ -180,6 +228,8 @@ def display_episodes_list(scrape_serie) -> str: Returns: last_command (str): Last command entered by the user. """ + if TELEGRAM_BOT: + bot = get_bot_instance() # Set up table for displaying episodes table_show_manager = TVShowManager() @@ -194,6 +244,8 @@ def display_episodes_list(scrape_serie) -> str: table_show_manager.add_column(column_info) # Populate the table with episodes information + if TELEGRAM_BOT: + choices = [] for i, media in enumerate(scrape_serie.episode_manager.episodes): table_show_manager.add_tv_show({ 'Index': str(media.number), @@ -201,6 +253,22 @@ def display_episodes_list(scrape_serie) -> str: 'Duration': str(media.duration) }) + if TELEGRAM_BOT: + # Creazione della stringa per il messaggio Telegram + choice_text = f"{media.number} - {media.name} ({media.duration} min)" + choices.append(choice_text) + + if TELEGRAM_BOT: + # creo episoded_count + #episoded_count = len(scrape_serie.episode_manager.episodes) + + # Invio a telegram + #bot.send_message(f"Episodi trovati: {episoded_count}", None) + + # Invia la lista degli episodi al bot Telegram + if choices: + bot.send_message(f"Lista episodi:", choices) + # Run the table and handle user input last_command = table_show_manager.run() diff --git a/StreamingCommunity/Api/Site/streamingcommunity/site.py b/StreamingCommunity/Api/Site/streamingcommunity/site.py index 19c2acb..a8b21fd 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/site.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/site.py @@ -27,6 +27,9 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaManager # Config from .costant import SITE_NAME, DOMAIN_NOW +# Telegram bot instance +from telegram_bot import get_bot_instance +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') # Variable media_search_manager = MediaManager() @@ -47,8 +50,8 @@ def get_version(domain: str): """ try: response = httpx.get( - url=f"https://{SITE_NAME}.{domain}/", - headers={'User-Agent': get_headers()}, + url=f"https://{SITE_NAME}.{domain}/", + headers={'User-Agent': get_headers()}, timeout=max_timeout ) response.raise_for_status() @@ -61,7 +64,7 @@ def get_version(domain: str): #console.print(f"[cyan]Get version [white]=> [red]{version} \n") return version - + except Exception as e: logging.error(f"Error extracting version: {e}") raise @@ -88,38 +91,37 @@ def get_version_and_domain(): #console.print("[green]Auto generate version ...") #version = secrets.token_hex(32 // 2) version = None - + return version, domain_to_use def title_search(title_search: str, domain: str) -> int: - """ - Search for titles based on a search query. - - Parameters: - - title_search (str): The title to search for. - - domain (str): The domain to search on. - - Returns: - int: The number of titles found. - """ media_search_manager.clear() table_show_manager.clear() - + + if TELEGRAM_BOT: + bot = get_bot_instance() + try: response = httpx.get( - url=f"https://{SITE_NAME}.{domain}/api/search?q={title_search.replace(' ', '+')}", - headers={'user-agent': get_headers()}, + url=f"https://{SITE_NAME}.{domain}/api/search?q={title_search.replace(' ', '+')}", + headers={'user-agent': get_headers()}, timeout=max_timeout ) response.raise_for_status() except Exception as e: + if TELEGRAM_BOT: + bot.send_message(f"Sito: {SITE_NAME}, errore richiesta ricerca", choices) console.print(f"Site: {SITE_NAME}, request search error: {e}") + return 0 - for dict_title in response.json()['data']: - try: - + try: + if TELEGRAM_BOT: + # Prepara le scelte per l'utente + choices = [] + for i, dict_title in enumerate(response.json()['data']): + # Aggiungi al media manager media_search_manager.add_media({ 'id': dict_title.get('id'), 'slug': dict_title.get('slug'), @@ -129,10 +131,20 @@ def title_search(title_search: str, domain: str) -> int: 'score': dict_title.get('score') }) - except Exception as e: - print(f"Error parsing a film entry: {e}") + if TELEGRAM_BOT: + # Crea una stringa formattata per ogni scelta con numero + choice_text = f"{i} - {dict_title.get('name')} ({dict_title.get('type')}) - {dict_title.get('last_air_date')}" + choices.append(choice_text) + + except Exception as e: + print(f"Error parsing entries: {e}") + return 0 + + if TELEGRAM_BOT: + if choices: + # Invio a telegram la lista + bot.send_message(f"Lista dei risultati:", choices) - # Return the number of titles found return media_search_manager.get_length() diff --git a/StreamingCommunity/Api/Template/Util/get_domain.py b/StreamingCommunity/Api/Template/Util/get_domain.py index 53b444f..0c7fb09 100644 --- a/StreamingCommunity/Api/Template/Util/get_domain.py +++ b/StreamingCommunity/Api/Template/Util/get_domain.py @@ -7,7 +7,7 @@ from urllib.parse import urlparse, unquote # External libraries import httpx -from googlesearch import search +from serpapi import search # Internal utilities @@ -15,6 +15,7 @@ from StreamingCommunity.Util.headers import get_headers from StreamingCommunity.Util.console import console, msg from StreamingCommunity.Util._jsonConfig import config_manager +api_key = "ebfaafb043613442e0010e3795c9ead4cab196e5448a6e3728d64edbbccdf731" base_headers = { 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7', @@ -75,7 +76,7 @@ def get_base_url(url_str): except Exception: return None -def validate_url(url, base_url, max_timeout, max_retries=2, sleep=1): +def validate_url(url, base_url, max_timeout, max_retries=3, sleep=3): """Validate if URL is accessible and matches expected base domain.""" console.print(f"\n[cyan]Starting validation for URL[white]: [yellow]{url}") @@ -163,9 +164,16 @@ def search_domain(site_name: str, base_url: str, get_first: bool = False): console.print(f"\n[cyan]Searching for alternate domains for[white]: [yellow]{base_domain}") try: - search_results = list(search(base_domain, num_results=20, lang="it")) - + params = { + "q": base_domain, + "hl": "it", + "gl": "it", + "num": 20, + "api_key": api_key + } + search_results = [element['link'] for element in search(params)['organic_results']] base_urls = set() + for url in search_results: element_url = get_base_url(url) if element_url: diff --git a/StreamingCommunity/Lib/Downloader/HLS/downloader.py b/StreamingCommunity/Lib/Downloader/HLS/downloader.py index 2f4bf23..fdaab35 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/downloader.py +++ b/StreamingCommunity/Lib/Downloader/HLS/downloader.py @@ -1,9 +1,11 @@ # 17.10.24 import os +import re import sys import time import logging +import subprocess # External libraries @@ -35,11 +37,17 @@ from ...M3U8 import ( ) from .segments import M3U8_Segments +# Telegram bot instance +from telegram_bot import get_bot_instance +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') # Config -DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio') +DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio') DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles') +DOWNLOAD_VIDEO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_video') +DOWNLOAD_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_audio') MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio') +DOWNLOAD_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_sub') MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs') REMOVE_SEGMENTS_FOLDER = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder') FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolution') @@ -54,29 +62,6 @@ list_MissingTs = [] -class HttpClient: - def __init__(self, headers: dict = None): - self.headers = headers or {'User-Agent': get_headers()} - self.client = httpx.Client(headers=self.headers, timeout=max_timeout, follow_redirects=True) - - def _make_request(self, url: str, return_content: bool = False): - for attempt in range(RETRY_LIMIT): - try: - response = self.client.get(url) - response.raise_for_status() - return response.content if return_content else response.text - except Exception as e: - logging.error(f"Attempt {attempt+1} failed for {url}: {str(e)}") - time.sleep(1.5 ** attempt) - return None - - def get(self, url: str) -> str: - return self._make_request(url) - - def get_content(self, url: str) -> bytes: - return self._make_request(url, return_content=True) - - class PathManager: def __init__(self, output_filename): """ @@ -86,11 +71,11 @@ class PathManager: output_filename (str): The name of the output file (should end with .mp4). """ self.output_filename = output_filename - + # Create the base path by removing the '.mp4' extension from the output filename self.base_path = str(output_filename).replace(".mp4", "") logging.info(f"class 'PathManager'; set base path: {self.base_path}") - + # Define the path for a temporary directory where segments will be stored self.base_temp = os.path.join(self.base_path, "tmp") self.video_segments_path = os.path.join(self.base_temp, "video") @@ -108,7 +93,66 @@ class PathManager: os.makedirs(self.subtitle_segments_path, exist_ok=True) +class HttpClient: + def __init__(self, headers: str = None): + """ + Initializes the HttpClient with specified headers. + """ + self.headers = headers + + def get(self, url: str): + """ + Sends a GET request to the specified URL and returns the response as text. + + Returns: + str: The response body as text if the request is successful, None otherwise. + """ + logging.info(f"class 'HttpClient'; make request: {url}") + try: + response = httpx.get( + url=url, + headers=self.headers, + timeout=max_timeout, + follow_redirects=True + ) + + response.raise_for_status() + return response.text + + except Exception as e: + console.print(f"Request to {url} failed with error: {e}") + return 404 + + def get_content(self, url): + """ + Sends a GET request to the specified URL and returns the raw response content. + + Returns: + bytes: The response content as bytes if the request is successful, None otherwise. + """ + logging.info(f"class 'HttpClient'; make request: {url}") + try: + response = httpx.get( + url=url, + headers=self.headers, + timeout=max_timeout + ) + + response.raise_for_status() + return response.content # Return the raw response content + + except Exception as e: + logging.error(f"Request to {url} failed: {response.status_code} when get content.") + return None + + class ContentExtractor: + def __init__(self): + """ + This class is responsible for extracting audio, subtitle, and video information from an M3U8 playlist. + """ + pass + def start(self, obj_parse: M3U8_Parser): """ Starts the extraction process by parsing the M3U8 playlist and collecting audio, subtitle, and video data. @@ -116,7 +160,10 @@ class ContentExtractor: Args: obj_parse (str): The M3U8_Parser obj of the M3U8 playlist. """ + self.obj_parse = obj_parse + + # Collect audio, subtitle, and video information self._collect_audio() self._collect_subtitle() self._collect_video() @@ -129,13 +176,13 @@ class ContentExtractor: # Collect available audio tracks and their corresponding URIs and names self.list_available_audio = self.obj_parse._audio.get_all_uris_and_names() - + # Check if there are any audio tracks available; if not, disable download if self.list_available_audio is not None: # Extract available languages from the audio tracks available_languages = [obj_audio.get('language') for obj_audio in self.list_available_audio] - set_language = DOWNLOAD_SPECIFIC_AUDIO + set_language = DOWNLOAD_SPECIFIC_AUDIO downloadable_languages = list(set(available_languages) & set(set_language)) # Only show if there is something available @@ -155,7 +202,7 @@ class ContentExtractor: # Collect available subtitles and their corresponding URIs and names self.list_available_subtitles = self.obj_parse._subtitle.get_all_uris_and_names() - + # Check if there are any subtitles available; if not, disable download if self.list_available_subtitles is not None: @@ -189,7 +236,7 @@ class ContentExtractor: # Otherwise, get the best available video quality self.m3u8_index, video_res = self.obj_parse._video.get_best_uri() - + self.codec: M3U8_Codec = self.obj_parse.codec # List all available resolutions @@ -203,7 +250,7 @@ class ContentExtractor: f"[yellow]Downloadable:[/yellow] [purple]{video_res[0]}x{video_res[1]}[/purple]") if self.codec is not None: - + # Generate the string for available codec information available_codec_info = ( f"[green]v[/green]: [yellow]{self.codec.video_codec_name}[/yellow] " @@ -242,7 +289,7 @@ class ContentExtractor: else: logging.error("[download_m3u8] Can't find a valid m3u8 index") raise ValueError("Invalid m3u8 index URL") - + print("") @@ -325,9 +372,9 @@ class DownloadTracker: # Skip this subtitle if its language is not in the specified list if obj_subtitle.get('language') not in DOWNLOAD_SPECIFIC_SUBTITLE: continue - + sub_language = obj_subtitle.get('language') - + # Construct the full path for the subtitle file sub_full_path = os.path.join(self.subtitle_segments_path, sub_language + ".vtt") @@ -374,7 +421,9 @@ class ContentDownloader: # Download the video streams and print status info_dw = video_m3u8.download_streams(f"{Colors.MAGENTA}video", "video") list_MissingTs.append(info_dw) - self.stopped=list_MissingTs.pop() + + # Print duration information of the downloaded video + #print_duration_table(downloaded_video[0].get('path')) else: console.log("[cyan]Video [red]already exists.") @@ -403,7 +452,9 @@ class ContentDownloader: # Download the audio segments and print status info_dw = audio_m3u8.download_streams(f"{Colors.MAGENTA}audio {Colors.RED}{obj_audio.get('language')}", f"audio_{obj_audio.get('language')}") list_MissingTs.append(info_dw) - self.stopped=list_MissingTs.pop() + + # Print duration information of the downloaded audio + #print_duration_table(obj_audio.get('path')) else: console.log(f"[cyan]Audio [white]([green]{obj_audio.get('language')}[white]) [red]already exists.") @@ -503,7 +554,7 @@ class ContentJoiner: self.downloaded_audio = downloaded_audio self.downloaded_subtitle = downloaded_subtitle self.codec = codec - + # Initialize flags to check if media is available self.converted_out_path = None self.there_is_video = len(downloaded_video) > 0 @@ -540,7 +591,7 @@ class ContentJoiner: # Rename the audio file to the new path os.rename(path, new_path) logging.info(f"Audio moved to {new_path}") - + except Exception as e: logging.error(f"Failed to move audio {path} to {new_path}: {e}") @@ -573,12 +624,12 @@ class ContentJoiner: new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.forced.vtt") else: new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.vtt") - + try: # Rename the subtitle file to the new path os.rename(path, new_path) logging.info(f"Subtitle moved to {new_path}") - + except Exception as e: logging.error(f"Failed to move subtitle {path} to {new_path}: {e}") @@ -664,8 +715,6 @@ class ContentJoiner: class HLS_Downloader: - stopped = bool(False) - def __init__(self, output_filename: str=None, m3u8_playlist: str=None, m3u8_index: str=None, is_playlist_url: bool=True, is_index_url: bool=True): """ Initializes the HLS_Downloader class. @@ -714,7 +763,7 @@ class HLS_Downloader: Returns: str: The generated output filename. """ - root_path = config_manager.get('DEFAULT', 'root_path') + root_path = config_manager.get('DEFAULT', 'root_path') new_filename = None new_folder = os.path.join(root_path, "undefined") logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); destination folder: {new_folder}") @@ -729,8 +778,8 @@ class HLS_Downloader: else: # Check if output_filename contains a folder path - folder, base_name = os.path.split(output_filename) - + folder, base_name = os.path.split(output_filename) + # If no folder is specified, default to 'undefined' if not folder: folder = new_folder @@ -745,17 +794,22 @@ class HLS_Downloader: logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); return path: {new_filename}") return new_filename - + def start(self): """ Initiates the downloading process. Checks if the output file already exists and proceeds with processing the playlist or index. - """ + """ + if TELEGRAM_BOT: + bot = get_bot_instance() + if os.path.exists(self.output_filename): console.log("[red]Output file already exists.") + if TELEGRAM_BOT: + bot.send_message(f"Contenuto già scaricato!", None) return 400 - + self.path_manager.create_directories() - + # Determine whether to process a playlist or index if self.m3u8_playlist: if self.m3u8_playlist is not None: @@ -773,14 +827,14 @@ class HLS_Downloader: if r_proc == 404: return 404 else: - return None - + return None + else: return { 'path': self.output_filename, - 'url': self.m3u8_playlist, + 'url': self.m3u8_playlist } - + else: console.log("[red]Error: URL passed to M3U8_Parser is an index playlist; expected a master playlist. Crucimorfo strikes again!") else: @@ -805,9 +859,9 @@ class HLS_Downloader: else: return { 'path': self.output_filename, - 'url': self.m3u8_index, + 'url': self.m3u8_index } - + else: console.log("[red]Error: URL passed to M3U8_Parser is an master playlist; expected a index playlist. Crucimorfo strikes again!") else: @@ -825,6 +879,8 @@ class HLS_Downloader: Args: out_path (str): The path of the output file to be cleaned up. """ + if TELEGRAM_BOT: + bot = get_bot_instance() # Check if the final output file exists logging.info(f"Check if end file converted exists: {out_path}") @@ -869,6 +925,13 @@ class HLS_Downloader: border_style="green" )) + if TELEGRAM_BOT: + message = f"Download completato\nDimensione: {formatted_size}\nDurata: {formatted_duration}\nPercorso: {os.path.abspath(self.output_filename)}" + # Rimuovere i tag di colore usando una regex + clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message) + # Invio a telegram + bot.send_message(clean_message, None) + # Handle missing segments if missing_ts: os.rename(self.output_filename, self.output_filename.replace(".mp4", "_failed.mp4")) @@ -890,7 +953,7 @@ class HLS_Downloader: logging.info("class 'HLS_Downloader'; call _valida_playlist()") # Retrieve the m3u8 playlist content - if self.is_playlist_url: + if self.is_playlist_url: if self.request_m3u8_playlist != 404: m3u8_playlist_text = self.request_m3u8_playlist m3u8_url_fixer.set_playlist(self.m3u8_playlist) @@ -905,7 +968,7 @@ class HLS_Downloader: # Check if the m3u8 content is valid if m3u8_playlist_text is None: console.log("[red]Playlist m3u8 to download is empty.") - sys.exit(0) + sys.exit(0) # Save the m3u8 playlist text to a temporary file open(os.path.join(self.path_manager.base_temp, "playlist.m3u8"), "w+", encoding="utf-8").write(m3u8_playlist_text) @@ -928,11 +991,11 @@ class HLS_Downloader: self.download_tracker.add_subtitle(self.content_extractor.list_available_subtitles) # Download each type of content - if len(self.download_tracker.downloaded_video) > 0: + if DOWNLOAD_VIDEO and len(self.download_tracker.downloaded_video) > 0: self.content_downloader.download_video(self.download_tracker.downloaded_video) - if len(self.download_tracker.downloaded_audio) > 0: + if DOWNLOAD_AUDIO and len(self.download_tracker.downloaded_audio) > 0: self.content_downloader.download_audio(self.download_tracker.downloaded_audio) - if len(self.download_tracker.downloaded_subtitle) > 0: + if DOWNLOAD_SUBTITLE and len(self.download_tracker.downloaded_subtitle) > 0: self.content_downloader.download_subtitle(self.download_tracker.downloaded_subtitle) # Join downloaded content @@ -940,7 +1003,7 @@ class HLS_Downloader: # Clean up temporary files and directories self._clean(self.content_joiner.converted_out_path) - + def _process_index(self): """ Processes the m3u8 index to download only video. @@ -950,7 +1013,7 @@ class HLS_Downloader: # Download video self.download_tracker.add_video(self.m3u8_index) self.content_downloader.download_video(self.download_tracker.downloaded_video) - + # Join video self.content_joiner.setup(self.download_tracker.downloaded_video, [], []) diff --git a/StreamingCommunity/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Lib/Downloader/HLS/segments.py index 0482ec2..d33d7f1 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Lib/Downloader/HLS/segments.py @@ -8,10 +8,10 @@ import signal import logging import binascii import threading + from queue import PriorityQueue from urllib.parse import urljoin, urlparse from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Dict # External libraries @@ -20,11 +20,12 @@ from tqdm import tqdm # Internal utilities -from StreamingCommunity.Util.color import Colors from StreamingCommunity.Util.console import console from StreamingCommunity.Util.headers import get_headers, random_headers +from StreamingCommunity.Util.color import Colors from StreamingCommunity.Util._jsonConfig import config_manager from StreamingCommunity.Util.os import os_manager +from StreamingCommunity.Util.call_stack import get_call_stack # Logic class @@ -34,19 +35,27 @@ from ...M3U8 import ( M3U8_Parser, M3U8_UrlFix ) +from ...FFmpeg.util import print_duration_table, format_duration from .proxyes import main_test_proxy # Config TQDM_DELAY_WORKER = config_manager.get_float('M3U8_DOWNLOAD', 'tqdm_delay') -TQDM_USE_LARGE_BAR = not ("android" in sys.platform or "ios" in sys.platform) +TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') + REQUEST_MAX_RETRY = config_manager.get_int('REQUESTS', 'max_retry') REQUEST_VERIFY = False + THERE_IS_PROXY_LIST = os_manager.check_file("list_proxy.txt") PROXY_START_MIN = config_manager.get_float('REQUESTS', 'proxy_start_min') PROXY_START_MAX = config_manager.get_float('REQUESTS', 'proxy_start_max') + DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_workser') DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workser') -MAX_TIMEOOUT = config_manager.get_int("REQUESTS", "timeout") + + + +# Variable +max_timeout = config_manager.get_int("REQUESTS", "timeout") @@ -64,6 +73,8 @@ class M3U8_Segments: self.tmp_folder = tmp_folder self.is_index_url = is_index_url self.expected_real_time = None + self.max_timeout = max_timeout + self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts") os.makedirs(self.tmp_folder, exist_ok=True) @@ -76,8 +87,8 @@ class M3U8_Segments: self.queue = PriorityQueue() self.stop_event = threading.Event() self.downloaded_segments = set() - self.base_timeout = 0.5 - self.current_timeout = 3.0 + self.base_timeout = 1.0 + self.current_timeout = 5.0 # Stopping self.interrupt_flag = threading.Event() @@ -89,41 +100,86 @@ class M3U8_Segments: self.info_nFailed = 0 def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes: + """ + Retrieves the encryption key from the M3U8 playlist. + + Parameters: + - m3u8_parser (M3U8_Parser): The parser object containing M3U8 playlist information. + + Returns: + bytes: The encryption key in bytes. + """ + + # Construct the full URL of the key key_uri = urljoin(self.url, m3u8_parser.keys.get('uri')) parsed_url = urlparse(key_uri) self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" - + logging.info(f"Uri key: {key_uri}") + + # Make request to get porxy try: - client_params = {'headers': {'User-Agent': get_headers()}, 'timeout': MAX_TIMEOOUT} - response = httpx.get(url=key_uri, **client_params) + response = httpx.get( + url=key_uri, + headers={'User-Agent': get_headers()}, + timeout=max_timeout + ) response.raise_for_status() - hex_content = binascii.hexlify(response.content).decode('utf-8') - return bytes.fromhex(hex_content) - except Exception as e: - raise Exception(f"Failed to fetch key: {e}") + raise Exception(f"Failed to fetch key from {key_uri}: {e}") + + # Convert the content of the response to hexadecimal and then to bytes + hex_content = binascii.hexlify(response.content).decode('utf-8') + byte_content = bytes.fromhex(hex_content) + logging.info(f"URI: Hex content: {hex_content}, Byte content: {byte_content}") + + #console.print(f"[cyan]Find key: [red]{hex_content}") + return byte_content def parse_data(self, m3u8_content: str) -> None: + """ + Parses the M3U8 content to extract segment information. + + Parameters: + - m3u8_content (str): The content of the M3U8 file. + """ m3u8_parser = M3U8_Parser() m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content) + self.expected_real_time = m3u8_parser.get_duration(return_string=False) self.expected_real_time_s = m3u8_parser.duration - if m3u8_parser.keys: - key = self.__get_key__(m3u8_parser) - self.decryption = M3U8_Decryption( - key, - m3u8_parser.keys.get('iv'), - m3u8_parser.keys.get('method') - ) + # Check if there is an encryption key in the playlis + if m3u8_parser.keys is not None: + try: - self.segments = [ - self.class_url_fixer.generate_full_url(seg) - if "http" not in seg else seg - for seg in m3u8_parser.segments - ] + # Extract byte from the key + key = self.__get_key__(m3u8_parser) + + except Exception as e: + raise Exception(f"Failed to retrieve encryption key {e}.") + + iv = m3u8_parser.keys.get('iv') + method = m3u8_parser.keys.get('method') + logging.info(f"M3U8_Decryption - IV: {iv}, method: {method}") + + # Create a decryption object with the key and set the method + self.decryption = M3U8_Decryption(key, iv, method) + + # Store the segment information parsed from the playlist + self.segments = m3u8_parser.segments + + # Fix URL if it is incomplete (missing 'http') + for i in range(len(self.segments)): + segment_url = self.segments[i] + + if "http" not in segment_url: + self.segments[i] = self.class_url_fixer.generate_full_url(segment_url) + logging.info(f"Generated new URL: {self.segments[i]}, from: {segment_url}") + + # Update segments for estimator self.class_ts_estimator.total_segments = len(self.segments) + logging.info(f"Segmnets to download: [{len(self.segments)}]") # Proxy if THERE_IS_PROXY_LIST: @@ -135,18 +191,35 @@ class M3U8_Segments: sys.exit(0) def get_info(self) -> None: + """ + Makes a request to the index M3U8 file to get information about segments. + """ if self.is_index_url: + try: - client_params = {'headers': {'User-Agent': get_headers()}, 'timeout': MAX_TIMEOOUT} - response = httpx.get(self.url, **client_params) + + # Send a GET request to retrieve the index M3U8 file + response = httpx.get( + self.url, + headers={'User-Agent': get_headers()}, + timeout=max_timeout, + follow_redirects=True + ) response.raise_for_status() - - self.parse_data(response.text) - with open(os.path.join(self.tmp_folder, "playlist.m3u8"), "w") as f: - f.write(response.text) - + + # Save the M3U8 file to the temporary folder + path_m3u8_file = os.path.join(self.tmp_folder, "playlist.m3u8") + open(path_m3u8_file, "w+").write(response.text) + + # Parse the text from the M3U8 index file + self.parse_data(response.text) + except Exception as e: - raise RuntimeError(f"M3U8 info retrieval failed: {e}") + print(f"Error during M3U8 index request: {e}") + + else: + # Parser data of content of index pass in input to class + self.parse_data(self.url) def setup_interrupt_handler(self): """ @@ -164,19 +237,7 @@ class M3U8_Segments: else: print("Signal handler must be set in the main thread") - def _get_http_client(self, index: int = None): - client_params = { - 'headers': random_headers(self.key_base_url) if hasattr(self, 'key_base_url') else {'User-Agent': get_headers()}, - 'timeout': MAX_TIMEOOUT, - 'follow_redirects': True - } - - if THERE_IS_PROXY_LIST and index is not None and hasattr(self, 'valid_proxy'): - client_params['proxies'] = self.valid_proxy[index % len(self.valid_proxy)] - - return httpx.Client(**client_params) - - def download_segment(self, ts_url: str, index: int, progress_bar: tqdm, backoff_factor: float = 1.3) -> None: + def make_requests_stream(self, ts_url: str, index: int, progress_bar: tqdm, backoff_factor: float = 1.5) -> None: """ Downloads a TS segment and adds it to the segment queue with retry logic. @@ -192,36 +253,84 @@ class M3U8_Segments: return try: - with self._get_http_client(index) as client: - start_time = time.time() - response = client.get(ts_url) - - # Validate response and content - response.raise_for_status() - segment_content = response.content - content_size = len(segment_content) - duration = time.time() - start_time + start_time = time.time() + + # Make request to get content + if THERE_IS_PROXY_LIST: - # Decrypt if needed and verify decrypted content - if self.decryption is not None: - try: - segment_content = self.decryption.decrypt(segment_content) - - except Exception as e: - logging.error(f"Decryption failed for segment {index}: {str(e)}") - self.interrupt_flag.set() # Interrupt the download process - self.stop_event.set() # Trigger the stopping event for all threads - break # Stop the current task immediately + # Get proxy from list + proxy = self.valid_proxy[index % len(self.valid_proxy)] + logging.info(f"Use proxy: {proxy}") - self.class_ts_estimator.update_progress_bar(content_size, duration, progress_bar) - self.queue.put((index, segment_content)) - self.downloaded_segments.add(index) - progress_bar.update(1) - return + with httpx.Client(proxies=proxy, verify=REQUEST_VERIFY) as client: + if 'key_base_url' in self.__dict__: + response = client.get( + url=ts_url, + headers=random_headers(self.key_base_url), + timeout=max_timeout, + follow_redirects=True + ) + + else: + response = client.get( + url=ts_url, + headers={'User-Agent': get_headers()}, + timeout=max_timeout, + follow_redirects=True + ) + + else: + with httpx.Client(verify=REQUEST_VERIFY) as client_2: + if 'key_base_url' in self.__dict__: + response = client_2.get( + url=ts_url, + headers=random_headers(self.key_base_url), + timeout=max_timeout, + follow_redirects=True + ) + + else: + response = client_2.get( + url=ts_url, + headers={'User-Agent': get_headers()}, + timeout=max_timeout, + follow_redirects=True + ) + + # Validate response and content + response.raise_for_status() + segment_content = response.content + content_size = len(segment_content) + duration = time.time() - start_time + + # Decrypt if needed and verify decrypted content + if self.decryption is not None: + try: + segment_content = self.decryption.decrypt(segment_content) + + except Exception as e: + logging.error(f"Decryption failed for segment {index}: {str(e)}") + self.interrupt_flag.set() # Interrupt the download process + self.stop_event.set() # Trigger the stopping event for all threads + break # Stop the current task immediately + + # Update progress and queue + self.class_ts_estimator.update_progress_bar(content_size, duration, progress_bar) + + # Add the segment to the queue + self.queue.put((index, segment_content)) + + # Track successfully downloaded segments + self.downloaded_segments.add(index) + progress_bar.update(1) + + # Break out of the loop on success + return except Exception as e: logging.info(f"Attempt {attempt + 1} failed for segment {index} - '{ts_url}': {e}") + # Update stat variable class if attempt > self.info_maxRetry: self.info_maxRetry = ( attempt + 1 ) self.info_nRetry += 1 @@ -231,6 +340,8 @@ class M3U8_Segments: self.queue.put((index, None)) # Marker for failed segment progress_bar.update(1) self.info_nFailed += 1 + + #break sleep_time = backoff_factor * (2 ** attempt) logging.info(f"Retrying segment {index} in {sleep_time} seconds...") @@ -242,6 +353,7 @@ class M3U8_Segments: """ buffer = {} expected_index = 0 + segments_written = set() with open(self.tmp_file_path, 'wb') as f: while not self.stop_event.is_set() or not self.queue.empty(): @@ -263,6 +375,7 @@ class M3U8_Segments: # Write segment if it's the next expected one if index == expected_index: f.write(segment_content) + segments_written.add(index) f.flush() expected_index += 1 @@ -272,6 +385,7 @@ class M3U8_Segments: if next_segment is not None: f.write(next_segment) + segments_written.add(expected_index) f.flush() expected_index += 1 @@ -280,7 +394,8 @@ class M3U8_Segments: buffer[index] = segment_content except queue.Empty: - self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.25) + self.current_timeout = min(self.max_timeout, self.current_timeout * 1.25) + if self.stop_event.is_set(): break @@ -297,34 +412,84 @@ class M3U8_Segments: """ self.setup_interrupt_handler() + # Get config site from prev stack + frames = get_call_stack() + logging.info(f"Extract info from: {frames}") + config_site = str(frames[-4]['folder_base']) + logging.info(f"Use frame: {frames[-1]}") + + # Workers to use for downloading + TQDM_MAX_WORKER = 0 + + # Select audio workers from folder of frames stack prev call. + try: + VIDEO_WORKERS = int(config_manager.get_dict('SITE', config_site)['video_workers']) + except: + #VIDEO_WORKERS = os.cpu_count() + VIDEO_WORKERS = DEFAULT_VIDEO_WORKERS + + try: + AUDIO_WORKERS = int(config_manager.get_dict('SITE', config_site)['audio_workers']) + except: + #AUDIO_WORKERS = os.cpu_count() + AUDIO_WORKERS = DEFAULT_AUDIO_WORKERS + + # Differnt workers for audio and video + if "video" in str(type): + TQDM_MAX_WORKER = VIDEO_WORKERS + + if "audio" in str(type): + TQDM_MAX_WORKER = AUDIO_WORKERS + + #console.print(f"[cyan]Video workers[white]: [green]{VIDEO_WORKERS} [white]| [cyan]Audio workers[white]: [green]{AUDIO_WORKERS}") + + # Custom bar for mobile and pc + if TQDM_USE_LARGE_BAR: + bar_format = ( + f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{description}{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}: " + 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=self._get_bar_format(description), + bar_format=bar_format, mininterval=0.05 ) try: + + # Start writer thread writer_thread = threading.Thread(target=self.write_segments_to_file) writer_thread.daemon = True writer_thread.start() # Configure workers and delay - max_workers = self._get_worker_count(type) + max_workers = len(self.valid_proxy) if THERE_IS_PROXY_LIST else TQDM_MAX_WORKER delay = max(PROXY_START_MIN, min(PROXY_START_MAX, 1 / (len(self.valid_proxy) + 1))) if THERE_IS_PROXY_LIST else TQDM_DELAY_WORKER # Download segments with completion verification with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [] for index, segment_url in enumerate(self.segments): - # Check for interrupt before submitting each task if self.interrupt_flag.is_set(): break time.sleep(delay) - futures.append(executor.submit(self.download_segment, segment_url, index, progress_bar)) + futures.append(executor.submit(self.make_requests_stream, segment_url, index, progress_bar)) # Wait for futures with interrupt handling for future in as_completed(futures): @@ -350,87 +515,59 @@ class M3U8_Segments: break try: - self.download_segment(self.segments[index], index, progress_bar) + self.make_requests_stream(self.segments[index], index, progress_bar) except Exception as e: logging.error(f"Failed to retry segment {index}: {str(e)}") + except Exception as e: + logging.error(f"Download failed: {str(e)}") + raise + finally: - self._cleanup_resources(writer_thread, progress_bar) - if not self.interrupt_flag.is_set(): - self._verify_download_completion() + # Clean up resources + self.stop_event.set() + writer_thread.join(timeout=30) + progress_bar.close() - return self._generate_results(type) - + # Check if download was interrupted + if self.download_interrupted: + console.log("[red] Download was manually stopped.") - def _get_bar_format(self, description: str) -> str: - """ - Generate platform-appropriate progress bar format. - """ - if not TQDM_USE_LARGE_BAR: - return ( - f"{Colors.YELLOW}Proc{Colors.WHITE}: " - f"{Colors.RED}{{percentage:.2f}}% " - f"{Colors.WHITE}| " - f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" - ) - - else: - return ( - f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{description}{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}]" - ) - - def _get_worker_count(self, stream_type: str) -> int: - """ - Calculate optimal parallel workers based on stream type and infrastructure. - """ - base_workers = { - 'video': DEFAULT_VIDEO_WORKERS, - 'audio': DEFAULT_AUDIO_WORKERS - }.get(stream_type.lower(), 1) - - if THERE_IS_PROXY_LIST: - return min(len(self.valid_proxy), base_workers * 2) - return base_workers - - def _generate_results(self, stream_type: str) -> Dict: - """Package final download results.""" - return { - 'type': stream_type, - 'nFailed': self.info_nFailed, - 'stopped': self.download_interrupted - } - - def _verify_download_completion(self) -> None: - """Validate final download integrity.""" - total = len(self.segments) - if len(self.downloaded_segments) / total < 0.999: - missing = sorted(set(range(total)) - self.downloaded_segments) - raise RuntimeError(f"Download incomplete ({len(self.downloaded_segments)/total:.1%}). Missing segments: {missing}") - - def _cleanup_resources(self, writer_thread: threading.Thread, progress_bar: tqdm) -> None: - """Ensure resource cleanup and final reporting.""" + # Clean up self.stop_event.set() writer_thread.join(timeout=30) progress_bar.close() - - if self.download_interrupted: - console.print("\n[red]Download terminated by user") - - if self.info_nFailed > 0: - self._display_error_summary() - def _display_error_summary(self) -> None: - """Generate final error report.""" - console.print(f"\n[cyan]Retry Summary: " - f"[white]Max retries: [green]{self.info_maxRetry} " - f"[white]Total retries: [green]{self.info_nRetry} " - f"[white]Failed segments: [red]{self.info_nFailed}") + # Final verification + try: + final_completion = (len(self.downloaded_segments) / total_segments) * 100 + if final_completion < 99.9: # Less than 99.9% complete + missing = set(range(total_segments)) - self.downloaded_segments + raise Exception(f"Download incomplete ({final_completion:.1f}%). Missing segments: {sorted(missing)}") + + except: + pass + + # Verify output file + if not os.path.exists(self.tmp_file_path): + raise Exception("Output file missing") - if self.info_nRetry > len(self.segments) * 0.3: - console.print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.") \ No newline at end of file + file_size = os.path.getsize(self.tmp_file_path) + if file_size == 0: + raise Exception("Output file is empty") + + # Display additional info when there is missing stream file + if self.info_nFailed > 0: + + # Get expected time + ex_hours, ex_minutes, ex_seconds = format_duration(self.expected_real_time_s) + ex_formatted_duration = f"[yellow]{int(ex_hours)}[red]h [yellow]{int(ex_minutes)}[red]m [yellow]{int(ex_seconds)}[red]s" + console.print(f"[cyan]Max retry per URL[white]: [green]{self.info_maxRetry}[green] [white]| [cyan]Total retry done[white]: [green]{self.info_nRetry}[green] [white]| [cyan]Missing TS: [red]{self.info_nFailed} [white]| [cyan]Duration: {print_duration_table(self.tmp_file_path, None, True)} [white]| [cyan]Expected duation: {ex_formatted_duration} \n") + + if self.info_nRetry >= len(self.segments) * 0.3: + console.print("[yellow]⚠ Warning:[/yellow] Too many retries detected! Consider reducing the number of [cyan]workers[/cyan] in the [magenta]config.json[/magenta] file. This will impact [bold]performance[/bold]. \n") + + # Info to return + return {'type': type, 'nFailed': self.info_nFailed} \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/MP4/downloader.py b/StreamingCommunity/Lib/Downloader/MP4/downloader.py index 0b19d76..01d96df 100644 --- a/StreamingCommunity/Lib/Downloader/MP4/downloader.py +++ b/StreamingCommunity/Lib/Downloader/MP4/downloader.py @@ -1,12 +1,12 @@ # 09.06.24 import os -import signal +import re import sys import ssl import certifi import logging -import atexit + # External libraries import httpx @@ -29,15 +29,17 @@ from ...FFmpeg import print_duration_table import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +# Telegram bot instance +from telegram_bot import get_bot_instance +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + # Config GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link') -TQDM_USE_LARGE_BAR = not ("android" in sys.platform or "ios" in sys.platform) +TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') -#Ending constant -KILL_HANDLER = bool(False) - + def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = None): """ @@ -49,6 +51,14 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No - referer (str, optional): The referer header value. - headers_ (dict, optional): Custom headers for the request. """ + if TELEGRAM_BOT: + bot = get_bot_instance() + + if os.path.exists(path): + console.log("[red]Output file already exists.") + if TELEGRAM_BOT: + bot.send_message(f"Contenuto già scaricato!", None) + return 400 # Early return for link-only mode if GET_ONLY_LINK: @@ -65,7 +75,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No headers = {} if referer: headers['Referer'] = referer - + # Use custom headers if provided, otherwise use default user agent if headers_: headers.update(headers_) @@ -83,15 +93,15 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No verify=False, # Disable SSL certificate verification http2=True # Optional: enable HTTP/2 support ) - + # Download with streaming and progress tracking with httpx.Client(transport=transport, timeout=httpx.Timeout(60.0)) as client: with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response: response.raise_for_status() - + # Get total file size total = int(response.headers.get('content-length', 0)) - + # Handle empty streams if total == 0: console.print("[bold red]No video stream found.[/bold red]") @@ -115,34 +125,15 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No # Ensure directory exists os.makedirs(os.path.dirname(path), exist_ok=True) - - def signal_handler(*args): - """ - Signal handler for SIGINT - - Parameters: - - args (tuple): The signal arguments (to prevent errors). - """ - - if(downloaded MAX_DOWNLOAD_SIZE: # break @@ -152,12 +143,20 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No console.print(Panel( f"[bold green]Download completed![/bold green]\n" f"[cyan]File size: [bold red]{internet_manager.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', ''))}", + 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" )) - return path,KILL_HANDLER - + + if TELEGRAM_BOT: + message = f"Download completato\nDimensione: {internet_manager.format_file_size(os.path.getsize(path))}\nDurata: {print_duration_table(path, description=False, return_string=True)}\nTitolo: {os.path.basename(path.replace('.mp4', ''))}" + # Rimuovere i tag di colore usando una regex + clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message) + # Invio a telegram + bot.send_message(clean_message, None) + + return path + else: console.print("[bold red]Download failed or file is empty.[/bold red]") return None @@ -166,17 +165,13 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No logging.error(f"HTTP error occurred: {http_err}") console.print(f"[bold red]HTTP Error: {http_err}[/bold red]") return None - + except httpx.RequestError as req_err: logging.error(f"Request error: {req_err}") console.print(f"[bold red]Request Error: {req_err}[/bold red]") return None - + except Exception as e: logging.error(f"Unexpected error during download: {e}") console.print(f"[bold red]Unexpected Error: {e}[/bold red]") return None - - except KeyboardInterrupt: - console.print("[bold red]Download stopped by user.[/bold red]") - return None \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/TOR/downloader.py b/StreamingCommunity/Lib/Downloader/TOR/downloader.py index d5a79c0..08bfd8e 100644 --- a/StreamingCommunity/Lib/Downloader/TOR/downloader.py +++ b/StreamingCommunity/Lib/Downloader/TOR/downloader.py @@ -28,7 +28,7 @@ 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 = not ("android" in sys.platform or "ios" in sys.platform) +TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') diff --git a/StreamingCommunity/Lib/FFmpeg/command.py b/StreamingCommunity/Lib/FFmpeg/command.py index cc9e8bc..51a8a83 100644 --- a/StreamingCommunity/Lib/FFmpeg/command.py +++ b/StreamingCommunity/Lib/FFmpeg/command.py @@ -31,7 +31,7 @@ FFMPEG_DEFAULT_PRESET = config_manager.get("M3U8_CONVERSION", "default_preset") # Variable -TQDM_USE_LARGE_BAR = not ("android" in sys.platform or "ios" in sys.platform) +TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') FFMPEG_PATH = os_summary.ffmpeg_path diff --git a/StreamingCommunity/Lib/M3U8/estimator.py b/StreamingCommunity/Lib/M3U8/estimator.py index 543e39b..76c3530 100644 --- a/StreamingCommunity/Lib/M3U8/estimator.py +++ b/StreamingCommunity/Lib/M3U8/estimator.py @@ -1,6 +1,6 @@ # 21.04.25 -import sys +import os import time import logging import threading @@ -15,10 +15,11 @@ from tqdm import tqdm # Internal utilities from StreamingCommunity.Util.color import Colors from StreamingCommunity.Util.os import internet_manager +from StreamingCommunity.Util._jsonConfig import config_manager # Variable -TQDM_USE_LARGE_BAR = not ("android" in sys.platform or "ios" in sys.platform) +TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') class M3U8_Ts_Estimator: @@ -34,10 +35,13 @@ class M3U8_Ts_Estimator: self.total_segments = total_segments self.lock = threading.Lock() self.speed = {"upload": "N/A", "download": "N/A"} + self.process_pid = os.getpid() # Get current process PID + logging.debug(f"Initializing M3U8_Ts_Estimator with PID: {self.process_pid}") + # Start the speed capture thread if TQDM_USE_LARGE_BAR is True if TQDM_USE_LARGE_BAR: logging.debug("TQDM_USE_LARGE_BAR is True, starting speed capture thread") - self.speed_thread = threading.Thread(target=self.capture_speed) + self.speed_thread = threading.Thread(target=self.capture_speed, args=(1, self.process_pid)) self.speed_thread.daemon = True self.speed_thread.start() @@ -46,6 +50,8 @@ class M3U8_Ts_Estimator: def add_ts_file(self, size: int, size_download: int, duration: float): """Add a file size to the list of file sizes.""" + logging.debug(f"Adding ts file - size: {size}, download size: {size_download}, duration: {duration}") + if size <= 0 or size_download <= 0 or duration <= 0: logging.error(f"Invalid input values: size={size}, size_download={size_download}, duration={duration}") return @@ -54,36 +60,95 @@ class M3U8_Ts_Estimator: self.now_downloaded_size += size_download logging.debug(f"Current total downloaded size: {self.now_downloaded_size}") - def capture_speed(self, interval: float = 1): + def capture_speed(self, interval: float = 1, pid: int = None): """Capture the internet speed periodically.""" - last_upload, last_download = 0, 0 - speed_buffer = deque(maxlen=3) + logging.debug(f"Starting speed capture with interval {interval}s for PID: {pid}") + def get_network_io(process=None): + try: + if process: + + # For process-specific monitoring + connections = process.connections(kind='inet') + if connections: + io_counters = process.io_counters() + logging.debug(f"Process IO counters: {io_counters}") + return io_counters + + else: + logging.debug("No active internet connections found for process") + return None + else: + + # For system-wide monitoring + io_counters = psutil.net_io_counters() + logging.debug(f"System IO counters: {io_counters}") + return io_counters + + except Exception as e: + logging.error(f"Error getting network IO: {str(e)}") + return None + + try: + process = psutil.Process(pid) if pid else None + logging.debug(f"Monitoring process: {process}") + + except Exception as e: + logging.error(f"Failed to get process with PID {pid}: {str(e)}") + process = None + + last_upload = None + last_download = None + first_run = True + + # Buffer circolare per le ultime N misurazioni + speed_buffer_size = 3 + speed_buffer = deque(maxlen=speed_buffer_size) + while True: try: - io_counters = psutil.net_io_counters() - if not io_counters: - raise ValueError("No IO counters available") + io_counters = get_network_io() - current_upload, current_download = io_counters.bytes_sent, io_counters.bytes_recv - if last_upload and last_download: - upload_speed = (current_upload - last_upload) / interval - download_speed = (current_download - last_download) / interval - speed_buffer.append(max(0, download_speed)) + if io_counters: + current_upload = io_counters.bytes_sent + current_download = io_counters.bytes_recv - with self.lock: - self.speed = { - "upload": internet_manager.format_transfer_speed(max(0, upload_speed)), - "download": internet_manager.format_transfer_speed(sum(speed_buffer) / len(speed_buffer)) - } - logging.debug(f"Updated speeds - Upload: {self.speed['upload']}, Download: {self.speed['download']}") + if not first_run and last_upload is not None and last_download is not None: + + # Calcola la velocità istantanea + upload_speed = max(0, (current_upload - last_upload) / interval) + download_speed = max(0, (current_download - last_download) / interval) + + # Aggiungi al buffer + speed_buffer.append(download_speed) + + # Calcola la media mobile delle velocità + if len(speed_buffer) > 0: + avg_download_speed = sum(speed_buffer) / len(speed_buffer) + + if avg_download_speed > 0: + with self.lock: + self.speed = { + "upload": internet_manager.format_transfer_speed(upload_speed), + "download": internet_manager.format_transfer_speed(avg_download_speed) + } + logging.debug(f"Updated speeds - Upload: {self.speed['upload']}, Download: {self.speed['download']}") + + last_upload = current_upload + last_download = current_download + first_run = False - last_upload, last_download = current_upload, current_download + time.sleep(interval) except Exception as e: - logging.error(f"Error in speed capture: {str(e)}") - self.speed = {"upload": "N/A", "download": "N/A"} - - time.sleep(interval) + logging.error(f"Error in speed capture loop: {str(e)}") + logging.exception("Full traceback:") + logging.sleep(interval) + + def get_average_speed(self) -> list: + """Calculate the average internet speed.""" + with self.lock: + logging.debug(f"Current speed data: {self.speed}") + return self.speed['download'].split(" ") def calculate_total_size(self) -> str: """ @@ -93,20 +158,38 @@ class M3U8_Ts_Estimator: str: The mean size of the files in a human-readable format. """ try: + if len(self.ts_file_sizes) == 0: + raise ValueError("No file sizes available to calculate total size.") + total_size = sum(self.ts_file_sizes) mean_size = total_size / len(self.ts_file_sizes) + + # Return formatted mean size return internet_manager.format_file_size(mean_size) + except ZeroDivisionError as e: + logging.error("Division by zero error occurred: %s", e) + return "0B" + except Exception as e: logging.error("An unexpected error occurred: %s", e) return "Error" + + def get_downloaded_size(self) -> str: + """ + Get the total downloaded size formatted as a human-readable string. + + Returns: + str: The total downloaded size as a human-readable string. + """ + return internet_manager.format_file_size(self.now_downloaded_size) def update_progress_bar(self, total_downloaded: int, duration: float, progress_counter: tqdm) -> None: """Updates the progress bar with download information.""" try: self.add_ts_file(total_downloaded * self.total_segments, total_downloaded, duration) - downloaded_file_size_str = internet_manager.format_file_size(self.now_downloaded_size) + downloaded_file_size_str = self.get_downloaded_size() file_total_size = self.calculate_total_size() number_file_downloaded = downloaded_file_size_str.split(' ')[0] @@ -115,13 +198,15 @@ class M3U8_Ts_Estimator: units_file_total_size = file_total_size.split(' ')[1] if TQDM_USE_LARGE_BAR: - speed_data = self.speed['download'].split(" ") + speed_data = self.get_average_speed() + #logging.debug(f"Speed data for progress bar: {speed_data}") if len(speed_data) >= 2: average_internet_speed = speed_data[0] average_internet_unit = speed_data[1] else: + logging.warning(f"Invalid speed data format: {speed_data}") average_internet_speed = "N/A" average_internet_unit = "" @@ -138,6 +223,7 @@ class M3U8_Ts_Estimator: ) progress_counter.set_postfix_str(progress_str) + #logging.debug(f"Updated progress bar: {progress_str}") except Exception as e: logging.error(f"Error updating progress bar: {str(e)}") \ No newline at end of file diff --git a/StreamingCommunity/Upload/version.py b/StreamingCommunity/Upload/version.py index 6e81304..e8f4c86 100644 --- a/StreamingCommunity/Upload/version.py +++ b/StreamingCommunity/Upload/version.py @@ -1,5 +1,5 @@ __title__ = 'StreamingCommunity' -__version__ = '2.5.0' +__version__ = '2.4.0' __author__ = 'Lovi-0' __description__ = 'A command-line program to download film' __copyright__ = 'Copyright 2024' diff --git a/StreamingCommunity/Util/table.py b/StreamingCommunity/Util/table.py index 65190c2..9845017 100644 --- a/StreamingCommunity/Util/table.py +++ b/StreamingCommunity/Util/table.py @@ -18,6 +18,11 @@ from typing import Dict, List, Any from .message import start_message from .call_stack import get_call_stack +# Telegram bot instance +from telegram_bot import get_bot_instance +from StreamingCommunity.Util._jsonConfig import config_manager +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') + class TVShowManager: def __init__(self): @@ -44,7 +49,7 @@ class TVShowManager: def add_column(self, column_info: Dict[str, Dict[str, str]]) -> None: """ Add column information. - + Parameters: - column_info (Dict[str, Dict[str, str]]): Dictionary containing column names, their colors, and justification. """ @@ -102,34 +107,34 @@ class TVShowManager: current_path = research_func['folder'] while not os.path.exists(os.path.join(current_path, 'StreamingCommunity')): current_path = os.path.dirname(current_path) - + # Add project root to Python path project_root = current_path #print(f"[DEBUG] Project Root: {project_root}") - + if project_root not in sys.path: sys.path.insert(0, project_root) - + # Import using full absolute import module_path = f'StreamingCommunity.Api.Site.{site_name}' #print(f"[DEBUG] Importing module: {module_path}") - + # Import the module module = importlib.import_module(module_path) - + # Get the search function search_func = getattr(module, 'search') - + # Call the search function with the search string search_func(None) - + except Exception as e: self.console.print(f"[red]Error during search: {e}") - + # Print detailed traceback import traceback traceback.print_exc() - + # Optionally remove the path if you want to clean up if project_root in sys.path: sys.path.remove(project_root) @@ -142,13 +147,16 @@ class TVShowManager: Parameters: - force_int_input(bool): If True, only accept integer inputs from 0 to max_int_input - max_int_input (int): range of row to show - + Returns: str: Last command executed before breaking out of the loop. """ total_items = len(self.tv_shows) last_command = "" # Variable to store the last command executed + if TELEGRAM_BOT: + bot = get_bot_instance() + while True: start_message() @@ -167,16 +175,33 @@ class TVShowManager: self.console.print(f"\n[green]Press [red]Enter [green]for next page, [red]'q' [green]to quit, or [red]'back' [green]to search.") if not force_int_input: - key = Prompt.ask( + if TELEGRAM_BOT: + self.console.print(f"\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " + "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end") + key = bot.ask( + "select_title_episode", + f"Inserisci l'indice dei media (ad esempio, 1), * per scaricare tutti i media, (ad esempio, 1-2) per un intervallo di media, o (ad esempio, 3-*) per scaricare dal un indice specifico fino alla fine.", + None + ) + else: + key = Prompt.ask( "\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end" - ) - + ) + else: choices = [str(i) for i in range(0, max_int_input)] choices.extend(["q", "quit", "b", "back"]) - - key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False) + if TELEGRAM_BOT: + self.console.print(f"[cyan]Insert media [red]index") + key = bot.ask( + "select_title", + f"Scegli il contenuto da scaricare:\n📺 Serie TV - 🎞️ Film - 🌀 Anime oppure `back` per tornare indietro", + None + ) + else: + key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False) + last_command = key if key.lower() == "q" or key.lower() == "quit": @@ -198,16 +223,34 @@ class TVShowManager: # Last slice, ensure all remaining items are shown self.console.print(f"\n [green]You've reached the end. [red]Enter [green]for first page, [red]'q' [green]to quit, or [red]'back' [green]to search.") if not force_int_input: - key = Prompt.ask( - "\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " - "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end" - ) + if TELEGRAM_BOT: + self.console.print(f"\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " + "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end") + key = bot.ask( + "select_title_episode", + f"Inserisci l'indice dei media (ad esempio, 1), * per scaricare tutti i media, (ad esempio, 1-2) per un intervallo di media, o (ad esempio, 3-*) per scaricare dal un indice specifico fino alla fine.", + None + ) + else: + key = Prompt.ask( + "\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " + "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end" + ) else: choices = [str(i) for i in range(0, max_int_input)] choices.extend(["q", "quit", "b", "back"]) - key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False) + if TELEGRAM_BOT: + self.console.print(f"[cyan]Insert media [red]index") + key = bot.ask( + "select_title", + f"Scegli il contenuto da scaricare:\n📺 Serie TV - 🎞️ Film - 🌀 Anime oppure `back` per tornare indietro", + None + ) + else: + key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False) + last_command = key if key.lower() == "q" or key.lower() == "quit": @@ -222,7 +265,7 @@ class TVShowManager: else: break - + return last_command def clear(self): diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index f1a616e..dd949d7 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -1,5 +1,8 @@ # 10.12.23 + +import threading +import asyncio import os import sys import time @@ -19,9 +22,13 @@ from StreamingCommunity.Upload.update import update as git_update from StreamingCommunity.Util.os import os_summary from StreamingCommunity.Util.logger import Logger +# Telegram bot instance +from telegram_bot import get_bot_instance # Config CLOSE_CONSOLE = config_manager.get_bool('DEFAULT', 'not_close') +TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') +from session import get_session, deleteScriptId def run_function(func: Callable[..., None], close_console: bool = False) -> None: @@ -33,16 +40,18 @@ def run_function(func: Callable[..., None], close_console: bool = False) -> None close_console (bool, optional): Whether to close the console after running the function once. Defaults to False. """ if close_console: - while 1: + while True: func() else: func() - def load_search_functions(): modules = [] loaded_functions = {} + # Lista dei siti da escludere se TELEGRAM_BOT è attivo + excluded_sites = {"cb01new", "ddlstreamitaly", "guardaserie", "ilcorsaronero", "mostraguarda"} if TELEGRAM_BOT else set() + # Find api home directory if getattr(sys, 'frozen', False): # Modalità PyInstaller base_path = os.path.join(sys._MEIPASS, "StreamingCommunity") @@ -51,12 +60,17 @@ def load_search_functions(): api_dir = os.path.join(base_path, 'Api', 'Site') init_files = glob.glob(os.path.join(api_dir, '*', '__init__.py')) - + # Retrieve modules and their indices for init_file in init_files: # Get folder name as module name module_name = os.path.basename(os.path.dirname(init_file)) + + # Se il modulo è nella lista da escludere, saltalo + if module_name in excluded_sites: + continue + logging.info(f"Load module name: {module_name}") try: @@ -118,13 +132,54 @@ def initialize(): sys.exit(0) # Attempting GitHub update - try: + """ try: git_update() except: - console.log("[red]Error with loading github.") + console.log("[red]Error with loading github.") """ + +def restart_script(): + """Riavvia lo script con gli stessi argomenti della riga di comando.""" + print("\n🔄 Riavvio dello script...\n") + python = sys.executable + os.execv(python, [python] + sys.argv) # Riavvia lo stesso script con gli stessi argomenti + +def force_exit(): + """Forza la chiusura dello script in qualsiasi contesto.""" + + print("\n🛑 Chiusura dello script in corso...") + + # 1️⃣ Chiudi tutti i thread tranne il principale + for t in threading.enumerate(): + if t is not threading.main_thread(): + print(f"🔄 Chiusura thread: {t.name}") + t.join(timeout=1) + + # 2️⃣ Ferma asyncio, se attivo + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + print("⚡ Arresto del loop asyncio...") + loop.stop() + except RuntimeError: + pass # Se non c'è un loop asyncio attivo, ignora l'errore + + # 3️⃣ Esce con sys.exit(), se fallisce usa os._exit() + try: + print("✅ Uscita con sys.exit(0)") + sys.exit(0) + except SystemExit: + pass # Se viene intercettato da un try/except, ignora + + print("🚨 Uscita forzata con os._exit(0)") + os._exit(0) # Se sys.exit() non funziona, esce forzatamente -def main(): +def main(script_id): + + if TELEGRAM_BOT: + bot = get_bot_instance() + bot.send_message(f"🏁 Avviato script {script_id}", None) + start = time.time() # Create logger @@ -140,6 +195,8 @@ def main(): description='Script to download movies and series from the internet. Use these commands to configure the script and control its behavior.' ) + parser.add_argument("script_id", nargs="?", default="unknown", help="ID dello script") + # Add arguments for the main configuration parameters parser.add_argument( '--add_siteName', type=bool, help='Enable or disable adding the site name to the file name (e.g., true/false).' @@ -173,7 +230,7 @@ def main(): "film_serie": "yellow", "film": "blue", "serie": "green", - "other": "white" + "altro": "white" } # Add dynamic arguments based on loaded search modules @@ -234,12 +291,67 @@ def main(): [f"{key}: [{color_map[label[1]]}]{label[0]}[/{color_map[label[1]]}]" for key, label in choice_labels.items()] ) + "[white])" - # Ask the user for input - category = msg.ask(prompt_message, choices=list(choice_labels.keys()), default="0", show_choices=False, show_default=False) + if TELEGRAM_BOT: + + # Mappa delle emoji per i colori + emoji_map = { + "yellow": "🟡", # Giallo + "red": "🔴", # Rosso + "blue": "🔵", # Blu + "green": "🟢" # Verde + } + + # Display the category legend in a single line + category_legend_str = "Categorie: \n" + " | ".join([ + f"{emoji_map.get(color, '⚪')} {category.capitalize()}" + for category, color in color_map.items() + ]) + + # Costruisci il messaggio con le emoji al posto dei colori + prompt_message = "Inserisci il sito:\n" + "\n".join( + [f"{key}: {emoji_map[color_map[label[1]]]} {label[0]}" for key, label in choice_labels.items()] + ) + + console.print(f"\n{prompt_message}") + + # Chiedi la scelta all'utente con il bot Telegram + category = bot.ask( + "select_provider", + f"{category_legend_str}\n\n{prompt_message}", + None # Passiamo la lista delle chiavi come scelte + ) + + else: + + #console.print(f"\n{prompt_message}") + + # Ask the user for input + category = msg.ask(prompt_message, choices=list(choice_labels.keys()), default="0", show_choices=False, show_default=False) + # Run the corresponding function based on user input if category in input_to_function: run_function(input_to_function[category]) else: + + if TELEGRAM_BOT: + bot.send_message(f"Categoria non valida", None) + console.print("[red]Invalid category.") - sys.exit(0) \ No newline at end of file + + if CLOSE_CONSOLE: + restart_script() # Riavvia lo script invece di uscire + # Riavvia lo script + # Chiude il processo attuale e avvia una nuova istanza dello script + """ subprocess.Popen([sys.executable] + sys.argv) + sys.exit() """ + else: + force_exit() # Usa la funzione per chiudere sempre + + if TELEGRAM_BOT: + bot.send_message(f"Chiusura in corso", None) + # Delete script_id + script_id = get_session() + if script_id != "unknown": + deleteScriptId(script_id) + \ No newline at end of file diff --git a/app.log b/app.log new file mode 100644 index 0000000..c9e5a55 --- /dev/null +++ b/app.log @@ -0,0 +1,138 @@ +[os.py:424 - get_system_summary() ] 2025-01-30 17:14:34,489 - INFO - Python: 3.13.1 (CPython x86_64) - Linux-6.12.9-200.fc41.x86_64-x86_64-with-glibc2.40 (glibc 2...4.0) +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,079 - INFO - Library: httpx-0.28.1 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,080 - INFO - Library: bs4-0.0.2 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,082 - INFO - Library: rich-13.9.4 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,084 - INFO - Library: tqdm-4.66.6 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,085 - INFO - Library: m3u8-6.0.0 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,086 - INFO - Library: psutil-5.9.8 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,088 - INFO - Library: unidecode-1.3.8 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,088 - INFO - Library: jsbeautifier-1.15.1 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,089 - INFO - Library: pathvalidate-3.2.3 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,090 - INFO - Library: pycryptodomex-3.21.0 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,091 - INFO - Library: fake-useragent-2.0.3 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,092 - INFO - Library: qbittorrent-api-2024.12.71 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,092 - INFO - Library: python-qbittorrent-0.4.3 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,093 - INFO - Library: serpapi-0.1.5 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,094 - INFO - Library: flask-3.1.0 +[os.py:476 - get_system_summary() ] 2025-01-30 17:14:35,094 - INFO - Library: flask-socketio-5.5.1 +[os.py:479 - get_system_summary() ] 2025-01-30 17:14:35,122 - INFO - Libraries: httpx-0.28.1, bs4-0.0.2, rich-13.9.4, tqdm-4.66.6, m3u8-6.0.0, psutil-5.9.8, unidecode-1.3.8, jsbeautifier-1.15.1, pathvalidate-3.2.3, pycryptodomex-3.21.0, fake-useragent-2.0.3, qbittorrent-api-2024.12.71, python-qbittorrent-0.4.3, serpapi-0.1.5, flask-3.1.0, flask-socketio-5.5.1 +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:38,841 - INFO - Load module name: 1337xx +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,057 - INFO - Read key: DEFAULT.map_episode_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,058 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,058 - INFO - Read key: SITE.1337xx +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,058 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,058 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,059 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,059 - INFO - Read key: DEFAULT.disable_searchDomain +[decryptor.py:20 - () ] 2025-01-30 17:14:39,062 - INFO - [cyan]Decrypy use: Cryptodomex +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,203 - INFO - Read key: M3U8_DOWNLOAD.tqdm_use_large_bar +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: DEFAULT.debug +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: M3U8_CONVERSION.use_codec +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: M3U8_CONVERSION.use_vcodec +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: M3U8_CONVERSION.use_acodec +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: M3U8_CONVERSION.use_bitrate +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: M3U8_CONVERSION.use_gpu +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,211 - INFO - Read key: M3U8_CONVERSION.default_preset +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,212 - INFO - Read key: M3U8_DOWNLOAD.tqdm_use_large_bar +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,213 - INFO - Read key: M3U8_DOWNLOAD.tqdm_delay +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,213 - INFO - Read key: M3U8_DOWNLOAD.tqdm_use_large_bar +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,213 - INFO - Read key: REQUESTS.max_retry +[os.py:229 - check_file() ] 2025-01-30 17:14:39,214 - INFO - Check if file exists: list_proxy.txt +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,214 - INFO - Read key: REQUESTS.proxy_start_min +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,214 - INFO - Read key: REQUESTS.proxy_start_max +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,214 - INFO - Read key: M3U8_DOWNLOAD.default_video_workser +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,214 - INFO - Read key: M3U8_DOWNLOAD.default_audio_workser +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,214 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.specific_list_audio +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.specific_list_subtitles +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.download_video +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.download_audio +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.merge_audio +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.download_sub +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,215 - INFO - Read key: M3U8_DOWNLOAD.merge_subs +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,216 - INFO - Read key: M3U8_DOWNLOAD.cleanup_tmp_folder +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,216 - INFO - Read key: M3U8_PARSER.force_resolution +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,216 - INFO - Read key: M3U8_PARSER.get_only_link +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,216 - INFO - Read key: REQUESTS.max_retry +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,216 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,217 - INFO - Read key: M3U8_PARSER.get_only_link +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,218 - INFO - Read key: M3U8_DOWNLOAD.tqdm_use_large_bar +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,218 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,220 - INFO - Read key: DEFAULT.config_qbit_tor +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,220 - INFO - Read key: DEFAULT.config_qbit_tor +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,220 - INFO - Read key: DEFAULT.config_qbit_tor +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,221 - INFO - Read key: DEFAULT.config_qbit_tor +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,221 - INFO - Read key: M3U8_DOWNLOAD.tqdm_use_large_bar +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,221 - INFO - Read key: REQUESTS.timeout +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,221 - INFO - Load module name: altadefinizionegratis +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,222 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,222 - INFO - Read key: SITE.altadefinizionegratis +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,223 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,223 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,223 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,223 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,223 - INFO - Read key: DEFAULT.disable_searchDomain +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,237 - INFO - Read key: REQUESTS.timeout +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,238 - INFO - Load module name: animeunity +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,239 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,240 - INFO - Read key: SITE.animeunity +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,240 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,240 - INFO - Read key: DEFAULT.anime_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,240 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,240 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,240 - INFO - Read key: DEFAULT.disable_searchDomain +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,243 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,244 - INFO - Read key: REQUESTS.timeout +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,244 - INFO - Load module name: cb01new +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,245 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,245 - INFO - Read key: SITE.cb01new +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,246 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,246 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,246 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,246 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,247 - INFO - Read key: DEFAULT.disable_searchDomain +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,248 - INFO - Read key: REQUESTS.timeout +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,249 - INFO - Load module name: ddlstreamitaly +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,251 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,251 - INFO - Read key: SITE.ddlstreamitaly +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,252 - INFO - Read key: SITE.ddlstreamitaly +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,252 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,252 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,253 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,253 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,254 - INFO - Read key: DEFAULT.disable_searchDomain +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,255 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,256 - INFO - Read key: REQUESTS.timeout +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,256 - INFO - Load module name: guardaserie +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: SITE.guardaserie +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,258 - INFO - Read key: DEFAULT.disable_searchDomain +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,260 - INFO - Load module name: ilcorsaronero +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,261 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,262 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,262 - INFO - Read key: SITE.ilcorsaronero +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,262 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,262 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,262 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,263 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,263 - INFO - Read key: DEFAULT.disable_searchDomain +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,263 - INFO - Load module name: mostraguarda +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,266 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,267 - INFO - Read key: SITE.mostraguarda +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,267 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,267 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,267 - INFO - Read key: DEFAULT.add_siteName +[run.py:77 - load_search_functions() ] 2025-01-30 17:14:39,267 - INFO - Load module name: streamingcommunity +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,269 - INFO - Read key: DEFAULT.root_path +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,269 - INFO - Read key: SITE.streamingcommunity +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,269 - INFO - Read key: DEFAULT.serie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,269 - INFO - Read key: DEFAULT.movie_folder_name +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,270 - INFO - Read key: DEFAULT.add_siteName +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,270 - INFO - Read key: REQUESTS.timeout +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,270 - INFO - Read key: DEFAULT.disable_searchDomain +[_jsonConfig.py:89 - read_key() ] 2025-01-30 17:14:39,271 - INFO - Read key: REQUESTS.timeout +[run.py:157 - main() ] 2025-01-30 17:14:39,272 - INFO - Load module in: 5.0520179271698 s diff --git a/config.json b/config.json index a9858a9..9a7093d 100644 --- a/config.json +++ b/config.json @@ -2,23 +2,24 @@ "DEFAULT": { "debug": false, "log_file": "app.log", - "log_to_file": true, - "show_message": true, + "log_to_file": false, + "show_message": false, "clean_console": true, - "root_path": "Video", + "root_path": "/home/giuseppepiccolo/Develop/docker/jellyfin/media/", "movie_folder_name": "Movie", - "serie_folder_name": "TV", + "serie_folder_name": "Serie", "anime_folder_name": "Anime", - "map_episode_name": "E%(episode)_%(episode_name)", + "map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)", "config_qbit_tor": { - "host": "192.168.1.99", - "port": "7060", + "host": "192.168.5.172", + "port": "8080", "user": "admin", "pass": "adminadmin" }, "add_siteName": false, "disable_searchDomain": false, - "not_close": false + "not_close": false, + "telegram_bot": true }, "REQUESTS": { "timeout": 30, @@ -28,13 +29,17 @@ }, "M3U8_DOWNLOAD": { "tqdm_delay": 0.12, + "tqdm_use_large_bar": false, "default_video_workser": 12, "default_audio_workser": 12, + "download_video": true, + "download_audio": true, "merge_audio": true, "specific_list_audio": [ "ita" ], - "merge_subs": true, + "download_sub": false, + "merge_subs": false, "specific_list_subtitles": [ "eng", "spa" @@ -58,10 +63,10 @@ "domain": "paris" }, "altadefinizionegratis": { - "domain": "site" + "domain": "info" }, "guardaserie": { - "domain": "meme" + "domain": "academy" }, "mostraguarda": { "domain": "stream" @@ -78,7 +83,7 @@ "domain": "so" }, "cb01new": { - "domain": "mobi" + "domain": "video" }, "1337xx": { "domain": "to" diff --git a/request_manager.py b/request_manager.py new file mode 100644 index 0000000..963bbad --- /dev/null +++ b/request_manager.py @@ -0,0 +1,79 @@ +import json +import time +from typing import Optional + +class RequestManager: + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, json_file: str = "active_requests.json"): + if not hasattr(self, 'initialized'): + self.json_file = json_file + self.initialized = True + self.on_response_callback = None # Aggiungi un campo per il callback + + def create_request(self, type: str) -> str: + request_data = { + "type": type, + "response": None, + "timestamp": time.time() + } + + # Aggiungi il tipo al salvataggio della richiesta + with open(self.json_file, "w") as f: + json.dump(request_data, f) + + return "Ok" + + def save_response(self, message_text: str) -> bool: + try: + # Carica il file JSON + with open(self.json_file, "r") as f: + data = json.load(f) + + # Controlla se esiste la chiave 'type' e se la risposta è presente + if "type" in data and "response" in data: + data["response"] = message_text # Aggiorna la risposta + + # Scrivi il file JSON aggiornato + with open(self.json_file, "w") as f: + json.dump(data, f, indent=4) # Formatta il file JSON + + return True + else: + return False + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"⚠️ save_response - errore: {e}") + return False + + def get_response(self) -> Optional[str]: + try: + with open(self.json_file, "r") as f: + data = json.load(f) + # Verifica se esiste la chiave "response" + if "response" in data: + response = data["response"] # Ottieni la risposta direttamente + if response is not None and self.on_response_callback: + # Se la risposta è disponibile, chiama il callback + self.on_response_callback(response) + return response + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"get_response - errore: {e}") + return None + + def clear_file(self) -> bool: + try: + # Svuota il file JSON scrivendo un oggetto vuoto + with open(self.json_file, "w") as f: + json.dump({}, f) + print(f"File {self.json_file} è stato svuotato con successo.") + return True + except Exception as e: + print(f"⚠️ clear_file - errore: {e}") + return False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 01f23e8..a710d62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,9 @@ unidecode jsbeautifier pathvalidate pycryptodomex -googlesearch-python fake-useragent qbittorrent-api -python-qbittorrent \ No newline at end of file +python-qbittorrent +serpapi +pyTelegramBotAPI +Pillow \ No newline at end of file diff --git a/session.py b/session.py new file mode 100755 index 0000000..b81add8 --- /dev/null +++ b/session.py @@ -0,0 +1,62 @@ +import json + +session_data = {} + +def set_session(value): + # salvo script_id in session_data + session_data['script_id'] = value + +def get_session(): + # controllo se script_id è presente in session_data + return session_data.get('script_id', 'unknown') + +def updateScriptId(screen_id, titolo): + # definisco il nome del file json + json_file = "scripts.json" + try: + # apro il file json + with open(json_file, 'r') as f: + scripts_data = json.load(f) + except FileNotFoundError: + # Se il file non esiste, inizializzo la lista vuota + scripts_data = [] + + # cerco lo script con lo screen_id + for script in scripts_data: + if script["screen_id"] == screen_id: + # se trovo il match, aggiorno il titolo + script["titolo"] = titolo + # aggiorno il file json + with open(json_file, 'w') as f: + json.dump(scripts_data, f, indent=4) + #print(f"Titolo aggiornato per screen_id {screen_id}") + return + + # se non trovo nessuno script con lo screen_id + print(f"Screen_id {screen_id} non trovato.") + +# creo la funzione che elimina lo script con lo screen_id specificato +def deleteScriptId(screen_id): + # definisco il nome del file json + json_file = "scripts.json" + try: + # apro il file json + with open(json_file, 'r') as f: + scripts_data = json.load(f) + except FileNotFoundError: + # Se il file non esiste, inizializzo la lista vuota + scripts_data = [] + + # cerco lo script con lo screen_id + for script in scripts_data: + if script["screen_id"] == screen_id: + # se trovo il match, elimino lo script + scripts_data.remove(script) + # aggiorno il file json + with open(json_file, 'w') as f: + json.dump(scripts_data, f, indent=4) + print(f"Script eliminato per screen_id {screen_id}") + return + + # se non trovo nessuno script con lo screen_id + print(f"Screen_id {screen_id} non trovato.") \ No newline at end of file diff --git a/telegram_bot.py b/telegram_bot.py new file mode 100644 index 0000000..61f3e45 --- /dev/null +++ b/telegram_bot.py @@ -0,0 +1,513 @@ +import telebot +from telebot import types +import time +import uuid +import subprocess +import os, re, sys +import json +from request_manager import RequestManager +import threading + +# Funzione per caricare variabili da un file .env +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path) as f: + for line in f: + if line.strip() and not line.startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +# Carica le variabili +load_env() + +class TelegramBot: + _instance = None + _config_file = "bot_config.json" + + @classmethod + def get_instance(cls): + if cls._instance is None: + # Prova a caricare la configurazione e inizializzare il bot + if os.path.exists(cls._config_file): + with open(cls._config_file, 'r') as f: + config = json.load(f) + cls._instance = cls.init_bot(config['token'], config['authorized_user_id']) + else: + raise Exception("Bot non ancora inizializzato. Chiamare prima init_bot() con token e authorized_user_id") + return cls._instance + + @classmethod + def init_bot(cls, token, authorized_user_id): + if cls._instance is None: + cls._instance = cls(token, authorized_user_id) + # Salva la configurazione + config = { + 'token': token, + 'authorized_user_id': authorized_user_id + } + with open(cls._config_file, 'w') as f: + json.dump(config, f) + return cls._instance + + def __init__(self, token, authorized_user_id): + + def monitor_scripts(): + while True: + try: + with open("scripts.json", "r") as f: + scripts_data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + scripts_data = [] + + current_time = time.time() + + # Crea una nuova lista senza gli script che sono scaduti + scripts_data_to_save = [] + + for script in scripts_data: + if "titolo" not in script and script["status"] == "running" and (current_time - script["start_time"]) > 600: + # Prova a terminare la sessione screen + try: + subprocess.check_output(["screen", "-S", script["screen_id"], "-X", "quit"]) + print(f"✅ La sessione screen con ID {script['screen_id']} è stata fermata automaticamente.") + except subprocess.CalledProcessError: + print(f"⚠️ Impossibile fermare la sessione screen con ID {script['screen_id']}.") + + # Aggiungi solo gli script che non sono scaduti + print(f"⚠️ Lo script con ID {script['screen_id']} ha superato i 10 minuti e verrà rimosso.") + else: + scripts_data_to_save.append(script) + + # Salva la lista aggiornata, senza gli script scaduti + with open("scripts.json", "w") as f: + json.dump(scripts_data_to_save, f, indent=4) + + time.sleep(60) # Controlla ogni minuto + + # Avvia il thread di monitoraggio + monitor_thread = threading.Thread(target=monitor_scripts, daemon=True) + monitor_thread.start() + + + if TelegramBot._instance is not None: + raise Exception("Questa classe è un singleton! Usa get_instance() per ottenere l'istanza.") + + self.token = token + self.authorized_user_id = authorized_user_id + self.chat_id = authorized_user_id + self.bot = telebot.TeleBot(token) + self.request_manager = RequestManager() + + # Registra gli handler + self.register_handlers() + + def register_handlers(self): + + """ @self.bot.message_handler(commands=['start']) + def start(message): + self.handle_start(message) """ + + @self.bot.message_handler(commands=['get_id']) + def get_id(message): + self.handle_get_id(message) + + @self.bot.message_handler(commands=['start']) + def start_script(message): + self.handle_start_script(message) + + @self.bot.message_handler(commands=['list']) + def list_scripts(message): + self.handle_list_scripts(message) + + @self.bot.message_handler(commands=['stop']) + def stop_script(message): + self.handle_stop_script(message) + + @self.bot.message_handler(commands=['screen']) + def screen_status(message): + self.handle_screen_status(message) + + """ @self.bot.message_handler(commands=['replay']) + def send_welcome(message): + # Crea una tastiera personalizzata + markup = types.ReplyKeyboardMarkup(row_width=2) + itembtn1 = types.KeyboardButton('Start') + itembtn2 = types.KeyboardButton('Lista') + markup.add(itembtn1, itembtn2) + + # Invia un messaggio con la tastiera + self.bot.send_message(message.chat.id, "Scegli un'opzione:", reply_markup=markup) """ + + """@self.bot.message_handler(commands=['inline']) + def send_welcome(message): + # Crea una tastiera inline + markup = types.InlineKeyboardMarkup() + itembtn1 = types.InlineKeyboardButton('Azione 1', callback_data='action1') + itembtn2 = types.InlineKeyboardButton('Azione 2', callback_data='action2') + itembtn3 = types.InlineKeyboardButton('Azione 3', callback_data='action3') + markup.add(itembtn1, itembtn2, itembtn3) + + # Invia un messaggio con la tastiera inline + self.bot.send_message(message.chat.id, "Scegli un'opzione:", reply_markup=markup) + + # Gestisce le callback delle tastiere inline + @self.bot.callback_query_handler(func=lambda call: True) + def handle_callback(call): + if call.data == 'action1': + self.bot.answer_callback_query(call.id, "Hai scelto Azione 1!") + self.bot.send_message(call.message.chat.id, "Hai eseguito Azione 1!") + elif call.data == 'action2': + self.bot.answer_callback_query(call.id, "Hai scelto Azione 2!") + self.bot.send_message(call.message.chat.id, "Hai eseguito Azione 2!") + elif call.data == 'action3': + self.bot.answer_callback_query(call.id, "Hai scelto Azione 3!") + self.bot.send_message(call.message.chat.id, "Hai eseguito Azione 3!") + """ + + @self.bot.message_handler(func=lambda message: True) + def handle_all_messages(message): + self.handle_response(message) + + def is_authorized(self, user_id): + return user_id == self.authorized_user_id + + def handle_get_id(self, message): + if not self.is_authorized(message.from_user.id): + self.bot.send_message(message.chat.id, "❌ Non sei autorizzato.") + return + + self.bot.send_message(message.chat.id, f"Il tuo ID utente è: `{message.from_user.id}`", parse_mode="Markdown") + + def handle_start_script(self, message): + if not self.is_authorized(message.from_user.id): + self.bot.send_message(message.chat.id, "❌ Non sei autorizzato.") + return + + screen_id = str(uuid.uuid4())[:8] + #screen_id = '0000' + + # Impostare a True per avviare il test_run.py in modalità verbose per il debug + + debug_mode = os.getenv("DEBUG") + verbose = debug_mode + + if debug_mode == "True": + subprocess.Popen(["python3", "test_run.py", screen_id]) + else: + # Verifica se lo screen con il nome esiste già + try: + subprocess.check_output(["screen", "-list"]) + existing_screens = subprocess.check_output(["screen", "-list"]).decode('utf-8') + if screen_id in existing_screens: + self.bot.send_message(message.chat.id, f"⚠️ Lo script con ID {screen_id} è già in esecuzione.") + return + except subprocess.CalledProcessError: + pass # Se il comando fallisce, significa che non ci sono screen attivi. + + # Crea la sessione screen e avvia lo script al suo interno + command = ["screen", "-dmS", screen_id, "python3", "test_run.py", screen_id] + + # Avvia il comando tramite subprocess + subprocess.Popen(command) + + # Creazione oggetto script info + script_info = { + "screen_id": screen_id, + "start_time": time.time(), + "status": "running", + "user_id": message.from_user.id + } + + # Salvataggio nel file JSON + json_file = "scripts.json" + + # Carica i dati esistenti o crea una nuova lista + try: + with open(json_file, 'r') as f: + scripts_data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + scripts_data = [] + + # Aggiungi il nuovo script + scripts_data.append(script_info) + + # Scrivi il file aggiornato + with open(json_file, 'w') as f: + json.dump(scripts_data, f, indent=4) + + def handle_list_scripts(self, message): + if not self.is_authorized(message.from_user.id): + self.bot.send_message(message.chat.id, "❌ Non sei autorizzato.") + return + + try: + with open("scripts.json", "r") as f: + scripts_data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + scripts_data = [] + + if not scripts_data: + self.bot.send_message(message.chat.id, "⚠️ Nessuno script registrato.") + return + + current_time = time.time() + msg = ["🖥️ **Script Registrati:**\n"] + + for script in scripts_data: + # Calcola la durata + duration = current_time - script["start_time"] + if "end_time" in script: + duration = script["end_time"] - script["start_time"] + + # Formatta la durata + hours, rem = divmod(duration, 3600) + minutes, seconds = divmod(rem, 60) + duration_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s" + + # Icona stato + status_icons = { + "running": "🟢", + "stopped": "🔴", + "completed": "⚪" + } + + # Costruisci riga + line = ( + f"• ID: `{script['screen_id']}`\n" + f"• Stato: {status_icons.get(script['status'], '⚫')}\n" + f"• Stop: `/stop {script['screen_id']}`\n" + f"• Screen: `/screen {script['screen_id']}`\n" + f"• Durata: {duration_str}\n" + f"• Download:\n{script.get('titolo', 'N/A')}\n" + ) + msg.append(line) + + # Formatta la risposta finale + final_msg = "\n".join(msg) + if len(final_msg) > 4000: + final_msg = final_msg[:4000] + "\n[...] (messaggio troncato)" + + self.bot.send_message(message.chat.id, final_msg, parse_mode="Markdown") + + def handle_stop_script(self, message): + if not self.is_authorized(message.from_user.id): + self.bot.send_message(message.chat.id, "❌ Non sei autorizzato.") + return + + parts = message.text.split() + if len(parts) < 2: + try: + with open("scripts.json", "r") as f: + scripts_data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + scripts_data = [] + + running_scripts = [s for s in scripts_data if s["status"] == "running"] + + if not running_scripts: + self.bot.send_message(message.chat.id, "⚠️ Nessuno script attivo da fermare.") + return + + msg = "🖥️ **Script Attivi:**\n" + for script in running_scripts: + msg += f"🔹 `/stop {script['screen_id']}` per fermarlo\n" + + self.bot.send_message(message.chat.id, msg, parse_mode="Markdown") + + elif len(parts) == 2: + screen_id = parts[1] + + try: + with open("scripts.json", "r") as f: + scripts_data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + scripts_data = [] + + # Filtra la lista eliminando lo script con l'ID specificato + new_scripts_data = [script for script in scripts_data if script["screen_id"] != screen_id] + + if len(new_scripts_data) == len(scripts_data): + # Nessun elemento rimosso, quindi ID non trovato + self.bot.send_message(message.chat.id, f"⚠️ Nessuno script attivo con ID `{screen_id}`.", parse_mode="Markdown") + return + + # Terminare la sessione screen + try: + subprocess.check_output(["screen", "-S", screen_id, "-X", "quit"]) + print(f"✅ La sessione screen con ID {screen_id} è stata fermata.") + except subprocess.CalledProcessError: + self.bot.send_message(message.chat.id, f"⚠️ Impossibile fermare la sessione screen con ID `{screen_id}`.", parse_mode="Markdown") + return + + # Salva la lista aggiornata senza lo script eliminato + with open("scripts.json", "w") as f: + json.dump(new_scripts_data, f, indent=4) + + self.bot.send_message(message.chat.id, f"✅ Script `{screen_id}` terminato con successo!", parse_mode="Markdown") + + def handle_response(self, message): + """ if message.text == 'Start': + self.handle_start_script(self, message) + elif message.text == 'Lista': + self.handle_list_scripts(self, message) + elif message.text == 'Azione 3': + self.bot.send_message(message.chat.id, "Hai scelto Azione 3!") + else: + self.bot.send_message(message.chat.id, "Comando non riconosciuto.") + + if not self.is_authorized(message.from_user.id): + self.bot.reply_to(message, "❌ Non sei autorizzato.") + return """ + + text = message.text + if self.request_manager.save_response(text): + print(f"📥 Risposta salvata correttamente per il tipo {text}") + else: + print("⚠️ Nessuna richiesta attiva.") + self.bot.reply_to(message, "❌ Nessuna richiesta attiva.") + + def handle_screen_status(self, message): + command_parts = message.text.split() + if len(command_parts) < 2: + self.bot.send_message(message.chat.id, "⚠️ ID mancante nel comando. Usa: /screen ") + return + + screen_id = command_parts[1] + temp_file = f"/tmp/screen_output_{screen_id}.txt" + + try: + # Cattura l'output della screen + subprocess.run(["screen", "-X", "-S", screen_id, "hardcopy", "-h", temp_file], check=True) + except subprocess.CalledProcessError as e: + self.bot.send_message(message.chat.id, f"❌ Errore durante la cattura dell'output della screen: {e}") + return + + if not os.path.exists(temp_file): + self.bot.send_message(message.chat.id, f"❌ Impossibile catturare l'output della screen.") + return + + try: + # Leggi il file con la codifica corretta + with open(temp_file, 'r', encoding='latin-1') as file: + screen_output = file.read() + + # Pulisci l'output + cleaned_output = re.sub(r'[\x00-\x1F\x7F]', '', screen_output) # Rimuovi caratteri di controllo + cleaned_output = cleaned_output.replace('\n\n', '\n') # Rimuovi newline multipli + + # Estrarre tutte le parti da "Download:" fino a "Video" o "Subtitle", senza includerli + download_matches = re.findall(r"Download: (.*?)(?:Video|Subtitle)", cleaned_output) + if download_matches: + # Serie TV e Film StreamingCommunity + + proc_matches = re.findall(r"Proc: ([\d\.]+%)", cleaned_output) + + # Creare una stringa unica con tutti i risultati + result_string = "\n".join([f"Download: {download_matches[i].strip()}\nDownload al {proc_matches[i]}" for i in range(len(download_matches)) if i < len(proc_matches)]) + + if result_string != "": + cleaned_output = result_string + else: + print(f"❌ La parola 'Download:' non è stata trovata nella stringa.") + else: + + download_list = [] + + # Estrai tutte le righe che iniziano con "Download:" fino al prossimo "Download" o alla fine della riga + matches = re.findall(r"Download:\s*(.*?)(?=Download|$)", cleaned_output) + + # Se sono stati trovati download, stampali + if matches: + for i, match in enumerate(matches, 1): + # rimuovo solo la parte "downloader.py:57Result:400" se esiste + match = re.sub(r"downloader.py:\d+Result:400", "", match) + match = match.strip() # Rimuovo gli spazi bianchi in eccesso + if match: # Assicurati che la stringa non sia vuota + print(f"Download {i}: {match}") + + # Aggiungi il risultato modificato alla lista + download_list.append(f"Download {i}: {match}") + + # Creare una stringa unica con tutti i risultati + cleaned_output = "\n".join(download_list) + else: + print("❌ Nessun download trovato") + + # Invia l'output pulito + self._send_long_message(message.chat.id, f"📄 Output della screen {screen_id}:\n{cleaned_output}") + + except Exception as e: + self.bot.send_message(message.chat.id, f"❌ Errore durante la lettura o l'invio dell'output della screen: {e}") + + # Cancella il file temporaneo + os.remove(temp_file) + + def send_message(self, message, choices): + if choices is None: + if self.chat_id: + self.bot.send_message(self.chat_id, message) + else: + formatted_choices = "\n".join(choices) + message = f"{message}\n\n{formatted_choices}" + if self.chat_id: + self.bot.send_message(self.chat_id, message) + + def _send_long_message(self, chat_id, text, chunk_size=4096): + """Suddivide e invia un messaggio troppo lungo in più parti.""" + for i in range(0, len(text), chunk_size): + self.bot.send_message(chat_id, text[i:i+chunk_size]) + + def ask(self, type, prompt_message, choices, timeout=60): + self.request_manager.create_request(type) + + if choices is None: + self.bot.send_message( + self.chat_id, + f"{prompt_message}", + ) + else: + self.bot.send_message( + self.chat_id, + f"{prompt_message}\n\nOpzioni: {', '.join(choices)}", + ) + + start_time = time.time() + while time.time() - start_time < timeout: + response = self.request_manager.get_response() + if response is not None: + return response + time.sleep(1) + + self.bot.send_message(self.chat_id, "⚠️ Timeout: nessuna risposta ricevuta.") + self.request_manager.clear_file() + return None + + def run(self): + print("🚀 Avvio del bot...") + # svuoto il file scripts.json + with open("scripts.json", "w") as f: + json.dump([], f) + self.bot.infinity_polling() + +def get_bot_instance(): + return TelegramBot.get_instance() + +# Esempio di utilizzo +if __name__ == "__main__": + + # Usa le variabili + token = os.getenv("TOKEN_TELEGRAM") + authorized_user_id = os.getenv("AUTHORIZED_USER_ID") + + TOKEN = token # Inserisci il token del tuo bot Telegram sul file .env + AUTHORIZED_USER_ID = int(authorized_user_id) # Inserisci il tuo ID utente Telegram sul file .env + + # Inizializza il bot + bot = TelegramBot.init_bot(TOKEN, AUTHORIZED_USER_ID) + bot.run() + +""" +start - Avvia lo script +list - Lista script attivi +get - Mostra ID utente Telegram +""" diff --git a/test_run.py b/test_run.py index 74806a1..73ea787 100644 --- a/test_run.py +++ b/test_run.py @@ -1,5 +1,15 @@ # 26.11.24 +import sys from StreamingCommunity.run import main +from request_manager import RequestManager +from session import set_session -main() \ No newline at end of file +# Svuoto il file +request_manager = RequestManager() +request_manager.clear_file() +script_id = sys.argv[1] if len(sys.argv) > 1 else "unknown" + +set_session(script_id) + +main(script_id) \ No newline at end of file