diff --git a/StreamingCommunity/Api/Player/ddl.py b/StreamingCommunity/Api/Player/ddl.py index 62d4efa..0f3b67a 100644 --- a/StreamingCommunity/Api/Player/ddl.py +++ b/StreamingCommunity/Api/Player/ddl.py @@ -18,21 +18,13 @@ max_timeout = config_manager.get_int("REQUESTS", "timeout") class VideoSource: - def __init__(self, cookie) -> None: + def __init__(self, url, cookie) -> None: """ Initializes the VideoSource object with default values. """ self.headers = {'user-agent': get_userAgent()} - self.cookie = cookie - - def setup(self, url: str) -> None: - """ - Sets up the video source with the provided URL. - - Parameters: - - url (str): The URL of the video source. - """ self.url = url + self.cookie = cookie def make_request(self, url: str) -> str: """ diff --git a/StreamingCommunity/Api/Player/mediapolisvod.py b/StreamingCommunity/Api/Player/mediapolisvod.py new file mode 100644 index 0000000..7aae8a2 --- /dev/null +++ b/StreamingCommunity/Api/Player/mediapolisvod.py @@ -0,0 +1,64 @@ +# 11.04.25 + + +# External libraries +import httpx + + +# Internal utilities +from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Util.headers import get_headers + + +# Variable +MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout") + + +class VideoSource: + + @staticmethod + def extract_m3u8_url(video_url: str) -> str: + """Extract the m3u8 streaming URL from a RaiPlay video URL.""" + if not video_url.endswith('.json'): + if '/video/' in video_url: + video_id = video_url.split('/')[-1].split('.')[0] + video_path = '/'.join(video_url.split('/')[:-1]) + video_url = f"{video_path}/{video_id}.json" + + else: + return "Error: Unable to determine video JSON URL" + + try: + response = httpx.get(video_url, headers=get_headers(), timeout=MAX_TIMEOUT) + if response.status_code != 200: + return f"Error: Failed to fetch video data (Status: {response.status_code})" + + video_data = response.json() + content_url = video_data.get("video").get("content_url") + + if not content_url: + return "Error: No content URL found in video data" + + # Extract the element key + if "=" in content_url: + element_key = content_url.split("=")[1] + else: + return "Error: Unable to extract element key" + + # Request the stream URL + params = { + 'cont': element_key, + 'output': '62', + } + stream_response = httpx.get('https://mediapolisvod.rai.it/relinker/relinkerServlet.htm', params=params, headers=get_headers(), timeout=MAX_TIMEOUT) + + if stream_response.status_code != 200: + return f"Error: Failed to fetch stream URL (Status: {stream_response.status_code})" + + # Extract the m3u8 URL + stream_data = stream_response.json() + m3u8_url = stream_data.get("video")[0] if "video" in stream_data else None + return m3u8_url + + except Exception as e: + return f"Error: {str(e)}" \ No newline at end of file diff --git a/StreamingCommunity/Api/Player/sweetpixel.py b/StreamingCommunity/Api/Player/sweetpixel.py index 30d5b44..1b2b02f 100644 --- a/StreamingCommunity/Api/Player/sweetpixel.py +++ b/StreamingCommunity/Api/Player/sweetpixel.py @@ -16,9 +16,9 @@ from StreamingCommunity.Util.headers import get_userAgent MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout") -class AnimeWorldPlayer: +class VideoSource: def __init__(self, full_url, episode_data, session_id, csrf_token): - """Initialize the AnimeWorldPlayer with session details, episode data, and URL.""" + """Initialize the VideoSource with session details, episode data, and URL.""" self.session_id = session_id self.csrf_token = csrf_token self.episode_data = episode_data @@ -33,7 +33,7 @@ class AnimeWorldPlayer: timeout=MAX_TIMEOUT ) - def get_download_link(self): + def get_playlist(self): """Fetch the download link from AnimeWorld using the episode link.""" try: # Make a POST request to the episode link and follow any redirects diff --git a/StreamingCommunity/Api/Player/vixcloud.py b/StreamingCommunity/Api/Player/vixcloud.py index 7f614fc..0c9d881 100644 --- a/StreamingCommunity/Api/Player/vixcloud.py +++ b/StreamingCommunity/Api/Player/vixcloud.py @@ -24,26 +24,20 @@ console = Console() class VideoSource: - def __init__(self, url: str, is_series: bool): + def __init__(self, url: str, is_series: bool, media_id: int = None): """ Initialize video source for streaming site. Args: - url (str): The URL of the streaming site. - is_series (bool): Flag for series or movie content + - media_id (int, optional): Unique identifier for media item """ self.headers = {'user-agent': get_userAgent()} self.url = url self.is_series = is_series - - def setup(self, media_id: int): - """ - Configure media-specific context. - - Args: - media_id (int): Unique identifier for media item - """ self.media_id = media_id + self.iframe_src = None def get_iframe(self, episode_id: int) -> None: """ @@ -164,6 +158,7 @@ class VideoSourceAnime(VideoSource): self.headers = {'user-agent': get_userAgent()} self.url = url self.src_mp4 = None + self.iframe_src = None def get_embed(self, episode_id: int): """ diff --git a/StreamingCommunity/Api/Site/1337xx/__init__.py b/StreamingCommunity/Api/Site/1337xx/__init__.py index 7f6c44b..fc8d4a2 100644 --- a/StreamingCommunity/Api/Site/1337xx/__init__.py +++ b/StreamingCommunity/Api/Site/1337xx/__init__.py @@ -39,7 +39,7 @@ def process_search_result(select_title): def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for diff --git a/StreamingCommunity/Api/Site/altadefinizione/__init__.py b/StreamingCommunity/Api/Site/altadefinizione/__init__.py index ec79b0b..403a896 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/__init__.py +++ b/StreamingCommunity/Api/Site/altadefinizione/__init__.py @@ -57,27 +57,43 @@ def get_user_input(string_to_search: str = None): return string_to_search -def process_search_result(select_title): +def process_search_result(select_title, selections=None): """ Handles the search result and initiates the download for either a film or series. + + Parameters: + select_title (MediaItem): The selected media item + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if select_title.type == 'tv': - download_series(select_title) + season_selection = None + episode_selection = None + + if selections: + season_selection = selections.get('season') + episode_selection = selections.get('episode') + + download_series(select_title, season_selection, episode_selection) + else: download_film(select_title) -def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): +# search("Game of Thrones", selections={"season": "1", "episode": "1-3"}) +def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for get_onylDatabase (bool, optional): If True, return only the database object direct_item (dict, optional): Direct item to process (bypass search) + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if direct_item: select_title = MediaItem(**direct_item) - process_search_result(select_title) + process_search_result(select_title, selections) return # Get the user input for the search term @@ -95,7 +111,7 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ if len_database > 0: select_title = get_select_title(table_show_manager, media_search_manager) - process_search_result(select_title) + process_search_result(select_title, selections) else: console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") @@ -105,4 +121,4 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ # If no results are found, ask again string_to_search = get_user_input() - search() \ No newline at end of file + search(string_to_search, get_onlyDatabase, None, selections) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/film.py b/StreamingCommunity/Api/Site/altadefinizione/film.py index 84629bd..2477b35 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/film.py +++ b/StreamingCommunity/Api/Site/altadefinizione/film.py @@ -42,7 +42,6 @@ def download_film(select_title: MediaItem) -> str: Return: - str: output path if successful, otherwise None """ - if site_constant.TELEGRAM_BOT: bot = get_bot_instance() bot.send_message(f"Download in corso:\n{select_title.name}", None) diff --git a/StreamingCommunity/Api/Site/altadefinizione/series.py b/StreamingCommunity/Api/Site/altadefinizione/series.py index 0c7c3dc..3d0b324 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/series.py +++ b/StreamingCommunity/Api/Site/altadefinizione/series.py @@ -19,8 +19,7 @@ from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance, Teleg from .util.ScrapeSerie import GetSerieInfo from StreamingCommunity.Api.Template.Util import ( manage_selection, - map_episode_title, - dynamic_format_number, + map_episode_title, validate_selection, validate_episode_selection, display_episodes_list @@ -40,23 +39,24 @@ console = Console() def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo) -> Tuple[str,bool]: """ - Download a single episode video. + Downloads a specific episode from a specified season. Parameters: - - index_season_selected (int): Index of the selected season. - - index_episode_selected (int): Index of the selected episode. + - index_season_selected (int): Season number + - index_episode_selected (int): Episode index + - scrape_serie (GetSerieInfo): Scraper object with series information - Return: - - str: output path - - bool: kill handler status + Returns: + - str: Path to downloaded file + - bool: Whether download was stopped """ start_message() - index_season_selected = dynamic_format_number(str(index_season_selected)) - # Get info about episode - obj_episode = scrape_serie.seasons_manager.get_season_by_number(int(index_season_selected)).episodes.get(index_episode_selected-1) + # Get episode information + obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + # Telegram integration if site_constant.TELEGRAM_BOT: bot = get_bot_instance() @@ -93,21 +93,21 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra return r_proc['path'], r_proc['stopped'] -def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False) -> None: +def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None) -> None: """ - Download episodes of a selected season. + Handle downloading episodes for a specific season. Parameters: - - index_season_selected (int): Index of the selected season. - - download_all (bool): Download all episodes in the season. + - index_season_selected (int): Season number + - scrape_serie (GetSerieInfo): Scraper object with series information + - download_all (bool): Whether to download all episodes + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ - start_message() - obj_episodes = scrape_serie.seasons_manager.get_season_by_number(index_season_selected).episodes - episodes_count = len(obj_episodes.episodes) + # Get episodes for the selected season + episodes = scrape_serie.getEpisodeSeasons(index_season_selected) + episodes_count = len(episodes) if download_all: - - # Download all episodes without asking for i_episode in range(1, episodes_count + 1): path, stopped = download_video(index_season_selected, i_episode, scrape_serie) @@ -117,16 +117,16 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, dow console.print(f"\n[red]End downloaded [yellow]season: [red]{index_season_selected}.") else: + if episode_selection is not None: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") - # Display episodes list and manage user selection - last_command = display_episodes_list(obj_episodes.episodes) + else: + last_command = display_episodes_list(episodes) + + # Prompt user for episode selection list_episode_select = manage_selection(last_command, episodes_count) - - try: - list_episode_select = validate_episode_selection(list_episode_select, episodes_count) - except ValueError as e: - console.print(f"[red]{str(e)}") - return + list_episode_select = validate_episode_selection(list_episode_select, episodes_count) # Download selected episodes if not stopped for i_episode in list_episode_select: @@ -135,69 +135,65 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, dow if stopped: break -def download_series(select_season: MediaItem) -> None: +def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None) -> None: """ - Download episodes of a TV series based on user selection. + Handle downloading a complete series. Parameters: - - select_season (MediaItem): Selected media item (TV series). + - select_season (MediaItem): Series metadata from search + - season_selection (str, optional): Pre-defined season selection that bypasses manual input + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - start_message() - - # Init class scrape_serie = GetSerieInfo(select_season.url) - # Collect information about seasons - scrape_serie.collect_season() - seasons_count = len(scrape_serie.seasons_manager) + # Get total number of seasons + seasons_count = scrape_serie.getNumberSeason() + + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() # Prompt user for season selection and download episodes console.print(f"\n[green]Seasons found: [red]{seasons_count}") - if site_constant.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") + # If season_selection is provided, use it instead of asking for input + if season_selection is None: + if site_constant.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) + bot.send_message(f"Stagioni trovate: {seasons_count}", None) - index_season_selected = bot.ask( - "select_title_episode", - "Menu di selezione delle stagioni\n\n" - "- Inserisci il numero della stagione (ad esempio, 1)\n" - "- Inserisci * per scaricare tutte le stagioni\n" - "- Inserisci un intervallo di stagioni (ad esempio, 1-2) per scaricare da una stagione all'altra\n" - "- Inserisci (ad esempio, 3-*) per scaricare dalla stagione specificata fino alla fine della serie", - None - ) + index_season_selected = bot.ask( + "select_title_episode", + "Menu di selezione delle stagioni\n\n" + "- Inserisci il numero della stagione (ad esempio, 1)\n" + "- Inserisci * per scaricare tutte le stagioni\n" + "- Inserisci un intervallo di stagioni (ad esempio, 1-2) per scaricare da una stagione all'altra\n" + "- Inserisci (ad esempio, 3-*) per scaricare dalla stagione specificata fino alla fine della serie", + 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" + ) 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" - ) + index_season_selected = season_selection + console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}") - # Manage and validate the selection + # Validate the selection list_season_select = manage_selection(index_season_selected, seasons_count) - - try: - list_season_select = validate_selection(list_season_select, seasons_count) - except ValueError as e: - console.print(f"[red]{str(e)}") - return + list_season_select = validate_selection(list_season_select, seasons_count) # Loop through the selected seasons and download episodes for i_season in list_season_select: if len(list_season_select) > 1 or index_season_selected == "*": - # Download all episodes if multiple seasons are selected or if '*' is used download_episode(i_season, scrape_serie, download_all=True) else: - # Otherwise, let the user select specific episodes for the single season - download_episode(i_season, scrape_serie, download_all=False) + download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection) if site_constant.TELEGRAM_BOT: bot.send_message(f"Finito di scaricare tutte le serie e episodi", None) @@ -205,4 +201,4 @@ def download_series(select_season: MediaItem) -> None: # Get script_id script_id = TelegramSession.get_session() if script_id != "unknown": - TelegramSession.deleteScriptId(script_id) + TelegramSession.deleteScriptId(script_id) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/site.py b/StreamingCommunity/Api/Site/altadefinizione/site.py index 8a522b9..0ae7a05 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/site.py +++ b/StreamingCommunity/Api/Site/altadefinizione/site.py @@ -78,7 +78,8 @@ def title_search(query: str) -> int: media_search_manager.add_media({ 'url': url, 'name': title, - 'type': tipo + 'type': tipo, + 'image': f"{site_constant.FULL_URL}{movie_div.find("img", class_="layer-image").get("data-src")}" }) if site_constant.TELEGRAM_BOT: diff --git a/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py index f88ff4d..b65f1d6 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py @@ -1,5 +1,8 @@ # 16.03.25 +import logging + + # External libraries import httpx from bs4 import BeautifulSoup @@ -15,7 +18,6 @@ from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager max_timeout = config_manager.get_int("REQUESTS", "timeout") - class GetSerieInfo: def __init__(self, url): """ @@ -69,4 +71,37 @@ class GetSerieInfo: 'number': ep_idx, 'name': episode_name, 'url': episode_url - }) \ No newline at end of file + }) + + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the series. + """ + if not self.seasons_manager.seasons: + self.collect_season() + + return len(self.seasons_manager.seasons) + + def getEpisodeSeasons(self, season_number: int) -> list: + """ + Get all episodes for a specific season. + """ + if not self.seasons_manager.seasons: + self.collect_season() + + # Get season directly by its number + season = self.seasons_manager.get_season_by_number(season_number) + return season.episodes.episodes if season else [] + + def selectEpisode(self, season_number: int, episode_index: int) -> dict: + """ + Get information for a specific episode in a specific season. + """ + episodes = self.getEpisodeSeasons(season_number) + if not episodes or episode_index < 0 or episode_index >= len(episodes): + logging.error(f"Episode index {episode_index} is out of range for season {season_number}") + return None + + return episodes[episode_index] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/__init__.py b/StreamingCommunity/Api/Site/animeunity/__init__.py index 08646d4..77655c7 100644 --- a/StreamingCommunity/Api/Site/animeunity/__init__.py +++ b/StreamingCommunity/Api/Site/animeunity/__init__.py @@ -18,7 +18,8 @@ from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance # Logic class from .site import title_search, media_search_manager, table_show_manager -from .film_serie import download_film, download_series +from .film import download_film +from .serie import download_series # Variable @@ -56,24 +57,42 @@ def get_user_input(string_to_search: str = None): return string_to_search -def process_search_result(select_title): +def process_search_result(select_title, selections=None): """ Handles the search result and initiates the download for either a film or series. + + Parameters: + select_title (MediaItem): The selected media item + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ - download_series(select_title) + if select_title.type == 'Movie' or select_title.type == 'OVA': + download_film(select_title) -def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): + else: + season_selection = None + episode_selection = None + + if selections: + season_selection = selections.get('season') + episode_selection = selections.get('episode') + + download_series(select_title, season_selection, episode_selection) + +def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for get_onlyDatabase (bool, optional): If True, return only the database object direct_item (dict, optional): Direct item to process (bypass search) + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if direct_item: select_title = MediaItem(**direct_item) - process_search_result(select_title) + process_search_result(select_title, selections) return # Get the user input for the search term @@ -82,7 +101,7 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ # Perform the database search len_database = title_search(string_to_search) - ##If only the database is needed, return the manager + # If only the database is needed, return the manager if get_onlyDatabase: return media_search_manager @@ -91,8 +110,8 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ if len_database > 0: select_title = get_select_title(table_show_manager, media_search_manager) - process_search_result(select_title) - + process_search_result(select_title, selections) + else: console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") @@ -101,4 +120,4 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ # If no results are found, ask again string_to_search = get_user_input() - search() \ No newline at end of file + search(string_to_search, get_onlyDatabase, None, selections) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/film.py b/StreamingCommunity/Api/Site/animeunity/film.py new file mode 100644 index 0000000..e910775 --- /dev/null +++ b/StreamingCommunity/Api/Site/animeunity/film.py @@ -0,0 +1,40 @@ +# 11.03.24 + +# External library +from rich.console import Console + + +# Logic class +from .serie import download_episode +from .util.ScrapeSerie import ScrapeSerieAnime +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Class.SearchType import MediaItem + + +# Player +from StreamingCommunity.Api.Player.vixcloud import VideoSourceAnime + + +# Variable +console = Console() + + +def download_film(select_title: MediaItem): + """ + Function to download a film. + + Parameters: + - id_film (int): The ID of the film. + - title_name (str): The title of the film. + """ + + # Init class + scrape_serie = ScrapeSerieAnime(site_constant.FULL_URL) + video_source = VideoSourceAnime(site_constant.FULL_URL) + + # Set up video source (only configure scrape_serie now) + scrape_serie.setup(None, select_title.id, select_title.slug) + scrape_serie.is_series = False + + # Start download + download_episode(0, scrape_serie, video_source) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/film_serie.py b/StreamingCommunity/Api/Site/animeunity/film_serie.py deleted file mode 100644 index 163074c..0000000 --- a/StreamingCommunity/Api/Site/animeunity/film_serie.py +++ /dev/null @@ -1,181 +0,0 @@ -# 11.03.24 - -import os -import logging -from typing import Tuple - - -# External library -from rich.console import Console -from rich.prompt import Prompt - - -# Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Lib.Downloader import MP4_downloader -from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession, get_bot_instance - - -# Logic class -from .util.ScrapeSerie import ScrapeSerieAnime -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Util import manage_selection, dynamic_format_number -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player -from StreamingCommunity.Api.Player.vixcloud import VideoSourceAnime - - -# Variable -console = Console() -msg = Prompt() -KILL_HANDLER = bool(False) - - - -def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_source: VideoSourceAnime) -> Tuple[str,bool]: - """ - Downloads the selected episode. - - Parameters: - - index_select (int): Index of the episode to download. - - Return: - - str: output path - - bool: kill handler status - """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - # Get information about the selected episode - obj_episode = scrape_serie.get_info_episode(index_select) - - if obj_episode is not None: - - start_message() - console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] ([cyan]E{obj_episode.number}[/cyan]) \n") - - if site_constant.TELEGRAM_BOT: - bot.send_message(f"Download in corso:\nTitolo:{scrape_serie.series_name}\nEpisodio: {obj_episode.number}", None) - - # Get script_id - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.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 - mp4_name = f"{scrape_serie.series_name}_EP_{dynamic_format_number(str(obj_episode.number))}.mp4" - - if scrape_serie.is_series: - mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.ANIME_FOLDER, scrape_serie.series_name)) - - else: - mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.MOVIE_FOLDER, scrape_serie.series_name)) - - # Create output folder - os_manager.create_path(mp4_path) - - # Start downloading - path, kill_handler = MP4_downloader( - url=str(video_source.src_mp4).strip(), - path=os.path.join(mp4_path, mp4_name) - ) - - return path, kill_handler - - else: - logging.error(f"Skip index: {index_select} cant find info with api.") - return None, True - - -def download_series(select_title: MediaItem): - """ - Function to download episodes of a TV series. - - Parameters: - - tv_id (int): The ID of the TV series. - - tv_name (str): The name of the TV series. - """ - start_message() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - scrape_serie = ScrapeSerieAnime(site_constant.FULL_URL) - video_source = VideoSourceAnime(site_constant.FULL_URL) - - # Set up video source - scrape_serie.setup(None, select_title.id, select_title.slug) - - # Get the count of episodes for the TV series - episoded_count = scrape_serie.get_count_episodes() - console.print(f"[cyan]Episodes find: [red]{episoded_count}") - - if site_constant.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") - bot.send_message(f"Episodi trovati: {episoded_count}", None) - - last_command = bot.ask( - "select_title", - "Menu di selezione degli episodi: \n\n" - "- Inserisci il numero dell'episodio (ad esempio, 1)\n" - "- Inserisci * per scaricare tutti gli episodi\n" - "- Inserisci un intervallo di episodi (ad esempio, 1-2) per scaricare da un episodio all'altro\n" - "- Inserisci (ad esempio, 3-*) per scaricare dall'episodio specificato fino alla fine della serie", - 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 != "*": - path, _ = download_episode(list_episode_select[0]-1, scrape_serie, video_source) - return path - - # Download all other episodes selecter - else: - kill_handler = False - for i_episode in list_episode_select: - if kill_handler: - break - _, kill_handler = download_episode(i_episode-1, scrape_serie, video_source) - - if site_constant.TELEGRAM_BOT: - bot.send_message(f"Finito di scaricare tutte le serie e episodi", None) - - # Get script_id - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.deleteScriptId(script_id) - - -def download_film(select_title: MediaItem): - """ - Function to download a film. - - Parameters: - - id_film (int): The ID of the film. - - title_name (str): The title of the film. - """ - - # Init class - scrape_serie = ScrapeSerieAnime(site_constant.FULL_URL) - video_source = VideoSourceAnime(site_constant.FULL_URL) - - # Set up video source - scrape_serie.setup(None, select_title.id, select_title.slug) - scrape_serie.is_series = False - - # Start download - download_episode(0, scrape_serie, video_source) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/serie.py b/StreamingCommunity/Api/Site/animeunity/serie.py new file mode 100644 index 0000000..b6a0265 --- /dev/null +++ b/StreamingCommunity/Api/Site/animeunity/serie.py @@ -0,0 +1,153 @@ +# 11.03.24 + +import os +from typing import Tuple + + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Util.os import os_manager +from StreamingCommunity.Util.message import start_message +from StreamingCommunity.Lib.Downloader import MP4_downloader +from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession, get_bot_instance + + +# Logic class +from .util.ScrapeSerie import ScrapeSerieAnime +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Util import manage_selection, dynamic_format_number +from StreamingCommunity.Api.Template.Class.SearchType import MediaItem + + +# Player +from StreamingCommunity.Api.Player.vixcloud import VideoSourceAnime + + +# Variable +console = Console() +msg = Prompt() +KILL_HANDLER = bool(False) + + +def download_episode(index_select: int, scrape_serie: ScrapeSerieAnime, video_source: VideoSourceAnime) -> Tuple[str,bool]: + """ + Downloads the selected episode. + + Parameters: + - index_select (int): Index of the episode to download. + + Return: + - str: output path + - bool: kill handler status + """ + start_message() + + # Get episode information + obj_episode = scrape_serie.selectEpisode(1, index_select) + console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] ([cyan]E{obj_episode.number}[/cyan]) \n") + + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() + bot.send_message(f"Download in corso\nAnime: {scrape_serie.series_name}\nEpisodio: {obj_episode.number}", None) + + # Get script_id and update it + script_id = TelegramSession.get_session() + if script_id != "unknown": + TelegramSession.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 + mp4_name = f"{scrape_serie.series_name}_EP_{dynamic_format_number(str(obj_episode.number))}.mp4" + + if scrape_serie.is_series: + mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.ANIME_FOLDER, scrape_serie.series_name)) + else: + mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.MOVIE_FOLDER, scrape_serie.series_name)) + + # Create output folder + os_manager.create_path(mp4_path) + + # Start downloading + path, kill_handler = MP4_downloader( + url=str(video_source.src_mp4).strip(), + path=os.path.join(mp4_path, mp4_name) + ) + + return path, kill_handler + + +def download_series(select_title: MediaItem, season_selection: str = None, episode_selection: str = None): + """ + Function to download episodes of a TV series. + + Parameters: + - select_title (MediaItem): The selected media item + - season_selection (str, optional): Season selection input that bypasses manual input (usually '1' for anime) + - episode_selection (str, optional): Episode selection input that bypasses manual input + """ + start_message() + + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() + + scrape_serie = ScrapeSerieAnime(site_constant.FULL_URL) + video_source = VideoSourceAnime(site_constant.FULL_URL) + + # Set up video source (only configure scrape_serie now) + scrape_serie.setup(None, select_title.id, select_title.slug) + + # Get episode information + episoded_count = scrape_serie.get_count_episodes() + console.print(f"[green]Episodes count:[/green] [red]{episoded_count}[/red]") + + # Telegram bot integration + if episode_selection is None: + if site_constant.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") + bot.send_message(f"Episodi trovati: {episoded_count}", None) + + last_command = bot.ask( + "select_title", + "Menu di selezione degli episodi: \n\n" + "- Inserisci il numero dell'episodio (ad esempio, 1)\n" + "- Inserisci * per scaricare tutti gli episodi\n" + "- Inserisci un intervallo di episodi (ad esempio, 1-2) per scaricare da un episodio all'altro\n" + "- Inserisci (ad esempio, 3-*) per scaricare dall'episodio specificato fino alla fine della serie", + 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") + else: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") + + # Manage user selection + list_episode_select = manage_selection(last_command, episoded_count) + + # Download selected episodes + if len(list_episode_select) == 1 and last_command != "*": + path, _ = download_episode(list_episode_select[0]-1, scrape_serie, video_source) + return path + + # Download all other episodes selected + else: + kill_handler = False + for i_episode in list_episode_select: + if kill_handler: + break + _, kill_handler = download_episode(i_episode-1, scrape_serie, video_source) + + if site_constant.TELEGRAM_BOT: + bot.send_message(f"Finito di scaricare tutte le serie e episodi", None) + + # Get script_id + script_id = TelegramSession.get_session() + if script_id != "unknown": + TelegramSession.deleteScriptId(script_id) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/site.py b/StreamingCommunity/Api/Site/animeunity/site.py index 6a3349d..d139932 100644 --- a/StreamingCommunity/Api/Site/animeunity/site.py +++ b/StreamingCommunity/Api/Site/animeunity/site.py @@ -1,6 +1,5 @@ # 10.12.23 -import sys import logging @@ -140,7 +139,7 @@ def title_search(query: str) -> int: 'type': dict_title.get('type'), 'status': dict_title.get('status'), 'episodes_count': dict_title.get('episodes_count'), - 'plot': ' '.join((words := str(dict_title.get('plot', '')).split())[:10]) + ('...' if len(words) > 10 else '') + 'image': dict_title.get('imageurl') }) if site_constant.TELEGRAM_BOT: diff --git a/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py index 26187a5..606af32 100644 --- a/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py @@ -94,3 +94,18 @@ class ScrapeSerieAnime: except Exception as e: logging.error(f"Error fetching episode information: {e}") return None + + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the anime. + Note: AnimeUnity typically doesn't have seasons, so returns 1. + """ + return 1 + + def selectEpisode(self, season_number: int = 1, episode_index: int = 0) -> Episode: + """ + Get information for a specific episode. + """ + return self.get_info_episode(episode_index) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeworld/__init__.py b/StreamingCommunity/Api/Site/animeworld/__init__.py index e040363..f97a571 100644 --- a/StreamingCommunity/Api/Site/animeworld/__init__.py +++ b/StreamingCommunity/Api/Site/animeworld/__init__.py @@ -14,6 +14,7 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem # Logic class from .site import title_search, media_search_manager, table_show_manager from .serie import download_series +from .film import download_film # Variable @@ -28,44 +29,56 @@ console = Console() -def process_search_result(select_title): +def process_search_result(select_title, selections=None): """ Handles the search result and initiates the download for either a film or series. + + Parameters: + select_title (MediaItem): The selected media item + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if select_title.type == "TV": - download_series(select_title) + episode_selection = None + if selections: + episode_selection = selections.get('episode') + download_series(select_title, episode_selection) -def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): + else: + download_film(select_title) + +def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for get_onlyDatabase (bool, optional): If True, return only the database object direct_item (dict, optional): Direct item to process (bypass search) + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if direct_item: select_title = MediaItem(**direct_item) - process_search_result(select_title) + process_search_result(select_title, selections) return # Get the user input for the search term - string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + if string_to_search is None: + string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() # Perform the database search len_database = title_search(string_to_search) - ##If only the database is needed, return the manager + # If only the database is needed, return the manager if get_onlyDatabase: return media_search_manager if len_database > 0: select_title = get_select_title(table_show_manager, media_search_manager) - process_search_result(select_title) - + process_search_result(select_title, selections) + else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # If no results are found, ask again - string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeworld/film.py b/StreamingCommunity/Api/Site/animeworld/film.py new file mode 100644 index 0000000..1c8d016 --- /dev/null +++ b/StreamingCommunity/Api/Site/animeworld/film.py @@ -0,0 +1,63 @@ +# 11.03.24 + +import os + +# External library +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.os import os_manager +from StreamingCommunity.Util.message import start_message +from StreamingCommunity.Lib.Downloader import MP4_downloader + + +# Logic class +from .util.ScrapeSerie import ScrapSerie +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Class.SearchType import MediaItem + + +# Player +from StreamingCommunity.Api.Player.sweetpixel import VideoSource + + +# Variable +console = Console() + + +def download_film(select_title: MediaItem): + """ + Function to download a film. + + Parameters: + - id_film (int): The ID of the film. + - title_name (str): The title of the film. + """ + start_message() + + scrape_serie = ScrapSerie(select_title.url, site_constant.FULL_URL) + episodes = scrape_serie.get_episodes() + + # Get episode information + episode_data = episodes[0] + console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] ([cyan]{scrape_serie.get_name()}[/cyan]) \n") + + # Define filename and path for the downloaded video + mp4_name = f"{scrape_serie.get_name()}.mp4" + mp4_path = os.path.join(site_constant.ANIME_FOLDER, scrape_serie.get_name()) + + # Create output folder + os_manager.create_path(mp4_path) + + # Get video source for the episode + video_source = VideoSource(site_constant.FULL_URL, episode_data, scrape_serie.session_id, scrape_serie.csrf_token) + mp4_link = video_source.get_playlist() + + # Start downloading + path, kill_handler = MP4_downloader( + url=str(mp4_link).strip(), + path=os.path.join(mp4_path, mp4_name) + ) + + return path, kill_handler \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeworld/serie.py b/StreamingCommunity/Api/Site/animeworld/serie.py index f27bb87..3f14cbc 100644 --- a/StreamingCommunity/Api/Site/animeworld/serie.py +++ b/StreamingCommunity/Api/Site/animeworld/serie.py @@ -19,12 +19,12 @@ from StreamingCommunity.Lib.Downloader import MP4_downloader # Logic class from .util.ScrapeSerie import ScrapSerie from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Util import manage_selection, dynamic_format_number, map_episode_title +from StreamingCommunity.Api.Template.Util import manage_selection, dynamic_format_number from StreamingCommunity.Api.Template.Class.SearchType import MediaItem # Player -from StreamingCommunity.Api.Player.sweetpixel import AnimeWorldPlayer +from StreamingCommunity.Api.Player.sweetpixel import VideoSource # Variable @@ -33,8 +33,7 @@ msg = Prompt() KILL_HANDLER = bool(False) - -def download_episode(index_select: int, scrape_serie: ScrapSerie, episodes) -> Tuple[str,bool]: +def download_episode(index_select: int, scrape_serie: ScrapSerie) -> Tuple[str,bool]: """ Downloads the selected episode. @@ -47,7 +46,8 @@ def download_episode(index_select: int, scrape_serie: ScrapSerie, episodes) -> T """ start_message() - # Get information about the selected episode + # Get episode information + episode_data = scrape_serie.selectEpisode(1, index_select) console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] ([cyan]E{index_select+1}[/cyan]) \n") # Define filename and path for the downloaded video @@ -57,9 +57,9 @@ def download_episode(index_select: int, scrape_serie: ScrapSerie, episodes) -> T # Create output folder os_manager.create_path(mp4_path) - # Collect mp4 link - video_source = AnimeWorldPlayer(site_constant.FULL_URL, episodes[index_select], scrape_serie.session_id, scrape_serie.csrf_token) - mp4_link = video_source.get_download_link() + # Get video source for the episode + video_source = VideoSource(site_constant.FULL_URL, episode_data, scrape_serie.session_id, scrape_serie.csrf_token) + mp4_link = video_source.get_playlist() # Start downloading path, kill_handler = MP4_downloader( @@ -70,38 +70,41 @@ def download_episode(index_select: int, scrape_serie: ScrapSerie, episodes) -> T return path, kill_handler -def download_series(select_title: MediaItem): +def download_series(select_title: MediaItem, episode_selection: str = None): """ Function to download episodes of a TV series. Parameters: - - tv_id (int): The ID of the TV series. - - tv_name (str): The name of the TV series. + - select_title (MediaItem): The selected media item + - episode_selection (str, optional): Episode selection input that bypasses manual input """ start_message() + # Create scrap instance scrape_serie = ScrapSerie(select_title.url, site_constant.FULL_URL) + episodes = scrape_serie.get_episodes() - # Get the count of episodes for the TV series - episodes = scrape_serie.get_episodes() - episoded_count = len(episodes) - console.print(f"[cyan]Episodes find: [red]{episoded_count}") + # Get episode count + console.print(f"[green]Episodes found:[/green] [red]{len(episodes)}[/red]") - # 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") + # Display episodes list and get user selection + if episode_selection is None: + 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") + else: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") - # Manage user selection - list_episode_select = manage_selection(last_command, episoded_count) + list_episode_select = manage_selection(last_command, len(episodes)) # Download selected episodes if len(list_episode_select) == 1 and last_command != "*": - path, _ = download_episode(list_episode_select[0]-1, scrape_serie, episodes) + path, _ = download_episode(list_episode_select[0]-1, scrape_serie) return path - # Download all other episodes selecter + # Download all selected episodes else: kill_handler = False for i_episode in list_episode_select: if kill_handler: break - _, kill_handler = download_episode(i_episode-1, scrape_serie, episodes) \ No newline at end of file + _, kill_handler = download_episode(i_episode-1, scrape_serie) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeworld/site.py b/StreamingCommunity/Api/Site/animeworld/site.py index d110e0c..a7baac1 100644 --- a/StreamingCommunity/Api/Site/animeworld/site.py +++ b/StreamingCommunity/Api/Site/animeworld/site.py @@ -101,7 +101,8 @@ def title_search(query: str) -> int: 'name': title, 'type': anime_type, 'DUB': is_dubbed, - 'url': url + 'url': url, + 'image': element.find('img').get('src') }) except Exception as e: diff --git a/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py index 7c236ab..0dd36a7 100644 --- a/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py @@ -1,5 +1,6 @@ # 21.03.25 +import logging # External libraries import httpx @@ -14,7 +15,7 @@ from StreamingCommunity.Util.os import os_manager # Player from ..site import get_session_and_csrf -from StreamingCommunity.Api.Player.sweetpixel import AnimeWorldPlayer +from StreamingCommunity.Api.Player.sweetpixel import VideoSource # Variable @@ -40,7 +41,6 @@ class ScrapSerie: except: raise Exception(f"Failed to retrieve anime page.") - def get_name(self): """Extract and return the name of the anime series.""" soup = BeautifulSoup(self.response.content, "html.parser") @@ -68,12 +68,39 @@ class ScrapSerie: return episodes def get_episode(self, index): - """Fetch a specific episode based on the index, and return an AnimeWorldPlayer instance.""" + """Fetch a specific episode based on the index, and return an VideoSource instance.""" episodes = self.get_episodes() if 0 <= index < len(episodes): episode_data = episodes[index] - return AnimeWorldPlayer(episode_data, self.session_id, self.csrf_token) + return VideoSource(episode_data, self.session_id, self.csrf_token) else: - raise IndexError("Episode index out of range") \ No newline at end of file + raise IndexError("Episode index out of range") + + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the anime. + Note: AnimeWorld typically doesn't have seasons, so returns 1. + """ + return 1 + + def getEpisodeSeasons(self, season_number: int = 1) -> list: + """ + Get all episodes for a specific season. + Note: For AnimeWorld, this returns all episodes as they're typically in one season. + """ + return self.get_episodes() + + def selectEpisode(self, season_number: int = 1, episode_index: int = 0) -> dict: + """ + Get information for a specific episode. + """ + episodes = self.get_episodes() + if not episodes or episode_index < 0 or episode_index >= len(episodes): + logging.error(f"Episode index {episode_index} is out of range") + return None + + return episodes[episode_index] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/cb01new/__init__.py b/StreamingCommunity/Api/Site/cb01new/__init__.py index 584a897..9fabae5 100644 --- a/StreamingCommunity/Api/Site/cb01new/__init__.py +++ b/StreamingCommunity/Api/Site/cb01new/__init__.py @@ -39,7 +39,7 @@ def process_search_result(select_title): def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for diff --git a/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py b/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py index e24259d..c192bcc 100644 --- a/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +++ b/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py @@ -42,7 +42,7 @@ def process_search_result(select_title): def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for diff --git a/StreamingCommunity/Api/Site/ddlstreamitaly/series.py b/StreamingCommunity/Api/Site/ddlstreamitaly/series.py index a7ec9f8..9693713 100644 --- a/StreamingCommunity/Api/Site/ddlstreamitaly/series.py +++ b/StreamingCommunity/Api/Site/ddlstreamitaly/series.py @@ -35,22 +35,22 @@ from StreamingCommunity.Api.Player.ddl import VideoSource console = Console() -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) -> Tuple[str,bool]: """ - Download a single episode video. + Downloads a specific episode. Parameters: - - tv_name (str): Name of the TV series. - - index_episode_selected (int): Index of the selected episode. + - index_episode_selected (int): Episode index + - scape_info_serie (GetSerieInfo): Scraper object with series information - Return: - - str: output path - - bool: kill handler status + Returns: + - str: Path to downloaded file + - bool: Whether download was stopped """ start_message() - # Get info about episode - obj_episode = scape_info_serie.list_episodes[index_episode_selected - 1] + # Get episode information + obj_episode = scape_info_serie.selectEpisode(1, index_episode_selected-1) console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [bold magenta]{obj_episode.get('name')}[/bold magenta] ([cyan]E{index_episode_selected}[/cyan]) \n") # Define filename and path for the downloaded video @@ -63,7 +63,7 @@ def download_video(index_episode_selected: int, scape_info_serie: GetSerieInfo, os_manager.create_path(mp4_path) # Setup video source - video_source.setup(obj_episode.get('url')) + video_source = VideoSource(site_constant.COOKIE, obj_episode.get('url')) # Get m3u8 master playlist master_playlist = video_source.get_playlist() @@ -82,38 +82,37 @@ def download_video(index_episode_selected: int, scape_info_serie: GetSerieInfo, console.print("[green]Result: ") console.print(r_proc) - return os.path.join(mp4_path, title_name) + return os.path.join(mp4_path, title_name), False -def download_thread(dict_serie: MediaItem): +def download_thread(dict_serie: MediaItem, episode_selection: str = None): """ Download all episode of a thread + + Parameters: + dict_serie (MediaItem): The selected media item + episode_selection (str, optional): Episode selection input that bypasses manual input """ - start_message() - - # Init class - scape_info_serie = GetSerieInfo(dict_serie, site_constant.COOKIE) - video_source = VideoSource(site_constant.COOKIE) - - # Collect information about thread - list_dict_episode = scape_info_serie.get_episode_number() - episodes_count = len(list_dict_episode) - + scrape_serie = GetSerieInfo(dict_serie, site_constant.COOKIE) + + # Get episode list + episodes = scrape_serie.getEpisodeSeasons() + episodes_count = len(episodes) + # Display episodes list and manage user selection - last_command = display_episodes_list(scape_info_serie.list_episodes) + if episode_selection is None: + last_command = display_episodes_list(scrape_serie.list_episodes) + else: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") + + # Validate episode selection list_episode_select = manage_selection(last_command, episodes_count) - - try: - list_episode_select = validate_episode_selection(list_episode_select, episodes_count) - - except ValueError as e: - console.print(f"[red]{str(e)}") - return + list_episode_select = validate_episode_selection(list_episode_select, episodes_count) # 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] \ No newline at end of file + kill_handler = download_video(i_episode, scrape_serie)[1] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/ddlstreamitaly/site.py b/StreamingCommunity/Api/Site/ddlstreamitaly/site.py index 31f04c0..aebb5a9 100644 --- a/StreamingCommunity/Api/Site/ddlstreamitaly/site.py +++ b/StreamingCommunity/Api/Site/ddlstreamitaly/site.py @@ -1,6 +1,5 @@ # 09.06.24 -import sys import logging @@ -67,7 +66,8 @@ def title_search(query: str) -> int: title_info = { 'name': name, 'url': link, - 'type': title_type + 'type': title_type, + 'image': title_div.find("div", class_="ipsColumn").find("img").get("src") } media_search_manager.add_media(title_info) diff --git a/StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py index ed02262..ea0b8db 100644 --- a/StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py @@ -47,7 +47,6 @@ class GetSerieInfo: Returns: List[Dict[str, str]]: List of dictionaries containing episode information. """ - try: response = httpx.get(f"{self.url}?area=online", cookies=self.cookies, headers=self.headers, timeout=max_timeout) response.raise_for_status() @@ -81,4 +80,33 @@ class GetSerieInfo: self.list_episodes = list_dict_episode return list_dict_episode - \ No newline at end of file + + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the series. + Note: DDLStreamItaly typically provides content organized as threads, not seasons. + """ + return 1 + + def getEpisodeSeasons(self, season_number: int = 1) -> list: + """ + Get all episodes for a specific season. + Note: For DDLStreamItaly, this returns all episodes as they're typically in one list. + """ + if not self.list_episodes: + self.list_episodes = self.get_episode_number() + + return self.list_episodes + + def selectEpisode(self, season_number: int = 1, episode_index: int = 0) -> dict: + """ + Get information for a specific episode. + """ + episodes = self.getEpisodeSeasons() + if not episodes or episode_index < 0 or episode_index >= len(episodes): + logging.error(f"Episode index {episode_index} is out of range") + return None + + return episodes[episode_index] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/guardaserie/__init__.py b/StreamingCommunity/Api/Site/guardaserie/__init__.py index 3af176b..f4aa87f 100644 --- a/StreamingCommunity/Api/Site/guardaserie/__init__.py +++ b/StreamingCommunity/Api/Site/guardaserie/__init__.py @@ -30,24 +30,38 @@ msg = Prompt() console = Console() -def process_search_result(select_title): +def process_search_result(select_title, selections=None): """ Handles the search result and initiates the download for either a film or series. + + Parameters: + select_title (MediaItem): The selected media item + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ - download_series(select_title) + season_selection = None + episode_selection = None + + if selections: + season_selection = selections.get('season') + episode_selection = selections.get('episode') + + download_series(select_title, season_selection, episode_selection) -def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): +def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for - get_onylDatabase (bool, optional): If True, return only the database object + get_onlyDatabase (bool, optional): If True, return only the database object direct_item (dict, optional): Direct item to process (bypass search) + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if direct_item: select_title = MediaItem(**direct_item) - process_search_result(select_title) + process_search_result(select_title, selections) return if string_to_search is None: @@ -62,7 +76,7 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ if len_database > 0: select_title = get_select_title(table_show_manager, media_search_manager) - process_search_result(select_title) + process_search_result(select_title, selections) else: diff --git a/StreamingCommunity/Api/Site/guardaserie/series.py b/StreamingCommunity/Api/Site/guardaserie/series.py index 3f00728..3f45853 100644 --- a/StreamingCommunity/Api/Site/guardaserie/series.py +++ b/StreamingCommunity/Api/Site/guardaserie/series.py @@ -1,6 +1,7 @@ # 13.06.24 import os +import logging from typing import Tuple @@ -39,22 +40,22 @@ console = Console() def download_video(index_season_selected: int, index_episode_selected: int, scape_info_serie: GetSerieInfo) -> Tuple[str,bool]: """ - Download a single episode video. + Downloads a specific episode from a specified season. Parameters: - - tv_name (str): Name of the TV series. - - index_season_selected (int): Index of the selected season. - - index_episode_selected (int): Index of the selected episode. + - index_season_selected (int): Season number + - index_episode_selected (int): Episode index + - scape_info_serie (GetSerieInfo): Scraper object with series information - Return: - - str: output path - - bool: kill handler status + Returns: + - str: Path to downloaded file + - bool: Whether download was stopped """ start_message() - index_season_selected = dynamic_format_number(str(index_season_selected)) - # Get info about episode - obj_episode = scape_info_serie.list_episodes[index_episode_selected - 1] + # Get episode information + obj_episode = scape_info_serie.selectEpisode(index_season_selected, index_episode_selected-1) + index_season_selected = dynamic_format_number(str(index_season_selected)) console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [bold magenta]{obj_episode.get('name')}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") # Define filename and path for the downloaded video @@ -80,24 +81,23 @@ def download_video(index_season_selected: int, index_episode_selected: int, scap return r_proc['path'], r_proc['stopped'] -def download_episode(scape_info_serie: GetSerieInfo, index_season_selected: int, download_all: bool = False) -> None: +def download_episode(scape_info_serie: GetSerieInfo, index_season_selected: int, download_all: bool = False, episode_selection: str = None) -> None: """ - Download all episodes of a season. + Handle downloading episodes for a specific season. Parameters: - - tv_name (str): Name of the TV series. - - index_season_selected (int): Index of the selected season. - - download_all (bool): Download all seasons episodes + - scape_info_serie (GetSerieInfo): Scraper object with series information + - index_season_selected (int): Season number + - download_all (bool): Whether to download all episodes + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ - - # Start message and collect information about episodes - start_message() - list_dict_episode = scape_info_serie.get_episode_number(index_season_selected) - episodes_count = len(list_dict_episode) + # Get episodes for the selected season + episodes = scape_info_serie.get_episode_number(index_season_selected) + episodes_count = len(episodes) if download_all: - - # Download all episodes without asking + + # Download all episodes in the season for i_episode in range(1, episodes_count + 1): path, stopped = download_video(index_season_selected, i_episode, scape_info_serie) @@ -109,14 +109,15 @@ def download_episode(scape_info_serie: GetSerieInfo, index_season_selected: int, else: # Display episodes list and manage user selection - last_command = display_episodes_list(scape_info_serie.list_episodes) + if episode_selection is None: + last_command = display_episodes_list(scape_info_serie.list_episodes) + else: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") + + # Validate the selection list_episode_select = manage_selection(last_command, episodes_count) - - try: - list_episode_select = validate_episode_selection(list_episode_select, episodes_count) - except ValueError as e: - console.print(f"[red]{str(e)}") - return + list_episode_select = validate_episode_selection(list_episode_select, episodes_count) # Download selected episodes for i_episode in list_episode_select: @@ -126,46 +127,47 @@ def download_episode(scape_info_serie: GetSerieInfo, index_season_selected: int, break -def download_series(dict_serie: MediaItem) -> None: +def download_series(dict_serie: MediaItem, season_selection: str = None, episode_selection: str = None) -> None: """ - Download all episodes of a TV series. + Handle downloading a complete series. Parameters: - - dict_serie (MediaItem): obj with url name type and score + - dict_serie (MediaItem): Series metadata from search + - season_selection (str, optional): Pre-defined season selection that bypasses manual input + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ - - # Start message and set up video source start_message() - # Init class - scape_info_serie = GetSerieInfo(dict_serie) - - # Collect information about seasons - seasons_count = scape_info_serie.get_seasons_number() + # Create class + scrape_serie = GetSerieInfo(dict_serie) + # Get season count + seasons_count = scrape_serie.get_seasons_number() + # 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" - ) - - # Manage and validate the selection - list_season_select = manage_selection(index_season_selected, seasons_count) - try: - list_season_select = validate_selection(list_season_select, seasons_count) - except ValueError as e: - console.print(f"[red]{str(e)}") - return + # If season_selection is provided, use it instead of asking for input + if season_selection is None: + 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" + ) + else: + index_season_selected = season_selection + console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}") + + # Validate the selection + list_season_select = manage_selection(index_season_selected, seasons_count) + list_season_select = validate_selection(list_season_select, seasons_count) # Loop through the selected seasons and download episodes for i_season in list_season_select: if len(list_season_select) > 1 or index_season_selected == "*": # Download all episodes if multiple seasons are selected or if '*' is used - download_episode(scape_info_serie, i_season, download_all=True) + download_episode(scrape_serie, i_season, download_all=True) else: # Otherwise, let the user select specific episodes for the single season - download_episode(scape_info_serie, i_season, download_all=False) \ No newline at end of file + download_episode(scrape_serie, i_season, download_all=False, episode_selection=episode_selection) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/guardaserie/site.py b/StreamingCommunity/Api/Site/guardaserie/site.py index af53ad5..9703a37 100644 --- a/StreamingCommunity/Api/Site/guardaserie/site.py +++ b/StreamingCommunity/Api/Site/guardaserie/site.py @@ -62,9 +62,10 @@ def title_search(query: str) -> int: link = serie_div.find('a').get("href") serie_info = { - 'name': title, + 'name': title.replace("streaming guardaserie", ""), 'url': link, - 'type': 'tv' + 'type': 'tv', + 'image': f"{site_constant.FULL_URL}/{serie_div.find('img').get("src")}", } media_search_manager.add_media(serie_info) diff --git a/StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py index c552a2c..3208829 100644 --- a/StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py @@ -104,4 +104,30 @@ class GetSerieInfo: except Exception as e: logging.error(f"Error parsing HTML page: {e}") - return [] \ No newline at end of file + return [] + + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the series. + """ + return self.get_seasons_number() + + def getEpisodeSeasons(self, season_number: int) -> list: + """ + Get all episodes for a specific season. + """ + episodes = self.get_episode_number(season_number) + return episodes + + def selectEpisode(self, season_number: int, episode_index: int) -> dict: + """ + Get information for a specific episode in a specific season. + """ + episodes = self.getEpisodeSeasons(season_number) + if not episodes or episode_index < 0 or episode_index >= len(episodes): + logging.error(f"Episode index {episode_index} is out of range for season {season_number}") + return None + + return episodes[episode_index] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/mostraguarda/__init__.py b/StreamingCommunity/Api/Site/mostraguarda/__init__.py deleted file mode 100644 index 656e29e..0000000 --- a/StreamingCommunity/Api/Site/mostraguarda/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -# 26.05.24 - -from urllib.parse import quote_plus - - -# External library -from rich.console import Console -from rich.prompt import Prompt - - -# Internal utilities -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem -from StreamingCommunity.Lib.TMBD import tmdb, Json_film - - -# Logic class -from .film import download_film - - -# Variable -indice = 7 -_useFor = "film" -_deprecate = True -_priority = 2 -_engineDownload = "hls" - -msg = Prompt() -console = Console() - - -def process_search_result(select_title): - """ - Handles the search result and initiates the download for either a film or series. - """ - download_film(select_title) - - -def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): - """ - Main function of the application for search film, series and anime. - - Parameters: - string_to_search (str, optional): String to search for - get_onylDatabase (bool, optional): If True, return only the database object - direct_item (dict, optional): Direct item to process (bypass search) - """ - if direct_item: - select_title = MediaItem(**direct_item) - process_search_result(select_title) - return - - if string_to_search is None: - string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{site_constant.SITE_NAME}").strip() - - # Not available for the moment - if get_onlyDatabase: - return 0 - - # Search on database - movie_id = tmdb.search_movie(quote_plus(string_to_search)) - - if movie_id is not None: - movie_details: Json_film = tmdb.get_movie_details(tmdb_id=movie_id) - - # Download only film - download_film(movie_details) - - else: - - # If no results are found, ask again - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/mostraguarda/film.py b/StreamingCommunity/Api/Site/mostraguarda/film.py deleted file mode 100644 index f2a4ce0..0000000 --- a/StreamingCommunity/Api/Site/mostraguarda/film.py +++ /dev/null @@ -1,93 +0,0 @@ -# 17.09.24 - -import os -import logging - - -# External libraries -import httpx -from bs4 import BeautifulSoup -from rich.console import Console - - -# Internal utilities -from StreamingCommunity.Util.os import os_manager, get_call_stack -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.table import TVShowManager -from StreamingCommunity.Lib.Downloader import HLS_Downloader - - -# Player -from StreamingCommunity.Api.Player.supervideo import VideoSource - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Lib.TMBD import Json_film - - -# Variable -console = Console() - - -def download_film(movie_details: Json_film) -> str: - """ - Downloads a film using the provided tmbd id. - - Parameters: - - movie_details (Json_film): Class with info about film title. - - Return: - - str: output path - """ - - # Start message and display film information - start_message() - console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{movie_details.title}[/cyan] \n") - - # Make request to main site - try: - url = f"{site_constant.FULL_URL}/set-movie-a/{movie_details.imdb_id}" - response = httpx.get(url, headers={'User-Agent': get_userAgent()}) - response.raise_for_status() - - except: - logging.error(f"Not found in the server. Dict: {movie_details}") - raise - - if "not found" in str(response.text): - logging.error(f"Cant find in the server: {movie_details.title}.") - - research_func = next(( - f for f in get_call_stack() - if f['function'] == 'search' and f['script'] == '__init__.py' - ), None) - TVShowManager.run_back_command(research_func) - - # Extract supervideo url - soup = BeautifulSoup(response.text, "html.parser") - player_links = soup.find("ul", class_ = "_player-mirrors").find_all("li") - supervideo_url = "https:" + player_links[0].get("data-link") - - # Set domain and media ID for the video source - video_source = VideoSource(url=supervideo_url) - - # Define output path - title_name = os_manager.get_sanitize_file(movie_details.title) + ".mp4" - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", "")) - - # Get m3u8 master playlist - master_playlist = video_source.get_playlist() - - # Download the film using the m3u8 playlist, and output filename - r_proc = HLS_Downloader( - m3u8_url=master_playlist, - output_path=os.path.join(mp4_path, title_name) - ).start() - - if r_proc['error'] is not None: - try: os.remove(r_proc['path']) - except: pass - - return r_proc['path'] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/__init__.py b/StreamingCommunity/Api/Site/raiplay/__init__.py new file mode 100644 index 0000000..7947fcd --- /dev/null +++ b/StreamingCommunity/Api/Site/raiplay/__init__.py @@ -0,0 +1,93 @@ +# 21.05.24 + + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import get_select_title +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Class.SearchType import MediaItem + + +# Logic class +from .site import title_search, table_show_manager, media_search_manager +from .series import download_series +from .film import download_film + + +# Variable +indice = 8 +_useFor = "film_serie" +_deprecate = False +_priority = 3 +_engineDownload = "hls" + +msg = Prompt() +console = Console() + + +def get_user_input(string_to_search: str = None): + """ + Asks the user to input a search term. + """ + return msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + +def process_search_result(select_title, selections=None): + """ + Handles the search result and initiates the download for either a film or series. + + Parameters: + select_title (MediaItem): The selected media item + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} + """ + if select_title.type == 'tv': + season_selection = None + episode_selection = None + + if selections: + season_selection = selections.get('season') + episode_selection = selections.get('episode') + + download_series(select_title, season_selection, episode_selection) + + else: + download_film(select_title) + +def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): + """ + Main function of the application for search. + + Parameters: + string_to_search (str, optional): String to search for + get_onlyDatabase (bool, optional): If True, return only the database object + direct_item (dict, optional): Direct item to process (bypass search) + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} + """ + if direct_item: + select_title = MediaItem(**direct_item) + process_search_result(select_title, selections) + return + + if string_to_search is None: + string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + + # Search on database + len_database = title_search(string_to_search) + + # If only the database is needed, return the manager + if get_onlyDatabase: + return media_search_manager + + if len_database > 0: + select_title = get_select_title(table_show_manager, media_search_manager) + process_search_result(select_title, selections) + + else: + # If no results are found, ask again + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") + search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/film.py b/StreamingCommunity/Api/Site/raiplay/film.py new file mode 100644 index 0000000..ea11581 --- /dev/null +++ b/StreamingCommunity/Api/Site/raiplay/film.py @@ -0,0 +1,65 @@ +# 3.12.23 + +import os +from typing import Tuple + + +# External library +import httpx +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.os import os_manager +from StreamingCommunity.Util.message import start_message +from StreamingCommunity.Lib.Downloader import HLS_Downloader +from StreamingCommunity.Util.headers import get_headers + + +# Logic class +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Class.SearchType import MediaItem + + +# Player +from StreamingCommunity.Api.Player.mediapolisvod import VideoSource + + +# Variable +console = Console() + + +def download_film(select_title: MediaItem) -> Tuple[str, bool]: + """ + Downloads a film using the provided MediaItem information. + + Parameters: + - select_title (MediaItem): The media item containing film information + + Return: + - str: Path to downloaded file + - bool: Whether download was stopped + """ + start_message() + console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") + + # Extract m3u8 URL from the film's URL + response = httpx.get(select_title.url + ".json", headers=get_headers(), timeout=10) + first_item_path = "https://www.raiplay.it" + response.json().get("first_item_path") + master_playlist = VideoSource.extract_m3u8_url(first_item_path) + + # Define the filename and path for the downloaded film + title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4" + mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", "")) + + # Download the film using the m3u8 playlist, and output filename + r_proc = HLS_Downloader( + m3u8_url=master_playlist, + output_path=os.path.join(mp4_path, title_name) + ).start() + + if r_proc['error'] is not None: + try: os.remove(r_proc['path']) + except: pass + + return r_proc['path'], r_proc['stopped'] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/series.py b/StreamingCommunity/Api/Site/raiplay/series.py new file mode 100644 index 0000000..13a28d0 --- /dev/null +++ b/StreamingCommunity/Api/Site/raiplay/series.py @@ -0,0 +1,162 @@ +# 3.12.23 + +import os +from typing import Tuple + + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Util.message import start_message +from StreamingCommunity.Lib.Downloader import HLS_Downloader + +# Logic class +from .util.ScrapeSerie import GetSerieInfo +from StreamingCommunity.Api.Template.Util import ( + manage_selection, + map_episode_title, + validate_selection, + validate_episode_selection, + display_episodes_list +) +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Class.SearchType import MediaItem + + +# Player +from StreamingCommunity.Api.Player.mediapolisvod import VideoSource + + +# Variable +msg = Prompt() +console = Console() + + +def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo) -> Tuple[str,bool]: + """ + Downloads a specific episode from the specified season. + + Parameters: + - index_season_selected (int): Season number + - index_episode_selected (int): Episode index + - scrape_serie (GetSerieInfo): Scraper object with series information + + Returns: + - str: Path to downloaded file + - bool: Whether download was stopped + """ + start_message() + + # Get episode information + obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) + console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + + # Get streaming URL + master_playlist = VideoSource.extract_m3u8_url(obj_episode.url) + + # Define filename and path + 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(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + + # Download the episode + r_proc = HLS_Downloader( + m3u8_url=master_playlist, + output_path=os.path.join(mp4_path, mp4_name) + ).start() + + if r_proc['error'] is not None: + try: os.remove(r_proc['path']) + except: pass + + return r_proc['path'], r_proc['stopped'] + + +def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None) -> None: + """ + Handle downloading episodes for a specific season. + + Parameters: + - index_season_selected (int): Season number + - scrape_serie (GetSerieInfo): Scraper object with series information + - download_all (bool): Whether to download all episodes + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input + """ + # Get episodes for the selected season + episodes = scrape_serie.getEpisodeSeasons(index_season_selected) + episodes_count = len(episodes) + + if download_all: + for i_episode in range(1, episodes_count + 1): + path, stopped = download_video(index_season_selected, i_episode, scrape_serie) + if stopped: + break + console.print(f"\n[red]End downloaded [yellow]season: [red]{index_season_selected}.") + + else: + # Display episodes list and manage user selection + if episode_selection is None: + last_command = display_episodes_list(episodes) + else: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") + + # Validate the selection + list_episode_select = manage_selection(last_command, episodes_count) + list_episode_select = validate_episode_selection(list_episode_select, episodes_count) + + # Download selected episodes if not stopped + for i_episode in list_episode_select: + path, stopped = download_video(index_season_selected, i_episode, scrape_serie) + if stopped: + break + +def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None) -> None: + """ + Handle downloading a complete series. + + Parameters: + - select_season (MediaItem): Series metadata from search + - season_selection (str, optional): Pre-defined season selection that bypasses manual input + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input + """ + start_message() + + # Extract program name from path_id + program_name = None + if select_season.path_id: + parts = select_season.path_id.strip('/').split('/') + if len(parts) >= 2: + program_name = parts[-1].split('.')[0] + + # Init scraper + scrape_serie = GetSerieInfo(program_name) + + # Get seasons info + scrape_serie.collect_info_title() + seasons_count = len(scrape_serie.seasons_manager) + console.print(f"\n[green]Seasons found: [red]{seasons_count}") + + # If season_selection is provided, use it instead of asking for input + if season_selection is None: + 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" + ) + + else: + index_season_selected = season_selection + console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}") + + # Validate the selection + list_season_select = manage_selection(index_season_selected, seasons_count) + list_season_select = validate_selection(list_season_select, seasons_count) + + # Loop through the selected seasons and download episodes + for season_number in list_season_select: + if len(list_season_select) > 1 or index_season_selected == "*": + download_episode(season_number, scrape_serie, download_all=True) + else: + download_episode(season_number, scrape_serie, download_all=False, episode_selection=episode_selection) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/site.py b/StreamingCommunity/Api/Site/raiplay/site.py new file mode 100644 index 0000000..291a9ab --- /dev/null +++ b/StreamingCommunity/Api/Site/raiplay/site.py @@ -0,0 +1,166 @@ +# 10.12.23 + +import threading +import queue + +# External libraries +import httpx +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Util.headers import get_userAgent +from StreamingCommunity.Util.table import TVShowManager +from StreamingCommunity.Lib.TMBD.tmdb import tmdb + + +# Logic class +from StreamingCommunity.Api.Template.config_loader import site_constant +from StreamingCommunity.Api.Template.Class.SearchType import MediaManager + + +# Variable +console = Console() +media_search_manager = MediaManager() +table_show_manager = TVShowManager() +max_timeout = config_manager.get_int("REQUESTS", "timeout") +MAX_THREADS = 4 + + +def determine_media_type(title): + """ + Use TMDB to determine if a title is a movie or TV show. + """ + try: + # First search as a movie + movie_results = tmdb._make_request("search/movie", {"query": title}) + movie_count = len(movie_results.get("results", [])) + + # Then search as a TV show + tv_results = tmdb._make_request("search/tv", {"query": title}) + tv_count = len(tv_results.get("results", [])) + + # If results found in only one category, use that + if movie_count > 0 and tv_count == 0: + return "film" + elif tv_count > 0 and movie_count == 0: + return "tv" + + # If both have results, compare popularity + if movie_count > 0 and tv_count > 0: + top_movie = movie_results["results"][0] + top_tv = tv_results["results"][0] + + return "film" if top_movie.get("popularity", 0) > top_tv.get("popularity", 0) else "tv" + + return "film" + + except Exception as e: + console.log(f"Error determining media type with TMDB: {e}") + return "film" + + +def worker_determine_type(work_queue, result_dict, worker_id): + """ + Worker function to process items from queue and determine media types. + + Parameters: + - work_queue: Queue containing items to process + - result_dict: Dictionary to store results + - worker_id: ID of the worker thread + """ + while not work_queue.empty(): + try: + index, item = work_queue.get(block=False) + title = item.get('titolo', '') + media_type = determine_media_type(title) + + result_dict[index] = { + 'id': item.get('id', ''), + 'name': title, + 'type': media_type, + 'path_id': item.get('path_id', ''), + 'url': f"https://www.raiplay.it{item.get('url', '')}", + 'image': f"https://www.raiplay.it{item.get('immagine', '')}", + } + + work_queue.task_done() + + except queue.Empty: + break + + except Exception as e: + console.log(f"Worker {worker_id} error: {e}") + work_queue.task_done() + + +def title_search(query: str) -> int: + """ + Search for titles based on a search query. + + Parameters: + - query (str): The query to search for. + + Returns: + int: The number of titles found. + """ + media_search_manager.clear() + table_show_manager.clear() + + search_url = f"https://www.raiplay.it/atomatic/raiplay-search-service/api/v1/msearch" + console.print(f"[cyan]Search url: [yellow]{search_url}") + + json_data = { + 'templateIn': '6470a982e4e0301afe1f81f1', + 'templateOut': '6516ac5d40da6c377b151642', + 'params': { + 'param': query, + 'from': None, + 'sort': 'relevance', + 'onlyVideoQuery': False, + }, + } + + try: + response = httpx.post(search_url, headers={'user-agent': get_userAgent()}, json=json_data, timeout=max_timeout, follow_redirects=True) + response.raise_for_status() + + except Exception as e: + console.print(f"Site: {site_constant.SITE_NAME}, request search error: {e}") + return 0 + + # Limit to only 15 results for performance + data = response.json().get('agg').get('titoli').get('cards') + data = data[:15] if len(data) > 15 else data + + # Use multithreading to determine media types in parallel + work_queue = queue.Queue() + result_dict = {} + + # Add items to the work queue + for i, item in enumerate(data): + work_queue.put((i, item)) + + # Create and start worker threads + threads = [] + for i in range(min(MAX_THREADS, len(data))): + thread = threading.Thread( + target=worker_determine_type, + args=(work_queue, result_dict, i), + daemon=True + ) + threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join() + + # Add all results to media manager in correct order + for i in range(len(data)): + if i in result_dict: + media_search_manager.add_media(result_dict[i]) + + # Return the number of titles found + return media_search_manager.get_length() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py new file mode 100644 index 0000000..ac69bd1 --- /dev/null +++ b/StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py @@ -0,0 +1,127 @@ +# 01.03.24 + +import logging + + +# External libraries +import httpx + + +# Internal utilities +from StreamingCommunity.Util.headers import get_headers +from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager + + +# Variable +max_timeout = config_manager.get_int("REQUESTS", "timeout") + + +class GetSerieInfo: + def __init__(self, program_name: str): + """Initialize the GetSerieInfo class.""" + self.base_url = "https://www.raiplay.it" + self.program_name = program_name + self.series_name = program_name + self.seasons_manager = SeasonManager() + + def collect_info_title(self) -> None: + """Get series info including seasons.""" + try: + program_url = f"{self.base_url}/programmi/{self.program_name}.json" + response = httpx.get(url=program_url, headers=get_headers(), timeout=max_timeout) + response.raise_for_status() + + json_data = response.json() + + # Look for seasons in the 'blocks' property + for block in json_data.get('blocks'): + if block.get('type') == 'RaiPlay Multimedia Block' and block.get('name', '').lower() == 'episodi': + self.publishing_block_id = block.get('id') + + # Extract seasons from sets array + for season_set in block.get('sets', []): + if 'stagione' in season_set.get('name', '').lower(): + self.seasons_manager.add_season({ + 'id': season_set.get('id', ''), + 'number': len(self.seasons_manager.seasons) + 1, + 'name': season_set.get('name', ''), + 'path': season_set.get('path_id', ''), + 'episodes_count': season_set.get('episode_size', {}).get('number', 0) + }) + + except Exception as e: + logging.error(f"Error collecting series info: {e}") + + def collect_info_season(self, number_season: int) -> None: + """Get episodes for a specific season.""" + try: + season = self.seasons_manager.get_season_by_number(number_season) + + url = f"{self.base_url}/programmi/{self.program_name}/{self.publishing_block_id}/{season.id}/episodes.json" + response = httpx.get(url=url, headers=get_headers(), timeout=max_timeout) + response.raise_for_status() + + episodes_data = response.json() + cards = [] + + # Extract episodes from different possible structures + if 'seasons' in episodes_data: + for season_data in episodes_data.get('seasons', []): + for episode_set in season_data.get('episodes', []): + cards.extend(episode_set.get('cards', [])) + + if not cards: + cards = episodes_data.get('cards', []) + + # Add episodes to season + for ep in cards: + episode = { + 'id': ep.get('id', ''), + 'number': ep.get('episode', ''), + 'name': ep.get('episode_title', '') or ep.get('toptitle', ''), + 'duration': ep.get('duration', ''), + 'url': f"{self.base_url}{ep.get('weblink', '')}" if 'weblink' in ep else f"{self.base_url}{ep.get('url', '')}" + } + season.episodes.add(episode) + + except Exception as e: + logging.error(f"Error collecting episodes for season {number_season}: {e}") + raise + + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the series. + """ + if not self.seasons_manager.seasons: + self.collect_info_title() + + return len(self.seasons_manager.seasons) + + def getEpisodeSeasons(self, season_number: int) -> list: + """ + Get all episodes for a specific season. + """ + season = self.seasons_manager.get_season_by_number(season_number) + + if not season: + logging.error(f"Season {season_number} not found") + return [] + + if not season.episodes.episodes: + self.collect_info_season(season_number) + + return season.episodes.episodes + + def selectEpisode(self, season_number: int, episode_index: int) -> dict: + """ + Get information for a specific episode in a specific season. + """ + episodes = self.getEpisodeSeasons(season_number) + if not episodes or episode_index < 0 or episode_index >= len(episodes): + logging.error(f"Episode index {episode_index} is out of range for season {season_number}") + return None + + return episodes[episode_index] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py index f839023..7735535 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py @@ -58,52 +58,59 @@ def get_user_input(string_to_search: str = None): return string_to_search -def process_search_result(select_title): +def process_search_result(select_title, selections=None): """ Handles the search result and initiates the download for either a film or series. + + Parameters: + select_title (MediaItem): The selected media item + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if select_title.type == 'tv': - download_series(select_title) + season_selection = None + episode_selection = None + + if selections: + season_selection = selections.get('season') + episode_selection = selections.get('episode') + + download_series(select_title, season_selection, episode_selection) + else: download_film(select_title) -def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None): +def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): """ - Main function of the application for search film, series and anime. + Main function of the application for search. Parameters: string_to_search (str, optional): String to search for - get_onylDatabase (bool, optional): If True, return only the database object + get_onlyDatabase (bool, optional): If True, return only the database object direct_item (dict, optional): Direct item to process (bypass search) + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + {'season': season_selection, 'episode': episode_selection} """ if direct_item: select_title = MediaItem(**direct_item) - process_search_result(select_title) + process_search_result(select_title, selections) return - - # Get the user input for the search term - string_to_search = get_user_input(string_to_search) - - # Perform the database search - len_database = title_search(quote_plus(string_to_search)) + + if string_to_search is None: + string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + + # Search on database + len_database = title_search(string_to_search) # If only the database is needed, return the manager if get_onlyDatabase: return media_search_manager - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - if len_database > 0: select_title = get_select_title(table_show_manager, media_search_manager) - process_search_result(select_title) + process_search_result(select_title, selections) else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - - if site_constant.TELEGRAM_BOT: - bot.send_message(f"No results found, please try again", None) - # If no results are found, ask again - string_to_search = get_user_input() + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/film.py b/StreamingCommunity/Api/Site/streamingcommunity/film.py index 08b757c..7e129f2 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/film.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/film.py @@ -55,8 +55,7 @@ def download_film(select_title: MediaItem) -> str: console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") # Init class - video_source = VideoSource(site_constant.FULL_URL, False) - video_source.setup(select_title.id) + video_source = VideoSource(site_constant.FULL_URL, False, select_title.id) # Retrieve scws and if available master playlist video_source.get_iframe(select_title.id) diff --git a/StreamingCommunity/Api/Site/streamingcommunity/series.py b/StreamingCommunity/Api/Site/streamingcommunity/series.py index d35eebb..aa4f52f 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/series.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/series.py @@ -39,28 +39,22 @@ console = Console() def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo, video_source: VideoSource) -> Tuple[str,bool]: """ - Download a single episode video. + Downloads a specific episode from the specified season. Parameters: - - index_season_selected (int): Index of the selected season. - - index_episode_selected (int): Index of the selected episode. + - index_season_selected (int): Season number + - index_episode_selected (int): Episode index + - scrape_serie (GetSerieInfo): Scraper object with series information + - video_source (VideoSource): Video source handler - Return: - - str: output path - - bool: kill handler status + Returns: + - str: Path to downloaded file + - bool: Whether download was stopped """ start_message() - index_season_selected = dynamic_format_number(str(index_season_selected)) - # SPECIAL: Get season number - season = None - for s in scrape_serie.seasons_manager.seasons: - if s.number == int(index_season_selected): - season = s - break - - # Get info about episode - obj_episode = season.episodes.get(index_episode_selected - 1) + # Get episode information + obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") if site_constant.TELEGRAM_BOT: @@ -98,28 +92,28 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra return r_proc['path'], r_proc['stopped'] -def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, video_source: VideoSource, download_all: bool = False) -> None: + +def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, video_source: VideoSource, download_all: bool = False, episode_selection: str = None) -> None: """ - Download episodes of a selected season. + Handle downloading episodes for a specific season. Parameters: - - index_season_selected (int): Index of the selected season. - - download_all (bool): Download all episodes in the season. + - index_season_selected (int): Season number + - scrape_serie (GetSerieInfo): Scraper object with series information + - video_source (VideoSource): Video source object + - download_all (bool): Whether to download all episodes + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ - start_message() - scrape_serie.collect_info_season(index_season_selected) - - # SPECIAL: Get season number - season = None - for s in scrape_serie.seasons_manager.seasons: - if s.number == index_season_selected: - season = s - break - episodes_count = len(season.episodes.episodes) + # Get episodes for the selected season + episodes = scrape_serie.getEpisodeSeasons(index_season_selected) + episodes_count = len(episodes) + + if episodes_count == 0: + console.print(f"[red]No episodes found for season {index_season_selected}") + return if download_all: - - # Download all episodes without asking + # Download all episodes in the season for i_episode in range(1, episodes_count + 1): path, stopped = download_video(index_season_selected, i_episode, scrape_serie, video_source) @@ -129,16 +123,16 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, vid console.print(f"\n[red]End downloaded [yellow]season: [red]{index_season_selected}.") else: - # Display episodes list and manage user selection - last_command = display_episodes_list(season.episodes.episodes) + if episode_selection is None: + last_command = display_episodes_list(episodes) + else: + last_command = episode_selection + console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") + + # Validate the selection list_episode_select = manage_selection(last_command, episodes_count) - - try: - list_episode_select = validate_episode_selection(list_episode_select, episodes_count) - except ValueError as e: - console.print(f"[red]{str(e)}") - return + list_episode_select = validate_episode_selection(list_episode_select, episodes_count) # Download selected episodes if not stopped for i_episode in list_episode_select: @@ -147,70 +141,65 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, vid if stopped: break -def download_series(select_season: MediaItem) -> None: + +def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None) -> None: """ - Download episodes of a TV series based on user selection. + Handle downloading a complete series. Parameters: - - select_season (MediaItem): Selected media item (TV series). - - domain (str): Domain from which to download. + - select_season (MediaItem): Series metadata from search + - season_selection (str, optional): Pre-defined season selection that bypasses manual input + - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - # Start message and set up video source start_message() # Init class - scrape_serie = GetSerieInfo(site_constant.FULL_URL) - video_source = VideoSource(site_constant.FULL_URL, True) + video_source = VideoSource(site_constant.FULL_URL, True, select_season.id) + scrape_serie = GetSerieInfo(site_constant.FULL_URL, select_season.id, select_season.name) - # Setup video source - scrape_serie.setup(select_season.id, select_season.slug) - video_source.setup(select_season.id) - - # Collect information about seasons - scrape_serie.collect_info_title() + # Collect information about season + scrape_serie.getNumberSeason() seasons_count = len(scrape_serie.seasons_manager) + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() + # Prompt user for season selection and download episodes console.print(f"\n[green]Seasons found: [red]{seasons_count}") - if site_constant.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") + # If season_selection is provided, use it instead of asking for input + if season_selection is None: + if site_constant.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) + bot.send_message(f"Stagioni trovate: {seasons_count}", None) - index_season_selected = bot.ask( - "select_title_episode", - "Menu di selezione delle stagioni\n\n" - "- Inserisci il numero della stagione (ad esempio, 1)\n" - "- Inserisci * per scaricare tutte le stagioni\n" - "- Inserisci un intervallo di stagioni (ad esempio, 1-2) per scaricare da una stagione all'altra\n" - "- Inserisci (ad esempio, 3-*) per scaricare dalla stagione specificata fino alla fine della serie", - None - ) + index_season_selected = bot.ask( + "select_title_episode", + "Menu di selezione delle stagioni\n\n" + "- Inserisci il numero della stagione (ad esempio, 1)\n" + "- Inserisci * per scaricare tutte le stagioni\n" + "- Inserisci un intervallo di stagioni (ad esempio, 1-2) per scaricare da una stagione all'altra\n" + "- Inserisci (ad esempio, 3-*) per scaricare dalla stagione specificata fino alla fine della serie", + 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" + ) 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" - ) + index_season_selected = season_selection + console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}") - # Manage and validate the selection + # Validate the selection list_season_select = manage_selection(index_season_selected, seasons_count) - - try: - list_season_select = validate_selection(list_season_select, seasons_count) - except ValueError as e: - console.print(f"[red]{str(e)}") - return + list_season_select = validate_selection(list_season_select, seasons_count) # Loop through the selected seasons and download episodes for i_season in list_season_select: - - # SPECIAL: Get season number season = None for s in scrape_serie.seasons_manager.seasons: if s.number == i_season: @@ -219,13 +208,10 @@ def download_series(select_season: MediaItem) -> None: season_number = season.number if len(list_season_select) > 1 or index_season_selected == "*": - - # Download all episodes if multiple seasons are selected or if '*' is used download_episode(season_number, scrape_serie, video_source, download_all=True) + else: - - # Otherwise, let the user select specific episodes for the single season - download_episode(season_number, scrape_serie, video_source, download_all=False) + download_episode(season_number, scrape_serie, video_source, download_all=False, episode_selection=episode_selection) if site_constant.TELEGRAM_BOT: bot.send_message(f"Finito di scaricare tutte le serie e episodi", None) diff --git a/StreamingCommunity/Api/Site/streamingcommunity/site.py b/StreamingCommunity/Api/Site/streamingcommunity/site.py index 2e000bf..1ec4e46 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/site.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/site.py @@ -1,7 +1,5 @@ # 10.12.23 -import sys - # External libraries import httpx @@ -75,7 +73,7 @@ def title_search(query: str) -> int: 'name': dict_title.get('name'), 'type': dict_title.get('type'), 'date': dict_title.get('last_air_date'), - 'score': dict_title.get('score') + 'image': f"{site_constant.FULL_URL.replace("stream", "cdn.stream")}/images/{dict_title.get('images')[0].get('filename')}" }) if site_constant.TELEGRAM_BOT: diff --git a/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py index 259d2fc..5cd194c 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py @@ -20,31 +20,21 @@ max_timeout = config_manager.get_int("REQUESTS", "timeout") class GetSerieInfo: - def __init__(self, url): + def __init__(self, url, media_id: int = None, series_name: str = None): """ Initialize the GetSerieInfo class for scraping TV series information. Args: - url (str): The URL of the streaming site. + - media_id (int, optional): Unique identifier for the media + - series_name (str, optional): Name of the TV series """ self.is_series = False self.headers = {'user-agent': get_userAgent()} self.url = url - - # Initialize the SeasonManager + self.media_id = media_id self.seasons_manager = SeasonManager() - def setup(self, media_id: int = None, series_name: str = None): - """ - Set up the scraper with specific media details. - - Args: - media_id (int, optional): Unique identifier for the media - series_name (str, optional): Name of the TV series - """ - self.media_id = media_id - - # If series name is provided, initialize series-specific properties if series_name is not None: self.is_series = True self.series_name = series_name @@ -127,4 +117,40 @@ class GetSerieInfo: except Exception as e: logging.error(f"Error collecting episodes for season {number_season}: {e}") - raise \ No newline at end of file + raise + + # ------------- FOR GUI ------------- + def getNumberSeason(self) -> int: + """ + Get the total number of seasons available for the series. + """ + if not self.seasons_manager.seasons: + self.collect_info_title() + + return len(self.seasons_manager.seasons) + + def getEpisodeSeasons(self, season_number: int) -> list: + """ + Get all episodes for a specific season. + """ + season = self.seasons_manager.get_season_by_number(season_number) + + if not season: + logging.error(f"Season {season_number} not found") + return [] + + if not season.episodes.episodes: + self.collect_info_season(season_number) + + return season.episodes.episodes + + def selectEpisode(self, season_number: int, episode_index: int) -> dict: + """ + Get information for a specific episode in a specific season. + """ + episodes = self.getEpisodeSeasons(season_number) + if not episodes or episode_index < 0 or episode_index >= len(episodes): + logging.error(f"Episode index {episode_index} is out of range for season {season_number}") + return None + + return episodes[episode_index] \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/site.py b/StreamingCommunity/Api/Template/site.py index 6c22244..ac2ef86 100644 --- a/StreamingCommunity/Api/Template/site.py +++ b/StreamingCommunity/Api/Template/site.py @@ -10,7 +10,7 @@ from rich.console import Console # Variable console = Console() available_colors = ['red', 'magenta', 'yellow', 'cyan', 'green', 'blue', 'white'] -column_to_hide = ['Slug', 'Sub_ita', 'Last_air_date', 'Seasons_count', 'Url'] +column_to_hide = ['Slug', 'Sub_ita', 'Last_air_date', 'Seasons_count', 'Url', 'Image', 'Path_id'] def get_select_title(table_show_manager, media_search_manager): @@ -81,4 +81,4 @@ def get_select_title(table_show_manager, media_search_manager): else: console.print("\n[red]Wrong index") - sys.exit(0) + sys.exit(0) \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/HLS/downloader.py b/StreamingCommunity/Lib/Downloader/HLS/downloader.py index a6ed152..2b56ef2 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/downloader.py +++ b/StreamingCommunity/Lib/Downloader/HLS/downloader.py @@ -514,7 +514,7 @@ class HLS_Downloader: for item in self.download_manager.missing_segments: if int(item['nFailed']) >= 1: missing_ts = True - missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n" + missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]" file_size = internet_manager.format_file_size(os.path.getsize(self.path_manager.output_path)) duration = print_duration_table(self.path_manager.output_path, description=False, return_string=True) diff --git a/StreamingCommunity/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Lib/Downloader/HLS/segments.py index 9e376ea..ca347b5 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Lib/Downloader/HLS/segments.py @@ -41,10 +41,9 @@ REQUEST_VERIFY = config_manager.get_bool('REQUESTS', 'verify') 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") -MAX_INTERRUPT_COUNT = 3 SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout") TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') - +MAX_INTERRUPT_COUNT = 3 # Variable console = Console() @@ -160,7 +159,7 @@ class M3U8_Segments: if self.is_index_url: try: client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT} - response = httpx.get(self.url, **client_params) + response = httpx.get(self.url, **client_params, follow_redirects=True) response.raise_for_status() self.parse_data(response.text) diff --git a/StreamingCommunity/Lib/FFmpeg/util.py b/StreamingCommunity/Lib/FFmpeg/util.py index 5c7d97c..469d83c 100644 --- a/StreamingCommunity/Lib/FFmpeg/util.py +++ b/StreamingCommunity/Lib/FFmpeg/util.py @@ -138,26 +138,53 @@ def get_ffprobe_info(file_path): Returns: dict: A dictionary containing the format name and a list of codec names. + Returns None if file does not exist or ffprobe crashes. """ + if not os.path.exists(file_path): + logging.error(f"File not found: {file_path}") + return None + try: - result = subprocess.run( - [get_ffprobe_path(), '-v', 'error', '-show_format', '-show_streams', '-print_format', 'json', file_path], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True - ) - output = result.stdout - info = json.loads(output) + # Use subprocess.Popen instead of run to better handle crashes + cmd = [get_ffprobe_path(), '-v', 'error', '-show_format', '-show_streams', '-print_format', 'json', file_path] + logging.info(f"FFmpeg command: {cmd}") - format_name = info['format']['format_name'] if 'format' in info else None - codec_names = [stream['codec_name'] for stream in info['streams']] if 'streams' in info else [] - - return { - 'format_name': format_name, - 'codec_names': codec_names - } + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as proc: + stdout, stderr = proc.communicate() + + if proc.returncode != 0: + logging.error(f"FFprobe failed with return code {proc.returncode} for file {file_path}") + if stderr: + logging.error(f"FFprobe stderr: {stderr}") + return { + 'format_name': None, + 'codec_names': [] + } + + # Make sure we have valid JSON before parsing + if not stdout or not stdout.strip(): + logging.warning(f"FFprobe returned empty output for file {file_path}") + return { + 'format_name': None, + 'codec_names': [] + } + + info = json.loads(stdout) + + format_name = info['format']['format_name'] if 'format' in info else None + codec_names = [stream['codec_name'] for stream in info['streams']] if 'streams' in info else [] + + return { + 'format_name': format_name, + 'codec_names': codec_names + } except Exception as e: - logging.error(f"Failed to parse JSON output from ffprobe for file {file_path}: {e}") - return None + logging.error(f"Failed to get ffprobe info for file {file_path}: {e}") + return { + 'format_name': None, + 'codec_names': [] + } def is_png_format_or_codec(file_info): @@ -173,8 +200,11 @@ def is_png_format_or_codec(file_info): if not file_info: return False - #console.print(f"[yellow][FFmpeg] [cyan]Avaiable codec[white]: [red]{file_info['codec_names']}") - return file_info['format_name'] == 'png_pipe' or 'png' in file_info['codec_names'] + # Handle None values in format_name gracefully + format_name = file_info.get('format_name') + codec_names = file_info.get('codec_names', []) + + return format_name == 'png_pipe' or 'png' in codec_names def need_to_force_to_ts(file_path): diff --git a/StreamingCommunity/Lib/M3U8/estimator.py b/StreamingCommunity/Lib/M3U8/estimator.py index 0d748d9..4d07008 100644 --- a/StreamingCommunity/Lib/M3U8/estimator.py +++ b/StreamingCommunity/Lib/M3U8/estimator.py @@ -31,16 +31,20 @@ class M3U8_Ts_Estimator: self.segments_instance = segments_instance self.lock = threading.Lock() self.speed = {"upload": "N/A", "download": "N/A"} + self._running = True if get_use_large_bar(): logging.debug("USE_LARGE_BAR is True, starting speed capture thread") self.speed_thread = threading.Thread(target=self.capture_speed) self.speed_thread.daemon = True self.speed_thread.start() - else: logging.debug("USE_LARGE_BAR is False, speed capture thread not started") + def __del__(self): + """Ensure thread is properly stopped when the object is destroyed.""" + self._running = False + def add_ts_file(self, size: int): """Add a file size to the list of file sizes.""" if size <= 0: @@ -50,32 +54,44 @@ class M3U8_Ts_Estimator: self.ts_file_sizes.append(size) def capture_speed(self, interval: float = 1.5): - """Capture the internet speed periodically.""" + """Capture the internet speed periodically with improved efficiency.""" last_upload, last_download = 0, 0 speed_buffer = deque(maxlen=3) - while True: + while self._running: try: + # Get IO counters only once per loop to reduce function calls io_counters = psutil.net_io_counters() if not io_counters: raise ValueError("No IO counters available") 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)) + # Only update buffer when we have valid data + if download_speed > 0: + speed_buffer.append(download_speed) + + # Use a more efficient approach for thread synchronization + avg_speed = sum(speed_buffer) / len(speed_buffer) if speed_buffer else 0 + formatted_upload = internet_manager.format_transfer_speed(max(0, upload_speed)) + formatted_download = internet_manager.format_transfer_speed(avg_speed) + + # Minimize lock time by preparing data outside the lock 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)) + "upload": formatted_upload, + "download": formatted_download } - logging.debug(f"Updated speeds - Upload: {self.speed['upload']}, Download: {self.speed['download']}") last_upload, last_download = current_upload, current_download + except Exception as e: - logging.error(f"Error in speed capture: {str(e)}") + if self._running: # Only log if we're still supposed to be running + logging.error(f"Error in speed capture: {str(e)}") self.speed = {"upload": "N/A", "download": "N/A"} time.sleep(interval) @@ -88,6 +104,10 @@ class M3U8_Ts_Estimator: str: The mean size of the files in a human-readable format. """ try: + # Only do calculations if we have data + if not self.ts_file_sizes: + return "0 B" + total_size = sum(self.ts_file_sizes) mean_size = total_size / len(self.ts_file_sizes) return internet_manager.format_file_size(mean_size) @@ -101,31 +121,40 @@ class M3U8_Ts_Estimator: self.add_ts_file(total_downloaded * self.total_segments) file_total_size = self.calculate_total_size() + if file_total_size == "Error": + return + number_file_total_size = file_total_size.split(' ')[0] units_file_total_size = file_total_size.split(' ')[1] + # Reduce lock contention by acquiring data with minimal synchronization + retry_count = 0 + if self.segments_instance: + with self.segments_instance.active_retries_lock: + retry_count = self.segments_instance.active_retries + if get_use_large_bar(): - speed_data = self.speed['download'].split(" ") + # Get speed data outside of any locks + speed_data = ["N/A", ""] + with self.lock: + download_speed = self.speed['download'] - if len(speed_data) >= 2: - average_internet_speed = speed_data[0] - average_internet_unit = speed_data[1] - else: - average_internet_speed = "N/A" - average_internet_unit = "" + if download_speed != "N/A": + speed_data = download_speed.split(" ") + + average_internet_speed = speed_data[0] if len(speed_data) >= 1 else "N/A" + average_internet_unit = speed_data[1] if len(speed_data) >= 2 else "" - retry_count = self.segments_instance.active_retries if self.segments_instance else 0 progress_str = ( f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size}" - f"{Colors.WHITE}, {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit}" - f"{Colors.WHITE}, {Colors.GREEN}CRR {Colors.RED}{retry_count} " + f"{Colors.WHITE}, {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit} " + #f"{Colors.WHITE}, {Colors.GREEN}CRR {Colors.RED}{retry_count} " ) else: - retry_count = self.segments_instance.active_retries if self.segments_instance else 0 progress_str = ( - f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size}" - f"{Colors.WHITE}, {Colors.GREEN}CRR {Colors.RED}{retry_count} " + f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size} " + #f"{Colors.WHITE}, {Colors.GREEN}CRR {Colors.RED}{retry_count} " ) progress_counter.set_postfix_str(progress_str) diff --git a/StreamingCommunity/Util/table.py b/StreamingCommunity/Util/table.py index 965fca7..7051e7b 100644 --- a/StreamingCommunity/Util/table.py +++ b/StreamingCommunity/Util/table.py @@ -158,7 +158,8 @@ class TVShowManager: else: key = Prompt.ask(prompt_msg) else: - choices = [str(i) for i in range(max_int_input + 1)] + ["q", "quit", "b", "back"] + # Include empty string in choices to allow pagination with Enter key + choices = [""] + [str(i) for i in range(max_int_input + 1)] + ["q", "quit", "b", "back"] prompt_msg = "[cyan]Insert media [red]index" telegram_msg = "Scegli il contenuto da scaricare:\n Serie TV - Film - Anime\noppure `back` per tornare indietro" @@ -199,7 +200,8 @@ class TVShowManager: else: key = Prompt.ask(prompt_msg) else: - choices = [str(i) for i in range(max_int_input + 1)] + ["q", "quit", "b", "back"] + # Include empty string in choices to allow pagination with Enter key + choices = [""] + [str(i) for i in range(max_int_input + 1)] + ["q", "quit", "b", "back"] prompt_msg = "[cyan]Insert media [red]index" telegram_msg = "Scegli il contenuto da scaricare:\n Serie TV - Film - Anime\noppure `back` per tornare indietro"