diff --git a/README.md b/README.md index b3749cb..aea532d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ - πŸ“₯ [Download](#m3u8_download-settings) - πŸ” [Parser](#m3u8_parser-settings) - πŸ“ [Command](#command) +- πŸ” [Global search](#global-search) - πŸ’» [Examples of terminal](#examples-of-terminal-usage) - πŸ”§ [Manual domain configuration](#update-domains) - 🐳 [Docker](#docker) @@ -637,19 +638,41 @@ Note: If `use_api` is set to `false` and no `domains.json` file is found, the sc #### πŸ’‘ Adding a New Site to the Legacy API If you want to add a new site to the legacy API, just message me on the Discord server, and I'll add it! -# COMMAND +# Global Search -- Download a specific season by entering its number. - * **Example:** `1` will download *Season 1* only. +You can now search across multiple streaming sites at once using the Global Search feature. This allows you to find content more efficiently without having to search each site individually. -- Use the wildcard `*` to download every available season. - * **Example:** `*` will download all seasons in the series. +## Using Global Search -- Specify a range of seasons using a hyphen `-`. - * **Example:** `1-2` will download *Seasons 1 and 2*. +The Global Search feature provides a unified interface to search across all supported sites: -- Enter a season number followed by `-*` to download from that season to the end. - * **Example:** `3-*` will download from *Season 3* to the final season. +## Search Options + +When using Global Search, you have three ways to select which sites to search: + +1. **Search all sites** - Searches across all available streaming sites +2. **Search by category** - Group sites by their categories (movies, series, anime, etc.) +3. **Select specific sites** - Choose individual sites to include in your search + +## Navigation and Selection + +After performing a search: + +1. Results are displayed in a consolidated table showing: + - Title + - Media type (movie, TV series, etc.) + - Source site + +2. Select an item by number to view details or download + +3. The system will automatically use the appropriate site's API to handle the download + +## Command Line Arguments + +The Global Search can be configured from the command line: + +- `--global` - Perform a global search across multiple sites. +- `-s`, `--search` - Specify the search terms. # Examples of terminal usage @@ -662,6 +685,9 @@ python test_run.py --specific_list_audio ita,eng --specific_list_subtitles eng,s # Keep console open after download python test_run.py --not_close true + +# Use global search +python test_run.py --global -s "cars" ``` # Docker diff --git a/StreamingCommunity/Api/Site/1337xx/__init__.py b/StreamingCommunity/Api/Site/1337xx/__init__.py index 5cea03f..7f6c44b 100644 --- a/StreamingCommunity/Api/Site/1337xx/__init__.py +++ b/StreamingCommunity/Api/Site/1337xx/__init__.py @@ -10,10 +10,11 @@ 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 StreamingCommunity.Api.Template.config_loader import site_constant from .site import title_search, media_search_manager, table_show_manager from .title import download_title @@ -29,30 +30,43 @@ console = Console() msg = Prompt() -def search(string_to_search: str = None, get_onylDatabase: bool = False): + +def process_search_result(select_title): """ - Main function of the application for film and series. + Handles the search result and initiates the download for either a film or series. """ + download_title(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() - # Search on database + # Perform the database search len_database = title_search(quote_plus(string_to_search)) - # Return list of elements - if get_onylDatabase: + # If only the database is needed, return the manager + if get_onlyDatabase: return media_search_manager if len_database > 0: - - # Select title from list select_title = get_select_title(table_show_manager, media_search_manager) - - # Download title download_title(select_title) else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # Retry + # 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/1337xx/site.py b/StreamingCommunity/Api/Site/1337xx/site.py index b309e23..cabc9b5 100644 --- a/StreamingCommunity/Api/Site/1337xx/site.py +++ b/StreamingCommunity/Api/Site/1337xx/site.py @@ -62,7 +62,8 @@ def title_search(word_to_search: str) -> int: 'seader': tr.find_all("td")[-5].get_text(strip=True), 'leacher': tr.find_all("td")[-4].get_text(strip=True), 'date': tr.find_all("td")[-3].get_text(strip=True).replace("'", ""), - 'size': tr.find_all("td")[-2].get_text(strip=True) + 'size': tr.find_all("td")[-2].get_text(strip=True), + 'type': 'torrent' } media_search_manager.add_media(title_info) diff --git a/StreamingCommunity/Api/Site/altadefinizione/__init__.py b/StreamingCommunity/Api/Site/altadefinizione/__init__.py index 4a6ae3c..ec79b0b 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/__init__.py +++ b/StreamingCommunity/Api/Site/altadefinizione/__init__.py @@ -2,19 +2,22 @@ import sys import subprocess - from urllib.parse import quote_plus + # 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 +from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance + # Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance from .site import title_search, table_show_manager, media_search_manager from .film import download_film from .series import download_series @@ -30,51 +33,76 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): +def get_user_input(string_to_search: str = None): """ - Main function of the application for film and series. + Asks the user to input a search term. + Handles both Telegram bot input and direct input. """ - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if string_to_search is None: - - # Chiedi la scelta all'utente con il bot Telegram + if string_to_search is None: + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() string_to_search = bot.ask( "key_search", - f"Inserisci la parola da cercare\noppure back per tornare alla scelta: ", + f"Enter the search term\nor type 'back' to return to the menu: ", None ) if string_to_search == 'back': - # Riavvia lo script - # Chiude il processo attuale e avvia una nuova istanza dello script + + # Restart the script subprocess.Popen([sys.executable] + sys.argv) sys.exit() - else: - if string_to_search is None: - string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{site_constant.SITE_NAME}").strip() + else: + string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + return string_to_search + +def process_search_result(select_title): + """ + Handles the search result and initiates the download for either a film or series. + """ + if select_title.type == 'tv': + download_series(select_title) + else: + 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 + + # 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)) - # Return list of elements - if get_onylDatabase: + # 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 from list select_title = get_select_title(table_show_manager, media_search_manager) - - if select_title.type == 'tv': - download_series(select_title) - - else: - download_film(select_title) - + process_search_result(select_title) + else: console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # Retry - 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() + search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/film.py b/StreamingCommunity/Api/Site/altadefinizione/film.py index c5c86c1..3e20974 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/film.py +++ b/StreamingCommunity/Api/Site/altadefinizione/film.py @@ -1,4 +1,4 @@ -# 3.12.23 +# 16.03.25 import os @@ -8,13 +8,14 @@ import httpx from bs4 import BeautifulSoup from rich.console import Console -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance, TelegramSession + # Internal utilities from StreamingCommunity.Util.os import os_manager from StreamingCommunity.Util.message import start_message from StreamingCommunity.Util.headers import get_headers from StreamingCommunity.Util.config_json import config_manager from StreamingCommunity.Lib.Downloader import HLS_Downloader +from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance, TelegramSession # Logic class diff --git a/StreamingCommunity/Api/Site/altadefinizione/series.py b/StreamingCommunity/Api/Site/altadefinizione/series.py index 242bff4..4d8969d 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/series.py +++ b/StreamingCommunity/Api/Site/altadefinizione/series.py @@ -1,4 +1,4 @@ -# 3.12.23 +# 16.03.25 import os from typing import Tuple diff --git a/StreamingCommunity/Api/Site/altadefinizione/site.py b/StreamingCommunity/Api/Site/altadefinizione/site.py index 8117e90..c6999c9 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/site.py +++ b/StreamingCommunity/Api/Site/altadefinizione/site.py @@ -1,4 +1,4 @@ -# 10.12.23 +# 16.03.25 # External libraries @@ -6,11 +6,12 @@ import httpx from bs4 import BeautifulSoup from rich.console import Console -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance + # 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.TelegramHelp.telegram_bot import get_bot_instance # Logic class @@ -61,10 +62,8 @@ def title_search(title_search: str) -> int: # Create soup istance soup = BeautifulSoup(response.text, "html.parser") - i = 0 - # Collect data from soup - for movie_div in soup.find_all("div", class_="movie"): + for i, movie_div in enumerate(soup.find_all("div", class_="movie")): title_tag = movie_div.find("h2", class_="movie-title") title = title_tag.find("a").get_text(strip=True) @@ -86,8 +85,6 @@ def title_search(title_search: str) -> int: choice_text = f"{i} - {title} ({tipo})" choices.append(choice_text) - i += 1 - if site_constant.TELEGRAM_BOT: if choices: bot.send_message(f"Lista dei risultati:", choices) diff --git a/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py index c41fa9c..f88ff4d 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py @@ -1,4 +1,4 @@ -# 01.03.24 +# 16.03.25 # External libraries import httpx diff --git a/StreamingCommunity/Api/Site/animeunity/__init__.py b/StreamingCommunity/Api/Site/animeunity/__init__.py index 172bf2b..08646d4 100644 --- a/StreamingCommunity/Api/Site/animeunity/__init__.py +++ b/StreamingCommunity/Api/Site/animeunity/__init__.py @@ -11,11 +11,12 @@ 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 from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance # Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant from .site import title_search, media_search_manager, table_show_manager from .film_serie import download_film, download_series @@ -31,53 +32,73 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if string_to_search is None: - - # Chiedi la scelta all'utente con il bot Telegram +def get_user_input(string_to_search: str = None): + """ + Asks the user to input a search term. + Handles both Telegram bot input and direct input. + """ + if string_to_search is None: + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() string_to_search = bot.ask( "key_search", - f"Inserisci la parola da cercare\noppure back per tornare alla scelta: ", + f"Enter the search term\nor type 'back' to return to the menu: ", None ) if string_to_search == 'back': - # Riavvia lo script - # Chiude il processo attuale e avvia una nuova istanza dello script + + # Restart the script subprocess.Popen([sys.executable] + sys.argv) sys.exit() + else: + string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() - else: - 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() + return string_to_search - # Search on database +def process_search_result(select_title): + """ + Handles the search result and initiates the download for either a film or series. + """ + download_series(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_onlyDatabase (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 + + # 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(string_to_search) - # Return list of elements - if get_onylDatabase: + ##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 from list (type: TV \ Movie \ OVA) select_title = get_select_title(table_show_manager, media_search_manager) + process_search_result(select_title) - if select_title.type == 'Movie' or select_title.type == 'OVA': - download_film(select_title) - - else: - download_series(select_title) - else: - if site_constant.TELEGRAM_BOT: - bot.send_message(f"Nessun risultato trovato riprova", None) - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # Retry + 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() search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/site.py b/StreamingCommunity/Api/Site/animeunity/site.py index 7d765c7..e72217b 100644 --- a/StreamingCommunity/Api/Site/animeunity/site.py +++ b/StreamingCommunity/Api/Site/animeunity/site.py @@ -77,10 +77,8 @@ def get_real_title(record): """ if record['title_eng'] is not None: return record['title_eng'] - elif record['title'] is not None: return record['title'] - else: return record['title_it'] diff --git a/StreamingCommunity/Api/Site/cb01new/__init__.py b/StreamingCommunity/Api/Site/cb01new/__init__.py index 6a29f3c..584a897 100644 --- a/StreamingCommunity/Api/Site/cb01new/__init__.py +++ b/StreamingCommunity/Api/Site/cb01new/__init__.py @@ -10,10 +10,11 @@ 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 StreamingCommunity.Api.Template.config_loader import site_constant from .site import title_search, media_search_manager, table_show_manager from .film import download_film @@ -29,10 +30,26 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): +def process_search_result(select_title): """ - Main function of the application for film and series. + Handles the search result and initiates the download for either a film or series. """ + # !!! ADD TYPE DONT WORK FOR SERIE + 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() @@ -40,21 +57,16 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): # Search on database len_database = title_search(quote_plus(string_to_search)) - # Return list of elements - if get_onylDatabase: + ## If only the database is needed, return the manager + if get_onlyDatabase: return media_search_manager if len_database > 0: - - # Select title from list select_title = get_select_title(table_show_manager, media_search_manager) - - # !!! ADD TYPE DONT WORK FOR SERIE - download_film(select_title) - - + process_search_result(select_title) + else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # Retry + # 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/cb01new/site.py b/StreamingCommunity/Api/Site/cb01new/site.py index 52aca12..1667dbc 100644 --- a/StreamingCommunity/Api/Site/cb01new/site.py +++ b/StreamingCommunity/Api/Site/cb01new/site.py @@ -62,7 +62,8 @@ def title_search(word_to_search: str) -> int: title_info = { 'name': title, - 'url': url + 'url': url, + 'type': 'film' } media_search_manager.add_media(title_info) diff --git a/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py b/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py index cfdadb1..e24259d 100644 --- a/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +++ b/StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py @@ -12,6 +12,7 @@ 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 @@ -30,10 +31,28 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): +def process_search_result(select_title): """ - Main function of the application for film and series. + Handles the search result and initiates the download for either a film or series. """ + if "Serie TV" in str(select_title.type): + download_thread(select_title) + else: + logging.error(f"Not supported: {select_title.type}") + +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() @@ -41,24 +60,16 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): # Search on database len_database = title_search(quote_plus(string_to_search)) - # Return list of elements - if get_onylDatabase: + # If only the database is needed, return the manager + if get_onlyDatabase: return media_search_manager if len_database > 0: - - # Select title from list select_title = get_select_title(table_show_manager, media_search_manager) - - # Download only film - if "Serie TV" in str(select_title.type): - download_thread(select_title) - - else: - logging.error(f"Not supported: {select_title.type}") + process_search_result(select_title) else: + + # If no results are found, ask again console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - - # Retry search() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/guardaserie/__init__.py b/StreamingCommunity/Api/Site/guardaserie/__init__.py index 40b9d8e..3af176b 100644 --- a/StreamingCommunity/Api/Site/guardaserie/__init__.py +++ b/StreamingCommunity/Api/Site/guardaserie/__init__.py @@ -11,6 +11,7 @@ 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 @@ -29,10 +30,25 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): +def process_search_result(select_title): """ - Main function of the application for film and series. + Handles the search result and initiates the download for either a film or series. """ + download_series(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() @@ -40,20 +56,16 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): # Search on database len_database = title_search(quote_plus(string_to_search)) - # Return list of elements - if get_onylDatabase: + # If only the database is needed, return the manager + if get_onlyDatabase: return media_search_manager if len_database > 0: - - # Select title from list select_title = get_select_title(table_show_manager, media_search_manager) - - # Download only film - download_series(select_title) + process_search_result(select_title) else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # Retry + # 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/guardaserie/site.py b/StreamingCommunity/Api/Site/guardaserie/site.py index 6fee90f..22f6635 100644 --- a/StreamingCommunity/Api/Site/guardaserie/site.py +++ b/StreamingCommunity/Api/Site/guardaserie/site.py @@ -63,7 +63,8 @@ def title_search(word_to_search: str) -> int: serie_info = { 'name': title, - 'url': link + 'url': link, + 'type': 'tv' } media_search_manager.add_media(serie_info) diff --git a/StreamingCommunity/Api/Site/mostraguarda/__init__.py b/StreamingCommunity/Api/Site/mostraguarda/__init__.py index a229465..d08e8d1 100644 --- a/StreamingCommunity/Api/Site/mostraguarda/__init__.py +++ b/StreamingCommunity/Api/Site/mostraguarda/__init__.py @@ -5,12 +5,16 @@ from urllib.parse import quote_plus # External library from rich.console import Console -from rich.prompt import Prompt, Confirm +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 StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Lib.TMBD import tmdb, Json_film from .film import download_film @@ -25,16 +29,32 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): +def process_search_result(select_title): """ - Main function of the application for film and series. + 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_onylDatabase: + if get_onlyDatabase: return 0 # Search on database @@ -47,7 +67,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): download_film(movie_details) else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") - # Retry + # 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/streamingcommunity/__init__.py b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py index 6fbc067..f839023 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py @@ -12,11 +12,12 @@ 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 from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance # Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant from .site import title_search, table_show_manager, media_search_manager from .film import download_film from .series import download_series @@ -33,54 +34,76 @@ msg = Prompt() console = Console() -def search(string_to_search: str = None, get_onylDatabase: bool = False): +def get_user_input(string_to_search: str = None): """ - Main function of the application for film and series. + Asks the user to input a search term. + Handles both Telegram bot input and direct input. """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if string_to_search is None: - - # Chiedi la scelta all'utente con il bot Telegram + if string_to_search is None: + if site_constant.TELEGRAM_BOT: + bot = get_bot_instance() string_to_search = bot.ask( "key_search", - f"Inserisci la parola da cercare\noppure back per tornare alla scelta: ", + f"Enter the search term\nor type 'back' to return to the menu: ", None ) if string_to_search == 'back': - # Riavvia lo script - # Chiude il processo attuale e avvia una nuova istanza dello script + + # Restart the script subprocess.Popen([sys.executable] + sys.argv) sys.exit() - - else: - if string_to_search is None: - string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{site_constant.SITE_NAME}").strip() + else: + string_to_search = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() + return string_to_search + +def process_search_result(select_title): + """ + Handles the search result and initiates the download for either a film or series. + """ + if select_title.type == 'tv': + download_series(select_title) + else: + 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 + + # 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)) - # Return list of elements - if get_onylDatabase: + # 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 from list select_title = get_select_title(table_show_manager, media_search_manager) - - if select_title.type == 'tv': - download_series(select_title) - - else: - download_film(select_title) + process_search_result(select_title) 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"Nessun risultato trovato riprova", None) + bot.send_message(f"No results found, please try again", None) - # Retry + # If no results are found, ask again + string_to_search = get_user_input() search() \ No newline at end of file diff --git a/StreamingCommunity/Upload/version.py b/StreamingCommunity/Upload/version.py index ab76dbc..795936c 100644 --- a/StreamingCommunity/Upload/version.py +++ b/StreamingCommunity/Upload/version.py @@ -1,5 +1,5 @@ __title__ = 'StreamingCommunity' -__version__ = '2.9.4' +__version__ = '2.9.5' __author__ = 'Arrowar' __description__ = 'A command-line program to download film' __copyright__ = 'Copyright 2024' diff --git a/StreamingCommunity/global_search.py b/StreamingCommunity/global_search.py new file mode 100644 index 0000000..8291baa --- /dev/null +++ b/StreamingCommunity/global_search.py @@ -0,0 +1,315 @@ +# 17.03.25 + +import os +import sys +import time +import glob +import logging +import importlib + + +# External library +from rich.console import Console +from rich.prompt import Prompt +from rich.table import Table +from rich.progress import Progress + + +# Internal utilities +from StreamingCommunity.Util.message import start_message + + +# Variable +console = Console() +msg = Prompt() + + +# !!! DA METTERE IN COMUNE CON QUELLA DI RUN +def load_search_functions(): + modules = [] + loaded_functions = {} + excluded_sites = set() + + # Find api home directory + if getattr(sys, 'frozen', False): # ModalitΓ  PyInstaller + base_path = os.path.join(sys._MEIPASS, "StreamingCommunity") + else: + base_path = os.path.dirname(__file__) + + api_dir = os.path.join(base_path, 'Api', 'Site') + init_files = glob.glob(os.path.join(api_dir, '*', '__init__.py')) + + # Retrieve modules and their indices + for init_file in init_files: + + # Get folder name as module name + module_name = os.path.basename(os.path.dirname(init_file)) + + # Se il modulo Γ¨ nella lista da escludere, saltalo + if module_name in excluded_sites: + continue + + logging.info(f"Load module name: {module_name}") + + try: + # Dynamically import the module + mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}') + + # Get 'indice' from the module + indice = getattr(mod, 'indice', 0) + is_deprecate = bool(getattr(mod, '_deprecate', True)) + use_for = getattr(mod, '_useFor', 'other') + + if not is_deprecate: + modules.append((module_name, indice, use_for)) + + except Exception as e: + console.print(f"[red]Failed to import module {module_name}: {str(e)}") + + # Sort modules by 'indice' + modules.sort(key=lambda x: x[1]) + + # Load search functions in the sorted order + for module_name, _, use_for in modules: + + # Construct a unique alias for the module + module_alias = f'{module_name}_search' + + try: + + # Dynamically import the module + mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}') + + # Get the search function from the module (assuming the function is named 'search' and defined in __init__.py) + search_function = getattr(mod, 'search') + + # Add the function to the loaded functions dictionary + loaded_functions[module_alias] = (search_function, use_for) + + except Exception as e: + console.print(f"[red]Failed to load search function from module {module_name}: {str(e)}") + + return loaded_functions + +def global_search(search_terms: str = None, selected_sites: list = None): + """ + Perform a search across multiple sites based on selection. + + Parameters: + search_terms (str, optional): The terms to search for. If None, will prompt the user. + selected_sites (list, optional): List of site aliases to search. If None, will search all sites. + + Returns: + dict: Consolidated search results from all searched sites. + """ + search_functions = load_search_functions() + all_results = {} + + if search_terms is None: + search_terms = msg.ask("\n[purple]Enter search terms for global search: ").strip() + + # Organize sites by category for better display + sites_by_category = {} + for alias, (func, category) in search_functions.items(): + if category not in sites_by_category: + sites_by_category[category] = [] + sites_by_category[category].append((alias, func)) + + # If no sites are specifically selected, prompt the user + if selected_sites is None: + console.print("\n[bold green]Select sites to search:[/bold green]") + console.print("[bold cyan]1.[/bold cyan] Search all sites") + console.print("[bold cyan]2.[/bold cyan] Search by category") + console.print("[bold cyan]3.[/bold cyan] Select specific sites") + + choice = msg.ask("[green]Enter your choice (1-3)", choices=["1", "2", "3"], default="1") + + if choice == "1": + # Search all sites + selected_sites = list(search_functions.keys()) + + elif choice == "2": + # Search by category + console.print("\n[bold green]Select categories to search:[/bold green]") + for i, category in enumerate(sites_by_category.keys(), 1): + console.print(f"[bold cyan]{i}.[/bold cyan] {category.capitalize()}") + + category_choices = msg.ask("[green]Enter category numbers separated by commas", default="1") + selected_categories = [list(sites_by_category.keys())[int(c.strip())-1] for c in category_choices.split(",")] + + selected_sites = [] + for category in selected_categories: + for alias, _ in sites_by_category.get(category, []): + selected_sites.append(alias) + + else: + # Select specific sites + console.print("\n[bold green]Select specific sites to search:[/bold green]") + + for i, (alias, _) in enumerate(search_functions.items(), 1): + site_name = alias.split("_")[0].capitalize() + console.print(f"[bold cyan]{i}.[/bold cyan] {site_name}") + + site_choices = msg.ask("[green]Enter site numbers separated by commas", default="1") + selected_indices = [int(c.strip())-1 for c in site_choices.split(",")] + selected_sites = [list(search_functions.keys())[i] for i in selected_indices if i < len(search_functions)] + + # Display progress information + console.print(f"\n[bold green]Searching for:[/bold green] [yellow]{search_terms}[/yellow]") + console.print(f"[bold green]Searching across:[/bold green] {len(selected_sites)} sites") + + with Progress() as progress: + search_task = progress.add_task("[cyan]Searching...", total=len(selected_sites)) + + # Search each selected site + for alias in selected_sites: + site_name = alias.split("_")[0].capitalize() + progress.update(search_task, description=f"[cyan]Searching {site_name}...") + + func, _ = search_functions[alias] + try: + # Call the search function with get_onlyDatabase=True to get database object + database = func(search_terms, get_onlyDatabase=True) + + # Check if database has media_list attribute and it's not empty + if database and hasattr(database, 'media_list') and len(database.media_list) > 0: + # Store media_list items with additional source information + all_results[alias] = [] + for element in database.media_list: + # Convert element to dictionary if it's an object + if hasattr(element, '__dict__'): + item_dict = element.__dict__.copy() + else: + item_dict = {} # Fallback for non-object items + + # Add source information + item_dict['source'] = site_name + item_dict['source_alias'] = alias + all_results[alias].append(item_dict) + + console.print(f"[green]Found {len(database.media_list)} results from {site_name}") + + except Exception as e: + console.print(f"[bold red]Error searching {site_name}:[/bold red] {str(e)}") + + progress.update(search_task, advance=1) + + # Display the consolidated results + if all_results: + all_media_items = [] + for alias, results in all_results.items(): + for item in results: + all_media_items.append(item) + + # Display consolidated results + display_consolidated_results(all_media_items, search_terms) + + # Allow user to select an item + selected_item = select_from_consolidated_results(all_media_items) + if selected_item: + # Process the selected item - download or further actions + process_selected_item(selected_item, search_functions) + + else: + console.print(f"\n[bold red]No results found for:[/bold red] [yellow]{search_terms}[/yellow]") + + # Optionally offer to search again or return to main menu + if msg.ask("[green]Search again? (y/n)", choices=["y", "n"], default="y") == "y": + global_search() + + return all_results + +def display_consolidated_results(all_media_items, search_terms): + """ + Display consolidated search results from multiple sites. + + Parameters: + all_media_items (list): List of media items from all searched sites. + search_terms (str): The search terms used. + """ + time.sleep(1) + start_message() + + console.print(f"\n[bold green]Search results for:[/bold green] [yellow]{search_terms}[/yellow] \n") + + table = Table(show_header=True, header_style="bold cyan") + table.add_column("#", style="dim", width=4) + table.add_column("Title", min_width=20) + table.add_column("Type", width=15) + table.add_column("Source", width=25) + + for i, item in enumerate(all_media_items, 1): + + # Extract values from item dict, with fallbacks if keys don't exist + title = item.get('title', item.get('name', 'Unknown')) + media_type = item.get('type', item.get('media_type', 'Unknown')) + source = item.get('source', 'Unknown') + + table.add_row( + str(i), + str(title), + str(media_type), + str(source), + ) + + console.print(table) + +def select_from_consolidated_results(all_media_items): + """ + Allow user to select an item from consolidated results. + + Parameters: + all_media_items (list): List of media items from all searched sites. + + Returns: + dict: The selected media item or None if no selection was made. + """ + if not all_media_items: + return None + + max_index = len(all_media_items) + choice = msg.ask( + f"[green]Select item # (1-{max_index}) or 0 to cancel", + choices=[str(i) for i in range(max_index + 1)], + default="1", + show_choices=False + ) + + if choice == "0": + return None + + return all_media_items[int(choice) - 1] + +def process_selected_item(selected_item, search_functions): + """ + Process the selected item - download the media using the appropriate site API. + + Parameters: + selected_item (dict): The selected media item. + search_functions (dict): Dictionary of search functions by alias. + """ + source_alias = selected_item.get('source_alias') + if not source_alias or source_alias not in search_functions: + console.print("[bold red]Error: Cannot process this item - source information missing.[/bold red]") + return + + # Get the appropriate search function for this source + func, _ = search_functions[source_alias] + + console.print(f"\n[bold green]Processing selection from:[/bold green] {selected_item.get('source')}") + + # Extract necessary information to pass to the site's search function + item_id = selected_item.get('id', selected_item.get('media_id')) + item_type = selected_item.get('type', selected_item.get('media_type', 'unknown')) + item_title = selected_item.get('title', selected_item.get('name', 'Unknown')) + + if item_id: + console.print(f"[bold green]Selected item:[/bold green] {item_title} (ID: {item_id}, Type: {item_type})") + + # Call the site's search function with direct_item parameter to process download + try: + func(direct_item=selected_item) + except Exception as e: + console.print(f"[bold red]Error processing download:[/bold red] {str(e)}") + else: + console.print("[bold red]Error: Item ID not found.[/bold red]") \ No newline at end of file diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index b5fd26c..b2dd487 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -18,6 +18,7 @@ from rich.prompt import Prompt # Internal utilities +from .global_search import global_search from StreamingCommunity.Util.message import start_message from StreamingCommunity.Util.config_json import config_manager from StreamingCommunity.Util.os import os_summary @@ -54,6 +55,7 @@ def run_function(func: Callable[..., None], close_console: bool = False, search_ func(search_terms) +# !!! DA METTERE IN COMUNE CON QUELLA DI GLOBAL def load_search_functions(): modules = [] loaded_functions = {} @@ -237,6 +239,11 @@ def main(script_id = 0): '--specific_list_subtitles', type=str, help='Comma-separated list of specific subtitle languages to download (e.g., eng,spa).' ) + # Add global search option + parser.add_argument( + '--global', action='store_true', help='Perform a global search across multiple sites.' + ) + # Add arguments for search functions color_map = { "anime": "red", @@ -253,6 +260,7 @@ def main(script_id = 0): parser.add_argument(f'-{short_option}', f'--{long_option}', action='store_true', help=f'Search for {alias.split("_")[0]} on streaming platforms.') parser.add_argument('-s', '--search', default=None, help='Search terms') + # Parse command-line arguments args = parser.parse_args() @@ -280,6 +288,11 @@ def main(script_id = 0): config_manager.write_config() + # Check if global search is requested + if getattr(args, 'global'): + global_search(search_terms) + return + # Map command-line arguments to functions arg_to_function = {alias: func for alias, (func, _) in search_functions.items()} @@ -295,13 +308,18 @@ def main(script_id = 0): # Create dynamic prompt message and choices choice_labels = {str(i): (alias.split("_")[0].capitalize(), use_for) for i, (alias, (_, use_for)) in enumerate(search_functions.items())} + # Add global search option to the menu + #global_search_key = str(len(choice_labels)) + #choice_labels[global_search_key] = ("Global Search", "all") + #input_to_function[global_search_key] = global_search + # Display the category legend in a single line legend_text = " | ".join([f"[{color}]{category.capitalize()}[/{color}]" for category, color in color_map.items()]) console.print(f"\n[bold green]Category Legend:[/bold green] {legend_text}") # Construct the prompt message with color-coded site names prompt_message = "[green]Insert category [white](" + ", ".join( - [f"{key}: [{color_map[label[1]]}]{label[0]}[/{color_map[label[1]]}]" for key, label in choice_labels.items()] + [f"{key}: [{color_map.get(label[1], 'white')}]{label[0]}[/{color_map.get(label[1], 'white')}]" for key, label in choice_labels.items()] ) + "[white])" if TELEGRAM_BOT: @@ -330,10 +348,16 @@ def main(script_id = 0): # Run the corresponding function based on user input if category in input_to_function: - run_function(input_to_function[category], search_terms = args.search) + """if category == global_search_key: + # Run global search + run_function(input_to_function[category], search_terms=search_terms) + + else:""" + + # Run normal site-specific search + run_function(input_to_function[category], search_terms=search_terms) else: - if TELEGRAM_BOT: bot.send_message(f"Categoria non valida", None) diff --git a/Test/big_search.py b/Test/big_search.py deleted file mode 100644 index 86010f3..0000000 --- a/Test/big_search.py +++ /dev/null @@ -1,116 +0,0 @@ -# 12.11.24 - -# Fix import -import os -import sys -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Other -import glob -import logging -import importlib -from rich.console import Console - - -# Other import -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager - - -# Variable -console = Console() - - -def load_search_functions(): - modules = [] - loaded_functions = {} - - # Traverse the Api directory - api_dir = os.path.join(os.path.dirname(__file__), '..', 'StreamingCommunity', 'Api', 'Site') - init_files = glob.glob(os.path.join(api_dir, '*', '__init__.py')) - - logging.info(f"Base folder path: {api_dir}") - logging.info(f"Api module path: {init_files}") - - # Retrieve modules and their indices - for init_file in init_files: - - # Get folder name as module name - module_name = os.path.basename(os.path.dirname(init_file)) - logging.info(f"Load module name: {module_name}") - - try: - # Dynamically import the module - mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}') - - # Get 'indice' from the module - indice = getattr(mod, 'indice', 0) - is_deprecate = bool(getattr(mod, '_deprecate', True)) - use_for = getattr(mod, '_useFor', 'other') - - if not is_deprecate: - modules.append((module_name, indice, use_for)) - - except Exception as e: - console.print(f"[red]Failed to import module {module_name}: {str(e)}") - - # Sort modules by 'indice' - modules.sort(key=lambda x: x[1]) - - # Load search functions in the sorted order - for module_name, _, use_for in modules: - - # Construct a unique alias for the module - module_alias = f'{module_name}_search' - logging.info(f"Module alias: {module_alias}") - - try: - # Dynamically import the module - mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}') - - # Get the search function from the module (assuming the function is named 'search' and defined in __init__.py) - search_function = getattr(mod, 'search') - - # Add the function to the loaded functions dictionary - loaded_functions[module_alias] = (search_function, use_for) - - except Exception as e: - console.print(f"[red]Failed to load search function from module {module_name}: {str(e)}") - - return loaded_functions - - -def search_all_sites(loaded_functions, search_string, max_sites=10): - total_len_database = 0 - site_count = 0 - - for module_alias, (search_function, use_for) in loaded_functions.items(): - if max_sites is not None and site_count >= max_sites: - break - - console.print(f"\n[blue]Searching in module: {module_alias} [white](Use for: {use_for})") - - try: - database: MediaManager = search_function(search_string, get_onylDatabase=True) - len_database = len(database.media_list) - - for element in database.media_list: - print(element.__dict__) - - console.print(f"[green]Database length for {module_alias}: {len_database}") - total_len_database += len_database - site_count += 1 - - except Exception as e: - console.print(f"[red]Error while executing search function for {module_alias}: {str(e)}") - - return total_len_database - - -# Main -search_string = "cars" -loaded_functions = load_search_functions() - -total_len = search_all_sites(loaded_functions, search_string) -console.print(f"\n[cyan]Total number of results from all sites: {total_len}") diff --git a/setup.py b/setup.py index f6257fb..c796d50 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(os.path.dirname(__file__), "requirements.txt"), "r", enco setup( name="StreamingCommunity", - version="2.9.4", + version="2.9.5", long_description=read_readme(), long_description_content_type="text/markdown", author="Lovi-0",